From 291cd0251305e11edb741babca14ee672dabb9df Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:11:56 +0300 Subject: [PATCH] sysupgrade: ignore --no_reboot when the live flash was rewritten (#2231) On a camera that flashes its own rootfs, `-x` finished the flash and then declined to reboot -- leaving a corpse. free_resources() drops the page cache to free RAM for the download, so once flashcp has rewritten the partition backing the mounted squashfs, every read that misses the cache comes back as the new image at stale offsets: SSH auth, libc and /etc return garbage. The closing "please reboot manually" was advice nobody could act on, because the shell that would issue the reboot was one of the things that stopped working. The camera still answered ping and only a power cycle recovered it. Decide the reboot on what was actually written and whether this camera runs from it, instead of on skip_reboot alone: - get_system_info() records root_on_flash, probing /proc/cmdline for a root= that names something other than flash. Anchored to the root= token rather than matching a bare nfs/mmcblk/ram anywhere in the line as init(1) does: a false "not on flash" is the one error here that bricks a camera, and the stock command line already carries mmz=anonymous and an mtdparts label ending -(rootfs_data). Anything not positively proven to live elsewhere -- NAND root=ubi0:rootfs, an unreadable /proc/cmdline -- counts as flash, which at worst costs a reboot. - do_update_rootfs, do_update_firmware and do_wipe_overlay mark the run before the write, not after: flashcp erases before it writes, so a half-erased partition is just as unreadable, and do_update_firmware reaches reboot_system through `|| die` by exactly that path. do_wipe_overlay counts because the jffs2 it erases is the upperdir of the running overlay. do_update_kernel does not -- that partition is not mounted, which is what keeps -x useful. - reboot_system() reboots anyway when the live flash was rewritten, and says why. A pre-flight notice lands before the trap that shields flashcp from a dying TTY, so the operator still has Ctrl-C. So -x still means what it says for a kernel-only update, an NFS/SD/ram root, or a same-version run that writes nothing -- and no longer promises a working camera it cannot deliver. reboot_system() now returns instead of exiting on the honoured path, so the main flow's `exit 0` is reached and die() keeps its own `exit 1`. A deliberate --no_reboot run used to report failure. Verified on the lab hi3516ev300 (NOR, overlay root, squashfs on mtd3): a real upgrade with -x prints the notice, flashes, overrides -x and comes back on the new build with key auth intact; a forced kernel-only write with -x is honoured and the box stays up, exit 0. 20 new behaviour cases and 12 drift assertions in test_sysupgrade.sh, all of which fail against 1.0.55. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/test_sysupgrade.sh | 303 +++++++++++++++++++++++++++- general/overlay/usr/sbin/sysupgrade | 78 ++++++- 2 files changed, 372 insertions(+), 9 deletions(-) diff --git a/.github/scripts/test_sysupgrade.sh b/.github/scripts/test_sysupgrade.sh index 2a9ba1cbb..c36c35cb8 100755 --- a/.github/scripts/test_sysupgrade.sh +++ b/.github/scripts/test_sysupgrade.sh @@ -61,6 +61,7 @@ sed -e 's|grep "GITHUB_VERSION" "$1/etc/os-release"|grep "GITHUB_VERSION" "${1:- -e "s|/tmp\\b|@SB@/tmp|g" \ -e "s|/etc/os-release|@SB@/etc/os-release|g" \ -e "s|/proc/mtd|@SB@/proc/mtd|g" \ + -e "s|/proc/cmdline|@SB@/proc/cmdline|g" \ -e "s|/proc/sys/vm/drop_caches|@SB@/tmp/drop_caches|g" \ -e "s|/etc/init.d/|@SB@/etc/init.d/|g" \ -e "s|@OSRELEASE@|/etc/os-release|g" \ @@ -74,7 +75,9 @@ for s in S99rc.local S60crond S49ntpd S02klogd S01syslogd; do printf '#!/bin/sh\nexit 0\n' > "$SB/etc/init.d/$s"; chmod +x "$SB/etc/init.d/$s" done -cat > "$SB/proc/mtd" <<'EOF' +set_mtd() { cat > "$SB/proc/mtd"; } + +set_mtd <<'EOF' dev: size erasesize name mtd0: 00040000 00010000 "boot" mtd1: 00010000 00010000 "env" @@ -83,6 +86,17 @@ mtd3: 00500000 00010000 "rootfs" mtd4: 00100000 00010000 "rootfs_data" EOF +# The kernel command line decides whether this camera is running FROM the flash +# sysupgrade is about to rewrite. The default is the real one off the lab +# hi3516ev300 -- deliberately including mmz=anonymous and an mtdparts label +# ending in "rootfs_data", the substrings a loose root-type probe trips over. +set_cmdline() { printf '%s\n' "$1" > "$SB/proc/cmdline"; } +CMDLINE_FLASH='mem=128M console=ttyAMA0,115200 panic=20 rootfstype=squashfs root=/dev/mtdblock3 mtdparts=hi_sfc:256k(boot),64k(env),2048k(kernel),5120k(rootfs),-(rootfs_data) mmz_allocator=cma mmz=anonymous,0,0x42000000,96M init=/init' +CMDLINE_NFS='console=ttyAMA0,115200 root=/dev/nfs nfsroot=192.168.1.1:/srv/cam,tcp,v3 ip=dhcp rw' +CMDLINE_MMC='console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rw' +CMDLINE_RAM='console=ttyAMA0,115200 root=/dev/ram0 rdinit=/linuxrc' +set_cmdline "$CMDLINE_FLASH" + # --- stubs ----------------------------------------------------------------- stub() { printf '#!/bin/bash\n%s\n' "$2" > "$SB/bin/$1"; chmod +x "$SB/bin/$1"; } @@ -112,10 +126,14 @@ chmod +x "$SB/bin/md5sum" # flashcp / flash_eraseall / reboot all arrive via `busybox `; record the # argv of anything that writes, in order, so a test can assert both WHAT was # written and WHETHER anything was. +# +# STUB_FLASHCP_FAIL makes the write fail AFTER it has been logged -- a partially +# erased partition, which is the state do_update_firmware's `|| die` reacts to. stub busybox ' applet=$1; shift case "$applet" in - flashcp|flash_eraseall) echo "$applet $*" >> "$FLASH_LOG" ;; + flashcp|flash_eraseall) echo "$applet $*" >> "$FLASH_LOG" + [ "1" = "$STUB_FLASHCP_FAIL" ] && exit 1 ;; reboot) echo "reboot" >> "$FLASH_LOG"; exit 0 ;; esac exit 0' @@ -203,23 +221,39 @@ run() { : > "$SB/tmp/flash.log" OUT=$(cd "$SB" && env PATH="$SB/bin:$PATH" \ HASERLVER=1 FLASH_LOG="$SB/tmp/flash.log" mount_wait="${MOUNT_WAIT:-3}" \ + abort_wait=0 \ STUB_MOUNT="${STUB_MOUNT:-ok}" STUB_VENDOR="${STUB_VENDOR:-sigmastar}" \ STUB_SOC="${STUB_SOC:-ssc338q}" \ STUB_IMG_SOC="${STUB_IMG_SOC:-ssc338q}" \ STUB_IMG_VERSION="${STUB_IMG_VERSION:-2026.07.11}" \ + STUB_FLASHCP_FAIL="${STUB_FLASHCP_FAIL:-0}" \ sh "$SB/sysupgrade" "$@" 2>&1) RC=$? } flashed() { grep -q "flashcp .*$1" "$SB/tmp/flash.log"; } +erased() { grep -q "flash_eraseall .*$1" "$SB/tmp/flash.log"; } nothing_wrote() { ! grep -q "flashcp" "$SB/tmp/flash.log"; } +# The busybox stub logs a bare "reboot" line, so whether the run rebooted is +# directly observable -- which is the whole question issue #2231 turns on. +rebooted() { grep -qx "reboot" "$SB/tmp/flash.log"; } # Line number of a phrase in $OUT, for ordering assertions. at() { printf '%s\n' "$OUT" | grep -n -- "$1" | head -1 | cut -d: -f1; } reset_env() { unset STUB_MOUNT STUB_VENDOR STUB_SOC STUB_IMG_SOC STUB_IMG_VERSION MOUNT_WAIT + unset STUB_FLASHCP_FAIL rm -f "$SB"/tmp/*.ssc338q "$SB"/tmp/firmware.bin.* "$SB"/tmp/*.tgz "$SB"/tmp/*.md5sum make_uimage "$SB/tmp/uImage.ssc338q" ssc338q make_rootfs "$SB/tmp/rootfs.squashfs.ssc338q" + set_cmdline "$CMDLINE_FLASH" + set_mtd <<'EOF' +dev: size erasesize name +mtd0: 00040000 00010000 "boot" +mtd1: 00010000 00010000 "env" +mtd2: 00200000 00010000 "kernel" +mtd3: 00500000 00010000 "rootfs" +mtd4: 00100000 00010000 "rootfs_data" +EOF } K="$SB/tmp/uImage.ssc338q" @@ -398,6 +432,198 @@ else bad "verify should print its own header/line before mounting" fi +# --- -x / --no_reboot on a camera flashing its own live rootfs (issue #2231) -- +# +# A NOR camera boots an overlay whose lowerdir is the squashfs on the "rootfs" +# MTD partition and whose upperdir is the jffs2 on "rootfs_data" -- the two +# partitions sysupgrade erases. free_resources() drops the page cache to make +# room for the download, so once flashcp has rewritten that partition every +# read that misses the cache comes back as the NEW image at STALE offsets: SSH +# auth, libc and /etc all return garbage. Honouring --no_reboot there does not +# leave a working camera pending a convenient reboot, it leaves a dead one that +# still answers ping and cannot be logged into to issue the reboot at all. +# +# So the reboot decision cannot rest on skip_reboot alone. It has to ask what +# actually got written, and whether this camera is running from it. + +echo +echo "=== Part 1b: --no_reboot vs the live rootfs (issue #2231) ===" + +# The bug itself. -x must not be honoured once the live rootfs is overwritten. +reset_env +run -z --rootfs="$R" -x +if [ "$RC" -eq 0 ] && flashed /dev/mtd3 && rebooted; then + ok "-x + live rootfs rewritten -> reboots anyway" +else + bad "-x + live rootfs -> expected flash then reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi +# And it must say why, rather than rebooting a camera whose operator asked it not to. +if printf '%s' "$OUT" | grep -q -- "--no_reboot ignored"; then + ok "-x override explains itself" +else + bad "-x override must explain itself; got: $(printf '%s' "$OUT" | tail -3)" +fi +# The warning has to come BEFORE the write, while Ctrl-C still works: the trap +# that shields flashcp from a dying TTY also takes the operator's way out. +n=$(at "NOTICE"); w=$(at "Update rootfs from") +if [ -n "$n" ] && [ -n "$w" ] && [ "$n" -lt "$w" ]; then + ok "-x notice precedes the first write (abort window is real)" +else + bad "-x notice must precede the write -- notice@${n:-none} write@${w:-none}" +fi + +# The flag still has a job. A kernel-only run never touches the mounted +# partition, so the camera survives it and -x means what it says. +reset_env +run -z --kernel="$K" -x +if [ "$RC" -eq 0 ] && flashed /dev/mtd2 && ! flashed /dev/mtd3 && ! rebooted; then + ok "-x + kernel only -> honoured, no reboot" +else + bad "-x + kernel only -> expected no reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi +# ...and exits 0. A deliberate --no_reboot used to report failure. +if printf '%s' "$OUT" | grep -q "asked me not to reboot"; then + ok "-x honoured path prints the soft notice and exits $RC" +else + bad "-x honoured path should print the soft notice; got: $(printf '%s' "$OUT" | tail -3)" +fi + +# Same version: do_update_rootfs returns before flashcp, nothing is written, so +# there is nothing to reboot for. Deciding on intent rather than on what was +# written would get this wrong. +reset_env +STUB_IMG_VERSION=2026.06.01 +run -z --rootfs="$R" -x +if [ "$RC" -eq 0 ] && ! flashed /dev/mtd3 && ! rebooted; then + ok "-x + same version -> nothing written, honoured" +else + bad "-x + same version -> expected no write and no reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi + +# Not every camera runs from the flash it writes. An NFS/SD/ram root is a +# supported layout (general/overlay/init, general/package/openipc-nfs-root) +# where the rootfs partition is just a target and -x is exactly the right flag. +for c in "$CMDLINE_NFS:nfs" "$CMDLINE_MMC:mmcblk" "$CMDLINE_RAM:ram"; do + reset_env + set_cmdline "${c%:*}" + run -z --rootfs="$R" -x + if [ "$RC" -eq 0 ] && flashed /dev/mtd3 && ! rebooted; then + ok "-x + ${c##*:} root -> rootfs flashed, honoured (not running from it)" + else + bad "-x + ${c##*:} root -> expected flash with no reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" + fi +done + +# The failure that would brick a camera is a FALSE "not on flash", so the probe +# is anchored to the root= token. The stock hi3516ev300 command line carries +# both `mmz=anonymous` and an mtdparts label ending `-(rootfs_data)`; a probe +# matching a bare `ram`/`mmcblk` anywhere in the line is one vendor bootarg away +# from waving through the exact case this test exists for. +reset_env +set_cmdline "$CMDLINE_FLASH ramdisk_size=8192 mmz=mmcblkish" +run -z --rootfs="$R" -x +if flashed /dev/mtd3 && rebooted; then + ok "-x + decoy 'ram'/'mmcblk' substrings -> still recognised as flash root" +else + bad "root-type probe must anchor to root=; decoy substrings fooled it, log='$(cat "$SB/tmp/flash.log")'" +fi + +# The combined image (cv6xx) reaches the rootfs by both of its paths. +reset_env +make_combined "$SB/tmp/firmware.bin.ssc338q" +make_archive "$SB/tmp/firmware.bin.ssc338q" +run -z --archive="$SB/tmp/fw.tgz" -x +if flashed /dev/mtd2 && flashed /dev/mtd3 && rebooted; then + ok "-x + combined image, split path -> reboots anyway" +else + bad "-x + combined split -> expected both flashed and a reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi + +# No separate kernel/rootfs partitions: one whole-blob write to "firmware", +# which overlaps the running rootfs. +reset_env +set_mtd <<'EOF' +dev: size erasesize name +mtd0: 00040000 00010000 "boot" +mtd1: 00010000 00010000 "env" +mtd2: 00700000 00010000 "firmware" +EOF +make_combined "$SB/tmp/firmware.bin.ssc338q" +make_archive "$SB/tmp/firmware.bin.ssc338q" +run -z --archive="$SB/tmp/fw.tgz" -x +if flashed /dev/mtd2 && rebooted; then + ok "-x + combined image, whole-blob path -> reboots anyway" +else + bad "-x + combined whole-blob -> expected flash and reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi + +# A write that FAILS is not a write that did not happen: flashcp erases before +# it writes, so a partition left half-erased is every bit as unreadable. This +# is why the run is marked dirty before the write, not after -- do_update_firmware +# reaches reboot_system through `|| die` precisely here. +reset_env +set_mtd <<'EOF' +dev: size erasesize name +mtd0: 00040000 00010000 "boot" +mtd1: 00010000 00010000 "env" +mtd2: 00700000 00010000 "firmware" +EOF +make_combined "$SB/tmp/firmware.bin.ssc338q" +make_archive "$SB/tmp/firmware.bin.ssc338q" +STUB_FLASHCP_FAIL=1 +run -z --archive="$SB/tmp/fw.tgz" -x +if [ "$RC" -ne 0 ] && rebooted; then + ok "-x + write started then failed -> reboots anyway (partial erase is fatal too)" +else + bad "-x + failed write -> expected a reboot despite the failure, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi + +# --wipe_overlay erases "rootfs_data" -- the rw jffs2 that is the upperdir of +# the running overlay, so overlayfs consults it for every lookup. Erasing it +# live breaks the camera just as thoroughly as rewriting the lowerdir. +reset_env +run -z --wipe_overlay -x +if erased /dev/mtd4 && rebooted; then + ok "-x + --wipe_overlay on flash root -> reboots anyway (live upperdir erased)" +else + bad "-x + --wipe_overlay -> expected erase and reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi +# On a non-flash root init mounts a tmpfs overlay instead, so there is no live +# upperdir on the partition being erased. +reset_env +set_cmdline "$CMDLINE_NFS" +run -z --wipe_overlay -x +if [ "$RC" -eq 0 ] && erased /dev/mtd4 && ! rebooted; then + ok "-x + --wipe_overlay on nfs root -> honoured" +else + bad "-x + --wipe_overlay on nfs root -> expected no reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi + +# Nothing was written, so a failure before the first flash still honours -x. +reset_env +STUB_IMG_SOC=gk7205v300 +run -z --rootfs="$R" -x +if [ "$RC" -ne 0 ] && nothing_wrote && ! rebooted; then + ok "-x + refusal before any write -> honoured, nothing written" +else + bad "-x + pre-write refusal -> expected no write and no reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi + +# The default path must be untouched by all of the above. +reset_env +run -z --kernel="$K" --rootfs="$R" +if [ "$RC" -eq 0 ] && flashed /dev/mtd2 && flashed /dev/mtd3 && rebooted; then + ok "no -x -> unconditional reboot, unchanged" +else + bad "no -x -> expected both flashed and a reboot, rc=$RC log='$(cat "$SB/tmp/flash.log")'" +fi +# ...and it must not print the -x notice at people who never passed -x. +if ! printf '%s' "$OUT" | grep -q "NOTICE\|--no_reboot ignored"; then + ok "no -x -> no --no_reboot chatter" +else + bad "a run without -x should not mention --no_reboot" +fi + # --------------------------------------------------------------------------- echo echo "=== Part 2: invariants in $SRC ===" @@ -444,6 +670,79 @@ else bad "CONFIG_TIMEOUT was dropped from busybox.config -- the bounded mount degrades" fi +# --- issue #2231 invariants ------------------------------------------------ + +# skip_reboot must never be the sole gate again. The behaviour tests above only +# see the cases they were written for; this pins the shape. +rb=$(sed -n '/^reboot_system()/,/^}/p' "$SRC") +if printf '%s' "$rb" | grep -q 'live_flash_dirty'; then + ok "reboot_system weighs what was written, not just skip_reboot" +else + bad "reboot_system must consult live_flash_dirty, not skip_reboot alone" +fi + +# It also must not exit on the honoured path: die() and the main flow disagree +# about the status, and the main flow's `exit 0` is what makes -x a success. +# Comments are stripped -- the ones in there discuss exit codes at length. +rbc=$(printf '%s\n' "$rb" | sed 's/#.*//') +if printf '%s\n' "$rbc" | grep -q 'return 0' && ! printf '%s\n' "$rbc" | grep -qw 'exit'; then + ok "reboot_system returns rather than exits (caller owns the status)" +else + bad "reboot_system must return on the honoured path so die() keeps its own exit 1" +fi + +# Every write that lands on flash the camera is running from has to be marked, +# and the one that does not must stay unmarked or -x loses its only real use. +for fn in do_update_rootfs do_update_firmware do_wipe_overlay; do + if sed -n "/^${fn}()/,/^}/p" "$SRC" | grep -q 'mark_live_flash_dirty'; then + ok "$fn marks the live flash dirty" + else + bad "$fn writes flash the camera runs from and must mark it dirty" + fi +done +if sed -n '/^do_update_kernel()/,/^}/p' "$SRC" | grep -q 'mark_live_flash_dirty'; then + bad "do_update_kernel must NOT mark dirty -- the kernel partition is not mounted" +else + ok "do_update_kernel leaves -x alone (its partition is not mounted)" +fi + +# The mark belongs before the write (a half-erased partition is just as dead) +# and after the same-version return (which writes nothing at all). +body=$(sed -n '/^do_update_rootfs()/,/^}/p' "$SRC") +e=$(printf '%s\n' "$body" | grep -n 'exit_update' | head -1 | cut -d: -f1) +m=$(printf '%s\n' "$body" | grep -n 'mark_live_flash_dirty' | head -1 | cut -d: -f1) +f=$(printf '%s\n' "$body" | grep -n 'flashcp' | head -1 | cut -d: -f1) +if [ -n "$e" ] && [ -n "$m" ] && [ -n "$f" ] && [ "$e" -lt "$m" ] && [ "$m" -lt "$f" ]; then + ok "do_update_rootfs marks after the same-version return and before the write" +else + bad "do_update_rootfs order must be exit_update -> mark -> flashcp; got ${e:-none}/${m:-none}/${f:-none}" +fi + +# The root-type probe must anchor to root=. An unanchored alternation matches a +# bare 'ram'/'mmcblk' anywhere in the command line, and a false "not on flash" +# is the one error here that bricks a camera. +if grep -q 'root_on_flash' "$SRC" \ + && grep -qF 'root=(/dev/)?(nfs|mmcblk|ram)' "$SRC" \ + && ! grep -qF 'nfs\|mmcblk\|ram' "$SRC"; then + ok "root-type probe is anchored to the root= token" +else + bad "root-type probe must anchor each alternative to root=, not match bare substrings" +fi + +# The abort window has to stay overridable, or this suite pays for it six times. +if grep -q 'abort_wait=${abort_wait:-' "$SRC"; then + ok "the -x abort window is overridable (abort_wait)" +else + bad "abort_wait must stay overridable so the suite does not sleep through it" +fi + +# The caveat belongs in --help, not only in the code. +if sed -n '/-x, --no_reboot/,/-z, --no_update/p' "$SRC" | grep -qi 'ignored'; then + ok "--help says -x is ignored when the live flash is rewritten" +else + bad "-x usage text must document that it is ignored on a live-flash rewrite" +fi + echo if [ "$fail" -eq 0 ]; then echo "All sysupgrade verification checks passed." diff --git a/general/overlay/usr/sbin/sysupgrade b/general/overlay/usr/sbin/sysupgrade index 182938735..98c7b06f5 100755 --- a/general/overlay/usr/sbin/sysupgrade +++ b/general/overlay/usr/sbin/sysupgrade @@ -1,11 +1,13 @@ #!/bin/sh # OpenIPC.org | 2025 -scr_version=1.0.55 +scr_version=1.0.56 args="$@" LOCK_FILE=/tmp/sysupgrade.lock # Seconds the rootfs verify-mount may take before it is treated as unmountable. mount_wait=${mount_wait:-45} +# Seconds the operator gets to Ctrl-C out of a run that will force a reboot. +abort_wait=${abort_wait:-5} # Route to firmware or builder manifest. A stock OpenIPC/firmware build # stamps BUILD_PLATFORM=${soc}_${variant} with variant in {lite,ultimate, # neo}. Anything else — a per-device override (${soc}_${variant}_${device}, @@ -199,6 +201,7 @@ do_update_rootfs() { echo "Update rootfs from $x" [ ! -f "$x" ] && die "File $x not found" [ "1" = "$exit_update" ] && return 0 + mark_live_flash_dirty set_progress flashcp -v "$x" "$(get_device "rootfs")" echo_c 32 "RootFS updated to ${rootfs_version:-unknown}" } @@ -243,6 +246,7 @@ do_update_firmware() { local fw_dev=$(get_device "firmware") [ "$fw_dev" = "/dev/" ] && die "No kernel/rootfs or firmware partition to flash" echo "Combined partition $fw_dev" + mark_live_flash_dirty set_progress flashcp -v "$x" "$fw_dev" || die "flashcp to $fw_dev failed" echo_c 32 "Firmware updated" } @@ -251,6 +255,7 @@ do_wipe_overlay() { echo_c 33 "\nOverlayFS" echo "Erase overlay partition" [ "$flash_type" = "nand" ] || jffs2="-j" + mark_live_flash_dirty set_progress flash_eraseall $jffs2 "$(get_device "rootfs_data")" } @@ -440,6 +445,17 @@ get_device() { echo -n "/dev/$(grep "\"$1\"" /proc/mtd | cut -d: -f1)" } +# Record that a write is about to land on flash that backs the RUNNING +# filesystem, so reboot_system knows --no_reboot can no longer be honoured. +# +# Called BEFORE the write, not after: flashcp erases the partition before it +# writes, so an interrupted or failed write is exactly as fatal as a completed +# one — and do_update_firmware's `|| die` reaches reboot_system by that path. +mark_live_flash_dirty() { + [ "1" = "$root_on_flash" ] && live_flash_dirty=1 + return 0 +} + get_kernel_version() { # A FIT image (DTB magic d00dfeed) has no uImage timestamp at offset 8; # the legacy read would print a bogus 1970 date. Used for display and, on @@ -474,6 +490,21 @@ get_system_info() { kernel_version=$(get_kernel_version "$kernel_device") system_version=$(get_system_version "") flash_type=$(ipcinfo -F) + # 1 when the running filesystem is served from the flash this run may + # rewrite. init(1) mounts the rw overlay from "rootfs_data" and pivots onto + # the squashfs the kernel booted from, so on a flash root BOTH partitions + # this script erases are backing the live mount. + # + # Only a `root=` naming something that is NOT flash proves the safe case, so + # the test is anchored to that token. general/overlay/init greps for a bare + # `mmcblk`/`ram` anywhere in the line — harmless when the answer only picks a + # mount type, but here a stray substring (an mtdparts label, an allocator + # name) would certify a flash-root camera as safe and brick it. Anything not + # positively proven to live elsewhere — NAND `root=ubi0:rootfs`, an + # unreadable /proc/cmdline — counts as flash, which at worst costs a reboot. + root_on_flash=1 + grep -qE '(^|[[:space:]])root=(/dev/)?(nfs|mmcblk|ram)' /proc/cmdline 2>/dev/null \ + && root_on_flash=0 } get_system_version() { @@ -509,7 +540,9 @@ Where: this platform; mark the running build with '*'. -f, --force_all Do not validate anything. -n, --wipe_overlay Wipe overlay partition. - -x, --no_reboot Do not reboot after updating. + -x, --no_reboot Do not reboot after updating. Ignored when the update + rewrites the flash the camera is running from, which + no running system survives. -z, --no_update Do not update self. --web Invoked by the WebUI: keep majestic alive to stream progress over /ws/upgrade, stop it only before flashing. @@ -520,16 +553,31 @@ Manifest URL: \$MANIFEST_URL (default: $MANIFEST_URL) } reboot_system() { - if [ "1" = "$skip_reboot" ]; then + # Nothing landed on flash the running system is served from: -x is safe to + # honour. Return rather than exit, so the caller owns the status — the main + # path falls through to `exit 0` (a deliberate --no_reboot run is a success, + # it used to report failure) while die() keeps its own `exit 1`. + if [ "1" = "$skip_reboot" ] && [ "1" != "$live_flash_dirty" ]; then echo_c 33 "\nYou asked me not to reboot, so I won't." echo_c 31 "Although a reboot is required to apply the changes." echo_c 37 "Please reboot the camera manually whenever possible." rm $LOCK_FILE - exit 1 - else - echo_c 37 "\nUnconditional reboot" - busybox reboot -d 1 -f + return 0 + fi + if [ "1" = "$skip_reboot" ]; then + # -x cannot be honoured here. The flash this camera runs from has been + # rewritten under the running kernel, so the live filesystem is already + # inconsistent: reads miss the cache free_resources() dropped and come + # back as the new image at stale offsets. There is no "reboot it later" — + # the shell that would issue the reboot is one of the things that stops + # working, which is how issue #2231 was reported: the camera still + # answered ping, but no login succeeded and only a power cycle recovered + # it. Reboot now, while we still can. + echo_c 31 "\n--no_reboot ignored: the flash this camera runs from was rewritten." + echo_c 31 "The running filesystem cannot survive that; only a reboot applies it." fi + echo_c 37 "\nUnconditional reboot" + busybox reboot -d 1 -f } for i in "$@"; do @@ -685,6 +733,22 @@ free_resources [ "1" != "$skip_unmount" ] && check_sdcard [ "1" = "$remote_update" ] && download_firmware +# -x cannot be honoured for a write that lands on the flash this camera runs +# from (see reboot_system). Say so before the first write rather than after it: +# the trap below takes Ctrl-C away, deliberately, for the rest of the run, so +# this is the last moment an operator who truly cannot afford a reboot can get +# out. image_combined is only known once download_firmware has run, and it is +# what covers a plain `-k` against a combined image (that writes the rootfs +# with update_rootfs unset). +if [ "1" = "$skip_reboot" ] && [ "1" = "$root_on_flash" ] && + { [ "1" = "$update_rootfs" ] || [ "1" = "$image_combined" ] || [ "1" = "$clear_overlay" ]; }; then + echo_c 31 "\nNOTICE: -x was requested, but this run rewrites the flash this camera" + echo_c 31 "is running from. -x cannot be honoured for that: the running filesystem" + echo_c 31 "does not survive it. The camera will be rebooted once flashing is done." + echo_c 37 "Press Ctrl-C now to abort. Continuing in $abort_wait seconds..." + sleep "$abort_wait" +fi + # Past this point we erase flash. A controlling-TTY death (SSH session drop, # stray Ctrl-C, etc.) must not kill flashcp mid-write — that leaves the rootfs # partition partially erased and unbootable. SIG_IGN is inherited across