carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package cgroup
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "os"
8 "strconv"
9 "strings"
10 "sync/atomic"
11 "time"
12)
13
14// ShimVerb is the carlos binary's hidden placement verb (Paul's ruling,
15// #272, 2026-10-13). carlos-edge.service's RestrictNamespaces=yes makes
16// systemd's seccomp filter answer clone3 with ENOSYS, so no child can be
17// created inside its cgroup. Instead the child starts as this binary,
18// writes its own PID into the target cgroup through a directory fd the
19// parent validated, checks where it landed, and only then execs the
20// tenant: the tenant's code never runs outside its limits.
21const ShimVerb = "__cgexec"
22
23// ProbeVerb prints the process's own cgroup and exits: the tenant the
24// arm-time placement check (VerifyPlacement) execs through the shim.
25const ProbeVerb = "__cgprobe"
26
27var shimHooked atomic.Bool
28
29// shimForcedForTest makes shimAvailable answer true while
30// FakePlacementForTest has swapped the placement for one that runs no shim.
31// startViaShim itself still asks shimHooked, so the override can never make
32// a test binary without the hook start itself as a shim.
33var shimForcedForTest atomic.Bool
34
35// RunShimIfRequested must be the first statement of main(), and of TestMain
36// in a test binary that places processes. It marks this process as able to
37// serve the shim (shimAvailable), and when this process IS a shim or a
38// probe it runs it and exits; it returns only when it is neither.
39func RunShimIfRequested() {
40 shimHooked.Store(true)
41 if len(os.Args) < 2 {
42 return
43 }
44 switch os.Args[1] {
45 case ShimVerb:
46 os.Exit(runShim(os.Args[2:]))
47 case ProbeVerb:
48 os.Exit(runProbe())
49 }
50}
51
52// placementError is a start whose process could not be placed in its
53// cgroup before exec. Nothing of the tenant ran.
54type placementError struct{ err error }
55
56func (e *placementError) Error() string { return "placement before exec: " + e.err.Error() }
57func (e *placementError) Unwrap() error { return e.err }
58
59func isPlacementError(err error) bool {
60 var p *placementError
61 return errors.As(err, &p)
62}
63
64// ErrPlacementInFlight is a cleanup asked for members while a process is
65// being placed in its cgroup (Task 2).
66var ErrPlacementInFlight = errors.New("cgroup: a process is being placed in this cgroup")
67
68// shimDeadline bounds each of the parent's two waits on the shim: start to
69// placement, and release to exec (Ruling 9).
70var shimDeadline = 10 * time.Second
71
72// shimExe is what the parent starts as the shim: the running build, even
73// after a roll has replaced the file on disk.
74var shimExe = "/proc/self/exe"
75
76// shimTestHold is copied into each spec's TestHold. Only test files set it,
77// and only a test binary's shim honours it.
78var shimTestHold int
79
80// shimAfterRelease, when set, runs between the release byte and the read of
81// the exec status. Only test files set it.
82var shimAfterRelease func()
83
84// shimSpec is what the parent sends the shim over the spec pipe: never in
85// the shim's argv or environment, so no secret shows in /proc/<pid>/cmdline
86// and no tenant variable shapes the shim before placement.
87type shimSpec struct {
88 Path string
89 Dir string // the tenant's cmd.Dir; the shim changes to it after placement
90 Argv []string
91 Env []string
92 Expected string // "/..." as /proc/self/cgroup shows it after "0::"
93 KeepFDs int // fds 0..KeepFDs-1 are the tenant's
94 TestHold int // test binaries only: 0 none, 1 hold before step 4, 2 hold after step 10 (before R)
95 // SigIgn is the set of signals the agent ignored when it started the
96 // shim, as /proc/self/status's SigIgn shows it: bit n-1 is signal n.
97 // A direct start's child keeps exactly these ignored through its exec
98 // (the runtime resets only the signals it handles, clearSignalHandlers),
99 // but the shim's own Go runtime replaces most inherited ignores with its
100 // handlers, which the tenant's execve then resets to default. So the
101 // shim ignores these again immediately before it execs (whole-branch
102 // review, finding 2).
103 SigIgn uint64
104}
105
106const (
107 shimSpecMagic = "carlos-cgexec-2"
108 // shimSpecMax bounds the encoded spec the shim will read.
109 shimSpecMax = 16 << 20
110 // shimSpecMaxCount bounds each decoded count (argv, env, KeepFDs).
111 shimSpecMaxCount = 1 << 20
112)
113
114// encodeShimSpec writes each field followed by a NUL, in order: the magic,
115// Path, Dir, Expected, KeepFDs, TestHold, SigIgn, len(Argv), each argv element,
116// len(Env), each env entry. Bytes pass through untouched (invalid UTF-8
117// included); a string holding a NUL is refused, as execve could not carry
118// it either.
119func encodeShimSpec(s shimSpec) ([]byte, error) {
120 var b []byte
121 put := func(field, v string) error {
122 if strings.IndexByte(v, 0) >= 0 {
123 return fmt.Errorf("cgroup: shim spec: %s contains a NUL byte", field)
124 }
125 b = append(b, v...)
126 b = append(b, 0)
127 return nil
128 }
129 if err := put("magic", shimSpecMagic); err != nil {
130 return nil, err
131 }
132 for _, f := range []struct{ name, v string }{
133 {"path", s.Path}, {"dir", s.Dir}, {"expected", s.Expected},
134 {"keep", strconv.Itoa(s.KeepFDs)}, {"hold", strconv.Itoa(s.TestHold)},
135 {"sigign", strconv.FormatUint(s.SigIgn, 10)},
136 {"argc", strconv.Itoa(len(s.Argv))},
137 } {
138 if err := put(f.name, f.v); err != nil {
139 return nil, err
140 }
141 }
142 for _, a := range s.Argv {
143 if err := put("argv", a); err != nil {
144 return nil, err
145 }
146 }
147 if err := put("envc", strconv.Itoa(len(s.Env))); err != nil {
148 return nil, err
149 }
150 for _, e := range s.Env {
151 if err := put("env", e); err != nil {
152 return nil, err
153 }
154 }
155 return b, nil
156}
157
158// decodeShimSpec is encodeShimSpec's inverse. It refuses a wrong magic, a
159// buffer that ends early or runs past the last field, and a count above
160// shimSpecMaxCount.
161func decodeShimSpec(b []byte) (shimSpec, error) {
162 var s shimSpec
163 next := func() (string, error) {
164 i := bytes.IndexByte(b, 0)
165 if i < 0 {
166 return "", errors.New("cgroup: shim spec: truncated")
167 }
168 v := string(b[:i])
169 b = b[i+1:]
170 return v, nil
171 }
172 count := func() (int, error) {
173 v, err := next()
174 if err != nil {
175 return 0, err
176 }
177 n, err := strconv.Atoi(v)
178 if err != nil || n < 0 || n > shimSpecMaxCount {
179 return 0, fmt.Errorf("cgroup: shim spec: bad count %q", v)
180 }
181 return n, nil
182 }
183 magic, err := next()
184 if err != nil {
185 return s, err
186 }
187 if magic != shimSpecMagic {
188 return s, fmt.Errorf("cgroup: shim spec: magic %q, want %q", magic, shimSpecMagic)
189 }
190 for _, p := range []*string{&s.Path, &s.Dir, &s.Expected} {
191 if *p, err = next(); err != nil {
192 return s, err
193 }
194 }
195 if s.KeepFDs, err = count(); err != nil {
196 return s, err
197 }
198 if s.TestHold, err = count(); err != nil {
199 return s, err
200 }
201 sigign, err := next()
202 if err != nil {
203 return s, err
204 }
205 if s.SigIgn, err = strconv.ParseUint(sigign, 10, 64); err != nil {
206 return s, fmt.Errorf("cgroup: shim spec: bad ignored-signal set %q", sigign)
207 }
208 argc, err := count()
209 if err != nil {
210 return s, err
211 }
212 s.Argv = []string{}
213 for range argc {
214 a, err := next()
215 if err != nil {
216 return s, err
217 }
218 s.Argv = append(s.Argv, a)
219 }
220 envc, err := count()
221 if err != nil {
222 return s, err
223 }
224 s.Env = []string{}
225 for range envc {
226 e, err := next()
227 if err != nil {
228 return s, err
229 }
230 s.Env = append(s.Env, e)
231 }
232 if len(b) != 0 {
233 return s, fmt.Errorf("cgroup: shim spec: %d trailing bytes", len(b))
234 }
235 return s, nil
236}
237