Skip to content

sysupgrade: verify the rootfs before flashing the kernel, and bound the verify-mount#2220

Open
wkumik wants to merge 1 commit into
OpenIPC:masterfrom
wkumik:fix/sysupgrade-verify-before-flash
Open

sysupgrade: verify the rootfs before flashing the kernel, and bound the verify-mount#2220
wkumik wants to merge 1 commit into
OpenIPC:masterfrom
wkumik:fix/sysupgrade-verify-before-flash

Conversation

@wkumik

@wkumik wkumik commented Jul 11, 2026

Copy link
Copy Markdown

Problem

do_update_rootfs() loop-mounts the new rootfs to read its SoC stamp and version before writing it. The check itself is reasonable — but its failure path has three problems, and together they can strand a device.

1. It runs too late — a failure leaves the device half-flashed

The kernel is flashed first, and the rootfs is verified after:

[ "1" = "$update_kernel" ] && do_update_kernel "$kernel_file"
[ "1" = "$update_rootfs" ] && do_update_rootfs "$rootfs_file"   # verification lives in here

So a rootfs that fails verification is only discovered once the kernel has already been committed. The device reboots with a new kernel on an old rootfs and does not recover by itself. The user sees a flash that "didn't take", with no obvious reason.

2. It can hang forever

The mount does not always fail on a rootfs the running kernel cannot read — it can block indefinitely. And there is no output at all between the echo and the flash write:

echo "Update rootfs from $x"                                    # ← last line the user ever sees
if mkdir -p "$y" && loop=$(losetup -f) && losetup "$loop" "$x" && mount "$loop" "$y"; then

Everything in that condition is silent, so a wedged mount is indistinguishable from a dead tool: the upgrade stops with no error, no timeout, and no way to tell what happened. Any GUI driving sysupgrade goes down with it.

We hit this on an SSC338Q air unit. Verbatim, from the flashing tool's log:

20:40:38  Kernel updated to 06:49:22 2026-06-15     <- kernel written + verified (2024/2024 kb)
20:40:38  RootFS
20:40:38  Update rootfs from /tmp/rootfs.squashfs.ssc338q
          <<< zero further output, ever >>>

It never emitted another byte. Note do_update_kernel() has no mount and goes straight to flashcp, which is exactly why the kernel writes fine and streams progress while the rootfs produces nothing — the asymmetry is structural.

3. It is fatal when it need not be

die "Unable to mount $y!" rejects the flash. But an unmountable rootfs is usually not a defect in the image — it means the running kernel lacks the squashfs decompressor the new image uses (commonly XZ). That is a property of the kernel being replaced, and it stops mattering the moment the new kernel boots. The mount is only a pre-check: flashcp writes the partition raw and never needs it. So a perfectly good image is refused on a criterion that doesn't apply to it.

Fix

Split verification out of the write into verify_rootfs(), and:

  • Run it before the first flashcp. A rootfs problem now costs nothing — nothing has been written, and the device stays on its working firmware. This alone removes the half-flashed state.
  • Bound the mount with timeout (falling back to a bare mount where the applet is absent), so the pre-flight check can never outlive the flash it guards.
  • Warn instead of die when the image cannot be mounted, and let flashcp write it.

The SoC check is preserved

Mounting the rootfs is how /etc/hostname is read to confirm the image matches the SoC, so it isn't dropped lightly:

  • Mount fails, kernel flashed in the same run → the uImage header carries the SoC and check_soc validates it with no mount at all, so the run is still guarded. Warn and proceed.
  • Mount fails, rootfs-only flash → no SoC evidence exists, and writing an unverified rootfs for a foreign SoC bricks the device. This case still refuses — before anything is written — and tells the user to flash the matching kernel in the same run or pass --skip_soc.

Same-version skip, --skip_ver, --skip_soc and the combined-image path all behave as before (do_update_firmware verifies before its split write too).

Testing

sh -n and dash -n parse clean. Behavioural harness against a stubbed device, 12/12, asserting in every failure case that the kernel was never written:

scenario expected result
mount OK, new version verify → flash kernel → flash rootfs
mount OK, same version rootfs write skipped (unchanged)
mount fails, kernel in same run warn, flash both
mount hangs, kernel in same run timeout, flash both — no hang
mount fails, rootfs-only refuse, write nothing

One file, +67/−8. scr_version bumped to 1.0.54.

…he mount

do_update_rootfs() loop-mounts the new rootfs to read its SoC stamp and version
before writing it. Three problems with that check, all in the failure path:

1. It runs too late. The kernel is flashed first and the rootfs is verified
   afterwards, so a rootfs that fails verification is only discovered once the
   kernel has already been committed. The device is left half-upgraded — new
   kernel, old rootfs — and does not recover on its own.

2. It can hang forever. The mount does not merely fail on a rootfs the running
   kernel cannot read; it can block indefinitely. Nothing is printed between the
   mount and the flash write, so a wedged mount is indistinguishable from a dead
   tool: the upgrade stops with no error and no timeout, and takes any GUI
   driving it down with it. Reported as a flashing tool "stuck at 60% forever",
   which is exactly the kernel-done/rootfs-about-to-start boundary.

3. It is fatal when it need not be. An unmountable rootfs is not a defect in the
   image — it means the RUNNING kernel lacks the squashfs decompressor the NEW
   image uses (commonly XZ), a property of the kernel being replaced and
   irrelevant the moment the new one boots. The mount is only a pre-check;
   flashcp writes the partition raw and never needs it. Aborting rejects an image
   that would have flashed correctly.

So: split verification out of the write into verify_rootfs(), run it before the
first flashcp, bound the mount with `timeout`, and warn rather than die when the
image cannot be mounted.

The SoC check is preserved. Mounting the rootfs is how its /etc/hostname is read
to confirm the image matches the SoC, so it is not dropped lightly. When the
mount fails but a kernel is flashed in the same run, the uImage header carries
the SoC and is validated with no mount at all, so that case stays covered. A
rootfs-only flash of an unmountable image has no SoC evidence, and writing an
unverified rootfs for a foreign SoC bricks the device — that case still refuses,
before anything is written, and says how to proceed.

Net effect: a rootfs problem now costs nothing instead of stranding the device
half-flashed, and the upgrade can no longer hang with no output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLuRBpBJzbd6Uba5U7u7Qy
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