Skip to content

restore/upgrade: stop running from the flash before rewriting it - #224

Merged
widgetii merged 2 commits into
masterfrom
restore-isolate-from-flash
Sep 29, 2026
Merged

widgetii merged 2 commits into
masterfrom
restore-isolate-from-flash

Conversation

@widgetii

Copy link
Copy Markdown
Member

Fixes #223.

restore and upgrade rewrote the whole flash while the system was still executing from it. umount_all() only detached mounts lazily and skipped /, so every process kept paging from the root squashfs. The first page fault on a rewritten block killed the system and left the flash half written.

What changes

A new step, isolate_from_flash(), runs after the dry run (Analyzing) and before the first erase:

  • ipctool pins itself in RAM (mlockall), so it is never paged in from flash.
  • Every other userspace process gets SIGKILL, except init.
    • Not kill(-1): that also reaches kernel threads that accept signals, such as jffs2_gcd_*.
    • Not SIGTERM: udhcpc -R releases its lease on a graceful exit, which takes down a network share holding the log.
  • The watchdog is taken over if a killed process was feeding it, and fed through the whole flash.
    • On a Hi3516EV300, write() returns 0 and the board resets 10 s after the open, so every keepalive form is sent, including the HiSilicon one from watchdog.c.
    • The HISINEW_* defines move to watchdog.h so both files share them.
  • Writable flash filesystems are remounted read-only, overlay first; that stops the jffs2 GC. Then they are unmounted.
  • No global sync() from then on, since it could hang on an unreachable NFS mount. reboot_with_msg() fsyncs only the log, so a log on a share keeps its ending.

A terminal on stdout/stderr is redirected to /dev/console; a redirect to a file is left alone.

Also fixed: xm_disable_watchdog() returned failure whenever one of the two EV300 watchdog modules was not loaded (open_wdt → ENOENT). That aborted every XM restore on such a board.

Testing

Tested on a Hi3516EV300 XM board (XT25F128B, 16M NOR), UPX-packed arm32 build, log redirected to NFS, recovery through U-Boot TFTP on standby:

