From 2f3a54a41fc1b80d2455fabf39e7533d2832f089 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:50:24 +0300 Subject: [PATCH 1/2] nand-migrate: move a NAND camera off the kernel-volume layout without UART The first FIT NAND layout kept the kernel in a UBI volume of its own; the current one keeps it as /boot/fitImage in the UBIFS rootfs, and firmware's sysupgrade refuses the old one, asking for a reinstall from U-Boot -- which needs a serial console. Every U-Boot that saved that layout's stock bootcmd is a FIT NAND build with ubifsload, so it already boots the new layout. nand-migrate writes a rootfs that carries /boot (the -nand- package's rootfs.ubifs) into the rootfs volume, switches bootcmd to the current stock one, and removes the kernel volume -- in that order, so a run stopped after the write still boots. It runs as PID 1 from a RAM root, handed over through the ::restart:/sbin/init action firmware's inittab carries, since UBI will not update the volume the system runs from. U-Boot is not written. Before anything is touched it refuses, saying why: an edited bootcmd, a kernel volume without a FIT, bootargs without ${rootargs}, an image that is not UBIFS for this LEB size or has no kernel in /boot, too little RAM, a dynamic build, an inittab without the restart action. A camera stopped after the bootcmd switch is finished live. The log is left in /root/nand-migrate.log. The U-Boot env is located, edited (exact key match) and CRC'd by nand_layout.c, and written with every erase block it spans erased first. --- .github/workflows/pr-build-check.yml | 4 +- CMakeLists.txt | 8 + src/main.c | 18 +- src/nand_layout.c | 209 +++++++ src/nand_layout.h | 91 +++ src/nand_layout_test.c | 261 ++++++++ src/nand_migrate.c | 862 +++++++++++++++++++++++++++ src/nand_migrate.h | 8 + 8 files changed, 1457 insertions(+), 4 deletions(-) create mode 100644 src/nand_layout.c create mode 100644 src/nand_layout.h create mode 100644 src/nand_layout_test.c create mode 100644 src/nand_migrate.c create mode 100644 src/nand_migrate.h diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index ab1cd937..6b588a53 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -140,11 +140,13 @@ jobs: cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Release cmake --build build --target cYAML_test reginfo_test longse_test \ sensors_test anyka_test hal_hisi_test \ - report_test http_test mtd_alias_test + report_test http_test mtd_alias_test \ + nand_layout_test ./build/cYAML_test ./build/report_test ./build/http_test ./build/mtd_alias_test + ./build/nand_layout_test ./build/reginfo_test ./build/longse_test ./build/anyka_test diff --git a/CMakeLists.txt b/CMakeLists.txt index 81506904..19dc363b 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/nand_layout.c + src/nand_layout.h + src/nand_migrate.c + src/nand_migrate.h src/bootrom.c src/bootrom.h src/clocks.c @@ -517,4 +521,8 @@ if(NOT ONLY_LIBRARY) # Which partitions are another name for the chip ("all" on Novatek). add_executable(mtd_alias_test src/mtd_alias_test.c src/mtd_alias.c) target_include_directories(mtd_alias_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) endif() diff --git a/src/main.c b/src/main.c index 50fca0bd..6c053601 100644 --- a/src/main.c +++ b/src/main.c @@ -13,7 +13,6 @@ #include #include "backup.h" -#include "report.h" #include "bootrom.h" #include "chipid.h" #include "cjson/cJSON.h" @@ -28,10 +27,12 @@ #include "i2cspi.h" #include "membw.h" #include "mtd.h" +#include "nand_migrate.h" #include "network.h" #include "ptrace.h" #include "ram.h" #include "reginfo.h" +#include "report.h" #include "sdcard.h" #include "sensors.h" #include "snstool.h" @@ -92,6 +93,9 @@ void print_usage() { " (experimental! use only on cameras with " "UART)\n" " [-f, --force] enforce\n" + " nand-migrate [--dry-run] [--wipe-settings] \n" + " move a NAND camera off the retired\n" + " layout with a `kernel` UBI volume\n" " printenv drop-in replacement for fw_printenv\n" " setenv drop-in replacement for fw_setenv\n" " dmesg drop-in replacement for dmesg\n" @@ -182,7 +186,8 @@ char *build_report_yaml(void) { static int backup_with_yaml(const char *backup_file) { cJSON *yaml = build_yaml(); - if (!yaml) return EXIT_FAILURE; + if (!yaml) + return EXIT_FAILURE; char *string = cYAML_Print(yaml); int ret = do_backup(string, strlen(string), backup_file); @@ -196,6 +201,10 @@ static int backup_with_yaml(const char *backup_file) { } int main(int argc, char *argv[]) { + // Only ever PID 1 as the /sbin/init `nand-migrate` hands busybox init + if (getpid() == 1) + return nand_migrate_pid1(); + // Don't use common option parser for these commands if (argc > 1) { if (!strcmp(argv[1], "gpio")) @@ -210,6 +219,8 @@ int main(int argc, char *argv[]) { return report_cmd(argc - 1, argv + 1); else if (!strcmp(argv[1], "restore") || !strcmp(argv[1], "upgrade")) return upgrade_restore_cmd(argc - 1, argv + 1); + else if (!strcmp(argv[1], "nand-migrate")) + return nand_migrate_cmd(argc - 1, argv + 1); else if (!strcmp(argv[1], "printenv")) return cmd_printenv(); else if (!strcmp(argv[1], "setenv")) @@ -317,7 +328,8 @@ int main(int argc, char *argv[]) { } cJSON *yaml = build_yaml(); - if (!yaml) return EXIT_FAILURE; + if (!yaml) + return EXIT_FAILURE; char *string = cYAML_Print(yaml); printf("%s", string); free(string); diff --git a/src/nand_layout.c b/src/nand_layout.c new file mode 100644 index 00000000..9d8e9629 --- /dev/null +++ b/src/nand_layout.c @@ -0,0 +1,209 @@ +#include "nand_layout.h" + +#include +#include + +uint32_t nm_crc32(uint32_t crc, const void *buf, size_t len) { + static uint32_t table[256]; + if (!table[1]) { + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (int k = 0; k < 8; k++) + c = c & 1 ? 0xEDB88320 ^ (c >> 1) : c >> 1; + table[i] = c; + } + } + const uint8_t *p = buf; + crc = ~crc; + while (len--) + crc = table[(crc ^ *p++) & 0xff] ^ (crc >> 8); + return ~crc; +} + +static uint32_t le32(const uint8_t *p) { + return p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24; +} + +static uint64_t le64(const uint8_t *p) { + return le32(p) | (uint64_t)le32(p + 4) << 32; +} + +size_t nm_env_locate(const uint8_t *buf, size_t size) { + static const size_t lens[] = {0x10000, 0x20000, 0x40000, 0x8000, + 0x4000, 0x2000, 0x1000}; + for (size_t i = 0; i < sizeof(lens) / sizeof(lens[0]); i++) { + if (lens[i] > size) + continue; + if (nm_crc32(0, buf + 4, lens[i] - 4) == le32(buf)) + return lens[i]; + } + return 0; +} + +const char *nm_env_get(const uint8_t *env, size_t len, const char *key) { + size_t klen = strlen(key); + const char *p = (const char *)env + 4, *end = (const char *)env + len; + while (p < end && *p) { + size_t n = strnlen(p, end - p); + if (n > klen && !strncmp(p, key, klen) && p[klen] == '=') + return p + klen + 1; + p += n + 1; + } + return NULL; +} + +bool nm_env_set(uint8_t *env, size_t len, const char *key, const char *value) { + size_t klen = strlen(key), vlen = strlen(value); + uint8_t *out = calloc(len, 1); + if (!out) + return false; + size_t o = 4; + const char *p = (const char *)env + 4, *end = (const char *)env + len; + while (p < end && *p) { + size_t n = strnlen(p, end - p); + if (!(n > klen && !strncmp(p, key, klen) && p[klen] == '=')) { + // +2: this entry's NUL and the list's + if (o + n + 2 > len) + goto toolong; + memcpy(out + o, p, n); + o += n + 1; + } + p += n + 1; + } + if (o + klen + 1 + vlen + 2 > len) + goto toolong; + memcpy(out + o, key, klen); + out[o + klen] = '='; + memcpy(out + o + klen + 1, value, vlen); + uint32_t crc = nm_crc32(0, out + 4, len - 4); + out[0] = crc; + out[1] = crc >> 8; + out[2] = crc >> 16; + out[3] = crc >> 24; + memcpy(env, out, len); + free(out); + return true; +toolong: + free(out); + return false; +} + +static bool has_word(const char *s, const char *w) { + size_t n = strlen(w); + for (const char *p = s; (p = strstr(p, w)); p += n) + if ((p == s || p[-1] == ' ') && + (p[n] == '\0' || p[n] == ' ' || p[n] == '\n')) + return true; + return false; +} + +#define REFUSE(msg) \ + do { \ + *why = msg; \ + return NM_REFUSE; \ + } while (0) + +nm_plan_t nm_check_camera(const nm_camera_t *cam, const char **why) { + *why = NULL; + if (!cam->has_rootfs_vol || !cam->has_data_vol) + REFUSE("no UBI volumes named rootfs and rootfs_data: not a UBI NAND " + "camera"); + if (!cam->cmdline || !has_word(cam->cmdline, "root=ubi0:rootfs")) + REFUSE("the kernel was not booted with root=ubi0:rootfs: not the " + "UBIFS layout this migrates from"); + if (!cam->has_kernel_vol) + REFUSE("there is no kernel volume: this camera is already on the " + "current layout, and sysupgrade upgrades it"); + if (!cam->bootcmd) + REFUSE("the U-Boot environment has no bootcmd"); + if (!strcmp(cam->bootcmd, NM_BOOTCMD_UBIFS)) { + // Booted with this bootcmd, so the running rootfs is where U-Boot + // found the kernel; the caller still checks /boot is there. + return NM_DROP_KERNEL_VOL; + } + if (strcmp(cam->bootcmd, NM_BOOTCMD_FITVOL)) + REFUSE("bootcmd is not the stock one of the kernel-volume layout; an " + "edited bootcmd is left to its owner"); + // A FIT starts with the FDT magic; a legacy uImage with 0x27051956 + if (cam->kernel_head_len < 4 || le32(cam->kernel_head) != 0xedfe0dd0) + REFUSE("the kernel volume does not hold a FIT, so this U-Boot is not " + "known to boot one from UBIFS"); + if (!cam->bootargs || !strstr(cam->bootargs, "${rootargs}")) + REFUSE("bootargs does not take its root from ${rootargs}, which the " + "new bootcmd sets"); + return NM_MIGRATE; +} + +#define UBIFS_MAGIC 0x06101831 +#define UBIFS_CH_SZ 24 +#define UBIFS_DENT_NODE 2 +#define UBIFS_SB_NODE 6 +#define UBIFS_ITYPE_REG 0 +#define UBIFS_ITYPE_DIR 1 +#define UBIFS_ROOT_INO 1 + +// A node header at p with room for it in the image and a CRC that holds. +static uint32_t ubifs_node(const uint8_t *p, size_t room) { + if (room < UBIFS_CH_SZ || le32(p) != UBIFS_MAGIC) + return 0; + uint32_t len = le32(p + 16); + if (len < UBIFS_CH_SZ || len > room) + return 0; + // UBIFS starts from ~0 and does not invert at the end + if (~nm_crc32(0, p + 8, len - 8) != le32(p + 4)) + return 0; + return len; +} + +const char *nm_check_image(const uint8_t *img, size_t size, uint32_t leb_size, + const char **kernel_name) { + *kernel_name = NULL; + uint32_t len = ubifs_node(img, size); + if (!len || img[20] != UBIFS_SB_NODE || len < 40) + return "not a UBIFS image (no superblock at its start)"; + if (le32(img + 36) != leb_size) + return "built for a different LEB size than this flash's"; + + /* + * Directory entries carry their names uncompressed, so finding /boot and + * the kernel in it needs no UBIFS reader: an entry's key starts with + * the inode of the directory it is in. A deleted entry has inode 0. + * Nodes are 8-byte aligned within a LEB, and LEBs are back to back. + */ + uint64_t boot_ino = 0; + for (int pass = 0; pass < 2 && !*kernel_name; pass++) { + for (size_t off = 0; off + UBIFS_CH_SZ <= size; off += 8) { + const uint8_t *p = img + off; + if (p[20] != UBIFS_DENT_NODE || !(len = ubifs_node(p, size - off))) + continue; + if (len < 56) + continue; + uint32_t parent = le32(p + 24); + uint64_t inum = le64(p + 40); + uint8_t type = p[49]; + uint16_t nlen = p[50] | p[51] << 8; + const char *name = (const char *)p + 56; + if (!inum || 56u + nlen > len) + continue; + if (pass == 0 && parent == UBIFS_ROOT_INO && + type == UBIFS_ITYPE_DIR && nlen == 4 && + !memcmp(name, "boot", 4)) + boot_ino = inum; + if (pass == 1 && boot_ino && parent == boot_ino && + type == UBIFS_ITYPE_REG) { + if (nlen == 8 && !memcmp(name, "fitImage", 8)) + *kernel_name = "/boot/fitImage"; + else if (nlen == 6 && !memcmp(name, "uImage", 6) && + !*kernel_name) + *kernel_name = "/boot/uImage"; + } + off += (len + 7) / 8 * 8 - 8; + } + if (pass == 0 && !boot_ino) + return "there is no /boot in it, so U-Boot would find no kernel"; + } + if (!*kernel_name) + return "/boot holds neither fitImage nor uImage, so U-Boot would find " + "no kernel"; + return NULL; +} diff --git a/src/nand_layout.h b/src/nand_layout.h new file mode 100644 index 00000000..46810550 --- /dev/null +++ b/src/nand_layout.h @@ -0,0 +1,91 @@ +#ifndef NAND_LAYOUT_H +#define NAND_LAYOUT_H + +/* + * The pure half of `ipctool nand-migrate`: everything that decides, as + * opposed to everything that touches a device. Kept apart so it can be tested + * on a host (nand_layout_test.c). + */ + +#include +#include +#include + +/* + * u-boot-xmedia's stock bootcmd of the first FIT NAND layout: a FIT in a UBI + * volume of its own named `kernel` (NAND_FITVOL_BOOTCOMMAND, e19b798a). Only a + * camera that still carries it byte for byte is migrated: it proves a FIT + * NAND build of that U-Boot, and every one of those has CMD_UBIFS. + */ +#define NM_BOOTCMD_FITVOL \ + "ubi part ubi; ubi read ${baseaddr} kernel; " \ + "if itest.l *${baseaddr} == 0xedfe0dd0; " \ + "then setenv rootargs root=ubi0:rootfs rootfstype=ubifs; " \ + "else setenv rootargs root=/dev/ubiblock0_1 ubi.block=0,1; fi; " \ + "setenv setargs setenv bootargs ${bootargs}; run setargs; " \ + "bootm ${baseaddr}; reset" + +/* + * The current one (CONFIG_BOOTCOMMAND, a138a692): the kernel is a file in the + * UBIFS rootfs. Byte for byte what a fresh install saves, so a later U-Boot + * recognises it as its own. + */ +#define NM_BOOTCMD_UBIFS \ + "ubi part ubi; ubifsmount ubi0:rootfs; " \ + "setenv rootargs root=ubi0:rootfs rootfstype=ubifs; " \ + "setenv setargs setenv bootargs ${bootargs}; run setargs; " \ + "if ubifsload ${baseaddr} /boot/fitImage; then bootm ${baseaddr}; fi; " \ + "if ubifsload ${baseaddr} /boot/uImage; then bootm ${baseaddr}; fi; reset" + +// zlib's CRC-32, which U-Boot's env and (uninverted) UBIFS both use. +uint32_t nm_crc32(uint32_t crc, const void *buf, size_t len); + +/* + * A U-Boot env at the start of buf (CRC, then key=value\0...\0\0). Returns + * the env's length, from the sizes U-Boot builds use, or 0 when no CRC + * matches. + */ +size_t nm_env_locate(const uint8_t *buf, size_t size); + +// The value of exactly `key` (not a key it is a prefix of), or NULL. +const char *nm_env_get(const uint8_t *env, size_t len, const char *key); + +/* + * Set exactly `key` to `value` in place and recompute the CRC. False when the + * result would not fit, in which case env is unchanged. + */ +bool nm_env_set(uint8_t *env, size_t len, const char *key, const char *value); + +/* What the running camera looks like, gathered by the caller. */ +typedef struct { + bool has_kernel_vol, has_rootfs_vol, has_data_vol; + const char *cmdline; + const uint8_t *kernel_head; // the kernel volume's first bytes + size_t kernel_head_len; + const char *bootcmd, *bootargs; +} nm_camera_t; + +typedef enum { + NM_REFUSE, + // the retired layout: write the rootfs, switch bootcmd, drop the volume + NM_MIGRATE, + // a migration that stopped after switching bootcmd: only the unused + // kernel volume is left, and it can go with the system running + NM_DROP_KERNEL_VOL, +} nm_plan_t; + +/* + * What can be done with this camera. On NM_REFUSE *why says why not, in one + * sentence. + */ +nm_plan_t nm_check_camera(const nm_camera_t *cam, const char **why); + +/* + * Whether img is a UBIFS image built for leb_size-byte LEBs that holds a + * kernel at /boot/fitImage or /boot/uImage. NULL when it is, otherwise why + * not. *kernel_name says which kernel it found. + */ +const char *nm_check_image(const uint8_t *img, size_t size, uint32_t leb_size, + const char **kernel_name); + +#endif /* NAND_LAYOUT_H */ diff --git a/src/nand_layout_test.c b/src/nand_layout_test.c new file mode 100644 index 00000000..0a224f88 --- /dev/null +++ b/src/nand_layout_test.c @@ -0,0 +1,261 @@ +/* + * nand_layout_test: what `ipctool nand-migrate` decides before it touches + * anything. Synthetic env and UBIFS buffers always; with NM_ENV= + * and NM_IMAGE= it also reads a real camera's env partition + * and a real image (LEB size 126976, 2 KiB pages on 128 KiB blocks). + */ +#include +#include +#include + +#include "nand_layout.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) + +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 void make_env(uint8_t *env, size_t len, const char *const *kv) { + memset(env, 0, len); + size_t o = 4; + for (; *kv; kv++) { + strcpy((char *)env + o, *kv); + o += strlen(*kv) + 1; + } + put32(env, nm_crc32(0, env + 4, len - 4)); +} + +static void test_env(void) { + enum { LEN = 0x40000 }; + uint8_t *env = malloc(LEN); + const char *kv[] = {"bootcmdx=decoy", "bootcmd=" NM_BOOTCMD_FITVOL, + "bootargs=mem=${osmem} ${rootargs}", "soc=gk7205v510", + NULL}; + make_env(env, LEN, kv); + + CHECK(nm_env_locate(env, LEN) == LEN, "a 256K env not found"); + CHECK(nm_env_locate(env, LEN - 1) == 0, "env found in too short a buffer"); + // A key that is a prefix of another is not that key + const char *v = nm_env_get(env, LEN, "bootcmd"); + CHECK(v && !strcmp(v, NM_BOOTCMD_FITVOL), "bootcmd read as '%s'", v); + CHECK(!nm_env_get(env, LEN, "boot"), "a key prefix matched"); + + CHECK(nm_env_set(env, LEN, "bootcmd", NM_BOOTCMD_UBIFS), "set failed"); + CHECK(nm_env_locate(env, LEN) == LEN, "CRC not recomputed"); + v = nm_env_get(env, LEN, "bootcmd"); + CHECK(v && !strcmp(v, NM_BOOTCMD_UBIFS), "bootcmd not replaced"); + v = nm_env_get(env, LEN, "bootcmdx"); + CHECK(v && !strcmp(v, "decoy"), "the decoy was touched"); + v = nm_env_get(env, LEN, "soc"); + CHECK(v && !strcmp(v, "gk7205v510"), "another key lost"); + + // Too long to fit: refused, env untouched + uint8_t small[64]; + const char *skv[] = {"a=1", NULL}; + make_env(small, sizeof(small), skv); + uint8_t copy[64]; + memcpy(copy, small, sizeof(small)); + CHECK(!nm_env_set(small, sizeof(small), "bootcmd", NM_BOOTCMD_UBIFS), + "an overlong value accepted"); + CHECK(!memcmp(copy, small, sizeof(small)), "a refused set changed env"); + free(env); +} + +static void test_camera(void) { + static const uint8_t fit[] = {0xd0, 0x0d, 0xfe, 0xed}; + static const uint8_t uimage[] = {0x27, 0x05, 0x19, 0x56}; + const char *cmdline = "mem=128M console=ttyAMA0,115200 panic=20 init=/init " + "root=ubi0:rootfs rootfstype=ubifs ubi.mtd=2,2048\n"; + nm_camera_t good = { + .has_kernel_vol = true, + .has_rootfs_vol = true, + .has_data_vol = true, + .cmdline = cmdline, + .kernel_head = fit, + .kernel_head_len = 4, + .bootcmd = NM_BOOTCMD_FITVOL, + .bootargs = "mem=128M console=ttyAMA0,115200 ${rootargs} ${mtdparts}", + }; + const char *why; +#define PLAN(c) nm_check_camera(&(c), &why) + nm_camera_t c = good; + CHECK(PLAN(c) == NM_MIGRATE, "the kvol camera refused: %s", why); + + c = good; + c.has_kernel_vol = false; + c.bootcmd = NM_BOOTCMD_UBIFS; + CHECK(PLAN(c) == NM_REFUSE && why, "a current-layout camera accepted"); + + c = good; + c.bootcmd = NM_BOOTCMD_UBIFS; + CHECK(PLAN(c) == NM_DROP_KERNEL_VOL, + "a half-migrated camera not finished: %s", why); + + c = good; + c.kernel_head = uimage; + CHECK(PLAN(c) == NM_REFUSE && why, "a uImage kernel volume accepted"); + + c = good; + c.bootcmd = NM_BOOTCMD_FITVOL "; echo mine"; + CHECK(PLAN(c) == NM_REFUSE && why, "an edited bootcmd accepted"); + + c = good; + c.bootargs = "mem=128M root=ubi0:rootfs rootfstype=ubifs"; + CHECK(PLAN(c) == NM_REFUSE && why, "bootargs without ${rootargs} accepted"); + + c = good; + c.cmdline = "root=/dev/ubiblock0_1 ubi.block=0,1"; + CHECK(PLAN(c) == NM_REFUSE && why, "a ubiblock camera accepted"); + + c = good; + c.cmdline = "root=ubi0:rootfs_data"; + CHECK(PLAN(c) == NM_REFUSE && why, + "root=ubi0:rootfs_data taken for rootfs"); + + c = good; + c.has_data_vol = false; + CHECK(PLAN(c) == NM_REFUSE && why, "no rootfs_data accepted"); +} + +/* A UBIFS node: common header + body, CRC'd as UBIFS does. */ +static size_t node(uint8_t *p, uint8_t type, const uint8_t *body, size_t blen) { + size_t len = 24 + blen; + memset(p, 0, 24); + put32(p, 0x06101831); + put32(p + 16, len); + p[20] = type; + memcpy(p + 24, body, blen); + put32(p + 4, ~nm_crc32(0, p + 8, len - 8)); + return (len + 7) / 8 * 8; +} + +static size_t dent(uint8_t *p, uint32_t parent, uint64_t inum, uint8_t itype, + const char *name) { + uint8_t b[64] = {0}; + size_t nlen = strlen(name); + put32(b, parent); + put32(b + 4, 0x12345 | 2u << 29); + put32(b + 16, inum); + put32(b + 20, inum >> 32); + b[25] = itype; + b[26] = nlen; + memcpy(b + 32, name, nlen + 1); + return node(p, 2, b, 32 + nlen + 1); +} + +static size_t sb(uint8_t *p, uint32_t leb) { + uint8_t b[64] = {0}; + put32(b + 12, leb); + return node(p, 6, b, sizeof(b)); +} + +static void test_image(void) { + enum { LEB = 126976 }; + uint8_t *img = calloc(3, LEB); + const char *k; + + sb(img, LEB); + size_t o = LEB; + o += dent(img + o, 1, 7, 1, "boot"); + o += dent(img + o, 1, 8, 1, "etc"); + o += dent(img + o, 7, 9, 0, "fitImage"); + CHECK(!nm_check_image(img, 3 * LEB, LEB, &k) && k && + !strcmp(k, "/boot/fitImage"), + "a /boot/fitImage image refused: %s", + nm_check_image(img, 3 * LEB, LEB, &k)); + CHECK(nm_check_image(img, 3 * LEB, 129024, &k), "LEB mismatch accepted"); + + // The kernel in another directory is not the kernel U-Boot loads + memset(img + LEB, 0, 2 * LEB); + o = LEB; + o += dent(img + o, 1, 7, 1, "boot"); + o += dent(img + o, 8, 9, 0, "fitImage"); + CHECK(nm_check_image(img, 3 * LEB, LEB, &k), "a stray fitImage accepted"); + + // A deleted /boot entry is no /boot + memset(img + LEB, 0, 2 * LEB); + o = LEB; + o += dent(img + o, 1, 0, 1, "boot"); + CHECK(nm_check_image(img, 3 * LEB, LEB, &k), "a deleted /boot accepted"); + + // A uImage-only image boots too + memset(img + LEB, 0, 2 * LEB); + o = LEB; + o += dent(img + o, 7, 9, 0, "uImage"); // before its directory, as in a log + o += dent(img + o, 1, 7, 1, "boot"); + CHECK(!nm_check_image(img, 3 * LEB, LEB, &k) && k && + !strcmp(k, "/boot/uImage"), + "a /boot/uImage image refused"); + + // A corrupted entry is not believed + img[LEB + 60] ^= 1; + CHECK(nm_check_image(img, 3 * LEB, LEB, &k), "a bad CRC believed"); + + // Not UBIFS at all (a squashfs) + memset(img, 0, LEB); + memcpy(img, "hsqs", 4); + CHECK(nm_check_image(img, 3 * LEB, LEB, &k), "a squashfs accepted"); + free(img); +} + +static uint8_t *slurp(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) + return NULL; + fseek(f, 0, SEEK_END); + *len = ftell(f); + rewind(f); + uint8_t *b = malloc(*len); + if (fread(b, 1, *len, f) != *len) { + free(b); + b = NULL; + } + fclose(f); + return b; +} + +static void test_real(void) { + const char *p = getenv("NM_ENV"); + size_t len; + uint8_t *b; + if (p && (b = slurp(p, &len))) { + size_t el = nm_env_locate(b, len); + CHECK(el, "no env in %s", p); + const char *v = el ? nm_env_get(b, el, "bootcmd") : NULL; + CHECK(v && !strcmp(v, NM_BOOTCMD_FITVOL), + "%s: bootcmd is not the stock kvol one: '%s'", p, v); + printf("real env: %zu bytes, bootcmd is the kvol stock one\n", el); + free(b); + } + p = getenv("NM_IMAGE"); + if (p && (b = slurp(p, &len))) { + const char *k, *why = nm_check_image(b, len, 126976, &k); + CHECK(!why, "%s refused: %s", p, why); + if (!why) + printf("real image: %zu bytes, kernel %s\n", len, k); + free(b); + } +} + +int main(void) { + test_env(); + test_camera(); + test_image(); + test_real(); + if (!failed) + printf("nand_layout_test: ok\n"); + return failed; +} diff --git a/src/nand_migrate.c b/src/nand_migrate.c new file mode 100644 index 00000000..3093ac00 --- /dev/null +++ b/src/nand_migrate.c @@ -0,0 +1,862 @@ +/* + * ipctool nand-migrate: move a NAND camera off the retired layout with a + * kernel volume of its own, onto the current one, with no UART. + * + * The retired layout (u-boot-xmedia e19b798a, OpenIPC firmware #2526): one + * UBI device holding `kernel` (a FIT), `rootfs` (UBIFS) and `rootfs_data`, + * booted by a bootcmd that reads the kernel volume. The current one (u-boot- + * xmedia a138a692, firmware #2537): no kernel volume; the kernel is + * /boot/fitImage inside the UBIFS rootfs, which U-Boot loads with ubifsload. + * firmware's sysupgrade refuses the old one and asks for a reinstall from + * U-Boot, which needs a serial console. + * + * Every U-Boot that saved the old layout's stock bootcmd can already boot the + * new one (a FIT NAND build, which always has CMD_UBIFS), so no bootloader is + * written. What changes is: + * 1. the rootfs volume gets an image with the kernel in /boot, + * 2. bootcmd becomes the current stock one, + * 3. the kernel volume goes. + * In that order, so a run stopped at any point after 1 still boots: the old + * bootcmd boots the old kernel volume with the new rootfs, and the new + * bootcmd boots /boot whether the kernel volume is there or not. + * + * Step 1 rewrites the volume the system runs from, which UBI refuses while + * anything has it open. So the work is done by ipctool itself as PID 1, from + * a RAM root: it pivots into a tmpfs and sends busybox init SIGQUIT, which + * runs the shutdown actions, kills every process and execs /sbin/init -- the + * ::restart: action firmware's inittab carries for sysupgrade. /sbin/init in + * the RAM root is this binary, which is static. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mtd.h" +#include "nand_layout.h" +#include "nand_migrate.h" +#include "sha1.h" + +#ifndef UBI_IOCRMVOL +#define UBI_IOCRMVOL _IOW(UBI_IOC_MAGIC, 1, int32_t) +#endif + +#define RAM_ROOT "/ram" +#define RAM_IMAGE "/rootfs.ubifs" +#define RAM_CONF "/nand-migrate.conf" +// What a run leaves in the camera's settings, so its outcome can be read +// over SSH afterwards as /root/nand-migrate.log: the overlay's upper +// directory is root/ in that volume. +#define LOG_PATH "/mnt/root/root/nand-migrate.log" + +typedef struct { + int ubi, rootfs, kernel, data; // UBI device and volume ids + int env_mtd; + size_t image_size; + char image[128]; // where phase 2 finds it, in the RAM root + bool wipe; + char stop[16]; // test knob: "write" or "env" stops after that step +} nm_conf_t; + +/* ---- what is on the camera ---------------------------------------------- */ + +static int mtd_by_name(const char *want) { + FILE *f = fopen("/proc/mtd", "r"); + if (!f) + return -1; + char line[128], name[64]; + int num, found = -1; + while (fgets(line, sizeof line, f)) + if (sscanf(line, "mtd%d: %*x %*x \"%63[^\"]\"", &num, name) == 2 && + !strcmp(name, want)) { + found = num; + break; + } + fclose(f); + return found; +} + +static long sysfs_long(int ubi, int vol, const char *attr) { + char path[96]; + snprintf(path, sizeof path, "/sys/class/ubi/ubi%d_%d/%s", ubi, vol, attr); + FILE *f = fopen(path, "r"); + long v = -1; + if (f) { + if (fscanf(f, "%ld", &v) != 1) + v = -1; + fclose(f); + } + return v; +} + +static char *read_file(const char *path, size_t *len) { + int fd = open(path, O_RDONLY); + if (fd < 0) + return NULL; + size_t cap = 4096, n = 0; + char *b = malloc(cap + 1); + for (ssize_t r; b && (r = read(fd, b + n, cap - n)) > 0;) { + n += r; + if (n == cap) { + char *nb = realloc(b, (cap *= 2) + 1); + if (!nb) + free(b); + b = nb; + } + } + close(fd); + if (b) + b[n] = '\0'; + if (len) + *len = n; + return b; +} + +// The image, mapped rather than read: on a tmpfs it is RAM already, and a +// copy on the heap is RAM a 128 MB camera does not have twice. +static uint8_t *map_file(const char *path, size_t *size) { + int fd = open(path, O_RDONLY); + if (fd < 0) + return NULL; + struct stat st; + void *m = MAP_FAILED; + if (!fstat(fd, &st) && st.st_size > 0) + m = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (m == MAP_FAILED) + return NULL; + *size = st.st_size; + return m; +} + +// The whole env partition, or the first erase block of it -- where U-Boot +// keeps the env. NULL and errno when it cannot be read. +static uint8_t *read_env(int mtd, size_t *len, uint32_t *erasesize) { + char dev[32]; + snprintf(dev, sizeof dev, "/dev/mtd%d", mtd); + int fd = open(dev, O_RDONLY); + if (fd < 0) + return NULL; + struct mtd_info_user info; + uint8_t *b = NULL; + if (ioctl(fd, MEMGETINFO, &info)) + goto out; + *erasesize = info.erasesize; + *len = info.size; + b = malloc(info.size); + if (b && pread(fd, b, info.size, 0) != (ssize_t)info.size) { + free(b); + b = NULL; + } +out: + close(fd); + return b; +} + +static bool is_static_binary(const char *path) { + size_t len; + char *elf = read_file(path, &len); + if (!elf || len < 52 || memcmp(elf, "\177ELF", 4)) { + free(elf); + return false; + } + bool is64 = elf[4] == 2, dyn = false; + uint64_t phoff = is64 ? *(uint64_t *)(elf + 32) : *(uint32_t *)(elf + 28); + uint16_t phentsize = *(uint16_t *)(elf + (is64 ? 54 : 42)); + uint16_t phnum = *(uint16_t *)(elf + (is64 ? 56 : 44)); + for (int i = 0; i < phnum; i++) { + size_t o = phoff + (size_t)i * phentsize; + if (o + 4 <= len && *(uint32_t *)(elf + o) == 3 /* PT_INTERP */) + dyn = true; + } + free(elf); + return !dyn; +} + +static long mem_available_kb(void) { + char *m = read_file("/proc/meminfo", NULL); + char *p = m ? strstr(m, "MemAvailable:") : NULL; + long kb = p ? atol(p + 13) : -1; + free(m); + return kb; +} + +/* ---- the flash steps, shared by both phases ------------------------------ */ + +static int wdt_fd = -1; + +static void feed(void) { + if (wdt_fd < 0) + return; + ioctl(wdt_fd, WDIOC_KEEPALIVE, 0); + ssize_t unused = write(wdt_fd, "k", 1); + (void)unused; +} + +static char logbuf[16384]; +static size_t loglen; + +static void say(const char *fmt, ...) { + char line[512]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(line, sizeof line, fmt, ap); + va_end(ap); + if (n < 0) + return; + if ((size_t)n >= sizeof line) + n = sizeof line - 1; + printf("nand-migrate: %s\n", line); + fflush(stdout); + if (loglen + n + 1 < sizeof logbuf) { + memcpy(logbuf + loglen, line, n); + logbuf[loglen + n] = '\n'; + loglen += n + 1; + } +} + +static bool ubi_rmvol(int ubi, int vol) { + char dev[32]; + snprintf(dev, sizeof dev, "/dev/ubi%d", ubi); + int fd = open(dev, O_RDONLY); + if (fd < 0) + return false; + int32_t id = vol; + bool ok = !ioctl(fd, UBI_IOCRMVOL, &id); + int e = errno; + close(fd); + errno = e; + return ok; +} + +// ubiupdatevol: UBI takes the volume exclusively for the update, and EBUSY +// means something still has it -- a lazily detached UBIFS on its way out. +static bool ubi_volup(int ubi, int vol, const uint8_t *data, size_t size) { + char dev[32]; + snprintf(dev, sizeof dev, "/dev/ubi%d_%d", ubi, vol); + int fd = -1; + int64_t bytes = size; + for (int i = 0; i < 100; i++) { + feed(); + fd = open(dev, O_RDWR); + if (fd >= 0 && !ioctl(fd, UBI_IOCVOLUP, &bytes)) + break; + int e = errno; + if (fd >= 0) + close(fd); + fd = -1; + if (e != EBUSY) { + errno = e; + return false; + } + usleep(100 * 1000); + } + if (fd < 0) { + errno = EBUSY; + return false; + } + size_t done = 0, chunk = 128 * 1024, step = size / 10 + 1, next = step; + while (done < size) { + size_t n = size - done < chunk ? size - done : chunk; + ssize_t w = write(fd, data + done, n); + if (w <= 0) { + int e = errno; + close(fd); + errno = e; + return false; + } + done += w; + feed(); + if (done >= next || done == size) { + say(" written %zu%%", done * 100 / size); + next += step; + } + } + return !close(fd); +} + +static bool sha1_matches(int ubi, int vol, const uint8_t *data, size_t size) { + unsigned char want[20], got[20]; + SHA1_CTX ctx; + SHA1Init(&ctx); + for (size_t o = 0; o < size; o += 65536) { + size_t n = size - o < 65536 ? size - o : 65536; + SHA1Update(&ctx, data + o, n); + } + SHA1Final(want, &ctx); + size_t read_len = 0; + feed(); + return sha1_ubi_volume(ubi, vol, size, got, &read_len) && + read_len == size && !memcmp(want, got, sizeof want); +} + +/* + * Write env back to the start of its partition, every erase block it spans + * erased first, and read it back. NAND has no partial-block erase, and + * U-Boot's env_nand writes the env from the partition's first good block, + * which the caller has checked is block 0. + */ +static bool write_env(int mtd, const uint8_t *env, size_t len, + uint32_t erasesize) { + char dev[32]; + snprintf(dev, sizeof dev, "/dev/mtd%d", mtd); + int fd = open(dev, O_RDWR); + if (fd < 0) + return false; + bool ok = false; + uint8_t *back = malloc(len); + for (uint32_t o = 0; o < len; o += erasesize) { + struct erase_info_user e = {.start = o, .length = erasesize}; + feed(); + if (ioctl(fd, MEMERASE, &e)) + goto out; + } + if (pwrite(fd, env, len, 0) != (ssize_t)len) + goto out; + ok = back && pread(fd, back, len, 0) == (ssize_t)len && + !memcmp(env, back, len); +out: + free(back); + close(fd); + return ok; +} + +/* ---- PID 1 --------------------------------------------------------------- */ + +static void keep_log(const nm_conf_t *c) { + // Into the settings volume, where the next boot shows it as + // /root/nand-migrate.log. Not after --wipe-settings: an empty volume is + // what makes init start from defaults. + if (c->wipe) + return; + char src[32]; + snprintf(src, sizeof src, "ubi%d:rootfs_data", c->ubi); + mkdir("/mnt", 0755); + if (mount(src, "/mnt", "ubifs", 0, NULL)) + return; + mkdir("/mnt/root", 0755); + mkdir("/mnt/root/root", 0700); + int fd = open(LOG_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) { + ssize_t unused = write(fd, logbuf, loglen); + (void)unused; + fsync(fd); + close(fd); + } + umount("/mnt"); +} + +static void __attribute__((noreturn)) finish(const nm_conf_t *c, bool ok) { + say(ok ? "done, rebooting" : "rebooting"); + if (c) + keep_log(c); + sync(); + sleep(1); + reboot(RB_AUTOBOOT); + for (;;) + pause(); +} + +// Detach the old root and everything mounted under it, deepest first. +static void release_old_root(void) { + char *mounts = read_file("/proc/mounts", NULL); + char *paths[64]; + int n = 0; + for (char *l = mounts ? strtok(mounts, "\n") : NULL; l && n < 64; + l = strtok(NULL, "\n")) { + char dev[128], path[128]; + if (sscanf(l, "%127s %127s", dev, path) == 2 && + (!strcmp(path, "/mnt") || !strncmp(path, "/mnt/", 5))) + paths[n++] = strdup(path); + } + // Longest path first: a mount under /mnt goes before /mnt + for (int i = 0; i < n; i++) + for (int j = i + 1; j < n; j++) + if (strlen(paths[j]) > strlen(paths[i])) { + char *t = paths[i]; + paths[i] = paths[j]; + paths[j] = t; + } + for (int i = 0; i < n; i++) { + if (umount2(paths[i], MNT_DETACH)) + say("cannot detach %s: %s", paths[i], strerror(errno)); + free(paths[i]); + } + free(mounts); +} + +static bool read_conf(nm_conf_t *c) { + char *s = read_file(RAM_CONF, NULL); + if (!s) + return false; + int wipe = 0; + int n = sscanf(s, + "ubi=%d rootfs=%d kernel=%d data=%d env_mtd=%d " + "image_size=%zu image=%127s wipe=%d stop=%15s", + &c->ubi, &c->rootfs, &c->kernel, &c->data, &c->env_mtd, + &c->image_size, c->image, &wipe, c->stop); + free(s); + c->wipe = wipe; + if (!strcmp(c->stop, "-")) + c->stop[0] = '\0'; + return n == 9; +} + +int nand_migrate_pid1(void) { + int con = open("/dev/console", O_RDWR | O_NOCTTY); + if (con >= 0) { + dup2(con, 0); + dup2(con, 1); + dup2(con, 2); + if (con > 2) + close(con); + } + setvbuf(stdout, NULL, _IOLBF, 0); + // The watchdog's old feeder is dead; its timer runs from its last ping + wdt_fd = open("/dev/watchdog", O_WRONLY); + if (wdt_fd >= 0) { + int t = 60; + ioctl(wdt_fd, WDIOC_SETTIMEOUT, &t); + feed(); + } + say("running as PID 1 from RAM"); + + nm_conf_t c; + if (!read_conf(&c)) { + say("cannot read " RAM_CONF ", nothing was written"); + finish(NULL, false); + } + release_old_root(); + + size_t size = 0; + uint8_t *img = map_file(c.image, &size); + if (!img || size != c.image_size) { + say("the image in RAM is not what was checked, nothing was written"); + finish(&c, false); + } + + // 1. the rootfs, with the kernel in /boot + say("writing the rootfs volume ubi%d_%d (%zu bytes)", c.ubi, c.rootfs, + size); + if (!ubi_volup(c.ubi, c.rootfs, img, size)) { + say("writing the rootfs failed: %s", strerror(errno)); + finish(&c, false); + } + if (!sha1_matches(c.ubi, c.rootfs, img, size)) { + say("the rootfs reads back different from the image"); + finish(&c, false); + } + say("rootfs written and read back: SHA-1 matches"); + munmap(img, size); + if (!strcmp(c.stop, "write")) + finish(&c, true); + + // 2. bootcmd. The old kernel volume stays until this is on flash: until + // then the old bootcmd boots it, with the new rootfs. + size_t plen, elen; + uint32_t erasesize; + uint8_t *part = read_env(c.env_mtd, &plen, &erasesize); + elen = part ? nm_env_locate(part, plen) : 0; + const char *cmd = elen ? nm_env_get(part, elen, "bootcmd") : NULL; + if (!cmd || + (strcmp(cmd, NM_BOOTCMD_FITVOL) && strcmp(cmd, NM_BOOTCMD_UBIFS))) { + say("the U-Boot env changed under the run; it boots the old kernel " + "volume with the new rootfs"); + finish(&c, false); + } + if (strcmp(cmd, NM_BOOTCMD_UBIFS)) { + if (!nm_env_set(part, elen, "bootcmd", NM_BOOTCMD_UBIFS)) { + say("no room in the env for the new bootcmd; it boots the old " + "kernel volume with the new rootfs"); + finish(&c, false); + } + bool ok = write_env(c.env_mtd, part, elen, erasesize); + // Read it back from the flash as U-Boot will + free(part); + part = read_env(c.env_mtd, &plen, &erasesize); + cmd = part && nm_env_locate(part, plen) == elen + ? nm_env_get(part, elen, "bootcmd") + : NULL; + if (!ok || !cmd || strcmp(cmd, NM_BOOTCMD_UBIFS)) { + // An env that fails its CRC gets U-Boot's built-in one, whose + // bootcmd reads the kernel volume: keep the volume, it boots. + say("writing the env failed; the kernel volume is kept, which " + "boots"); + finish(&c, false); + } + } + free(part); + say("bootcmd now loads the kernel from /boot in the rootfs"); + if (!strcmp(c.stop, "env")) + finish(&c, true); + + // 3. the kernel volume + if (!ubi_rmvol(c.ubi, c.kernel)) + say("removing the kernel volume failed (%s); it is unused now, and " + "`ipctool nand-migrate` removes it", + strerror(errno)); + else + say("kernel volume removed"); + + if (c.wipe) { + if (ubi_volup(c.ubi, c.data, NULL, 0)) + say("settings wiped"); + else + say("wiping the settings failed: %s", strerror(errno)); + } + finish(&c, true); +} + +/* ---- the command --------------------------------------------------------- */ + +static void unwind(void) { + mount(RAM_ROOT "/tmp", "/tmp", NULL, MS_MOVE, NULL); + mount(RAM_ROOT "/sys", "/sys", NULL, MS_MOVE, NULL); + mount(RAM_ROOT "/proc", "/proc", NULL, MS_MOVE, NULL); + mount(RAM_ROOT "/dev", "/dev", NULL, MS_MOVE, NULL); + umount2(RAM_ROOT, MNT_DETACH); +} + +static bool write_whole(const char *path, const void *data, size_t len, + mode_t mode) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); + if (fd < 0) + return false; + bool ok = write(fd, data, len) == (ssize_t)len; + return !close(fd) && ok; +} + +// Whether path is a file on the /tmp tmpfs, which the hand-off carries +// into the RAM root whole -- so the image needs no second copy in RAM. +static bool on_tmp_tmpfs(const char *path) { + char *mounts = read_file("/proc/mounts", NULL); + bool tmpfs = mounts && strstr(mounts, " /tmp tmpfs "); + free(mounts); + // Not under a mount of its own below /tmp, which would not move with it + struct stat f, t; + return tmpfs && !strncmp(path, "/tmp/", 5) && !stat(path, &f) && + !stat("/tmp", &t) && f.st_dev == t.st_dev; +} + +static int hand_off(nm_conf_t *c, const char *path, const uint8_t *img, + size_t size) { + char opts[64]; + bool move_tmp = on_tmp_tmpfs(path); + // This binary and a few files, plus the image when it has to be copied + snprintf(opts, sizeof opts, "size=%zuk", + (move_tmp ? 0 : size / 1024) + 4096); + mkdir(RAM_ROOT, 0755); + if (mount("tmpfs", RAM_ROOT, "tmpfs", 0, opts)) { + fprintf(stderr, "Cannot mount a RAM root on %s: %s\n", RAM_ROOT, + strerror(errno)); + return 1; + } + if (move_tmp) + snprintf(c->image, sizeof c->image, "%s", path); + else + snprintf(c->image, sizeof c->image, "%s", RAM_IMAGE); + size_t elen; + char *self = read_file("/proc/self/exe", &elen); + char conf[384]; + snprintf(conf, sizeof conf, + "ubi=%d rootfs=%d kernel=%d data=%d env_mtd=%d image_size=%zu " + "image=%s wipe=%d stop=%s\n", + c->ubi, c->rootfs, c->kernel, c->data, c->env_mtd, size, c->image, + c->wipe, *c->stop ? c->stop : "-"); + bool ok = self && !mkdir(RAM_ROOT "/sbin", 0755) && + !mkdir(RAM_ROOT "/dev", 0755) && !mkdir(RAM_ROOT "/proc", 0755) && + !mkdir(RAM_ROOT "/sys", 0755) && !mkdir(RAM_ROOT "/tmp", 01777) && + !mkdir(RAM_ROOT "/mnt", 0755) && + write_whole(RAM_ROOT "/sbin/init", self, elen, 0755) && + (move_tmp || write_whole(RAM_ROOT RAM_IMAGE, img, size, 0644)) && + write_whole(RAM_ROOT RAM_CONF, conf, strlen(conf), 0644); + free(self); + if (!ok) { + fprintf(stderr, "Cannot stage the RAM root: %s\n", strerror(errno)); + umount2(RAM_ROOT, MNT_DETACH); + return 1; + } + + printf("\nHanding PID 1 over to the RAM root. SSH ends here; the camera\n" + "writes the flash on its own and reboots when done (about a\n" + "minute). Progress goes to the serial console, and the log to\n" + "/root/nand-migrate.log once it is back.\n"); + fflush(stdout); + sync(); + + // Moved, not bound: nothing may point back into the old root + if (mount("/dev", RAM_ROOT "/dev", NULL, MS_MOVE, NULL) || + mount("/proc", RAM_ROOT "/proc", NULL, MS_MOVE, NULL) || + (move_tmp && mount("/tmp", RAM_ROOT "/tmp", NULL, MS_MOVE, NULL))) { + fprintf(stderr, "Cannot move /dev, /proc and /tmp: %s\n", + strerror(errno)); + unwind(); + return 1; + } + mount("/sys", RAM_ROOT "/sys", NULL, MS_MOVE, NULL); + if (chdir(RAM_ROOT) || syscall(SYS_pivot_root, ".", "mnt")) { + fprintf(stderr, "Cannot pivot into the RAM root: %s\n", + strerror(errno)); + (void)!chdir("/"); + unwind(); + return 1; + } + // From here PID 1's root is the RAM root too; it execs our /sbin/init + signal(SIGHUP, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + kill(1, SIGQUIT); + sleep(30); + // Still here: init did not hand over. Nothing has been written. + reboot(RB_AUTOBOOT); + return 1; +} + +static void usage(void) { + puts("Usage: ipctool nand-migrate [--dry-run] [--wipe-settings] " + "\n" + "\n" + "Moves a NAND camera off the retired layout with a `kernel` UBI\n" + "volume onto the current one, where the kernel is /boot/fitImage in\n" + "the UBIFS rootfs: writes (from the -nand- package\n" + "of the image to install) into the rootfs volume, sets the current\n" + "stock bootcmd, removes the kernel volume and reboots. U-Boot is\n" + "not touched. Settings are kept unless --wipe-settings."); +} + +int nand_migrate_cmd(int argc, char **argv) { + const struct option opts[] = { + {"dry-run", no_argument, NULL, 'n'}, + {"wipe-settings", no_argument, NULL, 'w'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0}, + }; + nm_conf_t c = {0}; + bool dry = false; + int o; + while ((o = getopt_long(argc, argv, "nwh", opts, NULL)) != -1) { + switch (o) { + case 'n': + dry = true; + break; + case 'w': + c.wipe = true; + break; + default: + usage(); + return o == 'h' ? 0 : 1; + } + } + const char *stop = getenv("IPCTOOL_MIGRATE_STOP"); + if (stop) + snprintf(c.stop, sizeof c.stop, "%s", stop); + + // What the camera is + int mtd_ubi = mtd_by_name("ubi"); + c.env_mtd = mtd_by_name("env"); + c.ubi = mtd_ubi >= 0 ? find_ubi_for_mtd(mtd_ubi) : -1; + if (c.ubi < 0 || c.env_mtd < 0) { + fprintf(stderr, "Not a NAND camera with `env` and `ubi` partitions " + "and UBI attached; nothing to migrate.\n"); + return 1; + } + ubi_vol_info_t vols[MAX_UBI_VOLS]; + int nvols = enum_ubi_volumes(c.ubi, vols, MAX_UBI_VOLS); + c.rootfs = c.kernel = c.data = -1; + for (int i = 0; i < nvols; i++) { + if (!strcmp(vols[i].name, "rootfs")) + c.rootfs = vols[i].vol_id; + else if (!strcmp(vols[i].name, "kernel")) + c.kernel = vols[i].vol_id; + else if (!strcmp(vols[i].name, "rootfs_data")) + c.data = vols[i].vol_id; + } + uint8_t head[4] = {0}; + size_t head_len = 0; + if (c.kernel >= 0) { + char dev[32]; + snprintf(dev, sizeof dev, "/dev/ubi%d_%d", c.ubi, c.kernel); + int fd = open(dev, O_RDONLY); + if (fd >= 0) { + ssize_t n = read(fd, head, sizeof head); + head_len = n > 0 ? n : 0; + close(fd); + } + } + size_t plen, elen = 0; + uint32_t erasesize = 0; + uint8_t *env = read_env(c.env_mtd, &plen, &erasesize); + if (env) + elen = nm_env_locate(env, plen); + if (!elen) { + fprintf(stderr, + "Cannot read a U-Boot env from mtd%d; nothing was " + "written.\n", + c.env_mtd); + free(env); + return 1; + } + char *cmdline = read_file("/proc/cmdline", NULL); + nm_camera_t cam = { + .has_kernel_vol = c.kernel >= 0, + .has_rootfs_vol = c.rootfs >= 0, + .has_data_vol = c.data >= 0, + .cmdline = cmdline, + .kernel_head = head, + .kernel_head_len = head_len, + .bootcmd = nm_env_get(env, elen, "bootcmd"), + .bootargs = nm_env_get(env, elen, "bootargs"), + }; + const char *why; + nm_plan_t plan = nm_check_camera(&cam, &why); + free(cmdline); + free(env); + if (plan == NM_REFUSE) { + fprintf(stderr, + "Cannot migrate this camera: %s. Nothing was " + "written.\n", + why); + return 1; + } + + if (plan == NM_DROP_KERNEL_VOL) { + // It booted with the new bootcmd, so /boot is on the running rootfs; + // confirm before taking away what the old one would boot. + struct stat st; + if (stat("/boot/fitImage", &st) && stat("/boot/uImage", &st)) { + fprintf(stderr, "bootcmd boots /boot, but the running rootfs has " + "no kernel there; the kernel volume is kept.\n"); + return 1; + } + printf("bootcmd already boots /boot/%s; only the unused kernel volume " + "ubi%d_%d is left.\n", + access("/boot/fitImage", F_OK) ? "uImage" : "fitImage", c.ubi, + c.kernel); + if (dry) + return 0; + if (!ubi_rmvol(c.ubi, c.kernel)) { + fprintf(stderr, "Removing it failed: %s\n", strerror(errno)); + return 1; + } + printf("Removed. This camera is on the current NAND layout; " + "sysupgrade upgrades it from now on.\n"); + return 0; + } + + // The image + if (optind >= argc) { + usage(); + return 1; + } + // Absolute and resolved: whether it is on /tmp decides how it is staged + char path[128]; + if (!realpath(argv[optind], path)) { + fprintf(stderr, "Cannot read %s: %s\n", argv[optind], strerror(errno)); + return 1; + } + size_t size; + uint8_t *img = map_file(path, &size); + if (!img) { + fprintf(stderr, "Cannot read %s: %s\n", path, strerror(errno)); + return 1; + } + long leb = sysfs_long(c.ubi, c.rootfs, "usable_eb_size"); + long ebs = sysfs_long(c.ubi, c.rootfs, "reserved_ebs"); + const char *kname; + why = nm_check_image(img, size, leb, &kname); + if (!why && leb > 0 && ebs > 0 && (long long)size > (long long)leb * ebs) + why = "it is bigger than the rootfs volume"; + if (why) { + fprintf(stderr, "Cannot install %s: %s. Nothing was written.\n", path, + why); + munmap(img, size); + return 1; + } + // An image on the /tmp tmpfs moves into the RAM root as it is; any + // other is copied there, which takes its size in RAM + long avail = mem_available_kb(); + long need = (on_tmp_tmpfs(path) ? 0 : (long)(size / 1024)) + 8192; + if (avail >= 0 && avail < need) { + fprintf(stderr, + "Not enough free RAM to stage %s (%ld KB available). " + "Nothing was written.\n", + path, avail); + munmap(img, size); + return 1; + } + if (!is_static_binary("/proc/self/exe")) { + fprintf(stderr, "This ipctool is dynamically linked and cannot run " + "from an empty RAM root; use a release build. Nothing " + "was written.\n"); + munmap(img, size); + return 1; + } + struct stat st; + if (access("/etc/inittab", F_OK) == 0) { + char *it = read_file("/etc/inittab", NULL); + bool restart = it && strstr(it, "::restart:/sbin/init"); + free(it); + if (!restart) { + fprintf(stderr, "/etc/inittab has no ::restart:/sbin/init, so init " + "cannot be handed the flash phase. Nothing was " + "written.\n"); + munmap(img, size); + return 1; + } + } + { + char dev[32]; + snprintf(dev, sizeof dev, "/dev/mtd%d", c.env_mtd); + int fd = open(dev, O_RDONLY); + long long zero = 0; // MEMGETBADBLOCK takes a loff_t + int bad = fd >= 0 ? ioctl(fd, MEMGETBADBLOCK, &zero) : -1; + if (fd >= 0) + close(fd); + if (bad > 0) { + fprintf(stderr, "The env's first block is bad; not writing the " + "env. Nothing was written.\n"); + munmap(img, size); + return 1; + } + } + (void)st; + + printf("Camera: retired NAND layout, UBI device ubi%d on mtd%d\n" + " kernel ubi%d_%d (FIT), rootfs ubi%d_%d (%ld KB), " + "rootfs_data ubi%d_%d\n" + " bootcmd: the kernel-volume stock one; env at mtd%d, %zu bytes\n" + "Image: %s, %zu KB, UBIFS for %ld-byte LEBs, kernel %s\n" + "Plan, from a RAM root as PID 1:\n" + " 1. write the image into ubi%d_%d and read it back\n" + " 2. set bootcmd to the current stock one (ubifsload /boot/...)\n" + " 3. remove the kernel volume ubi%d_%d\n" + " 4. %s, then reboot\n", + c.ubi, mtd_ubi, c.ubi, c.kernel, c.ubi, c.rootfs, leb * ebs / 1024, + c.ubi, c.data, c.env_mtd, elen, path, size / 1024, leb, kname, c.ubi, + c.rootfs, c.ubi, c.kernel, + c.wipe ? "wipe the settings" : "keep the settings"); + if (dry) { + printf("Dry run: nothing was written.\n"); + munmap(img, size); + return 0; + } + int ret = hand_off(&c, path, img, size); + munmap(img, size); + return ret; +} diff --git a/src/nand_migrate.h b/src/nand_migrate.h new file mode 100644 index 00000000..e828ffd1 --- /dev/null +++ b/src/nand_migrate.h @@ -0,0 +1,8 @@ +#ifndef NAND_MIGRATE_H +#define NAND_MIGRATE_H + +int nand_migrate_cmd(int argc, char **argv); +// The flash phase, run as PID 1 from the RAM root nand_migrate_cmd staged. +int nand_migrate_pid1(void); + +#endif /* NAND_MIGRATE_H */ From 5a25025ae6aa335846a7e1ca09f2e8b7b8e5331f Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:39:39 +0300 Subject: [PATCH 2/2] nand-migrate: address review -- retry the rootfs write, bound inputs - A failed rootfs write is not survivable: once UBI begins the update the old rootfs is gone, and the kernel volume kept for step 2 boots nothing without it. A write-then-swap needs the image's size free, which this layout never has (rootfs_data is autoresize). Retry from the copy in RAM instead -- a new update starts the volume over -- and say plainly what a run that cannot write it leaves behind. - realpath() into a fixed buffer could overflow it; let it allocate, and refuse a path the one-line hand-off conf cannot carry. - ${rootargs} must be an argument of its own in bootargs. - /boot and the kernel are looked up as the filesystem has them now: the newest directory entry per name wins, so a later deletion counts. - A UBIFS node is bounded by its LEB, not by the rest of the image. - An env entry that does not end inside the env is rejected, not read past. --- src/nand_layout.c | 100 ++++++++++++++++++++++++----------------- src/nand_layout_test.c | 55 ++++++++++++++++++++++- src/nand_migrate.c | 48 ++++++++++++++++---- 3 files changed, 154 insertions(+), 49 deletions(-) diff --git a/src/nand_layout.c b/src/nand_layout.c index 9d8e9629..f9c91186 100644 --- a/src/nand_layout.c +++ b/src/nand_layout.c @@ -45,6 +45,10 @@ const char *nm_env_get(const uint8_t *env, size_t len, const char *key) { const char *p = (const char *)env + 4, *end = (const char *)env + len; while (p < end && *p) { size_t n = strnlen(p, end - p); + // A CRC only proves the bytes are what was saved, not that they + // are a well-formed env: an entry must end inside it + if (n == (size_t)(end - p)) + return NULL; if (n > klen && !strncmp(p, key, klen) && p[klen] == '=') return p + klen + 1; p += n + 1; @@ -61,6 +65,8 @@ bool nm_env_set(uint8_t *env, size_t len, const char *key, const char *value) { const char *p = (const char *)env + 4, *end = (const char *)env + len; while (p < end && *p) { size_t n = strnlen(p, end - p); + if (n == (size_t)(end - p)) + goto toolong; if (!(n > klen && !strncmp(p, key, klen) && p[klen] == '=')) { // +2: this entry's NUL and the list's if (o + n + 2 > len) @@ -83,7 +89,7 @@ bool nm_env_set(uint8_t *env, size_t len, const char *key, const char *value) { memcpy(env, out, len); free(out); return true; -toolong: +toolong: // or not a well-formed env to begin with free(out); return false; } @@ -128,7 +134,8 @@ nm_plan_t nm_check_camera(const nm_camera_t *cam, const char **why) { if (cam->kernel_head_len < 4 || le32(cam->kernel_head) != 0xedfe0dd0) REFUSE("the kernel volume does not hold a FIT, so this U-Boot is not " "known to boot one from UBIFS"); - if (!cam->bootargs || !strstr(cam->bootargs, "${rootargs}")) + // An argument of its own: "x=${rootargs}y" would expand to no root= + if (!cam->bootargs || !has_word(cam->bootargs, "${rootargs}")) REFUSE("bootargs does not take its root from ${rootargs}, which the " "new bootcmd sets"); return NM_MIGRATE; @@ -155,10 +162,45 @@ static uint32_t ubifs_node(const uint8_t *p, size_t room) { return len; } +/* + * The entry for `name` in directory `parent` as the filesystem has it now: + * of every directory-entry node naming it, the one written last (highest + * sequence number). An entry that was deleted is a later node with inode 0; + * then, as when there is no entry at all, this returns 0. Otherwise the + * entry's inode, with its type in *type. + */ +static uint64_t ubifs_lookup(const uint8_t *img, size_t size, uint32_t leb, + uint64_t parent, const char *name, uint8_t *type) { + size_t nlen_want = strlen(name); + uint64_t best_sqnum = 0, best_inum = 0; + for (size_t off = 0; off + UBIFS_CH_SZ <= size; off += 8) { + const uint8_t *p = img + off; + if (p[20] != UBIFS_DENT_NODE) + continue; + // A node never crosses the end of its LEB + size_t room = leb - off % leb; + if (room > size - off) + room = size - off; + uint32_t len = ubifs_node(p, room); + if (len < 56) + continue; + uint64_t sqnum = le64(p + 8); + uint16_t nlen = p[50] | p[51] << 8; + if (le32(p + 24) == parent && nlen == nlen_want && 56u + nlen <= len && + !memcmp(p + 56, name, nlen) && sqnum >= best_sqnum) { + best_sqnum = sqnum; + best_inum = le64(p + 40); + *type = p[49]; + } + off += (len + 7) / 8 * 8 - 8; + } + return best_inum; +} + const char *nm_check_image(const uint8_t *img, size_t size, uint32_t leb_size, const char **kernel_name) { *kernel_name = NULL; - uint32_t len = ubifs_node(img, size); + uint32_t len = ubifs_node(img, size < leb_size ? size : leb_size); if (!len || img[20] != UBIFS_SB_NODE || len < 40) return "not a UBIFS image (no superblock at its start)"; if (le32(img + 36) != leb_size) @@ -167,43 +209,21 @@ const char *nm_check_image(const uint8_t *img, size_t size, uint32_t leb_size, /* * Directory entries carry their names uncompressed, so finding /boot and * the kernel in it needs no UBIFS reader: an entry's key starts with - * the inode of the directory it is in. A deleted entry has inode 0. - * Nodes are 8-byte aligned within a LEB, and LEBs are back to back. + * the inode of the directory it is in. Nodes are 8-byte aligned within + * a LEB, and LEBs are back to back. */ - uint64_t boot_ino = 0; - for (int pass = 0; pass < 2 && !*kernel_name; pass++) { - for (size_t off = 0; off + UBIFS_CH_SZ <= size; off += 8) { - const uint8_t *p = img + off; - if (p[20] != UBIFS_DENT_NODE || !(len = ubifs_node(p, size - off))) - continue; - if (len < 56) - continue; - uint32_t parent = le32(p + 24); - uint64_t inum = le64(p + 40); - uint8_t type = p[49]; - uint16_t nlen = p[50] | p[51] << 8; - const char *name = (const char *)p + 56; - if (!inum || 56u + nlen > len) - continue; - if (pass == 0 && parent == UBIFS_ROOT_INO && - type == UBIFS_ITYPE_DIR && nlen == 4 && - !memcmp(name, "boot", 4)) - boot_ino = inum; - if (pass == 1 && boot_ino && parent == boot_ino && - type == UBIFS_ITYPE_REG) { - if (nlen == 8 && !memcmp(name, "fitImage", 8)) - *kernel_name = "/boot/fitImage"; - else if (nlen == 6 && !memcmp(name, "uImage", 6) && - !*kernel_name) - *kernel_name = "/boot/uImage"; - } - off += (len + 7) / 8 * 8 - 8; + uint8_t type = 0; + uint64_t boot = + ubifs_lookup(img, size, leb_size, UBIFS_ROOT_INO, "boot", &type); + if (!boot || type != UBIFS_ITYPE_DIR) + return "there is no /boot in it, so U-Boot would find no kernel"; + static const char *const kernels[] = {"fitImage", "uImage"}; + for (size_t i = 0; i < sizeof kernels / sizeof *kernels; i++) + if (ubifs_lookup(img, size, leb_size, boot, kernels[i], &type) && + type == UBIFS_ITYPE_REG) { + *kernel_name = i ? "/boot/uImage" : "/boot/fitImage"; + return NULL; } - if (pass == 0 && !boot_ino) - return "there is no /boot in it, so U-Boot would find no kernel"; - } - if (!*kernel_name) - return "/boot holds neither fitImage nor uImage, so U-Boot would find " - "no kernel"; - return NULL; + return "/boot holds neither fitImage nor uImage, so U-Boot would find " + "no kernel"; } diff --git a/src/nand_layout_test.c b/src/nand_layout_test.c index 0a224f88..e94f6d1a 100644 --- a/src/nand_layout_test.c +++ b/src/nand_layout_test.c @@ -71,6 +71,15 @@ static void test_env(void) { CHECK(!nm_env_set(small, sizeof(small), "bootcmd", NM_BOOTCMD_UBIFS), "an overlong value accepted"); CHECK(!memcmp(copy, small, sizeof(small)), "a refused set changed env"); + + // CRC-valid but malformed: the last entry runs to the end of the env + uint8_t bad[32]; + memset(bad, 'x', sizeof(bad)); + memcpy(bad + 4, "bootcmd=", 8); + put32(bad, nm_crc32(0, bad + 4, sizeof(bad) - 4)); + CHECK(!nm_env_get(bad, sizeof(bad), "bootcmd"), + "an unterminated value returned"); + CHECK(!nm_env_set(bad, sizeof(bad), "a", "1"), "a malformed env rewritten"); free(env); } @@ -116,6 +125,11 @@ static void test_camera(void) { c.bootargs = "mem=128M root=ubi0:rootfs rootfstype=ubifs"; CHECK(PLAN(c) == NM_REFUSE && why, "bootargs without ${rootargs} accepted"); + // ${rootargs} inside another argument expands to no root= of its own + c = good; + c.bootargs = "mem=128M foo=${rootargs}bar"; + CHECK(PLAN(c) == NM_REFUSE && why, "an embedded ${rootargs} accepted"); + c = good; c.cmdline = "root=/dev/ubiblock0_1 ubi.block=0,1"; CHECK(PLAN(c) == NM_REFUSE && why, "a ubiblock camera accepted"); @@ -130,11 +144,14 @@ static void test_camera(void) { CHECK(PLAN(c) == NM_REFUSE && why, "no rootfs_data accepted"); } -/* A UBIFS node: common header + body, CRC'd as UBIFS does. */ +/* A UBIFS node: common header + body, CRC'd as UBIFS does. Each node gets + * the next sequence number, so one written later is newer, as on flash. */ +static uint64_t sqnum; static size_t node(uint8_t *p, uint8_t type, const uint8_t *body, size_t blen) { size_t len = 24 + blen; memset(p, 0, 24); put32(p, 0x06101831); + put32(p + 8, ++sqnum); put32(p + 16, len); p[20] = type; memcpy(p + 24, body, blen); @@ -200,7 +217,43 @@ static void test_image(void) { !strcmp(k, "/boot/uImage"), "a /boot/uImage image refused"); + // An entry deleted later is gone, though its old node is still there + memset(img + LEB, 0, 2 * LEB); + o = LEB; + o += dent(img + o, 1, 7, 1, "boot"); + o += dent(img + o, 7, 9, 0, "fitImage"); + o += dent(img + o, 7, 0, 0, "fitImage"); + CHECK(nm_check_image(img, 3 * LEB, LEB, &k), "a deleted fitImage accepted"); + + // ...and one written again after its deletion is back + o += dent(img + o, 7, 10, 0, "fitImage"); + CHECK(!nm_check_image(img, 3 * LEB, LEB, &k), + "a re-created fitImage refused"); + + // Newest wins wherever it sits: the deletion first in the image, but + // with the higher sequence number + memset(img + LEB, 0, 2 * LEB); + o = 2 * LEB; + o += dent(img + o, 1, 7, 1, "boot"); + o += dent(img + o, 7, 9, 0, "fitImage"); + o = LEB; + o += dent(img + o, 7, 0, 0, "fitImage"); + CHECK(nm_check_image(img, 3 * LEB, LEB, &k), + "a deletion earlier in the image but newer was ignored"); + + // A node never crosses its LEB's end: one that would is not believed + memset(img + LEB, 0, 2 * LEB); + o = LEB; + o += dent(img + o, 1, 7, 1, "boot"); + dent(img + 2 * LEB - 40, 7, 9, 0, "fitImage"); + CHECK(nm_check_image(img, 3 * LEB, LEB, &k), + "an entry across a LEB boundary accepted"); + // A corrupted entry is not believed + memset(img + LEB, 0, 2 * LEB); + o = LEB; + o += dent(img + o, 7, 9, 0, "uImage"); + o += dent(img + o, 1, 7, 1, "boot"); img[LEB + 60] ^= 1; CHECK(nm_check_image(img, 3 * LEB, LEB, &k), "a bad CRC believed"); diff --git a/src/nand_migrate.c b/src/nand_migrate.c index 3093ac00..9d363196 100644 --- a/src/nand_migrate.c +++ b/src/nand_migrate.c @@ -452,15 +452,33 @@ int nand_migrate_pid1(void) { finish(&c, false); } - // 1. the rootfs, with the kernel in /boot + /* + * 1. the rootfs, with the kernel in /boot. This is the one step a + * failure is not safe in: once UBI has begun the update, the old rootfs + * is gone, and the kernel volume kept for step 2 boots nothing without + * it. A write-then-swap needs the image's size free in the UBI device, + * and this layout has none: rootfs_data was created with autoresize. + * So a failure is retried from the copy in RAM -- a new update starts + * the volume over -- and only a run that cannot write it at all reboots, + * into a camera that needs its serial console. + */ say("writing the rootfs volume ubi%d_%d (%zu bytes)", c.ubi, c.rootfs, size); - if (!ubi_volup(c.ubi, c.rootfs, img, size)) { - say("writing the rootfs failed: %s", strerror(errno)); - finish(&c, false); + bool written = false; + for (int attempt = 1; attempt <= 3 && !written; attempt++) { + if (attempt > 1) + say("retrying the rootfs write (attempt %d of 3)", attempt); + if (!ubi_volup(c.ubi, c.rootfs, img, size)) + say("writing the rootfs failed: %s", strerror(errno)); + else if (!sha1_matches(c.ubi, c.rootfs, img, size)) + say("the rootfs reads back different from the image"); + else + written = true; } - if (!sha1_matches(c.ubi, c.rootfs, img, size)) { - say("the rootfs reads back different from the image"); + if (!written) { + say("the rootfs volume could not be written and is incomplete; the " + "camera will not boot until it is reinstalled over its serial " + "console"); finish(&c, false); } say("rootfs written and read back: SHA-1 matches"); @@ -765,11 +783,25 @@ int nand_migrate_cmd(int argc, char **argv) { return 1; } // Absolute and resolved: whether it is on /tmp decides how it is staged - char path[128]; - if (!realpath(argv[optind], path)) { + char *resolved = realpath(argv[optind], NULL); + if (!resolved) { fprintf(stderr, "Cannot read %s: %s\n", argv[optind], strerror(errno)); return 1; } + // Phase 2 is told where the image is through a one-line conf + char path[sizeof c.image]; + bool fits = strlen(resolved) < sizeof path && !strpbrk(resolved, " \t\n"); + if (fits) + strcpy(path, resolved); + else + fprintf(stderr, + "Cannot use %s: put the image at a shorter path with " + "no spaces, such as /tmp/rootfs.ubifs. Nothing was " + "written.\n", + resolved); + free(resolved); + if (!fits) + return 1; size_t size; uint8_t *img = map_file(path, &size); if (!img) {