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 "fmt"
9 "os"
10 "path/filepath"
11 "sort"
12 "strconv"
13 "strings"
14 "syscall"
15)
16
17type fdOps struct {
18 readDir func(string) ([]os.DirEntry, error)
19 readlink func(string) (string, error)
20 stat func(string) (os.FileInfo, error)
21 lstat func(string) (os.FileInfo, error)
22}
23
24func realFDOps() fdOps { return fdOps{os.ReadDir, os.Readlink, os.Stat, os.Lstat} }
25
26func FDHolders(procRoot string, paths []string) ([]int, error) {
27 return fdHolders(context.Background(), procRoot, paths, realFDOps())
28}
29
30func fdHolders(ctx context.Context, procRoot string, paths []string, ops fdOps) ([]int, error) {
31 if len(paths) == 0 {
32 return nil, ErrInvalidInput
33 }
34 held, err := fdDescriptors(ctx, procRoot, paths, nil, ops)
35 if err != nil {
36 return nil, err
37 }
38 var holders []int
39 for _, d := range held {
40 if len(holders) == 0 || holders[len(holders)-1] != d.PID {
41 holders = append(holders, d.PID)
42 }
43 }
44 return holders, nil
45}
46
47// fdDescriptors is the complete descriptor scan: every task's table of
48// every process under procRoot, each descriptor matched by name against
49// paths or by device and inode against paths' files and files. A match
50// carries the identity stat read through the descriptor; a name match whose
51// file cannot be reached keeps a zero identity, which no hint exempts. Any
52// unreadable entry fails the whole scan. Results are ordered by process,
53// task and descriptor.
54func fdDescriptors(ctx context.Context, procRoot string, paths []string, files []FileIdentity, ops fdOps) ([]Descriptor, error) {
55 return fdDescriptorsCovered(ctx, procRoot, paths, nil, files, ops)
56}
57
58func fdDescriptorsCovered(ctx context.Context, procRoot string, paths, prefixes []string, files []FileIdentity, ops fdOps) ([]Descriptor, error) {
59 if ctx == nil || !filepath.IsAbs(procRoot) || len(paths)+len(prefixes)+len(files) == 0 {
60 return nil, ErrInvalidInput
61 }
62 for _, prefix := range prefixes {
63 if !filepath.IsAbs(prefix) || filepath.Clean(prefix) != prefix || prefix == string(os.PathSeparator) {
64 return nil, ErrInvalidInput
65 }
66 }
67 wanted := make(map[string]bool, len(paths))
68 for _, path := range paths {
69 if !filepath.IsAbs(path) {
70 return nil, ErrInvalidInput
71 }
72 wanted[filepath.Clean(path)] = true
73 }
74 // The same file reached by another name (a hard link, another mount of
75 // the file system) is the same inode: match on device and inode too.
76 inodes := map[fileID]bool{}
77 for _, f := range files {
78 if f.Device == 0 || f.Inode == 0 {
79 return nil, ErrInvalidInput
80 }
81 inodes[fileID{dev: f.Device, ino: f.Inode}] = true
82 }
83 for path := range wanted {
84 fi, err := ops.stat(path)
85 if errors.Is(err, os.ErrNotExist) {
86 continue
87 }
88 if err != nil {
89 return nil, fmt.Errorf("%w: %s: %w", ErrIncomplete, path, err)
90 }
91 if id, ok := idOf(fi); ok {
92 inodes[id] = true
93 } else {
94 return nil, fmt.Errorf("%w: %s: no inode", ErrIncomplete, path)
95 }
96 }
97 if err := ctx.Err(); err != nil {
98 return nil, err
99 }
100 entries, err := ops.readDir(procRoot)
101 if err != nil {
102 return nil, fmt.Errorf("%w: %w", ErrIncomplete, err)
103 }
104 var held []Descriptor
105 for _, entry := range entries {
106 if err := ctx.Err(); err != nil {
107 return nil, err
108 }
109 pid, err := strconv.Atoi(entry.Name())
110 if err != nil || pid <= 0 {
111 continue
112 }
113 pidDir := filepath.Join(procRoot, entry.Name())
114 tasks, err := readProcDir(filepath.Join(pidDir, "task"), pidDir, ops)
115 if err != nil {
116 return nil, fmt.Errorf("%w: pid %d: %w", ErrIncomplete, pid, err)
117 }
118 for _, task := range tasks {
119 if err := ctx.Err(); err != nil {
120 return nil, err
121 }
122 tid, err := strconv.Atoi(task.Name())
123 if err != nil || tid <= 0 {
124 continue
125 }
126 taskDir := filepath.Join(pidDir, "task", task.Name())
127 found, err := taskDescriptorsCovered(ctx, taskDir, pid, tid, wanted, prefixes, inodes, ops)
128 if err != nil {
129 return nil, err
130 }
131 held = append(held, found...)
132 }
133 }
134 if err := ctx.Err(); err != nil {
135 return nil, err
136 }
137 sort.Slice(held, func(i, j int) bool {
138 a, b := held[i], held[j]
139 if a.PID != b.PID {
140 return a.PID < b.PID
141 }
142 if a.TID != b.TID {
143 return a.TID < b.TID
144 }
145 return a.FD < b.FD
146 })
147 return held, nil
148}
149
150// taskDescriptors reads one task's descriptor table.
151func taskDescriptors(ctx context.Context, taskDir string, pid, tid int, wanted map[string]bool, inodes map[fileID]bool, ops fdOps) ([]Descriptor, error) {
152 return taskDescriptorsCovered(ctx, taskDir, pid, tid, wanted, nil, inodes, ops)
153}
154
155func taskDescriptorsCovered(ctx context.Context, taskDir string, pid, tid int, wanted map[string]bool, prefixes []string, inodes map[fileID]bool, ops fdOps) ([]Descriptor, error) {
156 fdDir := filepath.Join(taskDir, "fd")
157 fds, err := readProcDir(fdDir, taskDir, ops)
158 if err != nil {
159 return nil, fmt.Errorf("%w: pid %d tid %d: %w", ErrIncomplete, pid, tid, err)
160 }
161 var held []Descriptor
162 for _, fd := range fds {
163 if err := ctx.Err(); err != nil {
164 return nil, err
165 }
166 n, err := strconv.Atoi(fd.Name())
167 if err != nil || n < 0 {
168 continue
169 }
170 fdPath := filepath.Join(fdDir, fd.Name())
171 target, err := ops.readlink(fdPath)
172 if procGone(err) {
173 continue
174 }
175 if err != nil {
176 return nil, fmt.Errorf("%w: pid %d tid %d fd %s: %w", ErrIncomplete, pid, tid, fd.Name(), err)
177 }
178 named := filepath.IsAbs(target) && (wanted[filepath.Clean(target)] || wanted[filepath.Clean(strings.TrimSuffix(target, " (deleted)"))])
179 if filepath.IsAbs(target) && !named {
180 name := filepath.Clean(strings.TrimSuffix(target, " (deleted)"))
181 for _, prefix := range prefixes {
182 if name == prefix || strings.HasPrefix(name, prefix+string(os.PathSeparator)) {
183 named = true
184 break
185 }
186 }
187 }
188 if !named && (len(inodes) == 0 || !filepath.IsAbs(target)) {
189 continue // sockets, pipes and anon inodes name no file
190 }
191 // Stat through the fd link reaches the open file itself, whatever
192 // name it was opened by. An unreadable entry fails the whole scan.
193 fi, err := ops.stat(fdPath)
194 if procGone(err) {
195 // Closed only if the entry itself is gone. An entry that still
196 // exists but cannot be followed is unreadable, not closed.
197 if _, lerr := ops.lstat(fdPath); procGone(lerr) {
198 continue
199 } else if lerr == nil {
200 err = fmt.Errorf("fd entry exists but its file cannot be reached: %w", err)
201 } else {
202 err = lerr
203 }
204 }
205 if named && err != nil {
206 // The name alone is the match; its identity stays unknown.
207 held = append(held, Descriptor{PID: pid, TID: tid, FD: n})
208 continue
209 }
210 if err != nil {
211 return nil, fmt.Errorf("%w: pid %d tid %d fd %s: %w", ErrIncomplete, pid, tid, fd.Name(), err)
212 }
213 id, ok := idOf(fi)
214 if !ok {
215 if named {
216 held = append(held, Descriptor{PID: pid, TID: tid, FD: n})
217 continue
218 }
219 return nil, fmt.Errorf("%w: pid %d tid %d fd %s: no inode", ErrIncomplete, pid, tid, fd.Name())
220 }
221 if named || inodes[id] {
222 held = append(held, Descriptor{PID: pid, TID: tid, FD: n, Device: id.dev, Inode: id.ino})
223 }
224 }
225 return held, nil
226}
227
228func scanFDs(ctx context.Context, root string, paths []string) ([]int, error) {
229 return fdHolders(ctx, root, paths, realFDOps())
230}
231
232func procGone(err error) bool {
233 return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ESRCH)
234}
235
236func readProcDir(path, owner string, ops fdOps) ([]os.DirEntry, error) {
237 entries, err := ops.readDir(path)
238 if procGone(err) {
239 if _, ownerErr := ops.stat(owner); procGone(ownerErr) {
240 return nil, nil
241 } else if ownerErr != nil {
242 return nil, ownerErr
243 }
244 }
245 return entries, err
246}
247
248type fileID struct{ dev, ino uint64 }
249
250func idOf(fi os.FileInfo) (fileID, bool) {
251 st, ok := fi.Sys().(*syscall.Stat_t)
252 if !ok {
253 return fileID{}, false
254 }
255 return fileID{dev: uint64(st.Dev), ino: st.Ino}, true
256}
257