Skip to content

sysupgrade: quiesce the overlay before erasing the medium under it - #2301

Merged
widgetii merged 2 commits into
masterfrom
sysupgrade-quiesce-overlay-before-erase
Aug 23, 2026
Merged

sysupgrade: quiesce the overlay before erasing the medium under it#2301
widgetii merged 2 commits into
masterfrom
sysupgrade-quiesce-overlay-before-erase

Conversation

@widgetii

Copy link
Copy Markdown
Member

Closes #2300.

do_wipe_overlay() hands flash_eraseall a partition that is still a mounted, read-write jffs2 with its garbage-collect thread running. Two agents then erase and write the same medium at once, and both of them notice.

The GC thread walks a filesystem being deleted underneath it:

jffs2: Header CRC failed on REF_PRISTINE node at 0x00706874: Read 0xffffffff, calculated 0x44660075
jffs2: notice: (573) jffs2_get_inode_nodes: Node header CRC failed at 0x886bac. {ffff,ffff,ffffffff,ffffffff}
jffs2: Node totlen on flash (0xffffffff) != totlen from node ref (0x0000002c)
jffs2: warning: (573) jffs2_do_read_inode_internal: no data nodes found for ino #20

and jffs2's own erase path reads back a block it has just erased and finds someone else's data in it:

jffs2: Newly-erased block contained word 0x20031985 at offset 0x003b0000

0x20031985 is a jffs2 cleanmarker — magic 0x1985, nodetype 0x2003 — the one flash_eraseall -j writes.

Why not just unmount it

It can't be. The jffs2 is the upperdir and workdir of the overlayfs that is still root for init(1), majestic, dropbear and getty:

/dev/mtdblock4 /overlay jffs2 rw,relatime 0 0
overlay / overlay rw,relatime,lowerdir=/,upperdir=/overlay/root,workdir=/overlay/work 0 0

That superblock holds a reference to the mount for as long as those processes exist, so umount is EBUSY by construction and umount -l only detaches it from our namespace while the superblock and the GC thread live on.

pivot_root does not help here and was never meant to — it moves this script's root off the partition it is about to overwrite (#2251), nobody else's, which is why enter_ramfs() deliberately leaves the old root mounted at /mnt. Nothing in the pivot changes in this PR.

What this does instead

mount -o remount,ro works on it even with the overlayfs live on top, and jffs2_remount_fs() stops the garbage-collect thread on the way to read-only. Measured on the running camera:

# ps w | grep jffs2
  573 root      0:00 [jffs2_gcd_mtd4]
# mount -o remount,ro /overlay ; echo rc=$?
rc=0
# ps w | grep jffs2
(gone)

That leaves flash_eraseall as the only thing touching the medium. Writes still arriving through the overlayfs get EROFS instead of reaching flash, which is the outcome we want a second before a reboot that wipes it anyway. flash_eraseall writes the raw /dev/mtdN character device and never needed the mount.

Shape notes:

  • The mount point is looked up in /proc/mounts, not assumed: after the pivot the overlay is at /mnt/overlay, on the in-place fallback it is still at /overlay. A hardcoded path would silently remount nothing on one of the two paths. Confirmed on hardware — the patched run reports /mnt/overlay.
  • awk and sed are already in the applet list enter_ramfs() stages, so this works after the pivot.
  • Not mounted at all (NAND/UBI rootfs_data, or a root that never mounted an overlay) → the lookup comes up empty and the function steps aside.
  • A refused remount warns and erases live, which is exactly what this did before, so the fallback is never worse than the status quo.

Verification

On hardware — lab hi3516ev300, NOR, serial console, ignore_loglevel on the cmdline (see below), the patched script pushed onto the camera and sysupgrade -n --web run with an overlay pre-loaded with ~6 MB of write history each time:

runs produced live-kernel jffs2 errors
before 4 3
after 5 0

The reset itself is still correct after a patched run:

