agent v0.36.5: AttachDrive normalizes to exactly one bind (converges stacked binds)

countHostMounts + normalize: no-op only when exactly one bind is guest-visible;
else strip all binds and lay one fresh. Converges a stacked double-bind to one
(the old umount-one+mount-one never did).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 20:24:42 +02:00
parent a356d6def4
commit 2da4c38773
3 changed files with 47 additions and 20 deletions
+39 -19
View File
@@ -169,29 +169,49 @@ func (b *GuestBinder) AttachDrive(ctx context.Context, vmid int, where string) (
if err := b.run(ctx, "mkdir", "-p", stable); err != nil {
return "", fmt.Errorf("guest-attach: stable dir %s: %w", stable, err)
}
hostHas := isHostMountpoint(stable)
guestSees := b.GuestSeesMount(ctx, vmid, stable)
switch {
case hostHas && guestSees:
return stable, nil // fully live — no-op
case hostHas && !guestSees:
// Guest rebooted (or bound the parent before this drive's bind existed): re-fire propagation.
if err := b.run(ctx, "umount", stable); err != nil {
b.logger.Warn("guest-attach: re-bind umount failed (continuing to re-mount)", "stable", stable, "err", err)
}
if err := b.run(ctx, "mount", "--bind", src, stable); err != nil {
return "", fmt.Errorf("guest-attach: re-bind %s -> %s: %w", src, stable, err)
}
b.logger.Info("guest-attach: re-bound drive to re-propagate into guest (post-reboot)", "vmid", vmid, "where", where, "stable", stable)
default: // !hostHas
if err := b.run(ctx, "mount", "--bind", src, stable); err != nil {
return "", fmt.Errorf("guest-attach: bind %s -> %s: %w", src, stable, err)
}
b.logger.Info("guest-attach: drive bound under shared parent (live, no reboot)", "vmid", vmid, "where", where, "stable", stable)
// NORMALIZE to EXACTLY ONE bind. The target is usable only when there is exactly one bind AND the
// guest sees it; in that case this is a no-op. Otherwise (zero binds, a post-reboot bind the guest
// can't see, OR stacked duplicate binds from an earlier race/operator action) we strip ALL existing
// binds (bounded loop) and lay down exactly one fresh bind — which also re-fires propagation into the
// current guest namespace. Counting (countHostMounts) rather than a boolean isHostMountpoint is what
// makes this converge a double-bind to one (the old umount-one+mount-one never did).
n := countHostMounts(stable)
if n == 1 && b.GuestSeesMount(ctx, vmid, stable) {
return stable, nil // exactly one bind + guest-visible → fully live, no-op
}
for i := 0; i < 16 && countHostMounts(stable) > 0; i++ {
if err := b.run(ctx, "umount", stable); err != nil {
b.logger.Warn("guest-attach: normalize umount failed (continuing)", "stable", stable, "err", err)
break
}
}
if err := b.run(ctx, "mount", "--bind", src, stable); err != nil {
return "", fmt.Errorf("guest-attach: bind %s -> %s: %w", src, stable, err)
}
b.logger.Info("guest-attach: drive bound under shared parent (normalized to one bind, live)", "vmid", vmid, "where", where, "stable", stable, "prior_binds", n)
return stable, nil
}
// countHostMounts returns how many times `path` appears as a mount target in /proc/self/mountinfo (i.e.
// how many stacked binds are at it). 0 = not mounted; >1 = stacked duplicates. Used to normalize to one.
func countHostMounts(path string) int {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return 0
}
defer f.Close()
n := 0
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for sc.Scan() {
fields := strings.Fields(sc.Text())
if len(fields) >= 5 && fields[4] == path {
n++
}
}
return n
}
// GuestSeesMount reports whether vmid's guest currently has `path` as a mount target in ITS mount
// namespace (read from /proc/<guest-init-pid>/mountinfo). This is the GUEST-side truth the host-side
// isHostMountpoint can't see — the signal that distinguishes "bound on the host" from "live in the