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 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/release"
14)
15
16type generationProcFixture struct {
17 proc, task, unrelated, versions string
18 reservation release.GenerationReservation
19}
20
21func generationProc(t *testing.T) generationProcFixture {
22 t.Helper()
23 f, disk, _ := generationLaunchFixture(t)
24 if _, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req); err != nil {
25 t.Fatal(err)
26 }
27 g := generationProcFixture{versions: filepath.Join(f.root.generations.optDir, "versions"), proc: t.TempDir(), unrelated: filepath.Join(t.TempDir(), "unrelated"), reservation: readGenerationLaunch(t, disk)}
28 if err := os.WriteFile(g.unrelated, []byte("other"), 0600); err != nil {
29 t.Fatal(err)
30 }
31 g.task = filepath.Join(g.proc, "41", "task", "41")
32 for _, p := range []string{g.task + "/fd", g.task + "/ns", g.proc + "/self/ns", g.proc + "/sys/kernel/random"} {
33 if err := os.MkdirAll(p, 0700); err != nil {
34 t.Fatal(err)
35 }
36 }
37 generationWrite(t, g.proc+"/sys/kernel/random/boot_id", "boot-fixture\n")
38 generationWrite(t, g.task+"/stat", "41 (test (child)) S 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 123 0\n")
39 generationWrite(t, g.task+"/status", "Uid:\t0\t0\t0\t0\nKthread:\t0\n")
40 generationWrite(t, g.task+"/maps", "")
41 generationWrite(t, g.task+"/mountinfo", fmt.Sprintf("1 0 %d:%d / / rw - tmpfs tmpfs rw\n", generationMajor(g.reservation.Binding.TreeIdentity.Device), generationMinor(g.reservation.Binding.TreeIdentity.Device)))
42 for _, ns := range []string{"mnt", "pid"} {
43 generationLink(t, "namespace-"+ns, g.proc+"/self/ns/"+ns)
44 generationLink(t, "namespace-"+ns, g.task+"/ns/"+ns)
45 }
46 for _, name := range []string{"exe", "cwd", "root"} {
47 generationLink(t, g.unrelated, g.task+"/"+name)
48 }
49 return g
50}
51func generationWrite(t *testing.T, p, s string) {
52 t.Helper()
53 if err := os.WriteFile(p, []byte(s), 0600); err != nil {
54 t.Fatal(err)
55 }
56}
57func generationLink(t *testing.T, target, path string) {
58 t.Helper()
59 if err := os.Symlink(target, path); err != nil {
60 t.Fatal(err)
61 }
62}
63func generationRelink(t *testing.T, target, path string) {
64 t.Helper()
65 if err := os.Remove(path); err != nil {
66 t.Fatal(err)
67 }
68 generationLink(t, target, path)
69}
70
71func TestPlan14GenerationKernelOwnDescriptors(t *testing.T) {
72 f := generationProc(t)
73 if err := os.Remove(f.task + "/fd"); err != nil {
74 t.Fatal(err)
75 }
76 generationLink(t, "/proc/self/fd", f.task+"/fd")
77 if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err != nil {
78 t.Fatal("scanner's own descriptor disappeared during inspection", err)
79 }
80 file, err := os.Open(f.reservation.Binding.Path)
81 if err != nil {
82 t.Fatal(err)
83 }
84 defer file.Close()
85 if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err == nil {
86 t.Fatal("real open staged file ignored")
87 }
88}
89
90func TestPlan14GenerationKernelReferences(t *testing.T) {
91 for _, attack := range []string{"none", "exe", "fd", "deleted-fd", "cwd", "root", "mapped", "mount", "foreign-mount-ns", "foreign-pid-ns", "namespace-fd", "uncontained", "malformed-stat", "malformed-maps", "missing-status", "missing-fds", "missing-mounts", "missing-boot", "cancel", "thread"} {
92 t.Run(attack, func(t *testing.T) {
93 f := generationProc(t)
94 b := f.reservation.Binding
95 ctx := t.Context()
96 switch attack {
97 case "exe", "cwd", "root":
98 generationRelink(t, b.Path, f.task+"/"+attack)
99 case "fd":
100 generationLink(t, b.Path, f.task+"/fd/3")
101 case "deleted-fd":
102 file, err := os.Open(b.Path)
103 if err != nil {
104 t.Fatal(err)
105 }
106 defer file.Close()
107 if err := os.Remove(b.Path); err != nil {
108 t.Fatal(err)
109 }
110 generationLink(t, fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), file.Fd()), f.task+"/fd/3")
111 case "mapped":
112 generationWrite(t, f.task+"/maps", fmt.Sprintf("1000-2000 r-xp 0000 %x:%x %d %s (deleted)\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Executable.Inode, b.Path))
113 case "mount":
114 generationWrite(t, f.task+"/mountinfo", "2 1 0:1 "+b.Tree+" /bound ro - tmpfs tmpfs ro\n")
115 case "foreign-mount-ns":
116 generationRelink(t, "foreign", f.task+"/ns/mnt")
117 case "foreign-pid-ns":
118 generationRelink(t, "foreign", f.task+"/ns/pid")
119 case "namespace-fd":
120 generationLink(t, "mnt:[12345]", f.task+"/fd/3")
121 case "uncontained":
122 generationWrite(t, f.task+"/status", "Uid:\t1001\t1001\t1001\t1001\nKthread:\t0\n")
123 case "malformed-stat":
124 generationWrite(t, f.task+"/stat", "broken")
125 case "malformed-maps":
126 generationWrite(t, f.task+"/maps", "broken")
127 case "missing-status":
128 if err := os.Remove(f.task + "/status"); err != nil {
129 t.Fatal(err)
130 }
131 case "missing-fds":
132 if err := os.Remove(f.task + "/fd"); err != nil {
133 t.Fatal(err)
134 }
135 case "missing-mounts":
136 if err := os.Remove(f.task + "/mountinfo"); err != nil {
137 t.Fatal(err)
138 }
139 case "missing-boot":
140 if err := os.Remove(f.proc + "/sys/kernel/random/boot_id"); err != nil {
141 t.Fatal(err)
142 }
143 case "cancel":
144 var cancel context.CancelFunc
145 ctx, cancel = context.WithCancel(ctx)
146 cancel()
147 case "thread":
148 other := filepath.Join(filepath.Dir(f.task), "42")
149 if err := os.Mkdir(other, 0700); err != nil {
150 t.Fatal(err)
151 }
152 // A separate descriptor table on a nonleader must not be omitted.
153 for _, name := range []string{"stat", "status", "maps", "mountinfo", "ns", "exe", "cwd", "root"} {
154 generationLink(t, filepath.Join(f.task, name), filepath.Join(other, name))
155 }
156 if err := os.Mkdir(other+"/fd", 0700); err != nil {
157 t.Fatal(err)
158 }
159 generationLink(t, b.Path, other+"/fd/7")
160 }
161 evidence, err := (generationKernel{proc: f.proc}).absent(ctx, f.reservation)
162 if attack == "none" {
163 if err != nil || len(evidence) != 64 {
164 t.Fatalf("positive: %q %v", evidence, err)
165 }
166 } else if err == nil {
167 t.Fatalf("%s released kernel reference: %s", attack, evidence)
168 }
169 })
170 }
171}
172
173func TestPlan14GenerationKernelMappingIdentity(t *testing.T) {
174 f := generationProc(t)
175 b := f.reservation.Binding
176 // A renamed/deleted mapping is found by device/inode without its old path.
177 generationWrite(t, f.task+"/maps", fmt.Sprintf("1000-2000 r-xp 0000 %x:%x %d /renamed (deleted)\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Executable.Inode))
178 if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err == nil {
179 t.Fatal("renamed mapping lost its pin")
180 }
181}
182
183func TestPlan14GenerationKernelControllerIdentity(t *testing.T) {
184 f := generationProc(t)
185 b := f.reservation.Binding
186 b.Target.BootID = "boot-fixture"
187 b.ControllerPID = 41
188 b.ControllerStart = "123"
189 generationLink(t, f.task+"/stat", f.proc+"/41/stat")
190 for _, test := range []struct {
191 name, start, boot string
192 allow bool
193 }{{"alive", "123", "boot-fixture", false}, {"pid-reused", "122", "boot-fixture", true}, {"reboot", "123", "old-boot", true}} {
194 t.Run(test.name, func(t *testing.T) {
195 b.ControllerStart, b.Target.BootID = test.start, test.boot
196 if err := (generationKernel{proc: f.proc}).controllerGone(t.Context(), b); (err == nil) != test.allow {
197 t.Fatalf("gone: %v", err)
198 }
199 })
200 }
201 b.ControllerPID = 99
202 if err := (generationKernel{proc: f.proc}).controllerGone(t.Context(), b); err != nil {
203 t.Fatal(err)
204 }
205 generationWrite(t, f.task+"/stat", strings.Repeat("x", 1<<20))
206 b.ControllerPID = 41
207 b.Target.BootID = "boot-fixture"
208 if err := (generationKernel{proc: f.proc}).controllerGone(t.Context(), b); err == nil {
209 t.Fatal("oversized stat accepted")
210 }
211}
212
213func TestPlan14GenerationKernelRetainedFiles(t *testing.T) {
214 for _, attack := range []string{"none", "path", "tree"} {
215 t.Run(attack, func(t *testing.T) {
216 f := generationProc(t)
217 b := f.reservation.Binding
218 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
219 if err != nil {
220 t.Fatal(err)
221 }
222 b.Target.BootID = strings.TrimSpace(string(boot))
223 switch attack {
224 case "path":
225 b.Executable.Inode++
226 case "tree":
227 b.TreeIdentity.Inode++
228 }
229 err = generationRetainedFiles(b, func(path string) (*os.File, error) {
230 file, err := os.Open(path)
231 if err != nil {
232 return nil, err
233 }
234 info, err := file.Stat()
235 if err != nil || !info.Mode().IsRegular() {
236 file.Close()
237 return nil, os.ErrPermission
238 }
239 return file, nil
240 })
241 if (err == nil) != (attack == "none") {
242 t.Fatalf("retained files: %v", err)
243 }
244 })
245 }
246}
247
248func TestPlan14GenerationKernelSiblingAlias(t *testing.T) {
249 f := generationProc(t)
250 asset := filepath.Join(f.reservation.Binding.Tree, "asset")
251 generationWrite(t, asset, "sibling")
252 alias := filepath.Join(t.TempDir(), "aliased-asset")
253 if err := os.Link(asset, alias); err != nil {
254 t.Fatal(err)
255 }
256 generationLink(t, alias, f.task+"/fd/8")
257 if _, err := (generationKernel{proc: f.proc}).absent(t.Context(), f.reservation); err == nil {
258 t.Fatal("aliased sibling did not pin its tree")
259 }
260}
261
262func generationRetainedFiles(b release.GenerationBinding, open func(string) (*os.File, error)) error {
263 _, err := generationCurrentFiles(b, open)
264 return err
265}
266
267func TestPlan14GenerationServiceReferences(t *testing.T) {
268 for _, mode := range []string{"service", "unknown", "fd", "mapped", "target mount"} {
269 t.Run(mode, func(t *testing.T) {
270 f := generationProc(t)
271 b := f.reservation.Binding
272 generationWrite(t, f.task+"/status", "Uid:\t1001\t1001\t1001\t1001\nKthread:\t0\n")
273 generationRelink(t, "private-service", f.task+"/ns/mnt")
274 switch mode {
275 case "fd":
276 generationLink(t, b.Path, f.task+"/fd/3")
277 case "mapped":
278 generationWrite(t, f.task+"/maps", fmt.Sprintf("1000-2000 r-xp 0000 %x:%x %d %s\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Executable.Inode, b.Path))
279 case "target mount":
280 generationWrite(t, f.task+"/mountinfo", fmt.Sprintf("1 0 %d:%d / / rw - ext4 disk rw\n2 1 %d:%d %s /bound ro - ext4 disk ro\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Tree))
281 }
282 k := generationKernel{proc: f.proc, service: func(string, []byte) (bool, error) { return mode != "unknown", nil }, recheck: func(string, bool) error { return nil }}
283 _, err := k.absent(t.Context(), f.reservation)
284 if (err == nil) != (mode == "service") {
285 t.Fatalf("service reference boundary: %v", err)
286 }
287 })
288 }
289}
290
291func TestPlan14GenerationCensusRetriesWholeObservation(t *testing.T) {
292 for _, refusal := range []error{os.ErrNotExist, os.ErrPermission} {
293 calls := 0
294 boot, digest, err := generationCensusRetry(t.Context(), func(context.Context) (string, string, error) {
295 calls++
296 if calls == 1 {
297 return "", "", fmt.Errorf("task exited: %w", refusal)
298 }
299 return "boot", "digest", nil
300 })
301 if refusal == os.ErrNotExist {
302 if err != nil || calls != 2 || boot != "boot" || digest != "digest" {
303 t.Fatal(calls, boot, digest, err)
304 }
305 } else if err == nil || calls != 1 {
306 t.Fatal("retried unexplained task", calls, err)
307 }
308 }
309 calls := 0
310 _, _, err := generationCensusRetry(t.Context(), func(context.Context) (string, string, error) { calls++; return "", "", os.ErrNotExist })
311 if err == nil || calls != 3 {
312 t.Fatal("unbounded or incomplete scan accepted", calls, err)
313 }
314}
315
316func TestPlan14GenerationServiceLookupKeepsStrictRootScan(t *testing.T) {
317 for _, mode := range []string{"root", "nonroot", "private-namespace", "fd", "ancestor-mount"} {
318 t.Run(mode, func(t *testing.T) {
319 f := generationProc(t)
320 switch mode {
321 case "nonroot":
322 generationWrite(t, f.task+"/status", "Uid:\t1001\t1001\t1001\t1001\nKthread:\t0\n")
323 case "private-namespace":
324 generationRelink(t, "private-service", f.task+"/ns/mnt")
325 case "fd":
326 generationLink(t, f.reservation.Binding.Path, f.task+"/fd/3")
327 case "ancestor-mount":
328 generationWrite(t, f.task+"/mountinfo", "2 1 0:1 "+filepath.Dir(f.reservation.Binding.Tree)+" /bound ro - tmpfs tmpfs ro\n")
329 }
330 k := generationKernel{proc: f.proc, service: func(string, []byte) (bool, error) { return false, os.ErrPermission }}
331 _, err := k.absent(t.Context(), f.reservation)
332 if (err == nil) != (mode == "root") {
333 t.Fatalf("strict root scan: %v", err)
334 }
335 })
336 }
337}
338
339// journal-migrate's own service-user registry child (harness 8.2,
340// 2026-10-20): from an operator shell it sits in no service, so the root-task
341// rule refused it. A direct child of the census's parent passes that rule
342// only; it still shares the namespaces and passes every reference check.
343func TestPlan14GenerationCensusAcceptsOwnChildOnly(t *testing.T) {
344 for _, mode := range []string{"own child", "no parent", "other parent", "exe", "fd", "mapped", "foreign-mount-ns", "target mount"} {
345 t.Run(mode, func(t *testing.T) {
346 f := generationProc(t)
347 b := f.reservation.Binding
348 // The fixture's stat names ppid 1.
349 generationWrite(t, f.task+"/status", "Uid:\t989\t989\t989\t989\nKthread:\t0\n")
350 k := generationKernel{proc: f.proc, parent: 1, service: func(string, []byte) (bool, error) { return false, os.ErrPermission }}
351 switch mode {
352 case "no parent":
353 k.parent = 0
354 case "other parent":
355 k.parent = 2
356 case "exe":
357 generationRelink(t, b.Path, f.task+"/exe")
358 case "fd":
359 generationLink(t, b.Path, f.task+"/fd/3")
360 case "mapped":
361 generationWrite(t, f.task+"/maps", fmt.Sprintf("1000-2000 r-xp 0000 %x:%x %d %s\n", generationMajor(b.Executable.Device), generationMinor(b.Executable.Device), b.Executable.Inode, b.Path))
362 case "foreign-mount-ns":
363 generationRelink(t, "foreign", f.task+"/ns/mnt")
364 case "target mount":
365 generationWrite(t, f.task+"/mountinfo", "2 1 0:1 "+b.Tree+" /bound ro - tmpfs tmpfs ro\n")
366 }
367 _, err := k.absent(t.Context(), f.reservation)
368 if (err == nil) != (mode == "own child") {
369 t.Fatalf("own-child boundary: %v", err)
370 }
371 })
372 }
373}
374
375func TestProcStatParent(t *testing.T) {
376 for raw, want := range map[string]int{
377 "41 (test (child)) S 7 0 0": 7,
378 "41 (a) b) c) R 916 1 1": 916,
379 "41 (x\n) S 3 0": 3,
380 } {
381 if got, err := procStatParent([]byte(raw)); err != nil || got != want {
382 t.Fatalf("%q = %d, %v; want %d", raw, got, err, want)
383 }
384 }
385 for _, raw := range []string{"", "41 (x", "41 (x) S", "41 (x) S y"} {
386 if _, err := procStatParent([]byte(raw)); err == nil {
387 t.Fatalf("%q accepted", raw)
388 }
389 }
390}
391