F11 matrix-correction: re-arm on guest-blind (active mounts not inherited by rebooted guest — live finding)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
|||||||
// netReasserter is the reassert capability (satisfied by *storage.SudoHostOps; faked in tests).
|
// netReasserter is the reassert capability (satisfied by *storage.SudoHostOps; faked in tests).
|
||||||
type netReasserter interface {
|
type netReasserter interface {
|
||||||
ReassertNetworkAutomounts(ctx context.Context) []storage.NetReassertResult
|
ReassertNetworkAutomounts(ctx context.Context) []storage.NetReassertResult
|
||||||
|
RearmNetworkAutomount(ctx context.Context, where string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostStartNetworkReassert re-arms idle NAS automount triggers after vmid started, then verifies
|
// PostStartNetworkReassert re-arms idle NAS automount triggers after vmid started, then verifies
|
||||||
@@ -48,9 +49,21 @@ func postStartNetworkReassert(ctx context.Context, vmid string, ops netReasserte
|
|||||||
if sees(ctx, vmid, res.Where) {
|
if sees(ctx, vmid, res.Where) {
|
||||||
fmt.Fprintf(os.Stderr, "felhom-agent guest-hook: vmid %s post-start — network share %s visible in guest (%s)\n",
|
fmt.Fprintf(os.Stderr, "felhom-agent guest-hook: vmid %s post-start — network share %s visible in guest (%s)\n",
|
||||||
vmid, res.Name, res.Action)
|
vmid, res.Name, res.Action)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// F11 matrix-correction: the guest is blind even though the pass classified this share
|
||||||
|
// skip-active/rearmed. A freshly-started namespace does NOT inherit an ACTIVE host mount — only
|
||||||
|
// a FRESH trigger event propagates in. Re-arm this specific share and re-verify once.
|
||||||
|
fmt.Fprintf(os.Stderr, "felhom-agent guest-hook: vmid %s post-start — network share %s not visible after reassert (%s) — re-arming\n",
|
||||||
|
vmid, res.Name, res.Action)
|
||||||
|
if err := ops.RearmNetworkAutomount(ctx, res.Where); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "felhom-agent guest-hook: vmid %s post-start — WARNING: re-arm of %s failed: %v\n", vmid, res.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sees(ctx, vmid, res.Where) {
|
||||||
|
fmt.Fprintf(os.Stderr, "felhom-agent guest-hook: vmid %s post-start — network share %s visible in guest after re-arm (healed)\n", vmid, res.Name)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "felhom-agent guest-hook: vmid %s post-start — WARNING: network share %s NOT visible in guest after reassert (%s)\n",
|
fmt.Fprintf(os.Stderr, "felhom-agent guest-hook: vmid %s post-start — WARNING: network share %s STILL NOT visible after re-arm\n", vmid, res.Name)
|
||||||
vmid, res.Name, res.Action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type fakeReasserter struct {
|
type fakeReasserter struct {
|
||||||
invoked int
|
invoked int
|
||||||
results []storage.NetReassertResult
|
rearmed []string
|
||||||
|
rearmErr error
|
||||||
|
results []storage.NetReassertResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeReasserter) ReassertNetworkAutomounts(context.Context) []storage.NetReassertResult {
|
func (f *fakeReasserter) ReassertNetworkAutomounts(context.Context) []storage.NetReassertResult {
|
||||||
@@ -17,6 +19,11 @@ func (f *fakeReasserter) ReassertNetworkAutomounts(context.Context) []storage.Ne
|
|||||||
return f.results
|
return f.results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeReasserter) RearmNetworkAutomount(_ context.Context, where string) error {
|
||||||
|
f.rearmed = append(f.rearmed, where)
|
||||||
|
return f.rearmErr
|
||||||
|
}
|
||||||
|
|
||||||
// The post-start core must run the reassert pass and verify guest visibility for every share the
|
// The post-start core must run the reassert pass and verify guest visibility for every share the
|
||||||
// pass acted on (or found actively mounted) — and never for foreign/errored rows.
|
// pass acted on (or found actively mounted) — and never for foreign/errored rows.
|
||||||
func TestPostStartNetworkReassert_Core(t *testing.T) {
|
func TestPostStartNetworkReassert_Core(t *testing.T) {
|
||||||
@@ -41,16 +48,41 @@ func TestPostStartNetworkReassert_Core(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A failed verify must be non-fatal: the core returns normally (the hook exits 0 regardless).
|
// F11 matrix-correction: a guest that does NOT see a share (even a skip-active one) must trigger a
|
||||||
func TestPostStartNetworkReassert_VerifyFailureNonFatal(t *testing.T) {
|
// re-arm of that specific share, then a re-verify. Encodes the live 2026-07-12 finding that a rebooted
|
||||||
|
// guest inherits neither an idle trigger nor an active mount.
|
||||||
|
func TestPostStartNetworkReassert_RearmsWhenGuestBlind(t *testing.T) {
|
||||||
ops := &fakeReasserter{results: []storage.NetReassertResult{
|
ops := &fakeReasserter{results: []storage.NetReassertResult{
|
||||||
{Name: "media", Where: "/mnt/felhom-drives/media", Action: storage.NetReassertRearmed},
|
{Name: "media", Where: "/mnt/felhom-drives/media", Action: storage.NetReassertSkipActive},
|
||||||
}}
|
}}
|
||||||
// Must not panic or abort; the WARNING goes to stderr (PVE task log).
|
// sees: false the first time (blind), true the second (post-rearm) — the heal path.
|
||||||
|
calls := 0
|
||||||
|
postStartNetworkReassert(context.Background(), "9201", ops, func(context.Context, string, string) bool {
|
||||||
|
calls++
|
||||||
|
return calls >= 2
|
||||||
|
})
|
||||||
|
if len(ops.rearmed) != 1 || ops.rearmed[0] != "/mnt/felhom-drives/media" {
|
||||||
|
t.Fatalf("a blind guest must trigger exactly one re-arm of the share, got %v", ops.rearmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failed verify must be non-fatal even if the re-arm also fails: the core returns normally (hook
|
||||||
|
// exits 0 regardless).
|
||||||
|
func TestPostStartNetworkReassert_VerifyFailureNonFatal(t *testing.T) {
|
||||||
|
ops := &fakeReasserter{
|
||||||
|
rearmErr: context.DeadlineExceeded,
|
||||||
|
results: []storage.NetReassertResult{
|
||||||
|
{Name: "media", Where: "/mnt/felhom-drives/media", Action: storage.NetReassertRearmed},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Must not panic or abort; the WARNING goes to stderr (PVE task log). Guest never sees it, re-arm errors.
|
||||||
postStartNetworkReassert(context.Background(), "9201", ops, func(context.Context, string, string) bool {
|
postStartNetworkReassert(context.Background(), "9201", ops, func(context.Context, string, string) bool {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if ops.invoked != 1 {
|
if ops.invoked != 1 {
|
||||||
t.Fatalf("reassert pass invoked %d times, want 1", ops.invoked)
|
t.Fatalf("reassert pass invoked %d times, want 1", ops.invoked)
|
||||||
}
|
}
|
||||||
|
if len(ops.rearmed) != 1 {
|
||||||
|
t.Fatalf("a blind guest must attempt one re-arm, got %d", len(ops.rearmed))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
// NetworkStorageOps interface (and its test fakes) stay unchanged.
|
// NetworkStorageOps interface (and its test fakes) stay unchanged.
|
||||||
type networkReasserter interface {
|
type networkReasserter interface {
|
||||||
ReassertNetworkAutomounts(ctx context.Context) []storage.NetReassertResult
|
ReassertNetworkAutomounts(ctx context.Context) []storage.NetReassertResult
|
||||||
|
RearmNetworkAutomount(ctx context.Context, where string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReassertNetworkMounts is the NAS counterpart of ReassertGuestBinds (RCA
|
// ReassertNetworkMounts is the NAS counterpart of ReassertGuestBinds (RCA
|
||||||
@@ -50,9 +51,26 @@ func (s *Server) ReassertNetworkMounts(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
if s.guestAttach.GuestSeesMount(ctx, vmid, res.Where) {
|
if s.guestAttach.GuestSeesMount(ctx, vmid, res.Where) {
|
||||||
s.logger.Debug("netreassert: guest sees network share", "vmid", vmid, "name", res.Name, "where", res.Where)
|
s.logger.Debug("netreassert: guest sees network share", "vmid", vmid, "name", res.Name, "where", res.Where)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// F11 matrix-correction (2026-07-12): a running guest that does NOT see a share must be
|
||||||
|
// remediated — a fresh namespace inherits neither an idle trigger NOR an active mount; only a
|
||||||
|
// FRESH trigger event propagates in. Re-arm this share and re-verify once (covers guests that
|
||||||
|
// autostarted before the agent).
|
||||||
|
r, ok := s.netStorage.(networkReasserter)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.logger.Warn("netreassert: guest does NOT see network share after reassert — re-arming",
|
||||||
|
"vmid", vmid, "name", res.Name, "where", res.Where, "action", res.Action)
|
||||||
|
if err := r.RearmNetworkAutomount(ctx, res.Where); err != nil {
|
||||||
|
s.logger.Warn("netreassert: re-arm failed", "vmid", vmid, "name", res.Name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.guestAttach.GuestSeesMount(ctx, vmid, res.Where) {
|
||||||
|
s.logger.Info("netreassert: guest sees network share after re-arm (healed)", "vmid", vmid, "name", res.Name, "where", res.Where)
|
||||||
} else {
|
} else {
|
||||||
s.logger.Warn("netreassert: guest does NOT see network share after reassert",
|
s.logger.Warn("netreassert: guest STILL does not see network share after re-arm", "vmid", vmid, "name", res.Name, "where", res.Where)
|
||||||
"vmid", vmid, "name", res.Name, "where", res.Where, "action", res.Action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type fakeNetOpsReassert struct {
|
type fakeNetOpsReassert struct {
|
||||||
fakeNetOps
|
fakeNetOps
|
||||||
reassertN int
|
reassertN int
|
||||||
|
rearmed []string
|
||||||
results []storage.NetReassertResult
|
results []storage.NetReassertResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,6 +20,11 @@ func (f *fakeNetOpsReassert) ReassertNetworkAutomounts(context.Context) []storag
|
|||||||
return f.results
|
return f.results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeNetOpsReassert) RearmNetworkAutomount(_ context.Context, where string) error {
|
||||||
|
f.rearmed = append(f.rearmed, where)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// seeingAttacher records GuestSeesMount calls and controls per-vmid running state.
|
// seeingAttacher records GuestSeesMount calls and controls per-vmid running state.
|
||||||
type seeingAttacher struct {
|
type seeingAttacher struct {
|
||||||
fakeGuestAttacher
|
fakeGuestAttacher
|
||||||
|
|||||||
@@ -156,6 +156,17 @@ func (h *SudoHostOps) resetNetworkAutomountIfFailed(ctx context.Context, where s
|
|||||||
return reset
|
return reset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RearmNetworkAutomount is the caller-driven remediation (CAMPAIGN-3 F11, matrix-corrected): when a
|
||||||
|
// running guest does NOT see a share after the reassert pass, re-arm its trigger regardless of the
|
||||||
|
// host's mount state. LIVE FINDING (matrix row 1/2, 2026-07-12): a freshly-rebooted guest does NOT
|
||||||
|
// inherit an ACTIVE host mount either — only a fresh trigger-mount event propagates into the new
|
||||||
|
// namespace — so skip-active is safe to skip PROACTIVELY (don't churn shares guests already see) but
|
||||||
|
// must be remediated REACTIVELY when a specific guest is blind. reset-failed first (F10), then re-arm.
|
||||||
|
func (h *SudoHostOps) RearmNetworkAutomount(ctx context.Context, where string) error {
|
||||||
|
h.resetNetworkAutomountIfFailed(ctx, where)
|
||||||
|
return h.rearmNetworkAutomount(ctx, where)
|
||||||
|
}
|
||||||
|
|
||||||
// rearmNetworkAutomount stops then re-enables+starts the .automount for a mountpoint. The stop is
|
// rearmNetworkAutomount stops then re-enables+starts the .automount for a mountpoint. The stop is
|
||||||
// tolerated failing (unit not loaded); the enable --now is the action that must succeed. Both verbs
|
// tolerated failing (unit not loaded); the enable --now is the action that must succeed. Both verbs
|
||||||
// are the existing FELHOM_NETMOUNT sudoers grants.
|
// are the existing FELHOM_NETMOUNT sudoers grants.
|
||||||
|
|||||||
Reference in New Issue
Block a user