// Package instancefs is how the agent opens anything inside an instance's // own directory. Every exec-backed instance runs as the agent's uid, and // its directory is writable by it, so any path the agent opens there is a // request the instance makes on the agent's behalf: a symlink at // .env pointing at /data/litestream.env would hand the instance the // bucket credentials as its own environment (isolated-exec design §1.3). // // The kernel closes that for every reader at once with the nosymfollow // mount option on the instance trees (design §3.2). This package is the // belt to that brace: it opens with openat2(RESOLVE_BENEATH | // RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS), so a symlink is refused // with a named error even on a box whose mount is missing the option, and // it writes through O_EXCL temp files and rename, so a planted symlink at // a destination is replaced as a directory entry rather than written // through. // // Nothing here is Linux-specific in its contract; the implementations // are, and the !linux build returns errors.ErrUnsupported so the CLI still // builds on a Mac (AGENTS.md's cross-compile gate). package instancefs import ( "errors" "fmt" "io" "os" "path/filepath" "strings" ) // ErrSymlink is returned when any component of the path is a symlink. var ErrSymlink = errors.New("instancefs: path contains a symlink") // ErrEscapes is returned for a name that is absolute or reaches above dir. var ErrEscapes = errors.New("instancefs: path escapes its directory") // ErrNotRegular is returned when a read-only open resolves to anything // other than a regular file: a FIFO with no writer blocks open(2) // indefinitely, and a directory or device is never a legitimate target for // the files this package reads on a tenant's behalf. var ErrNotRegular = errors.New("instancefs: not a regular file") // ErrDirUnopened wraps whatever kept OpenBeneathOrAbsent from opening its // directory: never evidence that the name is absent. var ErrDirUnopened = errors.New("instancefs: the directory could not be opened") // ErrNotDir is returned when OpenDirBeneath resolves to anything other // than a directory: a file, or a FIFO, which it never waits on. var ErrNotDir = errors.New("instancefs: not a directory") // ErrOpenat2Unavailable is what SelfCheck returns when this process cannot // make the openat2 call every function here is built on. The one cause // seen in practice is the unit's own hardening: systemd's RestrictSUIDSGID= // cannot read the mode inside open_how, so it refuses openat2 outright with // ENOSYS (#126, first roll). A kernel before 5.6 answers the same way. var ErrOpenat2Unavailable = errors.New("instancefs: openat2 is refused in this process (ENOSYS): the unit carries RestrictSUIDSGID=, or the kernel predates 5.6") // DirStat is what EnsureDirBeneath and StatDir found: the mode in // os.FileMode form (setgid included) and the owning uid and gid. type DirStat struct { Mode os.FileMode UID, GID int } // cleanRel validates name as a relative path that stays inside dir. func cleanRel(name string) (string, error) { if name == "" || filepath.IsAbs(name) { return "", ErrEscapes } c := filepath.Clean(name) if c == ".." || strings.HasPrefix(c, "../") { return "", ErrEscapes } return c, nil } // bareName validates name as exactly one path component: the directory // operations act on one entry beneath an fd, and a name carrying a slash // would resolve through intermediate components that fd never vetted. func bareName(name string) error { if name == "" || name == "." || name == ".." || strings.Contains(name, "/") { return ErrEscapes } return nil } // ReadBeneath is OpenBeneath followed by reading the whole file. func ReadBeneath(dir, name string) ([]byte, error) { f, err := OpenBeneath(dir, name, os.O_RDONLY, 0) if err != nil { return nil, err } defer f.Close() return io.ReadAll(f) } // ErrTooLarge is returned by ReadBeneathLimit when the file exceeds max. var ErrTooLarge = errors.New("instancefs: file larger than the limit") // ReadBeneathLimit is ReadBeneath with a size cap. Files in an instance // directory are tenant-writable, and some are read by the agent outside the // tenant's memory wall (the exit record, the launch file), so no read there // is unbounded. func ReadBeneathLimit(dir, name string, max int64) ([]byte, error) { f, err := OpenBeneath(dir, name, os.O_RDONLY, 0) if err != nil { return nil, err } defer f.Close() return readLimit(f, filepath.Join(dir, name), max) } // ReadBeneathLimitOrAbsent is ReadBeneathLimit with absence decided by // OpenBeneathOrAbsent: absent only when name was not found beneath the one // descriptor dir was opened as. Every other failure, a dir that would not // open included, is an error. func ReadBeneathLimitOrAbsent(dir, name string, max int64) ([]byte, bool, error) { f, absent, err := OpenBeneathOrAbsent(dir, name) if err != nil || absent { return nil, absent, err } defer f.Close() b, err := readLimit(f, filepath.Join(dir, name), max) return b, false, err } // readLimit reads f whole, refusing more than max bytes. func readLimit(f *os.File, path string, max int64) ([]byte, error) { b, err := io.ReadAll(io.LimitReader(f, max+1)) if err != nil { return nil, err } if int64(len(b)) > max { return nil, fmt.Errorf("%w: %s over %d bytes", ErrTooLarge, path, max) } return b, nil }