. "$BST_LIB" # BST-TIMEOUT: 600 # # gensio 2.8.4 (meta-oe recipes-connectivity) -- a stream I/O abstraction: one # API over TCP, UDP, serial, telnet, SSL and stdio, composed as stacked layers, # shipping libgensio.a, libgensioosh.a, the header tree and /usr/bin/gensiot. # # EVERY EXPECTED VALUE BELOW IS PINNED FROM AN x86-64 CONTROL, not guessed: the # same SRCREV (300644fc) configured with exactly the recipe's switches -- static # only, pselect poller, USE_PTHREADS on, C++/glib/tcl/sctp/sound/mdns off -- # built natively and driven through this same sequence on 2026-08-18. The # control was run in TWO network shapes, an ordinary host and a network # namespace carrying only `lo', because the second is what this guest is and the # first hides the defect gensio_%.bbappend's 0002 patch fixes. Both shapes run # 42/42 green through gensio-probe with that patch and 10/42 without it. # # THREE PLATFORM FACTS SHAPE THIS FILE. # # 1. THE GUEST HAS ONE INTERFACE, `lo', and no route out of the browser # sandbox. Everything here is therefore 127.0.0.1, and the port is always # asked for as 0 and read back, never picked -- a listener this file did not # start must never be mistaken for one it did, and a fixed port cannot # promise that. # # That same fact is why gensio needs a patch at all: its resolver hardcodes # AI_ADDRCONFIG, which glibc reads as "only if a NON-loopback address of this # family exists", so an unpatched gensio cannot allocate so much as # tcp,127.0.0.1,0 here. `gensio/addrconfig-independent' is the standing # guard on that patch and was verified to go red against the unpatched # archive, in this exact network shape, rather than assumed to. # # 2. sleep(1) IS A NO-OP ON THIS GUEST, so nothing here sleeps. The two-process # gensiot exchange in section E synchronises on facts instead: the server's # own --printacc line proves the accepter is listening, and a fifo held open # by this shell keeps each gensiot's io1 from reaching EOF before its # connection is up. Every wait is bounded by /proc/uptime, so a hang costs # one legible red rather than the suite's ==BST-END== sentinel. # # The fifo is not decoration. gensiot shuts down when its io1 closes, and a # plain `< file' redirect reaches EOF immediately -- measured on the control, # where that arrangement transferred nothing in either direction and both # processes still exited 0. # # 3. THREADS ARE REFUSED HERE (brintos/linux-in-the-browser#85) and nothing in # this file needs one. gensio creates no thread at start-up -- pthread_create # appears once in its whole library, inside gensio_os_new_thread() -- and # gensio-probe drives an accepter and a client in ONE thread, which works # because gensio's synchronous calls run the event loop while they wait. # If a case here goes red for a thread reason, that is news, not the # expected shape. # # WHAT IS DELIBERATELY NOT ASSERTED: that /usr/libexec/gensio-tests/gensio-probe # is executable where it sits. It is installed 0755 by the recipe, but the # rootfs is read-only at browser runtime, so section D stages it to /tmp and # lets the cp/chmod carry the executable question. require gensiot gensio/present PROBE=/usr/libexec/gensio-tests/gensio-probe D=${TMPDIR:-/tmp}/bst-gensio.$$ rm -rf "$D" mkdir -p "$D" || { bst_notok gensio/workdir "cannot create $D"; exit 0; } now() { read NOW _rest < /proc/uptime; NOW=${NOW%.*}; } spin() { _i=0; while [ $_i -lt 20000 ]; do _i=$((_i + 1)); done; } # alive -- true only while is a RUNNING child, false once it has # exited. `kill -0' is the obvious test and it is wrong here: this shell is the # parent, so an exited child stays a zombie until it is waited for, and kill -0 # succeeds against a zombie. Written with kill -0 this file reported "a gensiot # was still running 20s after its io1 closed" against two processes that had # both already exited -- caught on the x86-64 dry run of this very file, which # is why the dry run happened before it shipped. # # /proc//stat's state is the field after the ')' that ends comm; comm is # the only field that may contain spaces, so stripping through the last ')' # leaves state first. Builtins only -- no exec per poll. alive() { # The -r test is not redundant with the read's own failure: dash reports a # redirection onto a vanished /proc entry on stderr regardless of 2>/dev/null, # and this runs in a spin loop, so the transcript fills with it. [ -r "/proc/$1/stat" ] || return 1 read _al_line < "/proc/$1/stat" 2>/dev/null || return 1 case "${_al_line##*) }" in Z*) return 1 ;; esac return 0 } # have have() { if [ -f "$2" ]; then bst_ok "$1"; else bst_notok "$1" "missing: $2"; fi } # =========================================================================== # A. WHAT THE PORT SHIPS # =========================================================================== have gensio/tool /usr/bin/gensiot have gensio/lib-static /usr/lib/libgensio.a have gensio/libosh-static /usr/lib/libgensioosh.a have gensio/header /usr/include/gensio/gensio.h have gensio/header-osfuncs /usr/include/gensio/gensio_os_funcs.h have gensio/pkgconfig /usr/lib/pkgconfig/libgensio.pc have gensio/probe-shipped "$PROBE" # 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 gensio/no-shared "/usr/lib/libgensio.so*" # =========================================================================== # B. THE TOOL ANSWERS FOR ITSELF # =========================================================================== check_out gensio/tool-version "2.8.4" gensiot --version check_out gensio/tool-help "gensios together" gensiot --help # =========================================================================== # C. THE CERTIFICATE THIS SUITE GENERATES # # The ticket asks for the ssl case to run against a certificate the suite makes, # which is what proves the openssl PACKAGECONFIG linked rather than merely being # named in the recipe -- an unlinked engine would take gensio's ssl layer out # with it, and section D would then say so per case. # # prime256v1 rather than rsa:2048: measured at 4 ms against ~1 s natively, and # this guest is far slower than the control. RSA is the fallback so a build # whose openssl lacks EC still gets a certificate rather than losing section D's # whole ssl third. # =========================================================================== CERTDIR=$D/certs mkdir -p "$CERTDIR" CERTLOG=$D/certgen.log if openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ -days 3650 -keyout "$CERTDIR/key.pem" -out "$CERTDIR/cert.pem" \ -subj "/CN=localhost" > "$CERTLOG" 2>&1 \ && [ -s "$CERTDIR/key.pem" ] && [ -s "$CERTDIR/cert.pem" ]; then bst_ok gensio/cert-generated elif openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ -keyout "$CERTDIR/key.pem" -out "$CERTDIR/cert.pem" \ -subj "/CN=localhost" >> "$CERTLOG" 2>&1 \ && [ -s "$CERTDIR/key.pem" ] && [ -s "$CERTDIR/cert.pem" ]; then bst_ok gensio/cert-generated else bst_notok gensio/cert-generated "openssl req failed for both ec and rsa: $(_bst_1line "$(cat "$CERTLOG" 2>&1)")" fi # =========================================================================== # D. THE LIBRARY, THROUGH ONE IN-PROCESS CONSUMER # # gensio-probe opens an accepter and a client on tcp(127.0.0.1,) inside one # process, writes a 1024-byte fixture through and reads it back byte for byte, # then does the same over telnet stacked on that TCP gensio and again over ssl # with the certificate above. It also drives pipe(2), poll(2), socketpair(2) and # a complete POSIX TCP echo as negative controls, and measures both getaddrinfo # answers, so a red here can be localised instead of guessed at. # # Its case list is enumerated below rather than read out of its output, so a # probe that dies half way contributes `not ok' for what it never reached. # =========================================================================== PROBE_CASES="version err-to-str sys-pipe sys-poll sys-socketpair posix-tcp-echo resolve-plain osfuncs-alloc proc-setup tcp-acc-alloc tcp-acc-startup tcp-acc-port tcp-client-alloc tcp-client-open tcp-accepted tcp-sync tcp-c2s-exact tcp-s2c-exact tcp-close addrconfig-independent telnet-acc-alloc telnet-acc-startup telnet-acc-port telnet-client-alloc telnet-client-open telnet-accepted telnet-sync telnet-c2s-exact telnet-s2c-exact telnet-close ssl-acc-alloc ssl-acc-startup ssl-acc-port ssl-client-alloc ssl-client-open ssl-accepted ssl-encrypted ssl-sync ssl-c2s-exact ssl-s2c-exact ssl-close probe-complete" POUT=$D/probe.out PERR=$D/probe.err : > "$POUT"; : > "$PERR" PRC=n/a STAGED=$D/gensio-probe if cp "$PROBE" "$STAGED" 2>/dev/null && chmod +x "$STAGED" 2>/dev/null; then bst_ok gensio/probe-stage "$STAGED" "$CERTDIR" > "$POUT" 2> "$PERR"; PRC=$? else bst_notok gensio/probe-stage "cannot stage $PROBE into $D" fi # THE PROBE'S EXEC IMAGE IS ~46 MB AND THIS GUEST INTERMITTENTLY REFUSES TO # STAGE ONE. The kernel says so in its own voice, on the console: # # [k] binfmt_wasm: staging a 46740070-byte image needs more than 1024 # segments (heap too fragmented to hand out 4096-byte blocks; cap # WASM32_IMG_MAX_SEGS=1024) — refusing -ENOMEM rather than degrading # further (never-silent) # # Measured here: 1 boot in 3 on the merged-tree image. A case whose command was # never staged must not claim a verdict, so those 42 cases become bst_unrun -- # the attributed form bst.sh provides for exactly this, tagged # UNRUN-EXEC-ENOMEM against brintos/linux#44 -- rather than 42 ordinary reds # that read like a package regression. That misreporting is what the first # merged-tree run did, and it is why the probe's rc and stderr are now carried # into every detail below instead of being written to a file nobody reads. PERRTXT=$(cat "$PERR" 2>/dev/null) # ONE awk PASS EMITS ALL 42 RESULT LINES, rather than one command substitution # per case. Exec count is a correctness property of a test file on this platform, # not a performance one -- 88-civetweb wedged the guest at 62 -- and a per-case # `$(awk ...)' loop is 42 of them before this file has done anything else. # # The probe's `I resolve plain= addrconfig=' line is evidence, not a case: # both answers are correct glibc behaviour, and which one you get is a property # of the host's interface list. It is carried into the detail of every red, so a # reader never has to go looking for the network shape the run happened in. if [ ! -s "$POUT" ] && bst_execfail "$PERRTXT"; then for c in $PROBE_CASES; do bst_unrun "gensio/$c" "$PERRTXT"; done else awk -v cases="$PROBE_CASES" -v whynot="probe rc=$PRC $(_bst_1line "$PERRTXT")" ' function oneline(s) { gsub(/[\n\t]/, " ", s) if (length(s) > 100) s = substr(s, 1, 100) return s } /^I resolve / { sub(/^I resolve[ \t]+/, ""); resolve = $0; next } $1 == "R" && $2 ~ /^gensio\// { st[$2] = $3 d = "" for (i = 4; i <= NF; i++) d = d (i > 4 ? " " : "") $i det[$2] = d next } END { if (resolve == "") resolve = "resolve line absent -- the probe did not reach it" n = split(cases, c, /[ \t\n]+/) for (i = 1; i <= n; i++) { if (c[i] == "") continue id = "gensio/" c[i] if (st[id] == "OK") printf "BST ok %s\n", id else if (st[id] == "FAIL") printf "BST not ok %s : %s [%s]\n", id, oneline(det[id]), resolve else printf "BST not ok %s : gensio-probe reported no result for this case -- %s [%s]\n", id, whynot, resolve } }' "$POUT" fi # =========================================================================== # E. gensiot ON THE COMMAND LINE, TWO INSTANCES OVER THE LOOPBACK # # The ticket's last clause. gensiot carries one gensio per process, so this is # genuinely a different assertion from section D rather than a repeat of it: it # says the shipped PROGRAM connects two of itself over 127.0.0.1 and relays in # both directions. # # Synchronisation is by fact, not by sleep (see fact 2 at the top): the server's # --printacc line on stderr is what says the accepter is listening and what # supplies the port, and each side's io1 is a fifo this shell holds open so # neither shuts down before its connection is up. # =========================================================================== SF=$D/sf; CF=$D/cf SRV_OUT=$D/srv.out; SRV_ERR=$D/srv.err CLI_OUT=$D/cli.out; CLI_ERR=$D/cli.err SRV_MSG=gensiot-server-payload-1234 CLI_MSG=gensiot-client-payload-5678 if mkfifo "$SF" "$CF" 2>/dev/null; then bst_ok gensio/cli-fifo else bst_notok gensio/cli-fifo "mkfifo failed under $D" fi if [ -p "$SF" ] && [ -p "$CF" ]; then : > "$SRV_ERR" # `exec 3<> fifo' rather than `exec 3> fifo'. Opening a fifo write-only BLOCKS # until a reader appears, and on this guest that blocking open is interrupted: # the first run of this file died on `can't create .../cf: Interrupted system # call', the same EINTR-on-open class bst.sh records for linux#73. Read-write # never blocks, and it still holds a writer open so gensiot's io1 does not # reach EOF. Opening it BEFORE gensiot starts also means gensiot's own # read-only open finds a writer already there and does not block either. # Nothing ever reads fd 3 or fd 4, so the extra reader is inert. exec 3<> "$SF" # `3>&-' closes the shell's fifo-writer fd IN THE CHILD. A background job # inherits every fd the shell has open, so without it each gensiot is itself a # writer on the fifo it is reading, the writer count never reaches zero, and # neither process ever sees EOF -- both were still running 20 s after the # shell dropped its own fd on the x86-64 dry run of this file. The connector # below closes fd 3 as well as fd 4, or it would pin the accepter's fifo open. gensiot -p -a "tcp,127.0.0.1,0" < "$SF" > "$SRV_OUT" 2> "$SRV_ERR" 3>&- & SRVPID=$! # The accepter's own report of where it bound. Bounded: 20 s of /proc/uptime. PORT= now; t0=$NOW while [ -z "$PORT" ]; do PORT=$(awk '/^Address /{ n=split($NF, a, ","); print a[n]; exit }' "$SRV_ERR" 2>/dev/null) [ -n "$PORT" ] && break now; [ $((NOW - t0)) -ge 20 ] && break spin done case "$PORT" in ''|*[!0-9]*) bst_notok gensio/cli-accepter-port \ "gensiot -p printed no usable port in 20s: $(_bst_1line "$(cat "$SRV_ERR" 2>&1)")" PORT= ;; *) bst_ok gensio/cli-accepter-port ;; esac if [ -n "$PORT" ]; then exec 4<> "$CF" gensiot "tcp,127.0.0.1,$PORT" < "$CF" > "$CLI_OUT" 2> "$CLI_ERR" 3>&- 4>&- & CLIPID=$! printf '%s\n' "$SRV_MSG" >&3 printf '%s\n' "$CLI_MSG" >&4 now; t0=$NOW while : ; do grep -q "$CLI_MSG" "$SRV_OUT" 2>/dev/null && grep -q "$SRV_MSG" "$CLI_OUT" 2>/dev/null && break now; [ $((NOW - t0)) -ge 30 ] && break spin done if grep -q "$CLI_MSG" "$SRV_OUT" 2>/dev/null; then bst_ok gensio/cli-client-to-server else bst_notok gensio/cli-client-to-server \ "the accepting gensiot did not relay the client's payload in 30s: out=$(_bst_1line "$(cat "$SRV_OUT" 2>&1)") err=$(_bst_1line "$(cat "$SRV_ERR" 2>&1)")" fi if grep -q "$SRV_MSG" "$CLI_OUT" 2>/dev/null; then bst_ok gensio/cli-server-to-client else bst_notok gensio/cli-server-to-client \ "the connecting gensiot did not relay the server's payload in 30s: out=$(_bst_1line "$(cat "$CLI_OUT" 2>&1)") err=$(_bst_1line "$(cat "$CLI_ERR" 2>&1)")" fi exec 4>&- else bst_notok gensio/cli-client-to-server "no port to connect to" bst_notok gensio/cli-server-to-client "no port to connect to" fi exec 3>&- # Closing both fifos ends each gensiot's io1, which is how gensiot is meant to # terminate. A process still alive after that is a finding of its own, so it # gets a case rather than a silent kill. now; t0=$NOW while alive "$SRVPID" || { [ -n "${CLIPID:-}" ] && alive "$CLIPID"; }; do now; [ $((NOW - t0)) -ge 20 ] && break spin done STILL= alive "$SRVPID" && STILL="accepter" [ -n "${CLIPID:-}" ] && alive "$CLIPID" && STILL="${STILL:+$STILL and }connector" if [ -n "$STILL" ]; then bst_notok gensio/cli-exits "the $STILL gensiot was still running 20s after its io1 closed" kill -9 "$SRVPID" ${CLIPID:-} 2>/dev/null else bst_ok gensio/cli-exits fi wait "$SRVPID" ${CLIPID:-} 2>/dev/null else bst_notok gensio/cli-accepter-port "no fifo -- see gensio/cli-fifo" bst_notok gensio/cli-client-to-server "no fifo -- see gensio/cli-fifo" bst_notok gensio/cli-server-to-client "no fifo -- see gensio/cli-fifo" bst_notok gensio/cli-exits "no fifo -- see gensio/cli-fifo" fi rm -rf "$D"