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:
+34
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user