diff --git a/docs/next-release-20260930.md b/docs/next-release-20260930.md index 832f1cef..7309281e 100644 --- a/docs/next-release-20260930.md +++ b/docs/next-release-20260930.md @@ -385,3 +385,16 @@ are restored with the safe script. Dev's native Bitcoin/LND stayed running; all-container dev acceptance remains pending the companion-loop backend fix. Final-source UI suite: 1,133 passed. Heavy backend compilation is serialized with remaining build steps to reduce memory/IO pressure on the syncing dev node. + +Final source release gates at `96fb5a4f`: 1,613 backend tests passed, zero failed, +four explicitly ignored; 1,133 UI tests passed; type-check, production UI build, +catalog/trust, shell, pruning, LND readiness, NPM migration and doctor regressions +passed. The isolated companion-loop regression passed independently as well. + +ISO cache hardening: the installer now carries the current doctor script and +service/timer separately from rootfs.tar and overwrites both historical and active +script locations before first boot. This prevents a cached base image restoring +the old recovery code. A regression executes the actual installer block against +stale disposable files twice and confirms a missing safety payload fails closed. +The mounted-ISO smoke test also compares all three overlay files to source. +The final ISO build captures the exact newly deployed OTA UI/runtime payload. diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index b4f70394..5e273033 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -2607,6 +2607,11 @@ if [ -f "$SCRIPT_DIR/../../scripts/image-versions.sh" ]; then echo " ✅ Bundled image-versions.sh" fi +# Always overlay the current doctor, including when rootfs.tar is cached. +cp "$SCRIPT_DIR/../../scripts/container-doctor.sh" "$ARCH_DIR/scripts/" +cp "$SCRIPT_DIR/../configs/archipelago-doctor.service" "$ARCH_DIR/scripts/" +cp "$SCRIPT_DIR/../configs/archipelago-doctor.timer" "$ARCH_DIR/scripts/" + # Build-source apps need their complete contexts even on unbundled ISOs. # Keep this identical to the OTA runtime payload; a per-app allowlist silently # omitted GitWorkshop, FIPS and Cuprate and made fresh installs fail at 70%. @@ -3230,6 +3235,18 @@ for test_script in run-e2e-tests.sh run-post-install-tests.sh; do fi done +# BEGIN DOCTOR OVERLAY +# Replace both the active and historical script locations before first boot. +# A cached rootfs can contain the unsafe network recovery implementation. +mkdir -p /mnt/target/opt/archipelago/scripts /mnt/target/home/archipelago/archy/scripts +for doctor_dir in /mnt/target/opt/archipelago/scripts /mnt/target/home/archipelago/archy/scripts; do + install -m 755 "$BOOT_MEDIA/archipelago/scripts/container-doctor.sh" "$doctor_dir/container-doctor.sh" || exit 1 +done +for doctor_unit in archipelago-doctor.service archipelago-doctor.timer; do + install -m 644 "$BOOT_MEDIA/archipelago/scripts/$doctor_unit" "/mnt/target/etc/systemd/system/$doctor_unit" || exit 1 +done +# END DOCTOR OVERLAY + # Copy self-update script if [ -f "$BOOT_MEDIA/archipelago/scripts/self-update.sh" ]; then cp "$BOOT_MEDIA/archipelago/scripts/self-update.sh" /mnt/target/opt/archipelago/scripts/ diff --git a/scripts/iso-smoke-test.sh b/scripts/iso-smoke-test.sh index 6f4470ab..8a2f5c15 100755 --- a/scripts/iso-smoke-test.sh +++ b/scripts/iso-smoke-test.sh @@ -71,6 +71,25 @@ else bad "incomplete app build payload" fi +# The cached rootfs must never restore the unsafe historical doctor on boot. +for doctor_file in container-doctor.sh archipelago-doctor.service archipelago-doctor.timer; do + if [[ "$doctor_file" == container-doctor.sh ]]; then + doctor_source="$REPO/scripts/$doctor_file" + else + doctor_source="$REPO/image-recipe/configs/$doctor_file" + fi + if cmp -s "$doctor_source" "$MNT/archipelago/scripts/$doctor_file"; then + ok "current doctor payload: $doctor_file" + else + bad "missing/stale doctor overlay: $doctor_file" + fi +done +if grep -Fq '# BEGIN DOCTOR OVERLAY' "$MNT/archipelago/auto-install.sh"; then + ok "installer replaces cached doctor before first boot" +else + bad "installer lacks cached doctor replacement" +fi + # ── GRUB must boot the live system ─────────────────────────────────── if grep -q "boot=live" "$MNT/boot/grub/grub.cfg" 2>/dev/null; then ok "grub.cfg has boot=live" diff --git a/tests/regression/iso-doctor-overlay.py b/tests/regression/iso-doctor-overlay.py new file mode 100644 index 00000000..729f4937 --- /dev/null +++ b/tests/regression/iso-doctor-overlay.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Execute the installer's actual overlay against a stale disposable rootfs.""" +import pathlib, subprocess, tempfile, shutil, unittest +ROOT = pathlib.Path(__file__).resolve().parents[2] +class DoctorOverlayTests(unittest.TestCase): + def test_cached_rootfs_and_missing_payload(self): + source = (ROOT / 'image-recipe/_archived/build-auto-installer-iso.sh').read_text() + block = source.split('# BEGIN DOCTOR OVERLAY\n', 1)[1].split('# END DOCTOR OVERLAY', 1)[0] + with tempfile.TemporaryDirectory() as temp: + base = pathlib.Path(temp) + target = base / 'target' + media = base / 'media' + payload = media / 'archipelago/scripts' + payload.mkdir(parents=True) + units = target / 'etc/systemd/system' + units.mkdir(parents=True) + for directory in ['opt/archipelago/scripts', 'home/archipelago/archy/scripts']: + dest = target / directory + dest.mkdir(parents=True) + (dest / 'container-doctor.sh').write_text('UNSAFE OLD SCRIPT') + files = [ROOT / 'scripts/container-doctor.sh', + ROOT / 'image-recipe/configs/archipelago-doctor.service', + ROOT / 'image-recipe/configs/archipelago-doctor.timer'] + for path in files: + shutil.copyfile(path, payload / path.name) + script = block.replace('/mnt/target', str(target)) + for _ in range(2): + subprocess.run(['bash', '-c', script], env={'BOOT_MEDIA': str(media), 'PATH': '/usr/bin:/bin'}, check=True) + for directory in ['opt/archipelago/scripts', 'home/archipelago/archy/scripts']: + dest = target / directory / 'container-doctor.sh' + self.assertEqual(dest.read_bytes(), files[0].read_bytes()) + self.assertEqual(dest.stat().st_mode & 0o777, 0o755) + for path in files[1:]: + self.assertEqual((units / path.name).read_bytes(), path.read_bytes()) + (payload / 'container-doctor.sh').unlink() + failed = subprocess.run(['bash', '-c', script], env={'BOOT_MEDIA': str(media), 'PATH': '/usr/bin:/bin'}, capture_output=True) + self.assertNotEqual(failed.returncode, 0, 'Missing safety overlay must fail installation') +if __name__ == '__main__': + unittest.main() diff --git a/tests/release/run.sh b/tests/release/run.sh index 1c04af14..9102a1fb 100755 --- a/tests/release/run.sh +++ b/tests/release/run.sh @@ -74,6 +74,7 @@ stage "cargo-fmt" timeout 240 cargo fmt --manifest-path core/Cargo.toml - stage "app-build-contexts" python3 tests/regression/app-build-contexts.py stage "manifest-shell" python3 scripts/check-manifest-shell.py stage "npm-tunnel-migration" python3 -m unittest discover -s scripts/tests -p test_repair_npm_tunnel.py +stage "iso-doctor-overlay" python3 tests/regression/iso-doctor-overlay.py stage "doctor-egress" bash tests/regression/container-doctor-egress.sh stage "doctor-ports" bash tests/regression/container-doctor-ports.sh stage "bitcoin-pruning" python3 tests/regression/bitcoin-prune-entrypoint.py