. "$BST_LIB" # libexecinfo 1.1 -- Maxim Sobolev's BSD-licensed clone of the GNU backtrace() # family (backtrace, backtrace_symbols, backtrace_symbols_fd), as meta-oe ships # it from recipes-extended. # # --------------------------------------------------------------------------- # WHY THIS SUITE IS SHAPED THE WAY IT IS # # libexecinfo looks redundant on a glibc image and IS NOT, and the reason is the # whole point of this suite. glibc ships execinfo.h and debug_backtrace.bc on # this arm, which is exactly why the stock recipe is gated # COMPATIBLE_HOST = ".*-musl.*" and does not even parse for a glibc target. But # GLIBC'S backtrace() CANNOT BE LINKED HERE: # # wasmld: phantom import: env._Unwind_Backtrace (function) # wasmld: phantom import: env._Unwind_GetIP (function) # wasmld: FATAL (HWJS-3 coro arm): final image imports symbol(s) outside # the sanctioned surface ... Refusing at the link, not at the # first call. # # debug_backtrace.bc is written against the libgcc unwinder and NOTHING in this # toolchain provides _Unwind_Backtrace or _Unwind_GetIP. The gap was isolated to # that one function by linking each entry point on its own: # # backtrace_symbols + backtrace_symbols_fd link rc=0 BUILT # backtrace() link rc=1 FAILED # # and debug_backtracesyms.bc / debug_backtracesymsfd.bc mention no _Unwind_ # symbol at all, which is why two thirds of the family are fine. So on this # image the header advertises a function that no program can link against, and # libexecinfo is what makes backtrace() LINKABLE at all. That is the port's # actual value, and it is why the headers go under /usr/include/libexecinfo/ # rather than over glibc's: the two are not interchangeable in either direction. # # THERE IS THEREFORE NO GLIBC PROBE ON TARGET -- it could not be built, which is # itself the finding, recorded below as an attributed skip. src/ still carries # the -DBT_GLIBC mode of the probe source, because that is how the link failure # was found and it is what produced the native control that proves the same # source links and runs perfectly on x86_64. # # WHAT THE PORT IS REALLY PROBING, once it links, is the one question no other # port in this image asks: CAN A PROGRAM WALK ITS OWN CALL STACK HERE? # libexecinfo's backtrace() is a loop over getframeaddr()/getreturnaddr(), which # are 128-way switches over __builtin_frame_address(N) and # __builtin_return_address(N). On wasm32 the call stack is NOT in linear memory # -- a deliberate property of the architecture, not a brintOS decision -- so # those builtins have nothing to return for N > 0. # # EXPECTATIONS COME FROM A NATIVE x86_64 CONTROL, never from reading the source. # data/libexecinfo-native.txt is the verbatim output of this same probe built # against a native build of the identical 1.1 source (same two meta-oe patches) # and run statically linked, as it is here. The probe prints no pointer, no pid # and no path built from one -- which matters more here than anywhere else, # since ADDRESSES ARE THE ENTIRE SUBJECT of this library and printing one would # make the two runs trivially and uselessly different. # # ONE NATIVE RESULT IS ITSELF A FINDING, and it is upstream's, not this # platform's: E11-bt-deep-survives is 0 on x86_64. Asking libexecinfo for more # frames than the stack has CRASHES -- backtrace(buf,4) returns 4 and # backtrace(buf,8) dies on SIGSEGV, because the loop terminates on a NULL that # __builtin_frame_address never delivers (gcc documents that a nonzero argument # "can have unpredictable effects, including crashing the calling program"). # glibc's G11 is 1 for the same call. So the idiomatic backtrace(buf, 64) is # unsafe by construction in libexecinfo and safe in glibc, which is the sharpest # possible argument for the private-includedir packaging: a program that thought # it was calling glibc must not get this one by accident. # --------------------------------------------------------------------------- # A root prefix, empty on the target and set only when this suite is validated # NATIVELY under dash before it ships -- which is how the expectations below were # checked in the first place. The target paths cannot be reproduced natively any # other way: writing /usr/lib and /usr/include needs real root, and `unshare -r` # does not grant it over a filesystem owned by the initial user namespace. The # runner never sets this, so on the image it is exactly the absolute paths. R="${BST_LIBEXECINFO_ROOT:-}" ARCHIVE=$R/usr/lib/libexecinfo.a INCDIR=$R/usr/include/libexecinfo HDR=$INCDIR/execinfo.h STHDR=$INCDIR/stacktraverse.h GLIBC_HDR=$R/usr/include/execinfo.h PROBE=$BST_DATA/libexecinfo-probe NATIVE=$BST_DATA/libexecinfo-native.txt # The gap that makes this port worth having, recorded where the runner's report # will show it. It is a LINK-time fact, established when the probes were built, # so there is nothing to re-measure on target -- but leaving it out of the suite # would hide the single most important thing this port found. ISSUE_UNWIND="tracked as brintos.io/brintos/linux-in-the-browser/issues/141 (glibc backtrace() cannot be linked on wasm32-hwjs: debug_backtrace.bc calls _Unwind_Backtrace/_Unwind_GetIP and nothing in the toolchain provides them, so wasmld refuses with a phantom-import FATAL; backtrace_symbols() and backtrace_symbols_fd() from the same header link fine)" bst_skip libexecinfo/glibc-backtrace-linkable "$ISSUE_UNWIND" if [ ! -f "$ARCHIVE" ]; then bst_skip libexecinfo/present "libexecinfo not installed on this image" exit 0 fi bst_ok libexecinfo/present # --- the package is whole ---------------------------------------------------- check libexecinfo/header test -f "$HDR" check libexecinfo/stackheader test -f "$STHDR" check libexecinfo/staticlib test -f "$ARCHIVE" # ${PN} is empty by design: upstream's only non-archive output is a shared # object, refused distro-wide. A .so here would mean the bbappend's # do_compile/do_install override stopped taking effect. if [ -e "$R/usr/lib/libexecinfo.so" ] || [ -e "$R/usr/lib/libexecinfo.so.1" ]; then bst_notok libexecinfo/no-shared "a shared libexecinfo is installed on a static-only distro" else bst_ok libexecinfo/no-shared fi # --- THE PACKAGING DECISION, ASSERTED ---------------------------------------- # The headers must be under /usr/include/libexecinfo/ and must NOT have been # installed over glibc's own /usr/include/execinfo.h. This is the case that # would catch the bbappend's do_install silently reverting to upstream's paths # -- which would not fail the build, would not fail any other case here, and # would leave every consumer in the image quietly compiling against the BSD # clone while believing it had glibc. # # The two headers are told apart by their include guards, which differ by one # character and have since 2003: libexecinfo's is _EXECINFO_H_ (trailing # underscore), glibc's is _EXECINFO_H. if grep -q '_EXECINFO_H_' "$HDR" 2>/dev/null; then bst_ok libexecinfo/header-is-bsd else bst_notok libexecinfo/header-is-bsd "$HDR is not libexecinfo's own header" fi if [ ! -e "$GLIBC_HDR" ]; then # This image installs no glibc -dev package, so there is no header at the # unqualified path at all -- which satisfies the requirement (nothing is # shadowed) without being able to prove it positively. bst_skip libexecinfo/glibc-header-not-clobbered \ "$GLIBC_HDR does not exist on this image (no glibc -dev package installed)" elif grep -q '_EXECINFO_H_' "$GLIBC_HDR" 2>/dev/null; then bst_notok libexecinfo/glibc-header-not-clobbered \ "libexecinfo's header has been installed over glibc's $GLIBC_HDR" else bst_ok libexecinfo/glibc-header-not-clobbered fi # --- the header declares the API, and the archive carries it ----------------- for fn in backtrace backtrace_symbols backtrace_symbols_fd; do if grep -q "$fn" "$HDR" 2>/dev/null; then bst_ok "libexecinfo/hdr-$fn" else bst_notok "libexecinfo/hdr-$fn" "$fn missing from $HDR"; fi done check_out libexecinfo/stackheader-api "getframeaddr" cat "$STHDR" # On this arm the archive is ThinLTO bitcode, so symbol NAMES survive as plain # text while string constants do not -- the limitation libutempter recorded. if command -v strings >/dev/null 2>&1; then syms=$(strings "$ARCHIVE" 2>/dev/null) check_out libexecinfo/sym-backtrace "backtrace" printf '%s' "$syms" check_out libexecinfo/sym-backtrace-syms "backtrace_symbols" printf '%s' "$syms" check_out libexecinfo/sym-getframeaddr "getframeaddr" printf '%s' "$syms" check_out libexecinfo/sym-getreturnaddr "getreturnaddr" printf '%s' "$syms" else for c in sym-backtrace sym-backtrace-syms sym-getframeaddr sym-getreturnaddr; do bst_skip "libexecinfo/$c" "strings not installed on this image" done fi # --- the probe: the library actually running --------------------------------- for f in "$PROBE" "$NATIVE"; do if [ ! -f "$f" ]; then bst_skip libexecinfo/probe-runs "$(basename "$f") absent from the image" exit 0 fi done # data/* installs 0644, so stage a copy and make it executable. run_probe() { # $1 = probe path, $2 = output path, $3 = case id _p=${TMPDIR:-/tmp}/bst-libexecinfo-$$.bin if ! cp "$1" "$_p" 2>/dev/null || ! chmod +x "$_p" 2>/dev/null; then bst_notok "$3" "cannot stage the probe to $_p" return 1 fi _err=$("$_p" 2>&1 >"$2"); _rc=$? rm -f "$_p" if [ "$_rc" -eq 0 ]; then bst_ok "$3"; return 0 else bst_notok "$3" "probe exited rc=$_rc $(_bst_1line "$_err")"; return 1; fi } pout=${TMPDIR:-/tmp}/bst-libexecinfo-out.$$ run_probe "$PROBE" "$pout" libexecinfo/probe-runs # The probe prints a terminator, so a run that died halfway is distinguishable # from one that merely disagreed -- and on a library whose native failure mode # IS a crash, that distinction is the whole ballgame. if grep -q '^LIBEXECINFO-PROBE-END$' "$pout" 2>/dev/null; then bst_ok libexecinfo/probe-complete else bst_notok libexecinfo/probe-complete "probe output has no LIBEXECINFO-PROBE-END terminator" fi # --- the platform gap, decided from the measurement -------------------------- # # THE CALL STACK IS NOT WALKABLE HERE, and unlike every other finding in this # image's suites that is not a defect anyone can fix in a library: on wasm32 the # call stack lives outside linear memory by design, so __builtin_frame_address(N) # and __builtin_return_address(N) have nothing to return for N > 0 and the # switches in stacktraverse.c fall to their default. backtrace() then terminates # on its first iteration and reports zero frames. # # Whether the block is in effect is DECIDED FROM THIS RUN rather than assumed, # so that if the platform ever gains a shadow stack this suite goes green by # itself with no edit here. ISSUE_BT="blocked by the platform's non-walkable call stack -- NOT a defect: wasm32 keeps the call stack outside linear memory by design, so __builtin_frame_address(N>0) has nothing to return and backtrace() reports zero frames. A real backtrace would need a runtime-side shadow stack; noted under brintos.io/brintos/linux-in-the-browser/issues/141" want_pos=$(awk '$1 ~ /^E[0-9]*-bt-rc-positive$/{print $2; exit}' "$NATIVE") got_pos=$(awk '$1 ~ /^E[0-9]*-bt-rc-positive$/{print $2; exit}' "$pout") if [ "$want_pos" = "$got_pos" ]; then BLOCK=""; else BLOCK="$ISSUE_BT"; fi # The cases that are DOWNSTREAM of a zero-frame backtrace are not independent # observations -- they are the same fact restated, and they all flip back # together the day the platform can walk a stack. They carry the block as skips; # bt-rc-positive itself stays a hard failure, because that is the finding. BLOCKED_BT=" bt-rc-ge-3 bt-first-nonnull bt-all-nonnull bt-then-syms-nonnull frameaddr0-nonnull frameaddr1-nonnull returnaddr0-nonnull returnaddr1-nonnull " # --- the SECOND platform gap: dladdr() traps --------------------------------- # # dladdr() does not fail here, it TRAPS -- the runtime reports # `RuntimeError: unreachable at _dl_addr.resume` and the process dies on SIGILL. # backtrace_symbols() and backtrace_symbols_fd() both call it unconditionally on # every pointer they are given, so the entire symbolising half of this library # takes its caller down with it. # # That is a far worse failure mode than the walker's: a zero-frame backtrace is # a useless answer, but a trap is no answer and no process either. It is also # fixable in a way the walker's gap is not -- dladdr() is documented to return 0 # when it cannot resolve an address, and returning 0 would make both symbolisers # work (they already have a "%p"-only branch for exactly that case). # # Each dladdr-dependent block runs in its own child (see the probe's header), so # a trap arrives here as `*-survives 0` instead of an amputated run. The two # -survives cases stay HARD FAILURES because they are the finding; the values # the dead children could not produce are skips that name it. ISSUE_DL="blocked by brintos.io/brintos/linux-in-the-browser/issues/140 (dladdr() TRAPS rather than returning 0 -- RuntimeError: unreachable at _dl_addr.resume, and the process dies on SIGILL). backtrace_symbols() and backtrace_symbols_fd() call dladdr() on every pointer, so the whole symbolising half of libexecinfo is unusable" want_dl=$(awk '$1 ~ /^E[0-9]*-dladdr-survives$/{print $2; exit}' "$NATIVE") got_dl=$(awk '$1 ~ /^E[0-9]*-dladdr-survives$/{print $2; exit}' "$pout") if [ "$want_dl" = "$got_dl" ]; then BLOCK_DL=""; else BLOCK_DL="$ISSUE_DL"; fi BLOCKED_DL=" syms-nonnull syms-entries-nonnull syms-entries-nonempty syms-entries-have-0x syms-entries-distinct syms-size1-nonnull symsfd-tmp-open symsfd-lines symsfd-bytes-positive symsfd-all-have-0x dladdr-resolves " # --------------------------------------------------------------------------- # Score both probes case by case against their own native control. The expected # value for each tag is READ OUT OF THE CONTROL, never written here: a red then # means "wasm32-hwjs answered differently from x86_64" and nothing else. score() { # $1 = control file, $2 = probe output, $3 = tag regex prefix, $4 = case prefix while read -r tag want; do [ -n "$tag" ] || continue case "$tag" in LIBEXECINFO-PROBE-END) continue ;; esac got=$(awk -v t="$tag" '$1==t{print $2; found=1; exit} END{if(!found) print ""}' "$2") cs=$(printf '%s' "$tag" | sed "s/^$3[0-9]*-//") id="libexecinfo/$4$cs" # The block is checked BEFORE the comparison, deliberately. When a section's # child has died, the parent emits a fallback 0 for every value the child # never produced -- and a fallback that happens to coincide with the native # control would otherwise report `ok` for something that was never measured # at all. dladdr-resolves is exactly that case (native 0, fallback 0), and it # read green on the first run of this suite. A case downstream of a block is # unmeasured, so it skips whether or not the numbers agree. case " $BLOCKED_BT " in *" $cs "*) if [ -n "$BLOCK" ]; then bst_skip "$id" "$BLOCK"; continue; fi ;; esac case " $BLOCKED_DL " in *" $cs "*) if [ -n "$BLOCK_DL" ]; then bst_skip "$id" "$BLOCK_DL"; continue; fi ;; esac if [ "$want" = "$got" ]; then bst_ok "$id" continue fi case "$cs" in # The findings, each stated in its OWN detail string so the runner's # report is self-attributing and a reader never has to correlate a red # with anything else to know what it means. bt-rc-positive) bst_notok "$id" "backtrace() reported no frames (want:'$want' got:'$got'); $ISSUE_BT" ;; dladdr-survives|syms-survives|symsfd-survives|bt-then-syms-survives) bst_notok "$id" "the child process did not survive the call (want:'$want' got:'$got'); $ISSUE_DL" ;; # An upstream bug that this platform happens to be IMMUNE to. Native # x86_64 crashes here and wasm32 does not, so a mismatch in THIS direction # is good news and is recorded as such rather than as a failure. bt-deep-survives) if [ "$got" = "1" ]; then bst_ok "$id" else bst_notok "$id" "the idiomatic backtrace(buf,64) did not survive (want:'$want' got:'$got')" fi ;; *) bst_notok "$id" "wasm32-hwjs answered differently from x86_64 (want:'$want' got:'$got')" ;; esac done < "$1" } score "$NATIVE" "$pout" E "" # Aggregate, compared in the guest with cmp(1) -- the BRINTOS-260 lesson that a # console dump truncates long lines, so the comparison happens on the target and # not in the transcript. if cmp -s "$pout" "$NATIVE"; then bst_ok libexecinfo/matches-native elif [ -n "$BLOCK" ] || [ -n "$BLOCK_DL" ]; then bst_skip libexecinfo/matches-native "${BLOCK:-$BLOCK_DL}" else bst_notok libexecinfo/matches-native \ "probe output differs from the native x86_64 control; see the per-case results below" fi rm -f "$pout"