# BST-TIMEOUT: 600 # ~40 cases, of which ~25 are an exec, and an exec is a # # fresh wasm module instantiation (~2-3 s) on this arm -- # # so the floor is ~75 s before the probe runs at all, and # # the probe's own watchdog is 90 s. 600 is the headroom the # # nightly needs on a loaded host. . "$BST_LIB" # serial-forward 1.1+git (meta-oe recipes-support/serial-utils) -- one directory # of freesmartphone/cornucopia at SRCREV 07c6fdede087. TWO programs, both in # ${bindir} under their underscore names: # # serial_forward opens a device node, applies termios, binds a TCP port and # forwards the two in both directions, forever. # pty_forward connects to a serial_forward, opens a pty of its own and # announces the slave, so a local terminal has something to # attach to. # # --------------------------------------------------------------------------- # THE PACKAGE HAS TWO HALVES AND THEY FAIL FOR DIFFERENT REASONS. # # * THE ARGV HALF -- one usage line, its six triggers, the absence of a # version arm, and the exit status and diagnostic for a node that cannot be # opened. All of it is getopt and string arithmetic, identical on every # architecture, and a wasm32 build that got it wrong has no environmental # excuse available. # # * THE FORWARDING LOOP -- a pty, tcsetattr, a loopback bind, accept(), # select() over two descriptors, and read/write in both directions. This is # where the arm can genuinely differ, and it is where nothing can be # asserted from a shell, because serial_forward has NO verbose mode, NO # diagnostic between "the device opened" and "bytes are moving", and NO # exit path at all. Every failure in that half looks like one thing: a # process that does not come back. # # --------------------------------------------------------------------------- # THE PREMISE THE TICKET GIVES IS MEASURED, NOT ASSUMED, AND IT DOES NOT HAVE # THE CONSEQUENCE IT LOOKS LIKE IT HAS. # # The ticket says this image exposes no tty device. There is indeed no real # serial line: /dev/ttySER0 is registered by the wasm32 driver and refuses # open() with -ENODEV until a host WebSerial port attaches -- the grant # boundary, linux-in-the-browser#60 M2, the same one 59-picocom.test records # against the same node. # # But a pty SLAVE is a tty, tcsetattr works on one, and this guest HAS ptys: # 79-serial.test drives seventeen cases through a posix_openpt/grantpt/unlockpt # pair, including a full termios reconfiguration read back. So the forwarding # loop is measurable here, and the probe measures it. The skip the ticket asks # for is still wired -- but it fires on the probe REPORTING no pty, never on # the sentence above. # # --------------------------------------------------------------------------- # WHAT THE PROBE ANSWERS THAT A SHELL CANNOT. # # /usr/libexec/serial-forward-tests/serial-forward-probe stands both programs up # against a pty pair it owns and reports each stage separately: the pty # allocation, the connect, each direction of the transfer, the canonical-mode # boundary, the return to accept(), and the full two-process chain. # # THE CANONICAL CASE IS THE ONE THAT PROVES set_termios RAN. set_termios leaves # the slave in ICANON, so a partial line must NOT come through and the whole # line must arrive at once when it is completed. A pty left in raw mode -- a # tcsetattr that silently did nothing -- passes every transfer case above it and # fails only that one. # # ORACLE: a native x86-64 run of the same source, 2026-08-26. Every line rc=0, # connect on attempt 2, 14 bytes each way, early=0 then the whole line, a second # connection accepted, and CHAIN\r\n out the far end through both processes. # # --------------------------------------------------------------------------- # WHAT THIS SUITE DELIBERATELY DOES NOT DRIVE, SAID RATHER THAN OMITTED. # # * serial_forward AGAINST A NODE IT CAN OPEN, from the shell. It would # succeed, and then never return -- there is no exit path in main(). Every # such invocation lives inside the probe, which owns a watchdog and reaps # its children. # * pty_forward AGAINST A LIVE LISTENER, from the shell, for the same reason. # The probe does it. # * -t hsuart AND -t cy8mrln PAST THE OPEN. Both reach a block of ioctls # against Palm Pre hardware that does not exist on any brintOS image and # whose numbers collide with nothing. Only the open arm each selects is # asserted. # ---------------------------------------------------------------- 0. the decoy # The first result line of every suite is eaten in transport # (brintos/linux#96 / BRINTOS-1149) and the harness counts a dropped line as a # PASS, so the eaten position must never carry an assertion anyone depends on. bst_ok serial-forward/suite-canary require serial_forward serial-forward/present PROBE=/usr/libexec/serial-forward-tests/serial-forward-probe DEADPORT=45843 NONODE=/dev/bst-no-such-node # The probe's own watchdog is 180 s and every wait inside it is bounded, so this # wrapper is a second backstop and not the bound. It is deliberately LONGER than # the watchdog: a `timeout` that fired first would kill the probe mid-stage and # throw away the lines it had already printed, which are the evidence. T="bst_bound 240"; TS="bst_bound 20" # check_fail -- everything serial_forward does # from the command line ends in exit 1, so check_out (which requires rc 0) # cannot express a single case in this file. check_fail() { _id=$1; _want=$2; _needle=$3; shift 3 _out=$("$@" 2>&1 /dev/null)" check_eq serial-forward/pkg/which-pty /usr/bin/pty_forward "$(command -v pty_forward 2>/dev/null)" # Upstream ships no man page and no data of any kind, so there is no -doc # package and nothing under /usr/share to assert. Pinned as an absence, because # a future recipe that grew one should say so rather than drift. check serial-forward/pkg/no-man test ! -e /usr/share/man/man1/serial_forward.1 # --------------------------------------------- 2. the argv half, no descriptor # main() prints ONE usage line and exits 1 whenever it has not been given both # a node and a non-zero port. Six different roads reach it and each is asserted # separately, because they are separate branches of the same getopt loop and a # rewrite could easily lose one. USAGE="Usage: ./serial_forward -n -p " check_fail serial-forward/argv/usage-noargs 1 "$USAGE" serial_forward check_fail serial-forward/argv/usage-h 1 "$USAGE" serial_forward -h check_fail serial-forward/argv/usage-help 1 "$USAGE" serial_forward --help check_fail serial-forward/argv/usage-no-port 1 "$USAGE" serial_forward -n "$NONODE" check_fail serial-forward/argv/usage-no-node 1 "$USAGE" serial_forward -p "$DEADPORT" # atoi() on a non-numeric port yields 0, which is the same arm as no -p at all. check_fail serial-forward/argv/port-nonnumeric 1 "$USAGE" serial_forward -n "$NONODE" -p notanumber # ...and an explicit 0 is indistinguishable from unset, because the test is # `network_port == 0` and there is no separate "was it given" flag. check_fail serial-forward/argv/port-zero 1 "$USAGE" serial_forward -n "$NONODE" -p 0 # The usage text is a STRING LITERAL, "./serial_forward", not argv[0]. Invoked # by absolute path it still says "./serial_forward" -- so unlike most tools of # this shape there is no basename arm to exercise, and asserting the literal # through an absolute path is what establishes that. check_fail serial-forward/argv/usage-literal-path 1 "$USAGE" /usr/bin/serial_forward # The full option list, which is the only place -t's four spellings are # documented at all. check_fail serial-forward/argv/usage-types 1 "type=(serial|hsuart|cy8mrln|raw)" serial_forward check_fail serial-forward/argv/usage-raw 1 "[-r, --raw]" serial_forward # THERE IS NO VERSION ARM, and getopt_long is set opterr = 0, so an unknown # option is not even reported -- it is discarded and the run falls out through # the missing-argument arm. Both halves are pinned: the usage line comes back, # and NOTHING resembling a diagnostic does. If a future serial-forward grows # --version or starts complaining, this reddens, which is the correct signal -- # the ticket asked for version text and there is none to assert. vout=$($TS serial_forward --version 2>&1 " pty_forward # THE PATCHED CRASH. Upstream guards on `argc < 2` and then reads argv[2], so a # host with no port passes the guard and hands atoi() a NULL pointer -- # `pty_forward 127.0.0.1` is SIGSEGV on stock sources, measured on x86-64. # 0003 makes the guard `argc < 3`. This case is the patch: without it the shell # reports 139 and there is no usage line. check_fail serial-forward/pty/one-arg-guard 1 "Usage: ./pty_forward " \ pty_forward 127.0.0.1 # gethostbyname() is reached before the socket is used. The `files` backend is # compiled into static glibc and the `dns` one is a libnss_dns.so this image # does not carry, so an unresolvable name fails cleanly rather than taking the # process down -- the same boundary 61-netcat.test asserts. check_fail serial-forward/pty/bad-host 1 "Failed to get the hostent" \ $TS pty_forward nosuchhost.invalid "$DEADPORT" # A numeric host needs no resolver, so this one reaches connect(2) and comes # back refused. Nothing listens on $DEADPORT until the probe below starts its # own listener, and that is on a different port. check_fail serial-forward/pty/refused 1 "Connection failed" \ $TS pty_forward 127.0.0.1 "$DEADPORT" # ----------------------------------------------------------------- 5. the probe probe_ok=no PO="" if [ ! -x "$PROBE" ]; then bst_notok serial-forward/probe/present \ "no probe at $PROBE -- serial-forward-tests is not installed, so the forwarding loop cannot be attributed" else bst_ok serial-forward/probe/present PO=$($T "$PROBE" 2>&1 . Reads from a VARIABLE, never a redirect: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and # ash exits, ending the suite silently (brintos/linux#73). field() { printf '%s\n' "$PO" | sed -n "s/^$1 / /p" \ | sed -n "s/.*[ ]$2=\\([^ ]*\\).*/\\1/p" | sed -n 1p } line_of() { printf '%s\n' "$PO" | sed -n "s/^$1 //p" | sed -n 1p; } pty_rc=$(field SFP-pty rc); pty_name=$(field SFP-pty name) pty_slave=$(field SFP-pty slave) loop_state=$(field SFP-loop state) ts_present=$(field SFP-ttyser present); ts_openrc=$(field SFP-ttyser open_rc) ts_name=$(field SFP-ttyser name) # Echo what the probe answered, prefixed so it is greppable and plainly not # a result line. Every branch below is a function of these values, and # without them a stored dump records the verdicts without the evidence. printf 'SFP: pty rc=%s name=%s slave=%s | ttySER0 present=%s open_rc=%s name=%s | loop=%s\n' \ "$pty_rc" "$pty_name" "$pty_slave" "$ts_present" "$ts_openrc" "$ts_name" "$loop_state" # --- the pty, which is what decides everything below it ------------------ if [ "$pty_rc" = 0 ]; then bst_ok serial-forward/pty/alloc else bst_notok serial-forward/pty/alloc "posix_openpt/grantpt/unlockpt failed: $pty_name" fi # --- the real serial line, recorded rather than asserted green ------------ # What this node does is a property of the platform and of whether a browser # session granted a WebSerial port, not of this port. Both answers are # legitimate and neither is a defect, so the case that would have been a # verdict is a skip carrying the measurement. if [ "$ts_present" = 1 ]; then bst_ok serial-forward/ttyser/present if [ "$ts_openrc" = 0 ]; then bst_ok serial-forward/ttyser/open else bst_skip serial-forward/ttyser/open \ "open() refused with $ts_name -- no host WebSerial port is attached (the grant boundary, linux-in-the-browser#60 M2)" fi else bst_skip serial-forward/ttyser/present "/dev/ttySER0 absent on this kernel ($ts_name)" bst_skip serial-forward/ttyser/open "/dev/ttySER0 absent on this kernel ($ts_name)" fi # --- the loop ------------------------------------------------------------- case "$loop_state" in no-pty) # THE SKIP THE TICKET ASKS FOR, and it fires on a MEASUREMENT: the probe # tried to allocate a pty and could not, so there is no tty of any kind # here and the forwarding loop has nothing to run against. skip_loop "no pty on this guest: posix_openpt/grantpt/unlockpt failed with $pty_name, and there is no other tty (/dev/ttySER0 present=$ts_present open_rc=$ts_openrc)" ;; measured) c_rc=$(field SFP-connect rc); c_name=$(field SFP-connect name) c_try=$(field SFP-connect attempts) t2p_rc=$(field SFP-tcp2pty rc); t2p_data=$(field SFP-tcp2pty data) t2p_name=$(field SFP-tcp2pty name) p2t_rc=$(field SFP-pty2tcp rc); p2t_data=$(field SFP-pty2tcp data) p2t_name=$(field SFP-pty2tcp name) cn_rc=$(field SFP-canon rc); cn_early=$(field SFP-canon early) cn_data=$(field SFP-canon data) ra_rc=$(field SFP-reaccept rc); ra_data=$(field SFP-reaccept data) ra_name=$(field SFP-reaccept name) pf_rc=$(field SFP-ptyfwd rc); pf_line=$(line_of SFP-ptyfwd) pf_slave=$(field SFP-ptyfwd-slave name) ch_rc=$(field SFP-chain rc); ch_data=$(field SFP-chain data) ch_name=$(field SFP-chain name) reaped=$(field SFP-teardown reaped) printf 'SFP: connect rc=%s(%s) try=%s | tcp2pty rc=%s data=%s | pty2tcp rc=%s data=%s\n' \ "$c_rc" "$c_name" "$c_try" "$t2p_rc" "$t2p_data" "$p2t_rc" "$p2t_data" printf 'SFP: canon rc=%s early=%s data=%s | reaccept rc=%s data=%s | chain rc=%s data=%s | reaped=%s\n' \ "$cn_rc" "$cn_early" "$cn_data" "$ra_rc" "$ra_data" "$ch_rc" "$ch_data" "$reaped" # bind + listen + accept on loopback, from a program that gave no sign # it had got that far. if [ "$c_rc" = 0 ]; then bst_ok serial-forward/loop/connect else bst_notok serial-forward/loop/connect \ "serial_forward never accepted a connection: $c_name after $c_try attempts"; fi # socket -> serial_forward -> the device. c_oflag is 0 in set_termios, # so the bytes must arrive unchanged. if [ "$t2p_rc" = 0 ]; then has serial-forward/loop/tcp2pty 'PING-FROM-TCP' "$t2p_data" else bst_notok serial-forward/loop/tcp2pty "the network side never reached the device: $t2p_name" fi # the device -> serial_forward -> socket. if [ "$p2t_rc" = 0 ]; then has serial-forward/loop/pty2tcp 'PONG-FROM-PTY' "$p2t_data" else bst_notok serial-forward/loop/pty2tcp "the device side never reached the network: $p2t_name" fi # THE ONE THAT PROVES set_termios RAN. ICANON on the slave means a # partial line must not come through; early=1 says the pty is raw and # tcsetattr did nothing, which every case above would still have passed. if [ "$cn_rc" = 0 ] && [ "$cn_early" = 0 ]; then bst_ok serial-forward/loop/canon-early else bst_notok serial-forward/loop/canon-early \ "rc=$cn_rc early=$cn_early -- a partial line came through, so the slave is not in ICANON and set_termios did not take" fi if [ "$cn_rc" = 0 ]; then has serial-forward/loop/canon-line 'HALFLINE' "$cn_data" else bst_notok serial-forward/loop/canon-line "the completed line never arrived" fi # forward_data returns at EOF and main() goes back to accept(). A build # that fell out of the loop passes everything above and fails here. if [ "$ra_rc" = 0 ]; then has serial-forward/loop/reaccept 'SECOND' "$ra_data" else bst_notok serial-forward/loop/reaccept \ "no second connection after the first closed: $ra_name" fi # pty_forward against the live listener: it connects, opens a pty of its # own and announces the slave. if [ "$pf_rc" = 0 ]; then has serial-forward/loop/ptyfwd 'You can now use' "$pf_line" else bst_notok serial-forward/loop/ptyfwd "pty_forward announced nothing: $(_bst_1line "$pf_line")" fi case "$pf_slave" in /dev/pts/*) bst_ok serial-forward/loop/ptyfwd-slave ;; *) bst_notok serial-forward/loop/ptyfwd-slave \ "the announced slave is not a pts node: '$pf_slave'" ;; esac # THE WHOLE POINT OF THE PACKAGE, end to end: a byte written into # pty_forward's pty comes out of serial_forward's pty, through two # processes and one TCP connection. ONLCR is still set on pty_forward's # own pty (openpty's default), so the \n arrives as \r\n -- a fact about # that pty, not about the forwarding, which is why only CHAIN is # asserted. if [ "$ch_rc" = 0 ]; then has serial-forward/loop/chain 'CHAIN' "$ch_data" else bst_notok serial-forward/loop/chain "the chain did not close: $ch_name" fi # THE TEARDOWN IS A PLATFORM MEASUREMENT, NOT A VERDICT ON THIS PORT. # serial_forward's main() has no exit path, so at teardown it is parked # in accept(2) -- and on this arm a parked task cannot be reclaimed: # signal delivery is only observed when a task next crosses a dispatch # boundary (brintos/linux#21), and a task parked in accept never # reaches one. So SIGTERM and SIGKILL are both sent and neither # arrives. reaped=0 is therefore the EXPECTED answer here and is a fact # about the kernel; reaped=1 is what a host with working signal # delivery gives, and what this arm would give once #21 closes. # # The probe never waits on it either way -- a blocking waitpid for a # signal that is not delivered is an unbounded block, which is how # 88-socat.test hung the guest seven times before it stopped doing it. if [ "$reaped" = 1 ]; then bst_ok serial-forward/loop/teardown else bst_skip serial-forward/loop/teardown \ "serial_forward is parked in accept(2) and SIGTERM/SIGKILL were not delivered (reaped=$reaped) -- brintos/linux#21, a platform gap this port does not own; the probe left it parked rather than blocking on it" fi ;; *) # no-spawn / no-connect / anything else: the probe got past the pty and # then could not stand the package up. That is a failure of this port on # this arm, not an absence to skip on. fail_loop "the probe reported state='$loop_state' -- a pty was available but the forwarding loop could not be stood up: $(_bst_1line "$PO")" ;; esac fi