Skip to content

upgrade: move a NOR camera to another layout, keeping its bootloader and settings - #247

Merged
openipc-ai merged 2 commits into
masterfrom
upgrade-repartition
Oct 14, 2026
Merged

openipc-ai merged 2 commits into
masterfrom
upgrade-repartition

Conversation

@openipc-ai

Copy link
Copy Markdown
Contributor

ipctool upgrade could already write a new partition layout over the running system. On a lite-to-ultimate move on 16 MB NOR (kernel 2 → 3 MB, rootfs 5 → 10 MB at a new offset) it would have bricked the camera it was most needed on.

What was wrong

  • Partition names. The generated bundle skipped partitions boot and env by name. A XiongMai flash calls the 64 KB after boot wtf. The skip found nothing, said nothing, and the kernel went over it.
  • Bootloader environment. bootargs and bootcmd were regenerated, which drops what a vendor bootloader put there (an mmz carve-out, init=).
  • rootfs_data. It was never touched. After a repartition it starts in the middle of the old one, and jffs2 mounts the stale nodes.

What a bundle with no prefix, offset or skip now does

  • Keeps the front of the flash. Every partition in front of the first one the bundle writes (the kernel) stays as this flash names and sizes it. A XiongMai bootloader keeps its environment at 0x30000 inside boot, so "keep up to the environment" is not enough.
  • Edits the environment. Only root=, mtdparts= and the size of the one sf read change (src/upgrade_env.c). If it cannot find them, it refuses before anything is stopped.
  • Carries the settings. The whole new rootfs_data is written, with the settings in it as a jffs2 image (src/jffs2.c).
    • The image is taken once the old overlay is read-only.
    • It is also built beforehand as a trial, so settings that will not fit are refused while nothing is stopped.
    • --wipe-settings starts from defaults instead.
  • Sets variables. The U-Boot variables in the bundle's env object are set, e.g. the upgrade URL of the new firmware.
  • Builds the bundle. tools/upgrade_bundle.py --layout nor8m|nor16m -f <release.tgz> [-e KEY=VALUE] makes one.

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, about 10 MB more on a camera with about 40 MB available.
  • setenv matches the whole variable name, so bootcmd no longer edits bootcmdnor.
  • add_mtdpart no longer reads dst[-1] on an empty prefix.
  • New tests upgrade_env_test and jffs2_test run in pr-build-check.
  • README: a section on moving a NOR camera to another layout.

Tested

QEMU (hi3516ev300), booting a full flash dump of a lab XiongMai 85H50AI on lite, its own bootloader included:

  • One ipctool upgrade moved it to an ultimate image. U-Boot read 3 MB at 0x50000, with root on mtdblock3.
  • ssh still took the old password. Every overlay file was byte-identical, except the two rewritten on every boot (fake-hwclock, seedrng).
  • boot and wtf stayed byte-identical.

Refusals, each with services still running and the flash unchanged:

  • an oversized rootfs;
  • 3 MB of settings for a 2.7 MB rootfs_data.

--wipe-settings came up fresh: the customizer ran, then rebooted.

The real camera, the same 85H50AI: migrated over ssh and back in 93 s, on the standard ultimate layout 256k(boot),64k(wtf),3072k(kernel),10240k(rootfs),-(rootfs_data), with the same results as QEMU.

…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.
@openipc-ai

Copy link
Copy Markdown
Contributor Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Settings vanish when mount lookup misses ✓ Resolved
Description
do_upgrade() treats a failed jffs2_mountpoint() lookup as an empty settings partition and
registers an all-erased buffer for the new rootfs_data. If existing settings are mounted under a
device name other than the exact /dev/mtdblockN string the lookup requires, flashing proceeds
without copying them.
Code

src/backup.c[R1614-1617]

+        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");
Evidence
The lookup requires one exact device spelling; on failure, the already-erased region is still
registered for flashing, and the read-only carry callback is skipped.

