Files
admin 3f382bf762 v0.61.0: audit fixes B1 (random temp staging) + D1 (mkfs wrapper member/RO re-checks) + D2 (empty-lsblk fail-safe) + D3 (blank-format anti-retarget)
From AUDIT-blast-radius-hostroot-localapi-2026-07-02.md. Each fix ships with a
non-hollow test + a companion red-proof (shown failing on the pre-fix impl).
Sudoers install-source grants became globs — deploy the sudoers drop-in with
the binary. A1 (stale-lock pool-membership) deliberately excluded (spike).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 07:25:50 +02:00

148 lines
6.3 KiB
Go

package localapi
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
)
// ---- Audit D3: the blank-format branch's anti-retarget guard ------------------------------
//
// AGENT-001 closed the classify→mkfs TOCTOU on the data-bearing branch; these tests pin the same
// guard onto the BLANK branch: the format must bind to a durable id, re-resolve it to the CURRENT
// device, and format THAT — never the mutable caller-supplied /dev path.
// Pure-function coverage of antiRetargetResolveBlank (the injected-deps sibling; no real /dev).
func TestFormatBlankPath_AntiRetarget_ReassignedDataBearingRefused(t *testing.T) {
const durable = "byid:wwn-0xBLANK"
dataBearing := storage.DeviceProbe{Probed: true, HasFilesystem: true, FSType: "ext4"}
got, err := antiRetargetResolveBlank(durable,
func(string) (string, error) { return "/dev/sdb", nil },
func(string) (string, error) { return durable, nil },
func(string) (storage.DeviceProbe, error) { return dataBearing, nil },
)
if err == nil {
t.Fatalf("re-inspect found a DATA-BEARING disk on the blank path — must refuse, got device %q", got)
}
if !strings.Contains(err.Error(), "now data-bearing") {
t.Errorf("error %q missing the now-data-bearing explanation", err.Error())
}
}
func TestFormatBlankPath_AntiRetarget_ReassignedDifferentDiskRefused(t *testing.T) {
const durable = "byid:wwn-0xBLANK"
blank := storage.DeviceProbe{Probed: true}
got, err := antiRetargetResolveBlank(durable,
func(string) (string, error) { return "/dev/sdb", nil },
func(string) (string, error) { return "byid:wwn-0xDIFFERENT", nil }, // node reassigned → different id
func(string) (storage.DeviceProbe, error) { return blank, nil },
)
if err == nil || !strings.Contains(err.Error(), "durable-id mismatch") {
t.Fatalf("want durable-id mismatch refusal, got device %q err %v", got, err)
}
}
func TestFormatBlankPath_AntiRetarget_UnresolvableRefused(t *testing.T) {
blank := storage.DeviceProbe{Probed: true}
// Unresolvable durable id → refuse.
if got, err := antiRetargetResolveBlank("byid:wwn-0xGONE",
func(string) (string, error) { return "", errors.New("gone") },
func(string) (string, error) { return "byid:wwn-0xGONE", nil },
func(string) (storage.DeviceProbe, error) { return blank, nil },
); err == nil || !strings.Contains(err.Error(), "no longer resolves") {
t.Fatalf("want no-longer-resolves refusal, got device %q err %v", got, err)
}
// Empty durable id (nothing to bind) → refuse: a path-only blank format is what the guard prevents.
if got, err := antiRetargetResolveBlank("",
func(string) (string, error) { return "/dev/sdb", nil },
func(string) (string, error) { return "", nil },
func(string) (storage.DeviceProbe, error) { return blank, nil },
); err == nil || !strings.Contains(err.Error(), "path-only") {
t.Fatalf("want path-only refusal on empty durable id, got device %q err %v", got, err)
}
}
func TestFormatBlankPath_AntiRetarget_SameBlankProceeds(t *testing.T) {
const durable = "byid:wwn-0xBLANK"
blank := storage.DeviceProbe{Probed: true}
got, err := antiRetargetResolveBlank(durable,
func(string) (string, error) { return "/dev/sdb", nil },
func(string) (string, error) { return durable, nil },
func(string) (storage.DeviceProbe, error) { return blank, nil },
)
if err != nil {
t.Fatalf("same still-blank disk must proceed, got refusal: %v", err)
}
if got != "/dev/sdb" {
t.Fatalf("device = %q, want the re-resolved /dev/sdb", got)
}
}
// ---- handler wiring: the blank branch formats the RE-RESOLVED device, never req.Device ----
// The red-proof companion vs the pre-fix handler: pre-fix, the blank branch called
// startFormatDetached(req.Device, ...) directly, so the format hit the caller path even when the
// re-resolve seam says the durable id now lives at a different node. Post-fix, mkfs must run on the
// re-resolved device.
func TestFormat_Blank_FormatsReresolvedDeviceNotCallerPath(t *testing.T) {
d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true}} // blank
srv := newDiskServerRaw(t, d, &fakeGate{}, nil, nil)
var boundID string
srv.reresolveBlank = func(_ context.Context, durableID string) (string, error) {
boundID = durableID
return "/dev/sdz", nil // the durable id's CURRENT node differs from the caller path
}
h := srv.Handler()
w := do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb","fstype":"ext4"}`)
if w.Code != http.StatusOK {
t.Fatalf("blank format: got %d want 200 (%s)", w.Code, w.Body.String())
}
if boundID != "byid:wwn-sdb" {
t.Errorf("blank format bound durable id %q, want the one derived from req.Device", boundID)
}
got := d.formatted()
if len(got) != 1 || got[0] != "/dev/sdz" {
t.Fatalf("mkfs ran on %v — must run on the RE-RESOLVED device /dev/sdz, never the caller path /dev/sdb", got)
}
}
// A blank-branch anti-retarget refusal (device changed in the window) must refuse 409 with ZERO mkfs.
func TestFormat_Blank_ReresolveRefusalNoMkfs(t *testing.T) {
d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true}} // blank
srv := newDiskServerRaw(t, d, &fakeGate{}, nil, nil)
srv.reresolveBlank = func(_ context.Context, durableID string) (string, error) {
return "", fmt.Errorf("%s is now data-bearing (target changed since blank inspection) — refusing", "/dev/sdb")
}
h := srv.Handler()
w := do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb","fstype":"ext4"}`)
if w.Code != http.StatusConflict {
t.Fatalf("refused blank format: got %d want 409 (%s)", w.Code, w.Body.String())
}
if got := d.formatted(); len(got) != 0 {
t.Fatalf("mkfs WAS called after an anti-retarget refusal: %v", got)
}
}
// A device with no durable id cannot be bound → the blank format is refused (no path-only formats).
func TestFormat_Blank_NoDurableIDRefused(t *testing.T) {
d := &fakeDiskOps{probe: storage.DeviceProbe{Probed: true}} // blank
srv := newDiskServerRaw(t, d, &fakeGate{}, nil, nil)
srv.deviceDurableID = func(string) (string, error) { return "", errors.New("no by-id/by-uuid entry") }
h := srv.Handler()
w := do(t, h, "POST", "/disks/format", "A", `{"device":"/dev/sdb","fstype":"ext4"}`)
if w.Code != http.StatusConflict {
t.Fatalf("unbindable blank format: got %d want 409 (%s)", w.Code, w.Body.String())
}
if got := d.formatted(); len(got) != 0 {
t.Fatalf("mkfs WAS called with no durable-id binding: %v", got)
}
}