| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "github.com/carlosframework/platform/internal/cgroup" |
| 9 | "golang.org/x/sys/unix" |
| 10 | "net" |
| 11 | "os" |
| 12 | "os/exec" |
| 13 | "path/filepath" |
| 14 | "strconv" |
| 15 | "strings" |
| 16 | "time" |
| 17 | ) |
| 18 | |
| 19 | func filesSeedEdgePID(ctx context.Context) (int, error) { |
| 20 | ctx, cancel := context.WithTimeout(ctx, 3*time.Second) |
| 21 | defer cancel() |
| 22 | b, err := exec.CommandContext(ctx, "systemctl", "show", "-p", "MainPID", "--value", "carlos-edge.service").Output() |
| 23 | if err != nil { |
| 24 | return 0, err |
| 25 | } |
| 26 | pid, err := strconv.Atoi(strings.TrimSpace(string(b))) |
| 27 | if err != nil || pid <= 0 { |
| 28 | return 0, ErrClaimStopIdentity |
| 29 | } |
| 30 | return pid, nil |
| 31 | } |
| 32 | |
| 33 | // Root repeats both the service identity and the kernel process identity. |
| 34 | // Capability supplies no path or PID from the caller and authorizes no write. |
| 35 | func verifyFilesSeedDelegation(ctx context.Context, peer StopPeer) error { |
| 36 | if os.Geteuid() != 0 || peer.UID == 0 || peer.PID <= 0 { |
| 37 | return ErrClaimStopIdentity |
| 38 | } |
| 39 | check := func() error { |
| 40 | pid, err := filesSeedEdgePID(ctx) |
| 41 | if err != nil { |
| 42 | return err |
| 43 | } |
| 44 | if pid != peer.PID { |
| 45 | return ErrClaimStopIdentity |
| 46 | } |
| 47 | id, err := (productionStopControllers{}).VerifiedController(ctx, pid) |
| 48 | if err != nil { |
| 49 | return err |
| 50 | } |
| 51 | if id != peer.ControllerID { |
| 52 | return ErrClaimStopIdentity |
| 53 | } |
| 54 | return nil |
| 55 | } |
| 56 | if err := check(); err != nil { |
| 57 | return err |
| 58 | } |
| 59 | root, err := agentUnitCgroup(ctx) |
| 60 | if err != nil { |
| 61 | return err |
| 62 | } |
| 63 | rel, err := filepath.Rel("/sys/fs/cgroup", root) |
| 64 | if err != nil || !filepath.IsLocal(rel) || rel == "." { |
| 65 | return ErrClaimStopIdentity |
| 66 | } |
| 67 | first, err := inspectClaimContainment(rel) |
| 68 | if err != nil { |
| 69 | return err |
| 70 | } |
| 71 | fd, err := unix.Open(root, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0) |
| 72 | if err != nil { |
| 73 | return err |
| 74 | } |
| 75 | defer unix.Close(fd) |
| 76 | var st unix.Stat_t |
| 77 | if err := unix.Fstat(fd, &st); err != nil { |
| 78 | return err |
| 79 | } |
| 80 | if st.Uid != peer.UID || uint64(st.Dev) != first.Device || st.Ino != first.Inode { |
| 81 | return ErrClaimStopIdentity |
| 82 | } |
| 83 | controls, err := os.ReadFile(filepath.Join(root, "cgroup.subtree_control")) |
| 84 | if err != nil { |
| 85 | return err |
| 86 | } |
| 87 | enabled := map[string]bool{} |
| 88 | for _, c := range strings.Fields(string(controls)) { |
| 89 | enabled[c] = true |
| 90 | } |
| 91 | for _, c := range []string{"cpu", "memory", "io", "pids"} { |
| 92 | if !enabled[c] { |
| 93 | return ErrClaimStopIdentity |
| 94 | } |
| 95 | } |
| 96 | groups, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", peer.PID)) |
| 97 | if err != nil { |
| 98 | return err |
| 99 | } |
| 100 | if strings.TrimSpace(string(groups)) != "0::/"+filepath.ToSlash(filepath.Join(rel, "supervise")) { |
| 101 | return ErrClaimStopIdentity |
| 102 | } |
| 103 | second, err := inspectClaimContainment(rel) |
| 104 | if err != nil { |
| 105 | return err |
| 106 | } |
| 107 | if first != second { |
| 108 | return ErrClaimStopIdentity |
| 109 | } |
| 110 | if err := check(); err != nil { |
| 111 | return err |
| 112 | } |
| 113 | return ctx.Err() |
| 114 | } |
| 115 | |
| 116 | func filesSeedManager(ctx context.Context) (*cgroup.Manager, error) { |
| 117 | root, err := agentUnitCgroup(ctx) |
| 118 | if err != nil { |
| 119 | return nil, errFilesSeedDelegation |
| 120 | } |
| 121 | body, err := os.ReadFile("/proc/self/cgroup") |
| 122 | if err != nil { |
| 123 | return nil, err |
| 124 | } |
| 125 | rel, err := filepath.Rel("/sys/fs/cgroup", root) |
| 126 | if err != nil || !filepath.IsLocal(rel) || rel == "." { |
| 127 | return nil, errFilesSeedDelegation |
| 128 | } |
| 129 | if strings.TrimSpace(string(body)) != "0::/"+filepath.ToSlash(rel) { |
| 130 | return nil, errFilesSeedDelegation |
| 131 | } |
| 132 | return cgroup.SetupAtRetaining(root, func(string, ...any) {}) |
| 133 | } |
| 134 | |
| 135 | // The connection supplies the PID. pidfd pins that process while two complete |
| 136 | // observations detect exit, reuse, cgroup movement and an edge restart. |
| 137 | func authenticateFilesSeedPeer(ctx context.Context, conn *net.UnixConn, uid int, server bool) error { |
| 138 | raw, err := conn.SyscallConn() |
| 139 | if err != nil { |
| 140 | return err |
| 141 | } |
| 142 | var peer *unix.Ucred |
| 143 | var peerErr error |
| 144 | if err := raw.Control(func(fd uintptr) { peer, peerErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil { |
| 145 | return err |
| 146 | } |
| 147 | if peerErr != nil { |
| 148 | return peerErr |
| 149 | } |
| 150 | if peer == nil || peer.Pid <= 1 || peer.Uid != uint32(uid) || uid == 0 { |
| 151 | return ErrClaimStopIdentity |
| 152 | } |
| 153 | fd, err := unix.PidfdOpen(int(peer.Pid), 0) |
| 154 | if err != nil { |
| 155 | return err |
| 156 | } |
| 157 | defer unix.Close(fd) |
| 158 | first, err := filesSeedPeerObservation(ctx, int(peer.Pid), server) |
| 159 | if err != nil { |
| 160 | return err |
| 161 | } |
| 162 | second, err := filesSeedPeerObservation(ctx, int(peer.Pid), server) |
| 163 | if err != nil { |
| 164 | return err |
| 165 | } |
| 166 | if first != second { |
| 167 | return ErrClaimStopIdentity |
| 168 | } |
| 169 | if err := unix.PidfdSendSignal(fd, 0, nil, 0); err != nil { |
| 170 | return err |
| 171 | } |
| 172 | return ctx.Err() |
| 173 | } |
| 174 | |
| 175 | func filesSeedPeerObservation(ctx context.Context, pid int, server bool) (string, error) { |
| 176 | edge, err := filesSeedEdgePID(ctx) |
| 177 | if err != nil { |
| 178 | return "", err |
| 179 | } |
| 180 | if server && edge != pid || !server && edge != os.Getpid() { |
| 181 | return "", ErrClaimStopIdentity |
| 182 | } |
| 183 | controller, err := (productionStopControllers{}).VerifiedController(ctx, edge) |
| 184 | if err != nil { |
| 185 | return "", err |
| 186 | } |
| 187 | root, err := agentUnitCgroup(ctx) |
| 188 | if err != nil { |
| 189 | return "", err |
| 190 | } |
| 191 | rel, err := filepath.Rel("/sys/fs/cgroup", root) |
| 192 | if err != nil || !filepath.IsLocal(rel) || rel == "." { |
| 193 | return "", ErrClaimStopIdentity |
| 194 | } |
| 195 | raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) |
| 196 | if err != nil { |
| 197 | return "", err |
| 198 | } |
| 199 | end := strings.LastIndexByte(string(raw), ')') |
| 200 | if end < 0 { |
| 201 | return "", ErrClaimStopIdentity |
| 202 | } |
| 203 | fields := strings.Fields(string(raw)[end+1:]) |
| 204 | if len(fields) < 20 || fields[0] == "Z" { |
| 205 | return "", ErrClaimStopIdentity |
| 206 | } |
| 207 | groups, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pid)) |
| 208 | if err != nil { |
| 209 | return "", err |
| 210 | } |
| 211 | group := strings.TrimSpace(string(groups)) |
| 212 | if !strings.HasPrefix(group, "0::/") || strings.Contains(group, "\n") { |
| 213 | return "", ErrClaimStopIdentity |
| 214 | } |
| 215 | member := strings.TrimPrefix(group, "0::") |
| 216 | unit := "/" + filepath.ToSlash(rel) |
| 217 | if server { |
| 218 | if member != unit+"/supervise" { |
| 219 | return "", ErrClaimStopIdentity |
| 220 | } |
| 221 | } else if member == unit || strings.HasPrefix(member, unit+"/") { |
| 222 | return "", ErrClaimStopIdentity |
| 223 | } |
| 224 | return fmt.Sprintf("%d:%s:%s:%s:%s", edge, controller, root, fields[19], group), nil |
| 225 | } |
| 226 | |