/dev/mtdblock4   8.7M  328.0K  8.4M   4% /overlay      <- freshly wiped
/overlay/root/: crond.reboot  etc  var                 <- test churn gone
/dev/mtdblock4 /overlay jffs2 rw,relatime              <- rw again on the next boot
  573 root  0:00 [jffs2_gcd_mtd4]                      <- GC thread back
root fs writable OK ; dmesg | grep jffs2 -> nothing

An empty overlay is silent either way — that is why a bare flash_eraseall loop reproduces nothing and why this hides on a bench that has just been reset. It needs a populated one.

In the test suite.github/scripts/test_sysupgrade.sh, 107 checks green. Five are new:

ok   --wipe_overlay quiesces the overlay read-only BEFORE erasing it
ok   a refused remount warns and still erases (no worse than the old behaviour)
ok   an unmounted rootfs_data is erased with no remount attempted
ok   do_wipe_overlay quiesces before it erases
ok   quiesce_overlay looks the mount point up (it moves with the pivot)

Checked that they fail against the unpatched script rather than passing vacuously — four of the five do (the fifth is the no-op path, which is correct either way):

FAIL --wipe_overlay must remount the overlay ro before the erase
FAIL a refused remount must not block the erase
FAIL do_wipe_overlay must call quiesce_overlay before flash_eraseall
FAIL quiesce_overlay must read /proc/mounts; the overlay is at /mnt/overlay after the pivot

The harness gains a remount branch in the mount stub (logged, with STUB_REMOUNT_RC to inject a kernel that refuses), a jffs2 line in the fake /proc/mounts plus set_mounts no-overlay to remove it, and a logged_at helper so the ordering can be asserted rather than assumed.

Scope

This does not touch #2298 — the camera that fails to come back after a reset does so because U-Boot boots an unverified uImage that occasionally has a bit flipped in it. One of the five runs above still needed a power cycle for that reason; its jffs2 side was clean. The two are independent.

Footnote

None of these kernel messages are visible on a stock camera: /etc/sysctl.conf sets kernel.printk = 3 3 1 3 and S02sysctl applies it, but the live value is 0 0 0 0 on hi3516ev300, hi3516cv300 and gk7205v200 (ssc30kq keeps 3 3 1 3). console_loglevel=0 suppresses even KERN_EMERG. Everything above needed ignore_loglevel on the kernel cmdline to see at all. Worth fixing separately — a camera that panics is currently indistinguishable from one that hung.

do_wipe_overlay hands flash_eraseall a partition that is still a mounted,
read-write jffs2 with its garbage-collect thread running, so two agents
erase and write the same medium at once. Both of them notice.

The GC thread walks a filesystem being deleted underneath it:

  jffs2: Header CRC failed on REF_PRISTINE node at 0x00706874: Read 0xffffffff
  jffs2: notice: (573) jffs2_get_inode_nodes: Node header CRC failed at 0x886bac
  jffs2: Node totlen on flash (0xffffffff) != totlen from node ref (0x0000002c)
  jffs2: warning: (573) jffs2_do_read_inode_internal: no data nodes found for ino #20

and jffs2's own erase path reads back a block it has just erased and finds
somebody else's data in it:

  jffs2: Newly-erased block contained word 0x20031985 at offset 0x003b0000

0x20031985 is a jffs2 cleanmarker -- magic 0x1985, nodetype 0x2003 -- the
one `flash_eraseall -j` writes.

The overlay cannot be unmounted. It is the upperdir and workdir of the
overlayfs that is still root for init(1), majestic, dropbear and getty, so
that superblock holds a reference to the mount for as long as they exist:
umount is EBUSY by construction, and umount -l detaches it from our
namespace while the superblock and the GC thread live on. pivot_root does
not help and was never meant to -- it moves this script's own root off the
partition it is about to overwrite, nobody else's, which is why enter_ramfs
deliberately leaves the old root mounted at /mnt.

