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 instanceexec
4
5import (
6 "errors"
7 "fmt"
8 "net"
9 "os"
10 "runtime"
11 "strings"
12 "syscall"
13 "unsafe"
14
15 "golang.org/x/sys/unix"
16)
17
18// landlockScopeABI is the first Landlock ABI with scopes (Linux 6.12).
19const landlockScopeABI = 6
20
21// ScopeAbstractUnix confines the CALLING THREAD, and everything it execs,
22// to abstract unix sockets bound inside its own Landlock domain (#178).
23// Abstract names live in the network namespace, which every instance
24// shares with the box, and RestrictAddressFamilies= cannot tell them from
25// path sockets; LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET can. After this, a
26// connect to a name a sibling bound answers EPERM. The app's own
27// processes, all descended from the restricted thread, still reach each
28// other's.
29//
30// Landlock restricts a thread, not a process, so the caller must hold
31// runtime.LockOSThread and then exec, fork (the child inherits the
32// domain), or let the thread die locked.
33// Nothing here needs privilege: no_new_privs (which the unit already sets
34// with NoNewPrivileges=) is the only precondition.
35func ScopeAbstractUnix() error {
36 abi, _, e := unix.Syscall(unix.SYS_LANDLOCK_CREATE_RULESET, 0, 0, unix.LANDLOCK_CREATE_RULESET_VERSION)
37 if e != 0 {
38 return fmt.Errorf("landlock unavailable: %w", e)
39 }
40 if abi < landlockScopeABI {
41 return fmt.Errorf("landlock ABI %d cannot scope abstract unix sockets (needs %d, Linux 6.12)", abi, landlockScopeABI)
42 }
43 attr := unix.LandlockRulesetAttr{Scoped: unix.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET}
44 fd, _, e := unix.Syscall(unix.SYS_LANDLOCK_CREATE_RULESET, uintptr(unsafe.Pointer(&attr)), unsafe.Sizeof(attr), 0)
45 if e != 0 {
46 return fmt.Errorf("landlock ruleset: %w", e)
47 }
48 defer unix.Close(int(fd))
49 if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
50 return fmt.Errorf("no_new_privs: %w", err)
51 }
52 if _, _, e := unix.Syscall(unix.SYS_LANDLOCK_RESTRICT_SELF, fd, 0, 0); e != 0 {
53 return fmt.Errorf("landlock restrict: %w", e)
54 }
55 return nil
56}
57
58var errAbstractReached = errors.New("connected to an abstract socket bound outside the scope")
59
60// realAbstractCheck proves the scope on this kernel, end to end: it binds
61// an abstract name, connects to it once unscoped (the stack works), then
62// connects again from a scoped thread, which must answer EPERM. The scoped
63// thread is left locked so it dies with its restriction.
64func realAbstractCheck() error {
65 name := "@carlos-probe-" + randomName()
66 ln, err := net.Listen("unix", name)
67 if err != nil {
68 return fmt.Errorf("listen %s: %w", name, err)
69 }
70 defer ln.Close()
71 go func() {
72 for {
73 c, err := ln.Accept()
74 if err != nil {
75 return
76 }
77 c.Close()
78 }
79 }()
80 c, err := net.Dial("unix", name)
81 if err != nil {
82 return fmt.Errorf("unscoped connect: %w", err)
83 }
84 c.Close()
85 res := make(chan error, 1)
86 go func() {
87 runtime.LockOSThread()
88 if err := ScopeAbstractUnix(); err != nil {
89 res <- err
90 return
91 }
92 c, err := net.Dial("unix", name)
93 if err == nil {
94 c.Close()
95 res <- errAbstractReached
96 return
97 }
98 res <- unwrapErrno(err)
99 }()
100 if err := <-res; !errors.Is(err, syscall.EPERM) {
101 return err
102 }
103 return nil
104}
105
106// initIPCInode is PROC_IPC_INIT_INO, the inode of the box's own IPC
107// namespace; any other value is a private one.
108const initIPCInode = 0xEFFFFFFF
109
110func ipcNamespaceInode() (uint64, error) {
111 fi, err := os.Stat("/proc/self/ns/ipc")
112 if err != nil {
113 return 0, err
114 }
115 st, ok := fi.Sys().(*syscall.Stat_t)
116 if !ok {
117 return 0, errors.New("no inode")
118 }
119 return st.Ino, nil
120}
121
122// ioWeightEnforced reports whether io.weight has a controller behind it on
123// this box: io.cost enabled for some device (a line with enable=1 in the
124// root's io.cost.qos), or BFQ, which exposes io.bfq.weight in the unit's
125// own cgroup.
126func ioWeightEnforced(selfCgroup string) bool {
127 if b, err := os.ReadFile("/sys/fs/cgroup/io.cost.qos"); err == nil {
128 for _, line := range strings.Split(string(b), "\n") {
129 if strings.Contains(" "+line+" ", " enable=1 ") {
130 return true
131 }
132 }
133 }
134 _, err := os.Stat(selfCgroup + "/io.bfq.weight")
135 return err == nil
136}
137