. "$BST_LIB" # BST-TIMEOUT: 600 # socat 1.8.0.0 (poky recipes-connectivity/socat) -- the multipurpose # bidirectional stream relay. # # WHAT THE TICKET ASKS FOR, AND WHY IT IS THE RIGHT SHAPE FOR THIS IMAGE. # BRINTOS-583 names one pass criterion: relay a fixed payload through a # UNIX-domain socket and through a pipe pair, and compare the bytes out against # the bytes in. That is exactly deterministic and entirely on-target -- there is # no peer off this machine, no device, and no operator, and socat needs none of # them to be exercised meaningfully. A relay either delivers the bytes it was # given or it does not, and `cmp` decides it with no tolerance. # # TWO PAYLOADS, BECAUSE THEY ASK DIFFERENT QUESTIONS. The text payload is 48000 # bytes, comfortably more than socat's 8192-byte transfer block, so a relay that # moved one buffer and stopped fails it; the binary payload is all 256 byte # values, so a relay that is not eight-bit clean -- NUL-truncating, newline # translating, or high-bit stripping -- fails it. A suite carrying only the # second would not notice a short transfer, and one carrying only the first # would not notice a mangled byte. # # WHAT ELSE IS ASSERTED, AND WHY IT IS ALL OFFLINE. Everything past the named # criterion is still local by construction: AF_UNIX in both its stream and # datagram flavours and in the Linux abstract namespace, socat's own fork+exec # pipe pair (EXEC:, SYSTEM:), a pty, and loopback TCP/UDP. Loopback is a real # socket stack exercise with no route off the machine -- the same lo the openssh # suite drives -- so it belongs here rather than in the manual column. # # NOTHING IN THIS FILE EVER WAITS ON A SERVER, AND THAT IS NOT A STYLE CHOICE. # socat addresses are servers: UNIX-LISTEN, TCP4-LISTEN, UNIX-RECVFROM and PIPE # all park until a peer arrives. On this arm a parked process cannot be reclaimed # -- signal delivery is only observed when the task next crosses a dispatch # boundary (brintos/linux#21), and a task parked in accept(2) never reaches one -- # so `kill` does not kill it and `timeout` does not bound it, because `timeout` # bounds by sending the signal that is not delivered. A `wait` on such a pid is # therefore an unbounded block, not a reap: the first draft of this suite had # seven of them and hung the guest every time, reporting no cases at all rather # than the reds that would have named the cause. # # What makes the byte comparisons safe without a wait is that the CLIENT is # always the process that terminates: it reads to EOF and exits when the server # closes, so the client returning already means the transfer finished. The one # case whose result depends on a background writer instead -- the datagram # receiver -- polls its output file. The `timeout` wrappers are kept because they # cost nothing and do bound the case where the peer arrived, but they are not # what makes this file safe. # # Each pairing still waits on the listener's own artifact (the socket node, the # FIFO, the pty symlink) appearing rather than on a fixed delay -- sleep(1) does # not sleep on this image (~0.29 s, linux-in-the-browser#120), so a fixed delay # would be neither a bound nor a wait. require socat socat/present D=${TMPDIR:-/tmp}/bst-socat.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok socat/workdir "cannot create $D"; exit 0; } # Bound every server. busybox and coreutils both provide timeout; if neither # does, say so once and let the cases run unbounded rather than skipping the # ticket's whole criterion -- an unbounded run is still a result, and the # runner's own per-suite bound is the backstop. T=""; TC="" if command -v timeout >/dev/null 2>&1; then T="timeout 20"; TC="timeout 6"; else bst_notok socat/timeout-available "no timeout(1) on this image; server cases run unbounded under the suite bound alone" fi # A listener with no filesystem artifact (TCP, UDP, the abstract namespace) # cannot be waited on the way a socket node or FIFO can, so those clients retry. # The retry budget is deliberately small and the per-attempt bound short: this is # a product, and 40 attempts at 20 s apiece is 13 minutes PER CASE on the failure # path -- enough for four such cases to blow the whole suite's bound and report a # timeout instead of the four reds that would have named the cause. TRIES=10 # wait_for -- bounded poll on a listener's own artifact. wait_for() { _n=0 while [ "$_n" -lt 40 ]; do [ "$1" "$2" ] && return 0 _n=$((_n+1)); sleep 1 done return 1 } # gap -- the red for a failure this port has already traced # to a platform gap, named at the case rather than left for a reader to # re-derive. An unrecognised failure falls through to a plain red on purpose: # that is what a regression in socat itself should look like, and it must not be # absorbed by an attribution written for a different cause. gap() { case "$2" in *"Cannot allocate memory"*) bst_notok "$1" "$BST_UNRUN_TAG ($BST_UNRUN_ISSUE): a 51 MiB socat image could not be staged this late in the run, so the assertion never ran -- $(_bst_1line "$2")" ;; *"system("*"returned with status"*) bst_notok "$1" "brintos/linux#14 (system(3) ENOSYS): socat's SYSTEM: address goes through system(3), which this kernel does not wire -- $(_bst_1line "$2")" ;; *) bst_notok "$1" "$(_bst_1line "$2")" ;; esac } # relayed -- the byte-for-byte verdict this suite exists # for. Split from the rc case deliberately: a relay that exits 0 having moved # nothing is the failure mode a status check cannot see. relayed() { if [ ! -e "$3" ]; then gap "$1" "no output produced -- $4" elif cmp -s "$2" "$3"; then bst_ok "$1" else gap "$1" "$(wc -c < "$2") bytes in, $(wc -c < "$3" 2>/dev/null) out -- $4"; fi } # --- the payloads ------------------------------------------------------------- # 48000 bytes of fixed text: six transfer blocks, so a short relay is visible. awk 'BEGIN{for(n=1;n<=800;n++) printf "%04d the quick brown fox jumps over the lazy dog 0123456789\n", n}' > "$D/txt.src" # Every byte value 0..255, so a relay that is not eight-bit clean is visible. # # ONE PROCESS, AND THE LOCALE IS PINNED -- both matter. The shell idiom for this # (`printf "\NNN"` in a loop over a command substitution) forks 256 subshells, # and fork is expensive enough on this guest that it took longer than every # assertion in this file combined, with the kernel's per-exit chunk census # flooding the console the whole time. LC_ALL=C is the other half: under a UTF-8 # locale awk's %c emits the UTF-8 ENCODING of code points 128-255, so the # "256-byte" payload is silently 384 bytes and no longer covers the high half at # all. LC_ALL=C awk 'BEGIN{for(i=0;i<256;i++) printf "%c", i}' > "$D/bin.src" check_eq socat/payload-text 48000 "$(wc -c < "$D/txt.src" | tr -d ' ')" check_eq socat/payload-binary 256 "$(wc -c < "$D/bin.src" | tr -d ' ')" # # THIS SUITE IS SPLIT IN TWO, AND THE SPLIT IS LOAD-BEARING. socat links openssl # and readline statically, so /usr/bin/socat1 is 51 MiB, and this guest does not # return memory on process exit. One boot cannot stage the ~25 socat execs the # full set needs: the tail of the run failed with `timeout: failed to run # command 'socat': Cannot allocate memory` (brintos/linux#44), and WHICH cases # fell over moved between runs, because it is a resource boundary rather than a # defect in any one case. Splitting lets the host driver give each half a fresh # guest (`brintos_local_verify.py --batch 1`, the mechanism the packager README # documents for exactly this). `--suite 88-socat` still selects both halves. # --- identity and inventory --------------------------------------------------- check_out socat/version "socat version 1.8.0.0" socat -V inv="" for f in /usr/bin/socat /usr/bin/filan /usr/bin/procan; do [ -e "$f" ] || inv="$inv $f" done check_eq socat/inventory "" "$inv" # Nothing in these binaries is an unresolved-symbol trapping stub (BRINTOS-197): # socat's whole job is syscalls, so a stub here would surface as a relay that # traps rather than as a link error. check_eq socat/no-trap-stubs "" \ "$(strings /usr/bin/socat /usr/bin/filan /usr/bin/procan 2>/dev/null | grep signature_mismatch)" check_out socat/help "socat [options] " sh -c "socat -h 2>&1" # The compiled-in feature set is what decides which addresses below can work at # all, so read it off the binary rather than assuming the recipe's PACKAGECONFIG # survived the cross build. socat prints `#undef WITH_X` for a feature configure # turned off, which is the whole point of asserting it here: a probe that # silently answered no leaves a binary that runs fine and quietly cannot do the # thing it was configured to do. The last three are exactly the recipe's # PACKAGECONFIG -- readline, openssl, tcp-wrappers -- so a red there is the # recipe's request not having survived configure. feat=$(socat -V 2>&1) for f in WITH_UNIX WITH_ABSTRACT_UNIXSOCKET WITH_TCP WITH_UDP WITH_EXEC WITH_PTY \ WITH_READLINE WITH_OPENSSL WITH_LIBWRAP; do case "$feat" in *"#define $f 1"*) bst_ok "socat/feature-$f" ;; *) bst_notok "socat/feature-$f" "configure left it out; the addresses that need it cannot work" ;; esac done # --- error paths -------------------------------------------------------------- # A relay that accepted anything would pass every transfer case below by # accident, so the refusals are asserted in their own right. check_rc socat/reject-unknown-address 1 sh -c "socat -u NOSUCHADDR:x - &1; :" check_rc socat/reject-missing-file 1 sh -c "socat -u FILE:$D/no-such-file - "$D/ul.err" & lpid=$! if wait_for -S "$sock"; then bst_ok socat/unix-socket-created out=$($T socat -u UNIX-CONNECT:"$sock" FILE:"$D/txt.out",create 2>&1); rc=$? if [ "$rc" -eq 0 ]; then bst_ok socat/unix-relay-rc else bst_notok socat/unix-relay-rc "rc=$rc $(_bst_1line "$out")"; fi relayed socat/unix-relay-bytes "$D/txt.src" "$D/txt.out" "$(_bst_1line "$out")" else kill $lpid 2>/dev/null why="UNIX-LISTEN created no socket at $sock: $(_bst_1line "$(cat "$D/ul.err" 2>/dev/null)")" bst_notok socat/unix-socket-created "$why" bst_notok socat/unix-relay-rc "$why" bst_notok socat/unix-relay-bytes "$why" fi # ...and the same path with the eight-bit payload. sock=$D/relay-bin.sock $T socat -u FILE:"$D/bin.src" UNIX-LISTEN:"$sock" 2>"$D/ulb.err" & lpid=$! if wait_for -S "$sock"; then out=$($T socat -u UNIX-CONNECT:"$sock" FILE:"$D/bin.out",create 2>&1) relayed socat/unix-relay-binary "$D/bin.src" "$D/bin.out" "$(_bst_1line "$out")" else kill $lpid 2>/dev/null bst_notok socat/unix-relay-binary "UNIX-LISTEN created no socket: $(_bst_1line "$(cat "$D/ulb.err" 2>/dev/null)")" fi # Bidirectional: a forked EXEC:cat behind the listener, driven from stdio. This # is the round trip -- the bytes go out over AF_UNIX, through a child process's # stdin/stdout pipe pair, and back over the same socket -- so it exercises the # full-duplex half-close handling a unidirectional relay never reaches. sock=$D/echo.sock $T socat UNIX-LISTEN:"$sock" EXEC:cat 2>"$D/ue.err" & spid=$! if wait_for -S "$sock"; then $T socat -t2 - UNIX-CONNECT:"$sock" < "$D/txt.src" > "$D/echo.out" 2>"$D/uec.err"; rc=$? if [ "$rc" -eq 0 ]; then bst_ok socat/unix-echo-rc else bst_notok socat/unix-echo-rc "rc=$rc $(_bst_1line "$(cat "$D/uec.err" 2>/dev/null)")"; fi relayed socat/unix-echo-roundtrip "$D/txt.src" "$D/echo.out" "$(_bst_1line "$(cat "$D/uec.err" 2>/dev/null)")" else why="UNIX-LISTEN,fork created no socket: $(_bst_1line "$(cat "$D/ue.err" 2>/dev/null)")" bst_notok socat/unix-echo-rc "$why" bst_notok socat/unix-echo-roundtrip "$why" fi kill $spid 2>/dev/null # AF_UNIX datagrams are a different socket type on the same family, and a # datagram relay has no stream to hide a short write in. $T socat -u UNIX-RECVFROM:"$D/dg.sock" FILE:"$D/dg.out",create 2>"$D/dg.err" & rpid=$! if wait_for -S "$D/dg.sock"; then printf 'socat-dgram-marker\n' | $T socat -u - UNIX-SENDTO:"$D/dg.sock" 2>>"$D/dg.err" wait_for -s "$D/dg.out" check_eq socat/unix-datagram "socat-dgram-marker" "$(cat "$D/dg.out" 2>/dev/null)" else kill $rpid 2>/dev/null bst_notok socat/unix-datagram "UNIX-RECVFROM created no socket: $(_bst_1line "$(cat "$D/dg.err" 2>/dev/null)")" fi # The Linux abstract namespace has no filesystem entry at all, so it is the one # AF_UNIX case that cannot be satisfied by the virtiofs root doing something # helpful -- it is purely the kernel's socket layer. $T socat -u FILE:"$D/txt.src" ABSTRACT-LISTEN:bst-socat-583 2>"$D/ab.err" & lpid=$! i=0; arc=1 while [ $i -lt $TRIES ]; do $TC socat -u ABSTRACT-CONNECT:bst-socat-583 FILE:"$D/abs.out",create 2>>"$D/ab.err" && { arc=0; break; } i=$((i+1)); sleep 1 done if [ "$arc" -eq 0 ]; then relayed socat/abstract-unix "$D/txt.src" "$D/abs.out" "$(_bst_1line "$(cat "$D/ab.err" 2>/dev/null)")" else gap socat/abstract-unix "never connected: $(cat "$D/ab.err" 2>/dev/null)"; fi # --- the named criterion, part 2: a pipe pair --------------------------------- # socat's PIPE address makes the FIFO itself (mknod), so this asserts the # creation as well as the transfer. It is made under $TMPDIR rather than the # root: the virtiofs root refuses mkfifo, which psmisc found first, so a FIFO # there would fail for a reason that has nothing to do with socat. fifo=$D/relay.fifo $T socat -u FILE:"$D/txt.src" PIPE:"$fifo" 2>"$D/pw.err" & wpid=$! if wait_for -p "$fifo"; then bst_ok socat/fifo-created out=$($T socat -u PIPE:"$fifo" FILE:"$D/fifo.out",create 2>&1); rc=$? if [ "$rc" -eq 0 ]; then bst_ok socat/fifo-relay-rc else bst_notok socat/fifo-relay-rc "rc=$rc $(_bst_1line "$out")"; fi relayed socat/fifo-relay-bytes "$D/txt.src" "$D/fifo.out" "$(_bst_1line "$out")" else kill $wpid 2>/dev/null why="PIPE: created no FIFO at $fifo: $(_bst_1line "$(cat "$D/pw.err" 2>/dev/null)")" bst_notok socat/fifo-created "$why" bst_notok socat/fifo-relay-rc "$why" bst_notok socat/fifo-relay-bytes "$why" fi # The anonymous pipe pair: two socats joined by the shell's own pipe(2), with # the eight-bit payload so a pipe that mangled a byte would show. # The two stderrs go to SEPARATE files, and the reader's is quoted first. Both # ends failing at once is the normal shape here: when the reader cannot be # staged, the writer reports `Broken pipe` -- and that is the writer describing # the reader's death, not a fault of its own. Sharing one file put the writer's # EPIPE ahead of the reader's real diagnostic, which the detail field then # truncated away, and the case came out as an unattributed red. $T socat -u FILE:"$D/bin.src" - 2>"$D/anw.err" | $T socat -u - FILE:"$D/anon.out",create 2>"$D/anr.err" relayed socat/pipe-anonymous "$D/bin.src" "$D/anon.out" \ "reader: $(cat "$D/anr.err" 2>/dev/null) | writer: $(cat "$D/anw.err" 2>/dev/null)" # socat's OWN pipe pair: EXEC: forks a child and wires it to two pipes it made # itself. This is the half of brintos/linux#14 that works (fork+exec), and it is # what the ticket's "pipe pair" means when socat rather than the shell builds it. $T socat -u FILE:"$D/txt.src" EXEC:cat > "$D/exec.out" 2>"$D/ex.err" relayed socat/exec-pipe-pair "$D/txt.src" "$D/exec.out" "$(_bst_1line "$(cat "$D/ex.err" 2>/dev/null)")" # SYSTEM: is the same fork, then /bin/sh -c -- socat spawns the shell itself # rather than going through system(3), which is the ENOSYS half of #14. $T socat -u FILE:"$D/txt.src" SYSTEM:"cat > $D/system.out" 2>"$D/sy.err" relayed socat/system-shell "$D/txt.src" "$D/system.out" "$(_bst_1line "$(cat "$D/sy.err" 2>/dev/null)")"