| 1 | // Package instancefs is how the agent opens anything inside an instance's |
| 2 | // own directory. Every exec-backed instance runs as the agent's uid, and |
| 3 | // its directory is writable by it, so any path the agent opens there is a |
| 4 | // request the instance makes on the agent's behalf: a symlink at |
| 5 | // <host>.env pointing at /data/litestream.env would hand the instance the |
| 6 | // bucket credentials as its own environment (isolated-exec design §1.3). |
| 7 | // |
| 8 | // The kernel closes that for every reader at once with the nosymfollow |
| 9 | // mount option on the instance trees (design §3.2). This package is the |
| 10 | // belt to that brace: it opens with openat2(RESOLVE_BENEATH | |
| 11 | // RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS), so a symlink is refused |
| 12 | // with a named error even on a box whose mount is missing the option, and |
| 13 | // it writes through O_EXCL temp files and rename, so a planted symlink at |
| 14 | // a destination is replaced as a directory entry rather than written |
| 15 | // through. |
| 16 | // |
| 17 | // Nothing here is Linux-specific in its contract; the implementations |
| 18 | // are, and the !linux build returns errors.ErrUnsupported so the CLI still |
| 19 | // builds on a Mac (AGENTS.md's cross-compile gate). |
| 20 | package instancefs |
| 21 | |
| 22 | import ( |
| 23 | "errors" |
| 24 | "fmt" |
| 25 | "io" |
| 26 | "os" |
| 27 | "path/filepath" |
| 28 | "strings" |
| 29 | ) |
| 30 | |
| 31 | // ErrSymlink is returned when any component of the path is a symlink. |
| 32 | var ErrSymlink = errors.New("instancefs: path contains a symlink") |
| 33 | |
| 34 | // ErrEscapes is returned for a name that is absolute or reaches above dir. |
| 35 | var ErrEscapes = errors.New("instancefs: path escapes its directory") |
| 36 | |
| 37 | // ErrNotRegular is returned when a read-only open resolves to anything |
| 38 | // other than a regular file: a FIFO with no writer blocks open(2) |
| 39 | // indefinitely, and a directory or device is never a legitimate target for |
| 40 | // the files this package reads on a tenant's behalf. |
| 41 | var ErrNotRegular = errors.New("instancefs: not a regular file") |
| 42 | |
| 43 | // ErrDirUnopened wraps whatever kept OpenBeneathOrAbsent from opening its |
| 44 | // directory: never evidence that the name is absent. |
| 45 | var ErrDirUnopened = errors.New("instancefs: the directory could not be opened") |
| 46 | |
| 47 | // ErrNotDir is returned when OpenDirBeneath resolves to anything other |
| 48 | // than a directory: a file, or a FIFO, which it never waits on. |
| 49 | var ErrNotDir = errors.New("instancefs: not a directory") |
| 50 | |
| 51 | // ErrOpenat2Unavailable is what SelfCheck returns when this process cannot |
| 52 | // make the openat2 call every function here is built on. The one cause |
| 53 | // seen in practice is the unit's own hardening: systemd's RestrictSUIDSGID= |
| 54 | // cannot read the mode inside open_how, so it refuses openat2 outright with |
| 55 | // ENOSYS (#126, first roll). A kernel before 5.6 answers the same way. |
| 56 | var ErrOpenat2Unavailable = errors.New("instancefs: openat2 is refused in this process (ENOSYS): the unit carries RestrictSUIDSGID=, or the kernel predates 5.6") |
| 57 | |
| 58 | // DirStat is what EnsureDirBeneath and StatDir found: the mode in |
| 59 | // os.FileMode form (setgid included) and the owning uid and gid. |
| 60 | type DirStat struct { |
| 61 | Mode os.FileMode |
| 62 | UID, GID int |
| 63 | } |
| 64 | |
| 65 | // cleanRel validates name as a relative path that stays inside dir. |
| 66 | func cleanRel(name string) (string, error) { |
| 67 | if name == "" || filepath.IsAbs(name) { |
| 68 | return "", ErrEscapes |
| 69 | } |
| 70 | c := filepath.Clean(name) |
| 71 | if c == ".." || strings.HasPrefix(c, "../") { |
| 72 | return "", ErrEscapes |
| 73 | } |
| 74 | return c, nil |
| 75 | } |
| 76 | |
| 77 | // bareName validates name as exactly one path component: the directory |
| 78 | // operations act on one entry beneath an fd, and a name carrying a slash |
| 79 | // would resolve through intermediate components that fd never vetted. |
| 80 | func bareName(name string) error { |
| 81 | if name == "" || name == "." || name == ".." || strings.Contains(name, "/") { |
| 82 | return ErrEscapes |
| 83 | } |
| 84 | return nil |
| 85 | } |
| 86 | |
| 87 | // ReadBeneath is OpenBeneath followed by reading the whole file. |
| 88 | func ReadBeneath(dir, name string) ([]byte, error) { |
| 89 | f, err := OpenBeneath(dir, name, os.O_RDONLY, 0) |
| 90 | if err != nil { |
| 91 | return nil, err |
| 92 | } |
| 93 | defer f.Close() |
| 94 | return io.ReadAll(f) |
| 95 | } |
| 96 | |
| 97 | // ErrTooLarge is returned by ReadBeneathLimit when the file exceeds max. |
| 98 | var ErrTooLarge = errors.New("instancefs: file larger than the limit") |
| 99 | |
| 100 | // ReadBeneathLimit is ReadBeneath with a size cap. Files in an instance |
| 101 | // directory are tenant-writable, and some are read by the agent outside the |
| 102 | // tenant's memory wall (the exit record, the launch file), so no read there |
| 103 | // is unbounded. |
| 104 | func ReadBeneathLimit(dir, name string, max int64) ([]byte, error) { |
| 105 | f, err := OpenBeneath(dir, name, os.O_RDONLY, 0) |
| 106 | if err != nil { |
| 107 | return nil, err |
| 108 | } |
| 109 | defer f.Close() |
| 110 | return readLimit(f, filepath.Join(dir, name), max) |
| 111 | } |
| 112 | |
| 113 | // ReadBeneathLimitOrAbsent is ReadBeneathLimit with absence decided by |
| 114 | // OpenBeneathOrAbsent: absent only when name was not found beneath the one |
| 115 | // descriptor dir was opened as. Every other failure, a dir that would not |
| 116 | // open included, is an error. |
| 117 | func ReadBeneathLimitOrAbsent(dir, name string, max int64) ([]byte, bool, error) { |
| 118 | f, absent, err := OpenBeneathOrAbsent(dir, name) |
| 119 | if err != nil || absent { |
| 120 | return nil, absent, err |
| 121 | } |
| 122 | defer f.Close() |
| 123 | b, err := readLimit(f, filepath.Join(dir, name), max) |
| 124 | return b, false, err |
| 125 | } |
| 126 | |
| 127 | // readLimit reads f whole, refusing more than max bytes. |
| 128 | func readLimit(f *os.File, path string, max int64) ([]byte, error) { |
| 129 | b, err := io.ReadAll(io.LimitReader(f, max+1)) |
| 130 | if err != nil { |
| 131 | return nil, err |
| 132 | } |
| 133 | if int64(len(b)) > max { |
| 134 | return nil, fmt.Errorf("%w: %s over %d bytes", ErrTooLarge, path, max) |
| 135 | } |
| 136 | return b, nil |
| 137 | } |
| 138 | |