| /* |
| * This file is part of the flashrom project. |
| * |
| * Copyright 2015 Google Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; version 2 of the License. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #include <ctype.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <inttypes.h> |
| #include <libgen.h> |
| #include <limits.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <mtd/mtd-user.h> |
| #include <string.h> |
| #include <sys/ioctl.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include "file.h" |
| #include "flash.h" |
| #include "programmer.h" |
| #include "writeprotect.h" |
| |
| #define LINUX_DEV_ROOT "/dev" |
| #define LINUX_MTD_SYSFS_ROOT "/sys/class/mtd" |
| |
| /* enough space for LINUX_MTD_SYSFS_ROOT + directory name + filename */ |
| static char sysfs_path[PATH_MAX]; |
| |
| static int dev_fd = -1; |
| |
| static int mtd_device_is_writeable; |
| |
| static int mtd_no_erase; |
| |
| /* Size info is presented in bytes in sysfs. */ |
| static unsigned long int mtd_total_size; |
| static unsigned long int mtd_numeraseregions; |
| static unsigned long int mtd_erasesize; /* only valid if numeraseregions is 0 */ |
| |
| static struct wp wp_mtd; /* forward declaration */ |
| |
| static int stat_mtd_files(char *dev_path) |
| { |
| struct stat s; |
| |
| errno = 0; |
| if (stat(dev_path, &s) < 0) { |
| msg_pdbg("Cannot stat \"%s\": %s\n", dev_path, strerror(errno)); |
| return 1; |
| } |
| |
| if (lstat(sysfs_path, &s) < 0) { |
| msg_pdbg("Cannot stat \"%s\" : %s\n", |
| sysfs_path, strerror(errno)); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| /* read a string from a sysfs file and sanitize it */ |
| static int read_sysfs_string(const char *filename, char *buf, int len) |
| { |
| int fd, bytes_read, i; |
| char path[strlen(LINUX_MTD_SYSFS_ROOT) + 32]; |
| |
| snprintf(path, sizeof(path), "%s/%s", sysfs_path, filename); |
| |
| if ((fd = open(path, O_RDONLY)) < 0) { |
| msg_perr("Cannot open %s\n", path); |
| return 1; |
| } |
| |
| if ((bytes_read = read(fd, buf, len - 1)) < 0) { |
| msg_perr("Cannot read %s\n", path); |
| close(fd); |
| return 1; |
| } |
| |
| buf[bytes_read] = '\0'; |
| |
| /* |
| * Files from sysfs sometimes contain a newline or other garbage that |
| * can confuse functions like strtoul() and ruin formatting in print |
| * statements. Replace the first non-printable character (space is |
| * considered printable) with a proper string terminator. |
| */ |
| for (i = 0; i < len; i++) { |
| if (!isprint(buf[i])) { |
| buf[i] = '\0'; |
| break; |
| } |
| } |
| |
| close(fd); |
| return 0; |
| } |
| |
| static int read_sysfs_int(const char *filename, unsigned long int *val) |
| { |
| char buf[32]; |
| char *endptr; |
| |
| if (read_sysfs_string(filename, buf, sizeof(buf))) |
| return 1; |
| |
| errno = 0; |
| *val = strtoul(buf, &endptr, 0); |
| if (endptr != &buf[strlen(buf)]) { |
| msg_perr("Error reading %s\n", filename); |
| return 1; |
| } |
| |
| if (errno) { |
| msg_perr("Error reading %s: %s\n", filename, strerror(errno)); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| /* returns 0 to indicate success, non-zero to indicate error */ |
| static int get_mtd_info(void) |
| { |
| unsigned long int tmp; |
| char mtd_device_name[32]; |
| |
| /* Flags */ |
| if (read_sysfs_int("flags", &tmp)) |
| return 1; |
| if (tmp & MTD_WRITEABLE) { |
| /* cache for later use by write function */ |
| mtd_device_is_writeable = 1; |
| } |
| if (tmp & MTD_NO_ERASE) { |
| mtd_no_erase = 1; |
| } |
| |
| /* Device name */ |
| if (read_sysfs_string("name", mtd_device_name, sizeof(mtd_device_name))) |
| return 1; |
| |
| /* Total size */ |
| if (read_sysfs_int("size", &mtd_total_size)) |
| return 1; |
| if (__builtin_popcount(mtd_total_size) != 1) { |
| msg_perr("MTD size is not a power of 2\n"); |
| return 1; |
| } |
| |
| /* Erase size */ |
| if (read_sysfs_int("erasesize", &mtd_erasesize)) |
| return 1; |
| if (__builtin_popcount(mtd_erasesize) != 1) { |
| msg_perr("MTD erase size is not a power of 2\n"); |
| return 1; |
| } |
| |
| /* Erase regions */ |
| if (read_sysfs_int("numeraseregions", &mtd_numeraseregions)) |
| return 1; |
| if (mtd_numeraseregions != 0) { |
| msg_perr("Non-uniform eraseblock size is unsupported.\n"); |
| return 1; |
| } |
| |
| msg_pspew("%s: device_name: \"%s\", is_writeable: %d, " |
| "numeraseregions: %lu, total_size: %lu, erasesize: %lu\n", |
| __func__, mtd_device_name, mtd_device_is_writeable, |
| mtd_numeraseregions, mtd_total_size, mtd_erasesize); |
| |
| return 0; |
| } |
| |
| static int linux_mtd_probe(struct flashctx *flash) |
| { |
| flash->chip->wp = &wp_mtd; |
| if (mtd_no_erase) |
| flash->chip->feature_bits |= FEATURE_NO_ERASE; |
| flash->chip->tested = TEST_OK_PREW; |
| flash->chip->total_size = mtd_total_size / 1024; /* bytes -> kB */ |
| flash->chip->block_erasers[0].eraseblocks[0].size = mtd_erasesize; |
| flash->chip->block_erasers[0].eraseblocks[0].count = |
| mtd_total_size / mtd_erasesize; |
| return 1; |
| } |
| |
| static int linux_mtd_read(struct flashctx *flash, uint8_t *buf, |
| unsigned int start, unsigned int len) |
| { |
| unsigned int eb_size = flash->chip->block_erasers[0].eraseblocks[0].size; |
| unsigned int i; |
| |
| if (lseek(dev_fd, start, SEEK_SET) != start) { |
| msg_perr("Cannot seek to 0x%06x: %s\n", start, strerror(errno)); |
| return 1; |
| } |
| |
| for (i = 0; i < len; ) { |
| /* |
| * Try to align reads to eraseblock size. |
| * FIXME: Shouldn't actually be necessary, but not all MTD |
| * drivers handle arbitrary large reads well. See, for example, |
| * https://b/35573113 |
| */ |
| unsigned int step = eb_size - ((start + i) % eb_size); |
| step = min(step, len - i); |
| |
| if (read(dev_fd, buf + i, step) != step) { |
| msg_perr("Cannot read 0x%06x bytes at 0x%06x: %s\n", |
| step, start + i, strerror(errno)); |
| return 1; |
| } |
| |
| i += step; |
| } |
| |
| return 0; |
| } |
| |
| /* this version assumes we must divide the write request into chunks ourselves */ |
| static int linux_mtd_write(struct flashctx *flash, const uint8_t *buf, |
| unsigned int start, unsigned int len) |
| { |
| unsigned int chunksize = flash->chip->block_erasers[0].eraseblocks[0].size; |
| unsigned int i; |
| |
| if (!mtd_device_is_writeable) |
| return 1; |
| |
| if (lseek(dev_fd, start, SEEK_SET) != start) { |
| msg_perr("Cannot seek to 0x%06x: %s\n", start, strerror(errno)); |
| return 1; |
| } |
| |
| /* |
| * Try to align writes to eraseblock size. We want these large enough |
| * to give MTD room for optimizing performance. |
| * FIXME: Shouldn't need to divide this up at all, but not all MTD |
| * drivers handle arbitrary large writes well. See, for example, |
| * https://b/35573113 |
| */ |
| for (i = 0; i < len; ) { |
| unsigned int step = chunksize - ((start + i) % chunksize); |
| step = min(step, len - i); |
| |
| if (write(dev_fd, buf + i, step) != step) { |
| msg_perr("Cannot write 0x%06x bytes at 0x%06x: %s\n", |
| step, start + i, strerror(errno)); |
| return 1; |
| } |
| |
| i += step; |
| } |
| |
| return 0; |
| } |
| |
| static int linux_mtd_erase(struct flashctx *flash, |
| unsigned int start, unsigned int len) |
| { |
| uint32_t u; |
| |
| if (mtd_no_erase) { |
| msg_perr("%s: device does not support erasing. Please file a " |
| "bug report at flashrom@flashrom.org\n", __func__); |
| return 1; |
| } |
| |
| if (mtd_numeraseregions != 0) { |
| /* TODO: Support non-uniform eraseblock size using |
| use MEMGETREGIONCOUNT/MEMGETREGIONINFO ioctls */ |
| } |
| |
| for (u = 0; u < len; u += mtd_erasesize) { |
| struct erase_info_user erase_info = { |
| .start = start + u, |
| .length = mtd_erasesize, |
| }; |
| |
| if (ioctl(dev_fd, MEMERASE, &erase_info) == -1) { |
| msg_perr("%s: ioctl: %s\n", __func__, strerror(errno)); |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static struct opaque_programmer programmer_linux_mtd = { |
| /* max_data_{read,write} don't have any effect for this programmer */ |
| .max_data_read = MAX_DATA_UNSPECIFIED, |
| .max_data_write = MAX_DATA_UNSPECIFIED, |
| .probe = linux_mtd_probe, |
| .read = linux_mtd_read, |
| .write = linux_mtd_write, |
| .erase = linux_mtd_erase, |
| }; |
| |
| /* Returns 0 if setup is successful, non-zero to indicate error */ |
| static int linux_mtd_setup(int dev_num) |
| { |
| char dev_path[16]; /* "/dev/mtdN" */ |
| int ret = 1; |
| |
| if (dev_num < 0) { |
| char *tmp, *p; |
| |
| tmp = (char *)scanft(LINUX_MTD_SYSFS_ROOT, "type", "nor", 1); |
| if (!tmp) { |
| msg_pdbg("%s: NOR type device not found.\n", __func__); |
| goto linux_mtd_setup_exit; |
| } |
| |
| /* "tmp" should be something like "/sys/blah/mtdN/type" */ |
| p = tmp + strlen(LINUX_MTD_SYSFS_ROOT); |
| while (p[0] == '/') |
| p++; |
| |
| if (sscanf(p, "mtd%d", &dev_num) != 1) { |
| msg_perr("Can't obtain device number from \"%s\"\n", p); |
| free(tmp); |
| goto linux_mtd_setup_exit; |
| } |
| free(tmp); |
| } |
| |
| snprintf(sysfs_path, sizeof(sysfs_path), "%s/mtd%d", |
| LINUX_MTD_SYSFS_ROOT, dev_num); |
| snprintf(dev_path, sizeof(dev_path), "%s/mtd%d", |
| LINUX_DEV_ROOT, dev_num); |
| msg_pdbg("%s: sysfs_path: \"%s\", dev_path: \"%s\"\n", |
| __func__, sysfs_path, dev_path); |
| |
| if (stat_mtd_files(dev_path)) |
| goto linux_mtd_setup_exit; |
| |
| if (get_mtd_info()) |
| goto linux_mtd_setup_exit; |
| |
| if ((dev_fd = open(dev_path, O_RDWR)) == -1) { |
| msg_pdbg("%s: failed to open %s: %s\n", __func__, |
| dev_path, strerror(errno)); |
| goto linux_mtd_setup_exit; |
| } |
| |
| ret = 0; |
| linux_mtd_setup_exit: |
| return ret; |
| } |
| |
| static int linux_mtd_shutdown(void *data) |
| { |
| if (dev_fd != -1) { |
| close(dev_fd); |
| dev_fd = -1; |
| } |
| |
| return 0; |
| } |
| |
| int linux_mtd_init(void) |
| { |
| char *param; |
| int dev_num = -1; /* linux_mtd_setup will search if dev_num < 0 */ |
| int ret = 1; |
| |
| if (alias && alias->type != ALIAS_HOST) |
| return 1; |
| |
| param = extract_programmer_param("dev"); |
| if (param) { |
| char *endptr; |
| |
| dev_num = strtol(param, &endptr, 0); |
| if ((param == endptr) || (dev_num < 0)) { |
| msg_perr("Invalid device number %s. Use flashrom -p " |
| "linux_mtd:dev=N where N is a valid MTD " |
| "device number\n", param); |
| goto linux_mtd_init_exit; |
| } |
| } |
| |
| if (linux_mtd_setup(dev_num)) |
| goto linux_mtd_init_exit; |
| |
| if (register_shutdown(linux_mtd_shutdown, NULL)) |
| goto linux_mtd_init_exit; |
| |
| register_opaque_programmer(&programmer_linux_mtd); |
| |
| ret = 0; |
| linux_mtd_init_exit: |
| msg_pdbg("%s: %s\n", __func__, ret == 0 ? "success." : "failed."); |
| return ret; |
| } |
| |
| /* |
| * Write-protect functions. |
| */ |
| static int mtd_wp_list_ranges(const struct flashctx *flash) |
| { |
| /* TODO: implement this */ |
| msg_perr("--wp-list is not currently implemented for MTD.\n"); |
| return 1; |
| } |
| |
| /* |
| * We only have MEMLOCK to enable write-protection for a particular block, |
| * so we need to do force the user to use --wp-range and --wp-enable |
| * command-line arguments simultaneously. (Fortunately, CrOS factory |
| * installer does this already). |
| * |
| * The --wp-range argument is processed first and will set these variables |
| * which --wp-enable will use afterward. |
| */ |
| static unsigned int wp_range_start; |
| static unsigned int wp_range_len; |
| static int wp_set_range_called = 0; |
| |
| static int mtd_wp_set_range(const struct flashctx *flash, |
| unsigned int start, unsigned int len) |
| { |
| wp_range_start = start; |
| wp_range_len = len; |
| |
| wp_set_range_called = 1; |
| return 0; |
| } |
| |
| static int mtd_wp_enable_writeprotect(const struct flashctx *flash, enum wp_mode mode) |
| { |
| struct erase_info_user entire_chip = { |
| .start = 0, |
| .length = mtd_total_size, |
| }; |
| struct erase_info_user desired_range = { |
| .start = wp_range_start, |
| .length = wp_range_len, |
| }; |
| |
| if (!wp_set_range_called) { |
| msg_perr("For MTD, --wp-range and --wp-enable must be " |
| "used simultaneously.\n"); |
| return 1; |
| } |
| |
| /* |
| * MTD handles write-protection additively, so whatever new range is |
| * specified is added to the range which is currently protected. To be |
| * consistent with flashrom behavior with other programmer interfaces, |
| * we need to disable the current write protection and then enable |
| * it for the desired range. |
| */ |
| if (ioctl(dev_fd, MEMUNLOCK, &entire_chip) == -1) { |
| msg_perr("%s: Failed to disable write-protection, ioctl: %s\n", |
| __func__, strerror(errno)); |
| msg_perr("Did you disable WP#?\n"); |
| return 1; |
| } |
| |
| if (ioctl(dev_fd, MEMLOCK, &desired_range) == -1) { |
| msg_perr("%s: Failed to enable write-protection, ioctl: %s\n", |
| __func__, strerror(errno)); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static int mtd_wp_disable_writeprotect(const struct flashctx *flash) |
| { |
| struct erase_info_user erase_info; |
| |
| if (wp_set_range_called) { |
| erase_info.start = wp_range_start; |
| erase_info.length = wp_range_len; |
| } else { |
| erase_info.start = 0; |
| erase_info.length = mtd_total_size; |
| } |
| |
| if (ioctl(dev_fd, MEMUNLOCK, &erase_info) == -1) { |
| msg_perr("%s: ioctl: %s\n", __func__, strerror(errno)); |
| msg_perr("Did you disable WP#?\n"); |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| static int mtd_wp_status(const struct flashctx *flash) |
| { |
| uint32_t start = 0, len = 0; |
| int start_found = 0; |
| unsigned int u; |
| |
| /* For now, assume only one contiguous region can be locked (NOR) */ |
| /* FIXME: use flash struct members instead of raw MTD values here */ |
| for (u = 0; u < mtd_total_size; u += mtd_erasesize) { |
| int rc; |
| struct erase_info_user erase_info = { |
| .start = u, |
| .length = mtd_erasesize, |
| }; |
| |
| rc = ioctl(dev_fd, MEMISLOCKED, &erase_info); |
| if (rc < 0) { |
| msg_perr("%s: ioctl: %s\n", __func__, strerror(errno)); |
| return 1; |
| } else if (rc == 1) { |
| if (!start_found) { |
| start = erase_info.start; |
| start_found = 1; |
| } |
| len += mtd_erasesize; |
| } else if (rc == 0) { |
| if (start_found) { |
| /* TODO: changes required for supporting non-contiguous locked regions */ |
| break; |
| } |
| } |
| |
| } |
| |
| msg_cinfo("WP: write protect is %s.\n", |
| start_found ? "enabled": "disabled"); |
| msg_pinfo("WP: write protect range: start=0x%08x, " |
| "len=0x%08x\n", start, len); |
| |
| return 0; |
| } |
| |
| static struct wp wp_mtd = { |
| .list_ranges = mtd_wp_list_ranges, |
| .set_range = mtd_wp_set_range, |
| .enable = mtd_wp_enable_writeprotect, |
| .disable = mtd_wp_disable_writeprotect, |
| .wp_status = mtd_wp_status, |
| }; |