//go:build linux package apply import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "sync" "testing" "time" "golang.org/x/sys/unix" "github.com/novox/mesh-host/internal/store" ) // The engine's half of ADR 0325's checks (novox/hq ADR 0325, "How it is checked", issue 567): a shared folder kept // through access entries, on a filesystem of the test's own. Nothing here needs root: a file's owner may set its // ACL, and the identities are numbers no account here has. const ( radarr = 61001 // a container's identity, read-write plex = 61002 // a container's identity, read runner = 61003 // the tool runner's account ) // aclFolder is a folder of the test's own on a filesystem that carries ACLs, or the test is skipped saying why. func aclFolder(t *testing.T) string { t.Helper() dir := t.TempDir() fd, err := unix.Open(dir, unix.O_RDONLY|unix.O_DIRECTORY, 0) if err != nil { t.Fatal(err) } defer unix.Close(fd) probe := keptACL(minimalACL(0o755), map[uint32]uint16{radarr: 5}) if err := setACL(fd, aclAccessXattr, probe); errors.Is(err, ErrNoACL) { t.Skipf("the test's filesystem (%s) carries no POSIX ACLs (ENOTSUP), and every check here reads them", dir) } else if err != nil { t.Fatal(err) } if err := unix.Fremovexattr(fd, aclAccessXattr); err != nil { t.Fatal(err) } if err := os.Chmod(dir, 0o755); err != nil { t.Fatal(err) } return dir } // keeping has the test keep its own record of kept folders, and judge mounts as the test says. func keeping(t *testing.T, points []string, targets []string) { t.Helper() was, wasPoints, wasTargets := KeptFolders, MountPoints, MountTargets KeptFolders = NewKeptMemory() forgetMountHistory() MountPoints = func() []string { return append([]string{"/"}, points...) } MountTargets = func(context.Context) []string { return targets } t.Cleanup(func() { KeptFolders, MountPoints, MountTargets = was, wasPoints, wasTargets forgetMountHistory() }) } // accessJSON is an access of module to path with users, each "identity:uid:use:from". func accessJSON(id, path, mode string, taken bool, users ...string) string { var us []string for _, u := range users { f := strings.Split(u, ":") us = append(us, fmt.Sprintf(`{"identity":%q,"uid":%s,"use":%q,"from":%q}`, f[0], f[1], f[2], f[3])) } return fmt.Sprintf(`{"id":%q,"type":"access","path":%q,"mode":%q,"taken":%v,"users":[%s]}`, id, path, mode, taken, strings.Join(us, ",")) } // readACLs is a path's access ACL and default ACL (nil when it has none), and its stat. func readACLs(t *testing.T, path string) (acl, acl, unix.Stat_t) { t.Helper() fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW, 0) if err != nil { t.Fatal(err) } defer unix.Close(fd) var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil { t.Fatal(err) } access, err := getACL(fd, aclAccessXattr, uint32(st.Mode)&0o7777) if err != nil { t.Fatal(err) } def, err := getACL(fd, aclDefaultXattr, uint32(st.Mode)&0o7777) if err != nil { t.Fatal(err) } return access, def, st } // effectiveOn is what uid may do on path, by entry and mask together. func effectiveOn(t *testing.T, path string, uid uint32) uint16 { t.Helper() a, _, st := readACLs(t, path) return effective(a, uid, st.Uid, uint32(st.Mode)&0o7777) } func applyAccesses(t *testing.T, known store.State, resources ...string) (Report, store.State, error) { t.Helper() d := declare(t, strings.Join(resources, ",")) return Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, noServices, nil, nil) } // Row 5: entries and default entries per identity and use; owner, owning group, owner's and others' bits unchanged; // the owning group's entry equals its former group bits; the mask equals what the entries need; an entry the mesh // did not add is untouched. func TestAKeptFolderCarriesAnEntryPerUserAndNothingElseOfItChanges(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) lib := filepath.Join(dir, "movies") if err := os.Mkdir(lib, 0o750); err != nil { t.Fatal(err) } if err := os.Chmod(lib, 0o750); err != nil { t.Fatal(err) } // The operator's own entry, there before: never the mesh's to change. fd, _ := unix.Open(lib, unix.O_RDONLY|unix.O_DIRECTORY, 0) operator := keptACL(minimalACL(0o750), map[uint32]uint16{60099: permRead}) operator[len(operator)-1].Perm = permRead | permExec // its mask: r-x, as the operator set it if err := setACL(fd, aclAccessXattr, operator); err != nil { t.Fatal(err) } unix.Close(fd) _, _, before := readACLs(t, lib) report, _, err := applyAccesses(t, store.State{}, accessJSON("radarr.movies", lib, "read-write", false, "mesh-radarr:61001:read-write:container"), accessJSON("plex.movies", lib, "read", false, "mesh-plex:61002:read:container")) if err != nil { t.Fatalf("an empty folder was not kept: %v", err) } access, def, after := readACLs(t, lib) if bits, _ := access.perm(aclUser, radarr); bits != 7 { t.Errorf("the read-write identity's entry is %s, and it needs rwx", permWords(bits)) } if bits, _ := access.perm(aclUser, plex); bits != 5 { t.Errorf("the read identity's entry is %s, and it needs r-x", permWords(bits)) } for _, uid := range []uint32{radarr, plex} { if _, ok := def.perm(aclUser, uid); !ok { t.Errorf("uid %d has no default entry: what is made under the folder would not be its", uid) } } if bits, _ := access.perm(aclUser, 60099); bits != permRead { t.Errorf("the operator's entry was changed to %s", permWords(bits)) } if g, _ := access.perm(aclGroupObj, 0); g != 5 { t.Errorf("the owning group's entry is %s, and its group bits gave r-x", permWords(g)) } if m, _ := access.perm(aclMask, 0); m != 7 { t.Errorf("the mask is %s, and the entries need rwx", permWords(m)) } if after.Uid != before.Uid || after.Gid != before.Gid { t.Error("the folder's owner or owning group changed") } if after.Mode&0o707 != before.Mode&0o707 { t.Errorf("the owner's or others' bits changed: %o to %o", before.Mode&0o777, after.Mode&0o777) } if after.Mode&0o070 != 0o070 { t.Errorf("the group bits show %o, and they show the mask the entries need (rwx)", (after.Mode>>3)&7) } rec, _ := keptRecord(lib) if !rec.Kept || rec.How != store.KeptEmpty { t.Errorf("the folder was not recorded kept as one that held nothing: %+v", rec) } for _, o := range report.Outcomes { if o.ID == "radarr.movies" && o.Action == "waiting" { t.Errorf("an empty folder waited for a warrant: %s", o.Detail) } } } // Row 4: the runner's entry is the widest use among the modules whose tools use the folder. func TestTheToolRunnersEntryIsTheWidestUseAnyModuleGivesIt(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) lib := filepath.Join(dir, "books") if err := os.Mkdir(lib, 0o755); err != nil { t.Fatal(err) } _, _, err := applyAccesses(t, store.State{}, accessJSON("reader.books", lib, "read", false, "nox:61003:read:tool-runner"), accessJSON("importer.books", lib, "read-write", false, "nox:61003:read-write:tool-runner")) if err != nil { t.Fatal(err) } if got := effectiveOn(t, lib, runner); got != 7 { t.Fatalf("the tool runner may %s, and one module whose tools use it declared read-write", permWords(got)) } } // Row 6: a file made at 0666 by one identity is writable by another read-write identity at once; one made at 0644 // is writable by it after the watch's repair, and before it the judgement says so; with watches exhausted, the // sweep repairs it; the judgement reads the effective right (entry and mask). func TestWhatIsMadeInAKeptFolderIsGivenItsUsersRights(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) lib := filepath.Join(dir, "downloads") if err := os.Mkdir(lib, 0o755); err != nil { t.Fatal(err) } users := []string{"mesh-radarr:61001:read-write:container", "mesh-sab:61004:read-write:container"} if _, _, err := applyAccesses(t, store.State{}, accessJSON("radarr.downloads", lib, "read-write", false, users...)); err != nil { t.Fatal(err) } rec, _ := keptRecord(lib) usual := filepath.Join(lib, "usual") if err := os.WriteFile(usual, nil, 0o666); err != nil { t.Fatal(err) } if got := effectiveOn(t, usual, radarr); got&6 != 6 { t.Fatalf("a file made at 0666 gives another read-write identity %s at once", permWords(got)) } w, err := NewWatcher() if err != nil { t.Skipf("no inotify here: %v", err) } ctx, cancel := context.WithCancel(context.Background()) ended := make(chan struct{}) go func() { w.Run(ctx); close(ended) }() defer func() { cancel(); <-ended }() w.Set(map[string][]store.KeptUser{lib: rec.Want}) narrow := filepath.Join(lib, "narrow") f, err := os.OpenFile(narrow, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { t.Fatal(err) } f.Close() waitFor(t, "the watch widened the mask of a file made at 0644", func() bool { return effectiveOn(t, narrow, radarr)&6 == 6 }) // Before a repair the judgement says so: a mask narrowed by hand where nothing watches. w.Set(nil) if err := os.Chmod(usual, 0o644); err != nil { t.Fatal(err) } c, err := JudgeKept(lib, rec.Want, ActLimits) if err != nil { t.Fatal(err) } if c.Narrowed == 0 { t.Fatal("a file whose mask gives a read-write user only read is not said by the judgement") } // Watches run out: the record says so, and the sweep puts it back. was := MaxWatchedPerFolder MaxWatchedPerFolder = 0 defer func() { MaxWatchedPerFolder = was }() w.Set(map[string][]store.KeptUser{lib: rec.Want}) if r, _ := keptRecord(lib); !r.OutOfWatches { t.Error("watches that ran out are not recorded") } look := LookKept(lib, rec.Want, SweepLimits) if look.Repaired == 0 { t.Errorf("the sweep put nothing back: %+v", look) } if got := effectiveOn(t, usual, radarr); got&6 != 6 { t.Fatalf("after the sweep a read-write identity may %s", permWords(got)) } if c, _ := JudgeKept(lib, rec.Want, ActLimits); c.Narrowed != 0 { t.Fatalf("after the sweep %d entries still give a user less than its right", c.Narrowed) } } func waitFor(t *testing.T, what string, ok func() bool) { t.Helper() deadline := time.Now().Add(3 * time.Second) for !ok() { if time.Now().After(deadline) { t.Fatalf("not within 3s: %s", what) } time.Sleep(20 * time.Millisecond) } } // Row 8, the engine's part: a folder that holds data waits, and nothing is changed without a warrant; the act adds // the entries under the folder, follows no link, enters no other filesystem; a dropped access removes exactly its // own entries. func TestAFolderThatHoldsDataIsTakenOnlyOnAWarrantAndLetGoEntryByEntry(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) lib := filepath.Join(dir, "music") outside := filepath.Join(dir, "outside") for _, d := range []string{lib, filepath.Join(lib, "album"), outside} { if err := os.Mkdir(d, 0o755); err != nil { t.Fatal(err) } } song := filepath.Join(lib, "album", "song") if err := os.WriteFile(song, []byte("data"), 0o644); err != nil { t.Fatal(err) } if err := os.Symlink(outside, filepath.Join(lib, "link")); err != nil { t.Fatal(err) } a := accessJSON("lidarr.music", lib, "read-write", false, "mesh-lidarr:61001:read-write:container", "mesh-plex:61002:read:container") report, _, err := applyAccesses(t, store.State{}, a) if err != nil { t.Fatal(err) } if o := outcomeOf(report, "lidarr.music"); o.Action != "waiting" || !strings.Contains(o.Detail, KeepWords("music")) { t.Fatalf("a folder that holds data did not wait for the operator: %+v", o) } if has := effectiveOn(t, lib, radarr); has != 0 { t.Fatal("the folder was given an entry without a warrant") } if strings.Contains(KeepWords("music"), "/") || strings.Contains(DiskWords("music"), "/") { t.Fatal("a wait's words name a path") } d := declare(t, a) done, said, err := TakeFolder(context.Background(), d, "lidarr.music", lib, "warrant-7", "the operator", noServices) if err != nil { t.Fatalf("the take act was refused: %v", err) } <-done if !strings.Contains(said, "warrant-7") { t.Errorf("the act does not say its warrant: %s", said) } for _, p := range []string{lib, filepath.Join(lib, "album"), song} { if got := effectiveOn(t, p, radarr); got&6 != 6 { t.Errorf("after the act the read-write identity may %s", permWords(got)) } } if has := effectiveOn(t, outside, radarr); has != 0 { t.Error("the act followed a link out of the folder") } rec, _ := keptRecord(lib) if !rec.Kept || rec.How != store.KeptByWarrant || rec.Warrant != "warrant-7" || rec.Act == nil || rec.Act.State != "done" || rec.Act.Links != 1 { t.Fatalf("the act is not recorded as done under its warrant, with the link passed by: %+v %+v", rec, rec.Act) } // Kept now: the next apply keeps it without asking, and plex dropping it has exactly plex's entries removed. if _, _, err := applyAccesses(t, store.State{}, accessJSON("lidarr.music", lib, "read-write", false, "mesh-lidarr:61001:read-write:container")); err != nil { t.Fatal(err) } beforeMask, _ := func() (uint16, bool) { a, _, _ := readACLs(t, song); return a.perm(aclMask, 0) }() ReconcileKept(context.Background()) for _, p := range []string{lib, song} { a, def, _ := readACLs(t, p) if _, still := a.perm(aclUser, plex); still { t.Errorf("a dropped user's entry stayed") } if _, still := def.perm(aclUser, plex); still && def != nil { t.Errorf("a dropped user's default entry stayed") } if _, kept := a.perm(aclUser, radarr); !kept { t.Errorf("the user that stayed lost its entry") } } if m, _ := func() (uint16, bool) { a, _, _ := readACLs(t, song); return a.perm(aclMask, 0) }(); m != beforeMask { t.Errorf("the mask moved on a drop: %s to %s", permWords(beforeMask), permWords(m)) } } // Row 8: the act enters no other filesystem — a folder whose device is not the kept folder's is passed by. func TestAWalkEntersNoOtherFilesystem(t *testing.T) { dir := aclFolder(t) mount := filepath.Join(dir, "elsewhere") if err := os.Mkdir(mount, 0o755); err != nil { t.Fatal(err) } inside := filepath.Join(mount, "file") if err := os.WriteFile(inside, nil, 0o644); err != nil { t.Fatal(err) } // A stand-in for a device the test cannot mount: the folder named elsewhere reads as another filesystem. stats := walkStats{ at: func(dirfd int, name string, st *unix.Stat_t) error { err := unix.Fstatat(dirfd, name, st, unix.AT_SYMLINK_NOFOLLOW) if name == "elsewhere" { st.Dev++ } return err }, fd: unix.Fstat, } w, err := walkKept(dir, ActLimits, stats, nil, keepVisit(map[uint32]bool{radarr: true})) if err != nil { t.Fatal(err) } if w.counts.Mounts != 1 { t.Errorf("the walk counted %d folders on another filesystem, and there is one", w.counts.Mounts) } if got := effectiveOn(t, inside, radarr); got != 0 { t.Error("the walk entered another filesystem") } } // Row 9: a missing folder under a mounted dataset is made as its parent; with the dataset not mounted (its empty // mount point on the root filesystem) nothing is made and the access waits. func TestAMissingFolderIsMadeWhereItsDataLivesAndWaitsWhereItsDiskIsNot(t *testing.T) { dir := aclFolder(t) dataset := filepath.Join(dir, "tank") if err := os.Mkdir(dataset, 0o750); err != nil { t.Fatal(err) } if err := os.Chmod(dataset, 0o750); err != nil { t.Fatal(err) } keeping(t, []string{dataset}, []string{dataset}) movies := filepath.Join(dataset, "media", "movies") report, _, err := applyAccesses(t, store.State{}, accessJSON("radarr.movies", movies, "read-write", false, "mesh-radarr:61001:read-write:container")) if err != nil { t.Fatalf("a missing folder under a mounted dataset was not made: %v", err) } info, err := os.Stat(movies) if err != nil { t.Fatalf("the folder was not made: %v", err) } if info.Mode().Perm()&0o707 != 0o700 { t.Errorf("the folder was made at %o, and its parent is 0750", info.Mode().Perm()) } if got := effectiveOn(t, movies, radarr); got != 7 { t.Errorf("the folder made is not kept: the identity may %s", permWords(got)) } if rec, _ := keptRecord(movies); rec.How != store.KeptMade || rec.Mount != dataset { t.Errorf("not recorded as made on its dataset: %+v", rec) } _ = report // The dataset not mounted: its mount point an empty folder on the filesystem below. MountPoints = func() []string { return []string{"/"} } books := filepath.Join(dataset, "books") report, _, err = applyAccesses(t, store.State{}, accessJSON("readarr.books", books, "read-write", false, "mesh-readarr:61001:read-write:container")) if err != nil { t.Fatalf("a missing disk failed the apply: %v", err) } if o := outcomeOf(report, "readarr.books"); o.Action != "waiting" || !strings.Contains(o.Detail, DiskWords("books")) { t.Fatalf("a folder whose disk is not there did not wait for it: %+v", o) } if _, err := os.Stat(books); !errors.Is(err, os.ErrNotExist) { t.Fatal("a folder was made where its disk is not") } if v := AccessVerdict(context.Background(), books, false, true); v.Kind != VerdictWaitDisk { t.Errorf("the look judges it %s, not waiting for its disk", v.Kind) } // And the folder made before, its dataset gone: its disk is not there, whatever is at its path now. if v := AccessVerdict(context.Background(), movies, false, true); v.Kind != VerdictWaitDisk { t.Errorf("a kept folder whose dataset is not mounted is judged %s", v.Kind) } } // Row 10: a narrowed entry or mask is reported and put back; successive looks cover a folder whole; a look over // its bound says how far it got. func TestTheSweepPutsBackWhatAnAppNarrowedAndCoversTheFolderOverLooks(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) lib := filepath.Join(dir, "shows") if err := os.Mkdir(lib, 0o755); err != nil { t.Fatal(err) } a := accessJSON("sonarr.shows", lib, "read-write", false, "mesh-sonarr:61001:read-write:container") if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } rec, _ := keptRecord(lib) for i := 0; i < 25; i++ { p := filepath.Join(lib, fmt.Sprintf("episode-%02d", i)) if err := os.WriteFile(p, nil, 0o666); err != nil { t.Fatal(err) } if err := os.Chmod(p, 0o644); err != nil { // an app's chmod narrows the mask t.Fatal(err) } } // The folder itself narrowed too: put back on the next apply, said by the look until then. if err := os.Chmod(lib, 0o755); err != nil { t.Fatal(err) } if short, _ := judgeTop(lib, rec.Want); len(short) == 0 { t.Fatal("a folder whose mask an app narrowed is not said short") } if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } if short, _ := judgeTop(lib, rec.Want); len(short) != 0 { t.Fatalf("the apply did not put the folder back: %v", short) } limits := WalkLimits{Entries: 10, Time: time.Minute} looks, repaired := 0, 0 for { looks++ look := LookKept(lib, rec.Want, limits) repaired += look.Repaired if look.Failed != "" { t.Fatal(look.Failed) } if !look.Sweep.Bound || look.Sweep.Cursor == nil { break } if len(look.Sweep.Cursor) == 0 || look.Sweep.Read == 0 { t.Fatalf("a look over its bound does not say how far it got: %+v", look.Sweep) } if looks > 10 { t.Fatal("the sweep never covered the folder") } } if looks < 3 { t.Errorf("26 entries at 10 a look were covered in %d looks", looks) } if repaired != 25 { t.Errorf("the sweep put back %d of 25 narrowed entries", repaired) } if c, _ := JudgeKept(lib, rec.Want, ActLimits); c.Narrowed != 0 { t.Errorf("after the looks %d entries still give the user less than its right", c.Narrowed) } if r, _ := keptRecord(lib); r.Sweep.Covered.IsZero() { t.Error("a sweep that covered the folder is not recorded as having done so") } } // Row 12: a root-owned folder on its placed filesystem, and an absent one under a root-owned parent on it, are // unhealthy, nothing is made, and no ask is raised. Read from the machine's own root-owned folders, never changed. func TestRootsDataOnItsPlacedFilesystemIsNeverKept(t *testing.T) { keeping(t, nil, nil) // A root-owned folder the placement guard lets an access name: below a protected root, never one. root := "" for _, c := range []string{"/srv/http", "/srv/ftp", "/var/empty", "/var/opt"} { if info, err := os.Lstat(c); err == nil && info.IsDir() { if uid, _, _ := statOwner(info); uid == 0 { root = c break } } } if root == "" { t.Skip("this machine has no root-owned folder below /srv or /var to read") } absent := filepath.Join(root, "mesh-test-never-made") for _, path := range []string{root, absent} { report, _, err := applyAccesses(t, store.State{}, accessJSON("m.data", path, "read-write", false, "mesh-m:61001:read-write:container")) if err == nil || !strings.Contains(err.Error(), "root's data is never kept") { t.Errorf("root's data at %s was not refused for what it is: %v", path, err) } if o := outcomeOf(report, "m.data"); o.Action == "waiting" { t.Errorf("root's data waits, and is never asked about: %+v", o) } if v := AccessVerdict(context.Background(), path, false, true); v.Kind != VerdictRoot { t.Errorf("the look judges root's data at %s %s", path, v.Kind) } } if _, err := os.Stat(absent); !errors.Is(err, os.ErrNotExist) { t.Fatal("a folder was made under root's data") } if _, has := keptRecord(root); has { t.Error("root's data was recorded as a kept folder") } } // The codec reads back what it writes, in the kernel's order. func TestAnACLReadsBackAsWritten(t *testing.T) { a := keptACL(minimalACL(0o640), map[uint32]uint16{radarr: 6, plex: 4}) b, err := decodeACL(a.encode()) if err != nil { t.Fatal(err) } if !sameACL(a, b) || b[0].Tag != aclUserObj || b[len(b)-1].Tag != aclOther { t.Fatalf("read back %+v", b) } if err := b.check(); err != nil { t.Fatal(err) } if _, err := decodeACL([]byte{1, 0, 0, 0}); err == nil { t.Error("a version 1 attribute was read") } if m, _ := b.perm(aclMask, 0); m != 6 { t.Errorf("mask %s, and the entries need rw- and the group had r--", permWords(m)) } } func forgetMountHistory() { mountHistory.Lock() mountHistory.seen = map[string]bool{} mountHistory.Unlock() } // Review H1: a walk is judged as an apply is, first. With the dataset not mounted its mount point is what is at the // path, and neither an add nor a drop walk touches it; the record stays for the walk that can reach the data. func TestNoWalkRunsWhereTheDiskIsNotThere(t *testing.T) { dir := aclFolder(t) dataset := filepath.Join(dir, "tank") if err := os.Mkdir(dataset, 0o755); err != nil { t.Fatal(err) } keeping(t, []string{dataset}, nil) lib := filepath.Join(dataset, "movies") both := []string{"mesh-radarr:61001:read-write:container", "mesh-plex:61002:read:container"} if _, _, err := applyAccesses(t, store.State{}, accessJSON("radarr.movies", lib, "read-write", false, both...)); err != nil { t.Fatal(err) } if rec, _ := keptRecord(lib); rec.Mount != dataset || !sameUsers(rec.Users, rec.Want) { t.Fatalf("not kept on its dataset: %+v", rec) } // plex drops it, and the dataset is unmounted before the walk. if _, _, err := applyAccesses(t, store.State{}, accessJSON("radarr.movies", lib, "read-write", false, both[0])); err != nil { t.Fatal(err) } MountPoints = func() []string { return []string{"/"} } ReconcileKept(context.Background()) rec, had := keptRecord(lib) if !had || len(rec.Users) != 2 { t.Fatalf("a drop that could not reach the data forgot whose entries are on it: %+v", rec) } if rec.Act == nil || rec.Act.State != "waiting" { t.Errorf("the walk not run is not said: %+v", rec.Act) } if _, still := func() (uint16, bool) { a, _, _ := readACLs(t, lib); return a.perm(aclUser, plex) }(); !still { t.Fatal("a walk ran where the disk is not there") } // The disk back: the drop runs. MountPoints = func() []string { return []string{"/", dataset} } ReconcileKept(context.Background()) if _, still := func() (uint16, bool) { a, _, _ := readACLs(t, lib); return a.perm(aclUser, plex) }(); still { t.Fatal("the drop did not run once the disk was there") } } // Review H2: a folder kept while it was empty on the filesystem below a mount nobody named is judged afresh when // the disk mounts over it: holding data, it waits for the operator's word, and nothing is added to it. func TestADiskMountedOverAFolderKeptEmptyIsNotKeptWithoutAWarrant(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) pool := filepath.Join(dir, "pool") lib := filepath.Join(pool, "media") if err := os.MkdirAll(lib, 0o755); err != nil { t.Fatal(err) } a := accessJSON("radarr.media", lib, "read-write", false, "mesh-radarr:61001:read-write:container") if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } if rec, _ := keptRecord(lib); !rec.Kept || rec.How != store.KeptEmpty || rec.Mount != "/" { t.Fatalf("not kept as empty on /: %+v", rec) } // The pool is imported: a library is at the path now, on its own filesystem. film := filepath.Join(lib, "film") if err := os.WriteFile(film, []byte("data"), 0o644); err != nil { t.Fatal(err) } // The new disk's data carries no entries of the mesh's: the test's stand-in made it under the folder kept // below, so what it inherited there is taken off. if err := unix.Removexattr(film, aclAccessXattr); err != nil { t.Fatal(err) } MountPoints = func() []string { return []string{"/", pool} } if v := AccessVerdict(context.Background(), lib, false, true); v.Kind != VerdictWaitKeep { t.Fatalf("the look judges it %s, and the data on the new disk was never taken", v.Kind) } report, _, err := applyAccesses(t, store.State{}, a) if err != nil { t.Fatal(err) } if o := outcomeOf(report, "radarr.media"); o.Action != "waiting" { t.Fatalf("a folder whose disk mounted over it was kept without a warrant: %+v", o) } if rec, _ := keptRecord(lib); rec.Kept || rec.Mount != pool { t.Errorf("the record still says kept, or not on the new disk: %+v", rec) } if got := effectiveOn(t, film, radarr); got != 0 { t.Error("an entry was added to the data on the new disk") } } // Review M3: widening a mask widens nobody else — a file made at 0644 under a default ACL carries the owning group's // rwx under a mask of r--, and after the mesh widens the mask the owning group still reads only. func TestWideningTheMaskGivesTheOwningGroupNothingMore(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) lib := filepath.Join(dir, "shows") if err := os.Mkdir(lib, 0o775); err != nil { t.Fatal(err) } if err := os.Chmod(lib, 0o775); err != nil { t.Fatal(err) } a := accessJSON("sonarr.shows", lib, "read-write", false, "mesh-sonarr:61001:read-write:container") if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } ep := filepath.Join(lib, "episode") f, err := os.OpenFile(ep, os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { t.Fatal(err) } f.Close() groupRight := func() uint16 { acl, _, _ := readACLs(t, ep) g, _ := acl.perm(aclGroupObj, 0) m, _ := acl.perm(aclMask, 0) return g & m } before := groupRight() rec, _ := keptRecord(lib) LookKept(lib, rec.Want, SweepLimits) if got := effectiveOn(t, ep, radarr); got&6 != 6 { t.Fatalf("the sweep did not widen the mask: %s", permWords(got)) } if after := groupRight(); after != before { t.Fatalf("the owning group may %s after the mask widened, and %s before", permWords(after), permWords(before)) } } // watchedFolder is a kept folder with a watch running on it, its users, and the end of the watch's run. func watchedFolder(t *testing.T) (string, []store.KeptUser, *Watcher, context.CancelFunc, <-chan struct{}) { t.Helper() dir := aclFolder(t) keeping(t, nil, nil) lib := filepath.Join(dir, "music") if err := os.Mkdir(lib, 0o755); err != nil { t.Fatal(err) } if _, _, err := applyAccesses(t, store.State{}, accessJSON("lidarr.music", lib, "read-write", false, "mesh-lidarr:61001:read-write:container")); err != nil { t.Fatal(err) } rec, _ := keptRecord(lib) w, err := NewWatcher() if err != nil { t.Skipf("no inotify here: %v", err) } ctx, cancel := context.WithCancel(context.Background()) ended := make(chan struct{}) go func() { w.Run(ctx); close(ended) }() // The watch ends before the test's record of kept folders is put back. t.Cleanup(func() { cancel(); <-ended }) w.Set(map[string][]store.KeptUser{lib: rec.Want}) return lib, rec.Want, w, cancel, ended } // Review L4: a watch whose folder goes is let go (IN_IGNORED). func TestTheWatchOfAFolderRemovedIsLetGo(t *testing.T) { lib, _, w, _, _ := watchedFolder(t) album := filepath.Join(lib, "album") if err := os.Mkdir(album, 0o755); err != nil { t.Fatal(err) } waitFor(t, "the folder made is watched", func() bool { return w.watched() == 2 }) if err := os.Remove(album); err != nil { t.Fatal(err) } waitFor(t, "the watch of a folder removed is let go", func() bool { return w.watched() == 1 }) } // Review L4: notice lost re-arms every watch, and the record says the watch is whole again. func TestNoticeLostReArmsTheWatch(t *testing.T) { lib, _, w, _, _ := watchedFolder(t) w.lost() if w.watched() != 1 { t.Fatalf("notice lost did not re-arm the watch: %d watched", w.watched()) } if r, _ := keptRecord(lib); r.OutOfWatches { t.Error("a re-armed watch is still said out of watches") } late := filepath.Join(lib, "late") f, _ := os.OpenFile(late, os.O_CREATE|os.O_WRONLY, 0o644) f.Close() waitFor(t, "the re-armed watch repairs", func() bool { return effectiveOn(t, late, radarr)&6 == 6 }) } // Review L4: users given while notices are heard are read under the lock (run with -race). func TestUsersGivenWhileNoticesAreHeardAreReadUnderTheLock(t *testing.T) { lib, users, w, _, _ := watchedFolder(t) stop := make(chan struct{}) done := make(chan struct{}) go func() { defer close(done) for { select { case <-stop: return default: w.Set(map[string][]store.KeptUser{lib: users}) } } }() for i := 0; i < 50; i++ { _ = os.WriteFile(filepath.Join(lib, fmt.Sprintf("f%d", i)), nil, 0o644) } waitFor(t, "the last file is repaired", func() bool { return effectiveOn(t, filepath.Join(lib, "f49"), radarr)&6 == 6 }) close(stop) <-done } // Review L4: the watch ends with its context, however quiet the machine. func TestTheWatchEndsWithItsContext(t *testing.T) { _, _, _, cancel, ended := watchedFolder(t) cancel() select { case <-ended: case <-time.After(2 * time.Second): t.Fatal("the watch did not end with its context") } } // Review L6: a broken folder's words carry no path. func TestABrokenFoldersWordsCarryNoPath(t *testing.T) { dir := t.TempDir() keeping(t, nil, nil) file := filepath.Join(dir, "notafolder") if err := os.WriteFile(file, nil, 0o644); err != nil { t.Fatal(err) } for _, path := range []string{file, filepath.Join(file, "below")} { v := AccessVerdict(context.Background(), path, false, true) if v.Kind != VerdictBroken || strings.Contains(v.Why, "/") { t.Errorf("%s: %+v", path, v) } } } // Review H2: a systemd .mount unit says where a disk is meant to be, as fstab does; and a mount point seen mounted // once is remembered, so a folder below it is a missing disk on first sight when it is gone. func TestMountUnitsAndMountsSeenSayWhereADiskIsMeantToBe(t *testing.T) { units := t.TempDir() unit := "[Unit]\nDescription=pool\n\n[Mount]\nWhat=/dev/disk/by-label/pool\nWhere=/srv/pool\nType=ext4\n" if err := os.WriteFile(filepath.Join(units, "srv-pool.mount"), []byte(unit), 0o644); err != nil { t.Fatal(err) } if got := mountUnitTargets([]string{units}); len(got) != 1 || got[0] != "/srv/pool" { t.Fatalf("read %v from a .mount unit", got) } dir := t.TempDir() disk := filepath.Join(dir, "disk") if err := os.Mkdir(disk, 0o755); err != nil { t.Fatal(err) } keeping(t, []string{disk}, nil) _ = placedOn(context.Background(), disk, disk, "", nil) // seen mounted once MountPoints = func() []string { return []string{"/"} } if pl := placedOn(context.Background(), filepath.Join(disk, "films"), disk, "", nil); !pl.Away { t.Fatal("a folder below a mount point seen mounted and gone is not a missing disk") } } // keptOnDataset is a folder kept on a dataset the test says is mounted, with the users given, and the dataset. func keptOnDataset(t *testing.T, users ...string) (dataset, lib string) { t.Helper() dir := aclFolder(t) dataset = filepath.Join(dir, "tank") if err := os.Mkdir(dataset, 0o755); err != nil { t.Fatal(err) } keeping(t, []string{dataset}, nil) lib = filepath.Join(dataset, "movies") if _, _, err := applyAccesses(t, store.State{}, accessJSON("radarr.movies", lib, "read-write", false, users...)); err != nil { t.Fatal(err) } return dataset, lib } // Review M1: a container that mounts an access whose disk is not there is not created, though its empty mount // point is at the path: its writes would land on the filesystem below (§9). func TestAContainerWaitsWhileItsAccessesDiskIsNotThere(t *testing.T) { _, lib := keptOnDataset(t, "mesh-radarr:61001:read-write:container") MountPoints = func() []string { return []string{"/"} } var ran []string run := func(_ context.Context, name string, args ...string) (string, error) { ran = append(ran, name+" "+strings.Join(args, " ")) if len(args) > 0 && args[0] == "container" { return "", errors.New("no such container") } return "", nil } d := declare(t, accessJSON("radarr.movies", lib, "read-write", false, "mesh-radarr:61001:read-write:container")+ `,{"id":"radarr.app","type":"container","name":"radarr","image":"`+pinned+`","volumes":["`+lib+`:/movies"]}`) report, _, _ := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, run, nil, nil) if o := outcomeOf(report, "radarr.app"); o.Action != "waiting" { t.Fatalf("a container mounting a folder whose disk is not there was not held: %+v", o) } for _, r := range ran { if strings.Contains(r, " run ") || strings.HasSuffix(strings.Fields(r)[0], "docker") && strings.Contains(r, "run") { t.Fatalf("the container was started: %s", r) } } if !WaitingOnAccess(d)["radarr.app"] { t.Error("liveness is not told the container waits on its access") } } // Review L2: a folder no access names any more, whose disk is away, keeps its entries' record and is said until // the drop walk can run; then it is forgotten. func TestEntriesThatCouldNotBeDroppedAreSaidUntilTheyAre(t *testing.T) { dataset, lib := keptOnDataset(t, "mesh-radarr:61001:read-write:container") other := `{"id":"x.dir","type":"directory","path":"` + filepath.Join(filepath.Dir(dataset), "x") + `"}` if _, _, err := applyAccesses(t, store.State{}, other); err != nil { t.Fatal(err) } MountPoints = func() []string { return []string{"/"} } ReconcileKept(context.Background()) left := Leftovers() if len(left) != 1 || left[0].Access != "radarr.movies" || !strings.Contains(left[0].Why, "could not be removed") || strings.Contains(left[0].Why, dataset) { t.Fatalf("entries that could not be dropped are not said, path-free: %+v", left) } MountPoints = func() []string { return []string{"/", dataset} } ReconcileKept(context.Background()) if left := Leftovers(); len(left) != 0 { t.Fatalf("said after the drop ran: %+v", left) } if _, had := keptRecord(lib); had { t.Error("a folder nobody uses was not forgotten once its entries were out") } if _, still := func() (uint16, bool) { a, _, _ := readACLs(t, lib); return a.perm(aclUser, radarr) }(); still { t.Error("the entries were not taken out") } } // Review L3: a disk mounted over a folder kept below it leaves the mesh's entries there recorded and said; once that // filesystem holds the folder again, they are taken out. func TestEntriesLeftOnAFilesystemNoLongerAtTheFolderAreDroppedWhenItIsBack(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) pool := filepath.Join(dir, "pool") lib := filepath.Join(pool, "media") if err := os.MkdirAll(lib, 0o755); err != nil { t.Fatal(err) } a := accessJSON("radarr.media", lib, "read-write", false, "mesh-radarr:61001:read-write:container") if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } film := filepath.Join(lib, "film") if err := os.WriteFile(film, []byte("data"), 0o644); err != nil { t.Fatal(err) } _ = unix.Removexattr(film, aclAccessXattr) MountPoints = func() []string { return []string{"/", pool} } if _, _, err := applyAccesses(t, store.State{}, a); err != nil { t.Fatal(err) } if left := Leftovers(); len(left) != 1 || !strings.Contains(left[0].Why, "no longer at its folder") { t.Fatalf("entries left on the filesystem below are not said: %+v", left) } // The pool goes: the filesystem below holds the folder again, and the entries left there are taken out. MountPoints = func() []string { return []string{"/"} } ReconcileKept(context.Background()) if _, still := func() (uint16, bool) { a, _, _ := readACLs(t, lib); return a.perm(aclUser, radarr) }(); still { t.Fatal("the entries left on the filesystem that came back were not taken out") } if left := Leftovers(); len(left) != 0 { t.Fatalf("still said after they were taken out: %+v", left) } } // Review L4: the controller's word that a folder was taken holds for the filesystem the warrant was carried out on; // moved to another, it is not taken, the wait names the new filesystem, and nothing is walked. func TestAMovedFolderIsNotTakenOnTheOldWarrant(t *testing.T) { dir := aclFolder(t) keeping(t, nil, nil) pool := filepath.Join(dir, "pool") lib := filepath.Join(pool, "media") if err := os.MkdirAll(filepath.Join(lib, "old"), 0o755); err != nil { t.Fatal(err) } notTaken := accessJSON("radarr.media", lib, "read-write", false, "mesh-radarr:61001:read-write:container") if _, _, err := applyAccesses(t, store.State{}, notTaken); err != nil { t.Fatal(err) } done, _, err := TakeFolder(context.Background(), declare(t, notTaken), "radarr.media", lib, "w-1", "op", noServices) if err != nil { t.Fatal(err) } <-done before := AccessVerdict(context.Background(), lib, true, true) film := filepath.Join(lib, "film") if err := os.WriteFile(film, []byte("data"), 0o644); err != nil { t.Fatal(err) } _ = unix.Removexattr(film, aclAccessXattr) MountPoints = func() []string { return []string{"/", pool} } taken := accessJSON("radarr.media", lib, "read-write", true, "mesh-radarr:61001:read-write:container") report, _, err := applyAccesses(t, store.State{}, taken) if err != nil { t.Fatal(err) } if o := outcomeOf(report, "radarr.media"); o.Action != "waiting" { t.Fatalf("a folder moved to another filesystem was kept on the old warrant: %+v", o) } // And on the next apply too, its record now on the new filesystem. report, _, _ = applyAccesses(t, store.State{}, taken) if o := outcomeOf(report, "radarr.media"); o.Action != "waiting" { t.Fatalf("the second apply kept it on the old warrant: %+v", o) } v := AccessVerdict(context.Background(), lib, true, true) if v.Kind != VerdictWaitKeep || v.Placed == "" || v.Placed == before.Placed || strings.Contains(v.Placed, "/") { t.Fatalf("the look does not ask again naming the new filesystem: %+v (before %+v)", v, before) } ReconcileKept(context.Background()) if got := effectiveOn(t, film, radarr); got != 0 { t.Fatal("the data on the new filesystem was walked") } } // Review L5: a read that hangs holds one goroutine per path, whoever asks meanwhile. func TestAHungReadIsSharedByEveryoneWhoAsks(t *testing.T) { was := statBound statBound = 20 * time.Millisecond defer func() { statBound = was }() release := make(chan struct{}) defer close(release) var started int32 var mu sync.Mutex hang := func(string) (os.FileInfo, error) { mu.Lock() started++ mu.Unlock() <-release return nil, nil } for i := 0; i < 5; i++ { if _, err := within("test", hang, "/a/dead/mount"); !errors.Is(err, errNoAnswer) { t.Fatalf("a hung read answered: %v", err) } } mu.Lock() defer mu.Unlock() if started != 1 { t.Fatalf("%d reads of one path were started while the first hung", started) } }