carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package rootstate
2
3import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "golang.org/x/sys/unix"
13)
14
15func fixture(t *testing.T) (string, Store) {
16 t.Helper()
17 parent := t.TempDir()
18 if err := os.Chmod(parent, 0700); err != nil {
19 t.Fatal(err)
20 }
21 f, err := os.Open(parent)
22 if err != nil {
23 t.Fatal(err)
24 }
25 defer f.Close()
26 d, err := descend(f, []string{"trusted"}, true, false, uint32(os.Geteuid()))
27 if err != nil {
28 t.Fatal(err)
29 }
30 t.Cleanup(func() { d.Close() })
31 return filepath.Join(parent, "trusted"), d
32}
33
34func TestPlan14HostileOwnership(t *testing.T) {
35 if os.Geteuid() == 0 {
36 t.Skip("nonroot witness; root ownership substitution is covered separately")
37 }
38 path, _ := fixture(t)
39 if _, err := Open(path, false, false); err == nil {
40 t.Fatal("root trusted real non-root-owned ancestors")
41 }
42 f, err := os.Create(filepath.Join(path, "file"))
43 if err != nil {
44 t.Fatal(err)
45 }
46 defer f.Close()
47 if err := trusted(int(f.Fd()), 0, unix.S_IFREG); !errors.Is(err, os.ErrPermission) {
48 t.Fatalf("real non-root-owned leaf accepted: %v", err)
49 }
50}
51
52func TestPlan14SymlinksHardlinksFIFOAndModes(t *testing.T) {
53 for _, attack := range []string{"leaf-link", "hardlink", "fifo", "writable", "directory-writable", "ancestor-link"} {
54 t.Run(attack, func(t *testing.T) {
55 path, d := fixture(t)
56 leaf := filepath.Join(path, "record")
57 switch attack {
58 case "leaf-link":
59 if err := os.Symlink("missing", leaf); err != nil {
60 t.Fatal(err)
61 }
62 case "hardlink":
63 if err := d.Write("other", []byte("safe")); err != nil {
64 t.Fatal(err)
65 }
66 if err := os.Link(filepath.Join(path, "other"), leaf); err != nil {
67 t.Fatal(err)
68 }
69 case "fifo":
70 if err := unix.Mkfifo(leaf, 0600); err != nil {
71 t.Fatal(err)
72 }
73 case "writable":
74 if err := os.WriteFile(leaf, []byte("false proof"), 0600); err != nil {
75 t.Fatal(err)
76 }
77 if err := os.Chmod(leaf, 0666); err != nil {
78 t.Fatal(err)
79 }
80 case "directory-writable":
81 if err := d.Write("record", []byte("safe")); err != nil {
82 t.Fatal(err)
83 }
84 if err := os.Chmod(path, 0777); err != nil {
85 t.Fatal(err)
86 }
87 case "ancestor-link":
88 parent, err := os.Open(filepath.Dir(path))
89 if err != nil {
90 t.Fatal(err)
91 }
92 defer parent.Close()
93 if err := os.Symlink("trusted", filepath.Join(filepath.Dir(path), "alias")); err != nil {
94 t.Fatal(err)
95 }
96 if got, err := descend(parent, []string{"alias"}, false, false, uint32(os.Geteuid())); err == nil {
97 got.Close()
98 t.Fatal("followed ancestor")
99 }
100 return
101 }
102 if _, err := d.Read("record"); err == nil {
103 t.Fatal("read accepted hostile file")
104 }
105 if err := d.Write("record", []byte("replacement")); err == nil {
106 t.Fatal("write repaired hostile file")
107 }
108 })
109 }
110}
111
112func TestPlan14RenameKeepsDescriptor(t *testing.T) {
113 path, d := fixture(t)
114 if err := d.Write("record", []byte("original")); err != nil {
115 t.Fatal(err)
116 }
117 moved := path + "-old"
118 if err := os.Rename(path, moved); err != nil {
119 t.Fatal(err)
120 }
121 if err := os.Mkdir(path, 0700); err != nil {
122 t.Fatal(err)
123 }
124 if err := os.WriteFile(filepath.Join(path, "record"), []byte("forged"), 0600); err != nil {
125 t.Fatal(err)
126 }
127 b, err := d.Read("record")
128 if err != nil || string(b) != "original" {
129 t.Fatalf("%q %v", b, err)
130 }
131 if err := d.Write("record", []byte("updated")); err != nil {
132 t.Fatal(err)
133 }
134 b, err = os.ReadFile(filepath.Join(path, "record"))
135 if err != nil || string(b) != "forged" {
136 t.Fatalf("touched replacement: %q %v", b, err)
137 }
138}
139
140func TestPlan14LockAndLimits(t *testing.T) {
141 _, d := fixture(t)
142 unlock, err := d.Lock(t.Context(), "host.lock")
143 if err != nil {
144 t.Fatal(err)
145 }
146 ctx, cancel := context.WithTimeout(t.Context(), 20*time.Millisecond)
147 defer cancel()
148 if release, err := d.Lock(ctx, "host.lock"); err == nil {
149 release()
150 t.Fatal("split lock")
151 } else if !errors.Is(err, context.DeadlineExceeded) {
152 t.Fatal(err)
153 }
154 unlock()
155 unlock, err = d.Lock(t.Context(), "host.lock")
156 if err != nil {
157 t.Fatal(err)
158 }
159 unlock()
160 if err := d.Write("large", []byte(strings.Repeat("x", MaxRecord+1))); err == nil {
161 t.Fatal("unbounded write")
162 }
163 if err := d.Write("../escape", []byte("x")); err == nil {
164 t.Fatal("escaped store")
165 }
166 for i := 0; i < 2; i++ {
167 names, err := d.Names()
168 if err != nil || len(names) != 1 {
169 t.Fatalf("scan %d: %v %v", i, names, err)
170 }
171 }
172}
173
174func TestPlan14RootOwnedParentRejectsServiceLeaf(t *testing.T) {
175 if os.Geteuid() != 0 {
176 t.Skip("requires uid 0 and chown; run in disposable root-owned test root")
177 }
178 path, d := fixture(t)
179 if err := d.Write("record", []byte("forged")); err != nil {
180 t.Fatal(err)
181 }
182 if err := os.Chown(filepath.Join(path, "record"), 65534, 65534); err != nil {
183 t.Fatal(err)
184 }
185 if _, err := d.Read("record"); !errors.Is(err, os.ErrPermission) {
186 t.Fatalf("%v", err)
187 }
188 if err := d.Write("record", []byte("replacement")); !errors.Is(err, os.ErrPermission) {
189 t.Fatalf("%v", err)
190 }
191}
192
193func TestPlan14DescendRetriesEveryAncestorSync(t *testing.T) {
194 for failAt := 1; failAt <= 3; failAt++ {
195 t.Run(string(rune('0'+failAt)), func(t *testing.T) {
196 base := t.TempDir()
197 if err := os.Chmod(base, 0700); err != nil {
198 t.Fatal(err)
199 }
200 parent, err := os.Open(base)
201 if err != nil {
202 t.Fatal(err)
203 }
204 defer parent.Close()
205 parts := []string{"a", "b", "c"}
206 calls := 0
207 injected := errors.New("parent fsync")
208 _, err = descendSync(parent, parts, true, false, uint32(os.Geteuid()), func(fd int) error {
209 calls++
210 if calls == failAt {
211 return injected
212 }
213 return unix.Fsync(fd)
214 })
215 if !errors.Is(err, injected) {
216 t.Fatalf("first traversal: %v", err)
217 }
218 if _, err := os.Stat(filepath.Join(append([]string{base}, parts[:failAt]...)...)); err != nil {
219 t.Fatal(err)
220 }
221 calls = 0
222 d, err := descendSync(parent, parts, true, false, uint32(os.Geteuid()), func(fd int) error {
223 calls++
224 return unix.Fsync(fd)
225 })
226 if err != nil {
227 t.Fatal(err)
228 }
229 defer d.Close()
230 if calls != len(parts) {
231 t.Fatalf("retry synced %d parents, want %d", calls, len(parts))
232 }
233 if err := d.Write("record", []byte("durable")); err != nil {
234 t.Fatal(err)
235 }
236 // Even an all-existing path must still propagate a sync failure.
237 _, err = descendSync(parent, parts, true, false, uint32(os.Geteuid()), func(int) error { return injected })
238 if !errors.Is(err, injected) {
239 t.Fatalf("existing traversal: %v", err)
240 }
241 })
242 }
243}
244
245func TestPlan14RemoveCapability(t *testing.T) {
246 path, s := fixture(t)
247 if err := s.Write("capability", []byte("secret")); err != nil {
248 t.Fatal(err)
249 }
250 if err := s.Remove("capability"); err != nil {
251 t.Fatal(err)
252 }
253 if _, err := s.Read("capability"); !errors.Is(err, os.ErrNotExist) {
254 t.Fatal(err)
255 }
256 if err := s.Remove("capability"); err != nil {
257 t.Fatal(err)
258 }
259 if err := s.Remove("../escape"); err == nil {
260 t.Fatal("escaped")
261 }
262 if err := os.Symlink("outside", filepath.Join(path, "capability")); err != nil {
263 t.Fatal(err)
264 }
265 if err := s.Remove("capability"); err == nil {
266 t.Fatal("removed untrusted entry")
267 }
268}
269