3f382bf762
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
186 lines
7.9 KiB
Go
186 lines
7.9 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// classifyClaim is the pure guard verdict — the load-bearing safety logic. Table-driven over every
|
|
// claim signal + the fail-safe, plus the two ALLOW cases (clean disk, and re-init of our own drive).
|
|
func TestClassifyClaim(t *testing.T) {
|
|
base := func() claimFacts {
|
|
return claimFacts{device: "/dev/sdd", wholeDisk: "/dev/sdd", wholeDiskOK: true,
|
|
nodes: []claimNode{{name: "sdd", fstype: "", mountpoint: ""}, {name: "sdd1", fstype: "ntfs", mountpoint: ""}}}
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
mutate func(f *claimFacts)
|
|
wantUnclaimed bool
|
|
reasonHas string
|
|
}{
|
|
{"clean unclaimed disk (ntfs, unmounted)", func(f *claimFacts) {}, true, "unclaimed"},
|
|
{"blank disk", func(f *claimFacts) { f.nodes = []claimNode{{name: "sdd"}} }, true, "unclaimed"},
|
|
{"our own drive re-init (mounted under /mnt/felhom-drives)", func(f *claimFacts) {
|
|
f.nodes = []claimNode{{name: "sdd"}, {name: "sdd1", fstype: "ext4", mountpoint: "/mnt/felhom-drives/felhom-flash"}}
|
|
}, true, "unclaimed"},
|
|
{"system/OS disk (non-data-bearing) — RED-PROOF vs DataBearing", func(f *claimFacts) {
|
|
f.isSystem = true
|
|
f.nodes = []claimNode{{name: "sda"}} // blank: DataBearing would say benign; the guard must still refuse
|
|
}, false, "system/OS disk"},
|
|
{"LVM PV (pvs)", func(f *claimFacts) { f.lvmPV = true }, false, "LVM physical volume"},
|
|
{"LVM2_member via lsblk fstype", func(f *claimFacts) {
|
|
f.nodes = []claimNode{{name: "sdd"}, {name: "sdd1", fstype: "LVM2_member"}}
|
|
}, false, "LVM2_member"},
|
|
{"ZFS pool member (zpool)", func(f *claimFacts) { f.zfsMember = true }, false, "ZFS pool member"},
|
|
{"zfs_member via lsblk fstype", func(f *claimFacts) {
|
|
f.nodes = []claimNode{{name: "sdd", fstype: "zfs_member"}}
|
|
}, false, "zfs_member"},
|
|
{"mdraid member", func(f *claimFacts) {
|
|
f.nodes = []claimNode{{name: "sdd1", fstype: "linux_raid_member"}}
|
|
}, false, "linux_raid_member"},
|
|
{"active swap signature", func(f *claimFacts) {
|
|
f.nodes = []claimNode{{name: "sdd1", fstype: "swap"}}
|
|
}, false, "swap"},
|
|
{"foreign mount (outside /mnt/felhom-drives)", func(f *claimFacts) {
|
|
f.nodes = []claimNode{{name: "sdd1", fstype: "ext4", mountpoint: "/srv/data"}}
|
|
}, false, "mounted at /srv/data"},
|
|
{"read-only device", func(f *claimFacts) { f.readonly = true }, false, "read-only"},
|
|
{"undeterminable topology", func(f *claimFacts) { f.wholeDiskOK = false }, false, "undeterminable"},
|
|
{"fail-safe: gather error", func(f *claimFacts) { f.gatherErr = "lsblk failed" }, false, "could not determine"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
f := base()
|
|
tc.mutate(&f)
|
|
gotUnclaimed, reason := classifyClaim(f)
|
|
if gotUnclaimed != tc.wantUnclaimed {
|
|
t.Fatalf("unclaimed=%v want %v (reason %q)", gotUnclaimed, tc.wantUnclaimed, reason)
|
|
}
|
|
if !strings.Contains(reason, tc.reasonHas) {
|
|
t.Errorf("reason %q missing %q", reason, tc.reasonHas)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClassifyClaim_EmptyNodesRefused is the audit-D2 negative test: a successful-but-EMPTY lsblk
|
|
// (`{"blockdevices":[]}` → zero nodes) previously skipped the member/mount loop entirely and returned
|
|
// (true,"unclaimed") — the one hole in the "undeterminable ⇒ claimed" fail-safe. It must refuse.
|
|
func TestClassifyClaim_EmptyNodesRefused(t *testing.T) {
|
|
for _, nodes := range [][]claimNode{nil, {}} {
|
|
f := claimFacts{device: "/dev/sdd", wholeDisk: "/dev/sdd", wholeDiskOK: true, nodes: nodes}
|
|
unclaimed, reason := classifyClaim(f)
|
|
if unclaimed {
|
|
t.Fatalf("nodes=%v: empty topology classified UNCLAIMED (reason %q) — fail-safe hole", nodes, reason)
|
|
}
|
|
if !strings.Contains(reason, "empty block topology") {
|
|
t.Errorf("nodes=%v: reason %q missing the empty-topology explanation", nodes, reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestClassifyClaim_TargetAbsentFromTree (audit D2): lsblk returned SOME tree, but the target
|
|
// whole-disk is not in it — the loop inspected the wrong device's signals. Undeterminable → refuse.
|
|
func TestClassifyClaim_TargetAbsentFromTree(t *testing.T) {
|
|
f := claimFacts{device: "/dev/sdd", wholeDisk: "/dev/sdd", wholeDiskOK: true,
|
|
nodes: []claimNode{{name: "sdc"}, {name: "sdc1", fstype: "ntfs"}}} // benign signals, wrong disk
|
|
unclaimed, reason := classifyClaim(f)
|
|
if unclaimed {
|
|
t.Fatalf("target-absent tree classified UNCLAIMED (reason %q)", reason)
|
|
}
|
|
if !strings.Contains(reason, "absent from block topology") {
|
|
t.Errorf("reason %q missing the target-absent explanation", reason)
|
|
}
|
|
}
|
|
|
|
func TestParseLsblkNodes(t *testing.T) {
|
|
out := []byte(`{"blockdevices":[{"name":"sdd","fstype":null,"mountpoint":null,"children":[{"name":"sdd1","fstype":"ntfs","mountpoint":null}]}]}`)
|
|
nodes, err := parseLsblkNodes(out)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if len(nodes) != 2 || nodes[0].name != "sdd" || nodes[1].name != "sdd1" || nodes[1].fstype != "ntfs" {
|
|
t.Fatalf("nodes = %+v", nodes)
|
|
}
|
|
if _, err := parseLsblkNodes([]byte("not json")); err == nil {
|
|
t.Error("expected a parse error on garbage")
|
|
}
|
|
}
|
|
|
|
// --- Format guard integration: the guard is wired into Format and actually gates mkfs. ---
|
|
|
|
// newGuardOps builds a SudoHostOps with stubbed reads: a scriptRunner for lsblk/mkfs, a fakeHostReader
|
|
// for SystemDisks, Pvs/Zpool empty (→ binaryPresent=false → skipped), and readWholeDiskRO stubbed.
|
|
func newGuardOps(sr *scriptRunner, mounts []Mount) *SudoHostOps {
|
|
return &SudoHostOps{
|
|
runner: sr,
|
|
// Pvs/Zpool empty ⇒ binaryPresent=false ⇒ skipped (deterministic across OSes; lsblk carries the signals).
|
|
bins: Binaries{Lsblk: "/usr/bin/lsblk", MkfsGuarded: "/usr/local/sbin/felhom-mkfs-guarded"},
|
|
host: &fakeHostReader{mounts: mounts},
|
|
logger: quietLogger(),
|
|
}
|
|
}
|
|
|
|
func mkfsCalled(sr *scriptRunner) bool {
|
|
for _, c := range sr.calls {
|
|
if strings.Contains(c[0], "mkfs") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestFormatGuard_RefusesSystemDisk(t *testing.T) {
|
|
defer stubRO(false)()
|
|
sr := &scriptRunner{out: map[string][]byte{
|
|
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sda","children":[{"name":"sda2","mountpoint":"/"}]}]}`),
|
|
}}
|
|
// / on /dev/sda2 → SystemDisks resolves {/dev/sda}; formatting /dev/sda must be refused.
|
|
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
|
|
err := ops.Format(context.Background(), "/dev/sda", "ext4")
|
|
if err == nil || !strings.Contains(err.Error(), "system/OS disk") {
|
|
t.Fatalf("want system-disk refusal, got %v", err)
|
|
}
|
|
if mkfsCalled(sr) {
|
|
t.Fatal("mkfs was invoked on a claimed device — guard failed")
|
|
}
|
|
}
|
|
|
|
func TestFormatGuard_RefusesLVMMember(t *testing.T) {
|
|
defer stubRO(false)()
|
|
sr := &scriptRunner{out: map[string][]byte{
|
|
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sdd","children":[{"name":"sdd1","fstype":"LVM2_member"}]}]}`),
|
|
}}
|
|
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
|
|
err := ops.Format(context.Background(), "/dev/sdd", "ext4")
|
|
if err == nil || !strings.Contains(err.Error(), "LVM2_member") {
|
|
t.Fatalf("want LVM-member refusal, got %v", err)
|
|
}
|
|
if mkfsCalled(sr) {
|
|
t.Fatal("mkfs invoked on an LVM member — guard failed")
|
|
}
|
|
}
|
|
|
|
func TestFormatGuard_AllowsUnclaimed(t *testing.T) {
|
|
defer stubRO(false)()
|
|
sr := &scriptRunner{out: map[string][]byte{
|
|
"/usr/bin/lsblk": []byte(`{"blockdevices":[{"name":"sdd","children":[{"name":"sdd1","fstype":"ntfs"}]}]}`),
|
|
}}
|
|
// /dev/sdd is not the system disk, not a member, not mounted → guard passes → mkfs runs.
|
|
ops := newGuardOps(sr, []Mount{{Device: "/dev/sda2", MountPoint: "/"}})
|
|
if err := ops.Format(context.Background(), "/dev/sdd", "ext4"); err != nil {
|
|
t.Fatalf("unclaimed disk must format, got %v", err)
|
|
}
|
|
if !mkfsCalled(sr) {
|
|
t.Fatal("mkfs was NOT invoked on an unclaimed disk — guard over-refused")
|
|
}
|
|
}
|
|
|
|
// stubRO overrides the /sys read-only probe for the duration of a test; the returned func restores it.
|
|
func stubRO(ro bool) func() {
|
|
orig := readWholeDiskRO
|
|
readWholeDiskRO = func(string) (bool, error) { return ro, nil }
|
|
return func() { readWholeDiskRO = orig }
|
|
}
|