From fd696d38046b3568d7cf6dbc3ce6abec18962fb7 Mon Sep 17 00:00:00 2001 From: AI Dev Date: Wed, 14 Oct 2026 12:55:31 +0000 Subject: [PATCH 1/2] upgrade: move a NOR camera to another layout, keeping its bootloader and settings `upgrade` could already write a new layout over the running system, but on a lite-to-ultimate move (2 MB/5 MB to 3 MB/10 MB on 16 MB NOR) it would have bricked the camera it was most needed on: - The default bundle skipped partitions "boot" and "env" by name. A XiongMai flash calls the 64 KB after boot "wtf", the skip found nothing and said nothing, and the kernel went over it. - bootargs and bootcmd were regenerated, which drops what a vendor bootloader put there (an mmz carve-out, init=). - rootfs_data was never touched. After a repartition it starts in the middle of the old one, and jffs2 mounts the stale nodes. A bundle that names no prefix, offset or skip now: - keeps every partition in front of its first one (the kernel), as this flash names and sizes them. A XiongMai bootloader keeps its environment at 0x30000 inside "boot"; keeping only up to the environment wrote the kernel over "wtf" at 0x40000. - edits root=, mtdparts= and the kernel read's size in place (upgrade_env.c), and refuses before anything is stopped when it cannot find them. - writes the whole new rootfs_data. The settings go there as a jffs2 image (jffs2.c), taken once the old overlay is read-only and tried beforehand, so settings that will not fit are refused while nothing is stopped. --wipe-settings starts from defaults. - sets the U-Boot variables in the bundle's "env" object. Also: - A skip naming a partition the flash does not have is refused, not ignored. - The bundle is mapped, not read: in /tmp it would sit in RAM twice. - setenv matches the whole variable name: "bootcmd" no longer edits "bootcmdnor". - tools/upgrade_bundle.py --layout nor8m|nor16m builds such a bundle from a release .tgz. Tested on QEMU (hi3516ev300) with a full flash dump of a lab XiongMai 85H50AI on lite. One run of `ipctool upgrade` moved it to the 85H50AI ultimate image: u-boot read 3 MB at 0x50000, root on mtdblock3, ssh still took the old password, and every overlay file was byte-identical except the two rewritten on each boot (fake-hwclock, seedrng). boot and wtf were byte-identical. An oversize rootfs and 3 MB of settings were both refused with services running and the flash unchanged. --wipe-settings came up fresh: the customizer ran, then rebooted. --- .github/workflows/pr-build-check.yml | 5 +- CMakeLists.txt | 11 + README.md | 36 +++ src/backup.c | 264 +++++++++++++++++++-- src/jffs2.c | 331 +++++++++++++++++++++++++++ src/jffs2.h | 41 ++++ src/jffs2_test.c | 222 ++++++++++++++++++ src/main.c | 1 + src/uboot.c | 3 +- src/upgrade_env.c | 91 ++++++++ src/upgrade_env.h | 35 +++ src/upgrade_env_test.c | 95 ++++++++ tools/upgrade_bundle.py | 82 +++++++ 13 files changed, 1199 insertions(+), 18 deletions(-) create mode 100644 src/jffs2.c create mode 100644 src/jffs2.h create mode 100644 src/jffs2_test.c create mode 100644 src/upgrade_env.c create mode 100644 src/upgrade_env.h create mode 100644 src/upgrade_env_test.c diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 6b588a53..2acdafa3 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 19dc363b..e0cffe69 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 f606ef4f..5f0e153e 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 f34e07b9..d4f7054a 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,66 @@ 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` is mounted as jffs2, if it is +static bool jffs2_mountpoint(int num, char *path, size_t sz) { + FILE *fp = fopen("/proc/mounts", "r"); + if (!fp) + return false; + char want[32], line[256]; + snprintf(want, sizeof(want), "/dev/mtdblock%d", num); + 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(dev, want) && !strcmp(fs, "jffs2")) { + 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. +static bool build_carry(void *arg) { + carry_t *c = arg; + jffs2_img_t *img = jffs2_new(c->erasesize, c->region_len); + bool ok = img && jffs2_add_tree(img, c->dir, "work"); + if (ok) { + size_t len; + const uint8_t *data = jffs2_data(img, &len); + int entries, xattrs; + jffs2_counts(img, &entries, &xattrs); + memset(c->region, 0xff, c->region_len); + memcpy(c->region, data, len); + printf("Settings: %d entries in %zu of %zu KB\n", entries, len / 1024, + c->region_len / 1024); + if (xattrs) + printf(" %d extended attributes left behind (an overlayfs " + "opaque directory shows its lower layer again)\n", + xattrs); + } 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 +1309,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 +1382,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 +1413,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 +1499,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 +1572,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 +1583,69 @@ 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 || + !jffs2_mountpoint(data_part, carry.dir, sizeof(carry.dir))) + printf("No jffs2 settings partition is mounted; the new one " + "starts empty\n"); + 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); + } + if (!do_flash("Analyzing", mtdwrite, &mtd, false, true)) { printf("Early exit, check the logs\n"); ret = 4; @@ -1445,11 +1655,28 @@ 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 + const cJSON *jenv = cJSON_GetObjectItemCaseSensitive(json, "env"); + const cJSON *jvar; + cJSON_ArrayForEach(jvar, jenv) { + if (cJSON_IsString(jvar)) { + 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 +1727,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 +1745,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 +1761,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 00000000..5d56b256 --- /dev/null +++ b/src/jffs2.c @@ -0,0 +1,331 @@ +#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, xattrs; + 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; +} + +static int count_xattrs(const char *path) { + ssize_t n = llistxattr(path, NULL, 0); + if (n <= 0) + return 0; + char *names = malloc(n); + int count = 0; + if (names && (n = llistxattr(path, names, n)) > 0) + for (char *p = names; p < names + n; p += strlen(p) + 1) + count++; + free(names); + return count; +} + +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; + } + uint32_t cino = img->next_ino++; + img->entries++; + img->xattrs += count_xattrs(child); + 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 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; } + +void jffs2_counts(jffs2_img_t *img, int *entries, int *xattrs_dropped) { + *entries = img->entries; + *xattrs_dropped = img->xattrs; +} diff --git a/src/jffs2.h b/src/jffs2.h new file mode 100644 index 00000000..9c30547e --- /dev/null +++ b/src/jffs2.h @@ -0,0 +1,41 @@ +#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 xattrs, no hard links (a second name is a second copy). + */ + +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 nodes it holds, and how many + * extended attributes were left behind. */ +void jffs2_counts(jffs2_img_t *img, int *entries, int *xattrs_dropped); + +#endif /* JFFS2_H */ diff --git a/src/jffs2_test.c b/src/jffs2_test.c new file mode 100644 index 00000000..e68b11cc --- /dev/null +++ b/src/jffs2_test.c @@ -0,0 +1,222 @@ +/* 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 "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); + + 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 6c053601..3f703e74 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 d125ee60..2b464a2c 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 00000000..ee134bcf --- /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 00000000..7a838957 --- /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 00000000..271c9a94 --- /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 30e797a0..a8223e85 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,37 @@ 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') + 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)) From c40bd6a62e27661f416753968640294911a8714a Mon Sep 17 00:00:00 2001 From: AI Dev Date: Wed, 14 Oct 2026 13:18:28 +0000 Subject: [PATCH 2/2] upgrade: refuse what would lose settings, leave jffs2 room, keep bundle env honest Four findings from review of the layout move: - A settings partition not found mounted was taken as one with nothing on it, and the move went ahead and erased it. The lookup now also knows "mtdN" and "mtd:rootfs_data", the other ways the kernel shows a jffs2 source. A rootfs_data it still cannot find mounted is refused unless --wipe-settings asks to start from defaults. - Extended attributes were counted and dropped. One of them is how overlayfs hides deleted files (trusted.overlay.opaque), so dropping it brings them back. An entry carrying any is now refused, with nothing written. Neither the QEMU run nor the lab camera had one. - The carried image could fill rootfs_data to the last block. jffs2 mounts that, then fails the first write with ENOSPC. Five erase blocks are now kept free for its garbage collection. - A bundle setting bootargs or bootcmd was reported as set and then replaced by the values upgrade writes for the new layout. Both keys are now refused, along with a non-string value, before anything is stopped. upgrade_bundle.py refuses them as -e too. jffs2_test covers the xattr refusal. QEMU, with the lab 85H50AI dump again: - the move still carries every setting (57 entries, 576 of 2752 KB); - a bundle setting bootcmd exits 2 with services running. --- src/backup.c | 91 ++++++++++++++++++++++++++++++----------- src/jffs2.c | 31 +++++++------- src/jffs2.h | 9 ++-- src/jffs2_test.c | 15 +++++++ tools/upgrade_bundle.py | 3 ++ 5 files changed, 104 insertions(+), 45 deletions(-) diff --git a/src/backup.c b/src/backup.c index d4f7054a..23afd809 100644 --- a/src/backup.c +++ b/src/backup.c @@ -1210,18 +1210,24 @@ typedef struct { size_t region_len, erasesize; } carry_t; -// Where partition `num` is mounted as jffs2, if it is -static bool jffs2_mountpoint(int num, char *path, size_t sz) { +// 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 want[32], line[256]; - snprintf(want, sizeof(want), "/dev/mtdblock%d", num); + 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(dev, want) && !strcmp(fs, "jffs2")) { + !strcmp(fs, "jffs2") && + (!strcmp(dev, block) || !strcmp(dev, bare) || + !strcmp(dev, byname))) { snprintf(path, sz, "%s", mnt); found = true; } @@ -1233,23 +1239,29 @@ static bool jffs2_mountpoint(int num, char *path, size_t sz) { // 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; - jffs2_img_t *img = jffs2_new(c->erasesize, c->region_len); + 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); - int entries, xattrs; - jffs2_counts(img, &entries, &xattrs); memset(c->region, 0xff, c->region_len); memcpy(c->region, data, len); - printf("Settings: %d entries in %zu of %zu KB\n", entries, len / 1024, - c->region_len / 1024); - if (xattrs) - printf(" %d extended attributes left behind (an overlayfs " - "opaque directory shows its lower layer again)\n", - xattrs); + 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"); @@ -1611,11 +1623,21 @@ static int do_upgrade(const char *filename, bool force, bool wipe_settings) { data_part = p; if (wipe_settings) printf("Settings will not be carried over\n"); - else if (data_part < 0 || - !jffs2_mountpoint(data_part, carry.dir, sizeof(carry.dir))) - printf("No jffs2 settings partition is mounted; the new one " + else if (data_part < 0) + printf("This flash has no settings partition; the new one " "starts empty\n"); - else if (!build_carry(&carry)) { + 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 " @@ -1646,6 +1668,31 @@ static int do_upgrade(const char *filename, bool force, bool wipe_settings) { 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; @@ -1661,13 +1708,9 @@ static int do_upgrade(const char *filename, bool force, bool wipe_settings) { reboot_after_failure("Upgrading"); // Variables the bundle sets, e.g. the address this firmware upgrades from - const cJSON *jenv = cJSON_GetObjectItemCaseSensitive(json, "env"); - const cJSON *jvar; cJSON_ArrayForEach(jvar, jenv) { - if (cJSON_IsString(jvar)) { - set_env_param_ram(jvar->string, jvar->valuestring); - printf("set %s=%s\n", jvar->string, jvar->valuestring); - } + set_env_param_ram(jvar->string, jvar->valuestring); + printf("set %s=%s\n", jvar->string, jvar->valuestring); } if (keep_front) { diff --git a/src/jffs2.c b/src/jffs2.c index 5d56b256..9988886e 100644 --- a/src/jffs2.c +++ b/src/jffs2.c @@ -28,7 +28,7 @@ struct jffs2_img { uint8_t *buf; size_t len, cap, limit, erasesize; uint32_t next_ino, version; - int entries, xattrs; + int entries; char err[160]; }; @@ -213,17 +213,16 @@ static bool add_file(jffs2_img_t *img, uint32_t ino, const char *path, return ok; } -static int count_xattrs(const char *path) { +/* 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 0; - char *names = malloc(n); - int count = 0; - if (names && (n = llistxattr(path, names, n)) > 0) - for (char *p = names; p < names + n; p += strlen(p) + 1) - count++; - free(names); - return count; + 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, @@ -245,9 +244,10 @@ static bool add_dir(jffs2_img_t *img, uint32_t ino, const char *path, 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++; - img->xattrs += count_xattrs(child); if (!(ok = emit_dirent(img, ino, cino, e->d_name, st.st_mode, st.st_mtime))) break; @@ -308,7 +308,7 @@ bool jffs2_add_tree(jffs2_img_t *img, const char *path, 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 emit_inode(img, 1, &st, 0, 0, NULL, 0) && + return no_xattrs(img, path) && emit_inode(img, 1, &st, 0, 0, NULL, 0) && add_dir(img, 1, path, "", skip_contents); } @@ -325,7 +325,4 @@ const uint8_t *jffs2_data(jffs2_img_t *img, size_t *len) { const char *jffs2_error(jffs2_img_t *img) { return img->err; } -void jffs2_counts(jffs2_img_t *img, int *entries, int *xattrs_dropped) { - *entries = img->entries; - *xattrs_dropped = img->xattrs; -} +int jffs2_entries(jffs2_img_t *img) { return img->entries; } diff --git a/src/jffs2.h b/src/jffs2.h index 9c30547e..5faf9a69 100644 --- a/src/jffs2.h +++ b/src/jffs2.h @@ -15,7 +15,9 @@ * 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 xattrs, no hard links (a second name is a second copy). + * 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; @@ -34,8 +36,7 @@ bool jffs2_add_tree(jffs2_img_t *img, const char *path, /* 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 nodes it holds, and how many - * extended attributes were left behind. */ -void jffs2_counts(jffs2_img_t *img, int *entries, int *xattrs_dropped); +/* 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 index e68b11cc..9c103b11 100644 --- a/src/jffs2_test.c +++ b/src/jffs2_test.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "jffs2.h" @@ -207,6 +208,20 @@ int main(int argc, char **argv) { "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); diff --git a/tools/upgrade_bundle.py b/tools/upgrade_bundle.py index a8223e85..853360e3 100755 --- a/tools/upgrade_bundle.py +++ b/tools/upgrade_bundle.py @@ -159,6 +159,9 @@ def main(): 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