# BST-TIMEOUT: 900 # PROVISIONAL on first landing, to be re-set from the # # ==BST-SUITE line this suite emits. The cost here is not # # the scoring -- that is one awk pass over the probe's # # output -- it is staging SIX statically linked programs # # into the guest and running gctest, which drives the # # collector through thousands of collections. Staging is # # the part that stalls on a loaded guest, and a bound that # # trips turns a slow run into a red with no cases, which is # # the outcome least worth buying. . "$BST_LIB" # bdwgc 8.2.6 -- the Boehm-Demers-Weiser conservative garbage collector, usable # as a drop-in replacement for C malloc that reclaims unreachable memory by # scanning the program's roots (the machine registers, the C stack, the static # data segment, and any explicitly registered areas) and tracing every # pointer-shaped word it finds into the heap. What ships under --disable-shared # is /usr/lib/libgc.a and libcord.a, the headers under /usr/include/gc, and # /usr/lib/pkgconfig/bdw-gc.pc. # # --------------------------------------------------------------------------- # WHAT THIS SUITE CAN HONESTLY ASSERT ON THIS TARGET, AND WHAT IT CANNOT. # # A wasm frame keeps its values in engine-managed LOCALS and on the engine # VALUE STACK. Neither lives in linear memory and neither has an address, so no # scan of any kind can reach them -- the toolchain states it as machine state: # `ctl -- the engine control stack (native frames; opaque, not addressable)'. # What IS addressable, and is scanned as the stack in the normal way, is the C # shadow stack (`__stack_pointer', an index into linear memory), which carries # every ADDRESS-TAKEN local. # # So a pointer the compiler kept only in a wasm local would be invisible to the # mark phase, and an object reachable only through one could be collected while # still live. That is the shape of the risk on this arm, and it is a property # of the platform rather than of this package. # # WHAT WAS MEASURED IS THE OPPOSITE DIRECTION, AND IT IS THE MORE USEFUL FACT. # gctest -- the collector's own acceptance test, which holds live references in # ordinary locals throughout and is what that risk would break first -- passes # here, finalizing 13218 of 13218 objects across 125 collections and printing # `Collector appears to work'. Nothing this suite runs has produced an object # collected while live. What it did produce is over-RETENTION, of exactly the # kind conservative collection is entitled to: a pointer that has passed # through a frame keeps its object alive until that storage is used for # something else. The G group measures how much it takes -- one recursion over # the dead frame releases 1998 of 2000 and one re-entry releases all 2000 -- # and F03/F04 measure the case that never resolves, a pointer that passed # through main's frame, which is live until the program exits. # # What this target contributes is WHERE that frame is. An address-taken local # in a function that can suspend is hoisted off the C shadow stack (S05=0, and # L03 against L06 in the census) into the range the collector scans as STATIC # DATA, so main's is scanned wholesale for the program's life rather than # being a register window something else would reuse. # # This suite therefore does NOT assert that the collector is sound on this arm # -- no conservative collector's soundness can be established by testing, and # the wasm-locals risk above is real whether or not a case here trips on it. It # asserts the things that ARE decidable: allocation, sizing, accounting, the # reachability that runs through roots the platform can represent, and # reclamation once stale roots are overwritten. It runs gctest and reports what # it did. # # TWO THINGS ARE ASSERTED POSITIVELY BECAUSE THEY ARE THE PORT'S OWN WORK: # # * The static data segment IS a root here (C01, C02). DATASTART..DATAEND is # a real range on this port, derived from the __data_end that wasmld # exports. Upstream's Emscripten config leaves that range EMPTY, and # upstream's own gctest skips its global-root checks there with a `FIXME: # gctest fails unless check_ints(a_get(), ...) are skipped'. Those checks # run here unpatched. # # * The C shadow stack IS inside the scanned range (S03, S04). The only # reference the probe keeps to that object is an address-taken local, and # the case passes when its finalizer did NOT run. A red there would mean # the stack was not being scanned at all, which is a different and much # larger failure than the wasm-locals gap above. # # --------------------------------------------------------------------------- # WHY THERE IS A PROBE AND FIVE UPSTREAM PROGRAMS. # # `make install' leaves the archives, the headers and bdw-gc.pc -- nothing # executable. Upstream's own tests are check_PROGRAMS that `make install' # discards. On an image with no compiler that would leave "the files are # present" as the whole of what a self-test could say, so the bbappend builds # them into bdwgc-tests, relinked against the archive that ships on every # build so they cannot go stale against a library that moved under them. R=${BST_BDWGC_ROOT:-} tmp=${TMPDIR:-/tmp} TESTDIR=${BST_BDWGC_TESTDIR:-$R/usr/libexec/bdwgc-tests} LIBDIR=$R/usr/lib INCDIR=$R/usr/include # --- what ships ------------------------------------------------------------- if [ -f "$LIBDIR/libgc.a" ]; then bst_ok bdwgc/staticlib else bst_notok bdwgc/staticlib "no $LIBDIR/libgc.a" fi if [ -f "$LIBDIR/libcord.a" ]; then bst_ok bdwgc/cord-staticlib else bst_notok bdwgc/cord-staticlib "no $LIBDIR/libcord.a" fi # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape bdwgc/no-shared "$LIBDIR/libgc.so*" for h in gc.h gc_version.h gc_typed.h gc_mark.h; do if [ -f "$INCDIR/gc/$h" ]; then bst_ok "bdwgc/header-$h" else bst_notok "bdwgc/header-$h" "no $INCDIR/gc/$h" fi done # Upstream names it bdw-gc.pc, not gc.pc -- checked against the installed file # rather than assumed from the library name. PCFILE=$LIBDIR/pkgconfig/bdw-gc.pc if [ -f "$PCFILE" ]; then bst_ok bdwgc/pkgconfig check_out bdwgc/pkgconfig-version "8.2.6" cat "$PCFILE" check_out bdwgc/pkgconfig-libs "-lgc" cat "$PCFILE" else bst_notok bdwgc/pkgconfig "no $PCFILE" fi # The C++ half is not built on this arm (--disable-cplusplus; the toolchain has # no usable C++ include path -- hwjs-cc#10). Asserted so that the day it comes # back, this case is what says so rather than a silent change in the package. if [ ! -f "$INCDIR/gc/gc_cpp.h" ]; then bst_ok bdwgc/no-cplusplus else bst_notok bdwgc/no-cplusplus "gc_cpp.h is present -- the C++ half was built; revisit the port" fi # --- symbol census over the archive that ships ------------------------------ # # Bounded and named, so a truncation is visible: 24 of the entry points a # consumer actually links. strings(1) over the archive rather than nm, which # this image does not ship. SYMS="GC_init GC_malloc GC_malloc_atomic GC_malloc_uncollectable GC_free GC_realloc GC_strdup GC_gcollect GC_base GC_size GC_get_heap_size GC_get_free_bytes GC_get_bytes_since_gc GC_get_total_bytes GC_register_finalizer GC_general_register_disappearing_link GC_add_roots GC_remove_roots GC_set_warn_proc GC_is_heap_ptr GC_enable GC_disable GC_expand_hp GC_get_version" nsym=0 if [ -f "$LIBDIR/libgc.a" ]; then strings "$LIBDIR/libgc.a" > "$tmp/bst-bdwgc-syms.$$" 2>/dev/null for sym in $SYMS; do nsym=$((nsym + 1)) if grep -qx "$sym" "$tmp/bst-bdwgc-syms.$$" 2>/dev/null; then bst_ok "bdwgc/sym-$sym" else bst_notok "bdwgc/sym-$sym" "not in libgc.a" fi done rm -f "$tmp/bst-bdwgc-syms.$$" bst_ok "bdwgc/sym-count-$nsym" fi # --- stage and run the probe ------------------------------------------------ probe=$tmp/bst-bdwgc-probe.$$ got=$tmp/bst-bdwgc-got.$$ exp=$tmp/bst-bdwgc-exp.$$ if [ ! -f "$TESTDIR/bdwgc-probe" ]; then bst_notok bdwgc/probe-present "no probe at $TESTDIR/bdwgc-probe" else bst_ok bdwgc/probe-present if cp "$TESTDIR/bdwgc-probe" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then bst_ok bdwgc/probe-stage else bst_notok bdwgc/probe-stage "cannot stage the probe into $tmp" rm -f "$probe" fi if [ -x "$probe" ]; then "$probe" > "$got" 2>"$tmp/bst-bdwgc-err.$$" prc=$? check_eq bdwgc/probe-rc 0 "$prc" # The address-space census, always, whether or not the probe survived. # These four numbers are what say whether the collector's idea of "the # stack" means anything on this target, and they are the first thing to # read when a program here dies of a frame-arena OOM rather than of # anything to do with the heap. grep -E "^L0[0-9] " "$got" 2>/dev/null | sed "s|^|# layout: |" if [ "$prc" != 0 ]; then sed -n "1,20p" "$tmp/bst-bdwgc-err.$$" 2>/dev/null | sed "s|^|# probe-stderr: |" fi rm -f "$tmp/bst-bdwgc-err.$$" if [ ! -s "$got" ]; then bst_notok bdwgc/probe-output "the probe produced no output (rc=$prc)" else bst_ok bdwgc/probe-output # tagexpectedid. Written to a file rather than embedded in # the awk program so the values stay data. F04 (how many finalizers ran) # and F08 (free KiB) are REPORTED, not scored -- see below. cat > "$exp" <<'EOF' V02 4 wordsize V03 1 stackbottom-set V04 1 stack-grows-down A01 1 malloc A02 1 malloc-zeroed A03 1 malloc-atomic A04 1 malloc-uncollectable A05 1 strdup A06 1 realloc-preserves A07 1 size-ge-request A08 1 base-of-interior-pointer A09 1 blocks-distinct A10 1 is-heap-ptr H01 1 heapsize-nonzero H02 1 heapsize-grows H03 1 graph-walk-complete C01 1 static-root-graph C02 1 static-root-string C03 1 uncollectable-survives C04 1 heap-to-heap-link C05 1 explicit-root S03 1 shadow-stack-roots-object S04 1 shadow-stack-object-intact F01 1 graph-built F02 1 finalizers-not-run-early G07 1 finalizers-ran F06 1 free-bytes-rose F07 1 heap-did-not-shrink W01 1 disappearing-link-registered W02 1 disappearing-link-held Z01 1 probe-reached-end EOF awk ' NR == FNR { want[$1] = $2; id[$1] = $3; next } { got[$1] = $2 } END { n = 0 for (t in want) { n++ if (!(t in got)) printf "BST not ok bdwgc/%s : the probe emitted no %s line\n", id[t], t else if (got[t] == want[t]) printf "BST ok bdwgc/%s\n", id[t] else printf "BST not ok bdwgc/%s : %s = %s, expected %s\n", id[t], t, got[t], want[t] } } ' "$exp" "$got" # V01 is the packed version word; assert only that it is set, since its # encoding is upstream's business and bdw-gc.pc already pins 8.2.6. v01=$(awk '$1=="V01"{print $2}' "$got") if [ -n "$v01" ] && [ "$v01" -gt 0 ] 2>/dev/null; then bst_ok bdwgc/version-nonzero else bst_notok bdwgc/version-nonzero "V01=$v01" fi # HOW MANY finalizers ran is REPORTED, never scored, and that is # deliberate. A conservative collector is always free to RETAIN garbage # -- a stale pointer-shaped word anywhere in a root keeps its object # alive -- so "all N finalizers ran" is not a property the collector # promises and a case asserting it would be a false red on a collector # having a perfectly ordinary day. F09 above asserts the thing that IS # promised: that dropping the only reference, ALLOCATING OVER what the # drop left behind, and collecting runs at least one. The count goes into # the record so a regression from "most" to "one" is visible to a reader # even though neither is a failure. # # WHY THE SCORED CASE IS G07 AND NOT ONE OF THESE. F03/F04 report what # happens when the only reference is dropped and the collector is asked # to run: 0 of 2000. F09/F10 report the same after allocation churn: # still 0. Neither is a statement about the collector. The batch here is # published by assigning a CALLEE'S RETURN VALUE to a global, so the # pointer passes through main's own frame -- which is live until the # program exits and is scanned every collection. The G group below drops # a batch that main never holds, and G07 scores that. Reporting these # rather than scoring them is the same rule as the paragraph above: a # conservative collector is free to retain, so a red here would be a # false one. # S05 -- is an ordinary address-taken local inside the range the # collector scans as THE STACK? Reported, never scored, because the # answer is a property of what the toolchain chose to hoist for one # particular function and not a contract either way. # # 0 here is the expected answer on this target and is NOT a failure: # the transform that makes frames reifiable moves address-taken locals # off the shadow stack, so they fall outside [sp, GC_stackbottom). What # keeps the collector correct anyway is that those frames land inside # the range scanned as STATIC DATA -- which is what S03 measures, by # asserting that an object held only by such a local is not finalized. # Read S05 and S03 together: S05=0 with S03=1 is this platform working # as characterised; S03=0 would be the real defect. s05=$(awk '$1=="S05"{print $2}' "$got") bst_ok "bdwgc/local-in-scanned-stack-range-$s05" f04=$(awk '$1=="F04"{print $2}' "$got") f05=$(awk '$1=="F05"{print $2}' "$got") f08=$(awk '$1=="F08"{print $2}' "$got") f10=$(awk '$1=="F10"{print $2}' "$got") bst_ok "bdwgc/finalizers-no-churn-$f04-of-$f05" bst_ok "bdwgc/finalizers-after-churn-$f10-of-$f05" bst_ok "bdwgc/free-kib-$f08" # G01/G02/G04 say WHAT it takes to make a dropped object collectable # here. All three are REPORTED, never scored -- a conservative collector # is always free to retain, so no number here is a contract. # # G01 -- the batch is allocated, published and dropped entirely inside # a CALLEE, so no live frame holds a copy when the collection # runs. Measured: 0. Returning from the frame is not enough. # G02 -- the same, after recursing with a zeroed buffer to write over # the storage that dead frame used. Measured: 0. A few kB of # recursion does not reach whatever still holds the reference. # G04 -- the same, after ordinary allocation churn. # G05 -- after ONE re-entry of that same function, registering nothing, # so the only effect is to write over its own frame storage. # G06 -- after 64 more, and G08 after 4096. G07, the scored case above, # is `G08 > 0'. # # Measured on this target: 0, 1998, 1999, 2000, 2000, 2000. The # reference lives in ORDINARY REUSABLE FRAME STORAGE and goes as soon as # anything else uses it -- one recursion nearly clears it, one re-entry # clears it -- and the collector then reclaims every one of the 2000. # Read the group as a statement about STALE ROOTS, not about soundness. # # WHAT DOES NOT GO IS A POINTER THAT PASSED THROUGH main's OWN FRAME, # which is live for the whole program: that is what F03/F04 report as 0 # of 2000, unchanged by any amount of churning or re-entering. Both facts # are ordinary conservative collection. What this target contributes is # that main's address-taken locals sit in the range scanned as STATIC # DATA rather than on the C shadow stack (S05=0, and L03 against L06), # so the slot is scanned wholesale for the program's life instead of # being a register window something else would reuse. g01=$(awk '$1=="G01"{print $2}' "$got") g02=$(awk '$1=="G02"{print $2}' "$got") g03=$(awk '$1=="G03"{print $2}' "$got") g04=$(awk '$1=="G04"{print $2}' "$got") g05=$(awk '$1=="G05"{print $2}' "$got") g06=$(awk '$1=="G06"{print $2}' "$got") bst_ok "bdwgc/dead-frame-finalizers-$g01-of-$g03" bst_ok "bdwgc/scrubbed-frame-finalizers-$g02-of-$g03" bst_ok "bdwgc/churned-frame-finalizers-$g04-of-$g03" bst_ok "bdwgc/reentered-frame-finalizers-$g05-of-$g03" bst_ok "bdwgc/reentered-64x-frame-finalizers-$g06-of-$g03" g08=$(awk '$1=="G08"{print $2}' "$got") bst_ok "bdwgc/reentered-4096x-frame-finalizers-$g08-of-$g03" # M02 -- does mmap(MAP_FIXED) put the mapping at the address asked for? # bdwgc's GC_unmap() returns a page by munmap'ing it and re-mmap'ing the # SAME range MAP_FIXED to keep the address reserved, and ABORTs when the # answer differs; USE_MUNMAP is off in this port for that reason. This # records the reason as a measurement rather than as a comment. REPORTED, # never scored -- the recipe already accommodates whichever way it goes, # and a red here would be a duplicate of a decision already taken. # D03/D05 -- does 17 frames of recursion move __builtin_frame_address(0) # at all? GC_clear_stack()'s recursion terminates on the assumption that # it does, and ALWAYS_SMALL_CLEAR_STACK is set in this port because it # did not: the walk ran until the frame allocator was exhausted and the # program trapped. D03 asks it of a leaf that cannot suspend and so keeps # its locals on the C shadow stack; D05 asks it of one that can, whose # locals are hoisted into a frame instead -- which is the shape # GC_clear_stack has. REPORTED, never scored: the machine block already # accommodates whichever way each comes out, and a case that went red # here would be re-litigating a decision rather than testing bdwgc. d03=$(awk '$1=="D03"{print $2}' "$got") d05=$(awk '$1=="D05"{print $2}' "$got") bst_ok "bdwgc/frame-addr-moves-noalloc-$d03" bst_ok "bdwgc/frame-addr-moves-alloc-$d05" m01=$(awk '$1=="M01"{print $2}' "$got") m02=$(awk '$1=="M02"{print $2}' "$got") bst_ok "bdwgc/mmap-anon-$m01" bst_ok "bdwgc/mmap-fixed-honoured-$m02" fi fi rm -f "$probe" "$got" "$exp" fi # --- upstream's own programs ------------------------------------------------ # # gctest is the collector's acceptance test and the ticket's named criterion. # It is ALSO the case most exposed to the wasm-locals gap described at the top, # because it holds live references in ordinary locals throughout. A red here is # read as a measurement of that gap, not as a broken package -- see the design # doc's Issues Found for the attribution. run_upstream() { # $1 program basename, $2 case id, $3 needle expected in its output _src=$TESTDIR/$1 _bin=$tmp/bst-$1.$$ if [ ! -f "$_src" ]; then bst_notok "bdwgc/$2-present" "no $_src" return fi bst_ok "bdwgc/$2-present" if ! cp "$_src" "$_bin" 2>/dev/null || ! chmod +x "$_bin" 2>/dev/null; then bst_notok "bdwgc/$2-stage" "cannot stage $_src into $tmp" rm -f "$_bin" return fi _out=$tmp/bst-$1-out.$$ "$_bin" > "$_out" 2>&1 _rc=$? if [ "$_rc" = 0 ]; then bst_ok "bdwgc/$2-rc" else bst_notok "bdwgc/$2-rc" "exit $_rc; last line: $(tail -1 "$_out" 2>/dev/null)" # A bounded dump of what it actually said, as comment lines the BST # parsers ignore. The last line alone is nearly useless for this package: # the collector's ABORT() traps, so the shell reports `Illegal # instruction' and the REASON is the line above it. Bounded at 20 so a # program that fails by printing forever cannot cost the run its sentinel. sed -n "1,20p" "$_out" 2>/dev/null | sed "s|^|# $2: |" fi if [ -n "$3" ]; then if grep -q "$3" "$_out" 2>/dev/null; then bst_ok "bdwgc/$2-output" else bst_notok "bdwgc/$2-output" "no '$3' in output; last line: $(tail -1 "$_out" 2>/dev/null)" fi fi rm -f "$_bin" "$_out" } run_upstream bdwgc-gctest gctest "Collector appears to work" run_upstream bdwgc-leaktest leaktest "" run_upstream bdwgc-middletest middletest "Final heap size is" run_upstream bdwgc-realloc_test realloc_test "Heap size:" run_upstream bdwgc-smashtest smashtest ""