Name a filesystem by its kernel id, never its device name (hq ADR 0325 fourth review)

An ext4 or xfs library mounted by UUID can come back from a reboot as another device
(/dev/sdb1 as /dev/sdc1); naming the filesystem by mountinfo's source read that as a
move, stripped the warranted library's entries and asked for a new Approve. A
filesystem is now named by statfs's f_fsid (from the UUID, or the ZFS dataset), its
type and its mount point. Entries left on the filesystem a folder is on now are
merged back or held, never walked away, and a TakenOn naming a mount point is not
taken.
This commit is contained in:
2026-10-12 19:10:18 +02:00
parent 21cd036232
commit f00f9c2a2f
6 changed files with 147 additions and 32 deletions
+25 -17
View File
@@ -256,7 +256,7 @@ func assessFolder(ctx context.Context, path string, rec store.KeptFolder, taken
}
// The controller's word that the folder was taken holds for the filesystem the warrant was carried out on: a
// folder moved to another is not taken, and asks again (§8).
takenHere := taken && (takenOnID(rec.TakenOn) == here || (rec.TakenOn == "" && !moved))
takenHere := taken && (rec.TakenOn == here || (rec.TakenOn == "" && !moved))
switch {
case rec.Kept && !moved:
return folderVerdict{kind: folderKeep, mount: pl.Mount, how: rec.How}
@@ -279,24 +279,16 @@ func assessFolder(ctx context.Context, path string, rec store.KeptFolder, taken
func movedRecord(f store.KeptFolder, mount string) store.KeptFolder {
left := f.Left
if f.How == store.KeptByWarrant && len(f.Users) > 0 && f.Mount != "" {
left = addLeft(left, f.Mount, f.Users)
left = addLeft(left, f.Mount, f.Placed, f.Users)
}
return store.KeptFolder{Path: f.Path, Mount: mount, Placed: PlacedID(mount), Want: f.Want, IDs: f.IDs,
Act: f.Act, TakenOn: f.TakenOn, Left: left}
}
// takenOnID is a record's TakenOn as a filesystem's name: one written before names held the mount point itself.
func takenOnID(takenOn string) string {
if strings.HasPrefix(takenOn, "/") {
return PlacedID(takenOn)
}
return takenOn
}
// addLeft adds users' entries left on mount, once per mount and uid.
func addLeft(left []store.KeptLeft, mount string, users []store.KeptUser) []store.KeptLeft {
func addLeft(left []store.KeptLeft, mount, placed string, users []store.KeptUser) []store.KeptLeft {
for i := range left {
if left[i].Mount == mount {
if left[i].Mount == mount && left[i].Placed == placed {
have := usersOf(left[i].Users)
for _, u := range users {
if _, had := have[u.UID]; !had {
@@ -306,20 +298,37 @@ func addLeft(left []store.KeptLeft, mount string, users []store.KeptUser) []stor
return left
}
}
return append(left, store.KeptLeft{Mount: mount, Users: append([]store.KeptUser(nil), users...)})
return append(left, store.KeptLeft{Mount: mount, Placed: placed, Users: append([]store.KeptUser(nil), users...)})
}
// PlacedID names the filesystem at mount for a wait, with no path in it: the controller asks again when the
// folder's filesystem is not the one a warrant was given for.
// folder's filesystem is not the one a warrant was given for. It is built from what stays the same across reboots
// for one filesystem — the kernel's filesystem id (statfs f_fsid: ext4 and xfs derive it from the filesystem's
// UUID, ZFS from the dataset), the filesystem's type and its mount point — and never from the device name
// mountinfo gives as its source, which a reboot may renumber (/dev/sdb1 becoming /dev/sdc1).
func PlacedID(mount string) string {
if mount == "" {
return ""
}
source, fstype := mountSourceOf(mount)
sum := sha256.Sum256([]byte(mount + "\x00" + source + "\x00" + fstype))
_, fstype := mountSourceOf(mount)
fsid := fsidWithin(mount)
sum := sha256.Sum256([]byte(mount + "\x00" + fsid + "\x00" + fstype))
return "fs-" + hex.EncodeToString(sum[:6])
}
// fsidWithin is MountFsid within statBound: empty when it does not answer, so a mount that is down is named by its
// point and type alone, never held on.
func fsidWithin(mount string) string {
done := make(chan string, 1)
go func() { done <- MountFsid(mount) }()
select {
case id := <-done:
return id
case <-time.After(statBound):
return ""
}
}
// statBound is how long a read of a folder may take before it is said not to answer: a hard network mount whose
// server is down blocks a stat for ever, and one access must never hold the apply.
var statBound = 5 * time.Second
@@ -534,7 +543,6 @@ func applyAccessFolder(ctx context.Context, r *declaration.Access, in inputs) (O
// The controller says it was taken, and this engine has no record of where: here, now.
f.TakenOn = PlacedID(v.mount)
}
f.TakenOn = takenOnID(f.TakenOn)
if first {
f.How, f.At = v.how, time.Now().UTC()
// A folder that held nothing has nothing under it to walk; one the controller says was taken