# REPORT — controller v0.76.0: campaign-#3 hardening (3 fixes) ## Baseline → target `felhom-controller` `main` v0.75.0 → **v0.76.0**. Controller-only; agent/hub untouched. ## What changed (commit `b0dd131`) Three findings from chaos campaign #3, severity-ordered: - **S1 [MEDIUM] — corrupt `settings.json` no longer crash-loops the controller.** `internal/settings/settings.go`: `save()` writes a last-known-good `.bak` **after** the primary rename succeeds (best-effort). `Load()` on a parse error recovers from `.bak` (re-promotes to primary), else preserves the corrupt file as `*.corrupt-` and starts on safe defaults — never returns the error that drove `main.go`'s `Fatalf`/crash-loop. New `Settings.LoadWarning` → dashboard banner (`handlers.go` + `dashboard.html`). `main.go` `Fatalf` retained (now only the IO-unreadable path). - **F2 [MEDIUM, defense-in-depth] — `stack_name` validated against path traversal.** `internal/web/validate.go` `validStackName` (single segment; rejects `/ \ .. NUL`), gated in `backupRestoreHandler` (`handlers.go`) and `apiExportStart` (`handler_export.go`) before any restore/export work. - **S3 [LOW] — corrupt quiesce marker quarantined, not silently dropped.** `internal/quiesce/quiesce.go` `readMarker` logs a `[WARN]` + renames a bad-JSON marker to `*.corrupt-` (still returns "no marker" → no recovery, the correct contract). ## Tests (green; all red-proofed) `go build ./... && go vet ./... && go test ./...` — green. - T-S1a recover-from-bak, T-S1b unrecoverable→safe-defaults+preserve, T-S1c save-writes-bak, T-S1d happy. Red-proof: revert `Load` to error-on-parse → T-S1a/b FAIL. - T-F2a `validStackName` (pure), T-F2b restore handler rejects traversal (no nil-deref), T-F2c export handler → 400. Red-proof: drop the restore gate → traversal falls through to the nil-backupMgr redirect → T-F2b FAIL. - T-S3a quarantine+log, T-S3b happy. Red-proof: revert `readMarker` to silent → T-S3a FAIL. ## Deploy Built+pushed `:0.76.0` on 192.168.0.180 (build-server `git pull` done first), deployed to 9201 via bootstrap. `docker inspect`: `image=:0.76.0 running healthy`. ## LIVE re-test — the three campaign-#3 scenarios, now fixed 1. **S1:** ensured `.bak`, truncated `settings.json` → restart → `status=running RestartCount=0` (no crash-loop), logs `[ERROR] primary settings corrupt … — attempting recovery from .bak` → `[WARN] recovered settings from .bak; re-promoting to primary`; config intact (3 storage_paths), 25/0. 2. **F2:** `POST /backup/restore stack_name=../../../etc` → `flash_error=Érvénytelen alkalmazásnév`, log `[WARN] restore rejected: invalid stack_name "../../../etc"`, **no "Starting app-data restore"** (gate fired before the restore logic); `/etc/passwd` intact (24 lines). 3. **S3:** planted a corrupt `quiesce-state.json` → restart → `[WARN] [quiesce] marker … is corrupt … — quarantining; stacks not auto-recovered`, original renamed to `*.corrupt-`, `RestartCount=0`, 25/0 (stacks not stranded). ## Still-open campaign-#3 deferrals (NOT in this task) Time-chaos on a dedicated VM / injectable clock; host-reboot (supervised); `.fab` import compose-fuzzing. Agent/hub untouched.