. "$BST_LIB" # liblockfile 1.17 (Debian; meta-oe recipes-extended) -- the NFS-safe dot-locking # library behind mailbox locking. Installed payload on this image is # /usr/lib/liblockfile.a (-staticdev), /usr/include/{lockfile.h,maillock.h} # (-dev) and /usr/bin/dotlockfile (the CLI wrapper around the same code). # # --------------------------------------------------------------------------- # WHAT THIS PORT ACTUALLY PUTS AT RISK # # liblockfile does almost no string handling and no parsing. Its entire # correctness rests on ONE filesystem property, and that property is the thing # worth measuring on a filesystem as young as this one: # # lockfile_create() writes a uniquely-named temp file, link()s it to the # lock name, then lstat()s BOTH and decides it owns the lock iff # (st_rdev, st_ino) are equal. # # So a filesystem that reports the same st_ino for every file -- or that fails # to make link() a real hard link -- does not make this library fail. It makes # it SUCCEED FOR EVERY CALLER AT ONCE, silently, with no error raised anywhere # and every return code still L_SUCCESS. That is the BRINTOS-217 shape exactly # (oniguruma compiled and packaged perfectly and returned wrong offsets to every # caller), which is why this suite does not stop at payload shape. # # The probe's link-* group therefore measures the substrate with RAW SYSCALLS # before any liblockfile call happens, so a red there reads as the filesystem's # and not the library's; link-ino-distinct and multi-distinct-ino are the two # independent places the collapse would show. # # The dotlockfile-* group below is the only fork/exec coverage this port has: # `dotlockfile ` locks, fork()s, execvp()s the command, waitpid()s # with an alarm(30) heartbeat that lockfile_touch()es the lock on EINTR, and # unlinks the lock afterwards. The library's own fork path (run_helper's # fork+execl of LOCKPROG) is compiled in -- the archive carries run_helper, # is_maillock and mailgid, which is how MAILGROUP is asserted below -- but is # unreachable here by construction: it runs only when the temp-file open() gets # EACCES, and it refuses outright when geteuid()==0, which is every process on # this image. # # --------------------------------------------------------------------------- # WHY BUILTIN MATCHING INSTEAD OF check_out # # The probe runs ONCE and each payload file is read ONCE into a shell variable; # every assertion is a `case` glob against those. The cjson suite next door was # killed by the runner's per-suite bound at 156 cases written as check_out+cat: # on this runtime the forking is the cost, not the work. # # EVERY EXPECTED VALUE IS THE VALUE A NATIVE x86_64 BUILD OF THE IDENTICAL # RELEASE PRODUCES -- the probe compares internally and prints its own verdict, # and the native control scored 97/97 before any of this was run on target. # Four of those 97 are expected RED here, all from one kernel defect in # kill(pid, 0); the block at the end of this file names them and says why they # are left failing rather than skipped. # --------------------------------------------------------------------------- lib=/usr/lib/liblockfile.a incd=/usr/include dlf=/usr/bin/dotlockfile probe="$BST_DATA/lockfile-probe" nl=' ' hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } # grepa -- read a build-configuration fact off # the SHIPPED archive. grep -a rather than strings(1), which busybox need not # carry; the archive is LLVM bitcode, whose symbol table still holds these names # as raw bytes. grepa() { if grep -aq "$3" "$lib" 2>/dev/null; then _hit=1; else _hit=0; fi if [ "$2" = present ] && [ "$_hit" -eq 1 ]; then bst_ok "$1" elif [ "$2" = absent ] && [ "$_hit" -eq 0 ]; then bst_ok "$1" elif [ "$2" = present ]; then bst_notok "$1" "not in archive: $3" else bst_notok "$1" "unexpectedly in archive: $3"; fi } # An image built without liblockfile is a coverage gap, not a failure. if [ ! -s "$lib" ]; then bst_skip liblockfile/present "liblockfile.a not installed on this image" exit 0 fi bst_ok liblockfile/present # --- the shipped payload ----------------------------------------------------- hasfile liblockfile/archive "$lib" hasfile liblockfile/hdr-lockfile "$incd/lockfile.h" hasfile liblockfile/hdr-maillock "$incd/maillock.h" hasfile liblockfile/dotlockfile "$dlf" if [ -x "$dlf" ]; then bst_ok liblockfile/dotlockfile-exec else bst_notok liblockfile/dotlockfile-exec "not executable: $dlf"; fi # Upstream ships no pkg-config file and no cmake package files, unlike the # sibling library ports -- asserting their ABSENCE keeps a future recipe change # that starts shipping one from passing unnoticed. if [ -e /usr/lib/pkgconfig/liblockfile.pc ]; then bst_notok liblockfile/no-pkgconfig "upstream ships none, but one is installed" else bst_ok liblockfile/no-pkgconfig; fi # The two shared objects the stock meta-oe recipe would have built # (--enable-shared -> liblockfile.so, --with-libnfslock -> nfslock.so.0.1) must # NOT be here: this arm refuses shared objects distro-wide, and an LD_PRELOAD # NFS shim has nothing to interpose on in a statically-linked image. Unlike the # generic "no .so" case a sibling port would write, these two are specific to # switches this port's bbappend removes, so they are the bbappend's regression # guard. if [ -e /usr/lib/liblockfile.so ] || [ -e /usr/lib/liblockfile.so.1 ]; then bst_notok liblockfile/no-shared-lib "liblockfile.so present on a static-only arm" else bst_ok liblockfile/no-shared-lib; fi if [ -e /usr/lib/nfslock.so ] || [ -e /usr/lib/nfslock.so.0 ] || [ -e /usr/lib/nfslock.so.0.1 ]; then bst_notok liblockfile/no-nfslock "nfslock.so present; --with-libnfslock leaked back in" else bst_ok liblockfile/no-nfslock; fi # --- header contract --------------------------------------------------------- # The return codes are the library's whole error vocabulary and callers switch # on them by name, so a renumbering is an ABI break that no compile would catch. lfh=$(cat "$incd/lockfile.h" 2>/dev/null) # Normalise the #defines to NAME=VALUE in one sed pass, so the assertions below # pin the VALUES without depending on the header's tab layout -- which differs # per constant in this header (L_ERROR and L_PID carry two tabs, the rest one) # and would otherwise make a reformat look like an ABI change. defs=$(printf '%s\n' "$lfh" \ | sed -n 's/^#define[[:space:]][[:space:]]*\([A-Za-z_][A-Za-z0-9_]*\)[[:space:]][[:space:]]*\([0-9][0-9]*\).*/\1=\2/p') hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } hasline liblockfile/rc-success "L_SUCCESS=0" "$defs" hasline liblockfile/rc-namelen "L_NAMELEN=1" "$defs" hasline liblockfile/rc-tmplock "L_TMPLOCK=2" "$defs" hasline liblockfile/rc-tmpwrite "L_TMPWRITE=3" "$defs" hasline liblockfile/rc-maxtrys "L_MAXTRYS=4" "$defs" hasline liblockfile/rc-error "L_ERROR=5" "$defs" hasline liblockfile/rc-manlock "L_MANLOCK=6" "$defs" hasline liblockfile/rc-orphaned "L_ORPHANED=7" "$defs" hasline liblockfile/rc-rmstale "L_RMSTALE=8" "$defs" hasline liblockfile/flag-pid "L_PID=16" "$defs" hasline liblockfile/flag-ppid "L_PPID=32" "$defs" hasline liblockfile/flag-interval "__L_INTERVAL=64" "$defs" has liblockfile/hdr-lockargs "struct __lockargs" "$lfh" mlh=$(cat "$incd/maillock.h" 2>/dev/null) has liblockfile/maildir-macro "MAILDIR" "$mlh" # configure found paths.h, so MAILDIR comes from 's _PATH_MAILDIR # rather than from the directory search fallback that hardcodes a spool path. has liblockfile/maildir-from-paths "include " "$mlh" # --- build-configuration conclusions, read off the SHIPPED archive ------------ # None of these survives into an installed header: autoconf.h is not installed # and rm_work removes the build tree, so the archive is the only durable record # of which branch actually compiled. # # 1. HAVE_UTIME. lockfile_touch() is utime(path,NULL) under HAVE_UTIME and # utimes(path,NULL) otherwise. Both probes pass on this sysroot and the # #ifdef prefers utime, so utime must be the referenced one -- a sysroot that # stopped exposing utime would flip this rather than silently changing which # syscall every touchlock() makes. grepa liblockfile/cfg-utime present 'utime' grepa liblockfile/cfg-no-utimes absent 'utimes' # 2. MAILGROUP. --with-mailgroup=mail is kept by the bbappend, and it is what # compiles is_maillock(), mailgid() and the run_helper() fork+execl fallback # at all. Their presence is the assertion; dropping the switch would delete # the library's entire privileged path silently. grepa liblockfile/cfg-mailgroup present 'is_maillock' grepa liblockfile/cfg-mailgid present 'mailgid' grepa liblockfile/cfg-run-helper present 'run_helper' grepa liblockfile/cfg-fork present 'fork' grepa liblockfile/cfg-execl present 'execl' # 3. No threads anywhere -- liblockfile has no thread support and must not have # acquired any. grepa liblockfile/cfg-no-pthread absent 'pthread' # 4. The nfslock translation unit must not be in the archive either. grepa liblockfile/cfg-no-nfslock absent 'nfslock' # --- symbol completeness, derived from the INSTALLED headers ------------------ # Every function the installed headers declare must be present in the installed # archive, so a partial compile, a visibility mistake or a header/library skew # all fail this. Derived rather than written out, so it survives a version bump. # lockfile_create2 is included: it sits behind #ifdef LOCKFILE_EXPERIMENTAL in # the header but is compiled unconditionally into the archive under -DSTATIC, # which the Makefile passes for liblockfile.a and NOT for the shared build -- # so its presence is itself the -DSTATIC conclusion. hdrsyms=$(cat "$incd/lockfile.h" "$incd/maillock.h" 2>/dev/null \ | sed -n 's/^\(int\|void\)[ ][ ]*\([a-z_][a-z0-9_]*\)(.*/\2/p' | sort -u) nsyms=$(printf '%s\n' "$hdrsyms" | grep -c .) missing="" for s in $hdrsyms; do grep -aq "$s" "$lib" 2>/dev/null || missing="$missing $s" done if [ -z "$missing" ]; then bst_ok liblockfile/syms-all-present else bst_notok liblockfile/syms-all-present "absent from archive:$missing"; fi # Pin the count so an empty or broken derivation cannot make the loop vacuously # green: lockfile.h declares 5 (create, remove, touch, check, create2) and # maillock.h declares 3 (maillock, touchlock, mailunlock). check_eq liblockfile/syms-count 8 "$nsyms" # And spot-check the two that carry the most meaning, so a derivation that # silently produced the wrong NAMES would still be caught. grepa liblockfile/sym-create2 present 'lockfile_create2' grepa liblockfile/sym-maillock present 'maillock' # --- the dotlockfile CLI, including the only fork/exec on this port ---------- w=${TMPDIR:-/tmp}/bst-lockfile.$$ rm -rf "$w"; mkdir -p "$w" || bst_notok liblockfile/workdir "cannot create $w" lk="$w/mbox.lock" if [ -x "$dlf" ]; then rm -f "$lk" check_rc liblockfile/dlf-lock 0 "$dlf" -r 0 "$lk" if [ -f "$lk" ]; then bst_ok liblockfile/dlf-lock-exists else bst_notok liblockfile/dlf-lock-exists "no lock file after -l"; fi # A second attempt on a fresh lock must report L_MAXTRYS (4), not succeed. # -r 0 keeps it from sleeping through the back-off ladder. check_rc liblockfile/dlf-lock-busy 4 "$dlf" -r 0 "$lk" check_rc liblockfile/dlf-check-valid 0 "$dlf" -c "$lk" check_rc liblockfile/dlf-touch 0 "$dlf" -t "$lk" check_rc liblockfile/dlf-unlock 0 "$dlf" -u "$lk" if [ -f "$lk" ]; then bst_notok liblockfile/dlf-unlock-gone "lock survived -u" else bst_ok liblockfile/dlf-unlock-gone; fi check_rc liblockfile/dlf-check-absent 1 "$dlf" -c "$lk" check_rc liblockfile/dlf-touch-absent 1 "$dlf" -t "$lk" # Removing an absent lock is success (ENOENT -> 0), matching lockfile_remove. check_rc liblockfile/dlf-unlock-absent 0 "$dlf" -u "$lk" # THE FORK/EXEC PATH. `dotlockfile ` takes the lock, forks, # execvp()s the command, waits, and unlinks the lock afterwards. out=$("$dlf" -r 0 "$lk" /bin/echo child-ran 2>&1); rc=$? case "$out" in *child-ran*) hit=1 ;; *) hit=0 ;; esac if [ "$rc" -eq 0 ] && [ "$hit" -eq 1 ]; then bst_ok liblockfile/dlf-run-cmd else bst_notok liblockfile/dlf-run-cmd "rc=$rc out:'$(_bst_1line "$out")'"; fi # ...and the lock must be released once the child is reaped. if [ -f "$lk" ]; then bst_notok liblockfile/dlf-run-unlocks "lock survived the command" else bst_ok liblockfile/dlf-run-unlocks; fi # -P passes the child's exit status through, which only works if fork, # execvp, waitpid and WEXITSTATUS all behave. check_rc liblockfile/dlf-passthrough 7 "$dlf" -r 0 -P "$lk" /bin/sh -c 'exit 7' # Without -P the child's status is discarded and dotlockfile reports 0. check_rc liblockfile/dlf-no-passthrough 0 "$dlf" -r 0 "$lk" /bin/sh -c 'exit 7' # A command that cannot be exec'd makes the CHILD exit 127; -P surfaces it. check_rc liblockfile/dlf-exec-fail 127 "$dlf" -r 0 -P "$lk" /nonexistent-command-259 # An invalid -r argument is rejected with L_ERROR before anything is locked. check_rc liblockfile/dlf-bad-retries 5 "$dlf" -q -r notanumber "$lk" rm -f "$lk" else bst_skip liblockfile/dlf-cli "dotlockfile not executable" fi # --- runtime: the library actually running against the shipped archive ------- # The recipe installs everything under data/ with `install -m 0644`, so the # payload is NOT executable where it lands: stage it to $TMPDIR and chmod +x, # the same way every sibling probe suite does. Testing `[ -x "$BST_DATA/..." ]` # instead would skip this entire half of the suite on a correctly-built image # and report that as coverage. stage=${TMPDIR:-/tmp}/bst-lockfile-probe.$$ if ! cp "$probe" "$stage" 2>/dev/null || ! chmod +x "$stage" 2>/dev/null; then bst_skip liblockfile/probe "lockfile-probe payload missing or not stageable" rm -f "$stage" rm -rf "$w" exit 0 fi pw="$w/probe" out=$("$stage" "$pw" 2>&1) rm -f "$stage" # One case per probe assertion, DERIVED from the probe's own output rather than # listed here -- a red inside the probe becomes a red here with its measured # value in the detail, and a probe that dies early loses cases rather than # silently passing, which the tally case below catches. printf '%s\n' "$out" | while IFS= read -r line; do case "$line" in LF-SUMMARY*|LF-SKIP*) continue ;; LF-*" OK"|LF-*" OK "*) id=${line#LF-}; id=${id%% *}; bst_ok "liblockfile/$id" ;; LF-*" FAIL"|LF-*" FAIL "*) id=${line#LF-}; id=${id%% *}; bst_notok "liblockfile/$id" "$(_bst_1line "$line")" ;; LF-*" SKIP "*) id=${line#LF-}; id=${id%% *}; bst_skip "liblockfile/$id" "$(_bst_1line "$line")" ;; esac done # The probe's TOTAL is what makes the derived cases above meaningful: without # it, a probe truncated after ten lines would report ten greens and no absence. # 97 is the case count a native x86_64 control of the identical release runs, # and that control scores 97/97. # # The number asserted here is the TOTAL and not the pass count, deliberately. # Four of the 97 are expected to be RED on this platform today, all four from # one kernel defect and all four attributed: # # kill-absent-esrch kill(pid, 0) must report ESRCH for a pid that cannot # exist. This kernel returns 0 for EVERY pid, including # one far above pid_max and including a child this probe # forked, watched exit, and reaped. # pid-stale so lockfile_check(lock, L_PID) never sees a dead owner, # pid-takeover so lockfile_create() cannot reclaim that lock, and # pid-takeover-is-self so the lock keeps naming the dead pid. # # The consequence is liblockfile's, but the defect is not: a lock written with # L_PID/L_PPID (dotlockfile -p) is NEVER reclaimed after its owner dies, because # the pid branch short-circuits the 5-minute mtime rule that would otherwise # free it. Locks taken WITHOUT a pid are unaffected and every one of those cases # is green, which is why the split matters. # # These four are left FAILING rather than skipped or relaxed: they flip green on # their own the day the kernel arm is fixed, and that is the regression signal # this suite exists to buy. See the ticket's Issues Found for the filed issue # and the root cause (arch/wasm32/kernel/signal.c, wasm32_sys_kill: the pid > 0 # sig == 0 arm returns 0 without any existence check, while the pid <= 0 arms # correctly walk the task lists and return -ESRCH). sum=$(printf '%s\n' "$out" | sed -n 's/^LF-SUMMARY //p') check_eq liblockfile/probe-total "97" "${sum#*/}" skips=$(printf '%s\n' "$out" | sed -n 's/^LF-SKIP //p') check_eq liblockfile/probe-skips "0" "$skips" rm -rf "$w"