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 "os"
9 "os/exec"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "golang.org/x/sys/unix"
15)
16
17func makeRoleFixture(t *testing.T, p *Prepared, role, events string) {
18 t.Helper()
19 d := filepath.Join(p.dir.Name(), "role-"+role)
20 if err := os.Mkdir(d, 0700); err != nil {
21 t.Fatal(err)
22 }
23 for n, v := range map[string]string{"cgroup.events": events, "cgroup.kill": ""} {
24 if err := os.WriteFile(filepath.Join(d, n), []byte(v), 0600); err != nil {
25 t.Fatal(err)
26 }
27 }
28}
29
30func TestPrepareRoleChildEmptyAndIdentity(t *testing.T) {
31 p := strictFixture(t)
32 makeRoleFixture(t, p, "seed", "populated 0\n")
33 child, err := p.PrepareRoleChild("seed")
34 if err != nil {
35 t.Fatal(err)
36 }
37 flags, err := unix.FcntlInt(child.entryParent.Fd(), unix.F_GETFD, 0)
38 if err != nil || flags&unix.FD_CLOEXEC == 0 {
39 t.Fatalf("parent descriptor may escape exec: flags=%d err=%v", flags, err)
40 }
41 id, err := child.Identity()
42 if err != nil || id.Device == 0 || id.Inode == 0 {
43 t.Fatalf("identity=%+v %v", id, err)
44 }
45 if err := child.Close(); err != nil {
46 t.Fatal(err)
47 }
48}
49
50func TestRoleChildCannotOutliveParentCleanup(t *testing.T) {
51 for _, action := range []string{"kill", "close", "failed-kill", "failed-close"} {
52 t.Run(action, func(t *testing.T) {
53 p := strictFixture(t)
54 makeRoleFixture(t, p, "seed", "populated 0\n")
55 child, err := p.PrepareRoleChild("seed")
56 if err != nil {
57 t.Fatal(err)
58 }
59 defer child.Close()
60 starts := 0
61 child.start = func(*exec.Cmd) error { starts++; return nil }
62 if action == "failed-close" {
63 if err := strictWrite(p.events, "populated 1\n"); err != nil {
64 t.Fatal(err)
65 }
66 if err := p.Close(); !errors.Is(err, ErrStrictRefused) {
67 t.Fatalf("failed close=%v", err)
68 }
69 // Even once the parent's population drops, cleanup remains latched.
70 if err := strictWrite(p.events, "populated 0\n"); err != nil {
71 t.Fatal(err)
72 }
73 } else if action == "close" {
74 if err := p.Close(); err != nil {
75 t.Fatal(err)
76 }
77 } else {
78 if action == "failed-kill" {
79 p.kill.Close()
80 }
81 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
82 defer cancel()
83 err := p.KillAndEmpty(ctx)
84 if (err != nil) != (action == "failed-kill") {
85 t.Fatalf("cleanup: %v", err)
86 }
87 }
88 if err := child.Start(exec.Command("unused")); !errors.Is(err, ErrStrictState) || starts != 0 {
89 t.Fatalf("child reopened drained parent: %v, starts=%d", err, starts)
90 }
91 if _, err := p.PrepareRoleChild("another"); !errors.Is(err, ErrStrictState) {
92 t.Fatalf("new role after cleanup: %v", err)
93 }
94 })
95 }
96}
97
98func TestRoleChildRechecksParentLimits(t *testing.T) {
99 p := strictFixture(t)
100 makeRoleFixture(t, p, "seed", "populated 0\n")
101 child, err := p.PrepareRoleChild("seed")
102 if err != nil {
103 t.Fatal(err)
104 }
105 defer child.Close()
106 if err := strictWrite(p.limits["memory.max"], "0"); err != nil {
107 t.Fatal(err)
108 }
109 if err := child.Start(exec.Command("unused")); !errors.Is(err, ErrStrictRefused) {
110 t.Fatalf("changed aggregate limit: %v", err)
111 }
112}
113
114func TestPrepareRoleChildRefusals(t *testing.T) {
115 t.Run("nonempty", func(t *testing.T) {
116 p := strictFixture(t)
117 makeRoleFixture(t, p, "seed", "populated 1\n")
118 if _, err := p.PrepareRoleChild("seed"); !errors.Is(err, ErrStrictRefused) {
119 t.Fatalf("err=%v", err)
120 }
121 })
122 t.Run("symlink", func(t *testing.T) {
123 p := strictFixture(t)
124 if err := os.Symlink(t.TempDir(), filepath.Join(p.dir.Name(), "role-seed")); err != nil {
125 t.Fatal(err)
126 }
127 if _, err := p.PrepareRoleChild("seed"); err == nil {
128 t.Fatal("symlink accepted")
129 }
130 })
131 t.Run("no-delegation", func(t *testing.T) {
132 p := strictFixture(t)
133 if _, err := p.PrepareRoleChild("seed"); !errors.Is(err, ErrStrictUnsupported) {
134 t.Fatalf("err=%v", err)
135 }
136 })
137 t.Run("absent-role", func(t *testing.T) {
138 p := strictFixture(t)
139 if _, err := p.PrepareRoleChild(""); !errors.Is(err, ErrStrictRefused) {
140 t.Fatalf("err=%v", err)
141 }
142 })
143}
144
145func TestRoleChildRefusesAfterCleanupAcquisitionCancellation(t *testing.T) {
146 p := strictFixture(t)
147 makeRoleFixture(t, p, "seed", "populated 0\n")
148 child, err := p.PrepareRoleChild("seed")
149 if err != nil {
150 t.Fatal(err)
151 }
152 defer child.Close()
153 starts := 0
154 child.start = func(*exec.Cmd) error { starts++; return nil }
155 p.mu.Lock()
156 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
157 defer cancel()
158 done := make(chan error, 1)
159 go func() { done <- p.KillAndEmpty(ctx) }()
160 err = <-done
161 p.mu.Unlock()
162 if !errors.Is(err, ErrStrictDeadline) {
163 t.Fatalf("cleanup acquisition: %v", err)
164 }
165 if err := child.Start(exec.Command("unused")); !errors.Is(err, ErrStrictState) || starts != 0 {
166 t.Fatalf("child reopened parent after cancelled cleanup: %v, starts=%d", err, starts)
167 }
168 if _, err := p.PrepareRoleChild("another"); !errors.Is(err, ErrStrictState) {
169 t.Fatalf("new role after cancelled cleanup: %v", err)
170 }
171}
172
173// TestPrepareRoleChildWithIntentRefusesExistingDirectory verifies that the intent
174// path refuses with ErrStrictRefused when the child directory appears between the
175// intent callback and mkdir (i.e., the intent callback creates the directory).
176func TestPrepareRoleChildWithIntentRefusesExistingDirectory(t *testing.T) {
177 p := strictFixture(t)
178 intentCalled := false
179 intent := func(id ContainmentIdentity) error {
180 intentCalled = true
181 // Simulate another actor creating the directory after our intent checks for it
182 // but before mkdir is called
183 if err := os.Mkdir(filepath.Join(p.dir.Name(), "role-newchild"), 0700); err != nil {
184 t.Fatal(err)
185 }
186 // Also create the required cgroup files
187 for n, v := range map[string]string{"cgroup.events": "populated 0\n", "cgroup.kill": ""} {
188 if err := os.WriteFile(filepath.Join(p.dir.Name(), "role-newchild", n), []byte(v), 0600); err != nil {
189 t.Fatal(err)
190 }
191 }
192 return nil
193 }
194 child, err := p.PrepareRoleChildWithIntent("newchild", intent)
195 if err != nil && !intentCalled {
196 t.Fatalf("intent was not called before error: %v", err)
197 }
198 if !intentCalled {
199 t.Fatal("intent callback was not called")
200 }
201 if !errors.Is(err, ErrStrictRefused) || child != nil {
202 t.Fatalf("intent path did not refuse pre-existing directory: err=%v child=%v", err, child)
203 }
204}
205