aboutsummaryrefslogtreecommitdiff
path: root/goapps/captree/captree.go
diff options
context:
space:
mode:
Diffstat (limited to 'goapps/captree/captree.go')
-rw-r--r--goapps/captree/captree.go468
1 files changed, 468 insertions, 0 deletions
diff --git a/goapps/captree/captree.go b/goapps/captree/captree.go
new file mode 100644
index 0000000..7768b11
--- /dev/null
+++ b/goapps/captree/captree.go
@@ -0,0 +1,468 @@
+// 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/<PID>/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 = "<zombie>"
+ 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)
+ os.Exit(1)
+ }()
+ 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.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] [pid|glob] ...\nOptions:\n", os.Args[0])
+ flag.PrintDefaults()
+ }
+ 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: "<kernel>",
+ }
+
+ // 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]")
+ }
+ }
+}