. "$BST_LIB" # BST-TIMEOUT: 300 # MEASURED on target, batch-1527 image, 2026-08-25: the phase # clock below reaches the ssmpingd section at +27s having # emitted 56 of the 71 cases. 300 is ~10x that, and it is NOT # the guard against the ssmpingd wedge -- the fence canary at # that section is, because a bound cannot buy back the cases # after a hang. The earlier 300 was derived from a 5 s native # x86-64 run and happened to be right for the wrong reason. # # The floor is what this bound is really set over, because it # does not shrink on a faster machine: the daemon section # sleeps 2 s after starting ssmpingd and 1 s before its kill, # the no-usage case holds a 3 s fence to its end by design, and # the probe's query mode blocks up to 3 s on SO_RCVTIMEO when # nothing answers. Everything else is argv arithmetic and # getaddrinfo(3) on names that do not resolve. # # 300 leaves room for the fences to fire rather than for the # work to grow: four transport cases carry an 8 s fence each # and the second-bind case a 5 s one, so the worst honest run # -- every fence expiring, which means every one of those cases # is RED -- is ~45 s of fence plus the floor. A guest 4x slower # than the native build still lands well inside it. Re-derive # this from a real ==BST-SUITE ssmping elapsed=..== line once # one exists; until then it is deliberately generous and # labelled as such. # # Nothing here waits on a NETWORK: every address is 127.0.0.1, # a group on the local host, or an RFC 2606 .invalid name that # must not resolve. # ssmping -- a multicast reachability tester on a kernel built without IP # multicast. # # WHAT THIS MACHINE CAN AND CANNOT TEST. The package is four programs: ssmping # (source-specific/SSM ping), asmping (any-source/ASM ping), mcfirst # (time-to-first-packet on a group) and ssmpingd (the server the first two # ping). The three clients all join a multicast group BEFORE they send anything # -- joinchannel()/joingroup() precede doit() in ssmping.c and asmping.c -- so # on a kernel with no multicast they stop at the join and never reach the wire. # # arch/wasm32/configs/wasm32_defconfig names no CONFIG_IP_MULTICAST at all, so # it takes the Kconfig default, and `config IP_MULTICAST` in net/ipv4/Kconfig # is a bare bool with no `default y`. But that is a DERIVED reading of two # files, and this suite asserts none of it: igmp.o is in net/ipv4/Makefile's # unconditional object list on this kernel, so the multicast sockopt entry # points are compiled in whatever the config says, and what a join actually # returns here is an open question. Every branch below is written against the # LIVE answer from ${LIBEXEC}/ssmping-tests/ssmping-probe, so this suite stays # correct the day the kernel grows multicast: the skips turn back into # assertions on their own evidence rather than on a comment. # # AND THE CONTROLS ARE THE POINT OF THE PROBE. "The join failed" is a statement # about the kernel only while this process can open ordinary sockets and move a # datagram. The probe therefore opens AF_INET stream, AF_INET datagram and # AF_UNIX stream and completes a UDP loopback round-trip in the same run, and # every skip below is gated on all four. If they stop succeeding, this suite # reds rather than quietly skipping -- which is the failure mode a suite full of # environmental skips is otherwise built to have. # # TWO REFUSALS, NOT ONE. A multicast sockopt can fail because the kernel has no # such option (ENOPROTOOPT -- that is CONFIG_IP_MULTICAST), or because it has # one and this image has no multicast-capable interface (ENODEV/EADDRNOTAVAIL), # which would hold on a STOCK kernel whose only interface is `lo`, since lo # carries no IFF_MULTICAST. The probe reports the sockopt errno beside an # IFF_MULTICAST interface census and the cases below distinguish them, because # calling the second the first would be a false statement about the kernel. # # WHAT IS THEREFORE ASSERTED FOR REAL, AND IT IS MOST OF THE PACKAGE. Everything # the tools do before they reach a multicast call: getopt parsing for all three # clients, the usage text each prints, the version line above it, the four # argument-validation diagnostics, the three address-class rules, and the # documented exit status for an unresolvable target. That is arithmetic on argv # and strings plus a getaddrinfo(3) call; it is the same on every architecture, # so a wasm32 build that got it wrong has no environmental excuse. # # AND THE PING EXCHANGE IS PROBED, NOT SKIPPED. ssmpingd answers every request # TWICE -- once unicast to the sender, once to the group (respond() in # ssmpingd.c) -- and only the second needs multicast. So the exchange's unicast # half is fully exercisable here, and the probe's `query` mode speaks the wire # protocol to a live ssmpingd and reads back the server version string. The # clients cannot reach that exchange on this kernel (they join first), which is # precisely why the probe does it instead of being told the answer. # # ONE CORRECTION TO THE TICKET'S PREMISE, established by reading ssmpingd.c and # confirmed by running it: SSMPINGD HAS NO USAGE OR VERSION TEXT. It calls no # getopt and inspects argv nowhere; `ssmpingd -h --version bogus` ignores all # three and serves. There is no usage string in the program to assert. What it # is asserted on instead is what it does -- bind, serve, refuse a second bind -- # and the case below pins the ABSENCE of an option surface so that the day # upstream grows one, this reddens rather than passing on a stale expectation. # # WHY NOT A NATIVE ORACLE. The invariant text here (usage, version, the # diagnostics) was read off a native build of ssmping-0.9.1 during the port # rather than transcribed from the source, but it is asserted directly rather # than diffed against a stored capture: every other line these tools emit is a # function of the ENVIRONMENT -- which addresses resolve, which interfaces # exist, what the resolver's errno was -- so an oracle would differ from a # correct target run for reasons that are not defects. # Phase clock. The 300 s bound this suite shipped with was derived from a # native run and expired on target at case 56 of 71, so the boundaries are # stamped: a future bound is re-derived from these lines instead of guessed. _T0=$(date +%s 2>/dev/null || echo 0) mark() { _mn=$(date +%s 2>/dev/null || echo 0); printf '#diag: ssmping phase %s +%ss\n' "$1" "$((_mn - _T0))"; } bst_ok ssmping/suite-canary # first line: absorbs brintos/linux#96, which drops it require ssmping ssmping/present # --- the four commands upstream's Makefile installs --------------------------- for t in ssmping asmping ssmpingd mcfirst; do if command -v "$t" >/dev/null 2>&1; then bst_ok "ssmping/present/$t" else bst_notok "ssmping/present/$t" "not installed -- upstream's Makefile builds and installs four commands" fi done mark probe # --- the probe --------------------------------------------------------------- P=/usr/libexec/ssmping-tests/ssmping-probe if [ ! -x "$P" ]; then bst_notok ssmping/probe/present "no probe at $P -- ssmping-tests is not installed, so nothing below can be attributed" exit 0 fi PO=$("$P" 2>&1); prc=$? if bst_execfail "$PO"; then bst_unrun ssmping/probe/runs "$PO" exit 0 fi case "$PO" in *"probe end"*) bst_ok ssmping/probe/runs ;; *) bst_notok ssmping/probe/runs "rc=$prc, no 'probe end' -- the probe did not finish, so its values below are partial: $(_bst_1line "$PO")" exit 0 ;; esac # field -- the value of on the probe's # line. 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 } # state -- the third word of a ` present|absent` line. state() { printf '%s\n' "$PO" | sed -n "s/^$1 [^ ]* \\([a-z]*\\)$/\\1/p" | sed -n 1p } # ename -- the errno NAME on a report() line. ename() { field "$1" name; } # estr -- the strerror(3) text, which runs to end of line. estr() { printf '%s\n' "$PO" | sed -n "s/^$1 .*strerror=//p" | sed -n 1p; } inet_s=$(field inet-stream rc) inet_d=$(field inet-dgram rc) unix_s=$(field unix-stream rc) loop_rc=$(field udp-loopback rc) loop_n=$(field udp-loopback-bytes n) ssm_rc=$(field ssm-join rc); ssm_name=$(ename ssm-join) ssml_rc=$(field ssm-join-legacy rc); ssml_name=$(ename ssm-join-legacy) asm_rc=$(field asm-join rc); asm_name=$(ename asm-join) asml_rc=$(field asm-join-legacy rc); asml_name=$(ename asm-join-legacy) ttl_rc=$(field mcast-ttl rc); ttl_name=$(ename mcast-ttl) ttl_val=$(field mcast-ttl-value got) loopopt_rc=$(field mcast-loop rc) bindmc_rc=$(field bind-mcast rc); bindmc_name=$(ename bind-mcast) inet6_rc=$(field inet6-dgram rc); inet6_name=$(ename inet6-dgram) reuse_rc=$(field opt-reuseaddr rc) ts_rc=$(field opt-so-timestamp rc); ts_name=$(ename opt-so-timestamp) recvttl_rc=$(field opt-ip-recvttl rc); recvttl_name=$(ename opt-ip-recvttl) ipttl_rc=$(field opt-ip-ttl rc) pktinfo_rc=$(field opt-ip-pktinfo rc); pktinfo_name=$(ename opt-ip-pktinfo) if_all=$(field ifcensus all) if_mc=$(field ifcensus mc) procudp=$(state proc-net-udp) procigmp=$(state proc-net-igmp) # Echo what the probe answered, one line at a time, prefixed so it is greppable # and plainly not a result line. Costs nothing and saves the next reader of a # stored run a boot: every branch below is a function of these values, and # without them a stored dump records the verdicts without the evidence. printf '%s\n' "$PO" | while read -r _dl; do [ -n "$_dl" ] && printf '#diag: ssmping probe: %s\n' "$_dl" done mark controls # --- the controls ------------------------------------------------------------ # # Four things this kernel DOES do. They are asserted, not recorded: while they # pass, a join refusal is a fact about multicast; if they ever fail, it is a # fact about the process and nothing below may be attributed to the kernel's # multicast support. check_eq ssmping/control/inet-stream 0 "$inet_s" check_eq ssmping/control/inet-dgram 0 "$inet_d" check_eq ssmping/control/unix-stream 0 "$unix_s" check_eq ssmping/control/udp-loopback 0 "$loop_rc" check_eq ssmping/control/udp-loopback-bytes 3 "$loop_n" controls_ok=no if [ "$inet_s" = 0 ] && [ "$inet_d" = 0 ] && [ "$unix_s" = 0 ] && [ "$loop_rc" = 0 ]; then controls_ok=yes fi mark prep # --- prep_sock's four options ------------------------------------------------ # # ssmping and asmping call prep_sock() on BOTH their sockets before any # multicast call, and prep_sock uses errx() -- which exits 1 -- on every one of # these. So any failure here stops all three clients before multicast is # reached, and attributing that exit to multicast would be wrong. Asserted # rather than recorded: these are ordinary INET options with no multicast in # them, and a kernel with CONFIG_INET=y that refuses one has a finding. check_eq ssmping/prep/so-reuseaddr 0 "$reuse_rc" check_eq ssmping/prep/so-timestamp 0 "$ts_rc" check_eq ssmping/prep/ip-recvttl 0 "$recvttl_rc" check_eq ssmping/prep/ip-ttl 0 "$ipttl_rc" prep_ok=no if [ "$reuse_rc" = 0 ] && [ "$ts_rc" = 0 ] && [ "$recvttl_rc" = 0 ] && [ "$ipttl_rc" = 0 ]; then prep_ok=yes fi # ssmpingd's own hard requirement, and the one failure in this package that is # fatal without saying so: main() does `if (setpktinfo(...) == -1) { err(...); # continue; }`, and that `continue` skips the bind, so the socket never reaches # s4/s6 and the daemon ends at "both v4 and v6 binds failed". Asserted, so that # a daemon which would not start for want of IP_PKTINFO is never reported below # as a multicast finding. check_eq ssmping/prep/ip-pktinfo 0 "$pktinfo_rc" mark mcast # --- the multicast measurements ---------------------------------------------- # # Recorded as values with a definite answer required, rather than asserted in # one direction: both outcomes are legitimate results, and a suite that demanded # the absence would red on the good day. # # `answer` classifies each into one of three, and the three are different # findings: implemented and working; implemented but refused for want of a # multicast-capable interface; or not implemented at all. classify() { # classify case "$2:$3" in 0:*) bst_ok "$1" ;; # the kernel does this -1:ENOPROTOOPT) bst_ok "$1" ;; # not implemented -- a clean answer -1:ENODEV|-1:EADDRNOTAVAIL) bst_ok "$1" ;; # implemented, no interface -1:*) bst_notok "$1" "refused with $3, which is neither success, nor 'the option is not implemented' (ENOPROTOOPT), nor 'no multicast interface' (ENODEV/EADDRNOTAVAIL) -- the tools' failures below cannot be attributed to the kernel's multicast support on this evidence" ;; *) bst_notok "$1" "the probe printed no usable line (rc='$2' name='$3')" ;; esac } classify ssmping/mcast/ssm-join "$ssm_rc" "$ssm_name" classify ssmping/mcast/ssm-join-legacy "$ssml_rc" "$ssml_name" classify ssmping/mcast/asm-join "$asm_rc" "$asm_name" classify ssmping/mcast/asm-join-legacy "$asml_rc" "$asml_name" classify ssmping/mcast/ttl-option "$ttl_rc" "$ttl_name" classify ssmping/mcast/bind-group "$bindmc_rc" "$bindmc_name" # IP_MULTICAST_TTL is the cleanest reading in the probe -- it needs no group and # no interface, so it separates "this kernel has no multicast option surface" # from "this image has no interface to join on" with nothing else mixed in. # Recorded as the single fact the rest of the file names. mcast=unknown case "$ttl_rc:$ttl_name" in 0:*) mcast=present ;; -1:ENOPROTOOPT) mcast=absent ;; esac # The other half of the same question, and it is NOT interchangeable with the # one above: a kernel that implements multicast still cannot join on an image # whose only interface is `lo`. mcif=unknown if [ "${if_mc:-x}" != x ]; then if [ "${if_mc:-0}" -gt 0 ] 2>/dev/null; then mcif=yes; else mcif=no; fi fi case "$mcif" in yes|no) bst_ok ssmping/mcast/interface-census ;; *) bst_notok ssmping/mcast/interface-census "the probe printed no usable ifcensus line" ;; esac # When the multicast option surface is present, IP_MULTICAST_TTL must read back # what was written -- that is the one multicast value in the package with a # fixed correct answer, and ssmpingd sets it on every reply socket. if [ "$mcast" = present ]; then check_eq ssmping/mcast/ttl-roundtrip 64 "$ttl_val" else bst_skip ssmping/mcast/ttl-roundtrip "IP_MULTICAST_TTL is not implemented on this kernel ($ttl_name), so there is no value to read back" fi mark version-usage # --- version and usage text -------------------------------------------------- # # None of this reaches a socket, so it runs on its merits on any kernel. All # three clients print ` version `, a blank line, then `Usage:` and the # option line, ON STDERR, and exit 1 -- there is no success exit anywhere in # this package's argument handling. Note -v is NOT a version flag: it sets the # "ask the server for its version" bit (parseargs in ssmpngcl.c), and `ssmping # -v` with no target falls through to the same usage as no arguments at all. apr() { # apr _aid=$1; _awant=$2; _aneedle=$3; shift 3 _aout=$("$@" 2>&1); _arc=$? case "$_aout" in *"$_aneedle"*) _ahit=1 ;; *) _ahit=0 ;; esac if bst_execfail "$_aout"; then bst_unrun "$_aid" "$_aout" elif [ "$_arc" -eq "$_awant" ] && [ "$_ahit" -eq 1 ]; then bst_ok "$_aid" else bst_notok "$_aid" "rc=$_arc want=$_awant want:'$_aneedle' got:'$(_bst_1line "$_aout")'"; fi } # Is the fence available at all? Everything that can reach ssmping's ping loop # is gated on this, and skips rather than running unfenced. if command -v timeout >/dev/null 2>&1; then HAVE_TIMEOUT=yes; else HAVE_TIMEOUT=no; fi # aprt -- apr with a wall-clock fence. # # THIS IS NOT TIDINESS; IT IS THE DIFFERENCE BETWEEN A RED AND A WEDGED GUEST. # ssmping, asmping and mcfirst all run UNTIL INTERRUPTED when no -c is given: # doit() in ssmpngcl.c loops on `!count || seq < count`, and mcfirst's select(2) # has a NULL timeout when -t is absent. On this kernel they never get that far, # because they join a group first and the join is refused -- which is exactly # why the unfenced form looks safe here and is not. Every case below runs the # tool precisely to observe whether that refusal happens, so the run in which it # does NOT is the run these cases exist to catch, and unfenced that run hangs # the suite until the harness kills the machine. Measured during the port: with # the probe reporting a refusal and the kernel actually joining, `ssmping # 127.0.0.1` pinged until it was killed by hand. # # A fence kill is therefore its own verdict, not a timeout to retry: it means # the tool did not refuse. The status is accepted in any of its forms for the # reason spelled out at ssmping/daemon/no-usage -- GNU timeout reports 124 under # bash and 143 under busybox ash for the identical kill. aprt() { _tid=$1; _twant=$2; _tneedle=$3; shift 3 if [ "$HAVE_TIMEOUT" != yes ]; then bst_skip "$_tid" "no timeout(1) on this image, and this case runs a client that pings until interrupted whenever its join succeeds -- running it unfenced could wedge the guest, so it is not attempted" return fi # BST-TIMEOUT-OK: this suite carries its own signal FENCE and canaries it with # `timeout -s KILL' deliberately -- the canary exists to detect the very defect # BRINTOS-1645 is about, so replacing it would erase the measurement. _tout=$(timeout 8 "$@" 2>&1); _trc=$? case "$_tout" in *"$_tneedle"*) _thit=1 ;; *) _thit=0 ;; esac if bst_execfail "$_tout"; then bst_unrun "$_tid" "$_tout"; return; fi case "$_trc" in 124|143|137) bst_notok "$_tid" "the fence killed it after 8s: this case expects '$_tneedle' and an immediate exit, so a client still running means it did NOT refuse -- it joined and entered its ping loop, which contradicts the probe's own reading. got:'$(_bst_1line "$_tout")'" return ;; esac if [ "$_trc" -eq "$_twant" ] && [ "$_thit" -eq 1 ]; then bst_ok "$_tid" else bst_notok "$_tid" "rc=$_trc want=$_twant want:'$_tneedle' got:'$(_bst_1line "$_tout")'"; fi } # The version this recipe built, asserted against the recipe's PV rather than # against whatever the binary happens to say. apr ssmping/version/ssmping 1 'ssmping version 0.9.1 (20080418)' ssmping apr ssmping/version/asmping 1 'asmping version 0.9.1 (20080418)' asmping apr ssmping/version/mcfirst 1 'mcfirst version 0.9.1 (20080418)' mcfirst # The usage line each composes. Each is a different string built from a # different arm of parseargs' switch, so getting two of them crossed is exactly # what a "the tool printed something" assertion cannot see. apr ssmping/usage/ssmping 1 'ssmping [ -46v ] [ -I interface ] [ -c count ] [ -s size ] destination' ssmping apr ssmping/usage/asmping 1 'asmping [ -46v ] [ -I interface ] [ -c count ] [ -s size ] group destination' asmping apr ssmping/usage/asmping-alt 1 'asmping [ -46v ] [ -I interface ] [ -c count ] [ -s size ] destination group' asmping apr ssmping/usage/mcfirst 1 'mcfirst [ -46vr ] [ -I interface ] [ -c count ] [ -t time ] [source] group port' mcfirst apr ssmping/usage/mcfirst-alt 1 'mcfirst [ -46vr ] [ -I interface ] [ -c count ] [ -t time ] group [source] port' mcfirst # -I appears in all three because this is the non-WIN32 arm of the #ifdef; a # build that took the WIN32 arm would drop it and pass every other case here. apr ssmping/usage/mcfirst-r-flag 1 '[ -46vr ]' mcfirst mark argv-count # --- argument count validation ----------------------------------------------- # # Each mode requires a different number of positionals: SSMMODE exactly one, # ASMMODE exactly two, FIRSTMODE two or three. Asserted at the boundaries. apr ssmping/argv/ssmping-2args 1 'Usage:' ssmping a b apr ssmping/argv/asmping-1arg 1 'Usage:' asmping onearg apr ssmping/argv/asmping-3args 1 'Usage:' asmping a b c apr ssmping/argv/mcfirst-1arg 1 'Usage:' mcfirst 232.43.211.234 apr ssmping/argv/mcfirst-4args 1 'Usage:' mcfirst a b c d mark argv-opt # --- option validation ------------------------------------------------------- # # Four distinct diagnostics, each from its own arm of parseargs' switch, each # printed BEFORE the usage block. Checked by their own text rather than by # "usage appeared", since usage appears for all of them. apr ssmping/argv/badopt 1 'ssmping' ssmping -Z x apr ssmping/argv/badopt-usage 1 'Usage:' ssmping -Z x # atoi("0") is 0 and atoi("-3") is negative; both take the `*count > 0` arm. apr ssmping/argv/count-zero 1 'Count must be positive' ssmping -c 0 hostname apr ssmping/argv/count-negative 1 'Count must be positive' ssmping -c -3 hostname apr ssmping/argv/size-zero 1 'Invalid size' ssmping -s 0 hostname apr ssmping/argv/time-zero 1 'Invalid time to run' mcfirst -t 0 a b # -I resolves through if_nametoindex(3) and exits 1 WITHOUT printing usage -- # the only arm in parseargs that does, since it exits directly rather than # `goto usage`. Asserted in that direction on purpose. apr ssmping/argv/badif 1 'Unknown interface bst-no-such-if0' ssmping -I bst-no-such-if0 hostname ifout=$(ssmping -I bst-no-such-if0 hostname 2>&1) if bst_execfail "$ifout"; then bst_unrun ssmping/argv/badif-no-usage "$ifout" else case "$ifout" in *Usage:*) bst_notok ssmping/argv/badif-no-usage "an unknown -I exits directly and must NOT print the usage block; got:'$(_bst_1line "$ifout")'" ;; *) bst_ok ssmping/argv/badif-no-usage ;; esac fi # -I IS accepted for an interface that exists, and then the run proceeds to the # next failure rather than this one. Gated on the census so an image with no # interfaces skips rather than reds. if [ "${if_all:-0}" -gt 0 ] 2>/dev/null; then ifname=$(printf '%s\n' "$PO" | sed -n 's/^ifnames //p' | sed -n 1p | cut -d' ' -f1) fi if [ -n "${ifname:-}" ]; then ifok=$(ssmping -I "$ifname" bst-no-such-host.invalid 2>&1) if bst_execfail "$ifok"; then bst_unrun ssmping/argv/goodif "$ifok" else case "$ifok" in *"Unknown interface"*) bst_notok ssmping/argv/goodif "if_nametoindex(3) did not resolve '$ifname', which the probe's own census reports as an interface on this image" ;; *) bst_ok ssmping/argv/goodif ;; esac fi else bst_skip ssmping/argv/goodif "the probe's interface census found no interface to name, so there is no -I argument that could resolve" fi mark addr # --- address-class validation ------------------------------------------------ # # names2addrsocks enforces three rules about which argument may be multicast. # All three are decided by IN_MULTICAST(3) on a resolved address, which is # arithmetic on the first octet -- no socket option, no interface, no kernel # multicast support involved. So these are asserted unconditionally. apr ssmping/addr/group-where-source 1 'Argument must be a multicast address' mcfirst 127.0.0.1 4321 apr ssmping/addr/both-multicast 1 'Both addresses cannot be multicast addresses' asmping 232.43.211.234 232.43.211.235 apr ssmping/addr/both-unicast 1 'Both addresses cannot be unicast addresses' asmping 127.0.0.1 127.0.0.2 # Each of the three is followed by the caller's own errx(), which differs per # tool and is the line a user actually reads. apr ssmping/addr/asmping-failed-socket 1 'Failed to create socket for 232.43.211.234' asmping 232.43.211.234 232.43.211.235 mark resolve # --- the documented exit code for an unresolvable target --------------------- # # getaddrinfo(3) fails, err() reports it, and each tool then takes its own exit # path -- ssmping through errx("name2addrsocks failed"), asmping and mcfirst # through errx("Failed to create socket for ..."). All three exit 1. # # Only the leading text is asserted. errx() appends ": " and perror(3) output # whenever errno is non-zero at that moment, and whether the resolver left errno # set is a libc-internal detail that differs between runs and builds -- measured # during the port, where ssmping printed a trailing ": Invalid argument / # errno=22" and asmping printed nothing at all from the same failure. Asserting # that tail would pin a resolver's internals, not this package's behaviour. BADHOST=bst-no-such-host.invalid apr ssmping/resolve/ssmping-rc 1 'getaddrinfo failed: ' ssmping $BADHOST apr ssmping/resolve/ssmping-errx 1 'name2addrsocks failed' ssmping $BADHOST apr ssmping/resolve/asmping-errx 1 "Failed to create socket for" asmping 232.43.211.234 $BADHOST apr ssmping/resolve/mcfirst-errx 1 "Failed to create socket for" mcfirst $BADHOST 4321 # .invalid is reserved by RFC 2606 and must never resolve; a run in which it DID # would make every case above meaningless, so it is pinned rather than trusted. apr ssmping/resolve/invalid-tld 1 'getaddrinfo failed: ' ssmping $BADHOST mark daemon # --- ssmpingd ---------------------------------------------------------------- # # The daemon has no option surface at all: no getopt call, no argv inspection, # no usage string, no version flag. That is asserted as an ABSENCE, so the day # upstream grows one this reddens rather than passing on a stale expectation. # # Everything in this section is fenced by `timeout`. ssmpingd's normal state is # "blocked in select(2) forever", so an unfenced run would hang the suite, and a # suite that wedges the guest is quarantined and takes its own coverage with it. # Without `timeout` on the image the whole section skips rather than risking it. # THE FENCE IS PROVEN HERE, NOT ASSUMED, AND THAT IS THE WHOLE POINT. Measured # on the batch-1527 image (2026-08-25): the suite reaches this line at +27s with # 56 cases emitted, and then emits nothing for 1800s. Every command below is # already fenced by timeout(1), so a `timeout` that ended its child would have # let the section finish -- it did not. The cause is brintos/linux#21 -- signal # delivery does not work at all, kill(2) reports success and nothing arrives, # SIGKILL included -- so timeout(1)'s SIGTERM never reaches an ssmpingd blocked # in select(2). The runtime says the same at ==BST-BEGIN== ("the region holds pending # signal(s) the kernel does not ... a send is the only thing that may make a # signal pending"). timeout(1) then waits on a child that never dies and the # command substitution around it never sees EOF. # # A wedged suite is not one red: it costs 1800s of a SHARED batch verify and # takes every case after this line with it (15 of 71 here). So the fence is # canaried before it is trusted -- SIGKILL, which is delivered by the kernel # rather than through the signal region, against a `sleep` that outlives its # fence. The canary is itself unfenceable-safe: it cannot block, because # `timeout -s KILL` either returns or the canary reports the fence as broken. if [ "$HAVE_TIMEOUT" = yes ]; then timeout -s KILL 2 sleep 30 >/dev/null 2>&1 & _fcp=$! _fcn=0 # kill -0 and sleep(1), never `wait`: a `wait` on a child the fence cannot # end is the very hang this canary exists to detect. while [ "$_fcn" -lt 8 ] && kill -0 "$_fcp" 2>/dev/null; do sleep 1; _fcn=$((_fcn+1)); done # With brintos/linux#21 open this reports no, SIGKILL being undelivered too. # It flips to yes -- and the five cases below go live -- when that closes. if kill -0 "$_fcp" 2>/dev/null; then FENCE_OK=no; kill -9 "$_fcp" 2>/dev/null else FENCE_OK=yes; fi else FENCE_OK=no fi if [ "$HAVE_TIMEOUT" != yes ]; then for c in no-usage binds serves-version second-bind stays-up; do bst_skip "ssmping/daemon/$c" "no timeout(1) on this image, and ssmpingd blocks in select(2) forever -- running it unfenced could wedge the guest, so this section is not attempted" done elif [ "$FENCE_OK" != yes ]; then for c in no-usage binds serves-version second-bind stays-up; do bst_skip "ssmping/daemon/$c" "the fence canary did not return: timeout(1) exists but did not end a fenced child within its own bound on this image, so a fence here is not a fence -- brintos/linux#21, signal delivery does not work at all, SIGKILL included. ssmpingd blocks in select(2) forever and running it would wedge the suite -- measured on batch-1527, where this section consumed 1800s and cost the 15 cases after it. Re-enable by fixing signal delivery to a blocked process, not by removing this gate" done elif [ "$controls_ok" != yes ] || [ "$prep_ok" != yes ] || [ "$pktinfo_rc" != 0 ]; then # Named separately from a multicast skip on purpose: a daemon that cannot # start for want of IP_PKTINFO or an ordinary socket is not a multicast # finding, and calling it one would be false. for c in no-usage binds serves-version second-bind stays-up; do bst_notok "ssmping/daemon/$c" "the socket controls did not all pass (inet-stream=$inet_s inet-dgram=$inet_d unix-stream=$unix_s udp-loopback=$loop_rc reuseaddr=$reuse_rc so-timestamp=$ts_rc ip-recvttl=$recvttl_rc ip-ttl=$ipttl_rc ip-pktinfo=$pktinfo_rc), so ssmpingd cannot be expected to start and nothing here may be attributed to multicast" done else DLOG=${TMPDIR:-/tmp}/bst-ssmpingd.$$.log # `ssmpingd -h --version bogus`: it takes no options, so it must NOT print a # usage block and must NOT exit -- timeout(1) kills it and returns 124. # Both halves are the assertion: an exit 1 with usage text would mean # upstream grew an option surface. # # THE FENCE'S EXIT STATUS IS NOT PORTABLE, AND THE SHELL IS WHY. GNU # timeout(1) returns 124 when it kills a command -- under bash. Inside # busybox ash's command substitution, which is what runs on this image, the # SAME coreutils timeout against the SAME daemon yields 143 (128+SIGTERM) # and leaks a "Terminated" line to stderr: timeout re-raises the signal to # itself, and the two shells differ in whether that reaches the caller. # Measured both ways during the port (coreutils 9.4). busybox's own # timeout applet is a third answer again. So the status is accepted in any # of its killed-by-the-fence forms and the ASSERTION rests on the output, # which is the part that is actually about ssmpingd. Pinning 124 here # would have reddened this case on every run for a reason that has nothing # to do with the package. dout=$(timeout 3 ssmpingd -h --version bogus 2>&1); drc=$? case "$dout" in *Usage:*|*usage:*) _dusage=yes ;; *) _dusage=no ;; esac if bst_execfail "$dout"; then bst_unrun ssmping/daemon/no-usage "$dout" elif [ "$_dusage" = yes ]; then bst_notok ssmping/daemon/no-usage "ssmpingd printed a usage block: upstream 0.9.1 has no getopt call and inspects argv nowhere, so this image is running a different ssmpingd; got:'$(_bst_1line "$dout")'" else case "$drc" in 124|143|137) # 124 = GNU timeout's own report, 143/137 = killed by the # SIGTERM/SIGKILL it sent. All three mean the same thing here: # ssmpingd was still running when the fence expired, i.e. it # served rather than parsing its arguments. bst_ok ssmping/daemon/no-usage ;; *) bst_notok ssmping/daemon/no-usage "ssmpingd exited on its own with rc=$drc instead of serving until the fence killed it (124/143/137) -- it ignores argv entirely upstream, so three bogus options must not end it; got:'$(_bst_1line "$dout")'" ;; esac fi # Now a real one, fenced at 25 s so it cannot outlive this section even if # every kill below fails. timeout 25 ssmpingd >"$DLOG" 2>&1 & dpid=$! sleep 2 # Bound to UDP/4321. 0x10E1 is 4321; /proc/net/udp is the only observer on # this image that needs no extra package (netstat/ss are not assumed). if [ "$procudp" != present ]; then bst_skip ssmping/daemon/binds "/proc/net/udp is absent on this image, so the bind cannot be observed without assuming netstat(8) or ss(8)" elif grep -qi ':10E1 ' /proc/net/udp 2>/dev/null; then bst_ok ssmping/daemon/binds else bst_notok ssmping/daemon/binds "ssmpingd is running but no UDP socket is bound to port 4321 (0x10E1) in /proc/net/udp -- daemon log:'$(_bst_1line "$(cat "$DLOG" 2>/dev/null)")'" fi # THE PING EXCHANGE, unicast half. This is the one end-to-end case in the # suite: a real request on the wire, ssmpingd's real respond(), and the real # server version string coming back. It needs no multicast -- ssmpingd # answers unicast first and multicast second -- which is exactly why it is # probed here instead of being skipped with the clients. QO=$("$P" query 127.0.0.1 2>&1) qbytes=$(printf '%s\n' "$QO" | sed -n 's/^query-recv-bytes n=//p' | sed -n 1p) qver=$(printf '%s\n' "$QO" | sed -n 's/^query-version-text //p' | sed -n 1p) printf '%s\n' "$QO" | while read -r _ql; do [ -n "$_ql" ] && printf '#diag: ssmping query: %s\n' "$_ql" done if bst_execfail "$QO"; then bst_unrun ssmping/daemon/serves-version "$QO" elif [ -n "$qver" ]; then # SSMPING_SERVERVERSIONSTRING in ssmping.h. The [asm][size] suffix is # the server advertising which optional request types it honours, so # asserting it whole pins the build's capabilities and not just its PV. check_eq ssmping/daemon/serves-version '0.9.1 (20080418) [asm][size]' "$qver" else bst_notok ssmping/daemon/serves-version "no version TLV came back from ssmpingd on 127.0.0.1:4321 (recv bytes=${qbytes:-none}) -- the request was unicast and needs no multicast, so this is the daemon's serving path failing; daemon log:'$(_bst_1line "$(cat "$DLOG" 2>/dev/null)")'" fi # A second daemon must fail the bind and say so. ssmpingd sets no # SO_REUSEADDR on its listening sockets, so this is deterministic -- and it # is the one ssmpingd error path with fixed text, since errx() prints the # message before appending strerror. With no CONFIG_IPV6 the v6 arm never # runs (getaddrinfo returns no v6 entry), so the v4 bind is the only one # that fails and the summary line still names both. apr ssmping/daemon/second-bind 1 'both v4 and v6 binds failed' timeout 5 ssmpingd # Still serving after all of that -- i.e. the requests above did not kill # it. respond() calls err() (which does NOT exit) when its multicast send # fails, so a kernel with no multicast must leave the daemon up. if kill -0 "$dpid" 2>/dev/null; then bst_ok ssmping/daemon/stays-up else bst_notok ssmping/daemon/stays-up "ssmpingd exited while being queried -- respond()'s multicast sendmsg failure goes through err(), which does not exit, so the daemon must survive a kernel with no multicast; log:'$(_bst_1line "$(cat "$DLOG" 2>/dev/null)")'" fi kill "$dpid" 2>/dev/null sleep 1 kill -9 "$dpid" 2>/dev/null rm -f "$DLOG" 2>/dev/null fi mark transport # --- the client transport ---------------------------------------------------- # # All three clients stop at the same place on a kernel without multicast, but # WHICH place depends on two probe answers that fail independently: ssmping # binds its multicast socket to the GROUP address (bind(ms, grpaddr) in # ssmping.c) BEFORE it joins, so a bind refusal ends it at errx("bind # [multicast]") and a join refusal ends it at errx("Failed to join multicast # channel"). The suite reads which, rather than assuming, because the two are # different kernel facts and a future image can fix either one alone. # # The refusal itself is ASSERTED, not skipped: it is real code doing real work # and it is what a user of this image meets. What IS skipped -- and only on the # probe's confirmed refusal, with all controls passing -- is the half that needs # a reachable multicast group, which no assertion here can reach. mcast_reachable=no if [ "$ssm_rc" = 0 ] && [ "$bindmc_rc" = 0 ] && [ "$mcif" = yes ]; then mcast_reachable=yes fi if [ "$controls_ok" != yes ] || [ "$prep_ok" != yes ]; then for c in ssmping-refusal asmping-refusal mcfirst-refusal ssmping-exchange asmping-exchange mcfirst-exchange; do bst_notok "ssmping/transport/$c" "the controls did not pass (inet-stream=$inet_s inet-dgram=$inet_d unix-stream=$unix_s udp-loopback=$loop_rc prep_sock=$prep_ok), so this process cannot open ordinary sockets either and no ssmping failure may be attributed to the kernel's multicast support" done elif [ "$mcast_reachable" = yes ]; then bst_skip ssmping/transport/ssmping-refusal "this kernel joins an SSM channel and binds the group address (ssm-join rc=0, bind-mcast rc=0, $if_mc multicast-capable interface(s)), so the refusal path these cases pin is not the one that runs -- rewrite them against a live join" bst_skip ssmping/transport/asmping-refusal "same -- the ASM join also succeeds on this image (asm-join rc=0)" bst_skip ssmping/transport/mcfirst-refusal "same -- multicast is reachable on this image" bst_skip ssmping/transport/ssmping-exchange "multicast is reachable on this image, so this case is now reachable and needs an ssmpingd whose multicast reply routes back to the joining socket -- which is a network property of the image, not of this package" bst_skip ssmping/transport/asmping-exchange "same as ssmping/transport/ssmping-exchange" bst_skip ssmping/transport/mcfirst-exchange "same as ssmping/transport/ssmping-exchange" else # Which errx() the client reaches, decided by the probe rather than guessed. if [ "$bindmc_rc" != 0 ]; then want='bind [multicast]' why="binding a socket to the group address 232.43.211.234 is refused with $bindmc_name, which ssmping does before it joins" else want='Failed to join multicast channel' why="the SSM join is refused with $ssm_name" fi aprt ssmping/transport/ssmping-refusal 1 "$want" ssmping 127.0.0.1 if [ "$bindmc_rc" != 0 ]; then aprt ssmping/transport/asmping-refusal 1 'bind [multicast]' asmping 232.43.211.234 127.0.0.1 aprt ssmping/transport/mcfirst-refusal 1 'bind [multicast]' mcfirst 232.43.211.234 4321 else # joingrp.c's message differs from joinch.c's by one word, and the two # are reached by different tools -- asserted separately so a build that # linked the wrong join module is visible. aprt ssmping/transport/asmping-refusal 1 'Failed to join multicast group' asmping 232.43.211.234 127.0.0.1 aprt ssmping/transport/mcfirst-refusal 1 'Failed to join multicast group' mcfirst 232.43.211.234 4321 fi bst_skip ssmping/transport/ssmping-exchange "$why while AF_INET, AF_UNIX and a UDP loopback round-trip all succeed, so ssmping exits at that call and never sends its request -- the SSM ping exchange cannot be exercised. The exchange's unicast half IS asserted, in ssmping/daemon/serves-version, which speaks the same wire protocol to a live ssmpingd without joining anything" bst_skip ssmping/transport/asmping-exchange "the same absence reached through the ASM join (asm-join $asm_name); asmping joins before it sends, so no request leaves" bst_skip ssmping/transport/mcfirst-exchange "the same absence; mcfirst's whole purpose is to time the first packet on a group it has joined, so with no join there is nothing for it to time" fi mark exit # --- ssmping's exit status is a verdict, not a health check ------------------ # # doit() in ssmpngcl.c returns 1 from its `if (!mccount)` arm, so ssmping and # asmping exit 1 on a run whose unicast half succeeded perfectly and whose # multicast half received nothing. That is the tool working as documented -- # its exit status reports multicast reachability. Recorded here rather than # asserted through the tools, since on this image they exit earlier than that # (at the join), and a case that "passed" by reaching the same status for a # different reason would be worth nothing. if [ "$mcast_reachable" = yes ]; then bst_skip ssmping/exit/verdict-semantics "multicast is reachable on this image, so ssmping reaches doit() and its exit status becomes a live reachability verdict -- assert it against a real ssmpingd rather than against this note" else bst_ok ssmping/exit/verdict-semantics fi mark ipv6 # --- the IPv6 arm ------------------------------------------------------------ # # -6 is one of the three flags in every usage line, and it is the one this image # cannot honour. Reported from the probe rather than from the defconfig. case "$inet6_rc" in 0) bst_ok ssmping/ipv6/answer ;; -1) case "$inet6_name" in EAFNOSUPPORT) bst_ok ssmping/ipv6/answer ;; *) bst_notok ssmping/ipv6/answer "AF_INET6 refused with $inet6_name, which is not 'the family is absent' (EAFNOSUPPORT) -- ssmping -6's failure cannot be attributed to CONFIG_IPV6 on this evidence" ;; esac ;; *) bst_notok ssmping/ipv6/answer "the probe printed no usable inet6-dgram line" ;; esac if [ "$inet6_rc" = 0 ]; then bst_skip ssmping/ipv6/refusal "AF_INET6 opens on this image, so -6 reaches a real socket and the refusal this case pins is not what runs" else # names2addrsocks asks getaddrinfo for AF_INET6 and gets nothing back, so # the failure arrives as a resolver failure rather than as socket(2) -- # which is why this asserts the message and not an errno. aprt ssmping/ipv6/refusal 1 'Failed to create socket for' asmping -6 ff3e::4321:1234 ::1 fi mark igmp # --- the IGMP surface -------------------------------------------------------- # # Recorded, not asserted: /proc/net/igmp is what a multicast-capable kernel # exposes, and its presence or absence is a legitimate result either way. It is # here so that a stored run carries the evidence beside the verdicts. case "$procigmp" in present|absent) bst_ok ssmping/mcast/proc-igmp ;; *) bst_notok ssmping/mcast/proc-igmp "the probe printed no usable proc-net-igmp line" ;; esac mark end