diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 6b588a5..2acdafa 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -141,11 +141,14 @@ jobs: cmake --build build --target cYAML_test reginfo_test longse_test \ sensors_test anyka_test hal_hisi_test \ report_test http_test mtd_alias_test \ - nand_layout_test + nand_layout_test upgrade_env_test \ + jffs2_test ./build/cYAML_test ./build/report_test ./build/http_test ./build/mtd_alias_test + ./build/upgrade_env_test + ./build/jffs2_test ./build/nand_layout_test ./build/reginfo_test ./build/longse_test diff --git a/CMakeLists.txt b/CMakeLists.txt index 19dc363..e0cffe6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -312,6 +312,10 @@ message(STATUS "libipchw vendor HALs: hisi ${_ipchw_selected}") set(IPCTOOL_SRC src/backup.c src/backup.h + src/jffs2.c + src/jffs2.h + src/upgrade_env.c + src/upgrade_env.h src/nand_layout.c src/nand_layout.h src/nand_migrate.c @@ -522,6 +526,13 @@ if(NOT ONLY_LIBRARY) add_executable(mtd_alias_test src/mtd_alias_test.c src/mtd_alias.c) target_include_directories(mtd_alias_test PRIVATE include src) + # What upgrade edits in the bootloader's environment, and the image it + # carries the settings across a repartition in. + add_executable(upgrade_env_test src/upgrade_env_test.c src/upgrade_env.c) + target_include_directories(upgrade_env_test PRIVATE include src) + add_executable(jffs2_test src/jffs2_test.c src/jffs2.c) + target_include_directories(jffs2_test PRIVATE include src) + # 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) diff --git a/README.md b/README.md index f606ef4..5f0e153 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,42 @@ sensors: # sync ``` +### Moving a NOR camera to another layout + +`upgrade` writes a kernel and a rootfs in a new partition layout over the +running system: lite (2 MB kernel, 5 MB rootfs) to ultimate (3 MB, 10 MB) on +16 MB of NOR, for one. The bundle comes from the release archive: + +```console +$ tools/upgrade_bundle.py --layout nor16m \ + --firmware openipc.hi3516ev300-nor-ultimate.tgz \ + --env upgrade=https://github.com/OpenIPC/builder/releases/download/latest/-nor.tgz \ + -o ultimate.bundle +# ipctool upgrade /tmp/ultimate.bundle +``` + +What a `--layout` bundle does on the camera: + +* keeps every partition in front of the kernel as it is, under this flash's + own names -- the bootloader, its environment and whatever a vendor put + there (`wtf` on XiongMai); +* edits `bootargs` and `bootcmd` rather than replacing them: only `root=`, + `mtdparts=` and the size of the kernel read change, so a vendor + bootloader's other arguments survive; +* carries the settings: the jffs2 overlay is copied, once it is read-only, + into a jffs2 image written where the new `rootfs_data` starts. The camera + comes back with its passwords, keys and configuration. `--wipe-settings` + starts it from defaults instead; +* sets the U-Boot variables given with `--env`, such as the address the new + firmware upgrades itself from. + +Everything that can be refused is refused before any process is stopped: +an image larger than its partition, settings that will not fit the new +`rootfs_data`, a bootloader environment with no literal `mtdparts=` or no +single `sf read` to edit. After that every other process is stopped, the +flash filesystems go read-only, and the camera reboots when done. Keep a +backup and a UART at hand: a power cut mid-write leaves no kernel to boot. + ### Sending a board to openipc.org A camera nobody has reported yet reaches the [board diff --git a/src/backup.c b/src/backup.c index f34e07b..23afd80 100644 --- a/src/backup.c +++ b/src/backup.c @@ -33,11 +33,13 @@ #include "dns.h" #include "hal/common.h" #include "http.h" +#include "jffs2.h" #include "mtd.h" #include "network.h" #include "sha1.h" #include "tools.h" #include "uboot.h" +#include "upgrade_env.h" #include "watchdog.h" @@ -654,7 +656,11 @@ static bool pin_to_ram() { // Second half. From here on the system has no services left, so a failure // cannot return to the shell: the caller reboots, which on this path is safe // because nothing has been erased yet. -static bool isolate_from_flash() { +// +// `read_only` runs once every flash filesystem is read-only and before any is +// unmounted: the one moment their contents can be read and can no longer +// change, which is when upgrade takes the settings it carries. +static bool isolate_from_flash(bool (*read_only)(void *), void *arg) { printf("Stopping every other process before touching the flash.\n" "A network session will drop here; flashing continues and the\n" "device reboots when done. Progress goes to the serial console.\n"); @@ -700,6 +706,8 @@ static bool isolate_from_flash() { // this point: it would also wait on network filesystems. if (!remount_flash_ro()) return false; + if (read_only && !read_only(arg)) + return false; umount_all(); feed_watchdog(); return true; @@ -1113,7 +1121,7 @@ static int restore_backup(const char *arg, bool skip_env, bool force) { goto bailout; if (!pin_to_ram()) goto bailout; - if (!isolate_from_flash()) + if (!isolate_from_flash(NULL, NULL)) reboot_after_failure("Preparing the flash"); if (!do_flash("Restoring", mtdbackup, &mtd, skip_env, false)) reboot_after_failure("Restoring"); @@ -1131,7 +1139,7 @@ static int restore_backup(const char *arg, bool skip_env, bool force) { static void add_mtdpart(char *dst, const char *name, uint32_t size) { size_t len = strlen(dst); bool need_comma = false; - char prev = dst[len - 1]; + char prev = len ? dst[len - 1] : '\0'; if (len && prev != ':' && prev != '=' && prev != ',') need_comma = true; snprintf(dst + len, MAX_MTDPARTS - len, "%s%dk(%s)", need_comma ? "," : "", @@ -1190,7 +1198,78 @@ static void skip_signals() { #endif } -static int do_upgrade(const char *filename, bool force) { +/* + * The settings, carried across a repartition. They live on a jffs2 overlay + * whose partition moves when the layout changes, and the running kernel has + * no partition at the new place to mount, so they travel as a ready-made + * jffs2 image written there along with the kernel and the rootfs. + */ +typedef struct { + char dir[80]; // where the old settings partition is mounted + char *region; // the new settings partition's contents, flash-sized + size_t region_len, erasesize; +} carry_t; + +// Where partition `num` (named `name`) is mounted as jffs2, if it is. The +// kernel prints the source as it was given, and each of these mounts it. +static bool jffs2_mountpoint(int num, const char *name, char *path, + size_t sz) { + FILE *fp = fopen("/proc/mounts", "r"); + if (!fp) + return false; + char block[32], bare[16], byname[80], line[256]; + snprintf(block, sizeof(block), "/dev/mtdblock%d", num); + snprintf(bare, sizeof(bare), "mtd%d", num); + snprintf(byname, sizeof(byname), "mtd:%s", name); + bool found = false; + while (!found && fgets(line, sizeof line, fp)) { + char dev[80], mnt[80], fs[80]; + if (sscanf(line, "%79s %79s %79s", dev, mnt, fs) == 3 && + !strcmp(fs, "jffs2") && + (!strcmp(dev, block) || !strcmp(dev, bare) || + !strcmp(dev, byname))) { + snprintf(path, sz, "%s", mnt); + found = true; + } + } + fclose(fp); + return found; +} + +// The image of the settings into the region, everything after it erased. +// work/ goes empty: it is overlayfs's scratch space, and overlayfs makes +// what it needs there as it mounts. +// Erase blocks jffs2 must find free to collect garbage in. A partition the +// carried settings fill to the last block mounts, and the first write to it +// -- a saved setting, a changed password -- fails with ENOSPC. The kernel +// holds back about this many for its own use on a NOR partition this size. +#define JFFS2_GC_BLOCKS 5 + +static bool build_carry(void *arg) { + carry_t *c = arg; + size_t headroom = JFFS2_GC_BLOCKS * c->erasesize; + if (c->region_len <= headroom) { + fprintf(stderr, "Cannot carry the settings: the new settings " + "partition is too small to leave jffs2 room to work\n"); + return false; + } + jffs2_img_t *img = jffs2_new(c->erasesize, c->region_len - headroom); + bool ok = img && jffs2_add_tree(img, c->dir, "work"); + if (ok) { + size_t len; + const uint8_t *data = jffs2_data(img, &len); + memset(c->region, 0xff, c->region_len); + memcpy(c->region, data, len); + printf("Settings: %d entries in %zu of %zu KB\n", jffs2_entries(img), + len / 1024, c->region_len / 1024); + } else + fprintf(stderr, "Cannot carry the settings: %s\n", + img ? jffs2_error(img) : "out of memory"); + jffs2_free(img); + return ok; +} + +static int do_upgrade(const char *filename, bool force, bool wipe_settings) { if (!getchipname()) { fprintf(stderr, "Chip has not been detected\n"); return 1; @@ -1242,9 +1321,22 @@ static int do_upgrade(const char *filename, bool force) { return 1; } else printf("Using '%s' as upgrade bundle\n", filename); - char *jsond = file_to_buf(filename, &len); - if (!jsond) { - fprintf(stderr, "'%s' is not found\n", filename); + /* Mapped rather than read: the bundle is usually in /tmp, a tmpfs, and + * reading it would hold it in RAM twice -- 10 MB more on a camera with + * 40 MB to spare, before the partitions are even laid out. */ + char *jsond = NULL; + int bfd = open(filename, O_RDONLY); + struct stat bst; + if (bfd >= 0 && !fstat(bfd, &bst) && bst.st_size > 0) { + len = bst.st_size; + jsond = mmap(NULL, len, PROT_READ, MAP_PRIVATE, bfd, 0); + if (jsond == MAP_FAILED) + jsond = NULL; + } + if (bfd >= 0) + close(bfd); + if (!jsond || !memchr(jsond, '\0', len)) { + fprintf(stderr, "'%s' is not found or is not a bundle\n", filename); return 1; } @@ -1302,13 +1394,22 @@ static int do_upgrade(const char *filename, bool force) { if (cJSON_IsString(skip_env)) { const char *name = skip_env->valuestring; ssize_t size = mtd_size_by_name(&mtd, name); - if (size != -1) { - add_mtdpart(mtdparts, name, size); - curr_mtd_part++; - goff += size; - printf("skipped original partition %s sized %#x\n", name, - size); + if (size == -1) { + // Skipping nothing here wrote the kernel over the + // bootloader's environment on a board that calls it + // something else ("wtf" on XiongMai) + fprintf(stderr, + "The bundle keeps partition '%s', which this " + "flash does not have. Nothing was written.\n", + name); + ret = 1; + goto bailout; } + add_mtdpart(mtdparts, name, size); + curr_mtd_part++; + goff += size; + printf("skipped original partition %s sized %#zx\n", name, + (size_t)size); } } } @@ -1324,6 +1425,58 @@ static int do_upgrade(const char *filename, bool force) { ASSERT_JSON(parts); const cJSON *part; int i = 0; + + /* A bundle that says nothing about what to keep keeps what the camera + * has in front of its kernel: the bootloader, its environment and + * whatever else a vendor put there, under this flash's own names and + * sizes. The bootloader reads its parts at fixed offsets, so they are + * the one part of a layout that cannot move -- and they need not be + * where their names suggest: a XiongMai bootloader keeps its environment + * at 0x30000, inside "boot", with a 64 KB "wtf" after it. Keeping up to + * the environment alone wrote the kernel over "wtf". The environment is + * then edited rather than rewritten. */ + bool writes_boot = false; + const char *first_written = NULL; + cJSON_ArrayForEach(part, parts) { + const char *n = + cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(part, "name")); + if (n && !first_written) + first_written = n; + if (n && !strcmp(n, "boot")) + writes_boot = true; + } + bool keep_front = !writes_boot && !mtdparts_pr && !skip_envs && !offset; + if (keep_front) { + char id[64]; + if (!upgrade_mtd_id(uboot_env_get_param("bootargs"), id, sizeof(id))) { + fprintf(stderr, "The bootloader's bootargs name no partitions " + "(no literal mtdparts=), so there is no telling " + "what to keep. Nothing was written.\n"); + ret = 1; + goto bailout; + } + int keep = -1; + for (int p = 0; first_written && p < MAX_MTDBLOCKS && keep < 0; p++) + if (!strcmp(mtd.part[p].name, first_written)) + keep = p; + if (keep <= mtd.env_dev) { + fprintf(stderr, + "This flash has no '%s' partition after the bootloader's " + "environment (in #%d), so there is no telling where to " + "start. Nothing was written.\n", + first_written ? first_written : "?", mtd.env_dev); + ret = 1; + goto bailout; + } + snprintf(mtdparts, sizeof(mtdparts), "%s:", id); + for (int p = 0; p < keep; p++) { + add_mtdpart(mtdparts, mtd.part[p].name, mtd.part[p].size); + curr_mtd_part++; + goff += mtd.part[p].size; + printf("Keeping %s (%zu KB)\n", mtd.part[p].name, + mtd.part[p].size / 1024); + } + } int root_part = -1; int kernel_part = -1; @@ -1358,6 +1511,12 @@ static int do_upgrade(const char *filename, bool force) { return 1; } payload = jpayload->valueint; + if (payload > len - (read_ptr - jsond)) { + fprintf(stderr, "The bundle ends before '%s' does\n", + mtdwrite[i].name); + ret = 1; + goto bailout; + } mtdwrite[i].size = ceil_up(payload, psize ? psize : mtd.erasesize); mtdwrite[i].data = malloc(mtdwrite[i].size); if (mtdwrite[i].data == NULL) { @@ -1425,7 +1584,7 @@ static int do_upgrade(const char *filename, bool force) { i++; } - free(jsond); + munmap(jsond, len); if (root_part == -1 || kernel_part == -1) { fprintf(stderr, @@ -1436,6 +1595,104 @@ static int do_upgrade(const char *filename, bool force) { snprintf(mtdparts + strlen(mtdparts), MAX_MTDPARTS - strlen(mtdparts), ",-(rootfs_data)"); + carry_t carry = {.erasesize = mtd.erasesize}; + char new_bootargs[1024], new_bootcmd[1024]; + if (keep_front) { + if ((ssize_t)goff >= mtd.totalsz) { + fprintf(stderr, + "The images end at %#x, the flash at %#zx: no room for " + "the settings. Nothing was written.\n", + goff, (size_t)mtd.totalsz); + ret = 1; + goto bailout; + } + // The new settings partition is written whole, carried or not: + // what lies there now is the middle of the old one, and jffs2 would + // mount its stale nodes as files + carry.region_len = mtd.totalsz - goff; + carry.region = malloc(carry.region_len); + if (!carry.region) { + fprintf(stderr, "Allocation error\n"); + ret = 1; + goto bailout; + } + memset(carry.region, 0xff, carry.region_len); + int data_part = -1; + for (int p = 0; p < MAX_MTDBLOCKS && data_part < 0; p++) + if (!strcmp(mtd.part[p].name, "rootfs_data")) + data_part = p; + if (wipe_settings) + printf("Settings will not be carried over\n"); + else if (data_part < 0) + printf("This flash has no settings partition; the new one " + "starts empty\n"); + else if (!jffs2_mountpoint(data_part, "rootfs_data", carry.dir, + sizeof(carry.dir))) { + // Starting empty would lose the settings without a word if + // they are only mounted in a way this does not recognise + fprintf(stderr, + "rootfs_data (mtd%d) is not mounted as jffs2, so the " + "settings cannot be read to carry over. Nothing was " + "written; --wipe-settings upgrades without them.\n", + data_part); + ret = 5; + goto bailout; + } else if (!build_carry(&carry)) { + // A trial now, so a refusal comes while nothing is stopped. The + // real image is taken once the partition is read-only. + fprintf(stderr, "Nothing was written; --wipe-settings upgrades " + "without them.\n"); + ret = 5; + goto bailout; + } + strcpy(mtdwrite[i].name, "rootfs_data"); + mtdwrite[i].off_flashb = goff; + mtdwrite[i].size = carry.region_len; + mtdwrite[i].data = carry.region; + i++; + + if (!upgrade_bootargs(uboot_env_get_param("bootargs"), root_part, + mtdparts, new_bootargs, sizeof(new_bootargs)) || + !upgrade_bootcmd(uboot_env_get_param("bootcmd"), + mtdwrite[kernel_part].off_flashb, + mtdwrite[kernel_part].size, new_bootcmd, + sizeof(new_bootcmd))) { + fprintf(stderr, + "Cannot find root= and mtdparts= in bootargs, or the one " + "kernel read (sf read) in bootcmd, to edit. Nothing was " + "written.\n"); + ret = 1; + goto bailout; + } + printf("bootargs will be: %s\nbootcmd will be: %s\n", new_bootargs, + new_bootcmd); + } + + // The bundle's variables, checked while nothing is stopped. bootargs and + // bootcmd are what upgrade itself writes for the new layout: a bundle + // value would be reported as set and then replaced. + const cJSON *jenv = cJSON_GetObjectItemCaseSensitive(json, "env"); + const cJSON *jvar; + if (jenv && !cJSON_IsObject(jenv)) { + fprintf(stderr, "The bundle's env is not an object. Nothing was " + "written.\n"); + ret = 2; + goto bailout; + } + cJSON_ArrayForEach(jvar, jenv) { + if (!cJSON_IsString(jvar) || !strcmp(jvar->string, "bootargs") || + !strcmp(jvar->string, "bootcmd")) { + fprintf(stderr, + "The bundle sets '%s', which upgrade %s. Nothing was " + "written.\n", + jvar->string, + cJSON_IsString(jvar) ? "writes itself for the new layout" + : "can only set to a string"); + ret = 2; + goto bailout; + } + } + if (!do_flash("Analyzing", mtdwrite, &mtd, false, true)) { printf("Early exit, check the logs\n"); ret = 4; @@ -1445,11 +1702,24 @@ static int do_upgrade(const char *filename, bool force) { ret = 4; goto bailout; } - if (!isolate_from_flash()) + if (!isolate_from_flash(*carry.dir ? build_carry : NULL, &carry)) reboot_after_failure("Preparing the flash"); if (!do_flash("Upgrading", mtdwrite, &mtd, false, false)) reboot_after_failure("Upgrading"); + // Variables the bundle sets, e.g. the address this firmware upgrades from + cJSON_ArrayForEach(jvar, jenv) { + set_env_param_ram(jvar->string, jvar->valuestring); + printf("set %s=%s\n", jvar->string, jvar->valuestring); + } + + if (keep_front) { + set_env_param_ram("bootcmd", new_bootcmd); + set_env_param_rom("bootargs", new_bootargs, mtd.env_dev, + mtd.env_offset, mtd.erasesize); + reboot_with_msg(); + } + char value[1024]; cJSON *josmem = cJSON_GetObjectItemCaseSensitive(json, "osmem"); @@ -1500,10 +1770,12 @@ int upgrade_restore_cmd(int argc, char **argv) { const struct option long_options[] = { {"force", no_argument, NULL, 'f'}, {"skip-env", no_argument, NULL, 's'}, + {"wipe-settings", no_argument, NULL, 'w'}, {NULL, 0, NULL, 0}, }; bool skip_env = false; bool force = false; + bool wipe_settings = false; int res; int option_index; @@ -1516,6 +1788,9 @@ int upgrade_restore_cmd(int argc, char **argv) { case 'f': force = true; break; + case 'w': + wipe_settings = true; + break; case '?': print_usage(); @@ -1529,5 +1804,5 @@ int upgrade_restore_cmd(int argc, char **argv) { if (restore_mode) return restore_backup(optarg, skip_env, force); else - return do_upgrade(optarg, force); + return do_upgrade(optarg, force, wipe_settings); } diff --git a/src/jffs2.c b/src/jffs2.c new file mode 100644 index 0000000..9988886 --- /dev/null +++ b/src/jffs2.c @@ -0,0 +1,328 @@ +#include "jffs2.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define JFFS2_MAGIC 0x1985 +#define NODE_DIRENT 0xe001 +#define NODE_INODE 0xe002 +#define NODE_CLEANMARKER 0x2003 +#define NODE_PADDING 0x2004 +#define COMPR_NONE 0x00 +#define HDR_LEN 12 +#define INODE_LEN 68 +#define DIRENT_LEN 40 +#define PAGE 4096 + +struct jffs2_img { + uint8_t *buf; + size_t len, cap, limit, erasesize; + uint32_t next_ino, version; + int entries; + char err[160]; +}; + +/* The kernel's crc32(0, ...): reflected 0xEDB88320, no inversion in or out, + * which is not what zlib's crc32 computes. */ +static uint32_t jcrc(const void *data, size_t len) { + static uint32_t table[256]; + if (!table[1]) + for (uint32_t i = 0; i < 256; i++) { + uint32_t r = i; + for (int j = 0; j < 8; j++) + r = (r & 1) ? (r >> 1) ^ 0xEDB88320 : r >> 1; + table[i] = r; + } + uint32_t crc = 0; + const uint8_t *p = data; + while (len--) + crc = table[(crc ^ *p++) & 0xff] ^ (crc >> 8); + return crc; +} + +static void put16(uint8_t *p, uint16_t v) { + p[0] = v; + p[1] = v >> 8; +} + +static void put32(uint8_t *p, uint32_t v) { + p[0] = v; + p[1] = v >> 8; + p[2] = v >> 16; + p[3] = v >> 24; +} + +static bool fail(jffs2_img_t *img, const char *fmt, ...) { + if (!*img->err) { + va_list ap; + va_start(ap, fmt); + vsnprintf(img->err, sizeof(img->err), fmt, ap); + va_end(ap); + } + return false; +} + +static bool reserve(jffs2_img_t *img, size_t more) { + if (img->len + more > img->limit) + return fail(img, "the settings need more than the %zu KB there is " + "room for", + img->limit / 1024); + if (img->len + more <= img->cap) + return true; + size_t cap = img->cap ? img->cap : img->erasesize; + while (cap < img->len + more) + cap *= 2; + uint8_t *b = realloc(img->buf, cap); + if (!b) + return fail(img, "out of memory building the settings image"); + memset(b + img->cap, 0xff, cap - img->cap); + img->buf = b; + img->cap = cap; + return true; +} + +static void header(uint8_t *p, uint16_t type, uint32_t totlen) { + put16(p, JFFS2_MAGIC); + put16(p + 2, type); + put32(p + 4, totlen); + put32(p + 8, jcrc(p, 8)); +} + +/* Room for a node of `len` bytes in the current erase block: the rest of a + * block that cannot take it becomes a padding node, and every block starts + * with a cleanmarker. */ +static bool place(jffs2_img_t *img, size_t len) { + size_t in_block = img->len % img->erasesize; + if (in_block && in_block + len > img->erasesize) { + size_t rest = img->erasesize - in_block; + if (!reserve(img, rest)) + return false; + if (rest >= HDR_LEN) + header(img->buf + img->len, NODE_PADDING, rest); + img->len += rest; + in_block = 0; + } + if (!in_block) { + if (!reserve(img, HDR_LEN)) + return false; + header(img->buf + img->len, NODE_CLEANMARKER, HDR_LEN); + img->len += HDR_LEN; + } + return reserve(img, (len + 3) & ~3u); +} + +static bool emit_inode(jffs2_img_t *img, uint32_t ino, const struct stat *st, + uint32_t isize, uint32_t offset, const void *data, + uint32_t dlen) { + if (!place(img, INODE_LEN + dlen)) + return false; + uint8_t *p = img->buf + img->len; + header(p, NODE_INODE, INODE_LEN + dlen); + put32(p + 12, ino); + put32(p + 16, ++img->version); + put32(p + 20, st->st_mode); + put16(p + 24, st->st_uid); + put16(p + 26, st->st_gid); + put32(p + 28, isize); + put32(p + 32, st->st_mtime); // atime: the camera does not keep it + put32(p + 36, st->st_mtime); + put32(p + 40, st->st_ctime); + put32(p + 44, offset); + put32(p + 48, dlen); // csize + put32(p + 52, dlen); // dsize + p[56] = COMPR_NONE; + p[57] = COMPR_NONE; + put16(p + 58, 0); + if (dlen) + memcpy(p + INODE_LEN, data, dlen); + put32(p + 60, jcrc(p + INODE_LEN, dlen)); + put32(p + 64, jcrc(p, INODE_LEN - 8)); + img->len += (INODE_LEN + dlen + 3) & ~3u; + return true; +} + +static uint8_t dirent_type(mode_t m) { + // DT_* values, as the kernel's jffs2 stores them + return S_ISDIR(m) ? 4 + : S_ISREG(m) ? 8 + : S_ISLNK(m) ? 10 + : S_ISCHR(m) ? 2 + : S_ISBLK(m) ? 6 + : S_ISFIFO(m) ? 1 + : S_ISSOCK(m) ? 12 + : 0; +} + +static bool emit_dirent(jffs2_img_t *img, uint32_t pino, uint32_t ino, + const char *name, mode_t mode, time_t mtime) { + size_t n = strlen(name); + if (n > 254) + return fail(img, "'%s' is too long a name for jffs2", name); + if (!place(img, DIRENT_LEN + n)) + return false; + uint8_t *p = img->buf + img->len; + header(p, NODE_DIRENT, DIRENT_LEN + n); + put32(p + 12, pino); + put32(p + 16, ++img->version); + put32(p + 20, ino); + put32(p + 24, mtime); + p[28] = n; + p[29] = dirent_type(mode); + p[30] = p[31] = 0; + put32(p + 32, jcrc(p, DIRENT_LEN - 8)); + put32(p + 36, jcrc(name, n)); + memcpy(p + DIRENT_LEN, name, n); + img->len += (DIRENT_LEN + n + 3) & ~3u; + return true; +} + +static bool add_file(jffs2_img_t *img, uint32_t ino, const char *path, + const struct stat *st) { + int fd = open(path, O_RDONLY); + if (fd < 0) + return fail(img, "cannot read %s: %s", path, strerror(errno)); + uint8_t page[PAGE]; + uint32_t off = 0; + bool ok = true; + for (;;) { + ssize_t n = read(fd, page, sizeof(page)); + if (n < 0) { + ok = fail(img, "cannot read %s: %s", path, strerror(errno)); + break; + } + if (!n) + break; + if (!(ok = emit_inode(img, ino, st, st->st_size, off, page, n))) + break; + off += n; + } + close(fd); + // An empty file is still an inode + if (ok && !off) + ok = emit_inode(img, ino, st, 0, 0, NULL, 0); + return ok; +} + +/* This image holds no extended attributes, and dropping one is not safe to + * do quietly: overlayfs marks a directory that hides its lower layer with + * one (trusted.overlay.opaque), and without it files deleted on purpose + * come back. So an entry that has any is refused. */ +static bool no_xattrs(jffs2_img_t *img, const char *path) { + ssize_t n = llistxattr(path, NULL, 0); + if (n > 0) + return fail(img, "%s has extended attributes, which cannot be carried", + path); + return true; +} + +static bool add_dir(jffs2_img_t *img, uint32_t ino, const char *path, + const char *rel, const char *skip) { + DIR *d = opendir(path); + if (!d) + return fail(img, "cannot read %s: %s", path, strerror(errno)); + bool ok = true; + struct dirent *e; + while (ok && (e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) + continue; + char child[4096], crel[4096]; + snprintf(child, sizeof(child), "%s/%s", path, e->d_name); + snprintf(crel, sizeof(crel), "%s%s%s", rel, *rel ? "/" : "", + e->d_name); + struct stat st; + if (lstat(child, &st)) { + ok = fail(img, "cannot stat %s: %s", child, strerror(errno)); + break; + } + if (!(ok = no_xattrs(img, child))) + break; + uint32_t cino = img->next_ino++; + img->entries++; + if (!(ok = emit_dirent(img, ino, cino, e->d_name, st.st_mode, + st.st_mtime))) + break; + if (S_ISDIR(st.st_mode)) { + ok = emit_inode(img, cino, &st, 0, 0, NULL, 0); + if (ok && !(skip && !strcmp(crel, skip))) + ok = add_dir(img, cino, child, crel, skip); + } else if (S_ISREG(st.st_mode)) { + ok = add_file(img, cino, child, &st); + } else if (S_ISLNK(st.st_mode)) { + char target[4096]; + ssize_t n = readlink(child, target, sizeof(target)); + if (n < 0) + ok = fail(img, "cannot read link %s: %s", child, + strerror(errno)); + else + ok = emit_inode(img, cino, &st, n, 0, target, n); + } else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) { + // The kernel's old 16-bit encoding where it fits, which is what + // it reads back by length; overlayfs's whiteout is 0:0 + unsigned maj = major(st.st_rdev), min = minor(st.st_rdev); + uint8_t dev[4]; + if (maj < 256 && min < 256) { + put16(dev, (maj << 8) | min); + ok = emit_inode(img, cino, &st, 2, 0, dev, 2); + } else { + put32(dev, (min & 0xff) | (maj << 8) | ((min & ~0xffu) << 12)); + ok = emit_inode(img, cino, &st, 4, 0, dev, 4); + } + } else { + ok = emit_inode(img, cino, &st, 0, 0, NULL, 0); + } + } + closedir(d); + return ok; +} + +jffs2_img_t *jffs2_new(size_t erasesize, size_t limit) { + jffs2_img_t *img = calloc(1, sizeof(*img)); + if (!img) + return NULL; + img->erasesize = erasesize; + img->limit = limit; + img->next_ino = 2; // 1 is the root + return img; +} + +void jffs2_free(jffs2_img_t *img) { + if (img) { + free(img->buf); + free(img); + } +} + +bool jffs2_add_tree(jffs2_img_t *img, const char *path, + const char *skip_contents) { + struct stat st; + if (lstat(path, &st) || !S_ISDIR(st.st_mode)) + return fail(img, "%s is not a directory", path); + // The root's own inode, so its mode and owner are the ones it had + return no_xattrs(img, path) && emit_inode(img, 1, &st, 0, 0, NULL, 0) && + add_dir(img, 1, path, "", skip_contents); +} + +const uint8_t *jffs2_data(jffs2_img_t *img, size_t *len) { + size_t whole = (img->len + img->erasesize - 1) / img->erasesize * + img->erasesize; + if (whole > img->len && reserve(img, whole - img->len)) { + // The tail of the last block stays erased: free space to jffs2 + img->len = whole; + } + *len = img->len; + return img->buf; +} + +const char *jffs2_error(jffs2_img_t *img) { return img->err; } + +int jffs2_entries(jffs2_img_t *img) { return img->entries; } diff --git a/src/jffs2.h b/src/jffs2.h new file mode 100644 index 0000000..5faf9a6 --- /dev/null +++ b/src/jffs2.h @@ -0,0 +1,42 @@ +#ifndef JFFS2_H +#define JFFS2_H + +#include +#include +#include + +/* + * A JFFS2 image builder, for `ipctool upgrade` to carry a camera's settings + * across a repartition. The settings live on a jffs2 overlay; when the layout + * changes, the new rootfs_data starts somewhere else and the running kernel + * has no partition there to mount, so the files are written as a ready-made + * filesystem instead. + * + * What mkfs.jffs2 makes, minus everything optional: little-endian, nodes + * uncompressed, data in page-sized pieces, a cleanmarker at the start of + * every erase block, a padding node where the next one would cross into the + * next block. No hard links (a second name is a second copy), and no + * extended attributes: a tree with any is refused, since the one overlayfs + * sets hides deleted files and dropping it brings them back. + */ + +typedef struct jffs2_img jffs2_img_t; + +/* An image of erase blocks of `erasesize` that may grow to `limit` bytes. */ +jffs2_img_t *jffs2_new(size_t erasesize, size_t limit); +void jffs2_free(jffs2_img_t *img); + +/* Every entry under the directory `path`, recursively, as the root of the + * image. A directory named in `skip_contents` (relative to `path`, e.g. + * "work") is created empty. False on a read error or when the image would + * pass its limit; the reason is in jffs2_error(). */ +bool jffs2_add_tree(jffs2_img_t *img, const char *path, + const char *skip_contents); + +/* The image so far, padded to whole erase blocks. */ +const uint8_t *jffs2_data(jffs2_img_t *img, size_t *len); +const char *jffs2_error(jffs2_img_t *img); +/* How many files, directories, links and device nodes it holds. */ +int jffs2_entries(jffs2_img_t *img); + +#endif /* JFFS2_H */ diff --git a/src/jffs2_test.c b/src/jffs2_test.c new file mode 100644 index 0000000..9c103b1 --- /dev/null +++ b/src/jffs2_test.c @@ -0,0 +1,237 @@ +/* jffs2_test: the image `ipctool upgrade` carries a camera's settings in. + * + * Builds a tree like an OpenIPC overlay, makes an image of it, and reads the + * image back node by node: every CRC the kernel checks, every name, and every + * file's bytes reassembled from its pages. With an output path it also writes + * the image there, for mtd-utils' jffs2reader or a kernel to have the final + * word. */ +#include +#include +#include +#include +#include +#include +#include + +#include "jffs2.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 ERASE 0x10000 + +static uint32_t get16(const uint8_t *p) { return p[0] | p[1] << 8; } +static uint32_t get32(const uint8_t *p) { + return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; +} + +static uint32_t jcrc(const void *data, size_t len) { + uint32_t crc = 0; + const uint8_t *p = data; + while (len--) { + crc ^= *p++; + for (int j = 0; j < 8; j++) + crc = (crc & 1) ? (crc >> 1) ^ 0xEDB88320 : crc >> 1; + } + return crc; +} + +/* What the image says, rebuilt: names by (parent, name), and file contents + * by inode. */ +#define MAXI 64 +static struct { + uint32_t pino, ino; + char name[256]; +} dents[MAXI]; +static int ndents; +static struct { + uint32_t mode, isize; + uint8_t data[20000]; + int seen; +} inodes[MAXI]; + +static void parse(const uint8_t *img, size_t len) { + for (size_t blk = 0; blk < len; blk += ERASE) { + size_t off = blk; + while (off + 12 <= blk + ERASE) { + const uint8_t *p = img + off; + if (get16(p) == 0xffff) { + // Erased to the end of the block + for (size_t i = off; i < blk + ERASE; i++) + CHECK(img[i] == 0xff, "data after free space at %#zx", i); + break; + } + CHECK(get16(p) == 0x1985, "bad magic at %#zx", off); + if (get16(p) != 0x1985) + return; + uint32_t type = get16(p + 2), totlen = get32(p + 4); + CHECK(get32(p + 8) == jcrc(p, 8), "header crc at %#zx", off); + CHECK(off + totlen <= blk + ERASE, "node crosses a block at %#zx", + off); + if (off == blk) + CHECK(type == 0x2003 && totlen == 12, + "block %#zx starts without a cleanmarker", blk); + if (type == 0xe001) { + CHECK(get32(p + 32) == jcrc(p, 32), "dirent crc at %#zx", off); + uint8_t n = p[28]; + CHECK(get32(p + 36) == jcrc(p + 40, n), "name crc at %#zx", + off); + if (ndents < MAXI) { + dents[ndents].pino = get32(p + 12); + dents[ndents].ino = get32(p + 20); + memcpy(dents[ndents].name, p + 40, n); + dents[ndents].name[n] = 0; + ndents++; + } + } else if (type == 0xe002) { + CHECK(get32(p + 64) == jcrc(p, 60), "inode crc at %#zx", off); + uint32_t ino = get32(p + 12), doff = get32(p + 44), + dlen = get32(p + 48); + CHECK(get32(p + 60) == jcrc(p + 68, dlen), "data crc at %#zx", + off); + CHECK(p[56] == 0 && get32(p + 52) == dlen, + "compressed or sized oddly at %#zx", off); + if (ino < MAXI && doff + dlen <= sizeof(inodes[0].data)) { + inodes[ino].mode = get32(p + 20); + inodes[ino].isize = get32(p + 28); + memcpy(inodes[ino].data + doff, p + 68, dlen); + inodes[ino].seen = 1; + } + } else + CHECK(type == 0x2003 || type == 0x2004, + "unknown node %#x at %#zx", type, off); + off += (totlen + 3) & ~3u; + } + } +} + +static uint32_t lookup(uint32_t pino, const char *name) { + for (int i = 0; i < ndents; i++) + if (dents[i].pino == pino && !strcmp(dents[i].name, name)) + return dents[i].ino; + return 0; +} + +static void writefile(const char *path, const void *data, size_t len) { + FILE *f = fopen(path, "wb"); + if (f) { + fwrite(data, 1, len, f); + fclose(f); + } +} + +int main(int argc, char **argv) { + char root[] = "/tmp/jffs2_test.XXXXXX"; + if (!mkdtemp(root)) { + perror("mkdtemp"); + return 1; + } + char path[512]; + // The shape of an OpenIPC overlay: root/ with the settings, work/ that + // overlayfs keeps for itself + snprintf(path, sizeof(path), "%s/root", root); + mkdir(path, 0755); + snprintf(path, sizeof(path), "%s/root/etc", root); + mkdir(path, 0755); + snprintf(path, sizeof(path), "%s/work", root); + mkdir(path, 0755); + snprintf(path, sizeof(path), "%s/work/work", root); + mkdir(path, 0700); + snprintf(path, sizeof(path), "%s/work/work/leftover", root); + writefile(path, "x", 1); + + // Bigger than a page, so it is several nodes + static uint8_t big[10000]; + for (size_t i = 0; i < sizeof(big); i++) + big[i] = i * 7; + snprintf(path, sizeof(path), "%s/root/etc/majestic.yaml", root); + writefile(path, big, sizeof(big)); + snprintf(path, sizeof(path), "%s/root/etc/shadow", root); + writefile(path, "root:$1$x:1:0:99999:7:::\n", 25); + chmod(path, 0600); + snprintf(path, sizeof(path), "%s/root/etc/custom.ok", root); + writefile(path, "", 0); + snprintf(path, sizeof(path), "%s/root/etc/default.ini", root); + if (symlink("imx335.ini", path)) + perror("symlink"); + // An overlayfs whiteout, where the test may make one + snprintf(path, sizeof(path), "%s/root/etc/inittab.removed", root); + bool whiteout = !mknod(path, S_IFCHR | 0, makedev(0, 0)); + + jffs2_img_t *img = jffs2_new(ERASE, 0x2b0000); + CHECK(jffs2_add_tree(img, root, "work"), "add_tree: %s", + jffs2_error(img)); + size_t len; + const uint8_t *data = jffs2_data(img, &len); + CHECK(len && len % ERASE == 0, "image of %zu bytes", len); + parse(data, len); + + uint32_t r = lookup(1, "root"), w = lookup(1, "work"); + CHECK(r && w, "root/ or work/ missing"); + // overlayfs makes work/work again as it mounts + CHECK(!lookup(w, "work"), "work/ should be carried empty"); + uint32_t etc = lookup(r, "etc"); + uint32_t y = lookup(etc, "majestic.yaml"); + CHECK(y && inodes[y].isize == sizeof(big) && + !memcmp(inodes[y].data, big, sizeof(big)), + "majestic.yaml did not come back whole"); + uint32_t s = lookup(etc, "shadow"); + CHECK(s && (inodes[s].mode & 07777) == 0600 && inodes[s].isize == 25 && + !memcmp(inodes[s].data, "root:$1$x", 9), + "shadow lost its bytes or its mode"); + uint32_t c = lookup(etc, "custom.ok"); + CHECK(c && inodes[c].seen && inodes[c].isize == 0, "empty file missing"); + uint32_t l = lookup(etc, "default.ini"); + CHECK(l && S_ISLNK(inodes[l].mode) && inodes[l].isize == 10 && + !memcmp(inodes[l].data, "imx335.ini", 10), + "symlink wrong"); + if (whiteout) { + uint32_t wo = lookup(etc, "inittab.removed"); + CHECK(wo && S_ISCHR(inodes[wo].mode) && inodes[wo].isize == 2 && + get16(inodes[wo].data) == 0, + "whiteout lost"); + } else + printf("(no whiteout: mknod needs root here)\n"); + CHECK(inodes[1].seen && S_ISDIR(inodes[1].mode), "no root inode"); + + // A limit it cannot keep is refused, not truncated + jffs2_img_t *tiny = jffs2_new(ERASE, ERASE / 8); + CHECK(!jffs2_add_tree(tiny, root, "work") && *jffs2_error(tiny), + "an image past its limit was accepted"); + jffs2_free(tiny); + + // An extended attribute is refused, never dropped: overlayfs hides + // deleted files behind one. user.* stands in for trusted.overlay.*, + // which needs root to set. + snprintf(path, sizeof(path), "%s/root/etc", root); + if (!setxattr(path, "user.test", "y", 1, 0)) { + jffs2_img_t *x = jffs2_new(ERASE, 0x2b0000); + CHECK(!jffs2_add_tree(x, root, "work") && + strstr(jffs2_error(x), "extended attributes"), + "a directory with an xattr was carried without it"); + jffs2_free(x); + removexattr(path, "user.test"); + } else + printf("(no xattr case: this filesystem refuses user.*)\n"); + + if (argc > 1) + writefile(argv[1], data, len); + jffs2_free(img); + + char cmd[600]; + snprintf(cmd, sizeof(cmd), "rm -rf %s", root); + if (system(cmd)) + fprintf(stderr, "could not remove %s\n", root); + + if (!failed) + puts("jffs2_test: OK"); + return failed; +} diff --git a/src/main.c b/src/main.c index 6c05360..3f703e7 100644 --- a/src/main.c +++ b/src/main.c @@ -93,6 +93,7 @@ void print_usage() { " (experimental! use only on cameras with " "UART)\n" " [-f, --force] enforce\n" + " [--wipe-settings] do not carry the settings over\n" " nand-migrate [--dry-run] [--wipe-settings] \n" " move a NAND camera off the retired\n" " layout with a `kernel` UBI volume\n" diff --git a/src/uboot.c b/src/uboot.c index d125ee6..2b464a2 100644 --- a/src/uboot.c +++ b/src/uboot.c @@ -137,7 +137,8 @@ static void uboot_setenv_cb(int mtd, uint32_t offset, const char *env, char *nptr = newenv + CRC_SZ; bool found = false; while (*ptr) { - if (!strncmp(ptr, key, strlen(key))) { + // The whole name: "bootcmd" is not "bootcmdnor" + if (!strncmp(ptr, key, strlen(key)) && ptr[strlen(key)] == '=') { found = true; char *delim = strchr(ptr, '='); if (!delim) { diff --git a/src/upgrade_env.c b/src/upgrade_env.c new file mode 100644 index 0000000..ee134bc --- /dev/null +++ b/src/upgrade_env.c @@ -0,0 +1,91 @@ +#include "upgrade_env.h" + +#include +#include +#include + +/* The word of `s` that starts with `key` (at the start or after a space), + * or NULL. */ +static const char *find_word(const char *s, const char *key) { + for (const char *p = s; (p = strstr(p, key)); p++) + if (p == s || p[-1] == ' ') + return p; + return NULL; +} + +static size_t word_len(const char *w) { return strcspn(w, " "); } + +bool upgrade_mtd_id(const char *bootargs, char *id, size_t idsz) { + const char *w = bootargs ? find_word(bootargs, "mtdparts=") : NULL; + if (!w) + return false; + w += strlen("mtdparts="); + size_t n = strcspn(w, ": "); + if (!n || w[n] != ':' || memchr(w, '$', n) || n >= idsz) + return false; + memcpy(id, w, n); + id[n] = '\0'; + return true; +} + +bool upgrade_bootargs(const char *old, int root_idx, const char *mtdparts, + char *out, size_t outsz) { + char id[64]; + if (!old || !upgrade_mtd_id(old, id, sizeof(id)) || + !find_word(old, "root=")) + return false; + + size_t used = 0; + const char *p = old; + while (*p) { + if (*p == ' ') { + if (used + 1 >= outsz) + return false; + out[used++] = *p++; + continue; + } + size_t n = word_len(p); + int w; + if (!strncmp(p, "root=", 5)) + w = snprintf(out + used, outsz - used, "root=/dev/mtdblock%d", + root_idx); + else if (!strncmp(p, "mtdparts=", 9)) + w = snprintf(out + used, outsz - used, "mtdparts=%s", mtdparts); + else + w = snprintf(out + used, outsz - used, "%.*s", (int)n, p); + if (w < 0 || (size_t)w >= outsz - used) + return false; + used += w; + p += n; + } + out[used] = '\0'; + return true; +} + +bool upgrade_bootcmd(const char *old, uint32_t off, uint32_t size, char *out, + size_t outsz) { + if (!old) + return false; + const char *rd = strstr(old, "sf read "); + if (!rd || strstr(rd + 1, "sf read ")) + return false; + + const char *p = rd + strlen("sf read "); + char *end; + strtoul(p, &end, 16); // the load address stays + if (end == p || *end != ' ') + return false; + const char *addr_end = end; + p = end + 1; + strtoul(p, &end, 16); + if (end == p || *end != ' ') + return false; + p = end + 1; + strtoul(p, &end, 16); + if (end == p) + return false; + + int w = snprintf(out, outsz, "%.*s 0x%x 0x%x%s", (int)(addr_end - old), + old, off, size, end); + return w >= 0 && (size_t)w < outsz; +} diff --git a/src/upgrade_env.h b/src/upgrade_env.h new file mode 100644 index 0000000..7a83895 --- /dev/null +++ b/src/upgrade_env.h @@ -0,0 +1,35 @@ +#ifndef UPGRADE_ENV_H +#define UPGRADE_ENV_H + +#include +#include +#include + +/* + * What `ipctool upgrade` changes in the U-Boot environment when it moves the + * partitions, done as an edit of what the camera already has rather than a + * fresh line: a vendor bootloader's bootargs carry things only it knows about + * (an mmz carve-out, a console, a panic timeout), and regenerating the line + * dropped them. Pure functions, so they are tested off a camera. + */ + +/* The mtd id in front of the partitions, "hi_sfc" in + * "... mtdparts=hi_sfc:256k(boot),...". False when bootargs has no literal + * mtdparts= (one that names a variable, "${mtdparts}", is not literal). */ +bool upgrade_mtd_id(const char *bootargs, char *id, size_t idsz); + +/* bootargs with root= pointing at /dev/mtdblock and mtdparts= + * replaced by `mtdparts` (id included), every other word kept in its place. + * False, leaving `out` unspecified, when either word is missing or the + * mtdparts= value is not literal: an edit that cannot find what it edits + * must not produce a line that boots the old layout. */ +bool upgrade_bootargs(const char *old, int root_idx, const char *mtdparts, + char *out, size_t outsz); + +/* bootcmd with the kernel read, "sf read ", reading + * `size` bytes from `off` instead. False when there is no such read, or more + * than one. */ +bool upgrade_bootcmd(const char *old, uint32_t off, uint32_t size, char *out, + size_t outsz); + +#endif /* UPGRADE_ENV_H */ diff --git a/src/upgrade_env_test.c b/src/upgrade_env_test.c new file mode 100644 index 0000000..271c9a9 --- /dev/null +++ b/src/upgrade_env_test.c @@ -0,0 +1,95 @@ +/* upgrade_env_test: the U-Boot variables `ipctool upgrade` edits. */ +#include +#include + +#include "upgrade_env.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) + +// A XiongMai 85H50AI (Hi3516EV300, 16 MB NOR) on OpenIPC lite: the vendor +// bootloader keeps the whole line literal, the partition after boot is "wtf" +// rather than "env", and the line carries an mmz carve-out the old upgrade +// path regenerated away. +static const char *XM_BOOTARGS = + "mem=128M console=ttyAMA0,115200 panic=20 rootfstype=squashfs " + "root=/dev/mtdblock3 init=/init " + "mtdparts=hi_sfc:256k(boot),64k(wtf),2048k(kernel),5120k(rootfs),-(rootfs_" + "data) mmz_allocator=cma mmz=anonymous,0,0x42000000,96M"; +static const char *XM_BOOTCMD = + "setenv setargs setenv bootargs ${bootargs}; run setargs; sf probe 0; sf " + "read 0x42000000 0x50000 0x200000; bootm 0x42000000"; + +int main(void) { + char id[64], out[1024]; + + CHECK(upgrade_mtd_id(XM_BOOTARGS, id, sizeof(id)), "no mtd id"); + CHECK(!strcmp(id, "hi_sfc"), "mtd id '%s'", id); + CHECK(!upgrade_mtd_id("mtdparts=${mtdparts} root=/dev/mtdblock3", id, + sizeof(id)), + "a variable taken for a literal mtdparts"); + CHECK(!upgrade_mtd_id("console=ttyAMA0 root=/dev/mtdblock3", id, + sizeof(id)), + "mtd id found where there is no mtdparts"); + CHECK(!upgrade_mtd_id("xmtdparts=hi_sfc:1k(a)", id, sizeof(id)), + "a word ending in mtdparts= taken for it"); + + // To the ultimate layout: kernel 3 MB, rootfs 10 MB, everything else as + // it was, in place. + CHECK(upgrade_bootargs(XM_BOOTARGS, 3, + "hi_sfc:256k(boot),64k(wtf),3072k(kernel),10240k(" + "rootfs),-(rootfs_data)", + out, sizeof(out)), + "bootargs refused"); + CHECK(!strcmp(out, + "mem=128M console=ttyAMA0,115200 panic=20 rootfstype=squashfs " + "root=/dev/mtdblock3 init=/init " + "mtdparts=hi_sfc:256k(boot),64k(wtf),3072k(kernel),10240k(" + "rootfs),-(rootfs_data) mmz_allocator=cma " + "mmz=anonymous,0,0x42000000,96M"), + "bootargs '%s'", out); + + CHECK(upgrade_bootargs("root=/dev/mtdblock3 mtdparts=sfc:1k(a)", 4, + "sfc:2k(a)", out, sizeof(out)) && + !strcmp(out, "root=/dev/mtdblock4 mtdparts=sfc:2k(a)"), + "root index not moved: '%s'", out); + CHECK(!upgrade_bootargs("console=ttyAMA0 mtdparts=sfc:1k(a)", 3, + "sfc:2k(a)", out, sizeof(out)), + "a line without root= edited"); + CHECK(!upgrade_bootargs("root=/dev/mtdblock3 mtdparts=${mtdparts}", 3, + "sfc:2k(a)", out, sizeof(out)), + "a variable mtdparts edited"); + CHECK(!upgrade_bootargs(XM_BOOTARGS, 3, "hi_sfc:1k(a)", out, 32), + "a line that does not fit returned as done"); + CHECK(!upgrade_bootargs(NULL, 3, "hi_sfc:1k(a)", out, sizeof(out)), + "no bootargs edited"); + + CHECK(upgrade_bootcmd(XM_BOOTCMD, 0x50000, 0x300000, out, sizeof(out)), + "bootcmd refused"); + CHECK(!strcmp(out, "setenv setargs setenv bootargs ${bootargs}; run " + "setargs; sf probe 0; sf read 0x42000000 0x50000 " + "0x300000; bootm 0x42000000"), + "bootcmd '%s'", out); + CHECK(!upgrade_bootcmd("bootm 0x42000000", 0x50000, 0x300000, out, + sizeof(out)), + "a bootcmd without a kernel read edited"); + CHECK(!upgrade_bootcmd("sf read 0x42000000 0x50000 0x200000; sf read " + "0x43000000 0x250000 0x100000", + 0x50000, 0x300000, out, sizeof(out)), + "two reads: which one is the kernel was guessed"); + CHECK(!upgrade_bootcmd("sf read 0x42000000 0x50000", 0x50000, 0x300000, + out, sizeof(out)), + "a read without a size edited"); + + if (!failed) + puts("upgrade_env_test: OK"); + return failed; +} diff --git a/tools/upgrade_bundle.py b/tools/upgrade_bundle.py index 30e797a..853360e 100755 --- a/tools/upgrade_bundle.py +++ b/tools/upgrade_bundle.py @@ -4,6 +4,7 @@ import argparse import os import hashlib +import tarfile template = { @@ -16,6 +17,57 @@ valid_parts = {'boot': 0x40000, 'kernel': 0x200000, 'rootfs': 0x500000} +# OpenIPC's NOR layouts. With one of these the bundle names no prefix and +# skips nothing: ipctool keeps whatever the camera has in front of its kernel +# (the bootloader and its environment, under this flash's own names), edits +# bootargs and bootcmd instead of replacing them, and carries the settings +# into the new rootfs_data. +layouts = { + 'nor8m': {'kernel': 0x200000, 'rootfs': 0x500000}, + 'nor16m': {'kernel': 0x300000, 'rootfs': 0xa00000}, +} + + +def from_firmware(tgz): + """The kernel and rootfs out of an OpenIPC release archive.""" + found = {} + with tarfile.open(tgz) as t: + for m in t.getmembers(): + base = os.path.basename(m.name) + if base.startswith('uImage.') and not base.endswith('.md5sum'): + found['kernel'] = t.extractfile(m).read() + elif base.startswith('rootfs.squashfs.') and \ + not base.endswith('.md5sum'): + found['rootfs'] = t.extractfile(m).read() + if set(found) != {'kernel', 'rootfs'}: + raise Exception(f'{tgz}: no uImage.* and rootfs.squashfs.* in it') + return found + + +def write_layout_bundle(layout, blobs, env, output): + sizes = layouts[layout] + bundle = {'partitions': []} + if env: + bundle['env'] = env + data = [] + for name in ('kernel', 'rootfs'): + content = blobs[name] + if len(content) > sizes[name]: + raise Exception(f'{name} is {len(content)} bytes, the {layout} ' + f'partition {sizes[name]}') + bundle['partitions'].append({ + 'name': name, + 'payloadSize': len(content), + 'partitionSize': sizes[name], + 'sha1': hashlib.sha1(content).hexdigest()[:8], + }) + data.append(content) + with open(output, mode='wb') as o: + o.write(json.dumps(bundle).encode('utf-8')) + o.write(b'\0') + for d in data: + o.write(d) + def add_additional(original, value): if original != '': @@ -81,7 +133,40 @@ def main(): help="add /init/init") parser.add_argument("-p", "--pack", action="store_true", help="pack partitions tightly") + parser.add_argument("-l", "--layout", choices=sorted(layouts), + help="move to this OpenIPC NOR layout, keeping the " + "bootloader and the settings") + parser.add_argument("-f", "--firmware", + help="OpenIPC release .tgz to take -k and -r from") + parser.add_argument("-e", "--env", action="append", default=[], + metavar="KEY=VALUE", + help="U-Boot variable to set (repeatable)") args = parser.parse_args() + if args.layout: + blobs = from_firmware(args.firmware) if args.firmware else {} + for name in ('kernel', 'rootfs'): + if getattr(args, name): + with open(getattr(args, name), 'rb') as f: + blobs[name] = f.read() + if set(blobs) != {'kernel', 'rootfs'}: + parser.error('--layout needs a kernel and a rootfs ' + '(-f, or -k and -r)') + if args.boot or args.cma or args.init or args.pack: + parser.error('--layout keeps the bootloader and its bootargs; ' + '-b, -c, -i and -p do not apply') + env = {} + for kv in args.env: + key, sep, value = kv.partition('=') + if not sep or not key: + parser.error(f'--env {kv}: expected KEY=VALUE') + if key in ('bootargs', 'bootcmd'): + parser.error(f'--env {key}: ipctool writes it for the new ' + 'layout itself') + env[key] = value + write_layout_bundle(args.layout, blobs, env, args.output) + return + if args.firmware or args.env: + parser.error('-f and -e go with --layout') write_bundle(**vars(args))