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

100 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.