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 "errors"
7 "os"
8 "path/filepath"
9 "slices"
10 "strconv"
11 "syscall"
12 "testing"
13)
14
15// holdersFixture is a fake /proc: pid 20 (the agent) holds the database
16// at fd 7 and its WAL at fd 8 in its leader table, and an unrelated file
17// at fd 9; pid 30 holds nothing watched. Each pid has a stat file whose
18// field 22 is "start-<pid>".
19func holdersFixture(t *testing.T) (root, db string) {
20 t.Helper()
21 root, dir := t.TempDir(), t.TempDir()
22 db = filepath.Join(dir, "data.db")
23 for _, name := range []string{db, db + "-wal", filepath.Join(dir, "other")} {
24 if err := os.WriteFile(name, nil, 0o600); err != nil {
25 t.Fatal(err)
26 }
27 }
28 link := func(pid, tid, fd int, target string) {
29 fdDir := filepath.Join(root, strconv.Itoa(pid), "task", strconv.Itoa(tid), "fd")
30 if err := os.MkdirAll(fdDir, 0o700); err != nil {
31 t.Fatal(err)
32 }
33 if err := os.Symlink(target, filepath.Join(fdDir, strconv.Itoa(fd))); err != nil {
34 t.Fatal(err)
35 }
36 }
37 link(20, 20, 7, db)
38 link(20, 20, 8, db+"-wal")
39 link(20, 20, 9, filepath.Join(dir, "other"))
40 link(30, 30, 3, filepath.Join(dir, "other"))
41 for _, pid := range []int{20, 30} {
42 writeStat(t, root, pid, "start-"+strconv.Itoa(pid))
43 }
44 return root, db
45}
46
47func writeStat(t *testing.T, root string, pid int, start string) {
48 t.Helper()
49 fields := "S"
50 for i := 4; i <= 21; i++ {
51 fields += " 0"
52 }
53 body := strconv.Itoa(pid) + " (carlos) " + fields + " " + start + " 0 0\n"
54 if err := os.WriteFile(filepath.Join(root, strconv.Itoa(pid), "stat"), []byte(body), 0o600); err != nil {
55 t.Fatal(err)
56 }
57}
58
59func identity(t *testing.T, path string) (uint64, uint64) {
60 t.Helper()
61 fi, err := os.Stat(path)
62 if err != nil {
63 t.Fatal(err)
64 }
65 st := fi.Sys().(*syscall.Stat_t)
66 return uint64(st.Dev), st.Ino
67}
68
69func dbPathsOf(db string) []string { return []string{db, db + "-wal", db + "-shm"} }
70
71func TestHoldersReportsEveryDescriptorWithItsIdentity(t *testing.T) {
72 root, db := holdersFixture(t)
73 got, err := Holders(t.Context(), root, dbPathsOf(db), nil)
74 if err != nil {
75 t.Fatal(err)
76 }
77 dev, ino := identity(t, db)
78 wdev, wino := identity(t, db+"-wal")
79 want := []Descriptor{{PID: 20, TID: 20, FD: 7, Device: dev, Inode: ino}, {PID: 20, TID: 20, FD: 8, Device: wdev, Inode: wino}}
80 if !slices.Equal(got, want) {
81 t.Fatalf("got %+v want %+v", got, want)
82 }
83 // A file named only by its identity (the launch journal's record) is
84 // found under whatever name it was opened by.
85 byID, err := Holders(t.Context(), root, nil, []FileIdentity{{Device: dev, Inode: ino}})
86 if err != nil || !slices.Equal(byID, want[:1]) {
87 t.Fatalf("by identity: %+v %v", byID, err)
88 }
89 if _, err := Holders(t.Context(), root, nil, nil); !errors.Is(err, ErrInvalidInput) {
90 t.Fatalf("nothing watched: %v", err)
91 }
92 if _, err := Holders(t.Context(), filepath.Join(root, "missing"), dbPathsOf(db), nil); !errors.Is(err, ErrIncomplete) {
93 t.Fatalf("unreadable root: %v", err)
94 }
95}
96
97func TestOwnDescriptorsReadsOnlyTheLeaderTable(t *testing.T) {
98 root, db := holdersFixture(t)
99 // Another process's table is never read: pid 30 has no stat for this
100 // call to fail on, and its descriptors never appear.
101 got, err := OwnDescriptors(t.Context(), root, 20, dbPathsOf(db))
102 if err != nil {
103 t.Fatal(err)
104 }
105 dev, ino := identity(t, db)
106 wdev, wino := identity(t, db+"-wal")
107 want := []DescriptorHint{{FD: 7, Device: dev, Inode: ino}, {FD: 8, Device: wdev, Inode: wino}}
108 if !slices.Equal(got, want) {
109 t.Fatalf("got %+v want %+v", got, want)
110 }
111 if err := os.Chmod(filepath.Join(root, "20", "task", "20", "fd"), 0); err != nil {
112 t.Fatal(err)
113 }
114 defer os.Chmod(filepath.Join(root, "20", "task", "20", "fd"), 0o700)
115 if os.Geteuid() != 0 {
116 if _, err := OwnDescriptors(t.Context(), root, 20, dbPathsOf(db)); !errors.Is(err, ErrIncomplete) {
117 t.Fatalf("an unreadable table is not an empty one: %v", err)
118 }
119 }
120}
121
122func TestReplicatorOnlyIsANarrowingHint(t *testing.T) {
123 root, db := holdersFixture(t)
124 dev, ino := identity(t, db)
125 wdev, wino := identity(t, db+"-wal")
126 agent := Agent{PID: 20, Start: "start-20"}
127 hints := []DescriptorHint{{FD: 7, Device: dev, Inode: ino}, {FD: 8, Device: wdev, Inode: wino}}
128 held := func(t *testing.T) []Descriptor {
129 got, err := Holders(t.Context(), root, dbPathsOf(db), nil)
130 if err != nil {
131 t.Fatal(err)
132 }
133 return got
134 }
135 t.Run("every holder is a hinted replicator descriptor", func(t *testing.T) {
136 got, err := ReplicatorOnly(t.Context(), root, held(t), agent, hints)
137 if err != nil || len(got) != 2 {
138 t.Fatalf("%+v %v", got, err)
139 }
140 })
141 t.Run("an unhinted descriptor in the agent itself", func(t *testing.T) {
142 if _, err := ReplicatorOnly(t.Context(), root, held(t), agent, hints[:1]); !errors.Is(err, ErrForeignHolder) {
143 t.Fatalf("got %v", err)
144 }
145 })
146 t.Run("a hint naming another file at the same number", func(t *testing.T) {
147 wrong := []DescriptorHint{{FD: 7, Device: wdev, Inode: wino}, hints[1]}
148 if _, err := ReplicatorOnly(t.Context(), root, held(t), agent, wrong); !errors.Is(err, ErrForeignHolder) {
149 t.Fatalf("got %v", err)
150 }
151 })
152 t.Run("a hint never widens to another process", func(t *testing.T) {
153 other := filepath.Join(root, "30", "task", "30", "fd", "4")
154 if err := os.Symlink(db, other); err != nil {
155 t.Fatal(err)
156 }
157 defer os.Remove(other)
158 extra := append(slices.Clone(hints), DescriptorHint{FD: 4, Device: dev, Inode: ino})
159 if _, err := ReplicatorOnly(t.Context(), root, held(t), agent, extra); !errors.Is(err, ErrForeignHolder) {
160 t.Fatalf("got %v", err)
161 }
162 })
163 t.Run("a hinted descriptor root does not see is ignored", func(t *testing.T) {
164 extra := append(slices.Clone(hints), DescriptorHint{FD: 99, Device: dev, Inode: ino})
165 got, err := ReplicatorOnly(t.Context(), root, held(t), agent, extra)
166 if err != nil || len(got) != 2 {
167 t.Fatalf("%+v %v", got, err)
168 }
169 })
170 t.Run("the agent's pid now names another process", func(t *testing.T) {
171 writeStat(t, root, 20, "start-reused")
172 defer writeStat(t, root, 20, "start-20")
173 if _, err := ReplicatorOnly(t.Context(), root, held(t), agent, hints); !errors.Is(err, ErrForeignHolder) {
174 t.Fatalf("got %v", err)
175 }
176 })
177 t.Run("a name-only match has no identity to exempt", func(t *testing.T) {
178 in := []Descriptor{{PID: 20, TID: 20, FD: 7}}
179 if _, err := ReplicatorOnly(t.Context(), root, in, agent, []DescriptorHint{{FD: 7}}); !errors.Is(err, ErrForeignHolder) {
180 t.Fatalf("got %v", err)
181 }
182 })
183 t.Run("a thread's table", func(t *testing.T) {
184 thread := filepath.Join(root, "20", "task", "21", "fd")
185 if err := os.MkdirAll(thread, 0o700); err != nil {
186 t.Fatal(err)
187 }
188 defer os.RemoveAll(filepath.Join(root, "20", "task", "21"))
189 if err := os.Symlink(db, filepath.Join(thread, "7")); err != nil {
190 t.Fatal(err)
191 }
192 prior := sameFDTable
193 defer func() { sameFDTable = prior }()
194 for _, tc := range []struct {
195 name string
196 shared bool
197 err error
198 want error
199 }{
200 {"shares the leader's", true, nil, nil},
201 {"unshared", false, nil, ErrForeignHolder},
202 {"cannot be compared", false, syscall.EPERM, ErrIncomplete},
203 } {
204 t.Run(tc.name, func(t *testing.T) {
205 sameFDTable = func(string, int, int) (bool, error) { return tc.shared, tc.err }
206 _, err := ReplicatorOnly(t.Context(), root, held(t), agent, hints)
207 if !errors.Is(err, tc.want) {
208 t.Fatalf("got %v want %v", err, tc.want)
209 }
210 })
211 }
212 })
213 t.Run("invalid agent", func(t *testing.T) {
214 if _, err := ReplicatorOnly(t.Context(), root, held(t), Agent{}, hints); !errors.Is(err, ErrInvalidInput) {
215 t.Fatalf("got %v", err)
216 }
217 })
218 t.Run("nothing held needs no agent proof", func(t *testing.T) {
219 got, err := ReplicatorOnly(t.Context(), root, nil, Agent{PID: 77, Start: "gone"}, nil)
220 if err != nil || len(got) != 0 {
221 t.Fatalf("%+v %v", got, err)
222 }
223 })
224}
225
226// FDHolders keeps its result and strictness after the refactor onto
227// Holders: the deleted-name match still counts.
228func TestFDHoldersStillMatchesNamesAndIdentities(t *testing.T) {
229 root, db := procFixture(t)
230 got, err := FDHolders(root, dbPathsOf(db))
231 if err != nil || !slices.Equal(got, []int{3, 20}) {
232 t.Fatalf("%v %v", got, err)
233 }
234}
235