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