. "$BST_LIB" # libutempter 1.2.2-alt1 -- the library screen(1) and xterm(1) use to put a # terminal session into utmp/wtmp without being setuid themselves. # # --------------------------------------------------------------------------- # WHY THIS SUITE IS SHAPED THE WAY IT IS # # libutempter ships NO CLI. The only executable in the package is the privileged # helper, and the helper refuses to run unless its stdin is a pty MASTER -- which # a shell cannot arrange. So unlike libpaper next door, whose two CLIs were the # library's answer in printable form, every functional assertion here has to come # from a LINKED CONSUMER, and data/libutempter-probe (src/libutempter_probe.c, # cross-linked against the very /usr/lib/libutempter.a that ships) is it. # # What the probe is really asking about is not the ~150 lines of libutempter -- # a library that thin cannot fail on its own -- but the four platform mechanisms # they stand on, which no sibling port on this Epic exercises together: # # fork + execv the public API does no utmp work at all. utempter_add_record() # forks, dup2()s the caller's pty master over stdin and stdout, # and execv()s the helper. ONE API CALL IS A PROCESS BOUNDARY, # and the churn case takes 20 of them. # the devpts pair posix_openpt/grantpt/unlockpt/ptsname, validated the way a # privileged helper has to validate: the helper refuses unless # the slave name is under /dev/, fd 0 is O_RDWR, and THE SLAVE # IS OWNED BY THE CALLER. grantpt() and devpts ownership are # load-bearing here, not incidental. # NSS getpwuid() out of a static glibc, to name the record's user. # record locking pututline() rewrites in place and updwtmp() appends, and both # take an fcntl(F_SETLKW) write lock first. This is the only # port in the image whose correctness depends on POSIX record # locks working on this VFS. # # THE HELPER IS SILENT. diag.h compiles fatal_error() down to a bare # exit(EXIT_FAILURE) unless UTEMPTER_LOG or UTEMPTER_DEBUG is defined, and meta-oe # defines neither -- so every refusal is an exit status and NOTHING else. Every # helper case below therefore asserts rc and never message text. (libpaper taught # the same lesson one ticket earlier with its `(null)` program name: assert what # the binary does, not what you assume it says.) # # EXPECTATIONS COME FROM A NATIVE x86_64 CONTROL, never from reading the source. # data/libutempter-native.txt is the verbatim output of the same probe built # against a native build of the identical 1.2.2-alt1 source and run under a # private user+mount namespace with the same tmpfs /run, tmpfs /var/log and # helper location. The target's output is compared to it byte-for-byte IN THE # GUEST with cmp(1) -- the BRINTOS-260 lesson that a console dump truncates long # lines -- and then case by case so a red names itself. # --------------------------------------------------------------------------- HELPER=/usr/libexec/utempter/utempter HDR=/usr/include/utempter.h ARCHIVE=/usr/lib/libutempter.a PROBE=$BST_DATA/libutempter-probe NATIVE=$BST_DATA/libutempter-native.txt UTMP=/var/run/utmp WTMP=/var/log/wtmp if [ ! -f "$HELPER" ]; then bst_skip libutempter/present "libutempter not installed on this image" exit 0 fi bst_ok libutempter/present # --- the package is whole ---------------------------------------------------- check libutempter/helper-executable test -x "$HELPER" check libutempter/header test -s "$HDR" check libutempter/staticlib test -s "$ARCHIVE" check_out libutempter/header-new-api "utempter_add_record" cat "$HDR" check_out libutempter/header-old-api "addToUtmp" cat "$HDR" # The archive is a wasm bitcode archive, so ar(1)/nm(1) are not on this image; # strings(1) is, and the symbol names live in the object's name section. if command -v strings >/dev/null 2>&1; then syms=$(strings "$ARCHIVE" 2>/dev/null) check_out libutempter/staticlib-sym-add "utempter_add_record" printf '%s' "$syms" check_out libutempter/staticlib-sym-del "utempter_remove_record" printf '%s' "$syms" check_out libutempter/staticlib-sym-set "utempter_set_helper" printf '%s' "$syms" # The library locates its helper through UTEMPTER_DEFAULT_PATHNAME, and meta-oe's # stock recipe compiles that to the literal `"/usr/libexec"/utempter/utempter` -- # quote characters included -- because two disagreeing -DLIBEXECDIR definitions # reach the compiler and the 0002 patch stringizes the quoted one. # # This is a CHEAP static screen for that, not the gate. On this arm the archive # is ThinLTO bitcode: the symbol names above survive as plain text but string # CONSTANTS do not (they are bitcode-encoded .L.str blobs), so neither spelling # of the path is greppable and the case correctly skips. It earns its place for # the arms where the archive is a normal object -- and because a skip here plus # a red on add-default-helper below is a much narrower signal than the red alone. # The real gate is add-default-helper, which calls the library with no # utempter_set_helper() and therefore goes through the compiled-in path. case "$syms" in *'"/usr/libexec"/utempter/utempter'*) bst_notok libutempter/staticlib-helper-path \ "archive carries the quote-wrapped default helper path (meta-oe -DLIBEXECDIR defect)" ;; */usr/libexec/utempter/utempter*) bst_ok libutempter/staticlib-helper-path ;; *) bst_skip libutempter/staticlib-helper-path \ "archive is LTO bitcode; string constants are not plain text -- see libutempter/add-default-helper" ;; esac else bst_skip libutempter/staticlib-sym-add "strings not installed on this image" bst_skip libutempter/staticlib-sym-del "strings not installed on this image" bst_skip libutempter/staticlib-sym-set "strings not installed on this image" bst_skip libutempter/staticlib-helper-path "strings not installed on this image" fi # The distro is static-only; a shared object here would mean the bbappend's # TARGETS override stopped taking effect. if [ -e /usr/lib/libutempter.so ] || [ -e /usr/lib/libutempter.so.0 ]; then bst_notok libutempter/no-shared "a libutempter shared object is installed on a static-only distro" else bst_ok libutempter/no-shared fi # The man pages go to libutempter-doc, and this image installs no -doc packages, # so their absence is a packaging choice and not a fault. Assert them only when # they are actually here. if [ -s /usr/share/man/man3/utempter.3 ]; then bst_ok libutempter/man else bst_skip libutempter/man "libutempter-doc is not installed on this image" fi # --- the helper's refusals (rc only -- fatal_error prints nothing) ------------ # stdin is redirected from /dev/null on purpose: the helper must reach its own # argument checks rather than failing earlier on an fstat of a closed fd, and # ptsname() on a non-pty is what the last two cases are actually asking about. helper_rc() { _id=$1; _want=$2; shift 2 "$HELPER" "$@" /dev/null 2>&1 _rc=$? if [ "$_rc" -eq "$_want" ]; then bst_ok "$_id" else bst_notok "$_id" "rc=$_rc want=$_want"; fi } helper_rc libutempter/helper-no-args 1 helper_rc libutempter/helper-bad-verb 1 bogus helper_rc libutempter/helper-del-extraarg 1 del extra helper_rc libutempter/helper-add-nonpty 1 add somehost helper_rc libutempter/helper-del-nonpty 1 del # --- the two files glibc will write, and whether the image provided them ------ # glibc's pututline() opens _PATH_UTMP but never creates it, and updwtmp() is the # same for _PATH_WTMP. This image runs no populate-volatile step AND mounts none # of the tmpfs filesystems its own /etc/fstab lists, so on a fresh boot neither # file exists -- and /var/log is a symlink to volatile/log whose target directory # does not exist either. That is an image gap and not libutempter's, so each file # gets its own named case and the suite then provisions both itself rather than # letting every functional case go red for a reason that has nothing to do with # the library. What makes that possible at all is that / is mounted rw here. if [ -f "$UTMP" ]; then bst_ok libutempter/utmp-provisioned else bst_skip libutempter/utmp-provisioned \ "image provisions no $UTMP at boot (no populate-volatile); suite creates it" fi if [ -f "$WTMP" ]; then bst_ok libutempter/wtmp-provisioned else bst_skip libutempter/wtmp-provisioned \ "image provisions no $WTMP at boot (/var/log -> volatile/log is dangling); suite creates it" fi # EVERY creation below runs in a SUBSHELL, and that is load-bearing rather than # stylistic: `:` is a POSIX *special builtin*, so a failed redirection on it exits # the shell outright -- which on this runner means the rest of this file is never # executed and the suite reports a truncated pass list with no failure at all. The # subshell absorbs the exit. (Cost one iteration to find: /var/log/wtmp is the # redirect that fails, and the suite simply stopped after utmp-provisioned.) # Guarded so a genuinely provisioned utmp is never truncated out from under # whatever put it there; the probe matches on its own pty line, so pre-existing # records from other sessions do not interfere. ( mkdir -p /var/volatile/log ) 2>/dev/null [ -f "$UTMP" ] || ( : > "$UTMP" ) 2>/dev/null [ -f "$WTMP" ] || ( : > "$WTMP" ) 2>/dev/null if [ -w "$UTMP" ] && [ -w "$WTMP" ]; then bst_ok libutempter/utmp-writable else bst_notok libutempter/utmp-writable "$UTMP or $WTMP is not writable; the helper cannot record a session" exit 0 fi # --- the probe: the library actually running --------------------------------- if [ ! -f "$PROBE" ]; then bst_skip libutempter/probe-runs "data/libutempter-probe absent from the image" exit 0 fi probe=${TMPDIR:-/tmp}/bst-libutempter-probe.$$ pout=${TMPDIR:-/tmp}/bst-libutempter-out.$$ if ! cp "$PROBE" "$probe" 2>/dev/null || ! chmod +x "$probe" 2>/dev/null; then bst_notok libutempter/probe-runs "cannot stage the probe to $probe" exit 0 fi BST_HELPER=$HELPER BST_WHO=/usr/bin/who BST_CHURN=10 \ "$probe" >"$pout" 2>/dev/null prc=$? out=$(cat "$pout" 2>/dev/null) if [ "$prc" -eq 0 ]; then bst_ok libutempter/probe-runs else bst_notok libutempter/probe-runs "probe exited rc=$prc $(_bst_1line "$out")"; fi pv() { printf '%s\n' "$out" | awk -v k="$1" '$1==k{$1="";sub(/^ /,"");print;exit}'; } # --- POSIX record locking: the one thing that decides everything below -------- # # glibc takes an fcntl(F_SETLKW) WRITE LOCK before every pututline() and every # updwtmp(). On this platform that lock is refused, so the whole utmp/wtmp # subsystem is inert and libutempter -- which is a ~150-line wrapper around # exactly that subsystem -- cannot record anything however correct it is. # # Already tracked: brintos.io/brintos/linux/issues/16. sys_fcntl64 is not wired # in the wasm32 syscall table, and glibc on this arch (__TIMESIZE == 64) always # sends the LFS lock commands F_GETLK64/F_SETLK64/F_SETLKW64 (12/13/14), which # are that syscall's alone -- so every advisory lock in user space is refused. # The issue already names the utmp consequence, so this port adds a reproducer # and not a new report. # # This case stays a LOUD `not ok`: the defect is real, it is present in this # image, and downgrading it would hide the single fact that explains the rest. # The cases that are CONSEQUENCES of it are `skip`ped with the same issue named # -- runtime-blocked coverage rather than independent failures. Twenty identical # reds would bury the one that matters, and a `skip` that names its blocker is # what ticket_investigate asks of coverage that legitimately cannot run yet. BLOCK="blocked by brintos.io/brintos/linux/issues/16 (sys_fcntl64 unwired; glibc's utmp layer cannot take its record lock)" lock=$(pv P2-fcntl-lock) if [ "$lock" = "ok" ]; then bst_ok libutempter/fcntl-record-locking blocked=0 else bst_notok libutempter/fcntl-record-locking "fcntl(F_SETLKW) -> $lock; $BLOCK" blocked=1 fi # flock(2) is a different syscall with a different implementation, and it works # here -- which is what rules out "file locking is compiled out of this kernel" # and points at the fcntl64 wiring specifically. check_eq libutempter/flock-works ok "$(pv P2-flock)" # peq -- platform-independent assertion, always evaluated. # pequ -- depends on a successful utmp write; skipped, attributed, when blocked. # These MUST skip rather than assert: several of them expect a FAILURE # return, so with the lock refused they would pass for entirely the wrong # reason and report a false green. peq() { check_eq "libutempter/$1" "$3" "$(pv "$2")"; } pequ() { if [ "$blocked" -eq 1 ]; then bst_skip "libutempter/$1" "$BLOCK" else check_eq "libutempter/$1" "$3" "$(pv "$2")"; fi } # Aggregate: every line, compared in the guest against the native control. if [ ! -f "$NATIVE" ]; then bst_skip libutempter/matches-native "data/libutempter-native.txt absent from the image" elif [ "$blocked" -eq 1 ]; then bst_skip libutempter/matches-native "$BLOCK" elif cmp -s "$pout" "$NATIVE"; then bst_ok libutempter/matches-native else bst_notok libutempter/matches-native \ "probe output differs from the native x86_64 control; see the per-case results below" fi # the devpts pair the helper's validation rests on peq pty-openpt P1-openpt ok peq pty-grantpt P1-grantpt ok peq pty-unlockpt P1-unlockpt ok peq pty-ptsname P1-ptsname ok peq pty-slave-under-dev P1-slave-prefix ok peq pty-slave-owner P1-slave-owner ok peq pty-master-rdwr P1-master-rdwr ok # THE case that proves the repaired default helper path at RUNTIME: no # utempter_set_helper() is called before it, so it goes through # UTEMPTER_DEFAULT_PATHNAME. A 0 here with P7-explicit-helper at 1 is the exact # signature of the meta-oe -DLIBEXECDIR defect. pequ add-default-helper P3-add-default 1 # the record the helper wrote, field by field pequ rec-found P4-rec-found 1 pequ rec-type P4-rec-type USER_PROCESS pequ rec-user P4-rec-user root pequ rec-host P4-rec-host bst.example.org pequ rec-pid P4-rec-pid-is-ours 1 pequ rec-id P4-rec-id-is-line-tail 1 # updwtmp() is a separate open/lock/append from pututline()'s rewrite-in-place pequ wtmp-grew P9-wtmp-grew 1 # removal, and the saved-fd path that needs no fd argument pequ del P5-del 1 pequ del-type P5-rec-type-after DEAD_PROCESS pequ add-again P6-add-again 1 pequ remove-added P6-remove-added 1 pequ remove-added-type P6-rec-type-after DEAD_PROCESS pequ remove-added-twice P6-remove-added-twice 0 # utempter_set_helper() pequ set-helper-bad P7-bad-helper 0 pequ set-helper-good P7-explicit-helper 1 pequ set-helper-host P7-explicit-host bst-explicit # the deprecated interface, still exported from the archive pequ legacy-add P8-old-add-type USER_PROCESS pequ legacy-add-host P8-old-add-host bst-legacy pequ legacy-del P8-old-del-type DEAD_PROCESS pequ legacy-del-line P8-old-del-line-type DEAD_PROCESS # validate_hostname(): first char isgraph, the rest isprint pequ host-control-char P10-host-control-char 0 pequ host-leading-space P10-host-leading-space 0 pequ host-null P11-null-host 1 pequ host-null-empty P11-null-host-empty 1 # fds the helper must refuse pequ fd-closed P13-bad-fd 0 pequ fd-not-a-pty P14-non-pty-fd 0 # 20 forks and execs back to back pequ churn P12-churn 10/10 pequ churn-type P12-rec-type-after DEAD_PROCESS # a real in-image consumer reads what we wrote, across a process boundary pequ who-sees-record P15-who found peq probe-complete P-DONE ok rm -f "$probe" "$pout"