v0.117.0: consuming-namespace NAS verification + deploy-view truth (RCA fixes 2+4)
statfs fsclass helper (network/autofs/stub/unknown, fail-open); probe not_network_fs assertion (stub can never verify — red-proven); deploy-time stub refusal (idle autofs proceeds — red-proven); distinct stub badge, stub wins over unreachable (unreachable line byte-identical); deployed select shows stored HDD_PATH (red-proven vs IsDefault-only). MinAgent unchanged 0.81.0. Gates green. 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,64 @@
|
||||
package system
|
||||
|
||||
import "time"
|
||||
|
||||
// Consuming-namespace filesystem classification (RCA AUDIT-nas-cwa-rca-2026-07-11 fix 2).
|
||||
//
|
||||
// THE LESSON: the add-time probe, the deploy flow and the dashboard all trusted host-side (agent)
|
||||
// health for network storage — while the namespace the apps actually consume the path in (the
|
||||
// controller container's, an rslave copy of the guest's) can silently hold a plain local stub
|
||||
// after a guest reboot. This classifier answers "what IS this path in THIS process's namespace"
|
||||
// from statfs f_type, so verification happens where consumption happens.
|
||||
|
||||
// FS classes.
|
||||
const (
|
||||
// FSClassNetwork: a real network filesystem is mounted here (nfs/cifs/smb2).
|
||||
FSClassNetwork = "network"
|
||||
// FSClassAutofs: an idle automount trigger — HEALTHY (first access mounts it). Callers must
|
||||
// NOT force-mount to "check deeper"; waking the NAS defeats the idle-unmount design.
|
||||
FSClassAutofs = "autofs"
|
||||
// FSClassStub: anything else (ext4/tmpfs/plain dir on the system device) — the RCA's silent
|
||||
// local stub. For a registered network path this is always a defect.
|
||||
FSClassStub = "stub"
|
||||
// FSClassUnknown: statfs failed or timed out — no verdict (callers fail open; a wedged share
|
||||
// is the agent-unreachable branch's business).
|
||||
FSClassUnknown = "unknown"
|
||||
)
|
||||
|
||||
// classifyFSMagic maps a statfs f_type (linux/magic.h) to an FS class. Pure — unit-tested against
|
||||
// the magic table. Compared through the unsigned-32 view: the kernel returns f_type as a signed
|
||||
// long, so CIFS_MAGIC_NUMBER (0xFF534D42) can arrive negative depending on how it was widened.
|
||||
func classifyFSMagic(ftype int64) string {
|
||||
u := uint64(ftype) & 0xFFFFFFFF
|
||||
switch u {
|
||||
case 0x0187: // AUTOFS_SUPER_MAGIC
|
||||
return FSClassAutofs
|
||||
case 0x6969: // NFS_SUPER_MAGIC (all nfs versions)
|
||||
return FSClassNetwork
|
||||
case 0xFF534D42: // CIFS_MAGIC_NUMBER
|
||||
return FSClassNetwork
|
||||
case 0xFE534D42: // SMB2_MAGIC_NUMBER
|
||||
return FSClassNetwork
|
||||
default:
|
||||
return FSClassStub
|
||||
}
|
||||
}
|
||||
|
||||
// fsClassTimeout bounds a classification statfs — a mounted-but-dead network fs can block statfs
|
||||
// for the NFS soft-timeout window; the dashboard/deploy paths must not hang on it.
|
||||
const fsClassTimeout = 3 * time.Second
|
||||
|
||||
// ClassifyPathFSTimeout classifies path in this process's mount namespace, bounded by
|
||||
// fsClassTimeout. Timeout or statfs error → FSClassUnknown (no verdict — fail open). This is the
|
||||
// entry point for the deploy-time gate and the dashboard stub badge; the probe child uses the
|
||||
// unbounded ClassifyPathFS (its whole run is already deadline-bounded by the parent).
|
||||
func ClassifyPathFSTimeout(path string) string {
|
||||
ch := make(chan string, 1)
|
||||
go func() { ch <- ClassifyPathFS(path) }()
|
||||
select {
|
||||
case c := <-ch:
|
||||
return c
|
||||
case <-time.After(fsClassTimeout):
|
||||
return FSClassUnknown
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build linux
|
||||
|
||||
package system
|
||||
|
||||
import "syscall"
|
||||
|
||||
// statfsFn is the syscall seam (tests inject fake f_types without real mounts).
|
||||
var statfsFn = syscall.Statfs
|
||||
|
||||
// ClassifyPathFS classifies path by its filesystem magic in this process's mount namespace.
|
||||
// UNBOUNDED — statfs on a mounted-but-dead network fs can block for the soft-timeout window;
|
||||
// interactive callers use ClassifyPathFSTimeout. statfs error → FSClassUnknown.
|
||||
func ClassifyPathFS(path string) string {
|
||||
var st syscall.Statfs_t
|
||||
if err := statfsFn(path, &st); err != nil {
|
||||
return FSClassUnknown
|
||||
}
|
||||
return classifyFSMagic(int64(st.Type))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//go:build linux
|
||||
|
||||
package system
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The statfs seam wires f_type into the classifier; a statfs error yields UNKNOWN (fail open), not
|
||||
// a stub verdict.
|
||||
func TestClassifyPathFS_Seam(t *testing.T) {
|
||||
orig := statfsFn
|
||||
t.Cleanup(func() { statfsFn = orig })
|
||||
|
||||
statfsFn = func(_ string, st *syscall.Statfs_t) error { st.Type = 0x6969; return nil }
|
||||
if got := ClassifyPathFS("/anything"); got != FSClassNetwork {
|
||||
t.Errorf("nfs magic → %q, want network", got)
|
||||
}
|
||||
statfsFn = func(_ string, st *syscall.Statfs_t) error { st.Type = 0x0187; return nil }
|
||||
if got := ClassifyPathFS("/anything"); got != FSClassAutofs {
|
||||
t.Errorf("autofs magic → %q, want autofs", got)
|
||||
}
|
||||
statfsFn = func(_ string, _ *syscall.Statfs_t) error { return syscall.EIO }
|
||||
if got := ClassifyPathFS("/anything"); got != FSClassUnknown {
|
||||
t.Errorf("statfs error → %q, want unknown (fail open, never stub)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Real-IO: a plain local directory MUST classify as a stub — this is the exact state the RCA's
|
||||
// guest reboot produced, and the verdict everything downstream keys on.
|
||||
func TestClassifyPathFS_RealLocalDirIsStub(t *testing.T) {
|
||||
if got := ClassifyPathFS(t.TempDir()); got != FSClassStub {
|
||||
t.Fatalf("a plain local dir must classify as stub, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !linux
|
||||
|
||||
package system
|
||||
|
||||
// ClassifyPathFS is Linux-only (statfs f_type); off-linux (dev hosts) there is no mount namespace
|
||||
// to interrogate — no verdict, callers fail open.
|
||||
func ClassifyPathFS(path string) string {
|
||||
return FSClassUnknown
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package system
|
||||
|
||||
import "testing"
|
||||
|
||||
// The classification table (RCA fix 2): network fs magics and the healthy idle autofs trigger are
|
||||
// OK; anything local is a STUB.
|
||||
func TestClassifyFSMagic_Table(t *testing.T) {
|
||||
cifsU32 := uint32(0xFF534D42) // runtime value: the sign-extended form must classify identically
|
||||
cases := []struct {
|
||||
name string
|
||||
ftype int64
|
||||
want string
|
||||
}{
|
||||
{"autofs trigger (idle — healthy)", 0x0187, FSClassAutofs},
|
||||
{"nfs (all versions)", 0x6969, FSClassNetwork},
|
||||
{"cifs", 0xFF534D42, FSClassNetwork},
|
||||
{"cifs as sign-extended negative", int64(int32(cifsU32)), FSClassNetwork},
|
||||
{"smb2", 0xFE534D42, FSClassNetwork},
|
||||
{"ext4 (the RCA stub)", 0xEF53, FSClassStub},
|
||||
{"tmpfs", 0x01021994, FSClassStub},
|
||||
{"overlayfs", 0x794C7630, FSClassStub},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := classifyFSMagic(c.ftype); got != c.want {
|
||||
t.Errorf("%s: classifyFSMagic(%#x) = %q, want %q", c.name, c.ftype, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user