Direction Started from Result
XM firmware → OpenIPC telnet, Sofia running, squashfs root boot/kernel/rootfs read back identical to the image
OpenIPC → XM (the #223 case) ssh, majestic holding /dev/watchdog, overlay root boot/romfs/usr/web/custom identical; mtd differs only in jffs2 blocks Sofia wrote after boot; Sofia up

Each direction passed twice, the last round with the final binary. ipctool backup on OpenIPC produces a file whose SHA1s all match.

Host build and unit tests pass; the arm32 and arm64 cross builds are clean.

restore and upgrade rewrote the whole flash while the system kept executing
from it. umount_all() only detached mounts lazily and left "/" alone, so
every process kept paging from the root squashfs, and the first fault on a
rewritten block took the system down with the flash half written.

isolate_from_flash() now runs between the dry run and the first erase:

- mlockall() so ipctool itself is never paged in from flash;
- SIGKILL every userspace process but init (not kill(-1), which also hits
  signal-accepting kernel threads; not SIGTERM, since `udhcpc -R` drops the
  address on a graceful exit);
- if one of them held /dev/watchdog, take it over and feed it through the
  whole flash. The HiSilicon SDK driver ignores write() and the standard
  keepalive, so every form is sent; the HISINEW_* ioctls move to watchdog.h;
- remount every writable flash filesystem read-only (stopping the jffs2
  garbage collector), then unmount;
- no global sync() from then on, as it would wait on a dead network mount;
  reboot fsync()s just the log.

xm_disable_watchdog() failed whenever one of the two EV300 watchdog modules
was not loaded, which aborted every restore on such a board; a module that
is absent is now fine.

Tested on a Hi3516EV300 XM board with 16M NOR, both ways, with the log on
NFS: XM firmware -> OpenIPC and OpenIPC -> XM (the #223 case). All
partitions read back identical to the image, apart from jffs2 blocks written
after boot.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Isolate restore and upgrade from flash before rewriting it

🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Stop userspace and remount writable flash filesystems read-only before restore or upgrade erases
 flash.
• Keep the watchdog fed during flashing and preserve redirected logs through reboot.
• Allow XM restores when an optional watchdog module is absent.
Diagram

graph TD
  H["XM preflight"] --> A["Dry-run validation"] --> B["Pin in RAM"] --> C["Stop userspace"] --> D["Watchdog takeover"] --> E["Isolate mounts"] --> F["Rewrite flash"] --> G["Fsync and reboot"]
  D -.->|"keepalive"| F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Flash from an initramfs recovery environment
  • ➕ Provides stronger separation from the flash being rewritten.
  • ➕ Avoids relying on process termination and successful remounts for isolation.
  • ➖ Requires a recovery boot path and additional deployment support across devices.
  • ➖ Substantially broadens the scope of this targeted fix.

Recommendation: The in-process isolation step is a pragmatic fix for existing devices without changing their boot flow. An initramfs flasher is the stronger long-term option if more platforms need a hard isolation guarantee; this approach currently continues after failures to lock memory or remount flash read-only.

Files changed (4) +241 / -16

Bug fix (2) +233 / -13
backup.cIsolate the system before restore and upgrade writes +219/-4

Isolate the system before restore and upgrade writes

• Adds a shared post-validation step that attempts to pin ipctool in RAM, kills other userspace processes, takes over an active watchdog, and remounts writable flash filesystems read-only before unmounting. Feeds the watchdog during MTD and UBI writes, avoids global sync after isolation, and fsyncs redirected output before reboot.

src/backup.c

xm.cTolerate absent XM watchdog modules +14/-9

Tolerate absent XM watchdog modules

• Treats ENOENT when unloading a watchdog module as success, so a firmware that loads only one of the EV300 watchdog modules does not abort restore. Other unload failures remain errors.

src/boards/xm.c

Refactor (2) +8 / -3
watchdog.cUse shared HiSilicon watchdog ioctl definitions +0/-3

Use shared HiSilicon watchdog ioctl definitions

• Removes local HiSilicon ioctl definitions now supplied by the shared watchdog header.

src/watchdog.c

watchdog.hShare HiSilicon watchdog ioctl definitions +8/-0

Share HiSilicon watchdog ioctl definitions

• Exposes the HiSilicon-specific keepalive and set-options ioctls to both the watchdog command and the flash isolation code.

src/watchdog.h

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

qodo-free-for-open-source-projects Bot commented Sep 29, 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. Flash writes continue after lock failure ✓ Resolved
Description
isolate_from_flash() only logs a failed mlockall(MCL_CURRENT | MCL_FUTURE) call and returns
without ensuring the flashing process's pages are resident. When locking fails, restore and upgrade
still begin erasing live-root flash, allowing a later fault for code or data from the filesystem
being rewritten to interrupt the write.
Code

src/backup.c[R555-556]

+    if (mlockall(MCL_CURRENT | MCL_FUTURE))
+        fprintf(stderr, "mlockall: %s\n", strerror(errno));
Evidence
The failed mlockall() call is only logged and does not change control flow; both restore and
upgrade begin their writing passes immediately after isolation returns. Thus neither caller prevents
an erase when the flashing process may still need to fault in pages from the live-root flash.

Prevent restore from overwriting a live root filesystem unsafely
Protect upgrade from live-root overwrite
src/backup.c[554-556]
src/backup.c[1011-1014]
src/backup.c[1339-1341]
src/backup.c[416-425]
src/backup.c[1010-1015]
src/backup.c[1337-1342]

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 `mlockall()` leaves the flashing process dependent on the root filesystem it is about to overwrite.
## Fix Focus Areas
- src/backup.c[554-556]
- src/backup.c[1010-1015]
- src/backup.c[1337-1342]
## Recommended Fix
Make isolation return a failure status when `mlockall()` fails, and have both restore and upgrade abort before their writing passes begin or any flash is erased.

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


2. Writable flash survives failed remounts ✓ Resolved
Description
remount_flash_ro() logs remount errors but does not pass a failure status to
isolate_from_flash(). If the root overlay or another flash filesystem cannot be remounted
read-only, isolation continues, leaves / mounted, and both restore and upgrade can proceed to
erase the underlying device while the filesystem remains writable.
Code

src/backup.c[R538-540]

+        if (mount(NULL, paths[n], NULL, MS_REMOUNT | MS_RDONLY, NULL))
+            fprintf(stderr, "Cannot remount '%s' read-only: %s\n", paths[n],
+                    strerror(errno));
Evidence
The remount loop only logs failures, so they do not stop isolation; the subsequent unmount logic
explicitly leaves the root mount in place. Isolation then returns to the restore or upgrade path,
which can enter do_flash() without confirming that the flash filesystems were made safe.

Prevent restore from overwriting a live root filesystem unsafely
Protect upgrade from live-root overwrite
src/backup.c[519-544]
src/backup.c[372-380]
src/backup.c[605-610]
src/backup.c[1011-1014]
src/backup.c[1339-1341]
src/backup.c[605-611]
src/backup.c[1010-1015]
src/backup.c[1337-1342]

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 read-only remount can leave a flash filesystem writable while restore or upgrade proceeds to erase its underlying device.
## Fix Focus Areas
- src/backup.c[519-544]
- src/backup.c[605-611]
- src/backup.c[1010-1015]
- src/backup.c[1337-1342]
## Recommended Fix
Return a failure status when mount information cannot be read or any flash-filesystem remount fails. Propagate that status through isolation and prevent both restore and upgrade from starting a real flash write before the required flash filesystems have been made safe.

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


3. Init can fault on erased root pages ✓ Resolved
Description
kill_userspace() exempts PID 1, while mlockall() locks only the flashing process and
isolate_from_flash() does not verify that init’s reaping code and libraries are resident. When the
root filesystem is flash-backed, init can wake to reap killed processes during either writing pass
and fault in an executable page from a block that has been rewritten.
Code

src/backup.c[R495-497]

+        pid_t pid = atoi(p->d_name);
+        if (pid <= 1 || pid == self)
+            continue;
Evidence
The process filter retains PID 1, and the isolation code expects it to wake and reap processes while
locking only the flashing process’s pages. The code identifies flash-backed root pages as a risk,
and the upgrade constructs an MTD-backed squashfs root, so remounting the root read-only does not
rule out init reading pages from affected flash.

Prevent restore from overwriting a live root filesystem unsafely
Protect upgrade from live-root overwrite
src/backup.c[491-503]
src/backup.c[554-555]
src/backup.c[577-585]
src/backup.c[1011-1014]
src/backup.c[1339-1341]
src/backup.c[416-425]
src/backup.c[486-503]
src/backup.c[577-584]
src/backup.c[1370-1378]

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 surviving init process can still fault on code or libraries from the live, flash-backed root after erasure begins.
## Fix Focus Areas
- src/backup.c[482-507]
- src/backup.c[577-585]
- src/backup.c[1011-1014]
- src/backup.c[1339-1341]
## Recommended Fix
Before either writing pass, establish a safe strategy that guarantees PID 1 cannot fault from affected flash ranges; refuse the operation when that cannot be guaranteed. Do not assume its needed executable pages are already resident.

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


View high (2)
4. A killed watchdog feeder can reset the board ✓ Resolved
Description
isolate_from_flash() kills the detected watchdog holder before attempting to open /dev/watchdog,
and treats a failed takeover as a printed warning. If the device remains unavailable or its timer
expires during the kill passes and retry loop, flashing proceeds without the keepalives previously
supplied by that process.
Code

src/backup.c[R589-594]

+            wdt_fd = open("/dev/watchdog", O_WRONLY);
+            if (wdt_fd < 0)
+                usleep(100 * 1000);
+        }
+        if (wdt_fd < 0)
+            printf("Cannot take over the watchdog: %s\n", strerror(errno));
Evidence
The ownership scan precedes process termination; the first open and feed occur after the kill
passes, and open failure does not prevent the subsequent flash.

src/backup.c[441-479]
src/backup.c[546-548]
src/backup.c[583-610]
src/backup.c[1010-1015]

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 watchdog feeder is killed before takeover, and a failed takeover does not stop flashing.
## Fix Focus Areas
- src/backup.c[546-548]
- src/backup.c[583-603]
- src/backup.c[1010-1015]
- src/backup.c[1337-1342]
## Recommended Fix
Arrange watchdog ownership without an unattended interval where possible, and abort before erase if a required takeover cannot be confirmed.

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


5. Flash errors leave all services stopped ✓ Resolved
Description
Both callers jump to their existing bailout paths when do_flash() fails after the new
isolate_from_flash() call. An erase, write, or UBI error therefore returns from the command with
other userspace processes killed and flash filesystems remounted or detached; the restore path also
returns zero.
Code

src/backup.c[1013]

+        isolate_from_flash();
Evidence
Isolation terminates other processes and remounts filesystems; do_flash() can fail, but each
failure branch bypasses reboot and restore ultimately returns zero.

src/backup.c[546-611]
src/backup.c[770-850]
src/backup.c[1010-1025]
src/backup.c[1337-1350]

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

## Issue description
Post-isolation flash failures use bailout paths that neither recover service nor reboot, and restore reports success.
## Fix Focus Areas
- src/backup.c[1013-1025]
- src/backup.c[1340-1348]
- src/backup.c[546-611]
## Recommended Fix
Add explicit post-isolation failure handling for both commands, including a nonzero restore result and a defined safe recovery or reboot path.

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



Remediation recommended

6. A stalled network log prevents reboot ✓ Resolved
Description
reboot_with_msg() now waits for fsync() on stdout and stderr before calling reboot(). If
either descriptor is redirected to an unreachable network share, a completed restore or upgrade can
remain blocked after rewriting its boot-critical flash.
Code

src/backup.c[R873-874]

+    fsync(STDOUT_FILENO);
+    fsync(STDERR_FILENO);
Evidence
Isolation deliberately preserves file redirects, including network-backed logs; both success paths
require reboot_with_msg(), which now performs potentially blocking fsync calls before reboot.

src/backup.c[563-575]
src/backup.c[867-875]
src/backup.c[1013-1018]
src/backup.c[1378-1382]

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

## Issue description
An unbounded fsync of a network-backed log can prevent reboot after flashing.
## Fix Focus Areas
- src/backup.c[867-875]
- src/backup.c[563-575]
## Recommended Fix
Keep the log-flush attempt from indefinitely blocking the reboot path when a redirected output filesystem becomes unavailable.

ⓘ 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 add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/backup.c Outdated
Comment thread src/backup.c Outdated
Comment thread src/backup.c
Comment thread src/backup.c Outdated
Comment thread src/backup.c Outdated
Comment thread src/backup.c Outdated
Review follow-up:

- mlockall() failing aborts before anything is stopped (pin_to_ram()),
  instead of flashing a process that can still fault on the flash.
- init is the one process left running and wakes to reap; its executable
  and libraries are mapped under MCL_FUTURE, which locks the page-cache
  pages it faults on.
- The watchdog feeders are killed and the device taken over first, with
  no unattended gap behind a full kill pass; a takeover that fails is
  fatal.
- A flash filesystem that will not go read-only is fatal: it can write
  over the image.
- A failure after the isolation can no longer return to a system with no
  services: it reboots, cleanly when nothing was erased yet. restore now
  exits nonzero when it does not flash, and umount_fs() no longer exit()s.
- The log flush before reboot runs in a child with 5 s to finish, so a
  stalled share cannot hold the reboot.

Re-tested both ways on the Hi3516EV300 XM board (XM -> OpenIPC from
telnet, OpenIPC -> XM from ssh with majestic holding the watchdog).
@widgetii

Copy link
Copy Markdown
Member Author

Addressed the review in eee2707:

  1. mlockall failure: now checked in pin_to_ram(), before anything is stopped. restore and upgrade abort cleanly.
  2. Remount failure: remount_flash_ro() returns a status. A flash filesystem that stays writable is fatal.
  3. init faulting on rewritten flash: init can be neither killed nor stopped. pin_init_mappings() maps its executable and libraries (from /proc/1/maps) under mlockall(MCL_FUTURE), which locks the page-cache pages init faults on.
  4. Watchdog gap: the feeders are now killed and the device taken over first, before the general kill passes. A failed takeover is fatal.
  5. Failure after isolation: no longer returns to a system without services. It reboots via reboot_after_failure(), and when nothing was erased yet the reboot is clean. restore now exits nonzero when it does not flash. umount_fs() no longer exit()s: by that point everything on flash is read-only.
  6. Stalled log blocking reboot: the log fsync runs in a child with 5 s to finish, and the parent feeds the watchdog while it waits.

Re-tested both directions on the Hi3516EV300 XM board with this code. The partitions read back identical and both firmwares boot.

@widgetii
widgetii merged commit 6b087c7 into master Sep 29, 2026
5 checks passed
@widgetii
widgetii deleted the restore-isolate-from-flash branch September 29, 2026 11:58
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.

restore/upgrade overwrite the flash that the running root fs lives on; camera dies mid-flash and is left half-written

1 participant