diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 8898cb8..309cc51 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -150,7 +150,7 @@ jobs: sensors_test anyka_test hal_hisi_test \ report_test http_test mtd_alias_test \ nand_layout_test upgrade_env_test \ - jffs2_test + jffs2_test raw_image_test ./build/cYAML_test ./build/report_test ./build/http_test @@ -158,6 +158,7 @@ jobs: ./build/upgrade_env_test ./build/jffs2_test ./build/nand_layout_test + ./build/raw_image_test ./build/reginfo_test ./build/longse_test ./build/anyka_test diff --git a/CMakeLists.txt b/CMakeLists.txt index 5744aac..ac3ff5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -334,6 +334,8 @@ set(IPCTOOL_SRC src/nand_layout.h src/nand_migrate.c src/nand_migrate.h + src/raw_image.c + src/raw_image.h src/bootrom.c src/bootrom.h src/clocks.c @@ -550,4 +552,8 @@ if(NOT ONLY_LIBRARY) # What nand-migrate decides before it touches the flash. add_executable(nand_layout_test src/nand_layout_test.c src/nand_layout.c) target_include_directories(nand_layout_test PRIVATE include src) + + # What restore --raw decides before it touches the flash. + add_executable(raw_image_test src/raw_image_test.c src/raw_image.c) + target_include_directories(raw_image_test PRIVATE include src) endif() diff --git a/README.md b/README.md index b677e37..4a79d6e 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,9 @@ Where: restore restore from a backup file [-s, --skip-env] skip environment [-f, --force] enforce + [--raw] the file is a plain image of the whole + NOR flash, not a backup; must match the + flash size exactly upgrade upgrade to OpenIPC firmware (experimental! use only on cameras with UART) [-f, --force] enforce @@ -287,6 +290,24 @@ sensors: # sync ``` +* Write a plain image of the whole NOR flash, from a programmer or `dd`: + + ```console + # ipctool restore --raw /tmp/flash.bin + ``` + + A raw image describes nothing and carries no checksums, so `--raw` prints + its SHA1 to compare by hand, and checks that SHA1 again just before erasing. + It refuses before anything is stopped unless the image is exactly the size + the partitions cover, each byte by one partition. `--force` skips the + question, not those checks. NAND is refused: a raw NAND image has no answer + for bad blocks or UBI. + + Partitions are placed by where they really lie, so one that overlaps the + others whole, such as `all`, is left out. Kernels before 4.4 do not report + that, so it comes from `mtdparts=` on the command line or the boot log. If + both have nothing, reboot and run it again while the log still has it. + ### Moving a NOR camera to another layout `upgrade` writes a kernel and a rootfs in a new partition layout over the diff --git a/src/backup.c b/src/backup.c index 9462b5e..723e650 100644 --- a/src/backup.c +++ b/src/backup.c @@ -36,6 +36,7 @@ #include "jffs2.h" #include "mtd.h" #include "network.h" +#include "raw_image.h" #include "sha1.h" #include "tools.h" #include "uboot.h" @@ -234,6 +235,10 @@ typedef struct { int ubi_device; int vol_id; char vol_name[64]; + // Written straight to /dev/mtd from its start, for an image that + // knows where each partition really lies + bool to_dev; + int dev; } stored_mtd_t; static int yaml_parseblock(char *start, int indent, stored_mtd_t *mi) { @@ -933,9 +938,11 @@ static bool do_flash(const char *phase, stored_mtd_t *mtdbackup, } int cnt = mtdbackup[i].size / chunk; for (int c = 0; c < cnt; c++) { - size_t this_offset; - int newi = - map_old_new_mtd(i, c * chunk, &this_offset, mtdbackup, mtd); + size_t this_offset = c * chunk; + int newi = mtdbackup[i].to_dev + ? mtdbackup[i].dev + : map_old_new_mtd(i, c * chunk, &this_offset, + mtdbackup, mtd); if (newi == -1) { fprintf(stderr, "\nOffset algorithm error, aborting...\n"); return false; @@ -1168,6 +1175,158 @@ static int restore_backup(const char *arg, bool skip_env, bool force) { return 1; } +typedef struct { + mtd_restore_ctx_t mtd; // first: cb_mtd_restore() is handed this struct + raw_part_t part[MAX_MTDBLOCKS]; + size_t nparts; +} raw_ctx_t; + +static bool cb_mtd_raw(int i, const char *name, struct mtd_info_user *mtd, + void *ctx) { + raw_ctx_t *c = (raw_ctx_t *)ctx; + if (c->nparts == MAX_MTDBLOCKS || i >= MAX_MTDBLOCKS) + return false; + raw_part_t *p = &c->part[c->nparts++]; + *p = (raw_part_t){.num = i, + .size = mtd->size, + .erasesize = mtd->erasesize, + .nor = mtd->type == MTD_NORFLASH}; + p->has_offset = mtd_flash_offset(i, name, mtd->size, &p->offset); + return cb_mtd_restore(i, name, mtd, ctx); +} + +static void image_sha1(const char *image, size_t size, char digest[20]) { + SHA1_CTX sha; + SHA1Init(&sha); + for (size_t off = 0; off < size;) { + size_t n = MIN(size - off, 1024 * 1024); + SHA1Update(&sha, (const unsigned char *)image + off, n); + off += n; + feed_watchdog(); + } + SHA1Final((unsigned char *)digest, &sha); +} + +/* + * A plain image of the whole NOR flash, from a programmer or `dd`. It says + * nothing about itself, so what makes it safe to write is that it fits this + * flash exactly, and all of that is settled before anything is stopped. + */ +static int restore_raw(const char *arg, bool skip_env, bool force) { + if (arg == NULL || access(arg, R_OK)) { + fprintf(stderr, "restore --raw needs a flash image: " + "ipctool restore --raw \n"); + return 1; + } + + static raw_ctx_t ctx; + ctx.mtd.env_dev = skip_env ? -1 : 0; + ctx.mtd.skip_env = skip_env; + enum_mtd_info(&ctx, cb_mtd_raw); + if (skip_env && ctx.mtd.env_dev == -1) { + fprintf(stderr, "No U-Boot env found, aborting...\n"); + return 1; + } + + // Mapped rather than read, like an upgrade bundle: a dump in /tmp would + // be in RAM twice. pin_to_ram() holds its pages, not their contents, so + // what was shown is checked again once nothing else is left to change it. + int fd = open(arg, O_RDONLY); + struct stat st; + if (fd < 0 || fstat(fd, &st)) { + fprintf(stderr, "Cannot read '%s': %s\n", arg, strerror(errno)); + if (fd >= 0) + close(fd); + return 1; + } + char why[160]; + static raw_part_t plan[MAX_MTDBLOCKS]; + size_t nplan = raw_image_plan(ctx.part, ctx.nparts, st.st_size, plan, why, + sizeof(why)); + if (!nplan) { + fprintf(stderr, "Not writing '%s': %s\n", arg, why); + close(fd); + return 1; + } + char *image = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (image == MAP_FAILED) { + fprintf(stderr, "Cannot map '%s': %s\n", arg, strerror(errno)); + close(fd); + return 1; + } + + // Nothing in the image can be checked, so show what would be written + char digest[20]; + image_sha1(image, st.st_size, digest); + printf("Raw image %s, 0x%llx bytes, SHA1 ", arg, + (unsigned long long)st.st_size); + for (size_t i = 0; i < sizeof(digest); i++) + printf("%02x", (unsigned char)digest[i]); + printf("\n%s", skip_env ? " U-Boot env overwrite will be skipped\n" : ""); + + static stored_mtd_t stored[MAX_MTDBLOCKS]; + for (size_t k = 0; k < nplan; k++) { + const raw_part_t *p = &plan[k]; + // A name is what makes do_flash() write an entry at all + snprintf(stored[k].name, sizeof(stored[k].name), "mtd%d %s", p->num, + ctx.mtd.part[p->num].name); + stored[k].off_flashb = p->offset; + stored[k].size = p->size; + stored[k].data = image + p->offset; + stored[k].to_dev = true; + stored[k].dev = p->num; + printf("[%zu] 0x%.8zx\t0x%.8zx\t%s\n", k, stored[k].off_flashb, + stored[k].size, stored[k].name); + } + + if (!force) { + char c = 0; + fprintf(stderr, "This image replaces the whole flash, bootloader " + "included.\nAre you sure to proceed? (y/n)? "); + int ret = scanf(" %c", &c); + (void)ret; + if (c != 'y') + goto bailout; + } + + if (!free_resources(force)) + goto bailout; + if (!do_flash("Analyzing", stored, &ctx.mtd, skip_env, true)) + goto bailout; + if (!pin_to_ram()) + goto bailout; + if (!isolate_from_flash(NULL, NULL)) { + reboot_after_failure("Preparing the flash"); + goto bailout; + } + + // Size first: reading past the end of a truncated file is a SIGBUS, and + // from here that would leave a camera with no services and no reboot + struct stat now; + char again[sizeof(digest)]; + if (fstat(fd, &now) || now.st_size != st.st_size) { + fprintf(stderr, "The image changed size\n"); + reboot_after_failure("Checking the image"); + goto bailout; + } + image_sha1(image, st.st_size, again); + if (memcmp(digest, again, sizeof(digest))) { + fprintf(stderr, "The image is not the one shown\n"); + reboot_after_failure("Checking the image"); + goto bailout; + } + + if (!do_flash("Restoring", stored, &ctx.mtd, skip_env, false)) + reboot_after_failure("Restoring"); + + reboot_with_msg(); + +bailout: + close(fd); + munmap(image, st.st_size); + return 1; +} + #define MAX_MTDPARTS 1024 static void add_mtdpart(char *dst, const char *name, uint32_t size) { size_t len = strlen(dst); @@ -1847,11 +2006,13 @@ int upgrade_restore_cmd(int argc, char **argv) { {"force", no_argument, NULL, 'f'}, {"skip-env", no_argument, NULL, 's'}, {"wipe-settings", no_argument, NULL, 'w'}, + {"raw", no_argument, NULL, 'r'}, {NULL, 0, NULL, 0}, }; bool skip_env = false; bool force = false; bool wipe_settings = false; + bool raw = false; int res; int option_index; @@ -1867,6 +2028,9 @@ int upgrade_restore_cmd(int argc, char **argv) { case 'w': wipe_settings = true; break; + case 'r': + raw = true; + break; case '?': print_usage(); @@ -1877,6 +2041,12 @@ int upgrade_restore_cmd(int argc, char **argv) { const char *optarg = argv[optind]; bool restore_mode = argv[0][0] == 'r'; + if (raw && !restore_mode) { + fprintf(stderr, "--raw is an option of restore\n"); + return EXIT_FAILURE; + } + if (raw) + return restore_raw(optarg, skip_env, force); if (restore_mode) return restore_backup(optarg, skip_env, force); else diff --git a/src/main.c b/src/main.c index 3f703e7..32781d8 100644 --- a/src/main.c +++ b/src/main.c @@ -89,6 +89,9 @@ void print_usage() { " restore restore from a backup file\n" " [-s, --skip-env] skip environment\n" " [-f, --force] enforce\n" + " [--raw] the file is a plain image of the whole\n" + " NOR flash, not a backup; must match the\n" + " flash size exactly\n" " upgrade upgrade to OpenIPC firmware\n" " (experimental! use only on cameras with " "UART)\n" diff --git a/src/mtd.c b/src/mtd.c index fcba9f1..d207761 100644 --- a/src/mtd.c +++ b/src/mtd.c @@ -403,6 +403,39 @@ struct mtd_entry { bool valid; }; +bool mtd_flash_offset(int num, const char *name, uint64_t size, + uint64_t *offset) { + char path[64]; + snprintf(path, sizeof(path), "/sys/class/mtd/mtd%d/offset", num); + FILE *f = fopen(path, "r"); + unsigned long long o = 0; + bool known = f && fscanf(f, "%llu", &o) == 1; + if (f) + fclose(f); + if (known) { + *offset = o; + return true; + } + + // Before 4.4 the kernel keeps this to itself + char cmdline[4096]; + f = fopen("/proc/cmdline", "r"); + if (f) { + size_t n = fread(cmdline, 1, sizeof(cmdline) - 1, f); + fclose(f); + cmdline[n] = 0; + cmdline[strcspn(cmdline, "\n")] = 0; + if (mtd_offset_from_cmdline(cmdline, name, size, offset)) + return true; + } + + char *log = NULL; + ssize_t len = get_dmesg_buf(&log); + known = log && len > 0 && mtd_offset_from_log(log, len, name, size, offset); + free(log); + return known; +} + void enum_mtd_info(void *ctx, cb_mtd cb) { FILE *fp; char dev[80]; @@ -466,14 +499,9 @@ void enum_mtd_info(void *ctx, cb_mtd cb) { for (int i = 0; i < n; i++) { if (!mtds[i].valid) continue; - char path[64]; - snprintf(path, sizeof(path), "/sys/class/mtd/mtd%d/offset", mtds[i].i); - FILE *f = fopen(path, "r"); - unsigned long long o = 0; - has_off[m] = f && fscanf(f, "%llu", &o) == 1; - if (f) - fclose(f); - offs[m] = o; + offs[m] = 0; + has_off[m] = mtd_flash_offset(mtds[i].i, mtds[i].name, mtds[i].mtd.size, + &offs[m]); sizes[m] = mtds[i].mtd.size; idx[m++] = i; } diff --git a/src/mtd.h b/src/mtd.h index f345976..e870c49 100644 --- a/src/mtd.h +++ b/src/mtd.h @@ -10,6 +10,10 @@ typedef bool (*cb_mtd)(int i, const char *name, struct mtd_info_user *mtd, cJSON *get_mtd_info(); char *open_mtdblock(int i, int *fd, uint32_t size, int flags); void enum_mtd_info(void *ctx, cb_mtd cb); +// Where partition `num` starts on the flash: the kernel's word where it gives +// one, then the command line and the boot log, which a long uptime can lose +bool mtd_flash_offset(int num, const char *name, uint64_t size, + uint64_t *offset); bool mtd_write(int mtd, uint32_t offset, uint32_t erasesize, const char *data, size_t size); int mtd_unlock_cmd(); diff --git a/src/mtd_alias.c b/src/mtd_alias.c index 43b2393..2eec197 100644 --- a/src/mtd_alias.c +++ b/src/mtd_alias.c @@ -1,3 +1,7 @@ +#include +#include +#include + #include "mtd_alias.h" void mtd_mark_aliases(int n, const uint64_t *off, const bool *has_off, @@ -22,3 +26,117 @@ void mtd_mark_aliases(int n, const uint64_t *off, const bool *has_off, alias[i] = n > 1 && others == size[i]; } } + +// The kernel's memparse(): strtoull with a size suffix +static uint64_t memparse(const char *s, const char **end) { + char *e; + uint64_t v = strtoull(s, &e, 0); + if (e != s && *e && strchr("kKmMgG", *e)) { + v <<= *e == 'k' || *e == 'K' ? 10 : *e == 'm' || *e == 'M' ? 20 : 30; + e++; + } + *end = e; + return v; +} + +// Every partition of one :,... A part with no offset follows +// the one before it. Returns where the definition ends, or NULL if it is not +// one. +static const char *scan_mtddef(const char *s, const char *name, uint64_t size, + uint64_t *offset, int *found) { + uint64_t next = 0; + for (;;) { + bool rest = *s == '-'; + uint64_t psize = 0, off = next; + const char *e = s + 1; + if (!rest) { + psize = memparse(s, &e); + if (e == s) + return NULL; + } + s = e; + if (*s == '@') { + off = memparse(s + 1, &e); + if (e == s + 1) + return NULL; + s = e; + } + if (*s == '(') { + const char *close = strchr(s, ')'); + if (!close) + return NULL; + size_t n = close - s - 1; + if (n == strlen(name) && !strncmp(s + 1, name, n) && + (rest || psize == size)) { + if (*found && *offset != off) + *found = 2; + else if (!*found) + *found = 1; + *offset = off; + } + s = close + 1; + } + s += strcspn(s, ",; "); // the ro and lk flags + next = off + psize; + if (*s != ',') + return s; + s++; + } +} + +bool mtd_offset_from_cmdline(const char *cmdline, const char *name, + uint64_t size, uint64_t *offset) { + // The kernel keeps the last one it is given + const char *def = NULL; + for (const char *p = cmdline; (p = strstr(p, "mtdparts=")); p++) + if (p == cmdline || p[-1] == ' ') + def = p + strlen("mtdparts="); + if (!def) + return false; + + int found = 0; + while (def && *def && *def != ' ') { + const char *colon = strchr(def, ':'); + if (!colon) + break; + def = scan_mtddef(colon + 1, name, size, offset, &found); + if (def && *def == ';') + def++; + else + break; + } + return found == 1; +} + +bool mtd_offset_from_log(const char *log, size_t len, const char *name, + uint64_t size, uint64_t *offset) { + int found = 0; + const char *end = log + len; + while (log < end) { + const char *nl = memchr(log, '\n', end - log); + size_t n = (nl ? nl : end) - log; + char line[256], part[128]; + unsigned long long from, to; + if (n < sizeof(line)) { + memcpy(line, log, n); + line[n] = 0; + // Behind a <5> level and a timestamp, or neither + const char *p = strstr(line, "0x"); + int end_of_line = -1; + if (p) + sscanf(p, "0x%llx-0x%llx : \"%127[^\"]\"%n", &from, &to, part, + &end_of_line); + // %n is only reached past the closing quote: a line the ring + // buffer tore in two has a shorter name in it + if (end_of_line >= 0 && !strcmp(part, name) && to - from == size) { + if (found && *offset != from) + found = 2; + else if (!found) + found = 1; + *offset = from; + } + } + log += n + 1; + } + return found == 1; +} diff --git a/src/mtd_alias.h b/src/mtd_alias.h index ca1834f..5f98fce 100644 --- a/src/mtd_alias.h +++ b/src/mtd_alias.h @@ -2,6 +2,7 @@ #define MTD_ALIAS_H #include +#include #include /* Marks alias[i] for each partition that is another name for flash the @@ -12,4 +13,19 @@ void mtd_mark_aliases(int n, const uint64_t *off, const bool *has_off, const uint64_t *size, bool *alias); +/* + * Where a partition starts, for kernels before 4.4: they have no + * /sys/class/mtd/mtdN/offset. A partition is found by name and size, and is + * not found if two places claim it. + * + * From the `mtdparts=` on a kernel command line. Only says something when the + * kernel took its partitions from there, which the size match stands in for. + */ +bool mtd_offset_from_cmdline(const char *cmdline, const char *name, + uint64_t size, uint64_t *offset); +/* From the line the kernel logs per partition as it adds it, which also covers + * tables built into the kernel. `log` need not be NUL-terminated. */ +bool mtd_offset_from_log(const char *log, size_t len, const char *name, + uint64_t size, uint64_t *offset); + #endif /* MTD_ALIAS_H */ diff --git a/src/mtd_alias_test.c b/src/mtd_alias_test.c index b35a2c2..dfb0c7a 100644 --- a/src/mtd_alias_test.c +++ b/src/mtd_alias_test.c @@ -56,5 +56,86 @@ int main(void) { if (!failed) printf("mtd_alias_test: ok\n"); + // Where a partition lies, as a kernel without sysfs offsets tells it + uint64_t at; + const char *hi = "mem=32M console=ttyAMA0,115200 root=/dev/mtdblock3 " + "mtdparts=hi_sfc:256k(boot),64k(env),2048k(kernel)," + "5120k(rootfs),-(rootfs_data) panic=20"; + CHECK(mtd_offset_from_cmdline(hi, "boot", 0x40000, &at) && at == 0, "boot"); + CHECK(mtd_offset_from_cmdline(hi, "rootfs", 0x500000, &at) && + at == 0x250000, + "rootfs at %llx", (unsigned long long)at); + // "-" says nothing of the size, and the size is not asked of it + CHECK(mtd_offset_from_cmdline(hi, "rootfs_data", 0x2b0000, &at) && + at == 0x750000, + "rootfs_data at %llx", (unsigned long long)at); + // The size is what says this table is the one the kernel used + CHECK(!mtd_offset_from_cmdline(hi, "boot", 0x20000, &at), "wrong size"); + CHECK(!mtd_offset_from_cmdline(hi, "nope", 0x40000, &at), "wrong name"); + CHECK(!mtd_offset_from_cmdline("root=/dev/ram", "boot", 0x40000, &at), + "no mtdparts"); + + const char *over = "mtdparts=spi0.0:1M@0x10000(a)ro,0x20000(b)," + "16M@0(all),8m@2M(c)lk;nand:1g(d)"; + CHECK(mtd_offset_from_cmdline(over, "a", 0x100000, &at) && at == 0x10000, + "@ and ro"); + CHECK(mtd_offset_from_cmdline(over, "b", 0x20000, &at) && at == 0x110000, + "after @"); + CHECK(mtd_offset_from_cmdline(over, "all", 0x1000000, &at) && at == 0, + "all"); + CHECK(mtd_offset_from_cmdline(over, "c", 0x800000, &at) && at == 0x200000, + "suffixed @ and lk"); + CHECK(mtd_offset_from_cmdline(over, "d", 0x40000000, &at) && at == 0, + "second chip"); + + // The last mtdparts= is the one the kernel keeps, not a lookalike + CHECK(mtd_offset_from_cmdline("mtdparts=a:1M(x) mtdparts=a:1M@2M(x)", "x", + 0x100000, &at) && + at == 0x200000, + "last wins"); + CHECK(!mtd_offset_from_cmdline("xmtdparts=a:1M(x)", "x", 0x100000, &at), + "lookalike"); + // Two chips, one name and size: which is it + CHECK(!mtd_offset_from_cmdline("mtdparts=a:1M(x);b:1M@1M(x)", "x", 0x100000, + &at), + "ambiguous"); + CHECK(!mtd_offset_from_cmdline("mtdparts=a:zz(x)", "x", 0x100000, &at), + "garbage"); + CHECK(!mtd_offset_from_cmdline("mtdparts=a:1M(x", "x", 0x100000, &at), + "unterminated"); + + // The kernel's own format, on 3.0 to 6.x, in the shapes dmesg gives it + static const char klog[] = + "<5>Creating 5 MTD partitions on \"hi_sfc\":\n" + "<5>0x000000000000-0x000000020000 : \"BOOT\"\n" + "<5>[ 1.234567] 0x000000040000-0x0000001d0000 : \"KERNEL\"\n" + "0x000000020000-0x000000040000 : \"UBOOT\"\n" + "<5>0x000000100000-0x000000110000 : \"twice\"\n" + "<5>0x000000200000-0x000000210000 : \"twice\"\n" + "<5>0x000000300000-0x000000310000 : \"same\"\n" + "<5>0x000000300000-0x000000310000 : \"same\"\n" + "<6>spi: at 0x12340000 irq 5\n" + "<5>0x0000007b0000-0x000000800000 : \"cut"; + size_t klen = sizeof(klog) - 1; + CHECK(mtd_offset_from_log(klog, klen, "BOOT", 0x20000, &at) && at == 0, + "BOOT"); + CHECK(mtd_offset_from_log(klog, klen, "KERNEL", 0x190000, &at) && + at == 0x40000, + "timestamped"); + CHECK(mtd_offset_from_log(klog, klen, "UBOOT", 0x20000, &at) && + at == 0x20000, + "bare"); + CHECK(!mtd_offset_from_log(klog, klen, "BOOT", 0x10000, &at), "wrong size"); + CHECK(!mtd_offset_from_log(klog, klen, "nope", 0x20000, &at), "wrong name"); + CHECK(!mtd_offset_from_log(klog, klen, "twice", 0x10000, &at), + "two places"); + CHECK(mtd_offset_from_log(klog, klen, "same", 0x10000, &at) && + at == 0x300000, + "logged twice"); + CHECK(!mtd_offset_from_log(klog, klen, "cut", 0x50000, &at), "torn line"); + // Bounded by len: nothing after it is read + CHECK(!mtd_offset_from_log(klog, 10, "BOOT", 0x20000, &at), "len"); + CHECK(!mtd_offset_from_log("", 0, "BOOT", 0x20000, &at), "empty"); + return failed; } diff --git a/src/raw_image.c b/src/raw_image.c new file mode 100644 index 0000000..510b40b --- /dev/null +++ b/src/raw_image.c @@ -0,0 +1,84 @@ +#include +#include +#include + +#include "raw_image.h" + +size_t raw_image_plan(const raw_part_t *parts, size_t nparts, + uint64_t image_size, raw_part_t *plan, char *why, + size_t whylen) { + if (!nparts) { + snprintf(why, whylen, "no flash partitions found"); + return 0; + } + + for (size_t k = 0; k < nparts; k++) { + if (!parts[k].nor) { + snprintf(why, whylen, + "mtd%d is not NOR: a raw NAND image has no bad block or " + "UBI handling here", + parts[k].num); + return 0; + } + if (!parts[k].has_offset) { + snprintf(why, whylen, + "cannot tell where mtd%d lies on the flash: reboot and " + "run this again while the boot log still says", + parts[k].num); + return 0; + } + } + + // By offset; few enough that this is all the sort it needs + static raw_part_t sorted[64]; + if (nparts > sizeof(sorted) / sizeof(sorted[0])) { + snprintf(why, whylen, "too many partitions"); + return 0; + } + for (size_t k = 0; k < nparts; k++) { + size_t at = k; + for (; at && sorted[at - 1].offset > parts[k].offset; at--) + sorted[at] = sorted[at - 1]; + sorted[at] = parts[k]; + } + + size_t nplan = 0; + uint64_t covered = 0; + for (size_t k = 0; k < nparts; k++) { + const raw_part_t *p = &sorted[k]; + if (p->offset + p->size <= covered) + continue; + if (p->offset < covered) { + snprintf(why, whylen, + "mtd%d overlaps the partition before it at 0x%" PRIx64, + p->num, p->offset); + return 0; + } + if (p->offset > covered) { + snprintf(why, whylen, + "no partition covers 0x%" PRIx64 "-0x%" PRIx64 ": one is " + "missing or read-only", + covered, p->offset); + return 0; + } + if (!p->erasesize || p->erasesize != sorted[0].erasesize || + p->size % p->erasesize) { + snprintf(why, whylen, + "mtd%d is not a whole number of 0x%" PRIx32 " erase " + "blocks", + p->num, sorted[0].erasesize); + return 0; + } + plan[nplan++] = *p; + covered += p->size; + } + + if (image_size != covered) { + snprintf(why, whylen, + "the image is 0x%" PRIx64 " bytes, the partitions cover " + "0x%" PRIx64, + image_size, covered); + return 0; + } + return nplan; +} diff --git a/src/raw_image.h b/src/raw_image.h new file mode 100644 index 0000000..d13947d --- /dev/null +++ b/src/raw_image.h @@ -0,0 +1,36 @@ +#ifndef RAW_IMAGE_H +#define RAW_IMAGE_H + +/* + * The pure half of `ipctool restore --raw`: how an image with no description + * of its own is cut along this camera's partitions. Kept apart so it can be + * tested on a host (raw_image_test.c). + */ + +#include +#include +#include + +typedef struct { + int num; // N of /dev/mtdN + uint64_t size; + uint32_t erasesize; + bool nor; + // Where it starts on the flash, if anything could say + bool has_offset; + uint64_t offset; +} raw_part_t; + +/* + * Picks the partitions that cover an image of `image_size` bytes exactly once + * and puts them in `plan`, which holds `nparts`, in flash order. Returns how + * many, or 0 with `why` saying what is in the way. A partition that lies + * wholly inside the ones picked is another name for them and is left out. + * There is no override: an image that does not fit is the wrong image, and a + * layout that cannot be established cannot be written safely. + */ +size_t raw_image_plan(const raw_part_t *parts, size_t nparts, + uint64_t image_size, raw_part_t *plan, char *why, + size_t whylen); + +#endif /* RAW_IMAGE_H */ diff --git a/src/raw_image_test.c b/src/raw_image_test.c new file mode 100644 index 0000000..ed4860d --- /dev/null +++ b/src/raw_image_test.c @@ -0,0 +1,140 @@ +/* + * raw_image_test: when `ipctool restore --raw` agrees to write, and to which + * partitions. The checks are all that stands between a wrong file and a + * camera, and restore itself cannot run on a host. + */ +#include +#include + +#include "raw_image.h" + +static int failed; +#define CHECK(cond, ...) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + failed = 1; \ + } \ + } while (0) + +#define ES 0x10000u +#define FLASH 0x800000u +#define MAX 8 + +// 8 MB: boot, env, kernel, rootfs, as OpenIPC lays out a NOR +static void layout(raw_part_t *p) { + static const uint64_t sizes[] = {0x40000, 0x10000, 0x200000, 0x5b0000}; + uint64_t off = 0; + for (int i = 0; i < 4; i++) { + p[i] = (raw_part_t){.num = i, + .size = sizes[i], + .erasesize = ES, + .nor = true, + .has_offset = true, + .offset = off}; + off += sizes[i]; + } +} + +static void refused(const raw_part_t *p, size_t n, uint64_t size, + const char *expect) { + char why[128] = ""; + raw_part_t plan[MAX]; + CHECK(!raw_image_plan(p, n, size, plan, why, sizeof why), + "accepted, wanted '%s'", expect); + CHECK(strstr(why, expect), "'%s' does not say '%s'", why, expect); +} + +// The plan is these devices, in this order +static void planned(const raw_part_t *p, size_t n, const int *nums, size_t m) { + char why[128] = ""; + raw_part_t plan[MAX]; + size_t got = raw_image_plan(p, n, FLASH, plan, why, sizeof why); + CHECK(got == m, "%zu partitions planned, wanted %zu: %s", got, m, why); + for (size_t k = 0; k < got && k < m; k++) + CHECK(plan[k].num == nums[k], "plan[%zu] is mtd%d", k, plan[k].num); +} + +int main(void) { + raw_part_t p[MAX]; + + layout(p); + planned(p, 4, (int[]){0, 1, 2, 3}, 4); + + // The file is wrong. A zero-length file on a machine with no MTD at all + // has to stop here too: the next step stops every process. + layout(p); + refused(p, 4, FLASH - 1, "the image is"); + refused(p, 4, FLASH + 1, "the image is"); + refused(p, 4, FLASH * 2, "the image is"); + refused(p, 4, 0, "the image is"); + refused(p, 0, 0, "no flash partitions"); + + layout(p); + p[2].nor = false; + refused(p, 4, FLASH, "not NOR"); + + // Nothing could say where this one is; its size proves nothing + layout(p); + p[2].has_offset = false; + refused(p, 4, FLASH, "cannot tell where mtd2"); + + // A partition that could not be opened + layout(p); + p[2] = p[3]; + refused(p, 3, FLASH, "no partition covers 0x50000-0x250000"); + layout(p); + refused(p, 3, FLASH, "the image is"); + + // Numbers need not follow the flash: "all" went first and was left out + // by enumeration, so the real ones start at mtd1 + layout(p); + for (int i = 0; i < 4; i++) + p[i].num = i + 1; + planned(p, 4, (int[]){1, 2, 3, 4}, 4); + + // Anjoy: UBOOT is mtd3 and lies second, whichever order it is listed in + layout(p); + p[1].num = 3; + p[3].num = 1; + planned(p, 4, (int[]){0, 3, 2, 1}, 4); + raw_part_t swapped = p[1]; + p[1] = p[3]; + p[3] = swapped; + planned(p, 4, (int[]){0, 3, 2, 1}, 4); + + // A second name for one partition, and one inside what is already picked + layout(p); + p[4] = p[2]; + p[4].num = 4; + planned(p, 5, (int[]){0, 1, 2, 3}, 4); + p[4].offset = p[2].offset + ES; + p[4].size = ES; + planned(p, 5, (int[]){0, 1, 2, 3}, 4); + + // The sizes add up, and only the offsets show these two share flash + layout(p); + p[2].offset -= ES; + p[3].offset += ES; + refused(p, 4, FLASH, "mtd2 overlaps"); + layout(p); + p[3].offset = p[2].offset; + p[3].offset += p[2].size - ES; + refused(p, 4, FLASH, "mtd3 overlaps"); + + layout(p); + p[1].size += 1; + p[2].offset += 1; + p[3].offset += 1; + refused(p, 4, FLASH + 1, "erase blocks"); + layout(p); + p[3].erasesize = 0x1000; + refused(p, 4, FLASH, "erase blocks"); + layout(p); + p[0].erasesize = 0; + refused(p, 4, FLASH, "erase blocks"); + + return failed; +} diff --git a/src/tools.h b/src/tools.h index ca67e38..05d9b17 100644 --- a/src/tools.h +++ b/src/tools.h @@ -61,6 +61,8 @@ bool dts_items_by_regex(const char *filename, const char *re, char *outbuf, bool line_from_file(const char *filename, const char *re, char *outbuf, size_t outlen); int dmesg(); +// The kernel log, malloc()ed and not NUL-terminated; its length, or <= 0 +ssize_t get_dmesg_buf(char **buf); uint32_t read_le32(const char *ptr); char *file_to_buf(const char *filename, size_t *len); char *fread_to_buf(const char *filename, size_t *len, uint32_t round_up,