Files
felhom.eu/documentation/audits/night-2026-10-22/P3-candidates/analysis.md
T

9.2 KiB
Raw Blame History

Part 3 — the data-loss candidates: research (night of 2026-10-22)

Research by a helper (read only; it changed nothing). Saved by the main session from the helper's report (the helper's own write was refused by the harness). Baselines: controller cbc944c (v0.339.0), agent 2d5840b (v0.162.0), felhom.eu 1b442bcc. Architecture read: 07 §6.1.1, §10.2 (rows R-359, R-87), the design headers in R-899 nightowed.go, nightchain.go, offbox_clear.go, R-822 offbox_window.go, hub F-14 pbsdr.go. Live reads: demo-hp's agent journal (backup lines — demohp-whole-box-completions.txt) and docker ps on 9202. The main session's proofs and fixes are recorded in the night report; this file is the input.

Item Verdict Reason Action
1187a symlink REAL (code) appexport/fabplan.go:292-310 follows a link; the tar write fails; export.go:712 drops the whole drive folder; the export still ends „done" fix + red-proof, P2
1187b owner/symlink REAL (code) appexport/restore.go:1138-1183 handles only dirs and regular files, no chown fold with (a)
1187c stopped-app import REAL (code) restore.go:355-374 removes the volumes only when the app RUNS; docker cp merges 9202 proof, then refuse
1187d PLAUSIBLE no restoreOpBlocked check in StartImport fold into (c)
1187f REAL (code) the bundle's HDD_PATH is used; an empty one gives a relative path stays in the row
1187g REAL (code) classifyDBImage does not know postgis (adventurelog, dawarich); psql without ON_ERROR_STOP small fix, 9202 proof
1188b REAL in code, unlikely backup/restore.go:218 drops the rm error, then untars into the old volume P4
1188c ALREADY FIXED v0.337.0, TestR1188c_ALeftoverTempDoesNotBlockTheRetry close
1188d REAL (wording) 30-min cap; scratch is kept, so a second press probably continues (not measured) measure, then the sentence
1188e REAL, P4 offbox_restore.go:675-679 reads latest, _ = id P4
1189a REAL, half-live agent localapi/server.go:1179-1192: a future archive time gives a negative age agent fix needs the vouch
1189b SPLIT the skipped nights after an off-cycle run: FIXED by R-899 (v0.304.0), shown in the journal; the ~3 min/night creep is still real close that half; creep → P4
1189c REAL in code quiesce waits for the catch-up and the update leg, not for a restore 9202 recipe
1189d REAL (code) proof 05:30 and integrity 06:00 are daily jobs; the catch-up runs only the 3 backup legs; no hub age alarm own row, ~15-line fix
1189e REAL (code) the hub checks „since midnight"; any HH:MM window passes hub row
1189f OPERATOR DECISION design question —
1189g REAL, P4 spring-forward 02:30 → 03:30 collides with tier2 unit test
1189h not re-read — stays
1189i REAL, P4 UX check and begin are not atomic (web/handlers.go:1860/1881) P4
1190a REAL (code) the ceremony stages the off-site password only if off-site is configured; hub „abc"→"" demotes the good package unit tests, ~6-line fix
1190b REAL (code) the global bar ignores recoveryOffer(); the start handler does not refuse 409 fix, P2
1190c PLAUSIBLE — read-only fingerprint compare for Tester4
1190d OPERATOR DECISION — —
1190e/f/h not re-read — stay
1190g REAL (code) a nil AppConfig drops the app from liveDriveBinds, so its live folder can be listed (and deleted) as kept fix, P2
1205 OPERATOR DECISION the F-14 gate is deliberate (ruling 2026-07-13) two options to the operator
1206 no loss on the Felhom tier (by design, R-822); the NAS tier open the guard refuses a prune while any snapshot is future-dated; the NAS tier prunes without the guard pure unit test with the policy simulator

R-1187(c) — a .fab import into a STOPPED app merges into the old volumes. REAL (code)

controller/internal/appexport/restore.go:355-374 removes the volumes (RemoveStackVolumes = compose down --volumes, cmd/controller/main.go:3566-3591) only when IsStackRunning (main.go:3467-3471: Running or Degraded). Every other state (household-stopped, crashed, error, starting) „overwrites config in place". restoreVolumeData (:736-772) then runs volume create (a no-op on an existing volume) and docker cp - (:717-732), which adds and overwrites but never deletes: the result is old ∪ bundle. The drive half (restoreHDDData :590-648, extractTar) merges in both branches. Harm: a broken app (not running) is the likeliest import target; a stale SQLite -wal beside the bundle's .db is a corruption shape. „No SQL dump by default" alone is NOT-A-FINDING: with stop ticked the DB volume is captured cold, which is consistent (export.go:258-279, :366; appbackup/dbdump.go:142-144 lists only running containers). Fix: executeImport, ~8 lines — (B) refuse an import over an installed app that is not running (now); (A) remove its volumes too, only together with the D22 put-back copy. 9202 recipe (~20 min): a throwaway uptime-kuma; MARK-A into the volume, export (stop ticked), MARK-N; control: import while RUNNING → MARK-N gone; MARK-N again, Stop, import → MARK-N survives + the debug line „deployed but not running — will overwrite config in place".

