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
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user