xref: /aosp_15_r20/external/libcap/goapps/captree/captree.go (revision 2810ac1b38eead2603277920c78344c84ddf3aff)
1// Program captree explores a process tree rooted in the supplied
2// argument(s) and displays a process tree indicating the capabilities
3// of all the dependent PID values.
4//
5// This was inspired by the pstree utility. The key idea here, however,
6// is to explore a process tree for capability state.
7//
8// Each line of output is intended to capture a brief representation
9// of the capability state of a process (both *Set and *IAB) and
10// for its related threads.
11//
12// Ex:
13//
14//   $ bash -c 'exec captree $$'
15//   --captree(9758+{9759,9760,9761,9762})
16//
17// In the normal case, such as the above, where the targeted process
18// is not privileged, no distracting capability strings are displayed.
19// Where a process is thread group leader to a set of other thread
20// ids, they are listed as `+{...}`.
21//
22// For privileged binaries, we have:
23//
24//   $ captree 551
25//   --polkitd(551) "=ep"
26//     :>-gmain{552} "=ep"
27//     :>-gdbus{555} "=ep"
28//
29// That is, the text representation of the process capability state is
30// displayed in double quotes "..." as a suffix to the process/thread.
31// If the name of any thread of this process, or its own capability
32// state, is in some way different from the primary process then it is
33// displayed on a subsequent line prefixed with ":>-" and threads
34// sharing name and capability state are listed on that line. Here we
35// have two sub-threads with the same capability state, but unique
36// names.
37//
38// Sometimes members of a process group have different capabilities:
39//
40//   $ captree 1368
41//   --dnsmasq(1368) "cap_net_bind_service,cap_net_admin,cap_net_raw=ep"
42//     +-dnsmasq(1369) "=ep"
43//
44// Where the A and B components of the IAB tuple are non-default, the
45// output also includes these:
46//
47//   $ captree 925
48//   --dbus-broker-lau(925) [!cap_sys_rawio,!cap_mknod]
49//     +-dbus-broker(965) "cap_audit_write=eip" [!cap_sys_rawio,!cap_mknod,cap_audit_write]
50//
51// That is, the `[...]` appendage captures the IAB text representation
52// of that tuple. Note, if only the I part of that tuple is
53// non-default, it is already captured in the quoted process
54// capability state, so the IAB tuple is omitted.
55//
56// To view the complete system process map, rooted at the kernel, try
57// this:
58//
59//   $ captree 0
60//
61// To view a specific binary (as named in /proc/<PID>/status as 'Name:
62// ...'), matched by a glob, try this:
63//
64//   $ captree 'cap*ree'
65//
66// The quotes might be needed to avoid the '*' confusing your shell.
67package main
68
69import (
70	"flag"
71	"fmt"
72	"io/ioutil"
73	"log"
74	"os"
75	"path/filepath"
76	"sort"
77	"strconv"
78	"strings"
79	"sync"
80
81	"kernel.org/pub/linux/libs/security/libcap/cap"
82)
83
84var (
85	proc    = flag.String("proc", "/proc", "root of proc filesystem")
86	depth   = flag.Int("depth", 0, "how many processes deep (0=all)")
87	verbose = flag.Bool("verbose", false, "display empty capabilities")
88	color   = flag.Bool("color", true, "color targeted PIDs on tty in red")
89	colour  = flag.Bool("colour", true, "colour targeted PIDs on tty in red")
90)
91
92type task struct {
93	mu       sync.Mutex
94	viewed   bool
95	depth    int
96	pid      string
97	cmd      string
98	cap      *cap.Set
99	iab      *cap.IAB
100	parent   string
101	threads  []*task
102	children []string
103}
104
105func (ts *task) String() string {
106	return fmt.Sprintf("%s %q [%v] %s %v %v", ts.cmd, ts.cap, ts.iab, ts.parent, ts.threads, ts.children)
107}
108
109var (
110	wg      sync.WaitGroup
111	mu      sync.Mutex
112	colored bool
113)
114
115func isATTY() bool {
116	s, err := os.Stdout.Stat()
117	if err == nil && (s.Mode()&os.ModeCharDevice) != 0 {
118		return true
119	}
120	return false
121}
122
123func highlight(text string) string {
124	if colored {
125		return fmt.Sprint("\033[31m", text, "\033[0m")
126	}
127	return text
128}
129
130func (ts *task) fill(pid string, n int, thread bool) {
131	defer wg.Done()
132	wg.Add(1)
133	go func() {
134		defer wg.Done()
135		c, _ := cap.GetPID(n)
136		iab, _ := cap.IABGetPID(n)
137		ts.mu.Lock()
138		defer ts.mu.Unlock()
139		ts.pid = pid
140		ts.cap = c
141		ts.iab = iab
142	}()
143
144	d, err := ioutil.ReadFile(fmt.Sprintf("%s/%s/status", *proc, pid))
145	if err != nil {
146		ts.mu.Lock()
147		defer ts.mu.Unlock()
148		ts.cmd = "<zombie>"
149		ts.parent = "1"
150		return
151	}
152	for _, line := range strings.Split(string(d), "\n") {
153		if strings.HasPrefix(line, "Name:\t") {
154			ts.mu.Lock()
155			ts.cmd = line[6:]
156			ts.mu.Unlock()
157			continue
158		}
159		if strings.HasPrefix(line, "PPid:\t") {
160			ppid := line[6:]
161			if ppid == pid {
162				continue
163			}
164			ts.mu.Lock()
165			ts.parent = ppid
166			ts.mu.Unlock()
167		}
168	}
169	if thread {
170		return
171	}
172
173	threads, err := ioutil.ReadDir(fmt.Sprintf("%s/%s/task", *proc, pid))
174	if err != nil {
175		return
176	}
177	var ths []*task
178	for _, t := range threads {
179		tid := t.Name()
180		if tid == pid {
181			continue
182		}
183		n, err := strconv.ParseInt(pid, 10, 64)
184		if err != nil {
185			continue
186		}
187		thread := &task{}
188		wg.Add(1)
189		go thread.fill(tid, int(n), true)
190		ths = append(ths, thread)
191	}
192	ts.mu.Lock()
193	defer ts.mu.Unlock()
194	ts.threads = ths
195}
196
197var empty = cap.NewSet()
198var noiab = cap.IABInit()
199
200// rDump prints out the tree of processes rooted at pid.
201func rDump(pids map[string]*task, requested map[string]bool, pid, stub, lstub, estub string, depth int) {
202	info, ok := pids[pid]
203	if !ok {
204		panic("programming error")
205		return
206	}
207	if info.viewed {
208		// This process (tree) has already been viewed so skip
209		// repeating it.
210		return
211	}
212	info.viewed = true
213
214	c := ""
215	set := info.cap
216	if set != nil {
217		if val, _ := set.Cf(empty); val != 0 || *verbose {
218			c = fmt.Sprintf(" %q", set)
219		}
220	}
221	iab := ""
222	tup := info.iab
223	if tup != nil {
224		if val, _ := tup.Cf(noiab); val.Has(cap.Bound) || val.Has(cap.Amb) || *verbose {
225			iab = fmt.Sprintf(" [%s]", tup)
226		}
227	}
228	var misc []*task
229	var same []string
230	for _, t := range info.threads {
231		if val, _ := t.cap.Cf(set); val != 0 {
232			misc = append(misc, t)
233			continue
234		}
235		if val, _ := t.iab.Cf(tup); val != 0 {
236			misc = append(misc, t)
237			continue
238		}
239		if t.cmd != info.cmd {
240			misc = append(misc, t)
241			continue
242		}
243		same = append(same, t.pid)
244	}
245	tids := ""
246	if len(same) != 0 {
247		tids = fmt.Sprintf("+{%s}", strings.Join(same, ","))
248	}
249	hPID := pid
250	if requested[pid] {
251		hPID = highlight(pid)
252		requested[pid] = false
253	}
254	fmt.Printf("%s%s%s(%s%s)%s%s\n", stub, lstub, info.cmd, hPID, tids, c, iab)
255	// loop over any threads that differ in capability state.
256	for len(misc) != 0 {
257		this := misc[0]
258		var nmisc []*task
259		var hPID = this.pid
260		if requested[this.pid] {
261			hPID = highlight(this.pid)
262			requested[this.pid] = false
263		}
264		same := []string{hPID}
265		for _, t := range misc[1:] {
266			if val, _ := this.cap.Cf(t.cap); val != 0 {
267				nmisc = append(nmisc, t)
268				continue
269			}
270			if val, _ := this.iab.Cf(t.iab); val != 0 {
271				nmisc = append(nmisc, t)
272				continue
273			}
274			if this.cmd != t.cmd {
275				nmisc = append(nmisc, t)
276				continue
277			}
278			hPID = t.pid
279			if requested[t.pid] {
280				hPID = highlight(t.pid)
281				requested[t.pid] = false
282			}
283			same = append(same, hPID)
284		}
285		c := ""
286		set := this.cap
287		if set != nil {
288			if val, _ := set.Cf(empty); val != 0 || *verbose {
289				c = fmt.Sprintf(" %q", set)
290			}
291		}
292		iab := ""
293		tup := this.iab
294		if tup != nil {
295			if val, _ := tup.Cf(noiab); val.Has(cap.Bound) || val.Has(cap.Amb) || *verbose {
296				iab = fmt.Sprintf(" [%s]", tup)
297			}
298		}
299		fmt.Printf("%s%s:>-%s{%s}%s%s\n", stub, estub, this.cmd, strings.Join(same, ","), c, iab)
300		misc = nmisc
301	}
302	if depth == 1 {
303		return
304	}
305	if depth > 1 {
306		depth--
307	}
308	x := info.children
309	sort.Slice(x, func(i, j int) bool {
310		a, _ := strconv.Atoi(x[i])
311		b, _ := strconv.Atoi(x[j])
312		return a < b
313	})
314	stub = fmt.Sprintf("%s%s", stub, estub)
315	lstub = "+-"
316	for i, cid := range x {
317		estub := "| "
318		if i+1 == len(x) {
319			estub = "  "
320		}
321		rDump(pids, requested, cid, stub, lstub, estub, depth)
322	}
323}
324
325func findPIDs(list []string, pids map[string]*task, glob string) <-chan string {
326	finds := make(chan string)
327	go func() {
328		defer close(finds)
329		found := false
330		// search for PIDs, if found exit.
331		for _, pid := range list {
332			match, _ := filepath.Match(glob, pids[pid].cmd)
333			if !match {
334				continue
335			}
336			found = true
337			finds <- pid
338		}
339		if found {
340			return
341		}
342		fmt.Printf("no process matched %q\n", glob)
343		os.Exit(1)
344	}()
345	return finds
346}
347
348func setDepth(pids map[string]*task, pid string) int {
349	if pid == "0" {
350		return 0
351	}
352	x := pids[pid]
353	if x.depth == 0 {
354		x.depth = setDepth(pids, x.parent) + 1
355	}
356	return x.depth
357}
358
359func main() {
360	flag.Usage = func() {
361		fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] [pid|glob] ...\nOptions:\n", os.Args[0])
362		flag.PrintDefaults()
363	}
364	flag.Parse()
365
366	// Honor the command line request if possible.
367	colored = *color && *colour && isATTY()
368
369	// Just in case the user wants to override this, we set the
370	// cap package up to find it.
371	cap.ProcRoot(*proc)
372
373	pids := make(map[string]*task)
374	pids["0"] = &task{
375		cmd: "<kernel>",
376	}
377
378	// Ingest the entire process tree
379	fs, err := ioutil.ReadDir(*proc)
380	if err != nil {
381		log.Fatalf("unable to open %q: %v", *proc, err)
382	}
383	for _, f := range fs {
384		pid := f.Name()
385		n, err := strconv.ParseInt(pid, 10, 64)
386		if err != nil {
387			continue
388		}
389		ts := &task{}
390		mu.Lock()
391		pids[pid] = ts
392		mu.Unlock()
393		wg.Add(1)
394		go ts.fill(pid, int(n), false)
395	}
396	wg.Wait()
397
398	var list []string
399	for pid, ts := range pids {
400		setDepth(pids, pid)
401		list = append(list, pid)
402		if pid == "0" {
403			continue
404		}
405		if pts, ok := pids[ts.parent]; ok {
406			pts.children = append(pts.children, pid)
407		}
408	}
409
410	// Sort the process tree by tree depth - shallowest first,
411	// with numerical order breaking ties.
412	sort.Slice(list, func(i, j int) bool {
413		x, y := pids[list[i]], pids[list[j]]
414		if x.depth == y.depth {
415			a, _ := strconv.Atoi(x.pid)
416			b, _ := strconv.Atoi(y.pid)
417			return a < b
418		}
419		return x.depth < y.depth
420	})
421
422	args := flag.Args()
423	if len(args) == 0 {
424		args = []string{"1"}
425	}
426
427	wanted := make(map[string]int)
428	requested := make(map[string]bool)
429	for _, pid := range args {
430		if _, err := strconv.ParseUint(pid, 10, 64); err == nil {
431			requested[pid] = true
432			if info, ok := pids[pid]; ok {
433				wanted[pid] = info.depth
434				continue
435			}
436			if requested[pid] {
437				continue
438			}
439			requested[pid] = true
440			continue
441		}
442		for pid := range findPIDs(list, pids, pid) {
443			requested[pid] = true
444			if info, ok := pids[pid]; ok {
445				wanted[pid] = info.depth
446			}
447		}
448	}
449
450	var noted []string
451	for pid := range wanted {
452		noted = append(noted, pid)
453	}
454	sort.Slice(noted, func(i, j int) bool {
455		return wanted[noted[i]] < wanted[noted[j]]
456	})
457
458	// We've boiled down the processes to a unique set of targets.
459	for _, pid := range noted {
460		rDump(pids, requested, pid, "", "--", "  ", *depth)
461	}
462
463	for pid, missed := range requested {
464		if missed {
465			fmt.Println("[PID", pid, "not found]")
466		}
467	}
468}
469