diff options
author | Andrew G. Morgan <morgan@kernel.org> | 2021-08-22 15:47:16 -0700 |
---|---|---|
committer | Andrew G. Morgan <morgan@kernel.org> | 2021-08-22 15:55:24 -0700 |
commit | 596850bf55899c0217aa53fcff99491fbecdc2b2 (patch) | |
tree | e571da3887b764090c79347884a08f2ff49b0d1d | |
parent | ac297b51c6730c22a06b759bd4235b47c52053bc (diff) | |
download | libcap-596850bf55899c0217aa53fcff99491fbecdc2b2.tar.gz |
Add the captree example.
This is a small command line utility for doing something like pstree
but focused on revealing the full capability state of the processes
and threads shown.
This requires support provided in the cap.IABGetPID() function which
will debut in libcap-2.54. For now, the binary is only buildable from
HEAD in the git repository.
Signed-off-by: Andrew G. Morgan <morgan@kernel.org>
-rw-r--r-- | go/.gitignore | 1 | ||||
-rw-r--r-- | go/Makefile | 8 | ||||
-rw-r--r-- | goapps/captree/captree.go | 319 | ||||
-rw-r--r-- | goapps/captree/go.mod | 5 |
4 files changed, 331 insertions, 2 deletions
diff --git a/go/.gitignore b/go/.gitignore index 7b811c9..ac6056f 100644 --- a/go/.gitignore +++ b/go/.gitignore @@ -9,6 +9,7 @@ mknames web setid gowns +captree ok vendor go.sum diff --git a/go/Makefile b/go/Makefile index 6b69cbe..52e1f3e 100644 --- a/go/Makefile +++ b/go/Makefile @@ -15,7 +15,7 @@ PKGDIR=pkg/$(GOOSARCH)/$(IMPORTDIR) DEPS=../libcap/libcap.a ../libcap/libpsx.a -all: PSXGOPACKAGE CAPGOPACKAGE web setid gowns compare-cap try-launching psx-signals +all: PSXGOPACKAGE CAPGOPACKAGE web setid gowns captree compare-cap try-launching psx-signals $(DEPS): make -C ../libcap all @@ -70,6 +70,9 @@ setid: ../goapps/setid/setid.go CAPGOPACKAGE PSXGOPACKAGE gowns: ../goapps/gowns/gowns.go CAPGOPACKAGE CC="$(CC)" CGO_ENABLED="$(CGO_REQUIRED)" $(CGO_LDFLAGS_ALLOW) $(GO) build -mod=vendor -o $@ $< +captree: ../goapps/captree/captree.go CAPGOPACKAGE + CC="$(CC)" CGO_ENABLED="$(CGO_REQUIRED)" $(CGO_LDFLAGS_ALLOW) $(GO) build -mod=vendor -o $@ $< + ok: ok.go CC="$(CC)" CGO_ENABLED=0 $(GO) build -mod=vendor $< @@ -101,6 +104,7 @@ ifeq ($(CGO_REQUIRED),0) endif ./setid --caps=false ./gowns -- -c "echo gowns runs" + ./captree 0 # Note, the user namespace doesn't require sudo, but I wanted to avoid # requiring that the hosting kernel supports user namespaces for the @@ -127,7 +131,7 @@ install: all clean: rm -f *.o *.so *~ mknames ok good-names.go - rm -f web setid gowns + rm -f web setid gowns captree rm -f compare-cap try-launching try-launching-cgo rm -f $(topdir)/cap/*~ $(topdir)/psx/*~ rm -f b210613 psx-signals psx-signals-cgo diff --git a/goapps/captree/captree.go b/goapps/captree/captree.go new file mode 100644 index 0000000..527150c --- /dev/null +++ b/goapps/captree/captree.go @@ -0,0 +1,319 @@ +// 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 +// +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "sort" + "strconv" + "strings" + "sync" + + "kernel.org/pub/linux/libs/security/libcap/cap" +) + +var ( + proc = flag.String("proc", "/proc", "root of proc filesystem") +) + +type task struct { + mu sync.Mutex + 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 +) + +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, pid, stub, lstub, estub string) { + info, ok := pids[pid] + if !ok { + fmt.Println("[PID:", pid, "not found]") + return + } + c := "" + set := info.cap + if set != nil { + if val, _ := set.Cf(empty); val != 0 { + 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) { + 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, ",")) + } + fmt.Printf("%s%s%s(%s%s)%s%s\n", stub, lstub, info.cmd, pid, tids, c, iab) + // loop over any threads that differ in capability state. + for len(misc) != 0 { + this := misc[0] + var nmisc []*task + same := []string{this.pid} + 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 + } + same = append(same, t.pid) + } + c := "" + set := this.cap + if set != nil { + if val, _ := set.Cf(empty); val != 0 { + 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) { + 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 + } + 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, cid, stub, lstub, estub) + } +} + +func main() { + flag.Parse() + + pids := make(map[string]*task) + pids["0"] = &task{ + cmd: "<kernel>", + } + + 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 { + list = append(list, pid) + if pid == "0" { + continue + } + if pts, ok := pids[ts.parent]; ok { + pts.children = append(pts.children, pid) + } + } + sort.Slice(list, func(i, j int) bool { + a, _ := strconv.Atoi(list[i]) + b, _ := strconv.Atoi(list[j]) + return a < b + }) + + args := flag.Args() + if len(args) == 0 { + args = []string{"1"} + } + + for _, pid := range args { + rDump(pids, pid, "", "--", " ") + } +} diff --git a/goapps/captree/go.mod b/goapps/captree/go.mod new file mode 100644 index 0000000..ff1ad6c --- /dev/null +++ b/goapps/captree/go.mod @@ -0,0 +1,5 @@ +module captree + +go 1.16 + +require kernel.org/pub/linux/libs/security/libcap/cap v1.2.53 |