fix: replace cached container doctor before ISO first boot

This commit is contained in:
archipelago
2026-09-30 17:52:30 -04:00
parent 96fb5a4f19
commit d1bc1273d4
5 changed files with 89 additions and 0 deletions
+13
View File
@@ -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. all-container dev acceptance remains pending the companion-loop backend fix.
Final-source UI suite: 1,133 passed. Heavy backend compilation is serialized with 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. 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.
@@ -2607,6 +2607,11 @@ if [ -f "$SCRIPT_DIR/../../scripts/image-versions.sh" ]; then
echo " ✅ Bundled image-versions.sh" echo " ✅ Bundled image-versions.sh"
fi 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. # Build-source apps need their complete contexts even on unbundled ISOs.
# Keep this identical to the OTA runtime payload; a per-app allowlist silently # 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%. # 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 fi
done 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 # Copy self-update script
if [ -f "$BOOT_MEDIA/archipelago/scripts/self-update.sh" ]; then if [ -f "$BOOT_MEDIA/archipelago/scripts/self-update.sh" ]; then
cp "$BOOT_MEDIA/archipelago/scripts/self-update.sh" /mnt/target/opt/archipelago/scripts/ cp "$BOOT_MEDIA/archipelago/scripts/self-update.sh" /mnt/target/opt/archipelago/scripts/
+19
View File
@@ -71,6 +71,25 @@ else
bad "incomplete app build payload" bad "incomplete app build payload"
fi 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 ─────────────────────────────────── # ── GRUB must boot the live system ───────────────────────────────────
if grep -q "boot=live" "$MNT/boot/grub/grub.cfg" 2>/dev/null; then if grep -q "boot=live" "$MNT/boot/grub/grub.cfg" 2>/dev/null; then
ok "grub.cfg has boot=live" ok "grub.cfg has boot=live"
+39
View File
@@ -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()
+1
View File
@@ -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 "app-build-contexts" python3 tests/regression/app-build-contexts.py
stage "manifest-shell" python3 scripts/check-manifest-shell.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 "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-egress" bash tests/regression/container-doctor-egress.sh
stage "doctor-ports" bash tests/regression/container-doctor-ports.sh stage "doctor-ports" bash tests/regression/container-doctor-ports.sh
stage "bitcoin-pruning" python3 tests/regression/bitcoin-prune-entrypoint.py stage "bitcoin-pruning" python3 tests/regression/bitcoin-prune-entrypoint.py