# BST-TIMEOUT: 240 # 32 cases; 29 of them green on the native control run # below, the three reds being true statements about that HOST rather than about # sngrep. Ten cases start a capture, each bounded at 12s -- eight through # bst_probe and two through the local sgrun -- so the worst case this file can # cost is 120s of watchdog plus the fixture, inside the 240s bound with room for # a loaded host. The bound is not decoration: sngrep reaches pthread_create # before it reads anything (see THE CAPTURE THREAD below), and an unbounded call # to a binary that parked there would wedge the guest for every OTHER suite in # the run. Measured on target it does not park -- it takes the error return and # exits -- so the file cost 23s of its 240s bound. The bound stays: it is # insurance against the other mode of the same gap, not against the one measured. # # sngrep 1.8.2 (meta-networking recipes-support/sngrep, with a meta-brintos # bbappend carrying two lines and no patch) -- the SIP call-flow viewer. One # program, /usr/bin/sngrep, plus the conffile /etc/sngreprc. # # WHAT THIS SUITE ESTABLISHES. sngrep is an ncurses application, and its # interactive surface is not reachable from a self-test. What IS reachable, and # what the ticket's pass criterion is about, is its OFFLINE arm: `-I ` to # read a capture, `-N` to skip the interface, `--text` to write the call flow and # the messages out as text, and `-O` to write a capture back. Those are driven # exhaustively below over a fixture this suite builds itself. # # `-N' DOES NOT PRINT THE CALL FLOW, AND THAT IS WORTH SAYING PLAINLY because the # ticket's wording implies it does. main.c's no-interface branch prints exactly # one thing, on a carriage return, twice a second: # # printf("\rDialog count: %d", sip_calls_count_unrotated()); # # The messages themselves reach a file only through `--text', whose writer is # save_msg_txt() (ui_save.c:581) and whose format is one # "DATE TIME SRC -> DST" line followed by the whole payload. So the pass # criterion splits in two: -N carries the DIALOG count, --text carries the # METHODS, their ORDER and the Call-IDs. Both are asserted. # # USE THE LONG `--text', NEVER `-T'. The option table declares text as # required_argument, but the short-option string is "...ERf:FT" -- no colon after # T (main.c:177). So `-T file' returns 'T' with optarg NULL, main.c:233 stores a # null text_outfile, and the run silently writes nothing. Upstream defect in # 1.8.2, not ours and not a brintOS gap; nothing to reclaim # (docs/conventions/no-public-upstream-reports.md). # # THE CAPTURE THREAD IS THE ONE PLATFORM QUESTION THIS PORT TURNS ON, and the # batch verify of 2026-09-01 answered it: it is refused. sngrep does not read a # capture on the main thread. capture_launch_thread() (capture.c:1042) calls # pthread_create unconditionally, for the offline reader as much as for a live # device, and main.c:451 treats a non-zero return as fatal: # # Failed to launch capture thread. # rc=1 # # That is linux-in-the-browser#85 -- this arm refuses pthread_create outright -- # in the same clean-error-return mode 232-tiobench and 209-nanomsg carry, not the # trapping mode 76-asio and 62-ndpi carry. sngrep checks the return, tears down # ncurses and exits, so it never reaches the park protocol and the guest stays # alive for the rest of the run. # # MEASURED on core-image-brintos-emcraft, batch-1216 round 4: 13 ok, 19 not ok, # 0 skip, 23s. The reds are exactly the cases that reach line 451 -- eight "-run" # arms carrying `rc=1 Failed to launch capture thread.' and the ten assertions # below them reading empty output as a consequence -- plus sngrep/live-capture, # which is a SECOND and independent gap (brintos/linux#81, no AF_PACKET, # established here by sngrep/probe-no-af-packet rather than inferred). The 13 # greens are the argv, fixture and error-path cases that return before it. # # THAT SHAPE IS THE DIAGNOSTIC and it is why these are reds rather than skips: a # single offline case reddening ON ITS OWN would be a SIP-parsing fault and the # opposite conclusion. All 19 flip green together the day #85 lands, which is the # regression signal this suite was bought for # (docs/conventions/ticket-scoping-and-gates.md). # # sngrep/output-roundtrip is worth reading rather than skimming: it reports # in=2481 out=24. 24 bytes is the pcap global header alone, written by dump_open() # at main.c:407 -- BEFORE the thread launch at :451 -- so the file is created and # then never fed. It is the same one cause, not a writer defect. # # EVERY PINNED VALUE WAS MEASURED ON A NATIVE x86-64 BUILD of sngrep 1.8.2 -- # the same git SRCREV, configured with the same feature set the bbappend leaves # (-DWITH_UNICODE=ON -DUSE_IPV6=ON, and OPENSSL/GNUTLS/PCRE/ZLIB/EEP all OFF) -- # against libpcap, driven by this same shell, on 2026-09-01, before the target # ever ran it. The savefile format is fixed, the frames are wire format and the # parsing is byte-oriented, so a difference between the two architectures is a # real defect rather than expected variation. # # THE `-O' ROUND TRIP IS ASSERTED AS BYTE EQUALITY, not as an equal dialog count. # Measured natively: sngrep -N -I fix.pcap -O out.pcap reproduces the input # file exactly, all 2481 bytes. A count-only assertion would pass on a writer # that dropped every header it did not understand. . "$BST_LIB" require sngrep sngrep/present TMP=${TMPDIR:-/tmp} WORK=$TMP/bst-sngrep.$$ mkdir -p "$WORK" 2>/dev/null if [ ! -d "$WORK" ]; then bst_notok sngrep/workdir "cannot create $WORK -- the fixture needs somewhere writable" exit 0 fi bst_ok sngrep/workdir # --- the argv surface: everything that returns before capture_launch_thread --- # main.c handles -h at line 180, -V just below it and -D at line 282, each with # its own `return 0'. None of the three reaches line 451, so these cases are # about the BINARY -- that it links, starts and parses its own options -- and are # independent of whatever the capture thread does. check_out sngrep/version "sngrep - 1.8.2" sngrep -V check_out sngrep/unicode " * Compiled with Wide-character support." sngrep -V check_out sngrep/ipv6 " * Compiled with IPv6 support." sngrep -V check_out sngrep/usage "-N --no-interface" sngrep -h # The bbappend drops openssl because the coro pass refuses # bn_mod_exp_mont_fixed_top on this arm. version() prints one line per crypto # backend it was compiled with, so their ABSENCE is the readable evidence that # the PACKAGECONFIG:remove took -- and a red here means the image grew a crypto # arm that cannot work, which is worth catching at once. crypto=$(sngrep -V 2>&1 | grep -c 'Compiled with OpenSSL\|Compiled with GnuTLS') check_eq sngrep/no-openssl "0" "$crypto" # -D dumps the key bindings and then every setting. 127 lines natively; the # assertion is a floor rather than the exact count, because a later meta-brintos # change to /etc/sngreprc may legitimately add settings and must not turn this # red. Zero lines is the failure it is here to catch. dumped=$(sngrep -F -D 2>/dev/null | wc -l | tr -d ' ') if [ "${dumped:-0}" -ge 100 ]; then bst_ok sngrep/dump-config else bst_notok sngrep/dump-config "sngrep -D printed ${dumped:-0} lines, want >= 100"; fi # The conffile is packaged (/etc/sngreprc), so assert it is on the image and # parseable rather than assuming the package installed it. if [ -s /etc/sngreprc ]; then bst_ok sngrep/conffile else bst_notok sngrep/conffile "/usr/bin/sngrep is installed but /etc/sngreprc is missing or empty"; fi # --- the platform reading the live arm is scored against ---------------------- # libpcap's Linux capture backend is AF_PACKET, so `sngrep -d ' can only # work where the kernel carries CONFIG_PACKET. That option is what registers # packet_net_ops and creates /proc/net/packet, so its presence is a reading of # the option rather than an inference from a refusal -- the same probe 88-ptpd, # 88-mstpd and 79-libnet take. packet_proc=$([ -e /proc/net/packet ] && echo present || echo absent) check_eq sngrep/probe-no-af-packet "absent" "$packet_proc" # --- the fixture, verified before anything is concluded from it --------------- CAP=$WORK/sip.pcap LC_ALL=C awk -f "$BST_DATA/sngrep-mkcap.awk" > "$CAP" 2>/dev/null sz=$(wc -c < "$CAP" 2>/dev/null | tr -d ' ') check_eq sngrep/fixture-size 2481 "$sz" sum=$(od -An -v -tu1 "$CAP" 2>/dev/null | awk '{for(i=1;i<=NF;i++)s+=$i} END{print s+0}') check_eq sngrep/fixture-sum 157458 "$sum" # --- the offline arm ---------------------------------------------------------- # Every invocation goes through bst_probe: it runs sngrep in the background, # bounds it, kills it on overrun and leaves the transcript for the assertion # that follows. `count ' reads the LAST carriage-return-delimited field, # because -N overwrites one progress line rather than printing a fresh one. count() { tr '\r' '\n' < "$1" 2>/dev/null | tail -n 1; } # sgrun : bst_probe's watchdog WITHOUT its verdict -- # runs the command bounded, leaves stdout at and stderr at .err, and # returns the status (124 on overrun) while emitting no case of its own. # # It exists for the two arms below whose command is EXPECTED to exit non-zero. # bst_probe scores any non-zero rc as a red, so using it there would report a # correct refusal as a failure -- which it did, on the native control run this # suite's values were measured from. The bound is still wanted: `no live capture # on this kernel' and `no such file' are both refusals sngrep makes BEFORE # capture_launch_thread, so neither should reach a thread, but that is a reading # of 1.8.2's control flow rather than something this phase could measure, and a # suite that wedges the guest on a wrong reading costs every other suite in the # run. sgrun() { _sg_out=$1; _sg_bound=$2; shift 2 _sg_rcf=$_sg_out.rc rm -f "$_sg_out" "$_sg_out.err" "$_sg_rcf" { "$@" >"$_sg_out" 2>"$_sg_out.err"; echo $? >"$_sg_rcf"; } & _sg_pid=$! _sg_t0=; if [ -r /proc/uptime ] && read -r _sg_u _ < /proc/uptime; then _sg_t0=${_sg_u%.*}; fi _sg_ticks=0 while [ ! -e "$_sg_rcf" ]; do _sg_d=$_sg_ticks if [ -n "$_sg_t0" ] && read -r _sg_u _ < /proc/uptime; then _sg_d=$(( ${_sg_u%.*} - _sg_t0 )); fi if [ "$_sg_d" -ge "$_sg_bound" ]; then kill -9 "$_sg_pid" 2>/dev/null; return 124; fi sleep 1 _sg_ticks=$((_sg_ticks + 1)) done # `$(cat)' rather than bst_probe's `read < "$_sg_rcf"': the file is under # $TMPDIR and a redirect from there is safe, but a top-level stdin redirect is # what check-suite-redirects.py refuses and what ends a suite silently when the # path turns out to be the virtiofs root (brintos/linux#73). One exec, once per # bounded run, buys the whole question away. _sg_rc=$(cat "$_sg_rcf" 2>/dev/null) case "$_sg_rc" in ''|*[!0-9]*) _sg_rc=0 ;; esac rm -f "$_sg_rcf" return "$_sg_rc" } O=$WORK/o bst_probe sngrep/offline-run "$O" 12 sngrep -N -F -I "$CAP" check_eq sngrep/offline-dialog-count "Dialog count: 2" "$(count "$O")" # -c keeps only dialogs that start with INVITE. The fixture's second dialog is a # REGISTER precisely so this answer differs from the one above. bst_probe sngrep/only-calls-run "$O" 12 sngrep -N -F -c -I "$CAP" check_eq sngrep/only-calls "Dialog count: 1" "$(count "$O")" # A positional argument is the MATCH EXPRESSION (a payload substring), not a BPF # filter -- the BPF filter is the second positional. Case-sensitive by default, # which is what the -i case next door establishes independently. bst_probe sngrep/match-run "$O" 12 sngrep -N -F -I "$CAP" REGISTER check_eq sngrep/match-expression "Dialog count: 1" "$(count "$O")" bst_probe sngrep/icase-run "$O" 12 sngrep -N -F -i -I "$CAP" register check_eq sngrep/match-icase "Dialog count: 1" "$(count "$O")" bst_probe sngrep/limit-run "$O" 12 sngrep -N -F -l 1 -I "$CAP" check_eq sngrep/limit "Dialog count: 1" "$(count "$O")" # Without -F, sngrep reads the packaged /etc/sngreprc. Same answer, measured: # this case is what would catch a conffile edit that changed capture behaviour. bst_probe sngrep/with-conffile-run "$O" 12 sngrep -N -I "$CAP" check_eq sngrep/with-conffile "Dialog count: 2" "$(count "$O")" # --- the call flow itself, through --text ------------------------------------ TXT=$WORK/flow.txt bst_probe sngrep/text-run "$WORK/t.out" 12 sngrep -N -F -I "$CAP" --text "$TXT" # The header line of each message is "DATE TIME SRC -> DST"; the request or # status line is the line after it. Reading them in file order is what makes # this an assertion about ORDER and not merely about the set of methods. flow=$(awk '/ -> /{getline l; sub(/\r$/, "", l); print l}' "$TXT" 2>/dev/null | tr '\n' '|') want='INVITE sip:bob@10.0.0.2 SIP/2.0|SIP/2.0 100 Trying|SIP/2.0 180 Ringing|SIP/2.0 200 OK|ACK sip:bob@10.0.0.2 SIP/2.0|BYE sip:bob@10.0.0.2 SIP/2.0|SIP/2.0 200 OK|REGISTER sip:10.0.0.2 SIP/2.0|SIP/2.0 200 OK|' check_eq sngrep/text-call-flow "$want" "$flow" # The two Call-IDs, counted separately: 7 messages in the INVITE dialog and 2 in # the REGISTER one. A reader that attributed a message to the wrong dialog would # still satisfy the order assertion above but not this pair. na=$(grep -c 'Call-ID: bst-sngrep-invite-1@10.0.0.1' "$TXT" 2>/dev/null) nb=$(grep -c 'Call-ID: bst-sngrep-register-2@10.0.0.1' "$TXT" 2>/dev/null) check_eq sngrep/text-callid-invite 7 "${na:-0}" check_eq sngrep/text-callid-register 2 "${nb:-0}" # --- the -O round trip -------------------------------------------------------- RT=$WORK/out.pcap bst_probe sngrep/output-run "$O" 12 sngrep -N -F -I "$CAP" -O "$RT" if cmp -s "$CAP" "$RT" 2>/dev/null; then bst_ok sngrep/output-roundtrip else bst_notok sngrep/output-roundtrip \ "sngrep -O did not reproduce the input capture: in=$(wc -c < "$CAP" 2>/dev/null | tr -d ' ') out=$(wc -c < "$RT" 2>/dev/null | tr -d ' ') bytes"; fi # --- an input that is not there ---------------------------------------------- # The one offline path that does NOT reach the capture thread on a failure: # capture_offline() refuses in main's setup and returns 1. So this case is # expected to stay green even under linux-in-the-browser#85, and it pins the # refusal as a clean diagnostic rather than a crash. sgrun "$WORK/m" 12 sngrep -N -F -I "$WORK/nosuch.pcap"; mrc=$? merr=$(cat "$WORK/m.err" 2>/dev/null) check_eq sngrep/missing-input-rc 1 "$mrc" case "$merr" in *"Couldn't open pcap file"*) bst_ok sngrep/missing-input ;; *) bst_notok sngrep/missing-input "want a \"Couldn't open pcap file\" diagnostic, got:'$(_bst_1line "$merr")'" ;; esac # --- the live capture arm ----------------------------------------------------- # Attempted once, as an ATTRIBUTED RED rather than a skip. A skip would retire # the question; the red keeps it visible and flips green by itself the day this # kernel grows a packet socket. Scored on sngrep's own DIAGNOSTIC rather than on # its status, because the status alone cannot tell a refusal apart from a # successful capture of nothing: measured natively, an unprivileged # `sngrep -N -F -q -d lo' exits 1 after printing # `Couldn't activate capture: Attempt to create packet socket failed'. sgrun "$WORK/l" 12 sngrep -N -F -q -d lo lerr=$(cat "$WORK/l.err" 2>/dev/null) if [ "$packet_proc" = "absent" ]; then bst_notok sngrep/live-capture \ "no live capture on this kernel: it is built '# CONFIG_PACKET is not set' (arch/wasm32/configs/wasm32_defconfig), so libpcap's AF_PACKET backend cannot open lo -- brintos/linux#81 (https://brintos.io/brintos/linux/issues/81), established by sngrep/probe-no-af-packet. sngrep said: $(_bst_1line "$lerr")" else case "$lerr" in "") bst_ok sngrep/live-capture ;; *) bst_notok sngrep/live-capture "this kernel HAS AF_PACKET, so the refusal is not brintos/linux#81 and needs re-reading -- $(_bst_1line "$lerr")" ;; esac fi rm -rf "$WORK" 2>/dev/null