// 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/filepath" "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)) } // 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.readOpened(rel) } // 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) } // 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) } func (b *Brain) readOpenedMax(rel string, max int64) ([]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 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 are skipped, never followed). An entry error is passed to onErr and // the walk continues; a nil onErr ignores it. 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 a real directory first. fi, err := b.root.Lstat(relDir) if err == nil && !fi.IsDir() { err = fmt.Errorf("%s: walk root is not a directory: %w", relDir, ErrNotRegular) } 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) }) }