Remounting it read-only is enough: jffs2_remount_fs() stops the
garbage-collect thread on the way to ro, leaving flash_eraseall as the only
thing touching the medium. Writes still arriving through the overlayfs get
EROFS instead of reaching flash, which is what we want a second before a
reboot that wipes it anyway. flash_eraseall writes the raw /dev/mtdN
character device and never needed the mount.

The mount point is looked up in /proc/mounts rather than assumed: after the
pivot the overlay is at /mnt/overlay, and on the in-place fallback it is
still at /overlay. A refused remount warns and erases live, which is exactly
what this did before, so the fallback is never worse than the status quo.

Measured on a lab hi3516ev300 with an overlay carrying real write history:
3 of 4 runs logged live-kernel jffs2 errors before, 0 of 5 after, with the
reset itself still correct (overlay back to 328K, rw and the GC thread
restored on the next boot, dmesg clean). An empty overlay is silent either
way, which is why this hides on a bench that has just been reset.

Refs #2300. Split out of #2298, which is about the unverified uImage read
that actually bricks the camera; this is the other half and is independent.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

sysupgrade: remount overlay read-only before wiping rootfs_data

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Quiesce overlay JFFS2 by remounting read-only before running flash_eraseall.
• Discover overlay mountpoint via /proc/mounts to work across pivot and fallback paths.
• Add regression tests asserting remount/erase ordering and safe fallbacks.
Diagram

