# REPORT — blast-radius audit fixes B1 + D1 + D2 + D3 (v0.61.0) **Date:** 2026-07-03 · **Repo:** `felhom-agent` · **Class:** Implementation (destructive-op guard surfaces D1/D3 proven in unit-land + a loop-device recorder harness — NO real mkfs, no live /dev race, per the §13 STOP). Source of truth: `felhom.eu/documentation/audits/AUDIT-blast-radius-hostroot-localapi-2026-07-02.md`. ## 1. Baseline → target agent **v0.60.0** (`cc93dae`, confirmed before starting) → **v0.61.0**. Commits on `main`: - **`3f382bf`** — all four fixes + tests + harness + version bump + CHANGELOG/CONTEXT/README. - (this REPORT is the follow-up docs commit) ## 2. What shipped | Fix | Files | What changed | |---|---|---| | **B1** random temp staging | `internal/guesthook/install.go`, `internal/localapi/intermediary.go` (new `stageTemp`), `configs/felhom-agent.sudoers`, `internal/capability/manifest.go` | Fixed, predictable `/tmp` names (local TOCTOU into root-executed hookscript/boot script) → `os.CreateTemp` random names (the `lanresolver` pattern). Sudoers install-SOURCE grants became globs (`/tmp/felhom-guest-hook-*.sh`, `/tmp/felhom-shared-parent-*.{sh,service}`); destinations + modes unchanged. Manifest repr vectors updated to match. | | **D1** wrapper asymmetry | `configs/felhom-mkfs-guarded.sh`, new `scripts/mkfs-guarded-harness.sh` | Additive re-checks: `/sys/block//ro`==1 → die; lsblk-FSTYPE loop refusing exactly `claim.go`'s `memberFSTypes` (LVM2_member/zfs_member/linux_raid_member/crypto_LUKS/swap); pvs via absolute candidates (`/usr/sbin/pvs`, `/sbin/pvs`) with the FSTYPE loop as the independent LVM catch when pvs is absent. Nothing removed/reordered; a blank partitioned disk still formats. | | **D2** empty-lsblk fail-safe | `internal/storage/claim.go` | `classifyClaim` refuses when the node tree is EMPTY or the target whole-disk is absent from it (previously: empty-but-successful lsblk ⇒ `unclaimed`). Placed after the member/mount loop; the `gatherErr`/`wholeDiskOK`/`isSystem`/`readonly`/`lvmPV`/`zfsMember` ordering untouched. | | **D3** blank-format anti-retarget | `internal/localapi/wipe_reresolve.go` (shared `antiRetargetResolveExpect` core + new `antiRetargetResolveBlank`), `disks.go`, `formatjob.go`, `server.go` (new `reresolveBlank` seam) | The blank branch now derives the device's durable id (**no durable id ⇒ 409 refuse** — path-only formats not permitted), re-resolves it (resolve → re-derive exact match → re-inspect **STILL !DataBearing**) and formats the RE-RESOLVED device, never `req.Device`. The format-job record carries `blank`; restart recovery re-checks blank jobs with the blank variant. Confirmed (data-bearing) branch untouched. | **Not in this batch (per spec §0/§12):** A1 (stale-lock pool-membership — a SPIKE: the role lacks `Pool.Audit`, no cheap membership read exists); C1, C2, A2, B2–B5, E1, E2 (deferred). B2's `..` guard intentionally skipped — `DriveNameFromRaw` already rejects `/`, so `..` alone can't traverse. ## 3. Tests + red-proofs (each new test shown FAILING on the pre-fix impl, then green on the fix) | Fix | Tests | Red-proof result | |---|---|---| | B1 | `TestInstallSnippet_RandomTempName`, `TestInstallSharedParent_RandomTempName` (fake runner records the install source; random pattern + two-calls-differ + content + cleanup asserted) | Pre-fix (HEAD files restored): both FAIL — fixed name, identical across calls. Fix restored → green. | | D2 | `TestClassifyClaim_EmptyNodesRefused`, `TestClassifyClaim_TargetAbsentFromTree` | Pre-fix `claim.go`: both FAIL — `(true,"unclaimed")` returned. Fix restored → green. | | D3 | Pure: `TestFormatBlankPath_AntiRetarget_{ReassignedDataBearingRefused,ReassignedDifferentDiskRefused,UnresolvableRefused,SameBlankProceeds}`. Handler: `TestFormat_Blank_{FormatsReresolvedDeviceNotCallerPath,ReresolveRefusalNoMkfs,NoDurableIDRefused}` | Pre-fix blank branch (formats `req.Device`, no binding): all 3 handler tests FAIL — mkfs ran on the caller path with HTTP 200 and no durable binding. Fix restored → green. | | D1 | `scripts/mkfs-guarded-harness.sh` on felhom-pve (root): throwaway loop devices (`losetup`, one `-r` read-only), PATH-shimmed `lsblk` FSTYPE fixture, `/dev/null` bound over pvs for the pvs-absent case, and a RECORDER bind-mounted over `/usr/sbin/mkfs.ext4` in a private mount ns — **no real mkfs possible**; "formats" is proven by the recorder argv, refusal by its absence | **Fixed wrapper: 8/8 PASS** (5 member FSTYPEs refused, read-only refused, pvs-hidden LVM still refused, plain blank disk still reaches mkfs). **Pre-fix wrapper: 7/8 FAIL** — every member/RO/pvs-hidden fixture reached mkfs. The loop-device `-b` check ran for real (no stub). | Full gates green on Windows AND on the build server (go1.26.0): `go build ./... && go vet ./... && go test ./...` — **0 failures**. Test count **472 → 483** (+11). ## 4. Build / publish / deploy / verify - Built on 180 from `3f382bf`: `CGO_ENABLED=0 go build -ldflags "-X main.version=0.61.0"`; `--version` → `felhom-agent 0.61.0`. - **Published to Gitea** (generic package, delete-then-put + round-trip GET verified): `AGENT_VERSION=0.61.0`, `AGENT_SHA256=d5f2f283501dac9daac1c197c9665c98f4577e11d155f5da831b26048c736ed8`. Credentials sourced out-of-band (`/root/.golden-secrets.env` on the host; not committed anywhere). - **Deployed to felhom-pve** (backups kept: `felhom-agent.bak-0.60.0`, `felhom-mkfs-guarded.bak-0.60.0`): binary → `/usr/local/bin/felhom-agent`; **new wrapper** → `/usr/local/sbin/felhom-mkfs-guarded` (grep-confirmed the D1 checks in the installed copy); **new sudoers** → `/etc/sudoers.d/felhom-agent` (`visudo -cf` passed both staged and installed). - **Verify after restart:** service `active`, `--version` 0.61.0, **0 ERROR lines** in the startup journal; **capability self-check 46/46 ok, 0 degraded** (proves the new sudoers globs cover the new manifest vectors live); `ReassertGuestBinds`/reconcile re-bound exactly the three enrolled drives (felhom-flash, felhom-usb, teszt_enroll) — nothing else; stale-lock recovery ran with nothing to reap (no stale locks, no false action); local API listening; hub desired-state pulled. - **B1 live-proven end-to-end:** made the installed shared-parent script stale (appended a marker) → the next reconcile self-healed it through the REAL sudo path with random-named temps (`install -m 0755 -- /tmp/felhom-shared-parent-281383295.sh …`, `install -m 0644 -- /tmp/felhom-shared-parent-2841067761.service …`) — authorized by the new globs. - Cleanup: all /tmp staging removed from felhom-pve and 180; harness loop devices/workdir self-cleaned. ## 5. NOT yet live-validated — awaiting a supervised step - **D1 destructive proof:** a real `mkfs` attempt against a crafted ZFS/mdraid/LUKS/swap member disk. - **D3 race proof:** a live /dev re-enumeration race during a real format. Both are proven in unit-land + the recorder harness only, per the task's hard STOP. ## 6. Observations / follow-ups (recorded, not acted on) - **Operator follow-up:** the hub Day-0 artifact manifest still vouches agent **0.60.0** — record `0.61.0` / sha `d5f2f283…` in the password-gated operator UI (Configs → Day-0 artifacts) so Day-0 self-installs fetch the new build. (CC cannot do this; see hub-artifact-manifest gotcha.) - **A1 (LOW, pending SPIKE):** stale-lock recovery reaps by a pool-blind `ListLXC` scan — contained by the pool-scoped token; MUST be fixed before any broad/root-token deployment. Open design question: which pool-membership read the role can authorize (`Pool.Audit` absent today). - Deferred audit items: C1 (controller-swap version floor), C2 (NAS server allowlist), A2 (gate journal cross-check), B3/B4/B5 housekeeping, E1/E2 comment fixes. - Known pre-existing: the non-root agent cannot read the PBS key (`felhom-pbs.pw` permission denied at startup — the BUNDLE leftover, unchanged by this batch).