# libcap 2.69 -- the POSIX.1e capability tools capsh, getcap, setcap and # getpcaps (package libcap-bin, all four in /usr/sbin). # # This port splits THREE ways on this kernel, and the suite is organised around # that split rather than around the four binaries: # # * pure userspace -- cap_from_text/cap_to_text, the capability name table and # capsh's built-in per-capability documentation. Correct regardless of the # kernel, so green here is a claim about the port and nothing else. # * capget(2) -- WIRED on this arm (the brintos/linux#10 fix carried on our # kernel fork, BRINTOS-81), so the PROCESS-credential half genuinely works: # capsh --print's Current: line (capget with pid=0, i.e. self) and # getpcaps (capget through find_task_by_vpid). These assert # unconditionally -- a red here is a red, and never becomes a skip. # * prctl(2) and the *xattr syscalls -- NOT wired, and scored THREE ways # rather than two. Each family has one WITNESS that is independent of # capget, and a case becomes an attributed `skip` only when that witness # proves the gap is present AND the case failed in the shape that gap # predicts. It is `ok` the moment the syscall works -- so each flips green by # itself when its fix reaches our base, with no edit here -- and `not ok` for # every other failure, so a genuinely new libcap regression stays visible # instead of being buried under a standing red. # # TRAP, measured against a native libcap 2.69 build before this file was # written: getcap exits 0 on EVERY path -- a missing file, an unreadable store, # a directory, anything. `check getcap ...` is therefore a false-green # generator, and so is any needle that getcap's own FAILURE line also prints # (it repeats the filename back). Every getcap assertion below reads STDOUT # ONLY -- the diagnostics go to stderr -- or compares the entire output. . "$BST_LIB" require capsh libcap/present # ok iff exits NON-zero AND its output carries . Several of these # tools signal a usage/parse error through both a status and a message; a # status-only check would go green on a kernel that failed the same call for an # entirely different reason. check_err() { _id=$1; _needle=$2; shift 2 _out=$("$@" 2>&1); _rc=$? case "$_out" in *"$_needle"*) _hit=1 ;; *) _hit=0 ;; esac if [ "$_rc" -ne 0 ] && [ "$_hit" -eq 1 ]; then bst_ok "$_id" else bst_notok "$_id" "rc=$_rc want:'$_needle' got:'$(_bst_1line "$_out")'"; fi } # has ... : true iff EVERY needle is a substring. has() { _h=$1; shift for _n in "$@"; do case "$_h" in *"$_n"*) ;; *) return 1 ;; esac; done return 0 } # gap_case # # The three-way score. is the family witness -- "this syscall is missing"; # is per-case -- "and THIS case failed the way that gap makes it # fail". Both are required for a skip, and they are deliberately separate: a case # that fails some OTHER way is still a red on a kernel that happens to be missing # the syscall, which is what keeps a real regression out of the skip bucket. gap_case() { _id=$1; _pass=$2; _gap=$3; _pred=$4; _issue=$5; _detail=$6 if [ "$_pass" -eq 1 ]; then bst_ok "$_id" elif bst_execfail "$_detail"; then bst_unrun "$_id" "$_detail" elif [ "$_gap" -eq 1 ] && [ "$_pred" -eq 1 ]; then bst_skip "$_id" "$_issue -- $(_bst_1line "$_detail")" else bst_notok "$_id" "$(_bst_1line "$_detail")"; fi } T=${TMPDIR:-/tmp}/bst-libcap.$$ mkdir -p "$T/sub" || exit 0 trap 'rm -rf "$T"' EXIT printf '#!/bin/sh\nexit 0\n' > "$T/f"; chmod 755 "$T/f" cp "$T/f" "$T/sub/g" # ---------------------------------------------------------------- packaging -- # R2 is about all four tools SHIPPING, not just the one `require` guarded on. for b in getcap setcap getpcaps; do check "libcap/$b-present" command -v "$b" done # ------------------------------------------------ pure userspace: text + docs -- # cap_to_text over a raw mask -- no syscall on this path at all. check_out libcap/decode-low "0x0000000000000003=cap_chown,cap_dac_override" \ capsh --decode=0x3 check_out libcap/decode-high "0x0000000000002000=cap_net_raw" capsh --decode=0x2000 # cap_from_text rejects an unknown name in the library, before capsh consults # the kernel at all -- so this half of --supports is userspace. The other half # (a name the library KNOWS) is not: it goes on to CAP_IS_SUPPORTED, i.e. # cap_get_bound, i.e. prctl -- so that case lives in the prctl block below. check_err libcap/supports-unknown "not recognized by library" capsh --supports=cap_bogus # capshdoc -- the compiled-in explanation table. check_out libcap/explain-header "cap_net_raw (13)" capsh --explain=cap_net_raw check_out libcap/explain-body "RAW sockets" capsh --explain=cap_net_raw # --------------------------------------------------- option grammar + status -- # Three binaries, three different conventions -- pinned deliberately, because a # port that silently changed one of them would still look like it "runs". check libcap/capsh-bare capsh # bare capsh exits 0 check_err libcap/capsh-bad-option "usage:" capsh --nonsense check_rc libcap/getcap-usage-rc 1 getcap # bare getcap exits 1 check_out libcap/getcap-help "displays the capabilities on the queried file(s)" getcap -h check_rc libcap/setcap-usage-rc 1 setcap check_out libcap/setcap-help "must be a regular (non-symlink) file" setcap -h check_rc libcap/getpcaps-usage-rc 1 getpcaps check_err libcap/getpcaps-badpid "Cannot parse pid" getpcaps notanumber # cap_from_text rejects the text before setcap reaches any syscall, so this is a # parser assertion and stays green on a kernel with no capability store at all. check_err libcap/setcap-bad-text "fatal error" setcap bogus_cap+ep "$T/f" # getcap's own path diagnostics come from lstat(), not from the capability # store, so they survive the missing xattr syscalls. check_out libcap/getcap-missing-file "No such file or directory" \ getcap /nonexistent-libcap-probe check_out libcap/getcap-not-regular "(Not a regular file)" getcap -v "$T/sub" # --------------------------------------------- capget(2): process credentials -- # capsh --print is captured ONCE and every derived assertion reads this one # capture, so the cases cannot disagree about which run they describe. PR=$(capsh --print 2>&1); PR_RC=$? check_eq libcap/print-rc 0 "$PR_RC" case "$PR" in *"Current: "*) bst_ok libcap/print-current ;; *) bst_notok libcap/print-current "$(_bst_1line "$PR")" ;; esac case "$PR" in *"uid=0"*) bst_ok libcap/print-uid ;; *) bst_notok libcap/print-uid "$(_bst_1line "$PR")" ;; esac # The content of the permitted set, not merely the shape of the line: the # browser runtime runs as root, so cap_chown must be permitted. This is the one # assertion that proves capget returned real data rather than an empty set. check libcap/has-p-chown capsh --has-p=cap_chown # getpcaps takes the OTHER capget path -- an explicit pid through # find_task_by_vpid, rather than pid=0/current. check_out libcap/getpcaps-pid1 "1: " getpcaps 1 check_out libcap/getpcaps-verbose "Capabilities for '1':" getpcaps --verbose 1 check_out libcap/getpcaps-self "$$: " getpcaps $$ # ------------------------------------------------------------------ prctl(2) -- # BRINTOS-WORKAROUND: linux#29 remove -- prctl(2) has no entry in the wasm32 # syscall table and no hardwarejs worker route, so the bounding set, the ambient # set, securebits and keepcaps are all unreadable. Every skip below goes with it. PRCTL_ISSUE="brintos/linux#29 prctl(2) unimplemented" # The WITNESS. `capsh --keep=1` is a bare prctl(PR_SET_KEEPCAPS, 1): # PR_SET_KEEPCAPS is unprivileged and capsh does not call cap_get_proc() on this # path, so it reaches prctl whatever capget is doing. That independence is the # whole point -- a witness that could also fail for capget's reason would let a # capget regression license every skip below it. kc=$(capsh --keep=1 2>&1); kc_rc=$? prctl_gap=0 if [ "$kc_rc" -ne 0 ] && has "$kc" "prctl(" "Function not implemented"; then prctl_gap=1 fi # The three cases below read the CONTENT of capsh --print's lines rather than its # exit status, because when prctl alone is missing capsh degrades GRACEFULLY -- # it prints "Bounding set = " and still exits 0, so a status check # would report green on exactly the kernel the case exists to probe. # # capsh --print calls cap_get_proc() FIRST and exit(1)s if that fails, so where # CAPGET is broken these lines are never printed at all. A plain substring test # cannot tell "prctl said unsupported" from "capsh died before reaching the # line", and the two clear on DIFFERENT fixes -- so prline reports a third value, # `absent`, which is NOT a predicted prctl shape and therefore never earns a skip. # # Securebits degrades in a second form: capsh replaces the whole line with # "[Securebits ABI not supported]" -- bracketed, no colon -- so that form counts # as `unsupported` too, and `absent` keeps its single meaning of "capsh never got # this far". prline() { printf '%s\n' "$PR" | awk -v p="$1" \ 'index($0,p)==1 { print ($0 ~ /unsupported/) ? "unsupported" : "readable"; f=1; exit } index($0,"[" p " ABI not supported]")==1 { print "unsupported"; f=1; exit } END { if (!f) print "absent" }' } prctl_line_case() { _v=$(prline "$2") _lp=0; [ "$_v" = readable ] && _lp=1 _lu=0; [ "$_v" = unsupported ] && _lu=1 gap_case "$1" "$_lp" "$prctl_gap" "$_lu" "$PRCTL_ISSUE" "want:'readable' got:'$_v'" } prctl_line_case libcap/bounding-set 'Bounding set' prctl_line_case libcap/ambient-set 'Ambient set' prctl_line_case libcap/securebits 'Securebits' # The three below reach prctl WITHOUT going through capsh --print's # cap_get_proc, so they isolate the prctl gap on its own. # --keep=1 IS the witness, scored here rather than re-run. kp=0; [ "$kc_rc" -eq 0 ] && kp=1 gap_case libcap/keepcaps "$kp" "$prctl_gap" "$prctl_gap" "$PRCTL_ISSUE" "rc=$kc_rc $kc" # --supports= on a name the library knows goes to CAP_IS_SUPPORTED, which is # cap_get_bound, which is prctl(PR_CAPBSET_READ). sk=$(capsh --supports=cap_chown 2>&1); sk_rc=$? skp=0; [ "$sk_rc" -eq 0 ] && skp=1 sku=0; has "$sk" "not supported by kernel" && sku=1 gap_case libcap/supports-known "$skp" "$prctl_gap" "$sku" "$PRCTL_ISSUE" "rc=$sk_rc $sk" # PR_CAPBSET_DROP, on top of a capset() that IS in the syscall table -- so the # shape it fails in is capsh's own "failed to drop", not an ENOSYS text. Where # capget is ALSO broken capsh dies earlier, with "Capabilities not available"; # that is a different shape and stays a red. db=$(capsh --drop=cap_chown --print 2>&1); db_rc=$? dbp=0; [ "$db_rc" -eq 0 ] && dbp=1 dbu=0; has "$db" "failed to drop" && dbu=1 gap_case libcap/drop-bset "$dbp" "$prctl_gap" "$dbu" "$PRCTL_ISSUE" "rc=$db_rc $db" # -------------------------------------------------- *xattr: file capabilities -- # BRINTOS-WORKAROUND: linux#27 remove -- the *xattr syscalls answer ENOSYS, so # the security.capability attribute setcap writes and getcap reads exists # nowhere. Every skip below goes with it. XATTR_ISSUE="brintos/linux#27 *xattr ENOSYS" # The WITNESS, taken BEFORE anything writes. getcap goes straight to # cap_get_file() with no credential step, so its diagnostic is the store's own # and cannot be confused with capget's. It exits 0 on every path, so the witness # reads its STDERR and never its status. gw=$(getcap "$T/sub/g" 2>&1 1>/dev/null) xattr_gap=0 has "$gw" "Failed to get capabilities of file" "Function not implemented" && xattr_gap=1 # setcap is NOT an equally direct probe, and the difference decides how a red # here reads: it calls cap_get_proc() to raise CAP_SETFCAP before it ever touches # the store, so where capget is broken it fails at the PRIVILEGE step ("unable to # get process capabilities") and never reaches setxattr. That text is none of the # shapes predicted below, so such a red stays a red -- a capget regression cannot # be laundered into an xattr skip. ss=$(setcap cap_net_raw+ep "$T/f" 2>&1); ss_rc=$? ssp=0; [ "$ss_rc" -eq 0 ] && ssp=1 ssu=0; has "$ss" "Failed to set capabilities on file" "Function not implemented" && ssu=1 gap_case libcap/setcap-set "$ssp" "$xattr_gap" "$ssu" "$XATTR_ISSUE" "rc=$ss_rc $ss" # Read back through STDOUT ONLY: getcap's failure line repeats the filename and # would satisfy a filename needle on precisely the broken kernel this probes. gc=$(getcap "$T/f" 2>/dev/null) gcp=0; has "$gc" cap_net_raw && gcp=1 gcu=0; [ -z "$gc" ] && gcu=1 gap_case libcap/getcap-readback "$gcp" "$xattr_gap" "$gcu" "$XATTR_ISSUE" "stdout:'$(_bst_1line "$gc")'" gr=$(getcap -r "$T" 2>/dev/null) grp=0; has "$gr" cap_net_raw && grp=1 gru=0; [ -z "$gr" ] && gru=1 gap_case libcap/getcap-recursive "$grp" "$xattr_gap" "$gru" "$XATTR_ISSUE" "stdout:'$(_bst_1line "$gr")'" # setcap -v substitutes an EMPTY set when cap_get_file fails (setcap.c:202-208), # so against a store that reads nothing it reports the requested caps as missing # -- "differs in [pe]" -- rather than an ENOSYS. That is this case's predicted # shape; a MISmatch on a working store is not. The capget warning is excluded # explicitly: setcap prints "differs" after failing to raise CAP_SETFCAP too, and # that red belongs to capget, so no setcap case is ever skipped while it is broken. sv=$(setcap -v cap_net_raw+ep "$T/f" 2>&1); sv_rc=$? svp=0; [ "$sv_rc" -eq 0 ] && svp=1 svu=0 has "$sv" "differs in [pe]" && ! has "$sv" "unable to get process capabilities" && svu=1 gap_case libcap/setcap-verify "$svp" "$xattr_gap" "$svu" "$XATTR_ISSUE" "rc=$sv_rc $sv" sr=$(setcap -r "$T/f" 2>&1); sr_rc=$? srp=0; [ "$sr_rc" -eq 0 ] && srp=1 sru=0; has "$sr" "Failed to set capabilities on file" "Function not implemented" && sru=1 gap_case libcap/setcap-remove "$srp" "$xattr_gap" "$sru" "$XATTR_ISSUE" "rc=$sr_rc $sr" # A file with no capabilities: getcap -v prints the bare filename on stdout and # nothing else. Where the store is unreadable stdout is EMPTY and the message # goes to stderr, so comparing the whole of stdout separates the two -- an # exit-status or substring check cannot. gv=$(getcap -v "$T/sub/g" 2>/dev/null) gvp=0; [ "$gv" = "$T/sub/g" ] && gvp=1 gvu=0; [ -z "$gv" ] && gvu=1 gap_case libcap/getcap-verbose-clean "$gvp" "$xattr_gap" "$gvu" "$XATTR_ISSUE" \ "want:'$T/sub/g' got:'$(_bst_1line "$gv")'"