graph TD
  T["test_sysupgrade.sh"] --> S["sysupgrade (--wipe_overlay)"] --> Q["quiesce_overlay()"] --> P[/"/proc/mounts"/] --> M["mount -o remount,ro"] --> E["flash_eraseall (-j)"] --> D[("rootfs_data MTD")]

  subgraph Legend
    direction LR
    _p["Process"] ~~~ _f[/"File"/] ~~~ _d[("Device")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Kill users + unmount overlay
  • ➕ Eliminates the mounted filesystem before erase (no remount refusal path).
  • ➖ Risky during upgrade: may kill critical processes (init dependents, web/ssh), destabilizing the system before reboot.
  • ➖ Unmount is inherently EBUSY due to overlayfs upperdir/workdir references; often not achievable without heavy-handed process teardown.
2. Switch overlay upperdir to tmpfs before wipe
  • ➕ Avoids touching the live flash-backed upperdir during erase; writes continue to tmpfs.
  • ➖ More invasive: requires reconfiguring overlay/root while running, higher chance of breaking recovery paths.
  • ➖ Complexity disproportionate to the goal of preventing concurrent JFFS2 GC writes right before reboot.

Recommendation: Proceed with the current approach: remounting the JFFS2 upperdir read-only is the least invasive way to stop JFFS2 GC and prevent concurrent flash writes during flash_eraseall. The mountpoint lookup via /proc/mounts is the right robustness measure across pivot vs in-place fallback, and the ‘warn then erase live’ fallback preserves prior behavior when remount is refused.

Files changed (2) +130 / -4

Bug fix (1) +50 / -1
sysupgradeQuiesce overlay by remounting rootfs_data JFFS2 read-only before erase +50/-1

Quiesce overlay by remounting rootfs_data JFFS2 read-only before erase

• Bumps script version and adds quiesce_overlay(), which maps rootfs_data to its mounted /dev/mtdblockN entry, finds its mountpoint via /proc/mounts, syncs, and attempts a remount read-only. do_wipe_overlay() now calls quiesce_overlay before marking flash dirty and running flash_eraseall, warning (but continuing) if the remount is refused.

general/overlay/usr/sbin/sysupgrade

Tests (1) +80 / -3
test_sysupgrade.shAdd overlay-quiesce stubs and ordering assertions for --wipe_overlay +80/-3

Add overlay-quiesce stubs and ordering assertions for --wipe_overlay

• Introduces a controllable /proc/mounts fixture (including an optional JFFS2 overlay line) and enhances the mount stub to log remount operations with injectable failure via STUB_REMOUNT_RC. Adds helper functions to assert that remount,ro occurs and that it precedes flash_eraseall, plus static checks ensuring do_wipe_overlay calls quiesce_overlay and that quiesce_overlay consults /proc/mounts.

.github/scripts/test_sysupgrade.sh

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

qodo-free-for-open-source-projects Bot commented Aug 23, 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


Remediation recommended

1. UBIFS overlay not quiesced ✓ Resolved 🐞 Bug ≡ Correctness
Description
quiesce_overlay() only looks up the overlay mount by matching /proc/mounts device field against
/dev/mtdblockN, but on UBIFS systems init mounts rootfs_data as ubi0:rootfs_data, so the
lookup comes up empty and the remount-to-RO never happens. This leaves the “erase live filesystem”
race intact on UBIFS-based overlays while the script now implies it is quiescing the overlay before
erasing.
Code

general/overlay/usr/sbin/sysupgrade[R348-351]

+	local blk=$(get_device "rootfs_data" | sed 's|/dev/mtd|/dev/mtdblock|')
+	local mp=$(awk -v d="$blk" '$1 == d { print $2; exit }' /proc/mounts)
+	# Not mounted (NAND/UBI, or an overlay this camera never mounted): the erase
+	# is unopposed already and there is nothing to quiesce.
Evidence
Init mounts UBIFS overlays using ubi0:rootfs_data, but quiesce_overlay() only searches
/proc/mounts for /dev/mtdblockN, so on UBIFS boots it will not find a mountpoint and will skip
the remount.

general/overlay/usr/sbin/sysupgrade[347-353]
general/overlay/init[18-24]

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

## Issue description
`quiesce_overlay()` derives a block device path (`/dev/mtdblockN`) from `get_device rootfs_data` and then finds the mountpoint by matching that exact string in `/proc/mounts`. On boards booted with UBIFS, `general/overlay/init` mounts the overlay as `mount -t ubifs ubi0:rootfs_data /overlay`, so `/proc/mounts` contains `ubi0:rootfs_data` rather than `/dev/mtdblockN`. The new quiesce therefore becomes a no-op on UBIFS overlays.
### Issue Context
This PR is explicitly about preventing kernel-side filesystem activity (GC/background) from racing with `flash_eraseall`. That race isn’t specific to JFFS2; on UBIFS systems you still want the filesystem taken read-only (or otherwise quiesced) before erasing the underlying MTD.
### Fix Focus Areas
- general/overlay/usr/sbin/sysupgrade[347-360]
- general/overlay/init[18-34]
### Suggested fix
Extend the `/proc/mounts` lookup to also detect the UBIFS mount source used by init.
Concretely, after computing `blk`, if no match is found, add a secondary match for UBIFS, e.g.:
- match `$1 == "ubi0:rootfs_data"` (consistent with `general/overlay/init`), or
- more generically, match `$1 ~ /^ubi[0-9]+:rootfs_data$/`.
Then remount the resulting mountpoint read-only the same way.
Keep the existing behavior (warn-but-continue) if remount fails.

ⓘ 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 turn these tips off under Display preferences

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread general/overlay/usr/sbin/sysupgrade Outdated
The mount-point lookup matched only /dev/mtdblockN, but general/overlay/init
mounts the same partition as "ubi0:rootfs_data" on a NAND camera, so the
quiesce silently did nothing on every UBI board while the script claimed
otherwise. The race is the same there and so is the cure: ubifs_remount_ro()
stops the background thread and commits, exactly as jffs2_remount_fs() stops
the GC thread.

Match both spellings, and cover the UBI one in the test suite -- it fails
against the previous commit, so it pins the gap rather than restating it.

Reported by Qodo on #2301.
@widgetii
widgetii merged commit f5c7518 into master Aug 23, 2026
314 of 321 checks passed
@widgetii
widgetii deleted the sysupgrade-quiesce-overlay-before-erase branch August 23, 2026 19:59
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.

sysupgrade: do_wipe_overlay() erases a mounted, read-write jffs2 while the kernel is still using it

1 participant