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 host
4
5import (
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.
31func 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.
49func 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.
90var floorGateAfterStat = func(path string) {}
91
92// floorGateStat is what checkFloorGateStat judges, from an fstat.
93type 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.
102func 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