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 "context"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io"
13 "io/fs"
14 "os"
15 "path/filepath"
16 "strconv"
17 "strings"
18
19 "github.com/carlosframework/platform/internal/hostbox"
20 "github.com/carlosframework/platform/internal/release"
21 "github.com/carlosframework/platform/internal/rootstate"
22 "golang.org/x/sys/unix"
23)
24
25// This reader can prove absence only with launch exclusion held by its caller.
26// Uncontained service processes and foreign namespaces remain unavailable until
27// their reference-creation boundary can be established independently.
28type generationKernel struct {
29 proc, cgroups, boundApp string
30 owner uint32
31 contained []release.GenerationReservation
32 service func(string, []byte) (bool, error)
33 // parent, when set, is the pid of an offline root command (journal-migrate)
34 // whose own direct children pass the root-task rule. They still share
35 // its namespaces and pass every reference check below.
36 parent int
37 // recheck holds a task service trusted to the image it was trusted for,
38 // at each use of that trust and, final, at the end of its inspection
39 // (generationServiceEnv.recheck). A trusted task needs it.
40 recheck func(path string, final bool) error
41}
42
43func generationRead(path string) ([]byte, error) {
44 f, err := os.Open(path)
45 if err != nil {
46 return nil, err
47 }
48 defer f.Close()
49 raw, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1))
50 if err != nil {
51 return nil, err
52 }
53 if len(raw) > rootstate.MaxRecord {
54 return nil, os.ErrPermission
55 }
56 return raw, nil
57}
58func generationStart(raw []byte) (string, error) {
59 end := strings.LastIndexByte(string(raw), ')')
60 if end < 0 {
61 return "", os.ErrPermission
62 }
63 fields := strings.Fields(string(raw[end+1:]))
64 if len(fields) < 20 {
65 return "", os.ErrPermission
66 }
67 _, err := strconv.ParseUint(fields[19], 10, 64)
68 if err != nil {
69 return "", os.ErrPermission
70 }
71 return fields[19], nil
72}
73func (k generationKernel) boot() (string, error) {
74 raw, err := generationRead(filepath.Join(k.proc, "sys/kernel/random/boot_id"))
75 if err != nil {
76 return "", err
77 }
78 boot := strings.TrimSpace(string(raw))
79 if boot == "" {
80 return "", os.ErrPermission
81 }
82 return boot, nil
83}
84func (k generationKernel) controllerGone(ctx context.Context, b release.GenerationBinding) error {
85 if err := ctx.Err(); err != nil {
86 return err
87 }
88 boot, err := k.boot()
89 if err != nil {
90 return err
91 }
92 if b.ControllerPID <= 0 || b.ControllerStart == "" || b.Target.BootID == "" {
93 return os.ErrPermission
94 }
95 if boot != b.Target.BootID {
96 return nil
97 }
98 raw, err := generationRead(filepath.Join(k.proc, strconv.Itoa(b.ControllerPID), "stat"))
99 if errors.Is(err, os.ErrNotExist) {
100 return nil
101 }
102 if err != nil {
103 return err
104 }
105 start, err := generationStart(raw)
106 if err != nil {
107 return err
108 }
109 if start == b.ControllerStart {
110 return os.ErrPermission
111 }
112 return nil
113}
114func generationNames(path string) ([]string, error) {
115 f, err := os.Open(path)
116 if err != nil {
117 return nil, err
118 }
119 defer f.Close()
120 names, err := f.Readdirnames(8193)
121 if err != nil && err != io.EOF {
122 return nil, err
123 }
124 if len(names) > 8192 {
125 return nil, os.ErrPermission
126 }
127 return names, nil
128}
129func generationPathRef(path string, b release.GenerationBinding) bool {
130 path = strings.TrimSuffix(path, " (deleted)")
131 return path == b.Path || path == b.Tree || strings.HasPrefix(path, b.Tree+"/")
132}
133func generationIDRef(id rootstate.FileIdentity, b release.GenerationBinding) bool {
134 return id == b.Executable || id == b.TreeIdentity
135}
136func generationLinkRef(path string, b release.GenerationBinding, refs ...map[rootstate.FileIdentity]bool) error {
137 name, err := os.Readlink(path)
138 if err != nil {
139 return err
140 }
141 // A retained namespace descriptor can keep mounts alive after its last task.
142 if strings.HasPrefix(name, "mnt:[") || strings.HasPrefix(name, "pid:[") || generationPathRef(name, b) {
143 return os.ErrPermission
144 }
145 var st unix.Stat_t
146 if err := unix.Stat(path, &st); err != nil {
147 return err
148 }
149 id := rootstate.FileIdentity{Device: uint64(st.Dev), Inode: st.Ino}
150 for _, set := range refs {
151 if set[id] {
152 return os.ErrPermission
153 }
154 }
155 if generationIDRef(id, b) {
156 return os.ErrPermission
157 }
158 return nil
159}
160func generationMaps(raw []byte, b release.GenerationBinding, refs ...map[rootstate.FileIdentity]bool) error {
161 for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
162 if line == "" {
163 continue
164 }
165 fields := strings.Fields(line)
166 if len(fields) < 5 {
167 return os.ErrPermission
168 }
169 major, minor, ok := strings.Cut(fields[3], ":")
170 if !ok {
171 return os.ErrPermission
172 }
173 maj, e1 := strconv.ParseUint(major, 16, 32)
174 min, e2 := strconv.ParseUint(minor, 16, 32)
175 ino, e3 := strconv.ParseUint(fields[4], 10, 64)
176 if e1 != nil || e2 != nil || e3 != nil {
177 return os.ErrPermission
178 }
179 id := rootstate.FileIdentity{Device: unix.Mkdev(uint32(maj), uint32(min)), Inode: ino}
180 for _, set := range refs {
181 if ino != 0 && set[id] {
182 return os.ErrPermission
183 }
184 }
185 if ino != 0 && generationIDRef(rootstate.FileIdentity{Device: unix.Mkdev(uint32(maj), uint32(min)), Inode: ino}, b) {
186 return os.ErrPermission
187 }
188 if len(fields) > 5 && generationPathRef(strings.Join(fields[5:], " "), b) {
189 return os.ErrPermission
190 }
191 }
192 return nil
193}
194func generationUnescape(s string) (string, error) {
195 var out strings.Builder
196 for i := 0; i < len(s); i++ {
197 if s[i] != '\\' {
198 out.WriteByte(s[i])
199 continue
200 }
201 if i+3 >= len(s) {
202 return "", os.ErrPermission
203 }
204 switch s[i : i+4] {
205 case "\\040":
206 out.WriteByte(' ')
207 case "\\011":
208 out.WriteByte('\t')
209 case "\\012":
210 out.WriteByte('\n')
211 case "\\134":
212 out.WriteByte('\\')
213 default:
214 return "", os.ErrPermission
215 }
216 i += 3
217 }
218 return out.String(), nil
219}
220
221type generationMount struct {
222 id, root, point string
223 device uint64
224}
225
226func generationMounts(raw []byte, b release.GenerationBinding, trusted ...bool) error {
227 var mounts []generationMount
228 for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
229 f := strings.Fields(line)
230 if len(f) < 10 {
231 return os.ErrPermission
232 }
233 separator := -1
234 for i := 6; i < len(f); i++ {
235 if f[i] == "-" {
236 separator = i
237 break
238 }
239 }
240 if separator < 6 || len(f) != separator+4 {
241 return os.ErrPermission
242 }
243 // A bind-mounted namespace can retain staged mounts after its last task
244 // and descriptor disappear. Its nsfs device does not identify that tree.
245 if f[separator+1] == "nsfs" {
246 return os.ErrPermission
247 }
248 major, minor, ok := strings.Cut(f[2], ":")
249 if !ok {
250 return os.ErrPermission
251 }
252 maj, e1 := strconv.ParseUint(major, 10, 32)
253 min, e2 := strconv.ParseUint(minor, 10, 32)
254 root, e3 := generationUnescape(f[3])
255 point, e4 := generationUnescape(f[4])
256 if e1 != nil || e2 != nil || e3 != nil || e4 != nil || !filepath.IsAbs(root) || !filepath.IsAbs(point) {
257 return os.ErrPermission
258 }
259 if generationPathRef(root, b) || generationPathRef(point, b) {
260 return os.ErrPermission
261 }
262 mounts = append(mounts, generationMount{f[0], root, point, unix.Mkdev(uint32(maj), uint32(min))})
263 }
264 // Translate the canonical tree into the containing filesystem's namespace.
265 // Every additional mount of that tree, its contents or an ancestor keeps it.
266 var base generationMount
267 for _, m := range mounts {
268 rel, err := filepath.Rel(m.point, b.Tree)
269 if err == nil && filepath.IsLocal(rel) && len(m.point) > len(base.point) {
270 base = m
271 }
272 }
273 if base.id == "" || base.device != b.TreeIdentity.Device {
274 return os.ErrPermission
275 }
276 rel, err := filepath.Rel(base.point, b.Tree)
277 if err != nil {
278 return err
279 }
280 tree := filepath.Join(base.root, rel)
281 for _, m := range mounts {
282 if m.id == base.id || m.device != base.device {
283 continue
284 }
285 below, e1 := filepath.Rel(tree, m.root)
286 above, e2 := filepath.Rel(m.root, tree)
287 if (e1 == nil && filepath.IsLocal(below)) || (e2 == nil && filepath.IsLocal(above) && !(len(trusted) != 0 && trusted[0])) {
288 return os.ErrPermission
289 }
290 }
291 return nil
292}
293func generationRootTask(raw []byte) (bool, error) {
294 uid, kernel := false, false
295 for _, line := range strings.Split(string(raw), "\n") {
296 f := strings.Fields(line)
297 if len(f) == 0 {
298 continue
299 }
300 switch f[0] {
301 case "Uid:":
302 if uid || len(f) != 5 {
303 return false, os.ErrPermission
304 }
305 uid = true
306 for _, id := range f[1:] {
307 if id != "0" {
308 return false, os.ErrPermission
309 }
310 }
311 case "Kthread:":
312 if len(f) != 2 || (f[1] != "0" && f[1] != "1") {
313 return false, os.ErrPermission
314 }
315 kernel = f[1] == "1"
316 }
317 }
318 if !uid {
319 return false, os.ErrPermission
320 }
321 return kernel, nil
322}
323
324func generationTaskFDs(ctx context.Context, path string, b release.GenerationBinding, refs map[rootstate.FileIdentity]bool) error {
325 f, err := os.Open(path)
326 if err != nil {
327 return err
328 }
329 // Our own descriptor appears when scanning this process. Keep it open
330 // until inspection finishes, so it cannot manufacture a missing entry.
331 defer f.Close()
332 names, err := f.Readdirnames(8193)
333 if err != nil && err != io.EOF {
334 return err
335 }
336 if len(names) > 8192 {
337 return os.ErrPermission
338 }
339 for _, name := range names {
340 if err := ctx.Err(); err != nil {
341 return err
342 }
343 if err := generationLinkRef(filepath.Join(path, name), b, refs); err != nil {
344 return fmt.Errorf("%s: %w", name, err)
345 }
346 }
347 return nil
348}
349
350func (k generationKernel) task(ctx context.Context, path string, b release.GenerationBinding, ns map[string]string, refs map[rootstate.FileIdentity]bool) (string, error) {
351 if err := ctx.Err(); err != nil {
352 return "", err
353 }
354 raw, err := generationRead(path + "/stat")
355 if err != nil {
356 return "", err
357 }
358 start, err := generationStart(raw)
359 if err != nil {
360 return "", err
361 }
362 status, err := generationRead(path + "/status")
363 if err != nil {
364 return "", err
365 }
366 if len(k.contained) != 0 {
367 ok, err := k.containedTask(ctx, path, status)
368 if err != nil {
369 return "", err
370 }
371 if ok {
372 again, err := generationRead(path + "/stat")
373 if err != nil {
374 return "", err
375 }
376 end, err := generationStart(again)
377 if err != nil {
378 return "", os.ErrPermission
379 }
380 if end != start {
381 return "", errGenerationTaskReplaced
382 }
383 return start, nil
384 }
385 }
386 kernel, rootErr := generationRootTask(status)
387 if rootErr != nil && !kernel && k.parent != 0 {
388 if ppid, err := procStatParent(raw); err == nil && ppid == k.parent {
389 rootErr = nil
390 }
391 }
392 trusted := false
393 if k.service != nil && !kernel {
394 trusted, err = k.service(path, status)
395 if err != nil {
396 trusted = false
397 if rootErr != nil {
398 return "", fmt.Errorf("service: %w", err)
399 }
400 }
401 }
402 if rootErr != nil && !trusted {
403 return "", rootErr
404 }
405 bound := func(final bool) error {
406 if !trusted {
407 return nil
408 }
409 if k.recheck == nil {
410 return fmt.Errorf("service: no executable binding: %w", os.ErrPermission)
411 }
412 if err := k.recheck(path, final); err != nil {
413 return fmt.Errorf("service: %w", err)
414 }
415 return nil
416 }
417 if trusted && k.recheck != nil {
418 defer k.recheck(path, true)
419 }
420 if err := bound(false); err != nil {
421 return "", err
422 }
423 if !kernel {
424 for name, want := range ns {
425 got, err := os.Readlink(path + "/ns/" + name)
426 if err != nil {
427 return "", err
428 }
429 if got != want && !trusted {
430 return "", os.ErrPermission
431 }
432 }
433 for _, name := range []string{"exe", "cwd", "root"} {
434 if err := generationLinkRef(path+"/"+name, b, refs); err != nil {
435 return "", fmt.Errorf("%s: %w", name, err)
436 }
437 }
438 if err := generationTaskFDs(ctx, path+"/fd", b, refs); err != nil {
439 return "", fmt.Errorf("fd: %w", err)
440 }
441 maps, err := generationRead(path + "/maps")
442 if err != nil {
443 return "", err
444 }
445 if err := generationMaps(maps, b, refs); err != nil {
446 return "", err
447 }
448 mounts, err := generationRead(path + "/mountinfo")
449 if err != nil {
450 return "", err
451 }
452 if err := bound(false); err != nil {
453 return "", err
454 }
455 if err := generationMounts(mounts, b, trusted); err != nil {
456 return "", fmt.Errorf("mounts: %w", err)
457 }
458 }
459 raw, err = generationRead(path + "/stat")
460 if err != nil {
461 return "", err
462 }
463 again, err := generationStart(raw)
464 if err != nil {
465 return "", os.ErrPermission
466 }
467 if again != start {
468 return "", errGenerationTaskReplaced
469 }
470 if err := bound(true); err != nil {
471 return "", err
472 }
473 return start, nil
474}
475func (k generationKernel) absent(ctx context.Context, r release.GenerationReservation) (string, error) {
476 if err := ctx.Err(); err != nil {
477 return "", err
478 }
479 boot, err := k.boot()
480 if err != nil {
481 return "", err
482 }
483 ns := map[string]string{}
484 for _, name := range []string{"mnt", "pid"} {
485 ns[name], err = os.Readlink(filepath.Join(k.proc, "self/ns", name))
486 if err != nil || ns[name] == "" {
487 return "", os.ErrPermission
488 }
489 }
490 if r.Binding.TreeProof.Boot != "" {
491 current, err := generationCurrentFiles(r.Binding, rootstate.OpenTrustedFile)
492 if err != nil {
493 return "", err
494 }
495 r.Binding = current
496 }
497 refs, err := generationTree(ctx, r.Binding)
498 if err != nil {
499 return "", err
500 }
501 names, err := generationNames(k.proc)
502 if err != nil {
503 return "", err
504 }
505 observations := map[string]string{}
506 count := 0
507 for _, name := range names {
508 pid, err := strconv.ParseUint(name, 10, 32)
509 if err != nil {
510 continue
511 }
512 if pid == 0 {
513 return "", os.ErrPermission
514 }
515 tasks, err := generationNames(filepath.Join(k.proc, name, "task"))
516 if err != nil {
517 return "", &generationTaskRefusal{task: filepath.Join(k.proc, name), err: err}
518 }
519 if len(tasks) == 0 {
520 return "", os.ErrPermission
521 }
522 for _, task := range tasks {
523 if id, err := strconv.ParseUint(task, 10, 32); err != nil || id == 0 {
524 return "", os.ErrPermission
525 }
526 count++
527 if count > 8192 {
528 return "", os.ErrPermission
529 }
530 start, err := k.task(ctx, filepath.Join(k.proc, name, "task", task), r.Binding, ns, refs)
531 if err != nil {
532 return "", &generationTaskRefusal{task: filepath.Join(k.proc, name, "task", task), err: err}
533 }
534 observations[name+"/"+task] = start
535 }
536 }
537 if count == 0 {
538 return "", os.ErrPermission
539 }
540 again, err := k.boot()
541 if err != nil || boot != again {
542 return "", os.ErrPermission
543 }
544 raw, err := json.Marshal([]any{1, r, boot, ns, observations})
545 if err != nil {
546 return "", err
547 }
548 sum := sha256.Sum256(raw)
549 return hex.EncodeToString(sum[:]), nil
550}
551
552func productionGenerationAbsence(ctx context.Context, r release.GenerationReservation) (string, error) {
553 if os.Geteuid() != 0 {
554 return "", os.ErrPermission
555 }
556 if err := verifyStopKernelMounts("/sys/fs/cgroup", "/proc"); err != nil {
557 return "", err
558 }
559 if r.Binding.Path != release.VersionPath(release.OptDir(), r.Binding.Binding.Account, r.Binding.Version, r.Binding.Binding.App) {
560 return "", os.ErrPermission
561 }
562 current, err := generationRecordedAbsence(ctx, r)
563 if err != nil {
564 return "", err
565 }
566 service, recheck := generationServiceReader(ctx)
567 return (generationKernel{proc: "/proc", service: service, recheck: recheck}).absent(ctx, current)
568}
569
570func generationRecordedAbsence(ctx context.Context, r release.GenerationReservation) (release.GenerationReservation, error) {
571 current, err := generationRecordedTargets(ctx, r)
572 if err != nil {
573 return r, err
574 }
575 return current, (generationKernel{proc: "/proc"}).controllerGone(ctx, current.Binding)
576}
577
578func generationRecordedTargets(ctx context.Context, r release.GenerationReservation) (release.GenerationReservation, error) {
579 current, err := generationCurrentFiles(r.Binding, rootstate.OpenTrustedFile)
580 if err != nil {
581 return r, err
582 }
583 r.Binding = current
584 if err := generationGroupGone(ctx, "/sys/fs/cgroup", r); err != nil {
585 return r, err
586 }
587 k := generationKernel{proc: "/proc"}
588 // Missing cgroups alone never discharge a reservation. With its launch
589 // authority closed, both recorded process identities must also be gone.
590 for _, p := range []struct {
591 pid int
592 start string
593 }{{r.Process.PID, r.Process.Start}, {r.Process.TenantPID, r.Process.TenantStart}} {
594 if p.pid == 0 {
595 continue
596 }
597 b := r.Binding
598 b.ControllerPID, b.ControllerStart = p.pid, p.start
599 if err := k.controllerGone(ctx, b); err != nil {
600 return r, err
601 }
602 }
603 return r, nil
604}
605
606func generationGroupGone(ctx context.Context, root string, r release.GenerationReservation) error {
607 group := generationBindingGroup(r.Binding)
608 path := filepath.Join(root, group)
609 if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
610 return nil
611 } else if err != nil {
612 return err
613 }
614 if r.Process == (release.GenerationProcess{}) || r.Process.Group != group {
615 return os.ErrPermission
616 }
617 boot, err := (generationKernel{proc: "/proc"}).boot()
618 if err != nil {
619 return err
620 }
621 if r.Binding.Target.BootID == "" {
622 return os.ErrPermission
623 }
624 records, _ := ctx.Value(generationCensusReservationsKey{}).([]release.GenerationReservation)
625 k := generationKernel{proc: "/proc", cgroups: root, contained: records}
626 if k.replacedGroup(ctx, r) == nil {
627 return nil
628 }
629 if r.Binding.Target.BootID != boot {
630 _, err := scanRebootCgroups(ctx, root, []hostbox.StopCgroupIdentity{{RelativePath: group, Device: r.Process.GroupIdentity.Device, Inode: r.Process.GroupIdentity.Inode}}, r.Binding.Target.BootID, boot)
631 return err
632 }
633 _, err = scanStopCgroups(ctx, root, []hostbox.StopCgroupIdentity{{RelativePath: group, Device: r.Process.GroupIdentity.Device, Inode: r.Process.GroupIdentity.Inode}})
634 return err
635}
636
637func generationCurrentFiles(b release.GenerationBinding, open func(string) (*os.File, error)) (release.GenerationBinding, error) {
638 f, err := open(b.Path)
639 if err != nil {
640 return b, err
641 }
642 defer f.Close()
643 d, err := os.Open(b.Tree)
644 if err != nil {
645 return b, err
646 }
647 defer d.Close()
648 for _, item := range []struct {
649 file *os.File
650 path string
651 identity *rootstate.FileIdentity
652 proof rootstate.FileIdentity
653 }{
654 {f, b.Path, &b.Executable, b.ExecutableProof}, {d, b.Tree, &b.TreeIdentity, b.TreeProof},
655 } {
656 current, err := rootstate.Identity(item.file)
657 if err != nil {
658 return b, err
659 }
660 if item.proof.Boot == "" {
661 if current != *item.identity {
662 return b, os.ErrPermission
663 }
664 {
665 boot, err := (generationKernel{proc: "/proc"}).boot()
666 if err != nil || boot != b.Target.BootID {
667 return b, os.ErrPermission
668 }
669 }
670 } else {
671 observed, err := rootstate.PersistentIdentity(item.file)
672 if err != nil || !generationFileProof(item.path, item.proof, observed) {
673 return b, os.ErrPermission
674 }
675 }
676 *item.identity = current
677 }
678 return b, nil
679}
680
681func generationTree(ctx context.Context, b release.GenerationBinding) (map[rootstate.FileIdentity]bool, error) {
682 refs := map[rootstate.FileIdentity]bool{b.Executable: true, b.TreeIdentity: true}
683 count := 0
684 err := filepath.WalkDir(b.Tree, func(path string, entry fs.DirEntry, walkErr error) error {
685 if walkErr != nil {
686 return walkErr
687 }
688 if err := ctx.Err(); err != nil {
689 return err
690 }
691 count++
692 if count > 8192 || entry.Type()&os.ModeSymlink != 0 {
693 return os.ErrPermission
694 }
695 var st unix.Stat_t
696 if err := unix.Lstat(path, &st); err != nil {
697 return err
698 }
699 if st.Mode&unix.S_IFMT != unix.S_IFDIR && st.Mode&unix.S_IFMT != unix.S_IFREG {
700 return os.ErrPermission
701 }
702 id := rootstate.FileIdentity{Device: uint64(st.Dev), Inode: st.Ino}
703 if path == b.Tree && id != b.TreeIdentity {
704 return os.ErrPermission
705 }
706 refs[id] = true
707 return nil
708 })
709 return refs, err
710}
711
712func generationFileProof(path string, before, after rootstate.FileIdentity) bool {
713 return release.GenerationFileProof(path, before, after)
714}
715