v0.54.0: format-safety foundation — unclaimed-disk guard + guarded-mkfs wrapper

Impl-1. Format now runs a mandatory unclaimed-disk guard (internal/storage/claim.go:
SystemDisks + lsblk member-FSTYPE + foreign-mount + RO + pvs/zpool; fail-safe →
CLAIMED) before any mkfs — refuses the OS disk / LVM PV / ZFS-mdraid member /
foreign-mounted device even when non-data-bearing (guard sits in Format, not the
handler). Below the agent, mkfs goes ONLY through configs/felhom-mkfs-guarded.sh
(sudoers no longer allowlists raw mkfs.*), which re-checks the catastrophic cases
as root. Read-only pvs/zpool added to FELHOM_DISK. Tests + red-proof; capability
manifest updated. go build/vet/test clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 16:52:31 +02:00
parent 05f22a9ab4
commit 52098302ab
10 changed files with 600 additions and 32 deletions
+34 -13
View File
@@ -104,8 +104,11 @@ type Binaries struct {
Lvs string
Blkid string // device signature probe (8C data-bearing detection)
Lsblk string // partition/mount topology (8C)
MkfsExt4 string // 8C format executor (ext4)
MkfsXfs string // 8C format executor (xfs)
MkfsExt4 string // 8C format executor (ext4) — now invoked by the guarded wrapper, not the agent directly
MkfsXfs string // 8C format executor (xfs) — now invoked by the guarded wrapper, not the agent directly
MkfsGuarded string // Impl-1 Part B: the guarded-mkfs wrapper the agent execs (device+fstype)
Pvs string // Impl-1 claim filter: LVM physical-volume enumeration (read-only)
Zpool string // Impl-1 claim filter: ZFS pool member enumeration (read-only)
}
func (b Binaries) withDefaults() Binaries {
@@ -133,6 +136,15 @@ func (b Binaries) withDefaults() Binaries {
if b.MkfsXfs == "" {
b.MkfsXfs = "/usr/sbin/mkfs.xfs"
}
if b.MkfsGuarded == "" {
b.MkfsGuarded = "/usr/local/sbin/felhom-mkfs-guarded"
}
if b.Pvs == "" {
b.Pvs = "/usr/sbin/pvs"
}
if b.Zpool == "" {
b.Zpool = "/usr/sbin/zpool"
}
return b
}
@@ -145,6 +157,7 @@ type SudoHostOps struct {
bins Binaries
unitDir string // where enabled units live (e.g. /etc/systemd/system)
stageDir string // agent-owned staging dir for unit files before install
host HostReader // root-free reads (mount table) for the Impl-1 Format claim guard
logger *slog.Logger
}
@@ -154,6 +167,7 @@ type SudoHostOpsConfig struct {
Bins Binaries
UnitDir string // default /etc/systemd/system
StageDir string // default <dataDir>/units; must be agent-writable
Host HostReader // default NewProcHostReader(); the Format claim guard's mount-table read
Logger *slog.Logger
}
@@ -171,11 +185,16 @@ func NewSudoHostOps(cfg SudoHostOpsConfig) *SudoHostOps {
if logger == nil {
logger = slog.Default()
}
host := cfg.Host
if host == nil {
host = NewProcHostReader()
}
return &SudoHostOps{
runner: cfg.Runner,
bins: cfg.Bins.withDefaults(),
unitDir: unitDir,
stageDir: stageDir,
host: host,
logger: logger,
}
}
@@ -423,17 +442,19 @@ func (h *SudoHostOps) Format(ctx context.Context, device, fstype string) error {
if err := ValidateFSType(fstype); err != nil {
return err
}
switch fstype {
case "ext4":
if err := h.run(ctx, h.bins.MkfsExt4, "-F", device); err != nil {
return fmt.Errorf("storage: mkfs.ext4 %s: %w", device, err)
}
case "xfs":
if err := h.run(ctx, h.bins.MkfsXfs, "-f", device); err != nil {
return fmt.Errorf("storage: mkfs.xfs %s: %w", device, err)
}
default:
return fmt.Errorf("storage: unsupported fstype %q", fstype) // unreachable after Validate
// MANDATORY unclaimed-disk guard (Impl-1): mkfs is destructive and the sudoers permits `mkfs /dev/*`,
// so THIS check — not the caller's authorization and not `DataBearing` (the OS disk is data-bearing) —
// is the real guard. Evaluated on the device as passed (the caller re-resolves the durable-id first).
// Fail-safe: anything not provably unclaimed (incl. any read error) is refused BEFORE any mkfs.
if ok, reason := h.deviceUnclaimed(ctx, device); !ok {
h.logger.Warn("storage: REFUSING format — device is claimed", "device", device, "reason", reason)
return fmt.Errorf("storage: refusing to format %s: %s", device, reason)
}
// Part B: mkfs runs through the guarded wrapper (the sudoers allowlists ONLY the wrapper, not raw
// mkfs) — a second, below-the-agent gate that re-checks the catastrophic cases even against an agent
// bug. The wrapper takes <device> <fstype> and picks/execs the right mkfs.
if err := h.run(ctx, h.bins.MkfsGuarded, device, fstype); err != nil {
return fmt.Errorf("storage: guarded mkfs %s (%s): %w", device, fstype, err)
}
h.logger.Info("storage: formatted device", "device", device, "fstype", fstype)
return nil