R-1189(b) — split

The skipped nights after an off-cycle run are ALREADY FIXED by R-899, controller v0.304.0 (2026-10-09), quiesce/nightowed.go, TestR899_* (6). Proven on demo-hp: before the fix every off-cycle run was followed by a skipped night (10-01 22:10 → no 10-02; 10-05 11:19 → no 10-06; 10-08 22:20 → no 10-09); after it every one was followed by a night run (10-17 16:33 → 10-18 04:33; 10-19 03:25 → 04:32; 10-20 08:42 → 10-21 04:32). The creep is still REAL, low harm: agent localapi/server.go:1184 is age >= Cadence, age counted from the archive START; the controller polls every 5 min (quiesce.go:150) and the archive starts after the apps are stopped. Measured with no press between: 04:31:07 → 04:33:18 → 04:36:47 → 04:38:14, and 04:33:37 → 04:38:44 → 04:42:40 (~1.5–5 min/night). Unpressed, the start reaches W+6h in ~70–80 nights, then one night is skipped (44 h is below the 48 h valve, quiesce.go:807-818); the hub's 26 h line (deadline.go:29) alarms the operator. Fix: quiesce/tiers.go — inside the window a tier is due when age >= cadence - 4h (~6 lines, no customer data); proof: a unit test with an injected clock.

R-1189(d) — proof and integrity never run on a box that is off at 05:30–06:00. REAL (code)

main.go:1613 Daily("offsite-integrity","06:00"), :1635 Daily("offsite-proof","05:30"); the scheduler picks the next future time (scheduler.go:499-518) and skips late fires (:341-346); the catch-up runs only the three backup legs (main.go:1544-1553, catchUpLegs :4138). The comment at main.go:1601-1603 („checked the next day it is on") is true only if the box is on at 06:00. The hub shows offsite.last_integrity_* (web/offsite_box.go:199-210) with no age alarm. Design: 07 §10.2, R-359 („due-ness, not a weekday"). Fix: an After-hook on nightchain.CatchUp running the due-gated integrity check and proof once; the comment corrected (~15 lines); optional hub age alarm. Red-proof: a CatchUp test with a never-05:30 clock and a recording hook.

R-1190(a) — a ceremony with off-site not configured seals no repo password and supersedes the good package. REAL (code)

web/escrow_handlers.go:217-224 stages only if OffboxConfigured() (backup/offbox.go:695-710) — false when the target is cleared or disabled, although ClearOffboxTarget deliberately keeps the password file (offbox_clear.go header). The agent then seals no password (cmd/felhom-agent/main.go:2981-3003) and uploads sha "". Hub SaveHostEscrow sees curSHA != "" and demotes the good package to retained custody (operator-only). OffsiteRecoveryOffer reads "" as „covers no key" (offbox.go:1855-1873) and says nothing. Fix: stage when the password file exists (~6 lines; PushOffboxPasswordForEscrow offbox.go:800 reads only the file). Red-proof: a web test asserting the stage fn is called; a hub store test pinning „abc"→"" ⇒ superseded.

Other mechanism lines

1187a fabplan.go:292-310 (a FIFO also hangs the export on os.Open) · 1187b restore.go:1138-1183 · 1187g restore.go:1047-1056, 933-943 vs appbackup/dbservices.go:37 (the dump is --clean --if-exists, so ON_ERROR_STOP is safe; copy dbdump.go:775-784) · 1188b backup/restore.go:218 (StopStack is compose down, stacks/manager.go:1444) · 1188d web/offbox_handlers.go:455 (scratch kept, offbox_restore.go:373-392) · 1188e offbox_restore.go:675-679, marker SnapshotID :458 · 1189c quiesce.go:213-275, :565 · 1189e hub monitor/deadline.go:416-419, 555-565 · 1189i web/handlers.go:1860, 1881 · 1190b web/escrow_banner.go:23-35, templates/backups_remote.html:144-160 (its comment names the harm, R-199), escrow_handlers.go:180-245, reachable in offer shape (c) offbox.go:1866-1870 · 1190g stacks/kept.go:220-235, 336-360, 372-389; deploy.go:1113-1125 (a parse error gives nil) · 1205 hub web/pbsdr.go:236-248, store.go:3293-3297 (deliberate; operator ruling 2026-07-13) · 1206 backup/offbox_window.go:22-24, 130-137.