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 cgroup
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "io"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "sync"
15 "sync/atomic"
16 "syscall"
17 "time"
18
19 "github.com/carlosframework/platform/internal/controlname"
20 "golang.org/x/sys/unix"
21)
22
23type strictIdentity struct{ device, inode uint64 }
24
25var strictReservations = struct {
26 sync.Mutex
27 held map[strictIdentity]bool
28}{held: make(map[strictIdentity]bool)}
29
30// Prepared pins one empty host cgroup. It grants no lease, fence or database
31// authority. The caller must also obtain Cmd.Wait evidence after cleanup.
32type Prepared struct{ *strictPreparedState }
33
34type strictPreparedState struct {
35 mu sync.Mutex
36 root, dir, kill, events *os.File
37 limits map[string]*os.File
38 expected map[string]string
39 identity strictIdentity
40 entryParent *os.File
41 entryName string
42 parent *Prepared
43 draining atomic.Bool
44 host string
45 attempted, empty, closed bool
46 start func(*exec.Cmd) error
47 // genParent marks a generation parent (PrepareStrictParent): it holds
48 // no process, pins PrepareGen children, and moves its limit only
49 // through SetParentLimit. tier is the tier it was prepared with, or a
50 // generation child's own.
51 genParent bool
52 tier Limit
53}
54
55func strictOpen(dir *os.File, name string, flags int) (*os.File, error) {
56 fd, e := unix.Openat(int(dir.Fd()), name, flags|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
57 if e != nil {
58 return nil, e
59 }
60 return os.NewFile(uintptr(fd), name), nil
61}
62func strictID(f *os.File) (strictIdentity, error) {
63 var st unix.Stat_t
64 if e := unix.Fstat(int(f.Fd()), &st); e != nil {
65 return strictIdentity{}, e
66 }
67 return strictIdentity{uint64(st.Dev), st.Ino}, nil
68}
69func strictRead(f *os.File) (string, error) {
70 if _, e := f.Seek(0, 0); e != nil {
71 return "", e
72 }
73 b, e := io.ReadAll(io.LimitReader(f, 4097))
74 if e != nil || len(b) > 4096 {
75 return "", ErrStrictRefused
76 }
77 return strings.TrimSpace(string(b)), nil
78}
79func strictWrite(f *os.File, value string) error {
80 if _, e := f.Seek(0, 0); e != nil {
81 return e
82 }
83 n, e := f.WriteString(value)
84 if e != nil {
85 return e
86 }
87 if n != len(value) {
88 return io.ErrShortWrite
89 }
90 return nil
91}
92func strictPopulation(f *os.File) (bool, error) {
93 raw, e := strictRead(f)
94 if e != nil {
95 return false, e
96 }
97 seen := false
98 populated := false
99 for _, line := range strings.Split(raw, "\n") {
100 parts := strings.Fields(line)
101 if len(parts) != 2 {
102 return false, ErrStrictRefused
103 }
104 if parts[0] == "populated" {
105 if seen || (parts[1] != "0" && parts[1] != "1") {
106 return false, ErrStrictRefused
107 }
108 seen = true
109 populated = parts[1] == "1"
110 }
111 }
112 if !seen {
113 return false, ErrStrictRefused
114 }
115 return populated, nil
116}
117func (p *Prepared) identityOK() error {
118 if p.closed {
119 return ErrStrictState
120 }
121 id, e := strictID(p.dir)
122 if e != nil || id != p.identity {
123 return ErrStrictRefused
124 }
125 var st unix.Stat_t
126 parent, name := p.root, ExecName(p.host)
127 if p.entryParent != nil {
128 parent, name = p.entryParent, p.entryName
129 }
130 if e = unix.Fstatat(int(parent.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW); e != nil || st.Mode&unix.S_IFMT != unix.S_IFDIR || (strictIdentity{uint64(st.Dev), st.Ino}) != p.identity {
131 return ErrStrictRefused
132 }
133 return nil
134}
135func (p *Prepared) limitsOK() error {
136 for name, want := range p.expected {
137 got, e := strictRead(p.limits[name])
138 if e != nil || got != want {
139 return ErrStrictRefused
140 }
141 }
142 return nil
143}
144func (p *Prepared) closeFiles() {
145 for _, f := range p.limits {
146 f.Close()
147 }
148 for _, f := range []*os.File{p.events, p.kill, p.dir, p.entryParent, p.root} {
149 if f != nil {
150 f.Close()
151 }
152 }
153}
154
155// selfCgroupFile is where the strict preflight reads this process's own
156// cgroup. A variable so a test can place the agent where it needs.
157var selfCgroupFile = "/proc/self/cgroup"
158
159// strictRoot is the host-independent half of PrepareStrict's runtime
160// checks, and the whole of StrictPreflight: a delegated root, this process
161// placed in it or in its supervise child, and the root a real cgroup2
162// directory. It returns the root opened without following a symlink.
163func (m *Manager) strictRoot() (*os.File, error) {
164 if m == nil || m.root == "" {
165 return nil, ErrStrictUnsupported
166 }
167 own, e := os.ReadFile(selfCgroupFile)
168 rel, ok := ParseSelfCgroup(string(own))
169 if e != nil || !ok {
170 return nil, ErrStrictUnsupported
171 }
172 current := filepath.Join("/sys/fs/cgroup", rel)
173 rootPath := filepath.Clean(m.root)
174 if current != rootPath && current != filepath.Join(rootPath, "supervise") {
175 return nil, ErrStrictRefused
176 }
177 root, e := os.OpenFile(rootPath, os.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW, 0)
178 if e != nil {
179 return nil, ErrStrictUnsupported
180 }
181 var fs unix.Statfs_t
182 if e = unix.Fstatfs(int(root.Fd()), &fs); e != nil || uint64(fs.Type) != uint64(unix.CGROUP2_SUPER_MAGIC) {
183 root.Close()
184 return nil, ErrStrictUnsupported
185 }
186 return root, nil
187}
188
189// StrictPreflight reports whether PrepareStrict's runtime checks that do
190// not depend on the host pass now: the same function, not a copy, so a
191// readiness check (moves S2) cannot approve a box PrepareStrict refuses
192// for its placement or its mount. Nil means a strict spawn can be
193// attempted; the per-host state (a populated group, a held reservation)
194// is still PrepareStrict's to judge.
195func (m *Manager) StrictPreflight() error {
196 root, e := m.strictRoot()
197 if e != nil {
198 return e
199 }
200 return root.Close()
201}
202
203// PrepareStrict never uses accounting's fallback. Only the two existing tiers
204// are accepted; a zero or missing limit cannot create an uncapped command.
205func (m *Manager) PrepareStrict(host string, limit Limit) (*Prepared, error) {
206 return m.prepareStrict(host, limit, false)
207}
208
209// prepareStrict is PrepareStrict and PrepareStrictParent: with genParent the
210// group is pinned as a generation parent (memory.oom.group=0, controllers
211// delegated to its children, Start refused) instead of a leaf.
212func (m *Manager) prepareStrict(host string, limit Limit, genParent bool) (out *Prepared, err error) {
213 if m == nil || m.root == "" {
214 return nil, ErrStrictUnsupported
215 }
216 if !controlname.ValidHostShape(host) || (limit != TierDefault && limit != TierChildren) {
217 return nil, ErrStrictRefused
218 }
219 root, e := m.strictRoot()
220 if e != nil {
221 return nil, e
222 }
223 p := &Prepared{strictPreparedState: &strictPreparedState{root: root, host: host, limits: map[string]*os.File{}, start: func(cmd *exec.Cmd) error { return cmd.Start() }}}
224 reserved := false
225 defer func() {
226 if err != nil {
227 p.closeFiles()
228 if reserved {
229 strictReservations.Lock()
230 delete(strictReservations.held, p.identity)
231 strictReservations.Unlock()
232 }
233 }
234 }()
235 // A group that took cgroup.kill is removed and made fresh first (amadan
236 // #272). One a strict handle still holds stays, and the answer is the
237 // reservation's own ErrStrictState.
238 if e = m.freshen(host, m.hostDir(host)); e != nil {
239 if errors.Is(e, ErrStrictState) {
240 return nil, e
241 }
242 return nil, fmt.Errorf("%w: %w", ErrStrictRefused, e)
243 }
244 if e = unix.Mkdirat(int(root.Fd()), ExecName(host), 0755); e != nil && !errors.Is(e, unix.EEXIST) {
245 return nil, ErrStrictRefused
246 }
247 p.dir, e = strictOpen(root, ExecName(host), unix.O_RDONLY|unix.O_DIRECTORY)
248 if e != nil {
249 return nil, ErrStrictRefused
250 }
251 p.identity, e = strictID(p.dir)
252 if e != nil {
253 return nil, ErrStrictRefused
254 }
255 strictReservations.Lock()
256 if strictReservations.held[p.identity] {
257 strictReservations.Unlock()
258 return nil, ErrStrictState
259 }
260 strictReservations.held[p.identity] = true
261 reserved = true
262 strictReservations.Unlock()
263 p.events, e = strictOpen(p.dir, "cgroup.events", unix.O_RDONLY)
264 if e != nil {
265 return nil, ErrStrictUnsupported
266 }
267 p.kill, e = strictOpen(p.dir, "cgroup.kill", unix.O_WRONLY)
268 if e != nil {
269 return nil, ErrStrictUnsupported
270 }
271 if populated, e := strictPopulation(p.events); e != nil || populated {
272 return nil, ErrStrictRefused
273 }
274 oomGroup := "1"
275 if genParent {
276 oomGroup = "0"
277 }
278 p.expected = tierExpected(limit, oomGroup)
279 for name := range p.expected {
280 p.limits[name], e = strictOpen(p.dir, name, unix.O_RDWR)
281 if e != nil {
282 return nil, ErrStrictUnsupported
283 }
284 }
285 for _, name := range []string{"memory.max", "memory.high", "memory.swap.max", "memory.oom.group"} {
286 if e = strictWrite(p.limits[name], p.expected[name]); e != nil {
287 return nil, ErrStrictRefused
288 }
289 }
290 if e = p.limitsOK(); e != nil {
291 return nil, e
292 }
293 if genParent {
294 if e = delegateStrict(p.dir); e != nil {
295 return nil, e
296 }
297 p.genParent, p.tier = true, limit
298 }
299 if e = p.identityOK(); e != nil {
300 return nil, e
301 }
302 return p, nil
303}
304
305// Start consumes one attempt even if exec or containment fails. The caller
306// supplies the complete command, including its approved executable descriptor.
307func (p *Prepared) Start(cmd *exec.Cmd) error {
308 if p == nil || p.strictPreparedState == nil {
309 return ErrStrictState
310 }
311 if p.parent != nil {
312 p.parent.mu.Lock()
313 defer p.parent.mu.Unlock()
314 if p.parent.closed || p.parent.empty || p.parent.draining.Load() {
315 return ErrStrictState
316 }
317 if err := p.parent.identityOK(); err != nil {
318 return err
319 }
320 if err := p.parent.limitsOK(); err != nil {
321 return err
322 }
323 }
324 p.mu.Lock()
325 defer p.mu.Unlock()
326 if p.genParent {
327 // A generation parent holds no process (zero-downtime spec §7.3).
328 return ErrStrictState
329 }
330 if p.closed || p.attempted || p.empty || p.draining.Load() {
331 return ErrStrictState
332 }
333 p.attempted = true
334 if cmd == nil || cmd.Process != nil || cmd.ProcessState != nil {
335 return ErrStrictState
336 }
337 if e := p.identityOK(); e != nil {
338 return e
339 }
340 if e := p.limitsOK(); e != nil {
341 return e
342 }
343 if populated, e := strictPopulation(p.events); e != nil || populated {
344 return ErrStrictRefused
345 }
346 setInto(cmd, int(p.dir.Fd()))
347 cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
348 if e := p.start(cmd); e != nil {
349 if shouldLatch(e) {
350 return ErrStrictUnsupported
351 }
352 return ErrStrictRefused
353 }
354 if cmd.Process == nil {
355 return ErrStrictRefused
356 }
357 return nil
358}
359
360// Populated checks the retained group after the direct child has been reaped.
361func (p *Prepared) Populated() (bool, error) {
362 if p == nil || p.strictPreparedState == nil {
363 return false, ErrStrictState
364 }
365 p.mu.Lock()
366 defer p.mu.Unlock()
367 if e := p.identityOK(); e != nil {
368 return false, e
369 }
370 return strictPopulation(p.events)
371}
372
373// KillAndEmpty proves kernel population is zero before the caller's deadline.
374// Failure retains the pinned cleanup handle and permits another bounded attempt.
375func (p *Prepared) KillAndEmpty(ctx context.Context) error {
376 if p == nil || p.strictPreparedState == nil {
377 return ErrStrictState
378 }
379 p.draining.Store(true)
380 if ctx == nil {
381 return ErrStrictDeadline
382 }
383 deadline, ok := ctx.Deadline()
384 if !ok || ctx.Err() != nil || !time.Now().Before(deadline) {
385 return ErrStrictDeadline
386 }
387 lockTick := time.NewTicker(time.Millisecond)
388 defer lockTick.Stop()
389 for !p.mu.TryLock() {
390 select {
391 case <-ctx.Done():
392 return ErrStrictDeadline
393 case <-lockTick.C:
394 }
395 }
396 defer p.mu.Unlock()
397 if p.closed || p.empty {
398 return ErrStrictState
399 }
400 if ctx.Err() != nil {
401 return ErrStrictDeadline
402 }
403 if e := p.identityOK(); e != nil {
404 return e
405 }
406 p.attempted = true
407 // Marked first, so no later start reuses this directory (amadan #272,
408 // recreate_linux.go).
409 markKilled(p.identity)
410 if e := strictWrite(p.kill, "1"); e != nil {
411 return ErrStrictRefused
412 }
413 timer := time.NewTicker(5 * time.Millisecond)
414 defer timer.Stop()
415 for {
416 if ctx.Err() != nil || !time.Now().Before(deadline) {
417 return ErrStrictDeadline
418 }
419 if e := p.identityOK(); e != nil {
420 return e
421 }
422 populated, e := strictPopulation(p.events)
423 if e != nil {
424 return ErrStrictRefused
425 }
426 if !populated {
427 if ctx.Err() != nil || !time.Now().Before(deadline) {
428 return ErrStrictDeadline
429 }
430 p.empty = true
431 return nil
432 }
433 select {
434 case <-ctx.Done():
435 return ErrStrictDeadline
436 case <-timer.C:
437 }
438 }
439}
440
441// Close never removes a path. A populated or unverifiable group retains its
442// descriptors and reservation so the same handle can still attempt cleanup.
443func (p *Prepared) Close() error {
444 if p == nil || p.strictPreparedState == nil {
445 return ErrStrictState
446 }
447 p.mu.Lock()
448 defer p.mu.Unlock()
449 p.draining.Store(true)
450 if p.closed {
451 return ErrStrictState
452 }
453 if e := p.identityOK(); e != nil {
454 return e
455 }
456 if populated, e := strictPopulation(p.events); e != nil || populated {
457 return ErrStrictRefused
458 }
459 p.closed = true
460 p.closeFiles()
461 strictReservations.Lock()
462 delete(strictReservations.held, p.identity)
463 strictReservations.Unlock()
464 return nil
465}
466