captree: exploring process trees

Understanding the ways capabilities are used on a Linux system can be done by grep'ing /proc files. However, decoding 64-bit hex dumps (for example with capsh --decode) can be a little tiresome. For this reason, we've created the captree program. This article provides a write-up for what it does and how to use it.

The captree program was inspired by the utility pstree, but it uses the libcap/cap (Go package) API to explore process runtime state and display the capability status of processes and threads. If the Go build is enabled when you build the libcap sources, the captree program is built as the binary go/captree. Performing an install should install captree alongside getpcaps and friends.

The sources are here. If your distribution doesn't install it, you can build the binary yourself in one of two ways:

  1. Build it raw from its single source file (this builds against a released version of libcap and for this to work you need wget, gcc and go (or golang) packages installed):

$ mkdir testing

$ cd testing

$ wget https://git.kernel.org/pub/scm/libs/libcap/libcap.git/plain/goapps/captree/captree.go

$ go mod init captree

$ go mod tidy

$ CGO_ENABLED=0 go build captree

  1. Build it from HEAD of the libcap git repository (for this to work, you need to have these packages installed git, gcc, make, diffutils and go (or golang).):

$ git clone git://git.kernel.org/pub/scm/libs/libcap/libcap.git

$ cd libcap/go

$ make GOLANG=yes

Whichever way you build it, you should now be able to run it:

$ ./captree --verbose $$

--bash(476) "=" []

+-captree(5324+{5325,5326,5327,5328,5329,5330}) "=" []

The last command above is your first invocation of captree. It shows that the captree binary itself has been run under the user shell bash, neither of which are running with any capabilities. The quoted part, "=" captures the text representation of the POSIX.1e capabilities, and the [] part captures the text representation of the IAB tuple for the process. Both of these quantities represent the empty value. Since most of the processes in a system run without privilege of any sort, the captree program generally omits the empty variant of these text representations. By providing the --verbose command line flag, we force them to be printed. Without that flag (and demonstrating how to suppress the red colored target PID), we get the less cluttered output:

$ ./captree --color=false $$

--bash(476)

+-captree(3795+{3796,3797,3798,3799,3800,3801})

The basic command line syntax for captree is you list the process IDs (PIDs) of interest and it explores the process map from that specified PID. The main process thread is the first number inside the parentheses (...) and sub-process threads are listed inside braces {...}. If you are familiar with Go, the fact that the implicitly parallel Go binary is using more than one thread won't be a surprise.

The example invocation above is pretty boring. Something else to try is to direct it at a specific target binary, by name:

$ ./captree 'cron'

--cron(93) "=ep"

This shows that cron, the runner of all crontab entries, has full system privileges. If you think about that for a moment, you might wonder if that isn't more privilege than it strictly needs?

You can list a range of targets, and use globs to match them (on my system there is only one match for 'cr*', cron again):

$ ./captree 'cr*'

--cron(93) "=ep"

On my Chromebook, linux-in-a-container, I can get some more interesting output from:

$ ./captree 'systemd-log*'

--systemd-logind(5212) "cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_kill,cap_sys_admin,cap_sys_tty_config,cap_audit_control,cap_mac_admin=ep" [!cap_fsetid,!cap_setgid,!cap_setuid,!cap_setpcap,!cap_linux_immutable,!cap_net_bind_service,!cap_net_broadcast,!cap_net_admin,!cap_net_raw,!cap_ipc_lock,!cap_ipc_owner,!cap_sys_module,!cap_sys_rawio,!cap_sys_chroot,!cap_sys_ptrace,!cap_sys_pacct,!cap_sys_boot,!cap_sys_nice,!cap_sys_resource,!cap_sys_time,!cap_mknod,!cap_lease,!cap_audit_write,!cap_setfcap,!cap_mac_override,!cap_syslog,!cap_wake_alarm,!cap_block_suspend,!cap_audit_read]

In this case, we have both Permitted and Effective capabilities and we can see that that process is blocking a whole selection of capabilities via the Bounding set to its children (should it have any).

The captree program supports a number of command line flags, which must all be supplied before the first of the process targets to take effect. We previously introduced the --verbose and the --color arguments, the others are all listed as follows:

$ ./captree --help

Usage of go/captree:

-color

color targeted PIDs on tty in red (default true)

-colour

colour targeted PIDs on tty in red (default true)

-depth int

how many processes deep (0=all)

-proc string

root of proc filesystem (default "/proc")

-verbose

display empty capabilities

If you don't specify a process target, captree defaults to selecting process 1. (The process target 0 means everything including kernel launched processes.)

You can compare the output of these commands to see the effect of --depth=n:

$ ./captree --depth=1

$ ./captree --depth=2

$ ./captree

At the present time most Linux packagers seem to avoid building the Go parts distributed with libcap and thus don't build captree by default. Since I plan to use it to explain some other things, in other examples, I figured I would provide this write-up of how to compile and use it for yourself first...