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 "fmt"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "syscall"
13
14 "golang.org/x/sys/unix"
15)
16
17// Holders is root's complete descriptor scan over paths (each matched by
18// name, and by the device and inode it has now) and files (by device and
19// inode). It is FDHolders with each descriptor's identity: strict, so an
20// unreadable process, task or descriptor fails the whole scan.
21func Holders(ctx context.Context, procRoot string, paths []string, files []FileIdentity) ([]Descriptor, error) {
22 return fdDescriptors(ctx, procRoot, paths, files, realFDOps())
23}
24
25// HoldersWithPrefixes includes every descriptor beneath the validated directory
26// prefixes, even if its file was created after launch or has been unlinked.
27// Each match retains the descriptor identity needed by ReplicatorOnly.
28func HoldersWithPrefixes(ctx context.Context, procRoot string, paths, prefixes []string, files []FileIdentity) ([]Descriptor, error) {
29 return fdDescriptorsCovered(ctx, procRoot, paths, prefixes, files, realFDOps())
30}
31
32// OwnDescriptors is the agent's half: the descriptors of one process's own
33// leader table that hold one of paths. It reads only /proc/<pid>/fd, so the
34// agent (uid carlos, no CAP_SYS_PTRACE) can take it for itself immediately
35// before and after it opens a replicator. An unreadable table is
36// ErrIncomplete, never an empty set.
37func OwnDescriptors(ctx context.Context, procRoot string, pid int, paths []string) ([]DescriptorHint, error) {
38 if ctx == nil || pid <= 0 || !filepath.IsAbs(procRoot) || len(paths) == 0 {
39 return nil, ErrInvalidInput
40 }
41 wanted := map[string]bool{}
42 for _, path := range paths {
43 if !filepath.IsAbs(path) {
44 return nil, ErrInvalidInput
45 }
46 wanted[filepath.Clean(path)] = true
47 }
48 ops := realFDOps()
49 inodes := map[fileID]bool{}
50 for path := range wanted {
51 fi, err := ops.stat(path)
52 if os.IsNotExist(err) {
53 continue
54 }
55 if err != nil {
56 return nil, fmt.Errorf("%w: %s: %w", ErrIncomplete, path, err)
57 }
58 id, ok := idOf(fi)
59 if !ok {
60 return nil, fmt.Errorf("%w: %s: no inode", ErrIncomplete, path)
61 }
62 inodes[id] = true
63 }
64 taskDir := filepath.Join(procRoot, strconv.Itoa(pid), "task", strconv.Itoa(pid))
65 if _, err := ops.stat(taskDir); err != nil {
66 return nil, fmt.Errorf("%w: %w", ErrIncomplete, err)
67 }
68 held, err := taskDescriptors(ctx, taskDir, pid, pid, wanted, inodes, ops)
69 if err != nil {
70 return nil, err
71 }
72 out := make([]DescriptorHint, 0, len(held))
73 for _, d := range held {
74 if d.Device == 0 || d.Inode == 0 {
75 return nil, fmt.Errorf("%w: fd %d: no identity", ErrIncomplete, d.FD)
76 }
77 out = append(out, DescriptorHint{FD: d.FD, Device: d.Device, Inode: d.Inode})
78 }
79 return out, nil
80}
81
82// ReplicatorOnly passes when every descriptor in held is one of the agent's
83// replicator descriptors: in the agent's own process, in its leader's table
84// or a task sharing it, at a number the hints name, on the file that hint
85// names. Hints only narrow: one root did not observe is ignored, and none
86// can exempt another process, an unshared thread or a zero identity. The
87// agent's start time is read before and after, so a reused pid fails. It
88// returns the exempted descriptors.
89func ReplicatorOnly(ctx context.Context, procRoot string, held []Descriptor, agent Agent, hints []DescriptorHint) ([]Descriptor, error) {
90 if ctx == nil || !filepath.IsAbs(procRoot) {
91 return nil, ErrInvalidInput
92 }
93 if len(held) == 0 {
94 return []Descriptor{}, nil
95 }
96 if agent.PID <= 0 || agent.Start == "" {
97 return nil, ErrInvalidInput
98 }
99 if err := agentStarted(procRoot, agent); err != nil {
100 return nil, err
101 }
102 allowed := map[DescriptorHint]bool{}
103 for _, h := range hints {
104 if h.FD >= 0 && h.Device != 0 && h.Inode != 0 {
105 allowed[h] = true
106 }
107 }
108 shared := map[int]bool{agent.PID: true}
109 out := make([]Descriptor, 0, len(held))
110 for _, d := range held {
111 if err := ctx.Err(); err != nil {
112 return nil, err
113 }
114 if d.PID != agent.PID || d.Device == 0 || d.Inode == 0 || !allowed[DescriptorHint{FD: d.FD, Device: d.Device, Inode: d.Inode}] {
115 return nil, fmt.Errorf("%w: pid %d tid %d fd %d", ErrForeignHolder, d.PID, d.TID, d.FD)
116 }
117 if _, ok := shared[d.TID]; !ok {
118 same, err := sameFDTable(procRoot, d.PID, d.TID)
119 if err != nil {
120 return nil, fmt.Errorf("%w: pid %d tid %d: %w", ErrIncomplete, d.PID, d.TID, err)
121 }
122 shared[d.TID] = same
123 }
124 if !shared[d.TID] {
125 return nil, fmt.Errorf("%w: pid %d tid %d has its own descriptor table", ErrForeignHolder, d.PID, d.TID)
126 }
127 out = append(out, d)
128 }
129 if err := agentStarted(procRoot, agent); err != nil {
130 return nil, err
131 }
132 return out, nil
133}
134
135// agentStarted checks the agent's pid still names the process root
136// authenticated.
137func agentStarted(procRoot string, agent Agent) error {
138 start, err := processStart(procRoot, agent.PID)
139 if err != nil {
140 return fmt.Errorf("%w: %w", ErrForeignHolder, err)
141 }
142 if start != agent.Start {
143 return fmt.Errorf("%w: pid %d was restarted", ErrForeignHolder, agent.PID)
144 }
145 return nil
146}
147
148func processStart(procRoot string, pid int) (string, error) {
149 raw, err := os.ReadFile(filepath.Join(procRoot, strconv.Itoa(pid), "stat"))
150 if err != nil {
151 return "", err
152 }
153 i := strings.LastIndexByte(string(raw), ')')
154 if i < 0 {
155 return "", ErrIncomplete
156 }
157 fields := strings.Fields(string(raw)[i+1:])
158 if len(fields) < 20 || fields[19] == "" {
159 return "", ErrIncomplete
160 }
161 return fields[19], nil
162}
163
164// kcmpFiles is kcmp(2)'s KCMP_FILES: the same descriptor table.
165const kcmpFiles = 2
166
167// sameFDTable reports whether task tid of pid shares pid's leader table.
168// kcmp(2) takes pids in the caller's namespace, so it is asked only for the
169// real /proc; any other root has no kernel to ask and fails closed. Tests
170// replace it.
171var sameFDTable = func(procRoot string, pid, tid int) (bool, error) {
172 if tid == pid {
173 return true, nil
174 }
175 if filepath.Clean(procRoot) != "/proc" {
176 return false, syscall.ENOSYS
177 }
178 r, _, errno := unix.Syscall6(unix.SYS_KCMP, uintptr(pid), uintptr(tid), kcmpFiles, 0, 0, 0)
179 if errno != 0 {
180 return false, errno
181 }
182 return r == 0, nil
183}
184