authority: retained directory descriptors (plan step 1, Part A): Dir with Child, List, Lookat, Readlink, OpenRegular, Recheck and Walk; Root by the platform walk from a pinned slash with its ancestry; the design's Part A tests; a link under O_DIRECTORY named as one; the walk's grammar refused before any system call
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5 files changed,
+1195
−0
internal/authority/authority.go+144 −0internal/authority/authority_other.go+42 −0internal/authority/authority_unix.go+504 −0internal/authority/authority_unix_test.go+485 −0internal/authority/doc.go+20 −0
CI passed — run details
diff --git a/internal/authority/authority.go b/internal/authority/authority.go| new file mode 100644 |
| index 000000000..9ecc9fa99 |
| --- /dev/null |
| +++ b/internal/authority/authority.go |
| @@ -0,0 +1,144 @@ |
| +package authority |
| + |
| +import ( |
| + "errors" |
| + "fmt" |
| + "strings" |
| +) |
| + |
| +// Facts are what fstat says of an entry: enough to recognise it again. |
| +type Facts struct { |
| + Dev, Ino uint64 |
| + UID uint32 |
| + Mode uint32 // the file type and permission bits as the kernel has them |
| +} |
| + |
| +// Same reports whether two facts name the same object of the same type. |
| +func (f Facts) Same(o Facts) bool { |
| + return f.Dev == o.Dev && f.Ino == o.Ino && f.Mode&modeTypeMask == o.Mode&modeTypeMask |
| +} |
| + |
| +// Kind is what one look at a name found. |
| +type Kind int |
| + |
| +const ( |
| + Absent Kind = iota |
| + Directory |
| + Regular |
| + Link |
| + Other // a FIFO, a socket, a device |
| +) |
| + |
| +func (k Kind) String() string { |
| + return [...]string{"absent", "directory", "regular", "link", "other"}[k] |
| +} |
| + |
| +// Entry is one listed name with its facts. |
| +type Entry struct { |
| + Name string |
| + Kind Kind |
| + Facts Facts |
| +} |
| + |
| +// The refusals. Each is returned before any system call where the input |
| +// alone decides it. |
| +var ( |
| + ErrBadName = errors.New("authority: not a single name") |
| + ErrEmptySpelling = errors.New("authority: an empty spelling") |
| + ErrAscentPastRoot = errors.New("authority: \"..\" past the root") |
| + ErrAscentAfterName = errors.New("authority: \"..\" after a named component") |
| + ErrNotDirectory = errors.New("authority: not a directory") |
| + ErrLink = errors.New("authority: a link where a directory is required") |
| + ErrNotRegular = errors.New("authority: not the regular file that was looked at") |
| + ErrOutside = errors.New("authority: outside every pinned root") |
| + ErrNotPlatform = errors.New("authority: a component is not the platform's") |
| + ErrChanged = errors.New("authority: the entry changed") |
| + ErrClosed = errors.New("authority: closed") |
| + ErrUnsupported = errors.New("authority: not supported on this platform") |
| +) |
| + |
| +// EdgeError names the edge (parent name, child name) a refusal is about. |
| +type EdgeError struct { |
| + Name string |
| + Err error |
| +} |
| + |
| +func (e *EdgeError) Error() string { return fmt.Sprintf("%s: %v", e.Name, e.Err) } |
| +func (e *EdgeError) Unwrap() error { return e.Err } |
| + |
| +// validName accepts one component: not empty, not "." or "..", no |
| +// separator, no NUL. |
| +func validName(name string) error { |
| + if name == "" || name == "." || name == ".." || strings.ContainsAny(name, "/\x00") { |
| + return ErrBadName |
| + } |
| + return nil |
| +} |
| + |
| +// platformOK is the platform walk's rule for one component: owned by a |
| +// platform uid and not writable by group or others. |
| +func platformOK(f Facts, platform func(uint32) bool) bool { |
| + return platform != nil && platform(f.UID) && f.Mode&0o022 == 0 |
| +} |
| + |
| +// Pins are what an absolute spelling is walked against: the pinned "/" |
| +// and the platform roots a walk may enter. Platform decides which uids |
| +// are the platform's. |
| +type Pins struct { |
| + Slash *Dir |
| + Roots []*Root |
| + Platform func(uid uint32) bool |
| +} |
| + |
| +func (p *Pins) match(f Facts) *Root { |
| + if p == nil { |
| + return nil |
| + } |
| + for _, r := range p.Roots { |
| + if r != nil && r.Dir != nil && r.facts.Same(f) { |
| + return r |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +// Result is what a Walk found: the kind of the last component, its facts, |
| +// and, for a directory, a retained Dir for it. Close releases what the |
| +// walk opened and nothing it was given. |
| +type Result struct { |
| + Kind Kind |
| + Facts Facts |
| + Dir *Dir |
| + owned []*Dir |
| +} |
| + |
| +// Close closes the directories the walk acquired, deepest first. |
| +func (r *Result) Close() { |
| + for i := len(r.owned) - 1; i >= 0; i-- { |
| + r.owned[i].Close() |
| + } |
| + r.owned = nil |
| +} |
| + |
| +// normalise splits a spelling into components, dropping empty and "." |
| +// components, and reports whether the spelling demands a directory (a |
| +// trailing separator or a terminal "."). |
| +func normalise(spelling string) (comps []string, abs, wantDir bool, err error) { |
| + if spelling == "" { |
| + return nil, false, false, ErrEmptySpelling |
| + } |
| + if strings.ContainsRune(spelling, 0) { |
| + return nil, false, false, ErrBadName |
| + } |
| + abs = spelling[0] == '/' |
| + parts := strings.Split(spelling, "/") |
| + last := parts[len(parts)-1] |
| + wantDir = last == "" || last == "." |
| + for _, p := range parts { |
| + if p == "" || p == "." { |
| + continue |
| + } |
| + comps = append(comps, p) |
| + } |
| + return comps, abs, wantDir, nil |
| +} |
diff --git a/internal/authority/authority_other.go b/internal/authority/authority_other.go| new file mode 100644 |
| index 000000000..9721d022d |
| --- /dev/null |
| +++ b/internal/authority/authority_other.go |
| @@ -0,0 +1,42 @@ |
| +//go:build !unix |
| + |
| +package authority |
| + |
| +import "os" |
| + |
| +const modeTypeMask = 0o170000 |
| + |
| +// Dir on a platform without openat: the replicator does not run here; |
| +// every operation is ErrUnsupported. |
| +type Dir struct { |
| + facts Facts |
| + name string |
| + parent *Dir |
| + root bool |
| +} |
| + |
| +func Open(string) (*Dir, error) { return nil, ErrUnsupported } |
| +func (d *Dir) Facts() Facts { return d.facts } |
| +func (d *Dir) Name() string { return d.name } |
| +func (d *Dir) Parent() *Dir { return d.parent } |
| +func (d *Dir) Depth() int { return 0 } |
| +func (d *Dir) Close() error { return nil } |
| +func (d *Dir) Child(string, *Facts) (*Dir, error) { return nil, ErrUnsupported } |
| +func (d *Dir) List() ([]Entry, error) { return nil, ErrUnsupported } |
| +func (d *Dir) Lookat(string) (Kind, Facts, error) { return Absent, Facts{}, ErrUnsupported } |
| +func (d *Dir) Readlink(string) (string, error) { return "", ErrUnsupported } |
| +func (d *Dir) OpenRegular(string, Facts) (*os.File, error) { return nil, ErrUnsupported } |
| +func (d *Dir) Recheck() error { return ErrUnsupported } |
| +func (d *Dir) Walk(string, *Pins) (Result, error) { return Result{}, ErrUnsupported } |
| + |
| +// Root on a platform without openat. |
| +type Root struct { |
| + *Dir |
| + Path string |
| + facts Facts |
| +} |
| + |
| +func OpenRoot(*Dir, string, func(uint32) bool) (*Root, error) { return nil, ErrUnsupported } |
| +func (r *Root) Ancestry() []Facts { return nil } |
| +func (r *Root) AncestryChanged() (bool, error) { return true, ErrUnsupported } |
| +func (r *Root) Close() error { return nil } |
diff --git a/internal/authority/authority_unix.go b/internal/authority/authority_unix.go| new file mode 100644 |
| index 000000000..69cb85e7a |
| --- /dev/null |
| +++ b/internal/authority/authority_unix.go |
| @@ -0,0 +1,504 @@ |
| +//go:build unix |
| + |
| +package authority |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "sort" |
| + "strings" |
| + |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +const modeTypeMask = unix.S_IFMT |
| + |
| +const dirFlags = unix.O_DIRECTORY | unix.O_NOFOLLOW | unix.O_RDONLY | unix.O_CLOEXEC | unix.O_NOCTTY |
| + |
| +// Dir is a retained directory descriptor. |
| +type Dir struct { |
| + fd int |
| + name string // the name it was acquired by from parent; "" for a start |
| + parent *Dir |
| + facts Facts |
| + root bool // a Root: the top of a stack a Walk may not pop past |
| + closed bool |
| +} |
| + |
| +func factsOf(st *unix.Stat_t) Facts { |
| + return Facts{Dev: uint64(st.Dev), Ino: uint64(st.Ino), UID: st.Uid, Mode: uint32(st.Mode)} |
| +} |
| + |
| +func kindOf(mode uint32) Kind { |
| + switch mode & unix.S_IFMT { |
| + case unix.S_IFDIR: |
| + return Directory |
| + case unix.S_IFREG: |
| + return Regular |
| + case unix.S_IFLNK: |
| + return Link |
| + } |
| + return Other |
| +} |
| + |
| +// Open opens a directory by pathname, following whatever the path |
| +// contains. It claims no authority: it is for the pinned "/" and for tests. |
| +func Open(path string) (*Dir, error) { |
| + fd, err := unix.Open(path, unix.O_DIRECTORY|unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOCTTY, 0) |
| + if err != nil { |
| + return nil, &os.PathError{Op: "open", Path: path, Err: err} |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Fstat(fd, &st); err != nil { |
| + unix.Close(fd) |
| + return nil, &os.PathError{Op: "fstat", Path: path, Err: err} |
| + } |
| + return &Dir{fd: fd, facts: factsOf(&st)}, nil |
| +} |
| + |
| +// Facts returns the facts read when the Dir was acquired. |
| +func (d *Dir) Facts() Facts { return d.facts } |
| + |
| +// Name is the name the Dir was acquired by from its parent. |
| +func (d *Dir) Name() string { return d.name } |
| + |
| +// Parent is the Dir this one was acquired from, or nil. |
| +func (d *Dir) Parent() *Dir { return d.parent } |
| + |
| +// Depth is the number of Dirs above this one on its stack. |
| +func (d *Dir) Depth() int { |
| + n := 0 |
| + for p := d.parent; p != nil; p = p.parent { |
| + n++ |
| + } |
| + return n |
| +} |
| + |
| +// Close closes this descriptor only; the stack above it stays. |
| +func (d *Dir) Close() error { |
| + if d == nil || d.closed { |
| + return nil |
| + } |
| + d.closed = true |
| + return unix.Close(d.fd) |
| +} |
| + |
| +func (d *Dir) live() error { |
| + if d == nil || d.closed { |
| + return ErrClosed |
| + } |
| + return nil |
| +} |
| + |
| +// Child acquires one named subdirectory: openat with O_DIRECTORY and |
| +// O_NOFOLLOW (the kernel refuses a link, a FIFO or a device at lookup, |
| +// before any open effect), then fstat; when expect is given, the opened |
| +// directory must be the one the facts describe. |
| +func (d *Dir) Child(name string, expect *Facts) (*Dir, error) { |
| + if err := d.live(); err != nil { |
| + return nil, err |
| + } |
| + if err := validName(name); err != nil { |
| + return nil, err |
| + } |
| + fd, err := unix.Openat(d.fd, name, dirFlags, 0) |
| + if err != nil { |
| + // Linux answers ENOTDIR, not ELOOP, for a link under O_DIRECTORY |
| + // with O_NOFOLLOW; one look names it, so a link is never mistaken |
| + // for a file. The link is not followed either way. |
| + if err == unix.ENOTDIR { |
| + if k, _, lerr := d.Lookat(name); lerr == nil && k == Link { |
| + return nil, &EdgeError{Name: name, Err: ErrLink} |
| + } |
| + } |
| + return nil, &EdgeError{Name: name, Err: err} |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Fstat(fd, &st); err != nil { |
| + unix.Close(fd) |
| + return nil, &EdgeError{Name: name, Err: err} |
| + } |
| + f := factsOf(&st) |
| + if expect != nil && !expect.Same(f) { |
| + unix.Close(fd) |
| + return nil, &EdgeError{Name: name, Err: ErrChanged} |
| + } |
| + return &Dir{fd: fd, name: name, parent: d, facts: f}, nil |
| +} |
| + |
| +// List reads the directory through a fresh open file description (never a |
| +// dup, whose shared offset would make a second listing start where the |
| +// first ended), checked to be this directory, and looks at each entry |
| +// with fstatat without following links. Entries are sorted by name. |
| +func (d *Dir) List() ([]Entry, error) { |
| + if err := d.live(); err != nil { |
| + return nil, err |
| + } |
| + fd, err := unix.Openat(d.fd, ".", unix.O_DIRECTORY|unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOCTTY, 0) |
| + if err != nil { |
| + return nil, &EdgeError{Name: ".", Err: err} |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Fstat(fd, &st); err != nil { |
| + unix.Close(fd) |
| + return nil, &EdgeError{Name: ".", Err: err} |
| + } |
| + if !factsOf(&st).Same(d.facts) { |
| + unix.Close(fd) |
| + return nil, &EdgeError{Name: ".", Err: ErrChanged} |
| + } |
| + f := os.NewFile(uintptr(fd), ".") |
| + names, err := f.Readdirnames(-1) |
| + f.Close() |
| + if err != nil { |
| + return nil, &EdgeError{Name: ".", Err: err} |
| + } |
| + sort.Strings(names) |
| + out := make([]Entry, 0, len(names)) |
| + for _, n := range names { |
| + if n == "." || n == ".." { |
| + continue |
| + } |
| + k, fs, err := d.Lookat(n) |
| + if err != nil { |
| + return nil, err |
| + } |
| + if k == Absent { |
| + continue // gone between the listing and the look: churn |
| + } |
| + out = append(out, Entry{Name: n, Kind: k, Facts: fs}) |
| + } |
| + return out, nil |
| +} |
| + |
| +// Lookat is fstatat of one name without following a link: its kind and |
| +// facts, or Absent with no error when there is nothing by that name. |
| +func (d *Dir) Lookat(name string) (Kind, Facts, error) { |
| + if err := d.live(); err != nil { |
| + return Absent, Facts{}, err |
| + } |
| + if err := validName(name); err != nil { |
| + return Absent, Facts{}, err |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Fstatat(d.fd, name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| + if err == unix.ENOENT { |
| + return Absent, Facts{}, nil |
| + } |
| + return Absent, Facts{}, &EdgeError{Name: name, Err: err} |
| + } |
| + f := factsOf(&st) |
| + return kindOf(f.Mode), f, nil |
| +} |
| + |
| +// Readlink reads one named link relative to the descriptor. |
| +func (d *Dir) Readlink(name string) (string, error) { |
| + if err := d.live(); err != nil { |
| + return "", err |
| + } |
| + if err := validName(name); err != nil { |
| + return "", err |
| + } |
| + buf := make([]byte, 4096) |
| + for { |
| + n, err := unix.Readlinkat(d.fd, name, buf) |
| + if err != nil { |
| + return "", &EdgeError{Name: name, Err: err} |
| + } |
| + if n < len(buf) { |
| + return string(buf[:n]), nil |
| + } |
| + buf = make([]byte, len(buf)*2) |
| + } |
| +} |
| + |
| +// OpenRegular opens one named file for reading without following a link, |
| +// without blocking on a special file and without acquiring a terminal, |
| +// and requires it to be the regular file that was looked at. |
| +func (d *Dir) OpenRegular(name string, looked Facts) (*os.File, error) { |
| + if err := d.live(); err != nil { |
| + return nil, err |
| + } |
| + if err := validName(name); err != nil { |
| + return nil, err |
| + } |
| + fd, err := unix.Openat(d.fd, name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_NOCTTY|unix.O_CLOEXEC, 0) |
| + if err != nil { |
| + return nil, &EdgeError{Name: name, Err: err} |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Fstat(fd, &st); err != nil { |
| + unix.Close(fd) |
| + return nil, &EdgeError{Name: name, Err: err} |
| + } |
| + f := factsOf(&st) |
| + if kindOf(f.Mode) != Regular || f.Dev != looked.Dev || f.Ino != looked.Ino { |
| + unix.Close(fd) |
| + return nil, &EdgeError{Name: name, Err: ErrNotRegular} |
| + } |
| + return os.NewFile(uintptr(fd), name), nil |
| +} |
| + |
| +// Recheck looks at every edge of the stack from this Dir up to its top: |
| +// each child's name in its parent must still be the child's facts. A |
| +// difference anywhere is a changed edge, named. |
| +func (d *Dir) Recheck() error { |
| + if err := d.live(); err != nil { |
| + return err |
| + } |
| + for c := d; c.parent != nil; c = c.parent { |
| + if err := c.parent.live(); err != nil { |
| + return err |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Fstatat(c.parent.fd, c.name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil { |
| + return &EdgeError{Name: c.name, Err: err} |
| + } |
| + if !factsOf(&st).Same(c.facts) { |
| + return &EdgeError{Name: c.name, Err: ErrChanged} |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +// Walk classifies a captured link target. A relative spelling is walked |
| +// from this Dir; an absolute one from the pinned "/" under the platform |
| +// walk's rules until it enters a pinned root, then beneath that root. A |
| +// leading run of ".." pops the retained stack and is refused past the |
| +// root; ".." after a named component is refused. The last component is |
| +// looked at without following it and its kind returned; a spelling that |
| +// demands a directory of something else is ErrNotDirectory. A directory |
| +// result carries a retained Dir, which Close releases if the walk opened |
| +// it. |
| +func (d *Dir) Walk(spelling string, pins *Pins) (Result, error) { |
| + if err := d.live(); err != nil { |
| + return Result{}, err |
| + } |
| + comps, abs, wantDir, err := normalise(spelling) |
| + if err != nil { |
| + return Result{}, err |
| + } |
| + // the grammar first, before any system call: ".." only in a leading |
| + // run, and that run no deeper than the stack above the root |
| + ups := 0 |
| + for i, c := range comps { |
| + if c != ".." { |
| + for _, later := range comps[i:] { |
| + if later == ".." { |
| + return Result{}, ErrAscentAfterName |
| + } |
| + } |
| + break |
| + } |
| + ups++ |
| + } |
| + if ups > 0 && abs { |
| + return Result{}, ErrAscentPastRoot |
| + } |
| + for p, n := d, ups; n > 0; n-- { |
| + if p.root || p.parent == nil { |
| + return Result{}, ErrAscentPastRoot |
| + } |
| + p = p.parent |
| + } |
| + cur := d |
| + entered := true // a relative walk is already beneath its root |
| + if abs { |
| + if pins == nil || pins.Slash == nil { |
| + return Result{}, ErrOutside |
| + } |
| + cur = pins.Slash |
| + entered = false |
| + } |
| + res := Result{} |
| + fail := func(err error) (Result, error) { |
| + res.Close() |
| + return Result{}, err |
| + } |
| + leading := true |
| + for i, c := range comps { |
| + last := i == len(comps)-1 |
| + if c == ".." { |
| + if !leading { |
| + return fail(ErrAscentAfterName) |
| + } |
| + if abs || cur.root || cur.parent == nil { |
| + return fail(ErrAscentPastRoot) |
| + } |
| + cur = cur.parent |
| + continue |
| + } |
| + leading = false |
| + if !entered { |
| + child, err := cur.Child(c, nil) |
| + if err != nil { |
| + return fail(&EdgeError{Name: c, Err: ErrOutside}) |
| + } |
| + if !platformOK(child.facts, pins.Platform) { |
| + child.Close() |
| + return fail(&EdgeError{Name: c, Err: ErrNotPlatform}) |
| + } |
| + if pin := pins.match(child.facts); pin != nil { |
| + child.Close() |
| + cur = pin.Dir |
| + entered = true |
| + } else { |
| + res.owned = append(res.owned, child) |
| + cur = child |
| + } |
| + if last { |
| + if !entered { |
| + return fail(&EdgeError{Name: c, Err: ErrOutside}) |
| + } |
| + res.Kind, res.Facts, res.Dir = Directory, cur.facts, cur |
| + return res, nil |
| + } |
| + continue |
| + } |
| + if !last { |
| + child, err := cur.Child(c, nil) |
| + if err != nil { |
| + var ee *EdgeError |
| + if errors.As(err, &ee) { |
| + switch { |
| + case errors.Is(ee.Err, unix.ENOENT): |
| + res.Close() |
| + return Result{Kind: Absent}, nil |
| + case errors.Is(ee.Err, ErrLink): |
| + res.Close() |
| + return Result{Kind: Link}, nil |
| + } |
| + } |
| + return fail(err) |
| + } |
| + res.owned = append(res.owned, child) |
| + cur = child |
| + continue |
| + } |
| + k, f, err := cur.Lookat(c) |
| + if err != nil { |
| + return fail(err) |
| + } |
| + if k == Absent { |
| + res.Close() |
| + return Result{Kind: Absent}, nil |
| + } |
| + if wantDir && k != Directory { |
| + return fail(&EdgeError{Name: c, Err: ErrNotDirectory}) |
| + } |
| + res.Kind, res.Facts = k, f |
| + if k == Directory { |
| + child, err := cur.Child(c, &f) |
| + if err != nil { |
| + return fail(err) |
| + } |
| + res.owned = append(res.owned, child) |
| + res.Dir = child |
| + } |
| + return res, nil |
| + } |
| + // nothing named: the anchor or the popped directory, as a directory |
| + if !entered { |
| + return fail(ErrOutside) |
| + } |
| + res.Kind, res.Facts, res.Dir = Directory, cur.facts, cur |
| + return res, nil |
| +} |
| + |
| +// Root is a Dir reached from a pinned "/" by a walk whose every component |
| +// is the platform's; the facts of that walk are its ancestry. |
| +type Root struct { |
| + *Dir |
| + Path string |
| + slash *Dir |
| + comps []string |
| + ancestry []Facts |
| + platform func(uint32) bool |
| +} |
| + |
| +// OpenRoot walks path's components from slash, requiring each to be a |
| +// directory (no link: O_NOFOLLOW), owned by a platform uid and not |
| +// writable by group or others. The intermediate Dirs are retained as the |
| +// root's stack. Path is spelled from slash, so "/var/lib/carlos" against |
| +// the real "/" is that directory, and against a test's "/" it is beneath it. |
| +func OpenRoot(slash *Dir, path string, platform func(uint32) bool) (*Root, error) { |
| + if err := slash.live(); err != nil { |
| + return nil, err |
| + } |
| + if !platformOK(slash.facts, platform) { |
| + return nil, &EdgeError{Name: "/", Err: ErrNotPlatform} |
| + } |
| + var comps []string |
| + for _, c := range strings.Split(filepath.Clean("/"+path), "/") { |
| + if c != "" { |
| + comps = append(comps, c) |
| + } |
| + } |
| + cur := slash |
| + var ancestry []Facts |
| + var opened []*Dir |
| + fail := func(err error) (*Root, error) { |
| + for i := len(opened) - 1; i >= 0; i-- { |
| + opened[i].Close() |
| + } |
| + return nil, err |
| + } |
| + for _, c := range comps { |
| + child, err := cur.Child(c, nil) |
| + if err != nil { |
| + return fail(err) |
| + } |
| + if !platformOK(child.facts, platform) { |
| + child.Close() |
| + return fail(&EdgeError{Name: c, Err: ErrNotPlatform}) |
| + } |
| + opened = append(opened, child) |
| + ancestry = append(ancestry, child.facts) |
| + cur = child |
| + } |
| + cur.root = true |
| + return &Root{Dir: cur, Path: path, slash: slash, comps: comps, ancestry: ancestry, platform: platform}, nil |
| +} |
| + |
| +// Ancestry is the facts of each component of the root's walk, top down. |
| +func (r *Root) Ancestry() []Facts { return append([]Facts(nil), r.ancestry...) } |
| + |
| +// AncestryChanged walks the path again from the pinned "/" and reports |
| +// whether any component differs from the retained ancestry, or the walk |
| +// fails the platform rule now. |
| +func (r *Root) AncestryChanged() (bool, error) { |
| + cur := r.slash |
| + var opened []*Dir |
| + defer func() { |
| + for i := len(opened) - 1; i >= 0; i-- { |
| + opened[i].Close() |
| + } |
| + }() |
| + for i, c := range r.comps { |
| + child, err := cur.Child(c, nil) |
| + if err != nil { |
| + return true, err |
| + } |
| + opened = append(opened, child) |
| + if !platformOK(child.facts, r.platform) { |
| + return true, &EdgeError{Name: c, Err: ErrNotPlatform} |
| + } |
| + if !child.facts.Same(r.ancestry[i]) { |
| + return true, nil |
| + } |
| + cur = child |
| + } |
| + return false, nil |
| +} |
| + |
| +// Close closes the root and every intermediate Dir of its walk, not slash. |
| +func (r *Root) Close() error { |
| + var first error |
| + for c := r.Dir; c != nil && c != r.slash; c = c.parent { |
| + if err := c.Close(); err != nil && first == nil { |
| + first = err |
| + } |
| + } |
| + return first |
| +} |
diff --git a/internal/authority/authority_unix_test.go b/internal/authority/authority_unix_test.go| new file mode 100644 |
| index 000000000..8ac1d8ef3 |
| --- /dev/null |
| +++ b/internal/authority/authority_unix_test.go |
| @@ -0,0 +1,485 @@ |
| +//go:build unix |
| + |
| +package authority |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + "time" |
| + |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func me(uid uint32) bool { return uid == uint32(os.Geteuid()) } |
| + |
| +// tempDir is t.TempDir with the mode the platform rule wants, whatever the |
| +// umask (0002 makes a plain temp directory group-writable). |
| +func tempDir(t *testing.T) string { |
| + t.Helper() |
| + base := t.TempDir() |
| + if err := os.Chmod(base, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + return base |
| +} |
| + |
| +func mkdir(t *testing.T, p string) { |
| + t.Helper() |
| + if err := os.MkdirAll(p, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + // MkdirAll's mode is subject to the umask; set each created level |
| + for q := p; q != "/" && q != "."; q = filepath.Dir(q) { |
| + fi, err := os.Lstat(q) |
| + if err != nil || !fi.IsDir() { |
| + break |
| + } |
| + if fi.Mode().Perm()&0o022 == 0 { |
| + break |
| + } |
| + if err := os.Chmod(q, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| +} |
| + |
| +func write(t *testing.T, p, body string) { |
| + t.Helper() |
| + if err := os.WriteFile(p, []byte(body), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func link(t *testing.T, target, p string) { |
| + t.Helper() |
| + if err := os.Symlink(target, p); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func fifo(t *testing.T, p string) { |
| + t.Helper() |
| + if err := unix.Mkfifo(p, 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func open(t *testing.T, p string) *Dir { |
| + t.Helper() |
| + d, err := Open(p) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { d.Close() }) |
| + return d |
| +} |
| + |
| +func child(t *testing.T, d *Dir, name string) *Dir { |
| + t.Helper() |
| + c, err := d.Child(name, nil) |
| + if err != nil { |
| + t.Fatalf("Child(%s): %v", name, err) |
| + } |
| + t.Cleanup(func() { c.Close() }) |
| + return c |
| +} |
| + |
| +func edgeIs(err error, target error) bool { |
| + var ee *EdgeError |
| + return errors.As(err, &ee) && errors.Is(ee.Err, target) |
| +} |
| + |
| +// A platform ancestry with a group-writable component, a linked component |
| +// or a non-platform owner is refused; a clean one builds, and a component |
| +// replaced beneath it is a changed ancestry. |
| +func TestRootPlatformWalk(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + mkdir(t, filepath.Join(base, "a", "b")) |
| + if _, err := OpenRoot(slash, "a/b", func(uint32) bool { return false }); !edgeIs(err, ErrNotPlatform) { |
| + t.Fatalf("non-platform owner: %v", err) |
| + } |
| + if err := os.Chmod(filepath.Join(base, "a"), 0o775); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := OpenRoot(slash, "a/b", me); !edgeIs(err, ErrNotPlatform) { |
| + t.Fatalf("group-writable component: %v", err) |
| + } |
| + os.Chmod(filepath.Join(base, "a"), 0o755) |
| + mkdir(t, filepath.Join(base, "real", "b")) |
| + link(t, "real", filepath.Join(base, "lnk")) |
| + if _, err := OpenRoot(slash, "lnk/b", me); !edgeIs(err, ErrLink) { |
| + t.Fatalf("linked component: %v", err) |
| + } |
| + r, err := OpenRoot(slash, "a/b", me) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer r.Close() |
| + if len(r.Ancestry()) != 2 || r.Depth() != 2 || !r.root { |
| + t.Fatalf("ancestry %d depth %d root %v", len(r.Ancestry()), r.Depth(), r.root) |
| + } |
| + if changed, err := r.AncestryChanged(); changed || err != nil { |
| + t.Fatalf("unchanged ancestry read as changed: %v %v", changed, err) |
| + } |
| + if err := os.Rename(filepath.Join(base, "a", "b"), filepath.Join(base, "a", "c")); err != nil { |
| + t.Fatal(err) |
| + } |
| + mkdir(t, filepath.Join(base, "a", "b")) |
| + if changed, _ := r.AncestryChanged(); !changed { |
| + t.Fatal("a replaced component not seen") |
| + } |
| + if err := r.Recheck(); !edgeIs(err, ErrChanged) { |
| + t.Fatalf("Recheck on the moved root: %v", err) |
| + } |
| +} |
| + |
| +// A directory swapped between List and Walk changes nothing: the walk is |
| +// from the retained descriptor, not the path. |
| +func TestSwapBetweenListAndWalk(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + mkdir(t, filepath.Join(base, "A", "sub")) |
| + write(t, filepath.Join(base, "A", "sub", "x"), "db") |
| + link(t, "sub/x", filepath.Join(base, "A", "l")) |
| + a := child(t, slash, "A") |
| + entries, err := a.List() |
| + if err != nil || len(entries) != 2 { |
| + t.Fatalf("list: %v %v", entries, err) |
| + } |
| + if err := os.Rename(filepath.Join(base, "A"), filepath.Join(base, "old")); err != nil { |
| + t.Fatal(err) |
| + } |
| + mkdir(t, filepath.Join(base, "A", "sub")) // the swap: an empty A in its place |
| + target, err := a.Readlink("l") |
| + if err != nil || target != "sub/x" { |
| + t.Fatalf("readlink from the retained directory: %q %v", target, err) |
| + } |
| + res, err := a.Walk(target, nil) |
| + if err != nil || res.Kind != Regular { |
| + t.Fatalf("walk from the retained directory: %v %v", res.Kind, err) |
| + } |
| + fresh := open(t, filepath.Join(base, "A")) |
| + res, err = fresh.Walk(target, nil) |
| + if err != nil || res.Kind != Absent { |
| + t.Fatalf("walk from the swapped-in directory: %v %v", res.Kind, err) |
| + } |
| +} |
| + |
| +// A listed subdirectory replaced by a FIFO fails Child with ENOTDIR at |
| +// lookup, and nothing blocks on the FIFO. |
| +func TestChildRefusesFIFOWithoutBlocking(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + mkdir(t, filepath.Join(base, "sub")) |
| + entries, err := slash.List() |
| + if err != nil || len(entries) != 1 || entries[0].Kind != Directory { |
| + t.Fatalf("list: %v %v", entries, err) |
| + } |
| + if err := os.Rename(filepath.Join(base, "sub"), filepath.Join(base, "gone")); err != nil { |
| + t.Fatal(err) |
| + } |
| + fifo(t, filepath.Join(base, "sub")) |
| + done := make(chan error, 1) |
| + go func() { |
| + _, err := slash.Child("sub", &entries[0].Facts) |
| + done <- err |
| + }() |
| + select { |
| + case err := <-done: |
| + if !edgeIs(err, unix.ENOTDIR) { |
| + t.Fatalf("want ENOTDIR, got %v", err) |
| + } |
| + case <-time.After(5 * time.Second): |
| + t.Fatal("Child blocked on a FIFO") |
| + } |
| + // the same for OpenRegular: a FIFO in a file's place is refused, not read |
| + k, f, _ := slash.Lookat("sub") |
| + if k != Other { |
| + t.Fatalf("a FIFO looked at as %v", k) |
| + } |
| + go func() { |
| + _, err := slash.OpenRegular("sub", f) |
| + done <- err |
| + }() |
| + select { |
| + case err := <-done: |
| + if !edgeIs(err, ErrNotRegular) { |
| + t.Fatalf("want ErrNotRegular, got %v", err) |
| + } |
| + case <-time.After(5 * time.Second): |
| + t.Fatal("OpenRegular blocked on a FIFO") |
| + } |
| +} |
| + |
| +// ".." pops the retained stack: past the root it is refused, after a named |
| +// component it is refused, and otherwise it is the parent, as a directory. |
| +func TestAscent(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + mkdir(t, filepath.Join(base, "r", "a", "b")) |
| + write(t, filepath.Join(base, "r", "f"), "x") |
| + r, err := OpenRoot(slash, "r", me) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer r.Close() |
| + b := child(t, child(t, r.Dir, "a"), "b") |
| + if _, err := b.Walk("../../..", nil); !errors.Is(err, ErrAscentPastRoot) { |
| + t.Fatalf("past the root: %v", err) |
| + } |
| + if _, err := b.Walk("../x/..", nil); !errors.Is(err, ErrAscentAfterName) { |
| + t.Fatalf("after a name: %v", err) |
| + } |
| + res, err := b.Walk("../..", nil) |
| + if err != nil || res.Kind != Directory || !res.Facts.Same(r.Facts()) || res.Dir != r.Dir { |
| + t.Fatalf("to the root: %v %v", res, err) |
| + } |
| + res.Close() // closes nothing it was not given |
| + if err := r.Recheck(); err != nil { |
| + t.Fatalf("the root was closed by a result that did not own it: %v", err) |
| + } |
| + res, err = b.Walk("../../f", nil) |
| + if err != nil || res.Kind != Regular { |
| + t.Fatalf("a file via ascent: %v %v", res.Kind, err) |
| + } |
| + if _, err := r.Walk("..", nil); !errors.Is(err, ErrAscentPastRoot) { |
| + t.Fatalf("\"..\" from the root: %v", err) |
| + } |
| +} |
| + |
| +// A retained child moved under another parent is a changed edge at Recheck, |
| +// and so is a new directory put in its place. |
| +func TestRecheckDetectsMovedChild(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + mkdir(t, filepath.Join(base, "a", "b")) |
| + mkdir(t, filepath.Join(base, "c")) |
| + b := child(t, child(t, slash, "a"), "b") |
| + if err := b.Recheck(); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(filepath.Join(base, "a", "b"), filepath.Join(base, "c", "b")); err != nil { |
| + t.Fatal(err) |
| + } |
| + var ee *EdgeError |
| + if err := b.Recheck(); !errors.As(err, &ee) || ee.Name != "b" { |
| + t.Fatalf("moved child: %v", err) |
| + } |
| + mkdir(t, filepath.Join(base, "a", "b")) |
| + if err := b.Recheck(); !edgeIs(err, ErrChanged) { |
| + t.Fatalf("replaced child: %v", err) |
| + } |
| + // the moved directory itself still answers: its descriptor is the object |
| + if _, err := b.List(); err != nil { |
| + t.Fatalf("the retained descriptor stopped working: %v", err) |
| + } |
| +} |
| + |
| +// List twice on one Dir gives the same entries, and Child with the listed |
| +// facts refuses when the entry changed between the listing and the |
| +// acquisition. |
| +func TestListAndChildExpect(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + mkdir(t, filepath.Join(base, "sub")) |
| + write(t, filepath.Join(base, "f"), "x") |
| + link(t, "f", filepath.Join(base, "l")) |
| + fifo(t, filepath.Join(base, "p")) |
| + one, err := slash.List() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + two, err := slash.List() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(one) != 4 || len(two) != 4 { |
| + t.Fatalf("%d and %d entries", len(one), len(two)) |
| + } |
| + for i := range one { |
| + if one[i] != two[i] { |
| + t.Fatalf("listing differs: %v vs %v", one[i], two[i]) |
| + } |
| + } |
| + kinds := map[string]Kind{} |
| + for _, e := range one { |
| + kinds[e.Name] = e.Kind |
| + } |
| + if kinds["sub"] != Directory || kinds["f"] != Regular || kinds["l"] != Link || kinds["p"] != Other { |
| + t.Fatalf("kinds %v", kinds) |
| + } |
| + if err := os.Rename(filepath.Join(base, "sub"), filepath.Join(base, "sub2")); err != nil { |
| + t.Fatal(err) |
| + } |
| + mkdir(t, filepath.Join(base, "sub")) |
| + if _, err := slash.Child("sub", &one[3].Facts); !edgeIs(err, ErrChanged) { |
| + t.Fatalf("changed entry acquired: %v", err) |
| + } |
| + for _, bad := range []string{"", ".", "..", "a/b", "a\x00"} { |
| + if _, err := slash.Child(bad, nil); !errors.Is(err, ErrBadName) { |
| + t.Fatalf("Child(%q): %v", bad, err) |
| + } |
| + } |
| +} |
| + |
| +// Every link class is classified as written, none silently. |
| +func TestWalkClasses(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + r := filepath.Join(base, "r") |
| + mkdir(t, filepath.Join(r, "d", "inner")) |
| + write(t, filepath.Join(r, "f"), "x") |
| + link(t, "f", filepath.Join(r, "l")) |
| + link(t, "d", filepath.Join(r, "ld")) |
| + fifo(t, filepath.Join(r, "fifo")) |
| + mkdir(t, filepath.Join(base, "other")) |
| + write(t, filepath.Join(base, "other", "f"), "x") |
| + mkdir(t, filepath.Join(base, "w", "x")) |
| + os.Chmod(filepath.Join(base, "w"), 0o775) |
| + root, err := OpenRoot(slash, "r", me) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer root.Close() |
| + pins := &Pins{Slash: slash, Roots: []*Root{root}, Platform: me} |
| + inner := child(t, child(t, root.Dir, "d"), "inner") |
| + |
| + cases := []struct { |
| + from *Dir |
| + spelling string |
| + kind Kind |
| + err error |
| + }{ |
| + {root.Dir, "d", Directory, nil}, |
| + {root.Dir, "f", Regular, nil}, |
| + {root.Dir, "l", Link, nil}, |
| + {root.Dir, "ld", Link, nil}, |
| + {root.Dir, "fifo", Other, nil}, |
| + {root.Dir, "nothing", Absent, nil}, |
| + {root.Dir, "d/", Directory, nil}, |
| + {root.Dir, "d/.", Directory, nil}, |
| + {root.Dir, "d//inner/", Directory, nil}, |
| + {root.Dir, "f/", Absent, ErrNotDirectory}, |
| + {root.Dir, "f/.", Absent, ErrNotDirectory}, |
| + {root.Dir, ".", Directory, nil}, |
| + {root.Dir, "./", Directory, nil}, |
| + {root.Dir, "", Absent, ErrEmptySpelling}, |
| + {root.Dir, "l/x", Link, nil}, |
| + {root.Dir, "ld/inner", Link, nil}, |
| + {root.Dir, "nothing/x", Absent, nil}, |
| + {root.Dir, "f/x", Absent, unix.ENOTDIR}, |
| + {inner, "../../f", Regular, nil}, |
| + {inner, "../..", Directory, nil}, |
| + {inner, "../../..", Absent, ErrAscentPastRoot}, |
| + {inner, "../d/../f", Absent, ErrAscentAfterName}, |
| + {root.Dir, "/r/f", Regular, nil}, |
| + {root.Dir, "/r/d/inner", Directory, nil}, |
| + {root.Dir, "/r", Directory, nil}, |
| + {root.Dir, "/r/", Directory, nil}, |
| + {root.Dir, "/", Absent, ErrOutside}, |
| + {root.Dir, "/other/f", Absent, ErrOutside}, |
| + {root.Dir, "/other", Absent, ErrOutside}, |
| + {root.Dir, "/w/x", Absent, ErrNotPlatform}, |
| + {root.Dir, "/missing/f", Absent, ErrOutside}, |
| + {root.Dir, "/r/../other/f", Absent, ErrAscentAfterName}, |
| + {root.Dir, "/../r/f", Absent, ErrAscentPastRoot}, |
| + {inner, "/r/nothing", Absent, nil}, |
| + } |
| + for _, c := range cases { |
| + res, err := c.from.Walk(c.spelling, pins) |
| + if c.err != nil { |
| + if err == nil || !(errors.Is(err, c.err) || edgeIs(err, c.err)) { |
| + t.Errorf("Walk(%q): want %v, got kind %v err %v", c.spelling, c.err, res.Kind, err) |
| + } |
| + continue |
| + } |
| + if err != nil || res.Kind != c.kind { |
| + t.Errorf("Walk(%q): want %v, got %v %v", c.spelling, c.kind, res.Kind, err) |
| + } |
| + if res.Kind == Directory && res.Dir == nil { |
| + t.Errorf("Walk(%q): a directory with no Dir", c.spelling) |
| + } |
| + res.Close() |
| + } |
| + // the result of an absolute walk into the pinned root is the pinned Dir |
| + res, err := root.Walk("/r", pins) |
| + if err != nil || res.Dir != root.Dir { |
| + t.Fatalf("/r is not the pinned root: %v %v", res.Dir, err) |
| + } |
| + res.Close() |
| + // a walk without pins cannot take an absolute spelling anywhere |
| + if _, err := root.Walk("/r/f", nil); !errors.Is(err, ErrOutside) { |
| + t.Fatalf("absolute without pins: %v", err) |
| + } |
| + // a directory result's stack is retained and rechecks against the anchor |
| + res, err = root.Walk("d/inner", nil) |
| + if err != nil || res.Dir.Depth() != root.Depth()+2 { |
| + t.Fatalf("stack: %v %v", res, err) |
| + } |
| + if err := res.Dir.Recheck(); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(filepath.Join(r, "d"), filepath.Join(r, "d2")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := res.Dir.Recheck(); err == nil { |
| + t.Fatal("a moved directory on the stack not seen") |
| + } |
| + res.Close() |
| + if err := root.Recheck(); err != nil { |
| + t.Fatalf("closing a result closed its anchor: %v", err) |
| + } |
| +} |
| + |
| +// OpenRegular reads the file that was looked at and refuses a link, a FIFO |
| +// or another file in its place. |
| +func TestOpenRegular(t *testing.T) { |
| + base := tempDir(t) |
| + slash := open(t, base) |
| + write(t, filepath.Join(base, "f"), "hello") |
| + write(t, filepath.Join(base, "g"), "other") |
| + _, f, err := slash.Lookat("f") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fh, err := slash.OpenRegular("f", f) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + buf := make([]byte, 5) |
| + if _, err := fh.Read(buf); err != nil || string(buf) != "hello" { |
| + t.Fatalf("read %q %v", buf, err) |
| + } |
| + fh.Close() |
| + if err := os.Rename(filepath.Join(base, "g"), filepath.Join(base, "f")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := slash.OpenRegular("f", f); !edgeIs(err, ErrNotRegular) { |
| + t.Fatalf("another file in its place: %v", err) |
| + } |
| + os.Remove(filepath.Join(base, "f")) |
| + link(t, "/etc/hostname", filepath.Join(base, "f")) |
| + if _, err := slash.OpenRegular("f", f); !edgeIs(err, unix.ELOOP) { |
| + t.Fatalf("a link in its place: %v", err) |
| + } |
| +} |
| + |
| +// A closed Dir refuses everything and closing it again is harmless. |
| +func TestClosed(t *testing.T) { |
| + base := tempDir(t) |
| + d, err := Open(base) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + d.Close() |
| + d.Close() |
| + if _, err := d.List(); !errors.Is(err, ErrClosed) { |
| + t.Fatalf("List after Close: %v", err) |
| + } |
| + if _, err := d.Walk("x", nil); !errors.Is(err, ErrClosed) { |
| + t.Fatalf("Walk after Close: %v", err) |
| + } |
| +} |
diff --git a/internal/authority/doc.go b/internal/authority/doc.go| new file mode 100644 |
| index 000000000..ae3796d78 |
| --- /dev/null |
| +++ b/internal/authority/doc.go |
| @@ -0,0 +1,20 @@ |
| +// Package authority is Part A of the replicator's authority design |
| +// (docs/superpowers/specs/2026-10-03-replicator-pinned-roots-design.md): a |
| +// hand-walked, retained directory descriptor, and nothing resolved by |
| +// pathname after it is acquired. |
| +// |
| +// A Dir is one open directory descriptor with the facts (device, inode, |
| +// owner, mode) read at acquisition and a pointer to the Dir it was acquired |
| +// from, so a chain of Dirs is a stack. Every method takes a single name |
| +// and resolves it relative to the descriptor alone, with O_NOFOLLOW. List |
| +// uses a fresh open file description each time. Walk classifies a captured |
| +// link target by components from an anchor, popping the retained stack for |
| +// a leading run of ".." rather than asking the kernel for "..", and never |
| +// leaves the root it started in. A Root is a Dir reached from a pinned "/" |
| +// by a walk whose every component is the platform's. |
| +// |
| +// What this package does not claim: a bind mount or a device node already |
| +// inside a tree is reachable within containment; a swap put back between |
| +// two looks is not detected by polling. Those are the design's stated |
| +// support conditions and bounds, not this package's. |
| +package authority |