carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package safefile
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 "time"
10
11 "golang.org/x/sys/unix"
12)
13
14func reapTestStage(t *testing.T, parent, name string, data bool) string {
15 t.Helper()
16 path := filepath.Join(parent, name)
17 if err := os.MkdirAll(path, 0o700); err != nil {
18 t.Fatal(err)
19 }
20 if data {
21 if err := os.WriteFile(filepath.Join(path, "data"), []byte("abandoned"), 0o600); err != nil {
22 t.Fatal(err)
23 }
24 reapTestAge(t, filepath.Join(path, "data"))
25 }
26 reapTestAge(t, path)
27 return path
28}
29
30func reapTestAge(t *testing.T, path string) {
31 t.Helper()
32 old := time.Now().Add(-2 * time.Hour)
33 if err := os.Chtimes(path, old, old); err != nil {
34 t.Fatal(err)
35 }
36}
37
38func TestReapStaleWriterStages(t *testing.T) {
39 root := t.TempDir()
40 var removed, retained []string
41 for i, parent := range []string{root, filepath.Join(root, "versions", "v1"), filepath.Join(root, "staging")} {
42 removed = append(removed, reapTestStage(t, parent, fmt.Sprintf(".carlos-write-%032x", i+1), true))
43 }
44 removed = append(removed, reapTestStage(t, root, ".carlos-write-"+strings.Repeat("e", 32), false))
45 fresh := reapTestStage(t, root, ".carlos-write-"+strings.Repeat("f", 32), true)
46 now := time.Now()
47 if err := os.Chtimes(fresh, now, now); err != nil {
48 t.Fatal(err)
49 }
50 retained = append(retained, fresh)
51 active := reapTestStage(t, root, ".carlos-write-"+strings.Repeat("a", 32), true)
52 if err := os.Chtimes(filepath.Join(active, "data"), now, now); err != nil {
53 t.Fatal(err)
54 }
55 retained = append(retained, active)
56 for _, name := range []string{"ordinary", ".carlos-write-abc", ".carlos-write-" + strings.Repeat("A", 32), ".carlos-write-" + strings.Repeat("0", 33), ".carlos-write-" + strings.Repeat("g", 32)} {
57 retained = append(retained, reapTestStage(t, root, name, true))
58 }
59 if err := Reap(root); err != nil {
60 t.Fatal(err)
61 }
62 for _, path := range removed {
63 if _, err := os.Lstat(path); !os.IsNotExist(err) {
64 t.Errorf("stale stage remains: %s: %v", path, err)
65 }
66 }
67 for _, path := range retained {
68 if _, err := os.Stat(filepath.Join(path, "data")); err != nil {
69 t.Errorf("retained stage changed: %s: %v", path, err)
70 }
71 }
72 if err := Reap(filepath.Join(root, "missing")); err != nil {
73 t.Fatal(err)
74 }
75}
76
77func TestReapRefusesUnsafeStages(t *testing.T) {
78 for _, kind := range []string{"stage-symlink", "data-symlink", "hardlink", "fifo", "nested", "extra", "writable", "foreign-owner"} {
79 t.Run(kind, func(t *testing.T) {
80 root := t.TempDir()
81 outside := t.TempDir()
82 victim := filepath.Join(outside, "victim")
83 if err := os.WriteFile(victim, []byte("untouched"), 0o600); err != nil {
84 t.Fatal(err)
85 }
86 name := ".carlos-write-" + strings.Repeat("b", 32)
87 path := reapTestStage(t, root, name, false)
88 var err error
89 switch kind {
90 case "stage-symlink":
91 if err = os.Remove(path); err == nil {
92 err = os.Symlink(outside, path)
93 }
94 case "data-symlink":
95 err = os.Symlink(victim, filepath.Join(path, "data"))
96 case "hardlink":
97 err = os.Link(victim, filepath.Join(path, "data"))
98 case "fifo":
99 err = unix.Mkfifo(filepath.Join(path, "data"), 0o600)
100 case "nested":
101 reapTestStage(t, path, name, true)
102 case "extra":
103 err = os.WriteFile(filepath.Join(path, "unknown"), nil, 0o600)
104 case "writable":
105 err = os.Chmod(path, 0o777)
106 case "foreign-owner":
107 if os.Geteuid() != 0 {
108 t.Skip("requires root to make a foreign-owned stage")
109 }
110 err = os.Chown(path, 65534, 65534)
111 }
112 if err != nil {
113 t.Fatal(err)
114 }
115 if kind != "stage-symlink" {
116 reapTestAge(t, path)
117 }
118 if err := Reap(root); err != nil {
119 t.Fatal(err)
120 }
121 if _, err := os.Lstat(path); err != nil {
122 t.Fatalf("unsafe stage removed: %v", err)
123 }
124 if kind == "nested" {
125 if _, err := os.Stat(filepath.Join(path, name, "data")); err != nil {
126 t.Fatalf("unexpected recursive deletion inside stage: %v", err)
127 }
128 }
129 if body, err := os.ReadFile(victim); err != nil || string(body) != "untouched" {
130 t.Fatalf("victim changed: %q %v", body, err)
131 }
132 })
133 }
134}
135
136func TestReapPinsRootAndRefusesSwappedStage(t *testing.T) {
137 root := t.TempDir()
138 parent := filepath.Join(root, "parent")
139 out := filepath.Join(root, "outside")
140 name := ".carlos-write-" + strings.Repeat("c", 32)
141 stage := reapTestStage(t, parent, name, true)
142 victim := reapTestStage(t, out, name, true)
143 dir, err := OpenDir(parent)
144 if err != nil {
145 t.Fatal(err)
146 }
147 defer dir.Close()
148 pinned, err := OpenDir(stage)
149 if err != nil {
150 t.Fatal(err)
151 }
152 defer pinned.Close()
153 if err := os.Rename(stage, stage+"-moved"); err != nil {
154 t.Fatal(err)
155 }
156 if err := os.Symlink(victim, stage); err != nil {
157 t.Fatal(err)
158 }
159 if err := reapStage(int(dir.Fd()), name, pinned, time.Now().Add(-time.Hour)); err != nil {
160 t.Fatal(err)
161 }
162 if _, err := os.Stat(filepath.Join(stage+"-moved", "data")); err != nil {
163 t.Fatalf("replaced entry was not refused: %v", err)
164 }
165 if err := os.Rename(parent, parent+"-moved"); err != nil {
166 t.Fatal(err)
167 }
168 if err := os.Symlink(out, parent); err != nil {
169 t.Fatal(err)
170 }
171 if err := reapTree(dir, time.Now().Add(-time.Hour), 0); err != nil {
172 t.Fatal(err)
173 }
174 if _, err := os.Stat(filepath.Join(victim, "data")); err != nil {
175 t.Fatalf("followed swapped parent: %v", err)
176 }
177 if err := Reap(parent); err == nil {
178 t.Fatal("accepted symlink root")
179 }
180}
181
182func TestReapRefusesBindMount(t *testing.T) {
183 root := t.TempDir()
184 out := t.TempDir()
185 name := ".carlos-write-" + strings.Repeat("d", 32)
186 victim := reapTestStage(t, out, name, true)
187 mount := filepath.Join(root, "mounted")
188 if err := os.Mkdir(mount, 0o700); err != nil {
189 t.Fatal(err)
190 }
191 if err := unix.Mount(out, mount, "", unix.MS_BIND, ""); err != nil {
192 if err == unix.EPERM || err == unix.EACCES {
193 t.Skip("bind mounts unavailable")
194 }
195 t.Fatal(err)
196 }
197 defer func() {
198 if err := unix.Unmount(mount, 0); err != nil {
199 t.Errorf("unmount: %v", err)
200 }
201 }()
202 if err := Reap(root); err != nil {
203 t.Fatal(err)
204 }
205 if _, err := os.Stat(filepath.Join(victim, "data")); err != nil {
206 t.Fatalf("traversed bind mount: %v", err)
207 }
208}
209
210func TestReapContinuesPastUnreadableDirectory(t *testing.T) {
211 if os.Geteuid() == 0 {
212 t.Skip("root bypasses directory permissions")
213 }
214 root := t.TempDir()
215 unreadable := filepath.Join(root, "unreadable")
216 if err := os.Mkdir(unreadable, 0o700); err != nil {
217 t.Fatal(err)
218 }
219 if err := os.Chmod(unreadable, 0); err != nil {
220 t.Fatal(err)
221 }
222 defer os.Chmod(unreadable, 0o700)
223 stale := reapTestStage(t, root, ".carlos-write-"+strings.Repeat("9", 32), true)
224 if err := Reap(root); err == nil {
225 t.Fatal("unreadable directory was not reported")
226 }
227 if _, err := os.Lstat(stale); !os.IsNotExist(err) {
228 t.Fatalf("unreadable sibling prevented cleanup: %v", err)
229 }
230}
231