| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "io" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "slices" |
| 11 | |
| 12 | "golang.org/x/sys/unix" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/safefile" |
| 15 | ) |
| 16 | |
| 17 | // floorAllowsPrev is the floor gate's judgement: nil only when the floor |
| 18 | // file and the verdict file both open safely and hold the same bytes. |
| 19 | // Anything else is an error, and every error means the same thing to the |
| 20 | // units as a mismatch: carlos.prev does not run. |
| 21 | // |
| 22 | // The floor file lives under the agent's /data, so it is reached without |
| 23 | // following a symlink anywhere on its path (safefile.OpenDir walks from / |
| 24 | // with O_NOFOLLOW) and opened once, leaf O_NOFOLLOW and O_NONBLOCK: a |
| 25 | // symlink fails the open, and a FIFO opens at once instead of parking root |
| 26 | // until a writer appears, then fails the fstat. The verdict lives in root's |
| 27 | // own /opt/carlos, whose directories only root can change, so only its |
| 28 | // leaf is opened that way. Both are then fstat-checked on the descriptor |
| 29 | // they will be read from (checkFloorGateStat), so nothing can be swapped |
| 30 | // between the check and the read. |
| 31 | func floorAllowsPrev(floorPath, verdictPath string, floorOwners, verdictOwners []uint32) error { |
| 32 | floor, err := readFloorGateFile(floorPath, floorOwners, true) |
| 33 | if err != nil { |
| 34 | return err |
| 35 | } |
| 36 | verdict, err := readFloorGateFile(verdictPath, verdictOwners, false) |
| 37 | if err != nil { |
| 38 | return err |
| 39 | } |
| 40 | if !bytes.Equal(floor, verdict) { |
| 41 | return errFloorGateMismatch |
| 42 | } |
| 43 | return nil |
| 44 | } |
| 45 | |
| 46 | // readFloorGateFile opens path once and reads it from that descriptor, |
| 47 | // refusing anything checkFloorGateStat does. With pinDirs every directory |
| 48 | // on the path is opened without following a symlink too. |
| 49 | func readFloorGateFile(path string, owners []uint32, pinDirs bool) ([]byte, error) { |
| 50 | const flags = unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_NONBLOCK | unix.O_NOCTTY | unix.O_CLOEXEC |
| 51 | var fd int |
| 52 | var err error |
| 53 | if pinDirs { |
| 54 | dir, derr := safefile.OpenDir(filepath.Dir(path)) |
| 55 | if derr != nil { |
| 56 | return nil, derr |
| 57 | } |
| 58 | fd, err = unix.Openat(int(dir.Fd()), filepath.Base(path), flags, 0) |
| 59 | dir.Close() |
| 60 | } else { |
| 61 | fd, err = unix.Open(path, flags, 0) |
| 62 | } |
| 63 | if err != nil { |
| 64 | return nil, &os.PathError{Op: "open", Path: path, Err: err} |
| 65 | } |
| 66 | f := os.NewFile(uintptr(fd), path) |
| 67 | defer f.Close() |
| 68 | var st unix.Stat_t |
| 69 | if err := unix.Fstat(fd, &st); err != nil { |
| 70 | return nil, &os.PathError{Op: "fstat", Path: path, Err: err} |
| 71 | } |
| 72 | if err := checkFloorGateStat(floorGateStat{mode: st.Mode, uid: st.Uid, nlink: uint64(st.Nlink), size: st.Size}, owners); err != nil { |
| 73 | return nil, floorGateUnsafe(path) |
| 74 | } |
| 75 | floorGateAfterStat(path) |
| 76 | // Read one byte past the limit, so a file that grew after the fstat is |
| 77 | // refused rather than cut short into a match. |
| 78 | b, err := io.ReadAll(io.LimitReader(f, floorGateMaxBytes+1)) |
| 79 | if err != nil { |
| 80 | return nil, &os.PathError{Op: "read", Path: path, Err: err} |
| 81 | } |
| 82 | if len(b) > floorGateMaxBytes { |
| 83 | return nil, floorGateUnsafe(path) |
| 84 | } |
| 85 | return b, nil |
| 86 | } |
| 87 | |
| 88 | // floorGateAfterStat runs between a file's fstat and its read; a test sets |
| 89 | // it to grow the file in that window. |
| 90 | var floorGateAfterStat = func(path string) {} |
| 91 | |
| 92 | // floorGateStat is what checkFloorGateStat judges, from an fstat. |
| 93 | type floorGateStat struct { |
| 94 | mode uint32 |
| 95 | uid uint32 |
| 96 | nlink uint64 |
| 97 | size int64 |
| 98 | } |
| 99 | |
| 100 | // checkFloorGateStat refuses all but a regular file with one link, at most |
| 101 | // floorGateMaxBytes long, owned by one of owners. |
| 102 | func checkFloorGateStat(st floorGateStat, owners []uint32) error { |
| 103 | if st.mode&unix.S_IFMT != unix.S_IFREG || st.nlink != 1 || |
| 104 | st.size < 0 || st.size > floorGateMaxBytes || !slices.Contains(owners, st.uid) { |
| 105 | return errFloorGateUnsafe |
| 106 | } |
| 107 | return nil |
| 108 | } |
| 109 | |