. "$BST_LIB" # nPth ("New Portable Threads") is GnuPG's threading library: the runtime # gpg-agent, dirmngr and scdaemon are built on. It is NOT a thread pool and not # a green-thread runtime. It is ONE BINARY SEMAPHORE -- nPth calls it the # sceptre -- plus a wrapper around every blocking call in libc that releases the # sceptre before the syscall and re-takes it after. Exactly one nPth thread runs # at a time, by construction; that is what lets GnuPG's daemons use it without # auditing their own data structures for concurrency. # # The distro is static-only, so poky's ${PN} glob (${libdir}/libnpth.so.*) # matches nothing and what the image gets is npth-dev + npth-staticdev: the # archive, two headers, npth.pc, the aclocal macro, and poky's # binconfig-disabled refusal stub in place of npth-config. # # --------------------------------------------------------------------------- # WHY THIS SUITE RUNS A PROBE # # npth installs NO PROGRAM AT ALL. Without a cross-linked consumer this suite # could only assert that files exist -- which would pass identically if every # function in the archive trapped on first call. BRINTOS-217 is why that is not # good enough: oniguruma compiled and packaged perfectly on this arm and then # returned silently wrong offsets to every caller, so a payload-only suite would # have gone green on the exact defect the port existed to find. # # data/npth-probe (src/npth_probe.c) is that consumer: cross-linked with # wasmcc-glibc against THE VERY ARCHIVE THAT SHIPS (/usr/lib/libnpth.a), # reached through the way a real consumer reaches it. The cross-link # reported refused=0 skipped=0 -- no coroutinize refusal anywhere in a module # linking this archive. # # HOW THE EXPECTATIONS WERE SET. Every value below is the value a NATIVE x86_64 # build of the identical npth 1.6 sources produces, captured from the same probe # source and shipped verbatim as data/npth-native.txt. The probe deliberately # prints only what cannot legitimately differ between two machines -- rcs # normalised to ok/fail, booleans, byte counts, literals. No pointers, no pids, # no clock readings, no sizeof. That is what makes the tag-by-tag comparison # below meaningful rather than decorative: one differing line is a defect. # # THE ANCHORED CASES EXIST BECAUSE AN ORACLE CAN BE WRONG. The bulk comparison # would not catch a control captured from an already-broken build, so the values # a specification fixes -- the timer arithmetic, the byte counts, the EBUSY # answers, the exit status -- are ALSO asserted against literals. Those hold # whatever the control says. # # WHAT IS RED HERE, AND WHY IT IS NOT THIS PORT'S FAULT. 90 of the control's 92 # lines reproduce byte for byte. Two do not, and both halves of nPth that need # the platform's signal and thread machinery are held by OPEN brintOS defects # that have nothing to do with npth: # # * H06/H07 -- sigev. npth_sigev_* is the interface GnuPG's daemons use: # block a set of signals, hand nPth the mask, collect them from the event # loop. Measured in RAW POSIX with no npth in the picture # (src/npth_sigmask_probe.c), raise(SIGUSR1) with the signal blocked never # runs the handler -- not through pselect's mask swap, and not after an # ordinary sigprocmask unblock either. The signal is simply never delivered. # That is brintos/linux#21 ("signal delivery does not work at all -- kill(2) # reports success and nothing is delivered, including a process signalling # itself"). sigpending() reads back empty as well, which is separately # brintos/linux#23 (rt_sigpending is ENOSYS), so the readback is not # independent evidence and this suite does not lean on it. # # * the thread half -- npth_create/npth_join. pthread_create RETURNS 0 and the # handle is real; the process then dies in the join with glibc's "The futex # facility returned an unexpected error code." That is # brintos/hardwarejs#12 verbatim: the futex BITSET ops # (FUTEX_WAIT_BITSET/FUTEX_WAKE_BITSET) return ENOSYS in the Worker # fast-path before the kernel is consulted, so every pthread_join aborts. # The issue's own trace (create rc=0, then the abort at join) is exactly what # data/npth-thread-probe reproduces here. # # Both masked groups are SKIPPED rather than failed, and each carries a # BRINTOS-WORKAROUND marker so the mask is swept up and dropped when the issue # closes rather than quietly outliving it. # --------------------------------------------------------------------------- lib=/usr/lib/libnpth.a incd=/usr/include pc=/usr/lib/pkgconfig/npth.pc DATA=$BST_DATA NATIVE=$DATA/npth-native.txt nl=' ' if [ ! -f "$lib" ]; then bst_skip npth/present "npth not installed on this image" exit 0 fi bst_ok npth/present 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; } hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "not in archive: $2" ;; esac; } # check_out insists on rc 0 and check_rc discards the output; the refusal stub # has both its message and its exit status under test, so neither fits. out_rc() { _id=$1; _want=$2; _needle=$3; shift 3 _out=$("$@" 2>&1); _rc=$? case "$_out" in *"$_needle"*) _hit=1 ;; *) _hit=0 ;; esac if [ "$_rc" -eq "$_want" ] && [ "$_hit" -eq 1 ]; then bst_ok "$_id" else bst_notok "$_id" "rc=$_rc want=$_want want:'$_needle' got:'$(_bst_1line "$_out")'"; fi } # --- payload shape ----------------------------------------------------------- hasfile npth/archive "$lib" hasfile npth/header "$incd/npth.h" hasfile npth/header-32 "$incd/npth-32.h" hasfile npth/pkgconfig "$pc" hasfile npth/m4 /usr/share/aclocal/npth.m4 hasfile npth/bin-config /usr/bin/npth-config # The distro is static-only: poky's ${PN} packages ${libdir}/libnpth.so.*, which # must match nothing. A shared object here means --disable-shared stopped being # honoured and the image grew a package it does not want. if [ -e /usr/lib/libnpth.so ] || [ -e /usr/lib/libnpth.so.0 ]; then bst_notok npth/no-shared-object "a libnpth shared object shipped on a static-only distro" else bst_ok npth/no-shared-object fi pctext=$(cat "$pc" 2>/dev/null) has npth/pc-name "Name: npth" "$pctext" has npth/pc-desc "Description: a new portable posix threading library" "$pctext" has npth/pc-libs "Libs: -L\${libdir} -lnpth -lpthread" "$pctext" has npth/pc-libdir "libdir=/usr/lib" "$pctext" has npth/pc-cflags "Cflags: -I\${includedir}" "$pctext" # Yocto, not the platform: the cross build stamps an -unknown suffix the native # control does not carry. libgpg-error's port records the same thing. has npth/pc-version "Version: 1.6-unknown" "$pctext" # poky's binconfig-disabled replaces npth-config with a refusal stub: it must # fail loudly rather than hand a caller host paths. out_rc npth/config-stub-refuses 1 \ "should not be used, use an alternative such as pkg-config" npth-config --libs # --- every function the header declares is in the archive -------------------- # Read out of the header THAT SHIPS rather than from a hardcoded list, so a # release that adds an entry point is checked rather than permanently forgiven. arcsyms=$(grep -ao 'npth[a-z0-9_]*' "$lib" 2>/dev/null | sort -u) hdrsyms=$(grep -vE '^[[:space:]]*#' "$incd/npth-32.h" 2>/dev/null \ | grep -oE '\bnpth_[a-z0-9_]*[[:space:]]*\(' \ | sed -e 's/[[:space:]]*(//' | sort -u) # npth_sigwait is declared in npth.h AND exported in libnpth.vers, and is # DEFINED IN NO SOURCE FILE in the 1.6 release. That is an upstream nPth defect, # not a property of this arm: the native x86_64 control archive, built from the # identical tarball, does not carry the symbol either. It is excluded here by # name and then asserted on its own below, so the exclusion cannot quietly hide # a real regression and the suite notices if upstream ever defines it. NOTDEFINED_UPSTREAM="npth_sigwait" nsym=0; nmiss=0; missing="" for f in $hdrsyms; do case "$nl$NOTDEFINED_UPSTREAM$nl" in *"$nl$f$nl"*) continue ;; esac nsym=$((nsym + 1)) case "$nl$arcsyms$nl" in *"$nl$f$nl"*) ;; *) nmiss=$((nmiss + 1)); missing="$missing $f" ;; esac done if [ "$nsym" -gt 20 ]; then bst_ok npth/symbols-counted else bst_notok npth/symbols-counted "only $nsym npth_* entry points found in the header"; fi if [ "$nmiss" -eq 0 ]; then bst_ok npth/symbols-complete else bst_notok npth/symbols-complete "$nmiss of $nsym missing:$(_bst_1line "$missing")"; fi # the sceptre itself, the sigev TU (a whole second source file), and one wrapper # from each blocking family -- named so a partial archive is legible hasline npth/sym-init npth_init "$arcsyms" hasline npth/sym-protect npth_protect "$arcsyms" hasline npth/sym-sigev-init npth_sigev_init "$arcsyms" hasline npth/sym-pselect npth_pselect "$arcsyms" hasline npth/sym-waitpid npth_waitpid "$arcsyms" # The upstream gap, pinned so it is visible rather than merely excluded above. # A consumer that calls npth_sigwait fails at LINK time, which is the correct # loud failure, so this is recorded rather than worked around. If a later # release defines it, this case flips and the exclusion should come out. case "$nl$arcsyms$nl" in *"${nl}npth_sigwait$nl"*) bst_notok npth/upstream-sigwait-undefined \ "npth_sigwait is now DEFINED — drop it from NOTDEFINED_UPSTREAM and let the completeness check cover it" ;; *) bst_ok npth/upstream-sigwait-undefined ;; esac # --- the library RUNNING, via the cross-linked probe ------------------------- if [ ! -s "$NATIVE" ]; then bst_notok npth/native-present "$NATIVE missing -- no oracle to compare against" exit 0 fi bst_ok npth/native-present # do_install stages data/* at 0644 and the deployed rootfs is read-only at # browser runtime, so the probe cannot be executed where it lies. tmp=${TMPDIR:-/tmp}/bst-npth.$$ _p=$tmp.bin cp "$DATA/npth-probe" "$_p" 2>/dev/null && chmod 0755 "$_p" 2>/dev/null if [ ! -x "$_p" ]; then bst_notok npth/probe-exec "could not stage an executable copy of $DATA/npth-probe" rm -f "$_p" exit 0 fi bst_ok npth/probe-exec "$_p" > "$tmp.out" 2>"$tmp.err"; prc=$? rm -f "$_p" check_eq npth/probe-rc 0 "$prc" # A truncated run must be distinguishable from a run that legitimately produced # these lines: without a terminator a crash half way through would silently # reduce the number of compared cases rather than fail one. The probe marks each # group as it enters it, so a death names the group it died in. if [ "$(tail -n 1 "$tmp.out" 2>/dev/null)" = "NPTH-PROBE end" ]; then bst_ok npth/probe-complete else bst_notok npth/probe-complete "no terminator; last saw: $(_bst_1line "$(tail -n 2 "$tmp.out" 2>/dev/null)")" fi # --- tag-by-tag against the native control ----------------------------------- # One awk over both files rather than a shell loop of ~90 awk calls: process # spawn is expensive on this target and the whole comparison is in-memory. # # BRINTOS-WORKAROUND: linux#21 remove -- H06/H07 are masked because a raised signal is never delivered on this arm; drop the mask and let them compare once signal delivery works. MASK21="blocked: signal raised while blocked is never delivered - brintos/linux#21" awk -v masked="H06-pending-after-pselect H07-pending-signum-is-usr1" \ -v reason="$MASK21" ' BEGIN { k = split(masked, mm, " "); for (i = 1; i <= k; i++) M[mm[i]] = 1 } NR == FNR { if ($1 == "GROUP" || $1 == "NPTH-PROBE") next t = $1; $1 = ""; sub(/^ /, ""); W[t] = $0; order[++n] = t; next } { if ($1 == "GROUP" || $1 == "NPTH-PROBE") next t = $1; $1 = ""; sub(/^ /, ""); G[t] = $0; seen[t] = 1 } END { for (i = 1; i <= n; i++) { t = order[i] if (t in M) { printf "BST skip npth/cmp-%s : %s\n", t, reason; continue } if (!(t in seen)) { printf "BST not ok npth/cmp-%s : absent from target output\n", t; continue } if (G[t] == W[t]) printf "BST ok npth/cmp-%s\n", t else printf "BST not ok npth/cmp-%s : want:%s got:%s\n", t, W[t], G[t] } } ' "$NATIVE" "$tmp.out" # --- anchored: values a specification fixes, not the control ----------------- np_get() { awk -v k="$1" '$1==k { $1=""; sub(/^ /,""); print; exit }' "$tmp.out"; } # the sceptre: npth_init ends by TAKING it, so a caller returning from it is # protected. A 0 here is a semaphore that never initialised, which would make # every ENTER/LEAVE pair after it a silent no-op. check_eq npth/anchor-protected-after-init 1 "$(np_get A02-protected-after-init)" check_eq npth/anchor-protected-after-unprotect 0 "$(np_get A03-protected-after-unprotect)" check_eq npth/anchor-protected-after-protect 1 "$(np_get A04-protected-after-protect)" # a mutex held by this thread must report EBUSY to trylock, never succeed check_eq npth/anchor-trylock-busy 1 "$(np_get B03-trylock-busy)" # the recursive attr actually reached the mutex (a default mutex would deadlock) check_eq npth/anchor-attr-gettype "ok 1" "$(np_get B12-attr-gettype)" check_eq npth/anchor-tryrdlock-busy 1 "$(np_get C07-tryrdlock-while-wrlocked)" check_eq npth/anchor-tls-roundtrip 1 "$(np_get D04-getspecific-roundtrip)" # header-only arithmetic: 5.1s - 2.9s = 2.2s with a borrow, 5.1s + 2.9s = 8.0s # with a carry. A wrong answer means the header that shipped is not the header # upstream wrote. check_eq npth/anchor-timersub-borrow "2 200000000" "$(np_get E04-timersub-borrow)" check_eq npth/anchor-timeradd-carry "8 0" "$(np_get E05-timeradd-carry)" # the I/O wrappers move the bytes they are given, both directions, both families check_eq npth/anchor-write-count 4 "$(np_get G01-write-count)" check_eq npth/anchor-read-count 4 "$(np_get G03-read-count)" check_eq npth/anchor-read-content npth "$(np_get G04-read-content)" check_eq npth/anchor-sendmsg-count 3 "$(np_get G11-sendmsg-count)" check_eq npth/anchor-recvmsg-content msg "$(np_get G13-recvmsg-content)" # select with nothing waiting reports 0 ready and clears the set; with a byte # waiting, exactly one, and it is ours check_eq npth/anchor-pselect-empty-ready 0 "$(np_get G06-pselect-empty-ready)" check_eq npth/anchor-select-ready-count 1 "$(np_get G08-select-ready-count)" # npth_waitpid reaped the right child and reported its status check_eq npth/anchor-exit-status 7 "$(np_get I03-exit-status)" # the sceptre is still held after every release/re-acquire pair -- an unbalanced # one leaves the semaphore at the wrong count and the NEXT protected call blocks # forever check_eq npth/anchor-protected-after-usleep 1 "$(np_get F03-protected-after-usleep)" check_eq npth/anchor-protected-after-waitpid 1 "$(np_get I04-protected-after-waitpid)" rm -f "$tmp.out" "$tmp.err" # --- the thread half --------------------------------------------------------- # nPth's thread start thunk takes the sceptre as its FIRST act, so a created # thread parks immediately -- the shortest possible route to the platform's # thread machinery. Run under a timeout: this binary is expected to abort. tp=${TMPDIR:-/tmp}/bst-npth-tp.$$ _t=$tp.bin cp "$DATA/npth-thread-probe" "$_t" 2>/dev/null && chmod 0755 "$_t" 2>/dev/null if [ ! -x "$_t" ]; then bst_notok npth/thread-probe-exec "could not stage $DATA/npth-thread-probe" rm -f "$_t" exit 0 fi bst_ok npth/thread-probe-exec timeout 30 "$_t" > "$tp.out" 2>&1; trc=$? rm -f "$_t" tout=$(cat "$tp.out" 2>/dev/null) # Everything up to the create is ground npth-probe already covers, and it must # hold here too -- if it does not, the finding is NOT the platform's thread gap. has npth/thread-init "T01-init ok" "$tout" has npth/thread-protected "T02-protected 1" "$tout" # npth_create REPORTS SUCCESS on this arm, exactly as it does natively and # exactly as hardwarejs#12 describes. That is worth an assertion of its own: # the port is not refused at create, it dies later. has npth/thread-create-rc0 "T04-create-rc 0" "$tout" has npth/thread-created "T06-created ok" "$tout" # BRINTOS-WORKAROUND: hardwarejs#12 remove -- the join half is masked because FUTEX_WAIT_BITSET is ENOSYS in the Worker fast-path so every pthread_join aborts; drop the mask and assert T07/T08/T09 once it reaches the kernel. MASK12="blocked: pthread_join aborts, futex BITSET ops ENOSYS in the Worker fast-path - brintos/hardwarejs#12" case "$tout" in *"T09-done joined"*) # the gap closed -- say so loudly rather than passing quietly on a stale mask bst_ok npth/thread-join-now-works has npth/thread-worker-ran "T08-worker-ran 1" "$tout" has npth/thread-retval "T08b-retval 1" "$tout" ;; *"futex facility returned an unexpected error code"*) # the expected red, in its documented form: create succeeded, join aborted bst_skip npth/thread-join "$MASK12" bst_skip npth/thread-worker-ran "$MASK12" ;; *) bst_notok npth/thread-join \ "neither a join nor the documented hardwarejs#12 abort (rc=$trc): $(_bst_1line "$tout")" ;; esac rm -f "$tp.out"