diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad5346..7f91ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to **felhom-agent** are recorded here. Update on every code change that gets pushed. +## v0.36.3 — DetachDrive loop-umounts stacked binds (2026-06-15) + +DetachDrive now umounts ALL stacked binds at a stable path (bounded loop), not just one layer — so an +eject/detach fully detaches even if more than one bind accumulated (operator bind on top, or a rare +attach race), keeping the fail-close intact. Caught in the E13 rapid eject/reconnect sweep. + ## v0.36.2 — eject also keeps the raw mounted (reconnectable) (2026-06-15) Extends v0.36.1 to EJECT: eject now DetachDrive`s the bind under the parent but LEAVES the raw diff --git a/cmd/felhom-agent/main.go b/cmd/felhom-agent/main.go index 82b4890..38d1ba6 100644 --- a/cmd/felhom-agent/main.go +++ b/cmd/felhom-agent/main.go @@ -44,7 +44,7 @@ import ( // version is the agent version. Overridable at build time with // -ldflags "-X main.version="; defaults to the in-repo CHANGELOG version. -var version = "0.36.2" +var version = "0.36.3" // runGuestHook is the PVE pre-start hook body (`felhom-agent guest-hook `). On the // pre-start phase it creates placeholder dirs for any absent bind-mount source so the guest always boots diff --git a/internal/localapi/intermediary.go b/internal/localapi/intermediary.go index 293c331..c155cc8 100644 --- a/internal/localapi/intermediary.go +++ b/internal/localapi/intermediary.go @@ -293,13 +293,18 @@ func (b *GuestBinder) DetachDrive(ctx context.Context, where string) error { if stable == "" { return fmt.Errorf("guest-detach: %q is not a /mnt/ mount", where) } - if !isHostMountpoint(stable) { - return nil // already detached + // Loop-umount: a stable path can carry MORE THAN ONE stacked bind (e.g. an operator-applied bind on + // top of the agent's, or a rare attach race). Detach must remove ALL layers, else eject leaves a + // lower bind exposing data → fail-close broken. Bounded to avoid an infinite loop. + for i := 0; i < 16 && isHostMountpoint(stable); i++ { + if err := b.run(ctx, "umount", stable); err != nil { + return fmt.Errorf("guest-detach: umount %s (layer %d): %w", stable, i, err) + } } - if err := b.run(ctx, "umount", stable); err != nil { - return fmt.Errorf("guest-detach: umount %s: %w", stable, err) + if isHostMountpoint(stable) { + return fmt.Errorf("guest-detach: %s still a mountpoint after 16 umounts", stable) } - b.logger.Info("guest-detach: drive unmounted from shared parent (live, fail-closed)", "where", where, "stable", stable) + b.logger.Info("guest-detach: drive fully unmounted from shared parent (live, fail-closed)", "where", where, "stable", stable) return nil }