From 8cf2212e634520c98a54b415e31ef4c2151ec219 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Wed, 2 Sep 2026 08:00:51 -0400 Subject: [PATCH] install: the factory archive is written aside and verified before it is trusted The archive was written straight to its final name and a rerun accepted any non-empty file as complete, so a run interrupted mid-archive left a truncated image that the next run kept and then overwrote the slot. The archive is now written as .part and renamed on success; an existing archive counts only with its manifest line present and a whole gzip stream, otherwise it is archived again. --- scripts/install-forgefirm.sh | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts/install-forgefirm.sh b/scripts/install-forgefirm.sh index 99d2fd8..76b4d3e 100644 --- a/scripts/install-forgefirm.sh +++ b/scripts/install-forgefirm.sh @@ -50,25 +50,46 @@ stop_gf_services () { # archive_dev : dd|gzip with a live progress # line (compressed MB so far - old busybox dd has no status=progress). # dd's exit status is captured via a file so a read failure is not -# masked by gzip succeeding on truncated input. +# masked by gzip succeeding on truncated input. The archive is written +# as .part and renamed only when both ends succeeded: a run that +# dies mid-archive (power, a dropped SSH session) leaves nothing under +# the final name for a rerun to mistake for a complete archive. archive_dev () { RC_FILE=$(mktemp /tmp/ffinstall.rc.XXXXXX) || return 1 rm -f "$RC_FILE" - ( dd if="$1" bs=1M 2>/dev/null; echo $? > "$RC_FILE" ) | gzip -1 > "$2" & + PART="$2.part" + rm -f "$PART" + ( dd if="$1" bs=1M 2>/dev/null; echo $? > "$RC_FILE" ) | gzip -1 > "$PART" & GZPID=$! while kill -0 "$GZPID" 2>/dev/null; do sleep 3 - SZ=$(wc -c < "$2" 2>/dev/null) + SZ=$(wc -c < "$PART" 2>/dev/null) printf '\r %s MB compressed...' "$((${SZ:-0} / 1048576))" done wait "$GZPID" GZRC=$? DDRC=$(cat "$RC_FILE" 2>/dev/null) rm -f "$RC_FILE" - printf '\r %s MB compressed. \n' "$(($(wc -c < "$2") / 1048576))" - [ "$GZRC" = "0" ] && [ "$DDRC" = "0" ] + printf '\r %s MB compressed. \n' "$(($(wc -c < "$PART") / 1048576))" + if [ "$GZRC" = "0" ] && [ "$DDRC" = "0" ]; then + mv "$PART" "$2" + else + rm -f "$PART" + return 1 + fi } +# archived_ok : an existing archive counts only when the +# manifest records it and the gzip stream is whole. Anything else (a file +# left by an older installer's interrupted run, a manifest line that +# never got written) is archived again. +archived_ok () { + [ -s "$1" ] || return 1 + grep -q " $(basename "$1") md5=" "$ARCHIVE_DIR/manifest" 2>/dev/null || return 1 + gzip -t "$1" 2>/dev/null +} + + # slot_probe <1|2>: sets S_TYPE (factory|forgefirm|empty|unknown), # S_VER (the build datetime, used for archive naming), and S_FWVER (the # semantic FIRMWARE_VERSION, for display; factory only). The active slot @@ -245,10 +266,11 @@ for N in 1 2; do slot_probe $N [ "$S_TYPE" = "factory" ] || continue ARC="$ARCHIVE_DIR/factory-rootfs-$S_VER.img.gz" - if [ -s "$ARC" ]; then + if archived_ok "$ARC"; then echo -e "${ASTERISK}Slot $N (factory $S_VER) already archived." continue fi + [ -e "$ARC" ] && echo -e "${ASTERISK}Slot $N: the archive on disk is incomplete; archiving again." echo -e "${ASTERISK}Archiving slot $N ($(slot_desc)) - takes a few minutes:" archive_dev /dev/mmcblk2p$N "$ARC" \ || { rm -f "$ARC"; die "archiving slot $N failed"; } @@ -256,7 +278,7 @@ for N in 1 2; do done for B in 0 1; do ARC="$ARCHIVE_DIR/recovery-boot$B.img.gz" - [ -s "$ARC" ] && continue + archived_ok "$ARC" && continue echo -e "${ASTERISK}Archiving recovery boot$B:" archive_dev /dev/mmcblk2boot$B "$ARC" \ || { rm -f "$ARC"; die "archiving boot$B failed"; }