carlos / platform Public

Inventory on retained descriptors (plan Task 2, first slice): the fleet's pins (the pinned slash and a platform root per spelled directory, the parent of the first platform link's directory or of the spelled directory), the platform-link predicate on descriptors, strictTreeDBs walking Dirs with every link class named (may lead to a database, a link to a link, neither a file nor a directory, out of the tree by Within, cannot be looked at, a file in the tree skipped with its target, absent as churn), the pathname link look deleted; the owners tests keep their strings, three expectations move to the design's classes, and a link-class test covers each class and Astra 2's layout

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ccfd331adcda9cd67152069bf3251a7db1770256 parent a26cba8
7 files changed, +516 −231
  • internal/ops/authoritybridge.go +246 −0
  • internal/ops/linktarget_other.go +0 −26
  • internal/ops/linktarget_unix.go +0 −59
  • internal/ops/linktarget_unix_test.go +0 −50
  • internal/ops/litestream_owners.go +119 −78
  • internal/ops/litestream_owners_test.go +54 −18
  • internal/ops/litestream_owners_unix_test.go +97 −0

CI passed — run details

diff --git a/internal/ops/authoritybridge.go b/internal/ops/authoritybridge.go
new file mode 100644
index 000000000..f60078ed1
--- /dev/null
+++ b/internal/ops/authoritybridge.go
@@ -0,0 +1,246 @@
+package ops
+
+import (
+ "errors"
+ "path/filepath"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/authority"
+)
+
+// slashPath is where the pinned "/" is: "/" on a box. A test points it at
+// a directory whose ancestry it owns, since the platform walk requires
+// every component from the pinned slash down to be the platform's and a
+// developer's temp directory is not.
+var slashPath = "/"
+
+// authFacts is the seam the owners tests use to give entries owners by
+// name (withOwners), the same way factsOf is for pathname lookups; the
+// production value changes nothing.
+var authFacts = func(name string, f authority.Facts) authority.Facts { return f }
+
+// platformFacts is the platform rule's uid half for the authority package.
+func platformFacts(f authority.Facts) bool { return platformUID(f.UID) }
+
+// fleetPins is the pinned "/" and the platform roots a fleet's inventory
+// walks from, built once per inventory and closed with it. A root is the
+// parent of the first platform link's directory in a spelled path, or the
+// parent of the spelled directory when it has none (the plan's rule), so
+// systemd's "../private/..." pops from the link's directory to the root
+// and no further.
+type fleetPins struct {
+ slash *authority.Dir
+ pins *authority.Pins
+ roots map[string]*authority.Root
+}
+
+func newFleetPins() (*fleetPins, error) {
+ slash, err := authority.Open(slashPath)
+ if err != nil {
+ return nil, err
+ }
+ p := &fleetPins{slash: slash, roots: map[string]*authority.Root{}}
+ p.pins = &authority.Pins{Slash: slash, Platform: platformFacts}
+ return p, nil
+}
+
+func (p *fleetPins) Close() {
+ for _, r := range p.roots {
+ r.Close()
+ }
+ p.slash.Close()
+}
+
+// comps splits a spelled absolute path into its components beneath the
+// pinned slash; a path not beneath it is not something the fleet can look at.
+func (p *fleetPins) comps(path string) ([]string, bool) {
+ path = filepath.Clean(path)
+ base := filepath.Clean(slashPath)
+ if base != "/" {
+ if path != base && !strings.HasPrefix(path, base+"/") {
+ return nil, false
+ }
+ path = strings.TrimPrefix(path, base)
+ }
+ var out []string
+ for _, c := range strings.Split(path, "/") {
+ if c != "" {
+ out = append(out, c)
+ }
+ }
+ return out, true
+}
+
+// root is the pinned root at the joined components, built once; no
+// components means the pinned slash itself, pinned deliberately.
+func (p *fleetPins) root(comps []string) (*authority.Root, string) {
+ key := strings.Join(comps, "/")
+ if r, ok := p.roots[key]; ok {
+ return r, ""
+ }
+ var r *authority.Root
+ var err error
+ if len(comps) == 0 {
+ r, err = authority.SlashRoot(p.slash, platformFacts)
+ } else {
+ r, err = authority.OpenRoot(p.slash, key, platformFacts)
+ }
+ if err != nil {
+ var ee *authority.EdgeError
+ switch {
+ case errors.As(err, &ee) && errors.Is(ee.Err, authority.ErrNotPlatform):
+ return nil, "a directory in its path is not the platform's (it belongs to a user other than root or the platform's, or it can be written by its group or by anyone): " + ee.Name
+ case errors.As(err, &ee) && errors.Is(ee.Err, authority.ErrLink):
+ return nil, "a directory in its path is a link above where a link may be: " + ee.Name
+ default:
+ return nil, "a directory in its path cannot be looked at"
+ }
+ }
+ // the same rule again by name, through the owners seam the tests use
+ // (authFacts is the identity on a box): every component of the root's
+ // chain, slash excluded, is the platform's and not group or other
+ // writable
+ for d := r.Dir; d != nil && d != p.slash; d = d.Parent() {
+ if f := authFacts(d.Name(), d.Facts()); !platformUID(f.UID) || f.Mode&0o022 != 0 {
+ r.Close()
+ return nil, "a directory in its path is not the platform's (it belongs to a user other than root or the platform's, or it can be written by its group or by anyone): " + d.Name()
+ }
+ }
+ p.roots[key] = r
+ p.pins.Roots = append(p.pins.Roots, r)
+ return r, ""
+}
+
+// walk is Walk with the pinned slash's spelling applied: on a box the
+// slash is "/" and an absolute target is walked as spelled; under a test's
+// pinned slash an absolute target beneath it is walked from there.
+func (p *fleetPins) walk(d *authority.Dir, dest string) (authority.Result, error) {
+ if base := filepath.Clean(slashPath); base != "/" && filepath.IsAbs(dest) {
+ if dest == base {
+ dest = "/"
+ } else if strings.HasPrefix(dest, base+"/") {
+ dest = strings.TrimPrefix(dest, base)
+ }
+ }
+ return d.Walk(dest, p.pins)
+}
+
+// tenantRoot acquires the directory a path spells, through the platform
+// walk to its root and then the platform-link predicate on descriptors
+// (exactly resolvePlatformLinks's rule: a link is followed when the link
+// and its directory are the platform's, its target holds no further link
+// and no ".." past its leading run, and the target's parent is the
+// platform's). The result is confined: a relative walk from it or beneath
+// it cannot leave it. The caller releases what was acquired with the
+// returned func; the root stays with the pins. The reason, when the
+// directory cannot be reached, is a string the inventory renders.
+func (p *fleetPins) tenantRoot(dir string) (*authority.Dir, func(), string) {
+ comps, ok := p.comps(dir)
+ if !ok {
+ return nil, nil, "its directory is not beneath the pinned root"
+ }
+ var owned []interface{ Close() error }
+ var results []*authority.Result
+ release := func() {
+ for i := len(results) - 1; i >= 0; i-- {
+ results[i].Close()
+ }
+ for i := len(owned) - 1; i >= 0; i-- {
+ owned[i].Close()
+ }
+ }
+ // the first link in the spelling decides the root: the parent of the
+ // link's directory, or the parent of the spelled directory
+ linkAt := -1
+ {
+ cur := p.slash
+ var probe []*authority.Dir
+ for i, c := range comps {
+ child, err := cur.Child(c, nil)
+ if err != nil {
+ var ee *authority.EdgeError
+ if errors.As(err, &ee) && errors.Is(ee.Err, authority.ErrLink) {
+ linkAt = i
+ }
+ break
+ }
+ probe = append(probe, child)
+ cur = child
+ }
+ for i := len(probe) - 1; i >= 0; i-- {
+ probe[i].Close()
+ }
+ }
+ rootN := len(comps) - 1
+ if linkAt >= 0 {
+ rootN = linkAt - 1
+ }
+ if rootN < 0 {
+ rootN = 0
+ }
+ r, why := p.root(comps[:rootN])
+ if why != "" {
+ return nil, nil, why
+ }
+ cur := r.Dir
+ for _, c := range comps[rootN:] {
+ child, err := cur.Child(c, nil)
+ if err == nil {
+ owned = append(owned, child)
+ cur = child
+ continue
+ }
+ var ee *authority.EdgeError
+ if !errors.As(err, &ee) || !errors.Is(ee.Err, authority.ErrLink) {
+ release()
+ return nil, nil, "a directory in its path cannot be looked at"
+ }
+ // a link: the platform's, in the platform's directory, to a
+ // directory beneath the platform's, through no further link
+ if df := authFacts(cur.Name(), cur.Facts()); !platformUID(df.UID) || df.Mode&0o022 != 0 {
+ release()
+ return nil, nil, "a link in its path is in a directory that is not the platform's"
+ }
+ _, lf, lerr := cur.Lookat(c)
+ if lerr != nil {
+ release()
+ return nil, nil, "a link in its path cannot be looked at"
+ }
+ if !platformUID(authFacts(c, lf).UID) {
+ release()
+ return nil, nil, "a link in its path is not the platform's"
+ }
+ target, rerr := cur.Readlink(c)
+ if rerr != nil {
+ release()
+ return nil, nil, "a link in its path cannot be read"
+ }
+ res, werr := p.walk(cur, target)
+ switch {
+ case errors.Is(werr, authority.ErrAscentAfterName):
+ release()
+ return nil, nil, "a link in its path leads through another link: " + target + " steps up after a named component"
+ case werr != nil:
+ release()
+ return nil, nil, "a link in its path leads through another link: " + target + " cannot be looked at"
+ case res.Kind == authority.Link:
+ res.Close()
+ release()
+ return nil, nil, "a link in its path leads through another link: " + target + " is a link"
+ case res.Kind != authority.Directory:
+ res.Close()
+ release()
+ return nil, nil, "a link in its path does not lead to a directory"
+ }
+ results = append(results, &res)
+ if parent := res.Dir.Parent(); parent != nil {
+ if pf := authFacts(parent.Name(), parent.Facts()); !platformUID(pf.UID) || pf.Mode&0o022 != 0 {
+ release()
+ return nil, nil, "a link in its path leads to a directory that is not under the platform's"
+ }
+ }
+ cur = res.Dir
+ }
+ cur.Confine()
+ return cur, release, ""
+}
diff --git a/internal/ops/linktarget_other.go b/internal/ops/linktarget_other.go
deleted file mode 100644
index f6cdced30..000000000
--- a/internal/ops/linktarget_other.go
+++ /dev/null
@@ -1,26 +0,0 @@
-//go:build !unix
-
-package ops
-
-import (
- "os"
- "path/filepath"
-)
-
-type targetKind struct {
- dir, regular bool
-}
-
-// statTarget on a platform without fstatat: the replicator does not run
-// here (the header read refuses too); the concatenated spelling is looked
-// at for completeness.
-func statTarget(dir, dest string) (targetKind, error) {
- if !filepath.IsAbs(dest) {
- dest = dir + string(filepath.Separator) + dest
- }
- fi, err := os.Stat(dest)
- if err != nil {
- return targetKind{}, err
- }
- return targetKind{dir: fi.IsDir(), regular: fi.Mode().IsRegular()}, nil
-}
diff --git a/internal/ops/linktarget_unix.go b/internal/ops/linktarget_unix.go
deleted file mode 100644
index f3bcc5aa1..000000000
--- a/internal/ops/linktarget_unix.go
+++ /dev/null
@@ -1,59 +0,0 @@
-//go:build unix
-
-package ops
-
-import (
- "os"
- "path/filepath"
-
- "golang.org/x/sys/unix"
-)
-
-// targetKind is what a link's target is, by one look at it: a directory, a
-// regular file, something else, or nothing (err is os.ErrNotExist then).
-type targetKind struct {
- dir, regular bool
-}
-
-// statTarget looks at a link's target as the link spells it, in this
-// process's mount namespace: an absolute target as it is; a relative one
-// relative to the directory the link is in, through a descriptor opened by
-// that directory's path at this moment (not the descriptor the walk listed
-// from: the directory is resolved again from its whole path, and a link in
-// any component of it is followed, as the tree's own root is reached
-// through one; a directory, or a link to one, put in its place between the
-// listing and this open is looked at instead, which can omit or misread
-// this one link for this one look, and so can change whether a build is
-// refused for it; no database is streamed or attributed through the link
-// either way: stated open as EE2), with
-// fstatat, so the kernel resolves every ".." physically, from wherever the
-// component before it led, and the argument handed to it is the target
-// alone, so the link's directory adds nothing to the length the kernel
-// sees (the target itself is bounded by what a link can hold). The open
-// asks for a directory and nothing else: a FIFO, a device or a file a
-// tenant swaps into the directory's place between the listing and this
-// look is refused by the kernel before any driver's open (ENOTDIR), and
-// nothing here can block on it or become its controlling terminal. The
-// link is never read again.
-func statTarget(dir, dest string) (targetKind, error) {
- if filepath.IsAbs(dest) {
- fi, err := os.Stat(dest)
- if err != nil {
- return targetKind{}, err
- }
- return targetKind{dir: fi.IsDir(), regular: fi.Mode().IsRegular()}, nil
- }
- fd, err := unix.Open(dir, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NONBLOCK|unix.O_NOCTTY|unix.O_CLOEXEC, 0)
- if err != nil {
- return targetKind{}, &os.PathError{Op: "open", Path: dir, Err: err}
- }
- defer unix.Close(fd)
- var st unix.Stat_t
- if err := unix.Fstatat(fd, dest, &st, 0); err != nil {
- if err == unix.ENOENT {
- return targetKind{}, os.ErrNotExist
- }
- return targetKind{}, &os.PathError{Op: "stat", Path: dest, Err: err}
- }
- return targetKind{dir: st.Mode&unix.S_IFMT == unix.S_IFDIR, regular: st.Mode&unix.S_IFMT == unix.S_IFREG}, nil
-}
diff --git a/internal/ops/linktarget_unix_test.go b/internal/ops/linktarget_unix_test.go
deleted file mode 100644
index 2af42969c..000000000
--- a/internal/ops/linktarget_unix_test.go
+++ /dev/null
@@ -1,50 +0,0 @@
-//go:build unix
-
-package ops
-
-import (
- "errors"
- "os"
- "path/filepath"
- "testing"
- "time"
-
- "golang.org/x/sys/unix"
-)
-
-// The directory a relative target is looked at from is opened as a
-// directory and nothing else: a FIFO a tenant swaps into its place between
-// the listing and the look is refused at once (ENOTDIR), where a plain
-// open would wait for a writer that never comes and hang the watch and
-// Close behind it. (Review seventeen, Opus P2-1.)
-func TestStatTargetRefusesADirectorySwappedForAFIFO(t *testing.T) {
- root := platformDir(t)
- fifo := filepath.Join(root, "cache")
- if err := unix.Mkfifo(fifo, 0o666); err != nil {
- t.Fatal(err)
- }
- done := make(chan error, 1)
- go func() {
- _, err := statTarget(fifo, "live.sqlite")
- done <- err
- }()
- select {
- case err := <-done:
- // Refused by the open itself (O_DIRECTORY), not by the look through
- // a descriptor the open handed back: a device in the directory's
- // place would otherwise have had its driver's open run.
- var pe *os.PathError
- if !errors.Is(err, unix.ENOTDIR) || !errors.As(err, &pe) || pe.Op != "open" {
- t.Fatalf("a FIFO where the directory was: %v; want the open refused with ENOTDIR", err)
- }
- case <-time.After(5 * time.Second):
- t.Fatal("the look at a FIFO did not return: the open blocked")
- }
- // A directory still answers, by the target's own spelling.
- dir := filepath.Join(root, "u")
- writeFile(t, filepath.Join(dir, "live.sqlite"))
- k, err := statTarget(dir, "./live.sqlite")
- if err != nil || !k.regular || k.dir {
- t.Fatalf("a file through its directory: %+v %v", k, err)
- }
-}
diff --git a/internal/ops/litestream_owners.go b/internal/ops/litestream_owners.go
index 22a161ef5..399c57234 100644
--- a/internal/ops/litestream_owners.go
+++ b/internal/ops/litestream_owners.go
@@ -3,6 +3,7 @@ package ops
import (
"errors"
"fmt"
+ "github.com/carlosframework/platform/internal/authority"
"io"
"io/fs"
"os"
@@ -648,18 +649,27 @@ const strictTreeMaxDepth = 32
// header; an empty file is passed over, it holds nothing);
// - a directory that cannot be read, and one deeper than
// strictTreeMaxDepth, is a problem;
-// - a link is never walked into or opened; it is classified by where it
-// leads (one Stat). One that is named like a database, or that leads
-// to a directory, is a problem: the tenant's own software would follow
-// it, so there may be a database behind it. One whose target cannot
-// be looked at (a permission refused, a dead mount) is a problem too.
-// One that leads to a regular file under any other name is reported
-// in skipped, with its target, so a database the tenant reaches
-// through a link of its own (current -> live.sqlite) is named to the
-// caller, never passed over, and a link re-pointed later reads as a
-// change to the running fleet and to the Accept list at the next
-// build alike (the target is quoted, and a link entry is compared
-// as written). The links tenant software makes in numbers (a venv's, a
+// - a link is never walked into or opened; it is classified by where its
+// captured spelling leads, walked on retained descriptors from the
+// directory that listed it (internal/authority: a relative target
+// cannot leave the tenant's root, an absolute one goes from the
+// pinned slash into a pinned root). One that is named like a
+// database, or that leads to a directory in the tree, is a problem:
+// the tenant's own software would follow it, so there may be a
+// database behind it. One whose target is itself a link is a problem
+// (a link to a link that may lead to a database: it is refused, not
+// followed). One that leads out of the tree (above the root, to
+// another root, through a component that is not the platform's) is a
+// problem (a link out of the tree). One that leads to something that
+// is neither a file nor a directory is a problem. One whose target
+// cannot be looked at (a permission refused, a dead mount) is a
+// problem too. One that leads to a regular file in the tree under any
+// other name is reported in skipped, with its target, so a database
+// the tenant reaches through a link of its own (current ->
+// live.sqlite) is named to the caller, never passed over, and a link
+// re-pointed later reads as a change to the running fleet and to the
+// Accept list at the next build alike (the target is quoted, and a
+// link entry is compared as written). The links tenant software makes in numbers (a venv's, a
// node_modules' .bin) are all named this way: whether one tenant's
// unaccepted links should refuse the whole box's next build, as they
// do today through Accept, or that tenant's databases alone, is to be
@@ -674,98 +684,129 @@ const strictTreeMaxDepth = 32
// behind it); and FIFOs, sockets and devices, and links to them (not
// files; a database cannot be one).
func strictTreeDBs(root string) (dbs, skipped, problems []string) {
- var walk func(dir string, depth int, hidden bool)
- walk = func(dir string, depth int, hidden bool) {
- entries, err := os.ReadDir(dir)
+ pins, err := newFleetPins()
+ if err != nil {
+ return nil, nil, []string{root + ": cannot be read"}
+ }
+ defer pins.Close()
+ top, release, why := pins.tenantRoot(root)
+ if why != "" {
+ return nil, nil, []string{root + ": cannot be read (" + why + ")"}
+ }
+ defer release()
+ var walk func(d *authority.Dir, dir string, depth int, hidden bool)
+ walk = func(d *authority.Dir, dir string, depth int, hidden bool) {
+ entries, err := d.List()
if err != nil {
- if depth > 0 && errors.Is(err, fs.ErrNotExist) {
- return
- }
problems = append(problems, dir+": cannot be read")
return
}
for _, e := range entries {
- name := e.Name()
+ name := e.Name
p := filepath.Join(dir, name)
- fi, err := os.Lstat(p)
- if errors.Is(err, fs.ErrNotExist) {
- continue
- }
- if err != nil {
- problems = append(problems, p+": cannot be looked at")
- continue
- }
- switch {
- case fi.Mode()&os.ModeSymlink != 0:
- // Classified by where its target's spelling leads (one stat
- // of that spelling, as the kernel resolves it in this
- // process's namespace; the link is never walked into or
- // opened).
- // A link entry is named with its path and target, each
- // quoted behind a word no path begins with, so a link
- // re-pointed after it was accepted reads as a change and
- // no name, of a link or of a file, reads as another entry.
- dest, rerr := os.Readlink(p)
- if errors.Is(rerr, fs.ErrNotExist) || errors.Is(rerr, syscall.EINVAL) {
- continue // gone, or no longer a link, under the look: churn
- }
+ switch e.Kind {
+ case authority.Link:
+ // Classified by where its captured spelling leads, walked
+ // from the directory that listed it on retained descriptors
+ // (relative from this directory and never above the tenant's
+ // root; absolute from the pinned slash into a pinned root);
+ // the link is never walked into or opened, and every class
+ // is named: nothing disappears through an unnamed case.
+ dest, rerr := d.Readlink(name)
if rerr != nil {
+ var ee *authority.EdgeError
+ if errors.As(rerr, &ee) && (errors.Is(ee.Err, syscall.ENOENT) || errors.Is(ee.Err, syscall.EINVAL)) {
+ continue // gone, or no longer a link, under the look: churn
+ }
problems = append(problems, p+": a link that cannot be read")
continue
}
- // The entry names the target as read, and the target is
- // classified by that very spelling (from the link's own
- // directory when relative, through a descriptor opened by
- // that directory's path, as a directory and nothing else:
- // statTarget), never by reading the link again: what the
- // entry says and what was looked at are one string, whatever
- // the link is re-pointed to meanwhile, and the kernel
- // resolves a ".." in it physically, in this process's mount
- // namespace (an isolated instance's own namespace, with its
- // private /tmp and read-only bind mounts, may resolve the
- // same spelling elsewhere or nowhere; stated as open).
- target, terr := statTarget(dir, dest)
- // A problem names the path first (nothing compares a
- // problem with an Accept list); a skipped link is the
- // injective entry.
+ res, werr := pins.walk(d, dest)
+ quoted := p + " -> " + strconv.Quote(dest)
switch {
- case sqliteName(name) || (terr == nil && target.dir):
- problems = append(problems, p+" -> "+strconv.Quote(dest)+": a link that may lead to a database")
- case terr != nil && !errors.Is(terr, fs.ErrNotExist):
- problems = append(problems, p+" -> "+strconv.Quote(dest)+": a link whose target cannot be looked at")
- case terr == nil && target.regular:
+ case sqliteName(name):
+ problems = append(problems, quoted+": a link that may lead to a database")
+ case errors.Is(werr, authority.ErrAscentPastRoot), errors.Is(werr, authority.ErrOutside), errors.Is(werr, authority.ErrNotPlatform):
+ problems = append(problems, quoted+": a link out of the tree")
+ case werr != nil:
+ problems = append(problems, quoted+": a link whose target cannot be looked at")
+ case res.Kind == authority.Absent:
+ // nothing behind it: churn
+ case res.Kind == authority.Link:
+ problems = append(problems, quoted+": a link to a link that may lead to a database")
+ case res.Kind == authority.Other:
+ problems = append(problems, quoted+": a link to something that is neither a file nor a directory")
+ case !res.Within(top):
+ problems = append(problems, quoted+": a link out of the tree")
+ case res.Kind == authority.Directory:
+ problems = append(problems, quoted+": a link that may lead to a database")
+ default: // a regular file within the tree: named, never passed over
skipped = append(skipped, linkEntry(p, dest))
}
- case fi.IsDir():
+ res.Close()
+ case authority.Directory:
if depth+1 > strictTreeMaxDepth {
problems = append(problems, p+": too deep to walk")
continue
}
- walk(p, depth+1, hidden || strings.HasPrefix(name, ".") || depth+1 > treeDBWalkDepth)
- case !fi.Mode().IsRegular() || fi.Size() == 0:
- case strings.HasSuffix(name, ".db") && !hidden && !strings.HasPrefix(name, "."):
- dbs = append(dbs, p)
- case sqliteName(name):
- skipped = append(skipped, p)
- default:
- // Any other regular file is read for SQLite's header: a
- // database under some other name is a database, a sidecar
- // name (-wal, -shm, -journal) included, since a real
- // sidecar never begins with the database header. One that
- // cannot be read is a place a database could be.
- switch isDB, rerr := sqliteHeader(p, fi); {
- case rerr != nil:
+ child, cerr := d.Child(name, &e.Facts)
+ if cerr != nil {
+ var ee *authority.EdgeError
+ if errors.As(cerr, &ee) && (errors.Is(ee.Err, authority.ErrChanged) || errors.Is(ee.Err, authority.ErrLink) || errors.Is(ee.Err, syscall.ENOENT) || errors.Is(ee.Err, syscall.ENOTDIR)) {
+ continue // replaced under the look: churn, the next look sees what is there
+ }
problems = append(problems, p+": cannot be read")
- case isDB:
+ continue
+ }
+ walk(child, p, depth+1, hidden || strings.HasPrefix(name, ".") || depth+1 > treeDBWalkDepth)
+ child.Close()
+ case authority.Regular:
+ switch {
+ case e.Facts.Size == 0:
+ case strings.HasSuffix(name, ".db") && !hidden && !strings.HasPrefix(name, "."):
+ dbs = append(dbs, p)
+ case sqliteName(name):
skipped = append(skipped, p)
+ default:
+ // Any other regular file is read for SQLite's header: a
+ // database under some other name is a database, a sidecar
+ // name (-wal, -shm, -journal) included, since a real
+ // sidecar never begins with the database header. One that
+ // cannot be read is a place a database could be.
+ switch isDB, rerr := sqliteHeaderAt(d, name, e.Facts); {
+ case rerr != nil:
+ problems = append(problems, p+": cannot be read")
+ case isDB:
+ skipped = append(skipped, p)
+ }
}
}
}
}
- walk(root, 0, false)
+ walk(top, filepath.Clean(root), 0, false)
return dbs, skipped, problems
}
+// sqliteHeaderAt is sqliteHeader through a retained directory: the file is
+// opened by name relative to it, without following a link and without
+// blocking, and must be the regular file the listing looked at.
+func sqliteHeaderAt(d *authority.Dir, name string, looked authority.Facts) (bool, error) {
+ f, err := d.OpenRegular(name, looked)
+ if err != nil {
+ return false, err
+ }
+ defer f.Close()
+ var head [16]byte
+ n, err := io.ReadFull(f, head[:])
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ return n == len(head) && string(head[:]) == "SQLite format 3\x00", nil
+}
+
// sqliteName reports whether a file name is one SQLite databases go by,
// in any case.
func sqliteName(name string) bool {
diff --git a/internal/ops/litestream_owners_test.go b/internal/ops/litestream_owners_test.go
index 86d5451c6..499a5cd39 100644
--- a/internal/ops/litestream_owners_test.go
+++ b/internal/ops/litestream_owners_test.go
@@ -3,6 +3,7 @@ package ops
import (
"os"
"path/filepath"
+ "strconv"
"strings"
"testing"
@@ -26,9 +27,24 @@ func platformDir(t *testing.T) string {
if err := os.Chmod(dir, 0o755); err != nil {
t.Fatal(err)
}
+ // the pinned slash for this test is the directory the test's temp
+ // directories share (made 0700 by the testing package), since the
+ // platform walk requires every component beneath the slash to be the
+ // platform's and a developer's temp root is group-writable
+ pinSlash(t, filepath.Dir(dir))
return dir
}
+func pinSlash(t *testing.T, dir string) {
+ t.Helper()
+ if slashPath == dir {
+ return
+ }
+ prev := slashPath
+ slashPath = dir
+ t.Cleanup(func() { slashPath = prev })
+}
+
func writeFile(t *testing.T, p string) string {
t.Helper()
mkdirs(t, filepath.Dir(p))
@@ -377,8 +393,12 @@ func TestFleetDBOwnersCheckTheFileAndItsSiblings(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if _, ok := ownedByPath(inv)[f.db]; ok || !named(inv.Unowned, f.db, tc.why) {
- t.Fatalf("owned %+v, unowned %v; want the database unowned, saying %q", inv.Owned, inv.Unowned, tc.why)
+ // unowned with the reason, or, when the tree's own root fails
+ // the platform walk (the directory above the tenant's), a
+ // problem on the tree with the reason: either way the database
+ // is not owned and the reason is said
+ if _, ok := ownedByPath(inv)[f.db]; ok || !(named(inv.Unowned, f.db, tc.why) || named(inv.Problems, f.state, tc.why)) {
+ t.Fatalf("owned %+v, unowned %v, problems %v; want the database unowned, saying %q", inv.Owned, inv.Unowned, inv.Problems, tc.why)
}
})
}
@@ -655,13 +675,20 @@ func TestExplicitClaimsOutsideTheGlobsAreInventoried(t *testing.T) {
}
}
-// A link in a unit tree that leads to a regular file under a name that is
-// not a database's (current -> live.sqlite) is named to the caller as
-// skipped, never walked into or opened and never passed over. (Review
-// ten, Astra 8.)
+// A link in a unit tree that leads to a regular file in the tree under a
+// name that is not a database's (current -> live.sqlite) is named to the
+// caller as skipped, never walked into or opened and never passed over
+// (review ten, Astra 8). One that leads to a regular file outside the
+// tree is a problem, a link out of the tree, since the pinned-roots design:
+// the walk on retained descriptors does not follow a target out of the
+// tenant's root (design review twenty, Astra 5; the design's Part A).
func TestStrictTreeNamesALinkToAFile(t *testing.T) {
root := platformDir(t)
- target := writeFile(t, filepath.Join(platformDir(t), "elsewhere", "live.sqlite"))
+ outside := writeFile(t, filepath.Join(platformDir(t), "elsewhere", "live.sqlite"))
+ target := writeFile(t, filepath.Join(root, "store", "live.sqlite"))
+ if err := os.Symlink(outside, filepath.Join(root, "away")); err != nil {
+ t.Fatal(err)
+ }
if err := os.Symlink(target, filepath.Join(root, "current")); err != nil {
t.Fatal(err)
}
@@ -669,8 +696,11 @@ func TestStrictTreeNamesALinkToAFile(t *testing.T) {
t.Fatal(err)
}
dbs, skipped, problems := strictTreeDBs(root)
- if len(dbs) != 0 || len(problems) != 0 || !contains(skipped, linkEntry(filepath.Join(root, "current"), target)) || containsPrefix(skipped, filepath.Join(root, "dangling")) {
- t.Fatalf("dbs %v skipped %v problems %v; want the link to a file skipped with its target and the dangling one ignored", dbs, skipped, problems)
+ if len(dbs) != 0 || !contains(skipped, linkEntry(filepath.Join(root, "current"), target)) || containsPrefix(skipped, filepath.Join(root, "dangling")) {
+ t.Fatalf("dbs %v skipped %v problems %v; want the link to a file in the tree skipped with its target and the dangling one ignored", dbs, skipped, problems)
+ }
+ if len(problems) != 1 || !named(problems, filepath.Join(root, "away"), "a link out of the tree") || containsPrefix(skipped, filepath.Join(root, "away")) {
+ t.Fatalf("a link to a file outside the tree: skipped %v problems %v; want it a problem, out of the tree, and not skipped", skipped, problems)
}
// Re-pointed, the entry reads differently, to the running fleet's
// render and to the Accept list at the next build alike
@@ -678,7 +708,7 @@ func TestStrictTreeNamesALinkToAFile(t *testing.T) {
if err := os.Remove(filepath.Join(root, "current")); err != nil {
t.Fatal(err)
}
- other := writeFile(t, filepath.Join(platformDir(t), "elsewhere", "other.sqlite"))
+ other := writeFile(t, filepath.Join(root, "store", "other.sqlite"))
if err := os.Symlink(other, filepath.Join(root, "current")); err != nil {
t.Fatal(err)
}
@@ -686,8 +716,9 @@ func TestStrictTreeNamesALinkToAFile(t *testing.T) {
if !contains(skipped, linkEntry(filepath.Join(root, "current"), other)) || contains(skipped, linkEntry(filepath.Join(root, "current"), target)) {
t.Fatalf("re-pointed: skipped %v; want the new target named", skipped)
}
- // A target with a ".." is quoted, not folded: the entry names exactly
- // the target as written.
+ // A target with a ".." after a named component is refused by the walk
+ // (the component before it could be made a link later), and named as
+ // written, quoted, as a problem: the entry is never folded.
if err := os.Remove(filepath.Join(root, "current")); err != nil {
t.Fatal(err)
}
@@ -697,27 +728,32 @@ func TestStrictTreeNamesALinkToAFile(t *testing.T) {
if err := os.Symlink(odd, filepath.Join(root, "current")); err != nil {
t.Fatal(err)
}
- _, skipped, _ = strictTreeDBs(root)
- if !contains(skipped, linkEntry(filepath.Join(root, "current"), odd)) || contains(skipped, linkEntry(filepath.Join(root, "current"), other)) {
- t.Fatalf("a target spelt through ..: skipped %v; want it named as written", skipped)
+ _, skipped, problems = strictTreeDBs(root)
+ if !named(problems, filepath.Join(root, "current")+" -> "+strconv.Quote(odd), "cannot be looked at") || contains(skipped, linkEntry(filepath.Join(root, "current"), other)) || containsPrefix(skipped, filepath.Join(root, "current")) {
+ t.Fatalf("a target spelt through ..: skipped %v problems %v; want it refused and named as written", skipped, problems)
}
// A target holding a line break is quoted too, and the file behind it
// is named as the one file it is.
- broken := writeFile(t, filepath.Join(platformDir(t), "elsewhere", "a\nb.sqlite"))
+ broken := writeFile(t, filepath.Join(root, "store", "a\nb.sqlite"))
if err := os.Symlink(broken, filepath.Join(root, "newline")); err != nil {
t.Fatal(err)
}
_, skipped, _ = strictTreeDBs(root)
- if !contains(skipped, linkEntry(filepath.Join(root, "newline"), broken)) || strings.Contains(strings.Join(skipped, "|"), "\n") {
+ if !contains(skipped, linkEntry(filepath.Join(root, "newline"), broken)) {
t.Fatalf("a target with a line break: skipped %q; want it quoted", skipped)
}
+ for _, e := range skipped {
+ if isLinkEntry(e) && strings.Contains(e, "\n") {
+ t.Fatalf("a link entry with a raw line break: %q", e)
+ }
+ }
// A relative target with a ".." after a component that is itself a
// link to a directory outside the tree: the kernel steps up from where
// that link led, so the spelling is resolved as written, from the
// link's directory (fstatat on unix), never cleaned (a cleaned join
// would look beside the link and find nothing). (Review fifteen, Opus
// P3-1.)
- outside := platformDir(t)
+ outside = platformDir(t)
mkdirs(t, filepath.Join(outside, "o"), filepath.Join(outside, "hidden", "sub"), filepath.Join(outside, "hidden", "dbs"))
if err := os.Symlink(filepath.Join(outside, "hidden", "sub"), filepath.Join(outside, "o", "lnk")); err != nil {
t.Fatal(err)
diff --git a/internal/ops/litestream_owners_unix_test.go b/internal/ops/litestream_owners_unix_test.go
index 6603dd04f..2389ae6a7 100644
--- a/internal/ops/litestream_owners_unix_test.go
+++ b/internal/ops/litestream_owners_unix_test.go
@@ -5,11 +5,13 @@ package ops
import (
"os"
"path/filepath"
+ "strconv"
"strings"
"syscall"
"testing"
"time"
+ "github.com/carlosframework/platform/internal/authority"
"github.com/carlosframework/platform/internal/registry"
)
@@ -32,6 +34,18 @@ func withOwners(t *testing.T, uids map[string]uint32) {
return f, ok
}
t.Cleanup(func() { factsOf = real })
+ realAuth := authFacts
+ authFacts = func(name string, f authority.Facts) authority.Facts {
+ key := name
+ if f.Mode&syscall.S_IFMT == syscall.S_IFLNK {
+ key = "link:" + key
+ }
+ if uid, set := uids[key]; set {
+ f.UID = uid
+ }
+ return f
+ }
+ t.Cleanup(func() { authFacts = realAuth })
}
// A unit tenant with a user of its own: its file's chain must be its own
@@ -559,3 +573,86 @@ func TestATenantsFileInAPlatformDirectoryIsRefusedByBothTenantClaims(t *testing.
t.Fatalf("the platform's own file: %q", why)
}
}
+
+// Every link class the pinned-roots design names is rendered as written
+// and none is silent: a directory target and a database-named link (may
+// lead to a database), a regular file in the tree (skipped, with its
+// target), a regular file under another pinned root and an ascent past the
+// tenant's root and an absolute target outside every pin (out of the
+// tree), a link to a link, a FIFO target (neither a file nor a directory),
+// a target through a ".." after a name (cannot be looked at), and an
+// absent target (churn).
+func TestStrictTreeNamesEveryLinkClass(t *testing.T) {
+ plat := platformDir(t)
+ root := filepath.Join(plat, "tenant")
+ other := filepath.Join(plat, "other")
+ mkdirs(t, filepath.Join(root, "d"), other, filepath.Join(root, "x"))
+ inTree := writeFile(t, filepath.Join(root, "store", "live.sqlite"))
+ theirs := writeFile(t, filepath.Join(other, "theirs.sqlite"))
+ elsewhere := writeFile(t, filepath.Join(platformDir(t), "elsewhere", "e.sqlite"))
+ if err := syscall.Mkfifo(filepath.Join(root, "fifo"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ links := map[string]string{
+ "to-dir": "d",
+ "named.db": "store/live.sqlite",
+ "current": "store/live.sqlite",
+ "sibling": theirs,
+ "ascent": "../other/theirs.sqlite",
+ "far": elsewhere,
+ "to-link": "current",
+ "to-fifo": "fifo",
+ "dotdot": "x/../store/live.sqlite",
+ "dangling": "nothing",
+ "abs-in-tree": inTree,
+ }
+ for name, target := range links {
+ if err := os.Symlink(target, filepath.Join(root, name)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ dbs, skipped, problems := strictTreeDBs(root)
+ if len(dbs) != 0 {
+ t.Fatalf("dbs %v", dbs)
+ }
+ at := func(name string) string { return filepath.Join(root, name) + " -> " + strconv.Quote(links[name]) }
+ for name, why := range map[string]string{
+ "to-dir": "a link that may lead to a database",
+ "named.db": "a link that may lead to a database",
+ "sibling": "a link out of the tree",
+ "ascent": "a link out of the tree",
+ "far": "a link out of the tree",
+ "to-link": "a link to a link that may lead to a database",
+ "to-fifo": "a link to something that is neither a file nor a directory",
+ "dotdot": "a link whose target cannot be looked at",
+ } {
+ if !named(problems, at(name), why) {
+ t.Errorf("%s: want %q among problems %v", name, why, problems)
+ }
+ }
+ for _, name := range []string{"current", "abs-in-tree"} {
+ if !contains(skipped, linkEntry(filepath.Join(root, name), links[name])) {
+ t.Errorf("%s: want the link to a file in the tree skipped with its target: %v", name, skipped)
+ }
+ }
+ if containsPrefix(problems, filepath.Join(root, "dangling")) || containsPrefix(skipped, filepath.Join(root, "dangling")) {
+ t.Errorf("a dangling link is churn, not named: skipped %v problems %v", skipped, problems)
+ }
+ if len(problems) != 8 {
+ t.Errorf("%d problems, want 8: %v", len(problems), problems)
+ }
+ // the files themselves, named as today
+ for _, p := range []string{inTree} {
+ if !contains(skipped, p) {
+ t.Errorf("%s not in skipped %v", p, skipped)
+ }
+ }
+ // Astra 2's layout: a tenant-owned component above the platform's
+ // refuses the walk at the root, no tree is built over it
+ withOwners(t, map[string]uint32{"owned": 44001})
+ mkdirs(t, filepath.Join(plat, "owned", "sub", "t"))
+ _, _, problems = strictTreeDBs(filepath.Join(plat, "owned", "sub", "t"))
+ if len(problems) != 1 || !named(problems, filepath.Join(plat, "owned", "sub", "t"), "not the platform's") {
+ t.Fatalf("a tenant-owned component above the tree: %v", problems)
+ }
+}