// Program captree explores a process tree rooted in the supplied // argument(s) and displays a process tree indicating the capabilities // of all the dependent PID values. // // This was inspired by the pstree utility. The key idea here, however, // is to explore a process tree for capability state. // // Each line of output is intended to capture a brief representation // of the capability state of a process (both *Set and *IAB) and // for its related threads. // // Ex: // // $ bash -c 'exec captree $$' // --captree(9758+{9759,9760,9761,9762}) // // In the normal case, such as the above, where the targeted process // is not privileged, no distracting capability strings are displayed. // Where a process is thread group leader to a set of other thread // ids, they are listed as `+{...}`. // // For privileged binaries, we have: // // $ captree 551 // --polkitd(551) "=ep" // :>-gmain{552} "=ep" // :>-gdbus{555} "=ep" // // That is, the text representation of the process capability state is // displayed in double quotes "..." as a suffix to the process/thread. // If the name of any thread of this process, or its own capability // state, is in some way different from the primary process then it is // displayed on a subsequent line prefixed with ":>-" and threads // sharing name and capability state are listed on that line. Here we // have two sub-threads with the same capability state, but unique // names. // // Sometimes members of a process group have different capabilities: // // $ captree 1368 // --dnsmasq(1368) "cap_net_bind_service,cap_net_admin,cap_net_raw=ep" // +-dnsmasq(1369) "=ep" // // Where the A and B components of the IAB tuple are non-default, the // output also includes these: // // $ captree 925 // --dbus-broker-lau(925) [!cap_sys_rawio,!cap_mknod] // +-dbus-broker(965) "cap_audit_write=eip" [!cap_sys_rawio,!cap_mknod,cap_audit_write] // // That is, the `[...]` appendage captures the IAB text representation // of that tuple. Note, if only the I part of that tuple is // non-default, it is already captured in the quoted process // capability state, so the IAB tuple is omitted. // // To view the complete system process map, rooted at the kernel, try // this: // // $ captree 0 // // To view a specific binary (as named in /proc//status as 'Name: // ...'), matched by a glob, try this: // // $ captree 'cap*ree' // // The quotes might be needed to avoid the '*' confusing your shell. package main import ( "flag" "fmt" "io/ioutil" "log" "os" "path/filepath" "sort" "strconv" "strings" "sync" "kernel.org/pub/linux/libs/security/libcap/cap" ) var ( proc = flag.String("proc", "/proc", "root of proc filesystem") depth = flag.Int("depth", 0, "how many processes deep (0=all)") verbose = flag.Bool("verbose", false, "display empty capabilities") color = flag.Bool("color", true, "color targeted PIDs on tty in red") colour = flag.Bool("colour", true, "colour targeted PIDs on tty in red") ) type task struct { mu sync.Mutex viewed bool depth int pid string cmd string cap *cap.Set iab *cap.IAB parent string threads []*task children []string } func (ts *task) String() string { return fmt.Sprintf("%s %q [%v] %s %v %v", ts.cmd, ts.cap, ts.iab, ts.parent, ts.threads, ts.children) } var ( wg sync.WaitGroup mu sync.Mutex colored bool ) func isATTY() bool { s, err := os.Stdout.Stat() if err == nil && (s.Mode()&os.ModeCharDevice) != 0 { return true } return false } func highlight(text string) string { if colored { return fmt.Sprint("\033[31m", text, "\033[0m") } return text } func (ts *task) fill(pid string, n int, thread bool) { defer wg.Done() wg.Add(1) go func() { defer wg.Done() c, _ := cap.GetPID(n) iab, _ := cap.IABGetPID(n) ts.mu.Lock() defer ts.mu.Unlock() ts.pid = pid ts.cap = c ts.iab = iab }() d, err := ioutil.ReadFile(fmt.Sprintf("%s/%s/status", *proc, pid)) if err != nil { ts.mu.Lock() defer ts.mu.Unlock() ts.cmd = "" ts.parent = "1" return } for _, line := range strings.Split(string(d), "\n") { if strings.HasPrefix(line, "Name:\t") { ts.mu.Lock() ts.cmd = line[6:] ts.mu.Unlock() continue } if strings.HasPrefix(line, "PPid:\t") { ppid := line[6:] if ppid == pid { continue } ts.mu.Lock() ts.parent = ppid ts.mu.Unlock() } } if thread { return } threads, err := ioutil.ReadDir(fmt.Sprintf("%s/%s/task", *proc, pid)) if err != nil { return } var ths []*task for _, t := range threads { tid := t.Name() if tid == pid { continue } n, err := strconv.ParseInt(pid, 10, 64) if err != nil { continue } thread := &task{} wg.Add(1) go thread.fill(tid, int(n), true) ths = append(ths, thread) } ts.mu.Lock() defer ts.mu.Unlock() ts.threads = ths } var empty = cap.NewSet() var noiab = cap.IABInit() // rDump prints out the tree of processes rooted at pid. func rDump(pids map[string]*task, requested map[string]bool, pid, stub, lstub, estub string, depth int) { info, ok := pids[pid] if !ok { panic("programming error") return } if info.viewed { // This process (tree) has already been viewed so skip // repeating it. return } info.viewed = true c := "" set := info.cap if set != nil { if val, _ := set.Cf(empty); val != 0 || *verbose { c = fmt.Sprintf(" %q", set) } } iab := "" tup := info.iab if tup != nil { if val, _ := tup.Cf(noiab); val.Has(cap.Bound) || val.Has(cap.Amb) || *verbose { iab = fmt.Sprintf(" [%s]", tup) } } var misc []*task var same []string for _, t := range info.threads { if val, _ := t.cap.Cf(set); val != 0 { misc = append(misc, t) continue } if val, _ := t.iab.Cf(tup); val != 0 { misc = append(misc, t) continue } if t.cmd != info.cmd { misc = append(misc, t) continue } same = append(same, t.pid) } tids := "" if len(same) != 0 { tids = fmt.Sprintf("+{%s}", strings.Join(same, ",")) } hPID := pid if requested[pid] { hPID = highlight(pid) requested[pid] = false } fmt.Printf("%s%s%s(%s%s)%s%s\n", stub, lstub, info.cmd, hPID, tids, c, iab) // loop over any threads that differ in capability state. for len(misc) != 0 { this := misc[0] var nmisc []*task var hPID = this.pid if requested[this.pid] { hPID = highlight(this.pid) requested[this.pid] = false } same := []string{hPID} for _, t := range misc[1:] { if val, _ := this.cap.Cf(t.cap); val != 0 { nmisc = append(nmisc, t) continue } if val, _ := this.iab.Cf(t.iab); val != 0 { nmisc = append(nmisc, t) continue } if this.cmd != t.cmd { nmisc = append(nmisc, t) continue } hPID = t.pid if requested[t.pid] { hPID = highlight(t.pid) requested[t.pid] = false } same = append(same, hPID) } c := "" set := this.cap if set != nil { if val, _ := set.Cf(empty); val != 0 || *verbose { c = fmt.Sprintf(" %q", set) } } iab := "" tup := this.iab if tup != nil { if val, _ := tup.Cf(noiab); val.Has(cap.Bound) || val.Has(cap.Amb) || *verbose { iab = fmt.Sprintf(" [%s]", tup) } } fmt.Printf("%s%s:>-%s{%s}%s%s\n", stub, estub, this.cmd, strings.Join(same, ","), c, iab) misc = nmisc } if depth == 1 { return } if depth > 1 { depth-- } x := info.children sort.Slice(x, func(i, j int) bool { a, _ := strconv.Atoi(x[i]) b, _ := strconv.Atoi(x[j]) return a < b }) stub = fmt.Sprintf("%s%s", stub, estub) lstub = "+-" for i, cid := range x { estub := "| " if i+1 == len(x) { estub = " " } rDump(pids, requested, cid, stub, lstub, estub, depth) } } func findPIDs(list []string, pids map[string]*task, glob string) <-chan string { finds := make(chan string) go func() { defer close(finds) found := false // search for PIDs, if found exit. for _, pid := range list { match, _ := filepath.Match(glob, pids[pid].cmd) if !match { continue } found = true finds <- pid } if found { return } fmt.Printf("no process matched %q\n", glob) }() return finds } func setDepth(pids map[string]*task, pid string) int { if pid == "0" { return 0 } x := pids[pid] if x.depth == 0 { x.depth = setDepth(pids, x.parent) + 1 } return x.depth } func main() { flag.Parse() // Honor the command line request if possible. colored = *color && *colour && isATTY() // Just in case the user wants to override this, we set the // cap package up to find it. cap.ProcRoot(*proc) pids := make(map[string]*task) pids["0"] = &task{ cmd: "", } // Ingest the entire process tree fs, err := ioutil.ReadDir(*proc) if err != nil { log.Fatalf("unable to open %q: %v", *proc, err) } for _, f := range fs { pid := f.Name() n, err := strconv.ParseInt(pid, 10, 64) if err != nil { continue } ts := &task{} mu.Lock() pids[pid] = ts mu.Unlock() wg.Add(1) go ts.fill(pid, int(n), false) } wg.Wait() var list []string for pid, ts := range pids { setDepth(pids, pid) list = append(list, pid) if pid == "0" { continue } if pts, ok := pids[ts.parent]; ok { pts.children = append(pts.children, pid) } } // Sort the process tree by tree depth - shallowest first, // with numerical order breaking ties. sort.Slice(list, func(i, j int) bool { x, y := pids[list[i]], pids[list[j]] if x.depth == y.depth { a, _ := strconv.Atoi(x.pid) b, _ := strconv.Atoi(y.pid) return a < b } return x.depth < y.depth }) args := flag.Args() if len(args) == 0 { args = []string{"1"} } wanted := make(map[string]int) requested := make(map[string]bool) for _, pid := range args { if _, err := strconv.ParseUint(pid, 10, 64); err == nil { requested[pid] = true if info, ok := pids[pid]; ok { wanted[pid] = info.depth continue } if requested[pid] { continue } requested[pid] = true continue } for pid := range findPIDs(list, pids, pid) { requested[pid] = true if info, ok := pids[pid]; ok { wanted[pid] = info.depth } } } var noted []string for pid, _ := range wanted { noted = append(noted, pid) } sort.Slice(noted, func(i, j int) bool { return wanted[noted[i]] < wanted[noted[j]] }) // We've boiled down the processes to a unique set of targets. for _, pid := range noted { rDump(pids, requested, pid, "", "--", " ", *depth) } for pid, missed := range requested { if missed { fmt.Println("[PID", pid, "not found]") } } }