src/backup.c[1213-1230]
src/backup.c[1597-1629]
src/backup.c[1658-1660]
src/backup.c[908-940]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed exact-string mount lookup currently causes an upgrade to erase existing settings without carrying them.
## Fix Focus Areas
- src/backup.c[1213-1231]
- src/backup.c[1608-1629]
## Recommended Fix
Identify the mounted settings filesystem robustly, and refuse the upgrade before isolation whenever settings may exist but cannot be located. Allow an empty replacement only for an explicitly verified empty source or `--wipe-settings`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Deleted files can reappear after upgrade ✓ Resolved
Description
jffs2_add_tree() counts extended attributes but emits no nodes or values for them, so the carried
image loses OverlayFS opaque-directory markers. When an upper directory uses such a marker to hide
entries in the rootfs below it, those entries can become visible again after the new image boots.
Code

src/jffs2.c[R248-251]

+        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,
Evidence
Attribute names are only counted, while the emitted image contains inode and directory-entry nodes;
the upgrade itself warns that a dropped opaque-directory attribute exposes the lower layer but still
accepts the image.

src/jffs2.c[216-226]
src/jffs2.c[248-281]
src/backup.c[1236-1256]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The settings image drops extended attributes, including the opaque-directory marker needed to preserve OverlayFS deletion semantics.
## Fix Focus Areas
- src/jffs2.c[216-226]
- src/jffs2.c[248-257]
- src/backup.c[1236-1255]
## Recommended Fix
Serialize the necessary JFFS2 extended-attribute nodes and verify an opaque directory survives a round trip. If that cannot be supported, refuse to carry settings containing those attributes rather than writing an image with changed visibility.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Bundle boot variables do not take effect ✓ Resolved
Description
do_upgrade() applies the bundle's env entries before replacing bootcmd and bootargs with its
generated values. If a bundle supplies either variable, the upgrade prints that it was set but
persists the later generated value instead, on both the new keep-front path and the existing upgrade
path.
Code

src/backup.c[R1666-1669]

+    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);
Evidence
The new generic environment loop runs first; both subsequent paths set generated boot variables, and
the ROM setter persists the resulting in-memory environment.

src/backup.c[1663-1677]
src/backup.c[1699-1724]
src/uboot.c[133-194]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Bundle-provided bootargs and bootcmd are reported as set but are overwritten before the environment is persisted.
## Fix Focus Areas
- src/backup.c[1663-1677]
- src/backup.c[1699-1724]
- tools/upgrade_bundle.py[157-163]
## Recommended Fix
Define whether layout bundles may override generated boot variables. Either reject conflicting `--env` keys before flashing with a clear error, or apply permitted overrides after generation while ensuring the final values boot the new layout.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Carried settings can leave the overlay unwritable ✓ Resolved
Description
build_carry creates the image with jffs2_new(c->erasesize, c->region_len), so reserve() allows
the carried settings to fill the whole new rootfs_data partition, and the builder writes every
node uncompressed. Settings that fit the ~2.7 MB ultimate rootfs_data with no spare erase blocks
pass the pre-check (the original compressed overlay could be much smaller than this). The camera
then boots with a full jffs2: it mounts, but jffs2 needs a few free blocks reserved for garbage
collection, so the first config save or password change fails with ENOSPC.
Code

src/backup.c[R1236-1239]

+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");
Evidence
The limit passed to the builder is the full partition length, and reserve refuses only when `len +
more > limit`. Nothing leaves headroom for jffs2's reserved/GC blocks. Every data node is written
with COMPR_NONE in page-sized pieces, so the image is at least as big as the raw file contents. The
new ultimate rootfs_data (16 MB − 0x50000 − 13 MB ≈ 2.69 MB) is far smaller than the lite one it
replaces.

src/backup.c[1236-1246]
src/backup.c[1597-1601]
src/jffs2.c[75-79]
src/jffs2.c[140-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The settings image may grow to the full size of the new rootfs_data partition. A carried overlay that fits exactly leaves jffs2 with no free erase blocks for garbage collection, so the camera cannot write settings after the migration.
## Fix Focus Areas
- src/backup.c[1236-1239]
- src/jffs2.c[75-79]
## Recommended Fix
In `build_carry`, pass a smaller limit to `jffs2_new`, e.g. `c->region_len - N * c->erasesize`, where N is at least 4–5 erase blocks (or a percentage of the partition), and refuse when the region is not larger than that headroom. Update the refusal message so the user knows the settings must leave free space.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/backup.c Outdated
Comment thread src/jffs2.c
Comment thread src/backup.c Outdated
Comment thread src/backup.c
…le 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant