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 "errors"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strconv"
12 "strings"
13
14 "github.com/carlosframework/platform/internal/hostbox"
15 "github.com/carlosframework/platform/internal/instancectl"
16 "golang.org/x/sys/unix"
17)
18
19// linuxClaimLaunchKernel is root's production view of a claim launch. Owner
20// is the uid the release tree belongs to: root in production.
21type linuxClaimLaunchKernel struct {
22 OptRoot, CgroupRoot, ProcRoot string
23 Owner uint32
24 // BoundApp is the build the unit binds, relative to the unit's root;
25 // empty is opt/carlos/app.
26 BoundApp string
27}
28
29// rootOnly is a directory or file only Owner can change.
30func (k linuxClaimLaunchKernel) rootOnly(st unix.Stat_t, kind uint32) bool {
31 return st.Mode&unix.S_IFMT == kind && st.Uid == k.Owner && st.Mode&0o022 == 0
32}
33
34// Verify accepts a build only inside the release tree, outside its
35// retargetable live links and the platform binary, reached with no symlink,
36// in directories and a file only Owner can change, with the pinned identity.
37func (k linuxClaimLaunchKernel) Verify(path string, want hostbox.LaunchFile) error {
38 if !claimLaunchAbs(k.OptRoot) || !claimLaunchAbs(path) || !strings.HasPrefix(path, k.OptRoot+"/") ||
39 strings.HasPrefix(path, filepath.Join(k.OptRoot, "live")+"/") || path == filepath.Join(k.OptRoot, "carlos") || want.Device == 0 || want.Inode == 0 {
40 return hostbox.ErrRoleLaunchShape
41 }
42 rel, err := filepath.Rel(k.OptRoot, filepath.Dir(path))
43 if err != nil || !filepath.IsLocal(rel) {
44 return hostbox.ErrRoleLaunchShape
45 }
46 dir := k.OptRoot
47 parts := []string{}
48 if rel != "." {
49 parts = strings.Split(rel, "/")
50 }
51 for i := -1; i < len(parts); i++ {
52 if i >= 0 {
53 dir = filepath.Join(dir, parts[i])
54 }
55 var st unix.Stat_t
56 if err := unix.Lstat(dir, &st); err != nil {
57 return err
58 }
59 if !k.rootOnly(st, unix.S_IFDIR) {
60 return hostbox.ErrChanged
61 }
62 }
63 fd, err := unix.Openat2(unix.AT_FDCWD, path, &unix.OpenHow{Flags: unix.O_RDONLY | unix.O_CLOEXEC | unix.O_NOFOLLOW, Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS})
64 if err != nil {
65 return err
66 }
67 defer unix.Close(fd)
68 var st unix.Stat_t
69 if err := unix.Fstat(fd, &st); err != nil {
70 return err
71 }
72 if !k.rootOnly(st, unix.S_IFREG) || st.Mode&0o111 == 0 || uint64(st.Dev) != want.Device || st.Ino != want.Inode {
73 return hostbox.ErrChanged
74 }
75 return nil
76}
77
78func claimLaunchProcessIdentity(procRoot string, pid int) (string, int, error) {
79 raw, err := os.ReadFile(filepath.Join(procRoot, strconv.Itoa(pid), "stat"))
80 if err != nil {
81 return "", 0, err
82 }
83 i := strings.LastIndexByte(string(raw), ')')
84 if i < 0 {
85 return "", 0, hostbox.ErrChanged
86 }
87 fields := strings.Fields(string(raw)[i+1:])
88 if len(fields) < 20 || fields[0] == "Z" || fields[0] == "X" {
89 return "", 0, hostbox.ErrChanged
90 }
91 parent, err := strconv.Atoi(fields[1])
92 if err != nil {
93 return "", 0, hostbox.ErrChanged
94 }
95 return fields[19], parent, nil
96}
97
98func claimLaunchProcessStart(procRoot string, pid int) (string, error) {
99 start, _, err := claimLaunchProcessIdentity(procRoot, pid)
100 return start, err
101}
102
103func (k linuxClaimLaunchKernel) member(pid int, group string) error {
104 raw, err := os.ReadFile(filepath.Join(k.ProcRoot, strconv.Itoa(pid), "cgroup"))
105 if err != nil {
106 return err
107 }
108 for _, line := range strings.Split(string(raw), "\n") {
109 if line == "0::/"+group {
110 return nil
111 }
112 }
113 return hostbox.ErrChanged
114}
115
116func (k linuxClaimLaunchKernel) tenant(main int) (int, error) {
117 tasks := filepath.Join(k.ProcRoot, strconv.Itoa(main), "task")
118 entries, err := os.ReadDir(tasks)
119 if err != nil {
120 return 0, err
121 }
122 children := map[int]bool{}
123 for _, entry := range entries {
124 raw, err := os.ReadFile(filepath.Join(tasks, entry.Name(), "children"))
125 if errors.Is(err, os.ErrNotExist) {
126 continue
127 }
128 if err != nil {
129 return 0, err
130 }
131 for _, word := range strings.Fields(string(raw)) {
132 pid, err := strconv.Atoi(word)
133 if err != nil || pid <= 0 || pid == main {
134 return 0, hostbox.ErrChanged
135 }
136 children[pid] = true
137 }
138 }
139 if len(children) == 0 {
140 return 0, errClaimLaunchPending
141 }
142 if len(children) != 1 {
143 return 0, hostbox.ErrChanged
144 }
145 for pid := range children {
146 return pid, nil
147 }
148 return 0, hostbox.ErrChanged
149}
150
151// Observe keeps the main process as the unit identity and verifies its one
152// direct tenant child. runInit forks on a locked thread, so every task's
153// children file participates; the leader's children file alone is incomplete.
154func (k linuxClaimLaunchKernel) Observe(s instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) {
155 if s.MainPID <= 0 || !filepath.IsLocal(s.ControlGroup) || !claimLaunchAbs(k.CgroupRoot) || !claimLaunchAbs(k.ProcRoot) {
156 return ClaimLaunchObserved{}, hostbox.ErrChanged
157 }
158 start, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID)
159 if err != nil {
160 return ClaimLaunchObserved{}, err
161 }
162 var group unix.Stat_t
163 if err = unix.Lstat(filepath.Join(k.CgroupRoot, s.ControlGroup), &group); err != nil {
164 return ClaimLaunchObserved{}, err
165 }
166 if group.Mode&unix.S_IFMT != unix.S_IFDIR {
167 return ClaimLaunchObserved{}, hostbox.ErrChanged
168 }
169 var launcher unix.Stat_t
170 if err := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "exe"), &launcher); err != nil {
171 return ClaimLaunchObserved{}, err
172 }
173 observed := ClaimLaunchObserved{Launcher: hostbox.LaunchFile{Device: uint64(launcher.Dev), Inode: launcher.Ino}, ProcessStart: start, Group: hostbox.LaunchFile{Device: uint64(group.Dev), Inode: group.Ino}}
174 if err := k.member(s.MainPID, s.ControlGroup); err != nil {
175 return observed, err
176 }
177 child, childErr := k.tenant(s.MainPID)
178 if childErr != nil {
179 again, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID)
180 if err != nil || again != start {
181 return observed, hostbox.ErrChanged
182 }
183 return observed, childErr
184 }
185 childStart, parent, err := claimLaunchProcessIdentity(k.ProcRoot, child)
186 if err != nil || parent != s.MainPID {
187 return observed, hostbox.ErrChanged
188 }
189 observed.TenantPID, observed.TenantStart = child, childStart
190 if err := k.member(child, s.ControlGroup); err != nil {
191 return observed, err
192 }
193 bound := k.BoundApp
194 if bound == "" {
195 bound = "opt/carlos/app"
196 }
197 var app, mainApp, running unix.Stat_t
198 boundErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(child), "root", bound), &app)
199 mainBoundErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "root", bound), &mainApp)
200 execErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(child), "exe"), &running)
201 againChild, parent, err := claimLaunchProcessIdentity(k.ProcRoot, child)
202 if err != nil || againChild != childStart || parent != s.MainPID {
203 return observed, hostbox.ErrChanged
204 }
205 again, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID)
206 if err != nil || again != start {
207 return observed, hostbox.ErrChanged
208 }
209 if err := k.member(s.MainPID, s.ControlGroup); err != nil {
210 return observed, err
211 }
212 if err := k.member(child, s.ControlGroup); err != nil {
213 return observed, err
214 }
215 againPID, err := k.tenant(s.MainPID)
216 if err != nil || againPID != child {
217 return observed, hostbox.ErrChanged
218 }
219 for _, err := range []error{boundErr, mainBoundErr} {
220 if errors.Is(err, os.ErrNotExist) {
221 return observed, errClaimLaunchPending
222 }
223 if err != nil {
224 return observed, err
225 }
226 }
227 if app.Dev != mainApp.Dev || app.Ino != mainApp.Ino {
228 return observed, hostbox.ErrChanged
229 }
230 if execErr != nil {
231 return observed, execErr
232 }
233 observed.Executable = hostbox.LaunchFile{Device: uint64(app.Dev), Inode: app.Ino}
234 observed.Running = hostbox.LaunchFile{Device: uint64(running.Dev), Inode: running.Ino}
235 return observed, nil
236}
237
238func (k linuxClaimLaunchKernel) VerifyDigest(ctx context.Context, path string, want hostbox.LaunchFile, digest string) error {
239 if err := k.Verify(path, want); err != nil {
240 return err
241 }
242 f, err := verifiedClaimExecutable(ctx, exec.Command(path), path, digest)
243 if err != nil {
244 return err
245 }
246 defer f.Close()
247 var st unix.Stat_t
248 if err := unix.Fstat(int(f.Fd()), &st); err != nil {
249 return err
250 }
251 if uint64(st.Dev) != want.Device || st.Ino != want.Inode {
252 return hostbox.ErrChanged
253 }
254 return nil
255}
256