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 stopevidence
4
5import (
6 "context"
7 "errors"
8 "os"
9 "path/filepath"
10 "reflect"
11 "runtime"
12 "strconv"
13 "syscall"
14 "testing"
15
16 "golang.org/x/sys/unix"
17)
18
19func procFixture(t *testing.T) (string, string) {
20 t.Helper()
21 root, db := t.TempDir(), filepath.Join(t.TempDir(), "data.db")
22 for pid, targets := range map[string][]string{
23 "20": {db, db + "-wal", db + "-shm"},
24 "3": {db + " (deleted)"},
25 "90": {db + ".other"},
26 } {
27 dir := filepath.Join(root, pid, "task", pid, "fd")
28 if err := os.MkdirAll(dir, 0700); err != nil {
29 t.Fatal(err)
30 }
31 for n, target := range targets {
32 if err := os.Symlink(target, filepath.Join(dir, strconv.Itoa(n))); err != nil {
33 t.Fatal(err)
34 }
35 }
36 }
37 return root, db
38}
39
40func TestFDHolders(t *testing.T) {
41 root, db := procFixture(t)
42 got, err := FDHolders(root, []string{filepath.Join(filepath.Dir(db), ".", "data.db"), db + "-wal", db + "-shm"})
43 if err != nil || !reflect.DeepEqual(got, []int{3, 20}) {
44 t.Fatal(got, err)
45 }
46 if _, err := FDHolders(root, []string{"relative.db"}); !errors.Is(err, ErrInvalidInput) {
47 t.Fatal(err)
48 }
49 if _, err := FDHolders(root, nil); !errors.Is(err, ErrInvalidInput) {
50 t.Fatal(err)
51 }
52 if got, err := FDHolders(filepath.Join(root, "missing"), []string{db}); !errors.Is(err, ErrIncomplete) || got != nil {
53 t.Fatal(got, err)
54 }
55}
56
57func TestFDScanRefusesIncompleteReads(t *testing.T) {
58 root, db := procFixture(t)
59 for _, where := range []string{"root", "fd-directory", "readlink"} {
60 t.Run(where, func(t *testing.T) {
61 ops := realFDOps()
62 readDir, readlink := ops.readDir, ops.readlink
63 ops.readDir = func(p string) ([]os.DirEntry, error) {
64 if (where == "root" && p == root) || (where == "fd-directory" && p == filepath.Join(root, "20", "task", "20", "fd")) {
65 return nil, syscall.EACCES
66 }
67 return readDir(p)
68 }
69 ops.readlink = func(p string) (string, error) {
70 if where == "readlink" && p == filepath.Join(root, "20", "task", "20", "fd", "0") {
71 return "", syscall.EACCES
72 }
73 return readlink(p)
74 }
75 got, err := fdHolders(t.Context(), root, []string{db}, ops)
76 if !errors.Is(err, ErrIncomplete) || !errors.Is(err, syscall.EACCES) || got != nil {
77 t.Fatalf("partial success: %v %v", got, err)
78 }
79 })
80 }
81 ops := realFDOps()
82 readDir := ops.readDir
83 ops.readDir = func(p string) ([]os.DirEntry, error) {
84 if p == filepath.Join(root, "20", "task", "20", "fd") {
85 return nil, os.ErrNotExist
86 }
87 return readDir(p)
88 }
89 if _, err := fdHolders(t.Context(), root, []string{db}, ops); !errors.Is(err, ErrIncomplete) {
90 t.Fatal("live PID with missing fd directory", err)
91 }
92 stat := ops.stat
93 ops.stat = func(p string) (os.FileInfo, error) {
94 if p == filepath.Join(root, "20", "task", "20") {
95 return nil, os.ErrNotExist
96 }
97 return stat(p)
98 }
99 got, err := fdHolders(t.Context(), root, []string{db}, ops)
100 if err != nil || !reflect.DeepEqual(got, []int{3}) {
101 t.Fatal("vanished process", got, err)
102 }
103 ops = realFDOps()
104 readlink := ops.readlink
105 ops.readlink = func(p string) (string, error) {
106 if p == filepath.Join(root, "20", "task", "20", "fd", "0") {
107 return "", os.ErrNotExist
108 }
109 return readlink(p)
110 }
111 if got, err := fdHolders(t.Context(), root, []string{db}, ops); err != nil || !reflect.DeepEqual(got, []int{3}) {
112 t.Fatal("closed fd", got, err)
113 }
114 ctx, cancel := context.WithCancel(context.Background())
115 cancel()
116 if _, err := fdHolders(ctx, root, []string{db}, realFDOps()); !errors.Is(err, context.Canceled) {
117 t.Fatal(err)
118 }
119}
120
121func TestFDHoldersRealProc(t *testing.T) {
122 f, err := os.CreateTemp(t.TempDir(), "open-db-")
123 if err != nil {
124 t.Fatal(err)
125 }
126 defer f.Close()
127 pid := os.Getpid()
128 // Project the real /proc entry into an otherwise empty scan root. This
129 // proves real descriptors work without requiring access to other UIDs.
130 root := t.TempDir()
131 if err := os.Symlink(filepath.Join("/proc", strconv.Itoa(pid)), filepath.Join(root, strconv.Itoa(pid))); err != nil {
132 t.Fatal(err)
133 }
134 got, err := FDHolders(root, []string{f.Name()})
135 if err != nil || !reflect.DeepEqual(got, []int{pid}) {
136 t.Fatal(got, err)
137 }
138 got, err = FDHolders("/proc", []string{f.Name()})
139 if errors.Is(err, ErrIncomplete) {
140 t.Log("whole-machine scan correctly refused; real self descriptor witness passed")
141 return
142 }
143 if err != nil {
144 t.Fatal(err)
145 }
146 for _, holder := range got {
147 if holder == pid {
148 return
149 }
150 }
151 t.Fatal("real /proc scan missed own descriptor", got)
152}
153
154func TestFDHoldersTaskTables(t *testing.T) {
155 root, db := procFixture(t)
156 for _, tid := range []string{"91", "92"} {
157 dir := filepath.Join(root, "90", "task", tid, "fd")
158 if err := os.MkdirAll(dir, 0700); err != nil {
159 t.Fatal(err)
160 }
161 if err := os.Symlink(db, filepath.Join(dir, "4")); err != nil {
162 t.Fatal(err)
163 }
164 }
165 got, err := FDHolders(root, []string{db})
166 if err != nil || !reflect.DeepEqual(got, []int{3, 20, 90}) {
167 t.Fatalf("task-only holders: %v %v", got, err)
168 }
169}
170
171func TestFDTaskScanErrors(t *testing.T) {
172 root, db := procFixture(t)
173 taskDir := filepath.Join(root, "90", "task", "91")
174 fdDir := filepath.Join(taskDir, "fd")
175 if err := os.MkdirAll(fdDir, 0700); err != nil {
176 t.Fatal(err)
177 }
178 if err := os.Symlink(db, filepath.Join(fdDir, "4")); err != nil {
179 t.Fatal(err)
180 }
181 for _, path := range []string{filepath.Join(root, "90", "task"), fdDir, filepath.Join(fdDir, "4")} {
182 for _, failure := range []error{syscall.EACCES, syscall.ENOENT, syscall.ESRCH} {
183 t.Run(filepath.Base(path)+"/"+failure.Error(), func(t *testing.T) {
184 ops := realFDOps()
185 readDir, readlink, stat := ops.readDir, ops.readlink, ops.stat
186 ops.readDir = func(p string) ([]os.DirEntry, error) {
187 if p == path {
188 return nil, failure
189 }
190 return readDir(p)
191 }
192 ops.readlink = func(p string) (string, error) {
193 if p == path {
194 return "", failure
195 }
196 return readlink(p)
197 }
198 ops.stat = func(p string) (os.FileInfo, error) {
199 if p == taskDir || p == filepath.Join(root, "90") {
200 return nil, failure
201 }
202 return stat(p)
203 }
204 got, err := fdHolders(t.Context(), root, []string{db}, ops)
205 if failure == syscall.EACCES {
206 if got != nil || !errors.Is(err, ErrIncomplete) || !errors.Is(err, failure) {
207 t.Fatalf("partial success: %v %v", got, err)
208 }
209 } else if err != nil || !reflect.DeepEqual(got, []int{3, 20}) {
210 t.Fatalf("vanished task: %v %v", got, err)
211 }
212 })
213 }
214 }
215}
216
217func TestFDHoldersUnsharedThread(t *testing.T) {
218 path := filepath.Join(t.TempDir(), "thread.db")
219 ready, release, done := make(chan error), make(chan struct{}), make(chan struct{})
220 leader := false
221 go func() {
222 runtime.LockOSThread()
223 defer close(done)
224 // The goroutine may land on the thread-group leader, whose table is
225 // /proc/<pid>/fd itself; the leader-invisibility check below only
226 // holds for a non-leader thread.
227 leader = unix.Gettid() == os.Getpid()
228 if err := unix.Unshare(unix.CLONE_FILES); err != nil {
229 ready <- err
230 return
231 }
232 fd, err := unix.Open(path, unix.O_RDWR|unix.O_CREAT|unix.O_CLOEXEC, 0600)
233 if err != nil {
234 ready <- err
235 return
236 }
237 defer unix.Close(fd)
238 ready <- nil
239 <-release
240 }()
241 defer func() { close(release); <-done }()
242 if err := <-ready; err != nil {
243 if errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) || errors.Is(err, unix.ENOSYS) {
244 t.Skipf("unshare unavailable: %v", err)
245 }
246 t.Fatal(err)
247 }
248 pid := strconv.Itoa(os.Getpid())
249 entries, err := os.ReadDir(filepath.Join("/proc", pid, "fd"))
250 if err != nil {
251 t.Fatal(err)
252 }
253 for _, entry := range entries {
254 target, err := os.Readlink(filepath.Join("/proc", pid, "fd", entry.Name()))
255 if err == nil && target == path && !leader {
256 t.Fatal("private descriptor visible in leader table")
257 }
258 }
259 root := t.TempDir()
260 if err := os.Symlink(filepath.Join("/proc", pid), filepath.Join(root, pid)); err != nil {
261 t.Fatal(err)
262 }
263 got, err := FDHolders(root, []string{path})
264 if err != nil || !reflect.DeepEqual(got, []int{os.Getpid()}) {
265 t.Fatalf("unshared thread holder: %v %v", got, err)
266 }
267}
268
269// A writer holding the database through another name (a hard link, another
270// mount of the same file system) holds the same inode; the scan must see it.
271func TestFDHoldersMatchesTheInodeNotOnlyThePath(t *testing.T) {
272 root, dir := t.TempDir(), t.TempDir()
273 db, alias := filepath.Join(dir, "data.db"), filepath.Join(dir, "alias.db")
274 if err := os.WriteFile(db, []byte("db"), 0600); err != nil {
275 t.Fatal(err)
276 }
277 if err := os.Link(db, alias); err != nil {
278 t.Fatal(err)
279 }
280 fd := filepath.Join(root, "40", "task", "40", "fd")
281 if err := os.MkdirAll(fd, 0700); err != nil {
282 t.Fatal(err)
283 }
284 if err := os.Symlink(alias, filepath.Join(fd, "0")); err != nil {
285 t.Fatal(err)
286 }
287 got, err := FDHolders(root, []string{db, db + "-wal"})
288 if err != nil || !reflect.DeepEqual(got, []int{40}) {
289 t.Fatalf("hard-linked holder missed: %v %v", got, err)
290 }
291 ops := realFDOps()
292 stat := ops.stat
293 ops.stat = func(p string) (os.FileInfo, error) {
294 if p == filepath.Join(fd, "0") {
295 return nil, syscall.EACCES
296 }
297 return stat(p)
298 }
299 if got, err := fdHolders(t.Context(), root, []string{db}, ops); !errors.Is(err, ErrIncomplete) || got != nil {
300 t.Fatalf("unreadable fd counted as not holding: %v %v", got, err)
301 }
302 ops = realFDOps()
303 ops.stat = func(p string) (os.FileInfo, error) {
304 if p == db {
305 return nil, syscall.EACCES
306 }
307 return stat(p)
308 }
309 if got, err := fdHolders(t.Context(), root, []string{db}, ops); !errors.Is(err, ErrIncomplete) || got != nil {
310 t.Fatalf("unreadable wanted path accepted: %v %v", got, err)
311 }
312 // A live entry whose target cannot be followed is unreadable, not closed;
313 // a vanished entry is closed.
314 for _, gone := range []bool{false, true} {
315 ops = realFDOps()
316 ops.stat = func(p string) (os.FileInfo, error) {
317 if p == filepath.Join(fd, "0") {
318 return nil, os.ErrNotExist
319 }
320 return stat(p)
321 }
322 if gone {
323 lstat := ops.lstat
324 ops.lstat = func(p string) (os.FileInfo, error) {
325 if p == filepath.Join(fd, "0") {
326 return nil, os.ErrNotExist
327 }
328 return lstat(p)
329 }
330 }
331 got, err := fdHolders(t.Context(), root, []string{db}, ops)
332 if gone && (err != nil || len(got) != 0) {
333 t.Fatalf("closed fd: %v %v", got, err)
334 }
335 if !gone && (!errors.Is(err, ErrIncomplete) || got != nil) {
336 t.Fatalf("live entry with an unreachable target counted as closed: %v %v", got, err)
337 }
338 }
339}
340