carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package instancefs
4
5import (
6 "bufio"
7 "crypto/rand"
8 "encoding/hex"
9 "errors"
10 "fmt"
11 "io"
12 "os"
13 "path/filepath"
14 "strconv"
15 "strings"
16
17 "golang.org/x/sys/unix"
18)
19
20const resolveFlags = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS
21
22// openDir opens dir itself as a directory fd, refusing a symlink at ANY
23// component of dir, not only its final one: the instance directory's
24// parent is the agent's, but a same-uid process could have swapped an
25// intermediate component for a symlink before the mount option landed,
26// and openat2's RESOLVE_NO_SYMLINKS catches that the way a plain
27// O_NOFOLLOW open (which only refuses a symlink at the leaf) would not.
28// RESOLVE_BENEATH is deliberately absent here: dir is an absolute path
29// handed in by the caller, not a name resolved beneath some other fd, so
30// there is no "beneath" for it to stay under.
31func openDir(dir string) (int, error) {
32 how := unix.OpenHow{
33 Flags: unix.O_PATH | unix.O_DIRECTORY | unix.O_CLOEXEC,
34 Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS,
35 }
36 fd, err := unix.Openat2(unix.AT_FDCWD, dir, &how)
37 if err != nil {
38 if errors.Is(err, unix.ELOOP) {
39 return -1, fmt.Errorf("%w: %s", ErrSymlink, dir)
40 }
41 return -1, &os.PathError{Op: "openat2", Path: dir, Err: err}
42 }
43 return fd, nil
44}
45
46// SelfCheck is the one call a process makes before it trusts the rest of
47// this package: it opens dir exactly as openDir does and reports whether
48// the kernel let the call through at all. It exists because a seccomp
49// filter is invisible from inside: the agent would learn of one from the
50// first nested open that fails, which is to say from a layout migration
51// half-way through its first host. ENOSYS is ErrOpenat2Unavailable, named
52// so a gate can say what to fix; any other error is dir's own and is
53// returned as it is. The probe unit of design §8.5 runs under the
54// instance templates' text and so cannot see the edge unit's; this is the
55// edge's own check, run by the agent in its own unit.
56func SelfCheck(dir string) error {
57 fd, err := openDir(dir)
58 if err != nil {
59 if errors.Is(err, unix.ENOSYS) {
60 return fmt.Errorf("%w: %s", ErrOpenat2Unavailable, dir)
61 }
62 return err
63 }
64 unix.Close(fd)
65 return nil
66}
67
68// OpenBeneath opens name under dir with openat2, following no symlink at
69// any component and never leaving dir. Every open, whatever its flags,
70// carries O_NONBLOCK on the wire and is then checked with fstat: a FIFO
71// with no reader on the other end blocks a write-open indefinitely, and
72// one with no writer blocks a read-open indefinitely, and a tenant can
73// plant one anywhere under an instance directory the agent reads OR
74// writes by contract (its run directory, its env files, its admin token
75// file). O_NONBLOCK changes the two blocking cases without changing the
76// non-blocking ones: opening a regular file (the common case, and every
77// O_CREAT|O_EXCL caller's own fresh file) behaves identically either way,
78// a read-open of a FIFO with no writer now returns at once instead of
79// blocking, and a write-open of a FIFO with no reader answers ENXIO at
80// once instead of blocking. Whatever DOES come back with a valid fd (a
81// FIFO opened for read, or for write against an existing reader) is then
82// refused by the fstat check unless it is S_IFREG, so nothing non-regular
83// can be read or written through this function at all.
84func OpenBeneath(dir, name string, flag int, perm os.FileMode) (*os.File, error) {
85 return openBeneathAs(dir, name, flag, perm, unix.S_IFREG)
86}
87
88// OpenDirBeneath opens name under dir as a directory, read-only, exactly as
89// OpenBeneath opens a file: openat2 with no symlink at any component of dir
90// or name, O_NONBLOCK, and then fstat, which accepts only a directory. A
91// name of "." is dir itself. The descriptor is the directory that was
92// vetted: a caller that works from it (an *at call, or OpenRootBeneath)
93// never resolves the path again, so a link swapped in after the open is
94// never followed.
95func OpenDirBeneath(dir, name string) (*os.File, error) {
96 return openBeneathAs(dir, name, os.O_RDONLY|unix.O_DIRECTORY, 0, unix.S_IFDIR)
97}
98
99// OpenRootBeneath is OpenDirBeneath as an *os.Root, for code built on
100// os.Root (hostfiles.RestoreRoot). os.OpenRoot takes only a path and opens
101// it without O_NONBLOCK, so it would follow a link at the name and block
102// on a FIFO there. This opens the directory with OpenDirBeneath and hands
103// os.OpenRoot that descriptor's /proc/self/fd entry, a magic link the
104// kernel resolves to the very directory the descriptor holds: no
105// component is resolved twice. Without /proc it is an error, never a
106// fallback to the path.
107func OpenRootBeneath(dir, name string) (*os.Root, error) {
108 f, err := OpenDirBeneath(dir, name)
109 if err != nil {
110 return nil, err
111 }
112 defer f.Close()
113 betweenOpenAndRoot()
114 r, err := os.OpenRoot(procSelfFD + strconv.Itoa(int(f.Fd())))
115 if err != nil {
116 return nil, fmt.Errorf("instancefs: open %s as a root: %w", filepath.Join(dir, name), err)
117 }
118 return r, nil
119}
120
121// betweenOpenAndRoot is a test seam: where a path swapped after the open
122// would race a root opened by path.
123var betweenOpenAndRoot = func() {}
124
125// procSelfFD is where OpenRootBeneath finds its descriptor's magic link. A
126// test seam: a test points it at a missing directory to pin that no /proc
127// is an error, never a fallback to the path.
128var procSelfFD = "/proc/self/fd/"
129
130// OpenBeneathOrAbsent opens name under dir read-only as OpenBeneath does,
131// and is the one place "absent" is decided on ground someone else can
132// write. It opens dir once and keeps that descriptor: absent is true only
133// when the lookup of name beneath that very descriptor answers ENOENT. A
134// dir that cannot be opened (missing, a symlink, not a directory) is an
135// error wrapped in ErrDirUnopened, never absence: closing a check's
136// descriptor and opening the path again would read a directory renamed
137// away in between as a missing file.
138func OpenBeneathOrAbsent(dir, name string) (*os.File, bool, error) {
139 rel, err := cleanRel(name)
140 if err != nil {
141 return nil, false, err
142 }
143 dfd, err := openDir(dir)
144 if err != nil {
145 return nil, false, fmt.Errorf("%w: %w", ErrDirUnopened, err)
146 }
147 defer unix.Close(dfd)
148 betweenDirAndLookup()
149 f, err := openAt(dfd, dir, rel, os.O_RDONLY, 0, unix.S_IFREG)
150 if errors.Is(err, unix.ENOENT) {
151 return nil, true, nil
152 }
153 if err != nil {
154 return nil, false, err
155 }
156 return f, false, nil
157}
158
159// betweenDirAndLookup is a test seam: after OpenBeneathOrAbsent has opened
160// dir and before it looks name up beneath it.
161var betweenDirAndLookup = func() {}
162
163// openBeneathAs is OpenBeneath's open, accepting only the file type want
164// (S_IFREG or S_IFDIR) from fstat.
165func openBeneathAs(dir, name string, flag int, perm os.FileMode, want uint32) (*os.File, error) {
166 rel, err := cleanRel(name)
167 if err != nil {
168 return nil, err
169 }
170 dfd, err := openDir(dir)
171 if err != nil {
172 return nil, err
173 }
174 defer unix.Close(dfd)
175 return openAt(dfd, dir, rel, flag, perm, want)
176}
177
178// openAt is the open every function here shares: rel beneath the
179// directory descriptor dfd (which names dir, for errors), openat2 with no
180// symlink at any component, O_NONBLOCK, and then fstat, accepting only the
181// file type want.
182func openAt(dfd int, dir, rel string, flag int, perm os.FileMode, want uint32) (*os.File, error) {
183 how := unix.OpenHow{
184 Flags: uint64(flag) | unix.O_NONBLOCK | unix.O_CLOEXEC,
185 Mode: uint64(perm.Perm()),
186 Resolve: resolveFlags,
187 }
188 fd, err := unix.Openat2(dfd, rel, &how)
189 if err != nil {
190 switch {
191 case errors.Is(err, unix.ELOOP):
192 return nil, fmt.Errorf("%w: %s", ErrSymlink, filepath.Join(dir, rel))
193 case errors.Is(err, unix.EXDEV):
194 return nil, fmt.Errorf("%w: %s", ErrEscapes, filepath.Join(dir, rel))
195 }
196 return nil, &os.PathError{Op: "openat2", Path: filepath.Join(dir, rel), Err: err}
197 }
198 path := filepath.Join(dir, rel)
199 var st unix.Stat_t
200 if err := unix.Fstat(fd, &st); err != nil {
201 unix.Close(fd)
202 return nil, &os.PathError{Op: "fstat", Path: path, Err: err}
203 }
204 if st.Mode&unix.S_IFMT != want {
205 unix.Close(fd)
206 if want == unix.S_IFDIR {
207 return nil, fmt.Errorf("%w: %s", ErrNotDir, path)
208 }
209 return nil, fmt.Errorf("%w: %s", ErrNotRegular, path)
210 }
211 return os.NewFile(uintptr(fd), path), nil
212}
213
214// WriteAtomic writes data to a fresh O_EXCL temp file beside name and
215// renames it over name. rename(2) replaces the directory entry and never
216// follows a symlink at the destination, so a planted link is displaced,
217// not written through. The temp name is random, so no same-uid process
218// can pre-plant it (the O_EXCL refuses it if one does).
219func WriteAtomic(dir, name string, data []byte, perm os.FileMode) error {
220 rel, err := cleanRel(name)
221 if err != nil {
222 return err
223 }
224 if strings.Contains(rel, "/") {
225 return fmt.Errorf("%w: WriteAtomic takes a bare file name, got %q", ErrEscapes, name)
226 }
227 dfd, err := openDir(dir)
228 if err != nil {
229 return err
230 }
231 defer unix.Close(dfd)
232 var rnd [8]byte
233 if _, err := rand.Read(rnd[:]); err != nil {
234 return err
235 }
236 tmp := "." + rel + ".tmp-" + hex.EncodeToString(rnd[:])
237 how := unix.OpenHow{
238 Flags: unix.O_WRONLY | unix.O_CREAT | unix.O_EXCL | unix.O_CLOEXEC,
239 Mode: uint64(perm.Perm()),
240 Resolve: resolveFlags,
241 }
242 fd, err := unix.Openat2(dfd, tmp, &how)
243 if err != nil {
244 return &os.PathError{Op: "openat2", Path: filepath.Join(dir, tmp), Err: err}
245 }
246 f := os.NewFile(uintptr(fd), filepath.Join(dir, tmp))
247 cleanup := func() { f.Close(); _ = unix.Unlinkat(dfd, tmp, 0) }
248 if _, err := f.Write(data); err != nil {
249 cleanup()
250 return err
251 }
252 if err := f.Sync(); err != nil {
253 cleanup()
254 return err
255 }
256 if err := f.Close(); err != nil {
257 _ = unix.Unlinkat(dfd, tmp, 0)
258 return err
259 }
260 if err := unix.Renameat(dfd, tmp, dfd, rel); err != nil {
261 _ = unix.Unlinkat(dfd, tmp, 0)
262 return &os.PathError{Op: "renameat", Path: filepath.Join(dir, rel), Err: err}
263 }
264 return nil
265}
266
267// MountOptionOf reports whether mountpoint is a mountpoint whose per-mount
268// options include opt, by reading /proc/self/mountinfo. A path that is not
269// a mountpoint is (false, nil).
270func MountOptionOf(mountpoint, opt string) (bool, error) {
271 f, err := os.Open("/proc/self/mountinfo")
272 if err != nil {
273 return false, err
274 }
275 defer f.Close()
276 return mountOptionOf(f, mountpoint, opt)
277}
278
279// MountNoSymfollow reports whether mountpoint carries nosymfollow. It
280// predates MountOptionOf and stays as a thin wrapper because most callers
281// only ever ask this one question and naming it directly reads better at
282// the call site than spelling the option out every time.
283func MountNoSymfollow(mountpoint string) (bool, error) {
284 return MountOptionOf(mountpoint, "nosymfollow")
285}
286
287// mountOptionOf is MountOptionOf's body, taking its mountinfo source as an
288// io.Reader so a test can feed it a fixture with stacked mount lines
289// instead of the live /proc/self/mountinfo.
290//
291// Stacked mounts at one mountpoint (a bind mount, then a later remount)
292// list oldest first in mountinfo, and the LAST line for a mountpoint is
293// the one actually in effect. Every line is therefore scanned to the end
294// and the last matching line's verdict remembered, rather than returning
295// on the first match: an earlier stacked mount carrying opt must not
296// outvote a later one that dropped it, or vice versa.
297func mountOptionOf(r io.Reader, mountpoint, opt string) (bool, error) {
298 want := filepath.Clean(mountpoint)
299 sc := bufio.NewScanner(r)
300 verdict := false
301 for sc.Scan() {
302 line := sc.Text()
303 mp, ok := mountinfoLineMountpoint(line)
304 if !ok || mp != want {
305 continue
306 }
307 verdict = mountinfoLineHas(line, want, opt)
308 }
309 return verdict, sc.Err()
310}
311
312// mountinfoLineMountpoint returns field 5 of a mountinfo line (the
313// mountpoint, octal-escaped), unescaped, and whether the line has enough
314// fields to have one at all.
315func mountinfoLineMountpoint(line string) (string, bool) {
316 fields := strings.Fields(line)
317 if len(fields) < 6 {
318 return "", false
319 }
320 return unescapeMountinfo(fields[4]), true
321}
322
323// mountinfoLineHas parses one mountinfo line: field 5 is the mountpoint
324// (octal-escaped), field 6 the per-mount options.
325func mountinfoLineHas(line, mountpoint, option string) bool {
326 fields := strings.Fields(line)
327 if len(fields) < 6 {
328 return false
329 }
330 if unescapeMountinfo(fields[4]) != mountpoint {
331 return false
332 }
333 for _, o := range strings.Split(fields[5], ",") {
334 if o == option {
335 return true
336 }
337 }
338 return false
339}
340
341// unescapeMountinfo undoes the \040-style escapes mountinfo applies to
342// spaces, tabs, newlines and backslashes in paths.
343func unescapeMountinfo(s string) string {
344 if !strings.Contains(s, `\`) {
345 return s
346 }
347 var b strings.Builder
348 for i := 0; i < len(s); i++ {
349 if s[i] == '\\' && i+3 < len(s) {
350 var v int
351 if _, err := fmt.Sscanf(s[i+1:i+4], "%o", &v); err == nil {
352 b.WriteByte(byte(v))
353 i += 3
354 continue
355 }
356 }
357 b.WriteByte(s[i])
358 }
359 return b.String()
360}
361