// Package brainfs reads the brain directory through an os.Root (hyperguild#157). // The brain dir is a Syncthing sendreceive folder and Syncthing syncs // symlinks; a reader that walks and reads by path follows a planted symlink // out of the brain, and blocks forever on a planted FIFO. Readers that go // through a Brain only ever read regular files inside the brain: os.Root // refuses to leave the root even if an entry is swapped mid-read, and every // open is non-blocking with the type checked on the opened handle. package brainfs import ( "errors" "fmt" "io" "io/fs" "os" "path" "path/filepath" "strings" "syscall" ) // ErrNotRegular is returned for an entry that is not a regular file. var ErrNotRegular = errors.New("not a regular file") // ErrTooLarge is returned by ReadRegularMax for a file over the limit. var ErrTooLarge = errors.New("file too large") // Brain is an open brain directory. type Brain struct { root *os.Root dir string } // Open opens brainDir (which may itself be a mount symlink) as a root. func Open(brainDir string) (*Brain, error) { r, err := os.OpenRoot(brainDir) if err != nil { return nil, fmt.Errorf("open brain root: %w", err) } return &Brain{root: r, dir: brainDir}, nil } // Close releases the root. func (b *Brain) Close() error { return b.root.Close() } // Rel converts a path under the brain dir to a root-relative path. func (b *Brain) Rel(path string) (string, error) { rel, err := filepath.Rel(b.dir, path) if err != nil { return "", err } if rel == ".." || len(rel) > 2 && rel[:3] == ".."+string(filepath.Separator) { return "", fmt.Errorf("%s is outside the brain dir", path) } return filepath.ToSlash(rel), nil } // Lstat stats a brain-relative path through the root without following a // final symlink. func (b *Brain) Lstat(rel string) (fs.FileInfo, error) { return b.root.Lstat(filepath.ToSlash(rel)) } // realDir returns ErrNotRegular unless every component of rel is a real // directory. A swap after the check is not caught (accepted residual, // hyperguild#157). func (b *Brain) realDir(rel string) error { if rel == "." { return nil } prefix := "" for _, part := range strings.Split(rel, "/") { prefix = path.Join(prefix, part) fi, err := b.root.Lstat(prefix) if err != nil { return err } if !fi.IsDir() { return fmt.Errorf("%s: walk root component %s is not a directory: %w", rel, prefix, ErrNotRegular) } } return nil } // ReadDirRegular lists the regular files directly in a brain-relative // directory, by name. Every component of relDir must be a real directory // (ErrNotRegular otherwise); symlinks, FIFOs, devices and subdirectories in // it are left out. func (b *Brain) ReadDirRegular(relDir string) ([]string, error) { relDir = filepath.ToSlash(relDir) if err := b.realDir(relDir); err != nil { return nil, err } ents, err := fs.ReadDir(b.root.FS(), relDir) if err != nil { return nil, err } var names []string for _, e := range ents { if e.Type().IsRegular() { names = append(names, e.Name()) } } return names, nil } // ReadRegular reads a brain-relative path, refusing anything that is not a // regular file, both before the open and on the opened handle. func (b *Brain) ReadRegular(rel string) ([]byte, error) { fi, err := b.root.Lstat(rel) if err != nil { return nil, err } if !fi.Mode().IsRegular() { return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular) } return b.readOpenedMax(rel, -1, fi) } // readOpened opens rel non-blocking and reads it only if the OPENED handle is // a regular file. This is the check that matters under a race: an entry // swapped for a FIFO after the Lstat, with a writer holding it open, would // otherwise park io.ReadAll forever (PR #161 review: removing it hung search // after 16 iterations). // // Accepted residual, as in api/rawroot.go: a HARD link to an outside file is // a regular file and is read. Syncthing syncs contents, not links, so a peer // cannot create one; fs.protected_hardlinks=1 and the pod's secrets are on a // different filesystem from the brain volume, so a local caller cannot link // them in. Refusing Nlink > 1 is the next step if either changes. func (b *Brain) readOpened(rel string) ([]byte, error) { return b.readOpenedMax(rel, -1, nil) } // ReadRegularMax is ReadRegular with a size limit, checked on the opened // handle before reading (max < 0 means no limit). func (b *Brain) ReadRegularMax(rel string, max int64) ([]byte, error) { fi, err := b.root.Lstat(rel) if err != nil { return nil, err } if !fi.Mode().IsRegular() { return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular) } return b.readOpenedMax(rel, max, fi) } // readOpenedMax opens rel and reads it if the opened handle is a regular file // and, when lfi is set, the same file the caller Lstat-ed: os.Root follows an // in-root final symlink even with O_NOFOLLOW, so a symlink to another brain // file swapped in between the Lstat and the open is refused only here. func (b *Brain) readOpenedMax(rel string, max int64, lfi fs.FileInfo) ([]byte, error) { f, err := b.root.OpenFile(rel, os.O_RDONLY|syscall.O_NONBLOCK, 0) if err != nil { return nil, err } defer func() { _ = f.Close() }() hfi, err := f.Stat() if err != nil { return nil, err } if !hfi.Mode().IsRegular() { return nil, fmt.Errorf("%s: %w", rel, ErrNotRegular) } if lfi != nil && !os.SameFile(lfi, hfi) { return nil, fmt.Errorf("%s: changed while opening: %w", rel, ErrNotRegular) } if max >= 0 && hfi.Size() > max { return nil, fmt.Errorf("%s is %d bytes (limit %d): %w", rel, hfi.Size(), max, ErrTooLarge) } var r io.Reader = f if wrapAfterStat != nil { r = wrapAfterStat(r) } if max < 0 { return io.ReadAll(r) } // The handle's size can grow after the check; bound the read itself. data, err := io.ReadAll(io.LimitReader(r, max+1)) if err != nil { return nil, err } if int64(len(data)) > max { return nil, fmt.Errorf("%s grew past %d bytes while read: %w", rel, max, ErrTooLarge) } return data, nil } // wrapAfterStat is a test seam: it runs between the size check and the read, // and wraps the reader the read consumes. A package global: brainfs tests // must not use t.Parallel. var wrapAfterStat func(io.Reader) io.Reader // WalkRegular walks a brain-relative directory through the root and calls fn // for each regular file. Directories are descended; symlinks, FIFOs and // devices found by the walk are skipped. A walk root with any component that // is not a real directory goes to onErr as ErrNotRegular and nothing is // yielded. An entry error is passed to onErr and the walk continues; a nil // onErr ignores it. // // Accepted residual (hyperguild#157): a directory swapped for a symlink // between the listing and the read is followed. os.Root still keeps the read // inside the brain, but not inside relDir. func (b *Brain) WalkRegular(relDir string, fn func(rel string) error, onErr func(rel string, err error)) error { relDir = filepath.ToSlash(relDir) // fs.WalkDir stats its root with a follow, and os.Root allows a symlink // that stays inside the brain: knowledge -> . would alias the whole // brain. Require every component of the root to be a real directory // (Lstat checks only the last one). err := b.realDir(relDir) if err != nil { if onErr != nil { onErr(relDir, err) } return nil } return fs.WalkDir(b.root.FS(), relDir, func(p string, d fs.DirEntry, err error) error { if err != nil { if onErr != nil { onErr(p, err) } if d != nil && d.IsDir() { return fs.SkipDir } return nil } if d.IsDir() || !d.Type().IsRegular() { return nil } return fn(p) }) } // WriteRegular replaces the contents of an existing regular file at a // brain-relative path. It never creates a file and never writes through a // final symlink: os.Root.OpenFile follows an in-root final symlink even with // O_NOFOLLOW (verified on go1.27), so the path is Lstat-ed through the root // first and the opened handle must be that same file (os.SameFile), which // also catches a swap between the two. O_NONBLOCK keeps a FIFO from blocking // the open. Intermediate in-brain symlinks are still followed (os.Root keeps // the write inside the brain; hyperguild#167). func (b *Brain) WriteRegular(rel string, data []byte) error { rel = filepath.ToSlash(rel) lfi, err := b.root.Lstat(rel) if err != nil { return err } if !lfi.Mode().IsRegular() { return fmt.Errorf("%s: %w", rel, ErrNotRegular) } f, err := b.root.OpenFile(rel, os.O_WRONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) if err != nil { return err } defer func() { _ = f.Close() }() fi, err := f.Stat() if err != nil { return err } if !fi.Mode().IsRegular() || !os.SameFile(lfi, fi) { return fmt.Errorf("%s: changed while opening: %w", rel, ErrNotRegular) } if err := f.Truncate(0); err != nil { return err } if _, err := f.Write(data); err != nil { return err } return f.Close() }