# BST-TIMEOUT: 300 # MEASURED: elapsed 21 s for 124 cases at a 2889 MiB peak # # (2026-08-18, local functional verify, idle host). 300 is # # ~14x that, which is the headroom the nightly needs on a # # loaded host. The figure is small because the scoring # # forks almost nothing -- see the fork-budget note below, # # which is the reason and not an aside: the first version # # of this file took 2 minutes and was killed. . "$BST_LIB" # libnfnetlink 1.0.2 -- the low-level netfilter netlink transport library # (meta-networking recipes-filter/libnfnetlink), the messaging substrate the # whole libnetfilter_* family is built on. What ships is /usr/lib/ # libnfnetlink.a, /usr/lib/pkgconfig/libnfnetlink.pc and three headers under # /usr/include/libnfnetlink/, and libnfnetlink.so.0 where the recipe converts. # There is no CLI, no daemon, no check_PROGRAMS and no tests/ directory at any # tag, so on an image with no compiler nothing that ships can drive a line of it # -- hence the probe, built in the recipe against the library that ships. # # --------------------------------------------------------------------------- # THE LIBRARY HAS THREE SURFACES AND THEY FAIL INDEPENDENTLY ON THIS ARM. # # * THE FRAMING LAYER -- nfnl_addattr_l and its typed wrappers, the # nfnl_nfa_addattr_* family, nfnl_parse_attr, nfnl_parse_hdr, the nest # macros, nfnl_build_nfa_iovec, nfnl_dump_packet. Every one takes a plain # struct nlmsghdr * or struct nfattr * and touches no descriptor. Z, H, A, # R, N, P, V and D below. This is the half the ticket names and it stands # entirely on its own. # * THE NFNETLINK HANDLE -- everything behind nfnl_open(), which is # socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER). The S group. # * THE INTERFACE TABLE -- nlif_open()/nlif_query()/nlif_index2name()/ # nlif_get_ifflags(), which open NETLINK_ROUTE instead. The I group. This # is not a detail: it is a full socket -> sendto -> dump-receive -> # message-parse path through this library's own code, on a protocol this # kernel DOES register, so it is the proof that the port's socket code # works even while the S group cannot run. # # nfnl_fill_hdr sits in the SECOND group, which is the one surprise. It reads # ssh->subsys_id and ssh->nfnlh->{flags,seq}, and the only constructor for # either struct is nfnl_open(). So "compose the header" is not reachable # without the socket, and H01/H02 compose the same 20 bytes from the uapi # macros instead so the A and P groups have a well-formed message to build on. # That is not presented as a test of nfnl_fill_hdr -- S08 is, and is scored # separately. # # --------------------------------------------------------------------------- # THE EXPECTED REDS, ONE CAUSE, LEFT RED ON PURPOSE # # S02-S16 are expected to fail on this image and are attributed rather than # skipped. wasm32_defconfig sets CONFIG_NET=y but no CONFIG_NETFILTER at all, # so net/netfilter/nfnetlink.c is not built and netlink protocol 12 is never # registered: socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER) answers # EPROTONOSUPPORT and nfnl_open() returns NULL. Filed as brintos/linux#86 # (https://brintos.io/brintos/linux/issues/86), with the two defconfig symbols # as a candidate fix. # # S01 IS THE CONTROL THAT MAKES THAT CLAIM MEAN ANYTHING, and it must stay # green. It opens NETLINK_ROUTE -- protocol 0, registered unconditionally under # CONFIG_NET. S01 green with S02 red says "protocol 12 is not registered", # which is a kernel .config fact with a one-line fix. S01 AND S02 both red # would say "AF_NETLINK is unavailable", which is a different fault in a # different place and would make the filed issue wrong. Without S01 the two are # indistinguishable from this suite's output. # # The reds flip green by themselves the day netfilter is configured in, which # is the regression signal this suite is bought for -- so they are reported # failing and not skipped, and the I group next door means the socket half of # the port is still measured meanwhile. # # --------------------------------------------------------------------------- # THE ORACLE IS A NATIVE x86-64 RUN OF THE SAME PROBE AGAINST THE SAME 1.0.2 # # Every value pinned below was produced by building libnfnetlink_probe.c # against libnfnetlink 1.0.2 (sha256 b064c7c3...bc376, the recipe's own # tarball) on x86-64 and reading the output off it. Netlink's TLV encoding is a # wire format -- 4-byte alignment, host-order scalars, a 4-byte {u16 len, u16 # type} header -- and struct nlmsghdr is 16 bytes and struct nfgenmsg 4 on both # a 32- and a 64-bit build, so the two architectures MUST agree byte for byte. # A mismatch is a real defect on this arm, not a platform difference to reason # about. # # That is also the assertion a target-only run cannot make. Writing an # attribute and reading it back through nfnl_parse_attr proves the writer and # the reader agree; it does not prove they are RIGHT. A port that placed # nfa_len at the wrong offset would round-trip through its own mistake and # report every value correct. Only the pinned bytes catch that, and they are # hand-checked against the encoding rather than merely copied: # # A02 = 1c000000 0001 0100 00000000 00000000 02 00 0000 0800 0100 efbeadde # ^len 28 ^type 0x0100 (subsys 1 << 8 | 0) # ^flags NLM_F_REQUEST ^seq 0 ^pid 0 # ^family AF_INET # ^version 0 # ^res_id 0 # ^nfa_len 8 # ^nfa_type 1 # ^0xdeadbeef # host order # # TWO ORACLE VALUES ARE ENVIRONMENT-DEPENDENT AND ARE HANDLED, NOT PINNED. # S15/S16 need CAP_NET_ADMIN to bind a non-zero group mask (the native control # was taken under `unshare -rn`; the target runs as root), and I08's flag word # is a property of whichever netns the run is in -- lo is UP on the target and # DOWN in the control's namespace. I08 therefore asserts a DIFFERENTIAL, not a # value: rtnetlink's view of lo's flags must equal the SIOCGIFFLAGS ioctl's # view of the same interface. That holds in both environments and is a # statement about this library's message parsing rather than about the host. # # --------------------------------------------------------------------------- # WHY THIS SUITE SCORES WITH ONE awk PASS AND NOT ONE PER CASE # # THE GUEST DOES NOT RETURN MEMORY ON PROCESS EXIT, so a suite's PROCESS COUNT # is a hard resource budget rather than a performance detail. Measured on this # image, 2026-08-18: a 50-case cut of this suite -- 40 per-symbol `grep` calls # and little else -- reached a 3953 MiB host RSS, about 79 MiB per fork that is # never given back. The first version of this file scored each of its ~90 probe # values with its own `awk`, which together with the 40 greps came to ~135 # processes; the guest passed 12.3 GiB and the local-verify runner killed it at # a growth rate of 5171 MiB/min, with `==BST-BEGIN==` emitted and not one case # after it. The probe itself was never the problem: run alone it completes in # 12 s at a 1081 MiB peak. # # So the scoring below reads the probe's whole output ONCE into shell variables # and compares them with `eval`, which is a builtin and forks nothing -- ~90 # cases at zero processes. The symbol census keeps a fork per symbol because a # single-pass alternative would depend on `grep -o`, but it names TWELVE # representative symbols across all three surfaces rather than the whole API, # which is the same statement at a third of the cost. Total: about twenty # processes for 150 cases. # # Do not "simplify" this back to a per-case `awk`. The neighbouring suites can # afford that idiom because they are smaller; this one measured that it cannot. # # --------------------------------------------------------------------------- # ONE CASE THIS SUITE DELIBERATELY DOES NOT DRIVE, SAID RATHER THAN OMITTED # # nfnl_parse_attr stores at tb[NFA_TYPE(nfa)-1] under only a `<= max` guard, so # an attribute of type 0 writes tb[-1] -- out of bounds, on any architecture. # nfnl_check_attributes next door carries the `if (flavor)` guard that would # have stopped it. It is an upstream defect and it is not arm-specific; a case # that drove it would corrupt the probe's own stack rather than report # anything, so what is owed here is the note and not the case. # $BST_ROOT prefixes the shipped paths so this file can be dry-run against a # native build before an image is spent on it -- unset on the target, which is # the only configuration that matters there. It is a prefix and not four # separate overrides so a dry run cannot half-point at the image. R=${BST_LIBNFNETLINK_ROOT:-} PROBE=${BST_LIBNFNETLINK_PROBE:-$R/usr/libexec/libnfnetlink-tests/libnfnetlink-probe} ARCHIVE=$R/usr/lib/libnfnetlink.a PCFILE=$R/usr/lib/pkgconfig/libnfnetlink.pc INC=$R/usr/include/libnfnetlink # The version the recipe pins. Asserted rather than read back from the library, # so a recipe bumped without updating this suite is a red here instead of a # silent change of subject -- the bytes above are only the oracle for THIS # version. WANT_VERSION=1.0.2 if [ ! -f "$ARCHIVE" ]; then bst_skip libnfnetlink/present "libnfnetlink not installed on this image" exit 0 fi bst_ok libnfnetlink/present # --- the package is whole ----------------------------------------------------- check libnfnetlink/staticlib test -s "$ARCHIVE" check libnfnetlink/pkgconfig test -s "$PCFILE" check libnfnetlink/hdr-main test -s "$INC/libnfnetlink.h" check libnfnetlink/hdr-nfnetlink test -s "$INC/linux_nfnetlink.h" check libnfnetlink/hdr-compat test -s "$INC/linux_nfnetlink_compat.h" # 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 libnfnetlink/no-shared "$R/usr/lib/libnfnetlink.so" "$R/usr/lib/libnfnetlink.so.0" # The .pc must name the version the recipe pinned, or a consumer resolving # libnfnetlink through pkg-config gets a different answer from the one this # suite pinned its bytes against. pcver=$(sed -n 's/^Version: *//p' "$PCFILE" 2>/dev/null | head -1) check_eq libnfnetlink/pkgconfig-version "$WANT_VERSION" "$pcver" # The .pc's link line is what every libnetfilter_* consumer will resolve # through; -lnfnetlink is the whole of it here since the library has no # dependency of its own. check_out libnfnetlink/pkgconfig-libs "-lnfnetlink" cat "$PCFILE" # The archive carries the API. grep rather than nm: the image ships no binutils # nm, and the names are present as plain bytes. -a where the grep supports it -- # on this arm the archive members are LLVM IR bitcode (ThinLTO), so a grep that # suppresses matches in a file containing NULs reports nothing; GNU grep needs # -a, busybox grep as shipped does not. Probing keeps this measuring # libnfnetlink rather than measuring the image's grep. if echo x | grep -aq x 2>/dev/null; then _grepq="grep -aq"; else _grepq="grep -q"; fi # TWELVE representative symbols, not the whole API -- see the fork-budget note # above. Four per surface: the handle, the framing layer, the interface table. for sym in nfnl_open nfnl_close nfnl_portid nfnl_subsys_open nfnl_fill_hdr \ nfnl_addattr_l nfnl_addattr32 nfnl_parse_attr nfnl_parse_hdr \ nfnl_build_nfa_iovec nlif_open nlif_index2name; do if $_grepq "$sym" "$ARCHIVE" 2>/dev/null; then bst_ok "libnfnetlink/sym-$sym" else bst_notok "libnfnetlink/sym-$sym" "$sym absent from $ARCHIVE" fi done # --- the probe ---------------------------------------------------------------- if [ ! -x "$PROBE" ]; then bst_skip libnfnetlink/probe-runs "$PROBE absent from the image" exit 0 fi # The probe links what shipped: libnfnetlink.so where the recipe converts, # libnfnetlink.a where it does not. A main that links a shared library records it # in its wasm.linux.needed section by file name, so the probe's bytes say which. if bst_ps_converted "$R/usr/lib/libnfnetlink.so.0"; then if $_grepq 'libnfnetlink\.so' "$PROBE" 2>/dev/null; then bst_ok libnfnetlink/probe-links-shipped else bst_notok libnfnetlink/probe-links-shipped "libnfnetlink.so.0 shipped, but the probe names no libnfnetlink.so -- it linked the archive" fi elif $_grepq 'libnfnetlink\.so' "$PROBE" 2>/dev/null; then bst_notok libnfnetlink/probe-links-shipped "no libnfnetlink.so.0 shipped, but the probe needs one" else bst_ok libnfnetlink/probe-links-shipped fi tmp=${TMPDIR:-/tmp} pout=$tmp/bst-libnfnetlink-out.$$ "$PROBE" > "$pout" 2>&1 prc=$? if [ "$prc" -eq 0 ]; then bst_ok libnfnetlink/probe-runs elif bst_execfail "$(head -c 200 "$pout" 2>/dev/null)"; then bst_unrun libnfnetlink/probe-runs "$(head -c 200 "$pout" 2>/dev/null)" rm -f "$pout" exit 0 else bst_notok libnfnetlink/probe-runs "probe exited $prc: $(_bst_1line "$(head -c 300 "$pout")")" fi if grep -q '^PROBE-END$' "$pout" 2>/dev/null; then bst_ok libnfnetlink/probe-complete else bst_notok libnfnetlink/probe-complete "probe output has no PROBE-END terminator (last line: $(tail -1 "$pout" 2>/dev/null))" fi # ONE awk pass over the probe's output, turning every value into a shell # variable V_ and the nfnl_dump_packet block into four D_* variables. This # is the whole of the process cost of scoring ~90 cases -- see the fork-budget # note in the header for what the per-case idiom cost instead. The ID is matched # in field 1 rather than by line position, because nfnl_dump_packet writes its # own lines into the same stream. eval "$(awk ' function q(v) { gsub(/'"'"'/, "'"'"'\\'"'"''"'"'", v); return "'"'"'" v "'"'"'" } /^PROBE-DUMP-BEGIN$/ { d = 1; next } /^PROBE-DUMP-END$/ { d = 0; next } d && /NFM_PAYLOAD\(nlh\) = / { p = $0; sub(/.*NFM_PAYLOAD\(nlh\) = /, "", p); dp = p; next } d && /nlmsg_type = / { h = $0; sub(/^[ \t]+/, "", h); dh = h; next } d && /nfa_type=/ { n++; a = $0; sub(/.*nfa_type=/, "", a); sub(/, *nfa_len=/, ":", a); walk = walk (walk == "" ? "" : " ") a; next } !d && $1 ~ /^[ZHARNPVSI][0-9][0-9]$/ && seen[$1]++ == 0 { id = $1; v = $0; sub(/^[^ ]+ ?/, "", v); print "V_" id "=" q(v) } END { print "D_PAYLOAD=" q(dp); print "D_HDR=" q(dh); print "D_COUNT=" q(n + 0); print "D_WALK=" q(walk) } ' "$pout")" # want -- score one probe value against a pinned answer. # `eval` is a builtin and a plain assignment forks nothing, which is the point. # A missing value is reported as the probe having stopped early rather than as # a mismatch against '"'"''"'"', which is a different fault and localises elsewhere. want() { eval "_got=\${V_$1-}" if [ -z "$_got" ]; then bst_notok "libnfnetlink/$2" "no value on target (probe stopped before $1); want='$3'" elif [ "$_got" = "$3" ]; then bst_ok "libnfnetlink/$2" else bst_notok "libnfnetlink/$2" "want:'$3' got:'$_got'" fi } # The netfilter-netlink protocol, read once. Every S mismatch is attributed to # it when it is absent, so the reds carry their cause instead of reading as # fifteen independent faults. NFNL_PROTO=${V_S02-} NFNL_HELD="held by S02 netfilter-socket ($NFNL_PROTO): wasm32_defconfig sets no CONFIG_NETFILTER, so netlink protocol 12 is unregistered (brintos/linux#86)" # want_s -- want() for the handle group. Same scoring; a mismatch says WHY when # the protocol is missing, and stays a bare mismatch when it is not, so the day # netfilter lands these become ordinary strict cases with no edit here. want_s() { eval "_got=\${V_$1-}" if [ "$_got" = "$3" ]; then bst_ok "libnfnetlink/$2" elif [ "$NFNL_PROTO" != "ok" ]; then bst_notok "libnfnetlink/$2" "want:'$3' got:'$_got' -- $NFNL_HELD" elif [ -z "$_got" ]; then bst_notok "libnfnetlink/$2" "no value on target (probe stopped before $1); want='$3'" else bst_notok "libnfnetlink/$2" "want:'$3' got:'$_got'" fi } # --- Z: the sizes and macro arithmetic the whole encoding rests on ------------ # A header that computed these differently would produce a message every field # of which round-trips and no kernel accepts, so they are asserted before # anything is built on them. want Z01 sizeof-nlmsghdr 16 want Z02 sizeof-nfgenmsg 4 want Z03 sizeof-nfattr 4 want Z04 nlmsg-alignto 4 want Z05 nfa-alignto 4 want Z06 nfnl-header-len 20 want Z07 nfnl-buffsize 8192 want Z08 nfa-length "4 8 11" want Z09 nfa-space "4 8 12" want Z10 nlmsg-length "16 20" # --- H: the message header the A/P groups build on ---------------------------- want H01 hdr-len 20 want H02 hdr-bytes 1400000000010100000000000000000002000000 # --- A: append typed attributes, byte for byte -------------------------------- want A01 add32-rc "rc=0 len=28" want A02 add32-bytes 1c0000000001010000000000000000000200000008000100efbeadde want A03 add16-rc "rc=0 len=36" # 2 bytes of payload -> nfa_len 6, but the message advances by 8. That pad is # the field a port most easily gets wrong, and the two 00 bytes after 3412 are # it. want A04 add16-bytes 240000000001010000000000000000000200000008000100efbeadde0600020034120000 want A05 add8-rc "rc=0 len=44" want A06 add8-bytes 2c0000000001010000000000000000000200000008000100efbeadde0600020034120000050003005a000000 want A07 addl-rc "rc=0 len=56" # Seven bytes -> one pad byte, which nfnl_addattr_l zeroes EXPLICITLY. The 00 # after 6272696e746f73 ("brintos") is what proves it was zeroed rather than # left as whatever the buffer held. want A08 addl-bytes 380000000001010000000000000000000200000008000100efbeadde0600020034120000050003005a0000000b0004006272696e746f7300 want A09 addl-empty-rc "rc=0 len=60" # A zero-length attribute is legal netlink and is how a flag is carried: # nfa_len is the bare 4-byte header and the message advances by 4. want A10 addl-empty-bytes 3c0000000001010000000000000000000200000008000100efbeadde0600020034120000050003005a0000000b0004006272696e746f730004000500 # nfnl_nest appends a zero-length attribute with NLA_F_NESTED (0x8000) set... want A11 nest-open "len=64 nfa_type=0x8006" want A12 nest-inner32 "rc=0 len=72" want A13 nest-inner16 "rc=0 len=80" # ...and nfnl_nest_end back-patches its length from how far the tail moved: 4 # header + 8 + 8 = 20. NFA_TYPE masks the nest bit back off. want A14 nest-close "nfa_len=20 nfa_type=0x8006 nfa_type_masked=6" want A15 msg-len 80 want A16 msg-bytes 500000000001010000000000000000000200000008000100efbeadde0600020034120000050003005a0000000b0004006272696e746f730004000500140006800800010004030201060002000b0a0000 # --- R: the refusals, and that a refusal does not corrupt --------------------- # maxlen is what the CALLER declares, not the buffer's real size -- the library # has no other way to know -- so 28 over a 64-byte buffer drives the boundary # without a real overflow, which is what a caller with a short skb does. want R01 fit-exact "rc=0 len=28" # The message length must be UNCHANGED after the refusal. A refusal that had # already advanced nlmsg_len would leave the caller holding a message whose # declared length covers bytes nothing wrote -- worse than the overflow it # refused. want R02 overflow-refused "rc=-1 errno=ENOSPC len=28 unchanged=true" # Exactly-fits is the other side of the same boundary and is the case a naive # `>=` gets wrong: 28 bytes hold the 20-byte header and one 8-byte attribute # with nothing to spare. want R03 boundary-exact "rc=0 len=28" want R04 nfa-fit "rc=0 nfa_len=12" want R05 nfa-overflow "rc=-1 errno=ENOSPC nfa_len=12 unchanged=true" # --- N: the standalone-nfattr family ------------------------------------------ want N01 nfa-add32 "rc=0 nfa_len=12" want N02 nfa-add16 "rc=0 nfa_len=18" want N03 nfa-addl "rc=0 nfa_len=27" want N04 nfa-bytes 1b0006000800010004030201060002000b0a0000070003006e667100 # nfnl_nfa_addattr_* is the one place the family does NOT zero its pad, unlike # nfnl_addattr_l which memsets it. Stated rather than hidden: the byte after # "nfq" is whatever the caller's buffer held, and every caller in the netfilter # tree memsets first. The probe's buffer is memset, so 00 here is the caller's # zero and not the library's. want N05 nfa-pad-uninit 00 # --- P: parse it back --------------------------------------------------------- # nfnl_parse_hdr takes a handle and never dereferences it -- read off the 1.0.2 # source, not assumed -- which is what makes the whole P group survive the S # group's absence. want P01 hdr-first-attr non-null want P02 hdr-first-offset 20 want P03 hdr-genmsg "family=2 version=0 res_id=0" # A message that is EXACTLY the header carries no attribute, and the library # says so by returning NULL while still handing back the genmsg -- a different # answer from the error case below, and callers distinguish them. want P04 hdr-only "null genmsg=set" want P05 hdr-only-genmsg "family=10 version=0 res_id=7" # Short of even that: NULL, and genmsg left ALONE. The library must not hand # back a pointer into a buffer it has just decided is too short. want P06 hdr-short "null genmsg_untouched=true" want P07 attr-region-len 60 want P08 parse-remainder 0 want P09 parse-bitmap 1111110 want P10 rt-u32 0xdeadbeef want P11 rt-u16 0x1234 want P12 rt-u8 0x5a want P13 rt-str-len 7 want P14 rt-str brintos # The pointer must land INSIDE the message, not into a copy: a caller that # mutates through it expects to be editing the message. want P15 rt-str-aliases in-message want P16 rt-empty-len 0 want P17 attr-present "1 0" # nfnl_get_data over an absent attribute is defined to yield 0 rather than to # fault -- the macro tests the slot first. want P18 absent-yields-0 0 want P19 nest-type-mask "0x8006 6" want P20 nest-payload-len 16 want P21 nest-remainder 0 want P22 nest-bitmap 1100 want P23 nest-u32 0x01020304 want P24 nest-u16 0x0a0b # A type ABOVE max is dropped rather than stored -- the forward-compatibility # rule netlink is built on. With max=2 only the first two survive, and nothing # is reported left over. want P25 parse-max-clamp "11 remain=0" # Truncation: a buffer cut mid-attribute stops the walk and the leftover is # REPORTED rather than swallowed. That return value is the only signal a caller # gets that the message was short. The last attribute is the 20-byte nest, so # cutting 1/2/3 bytes leaves 19/18/17. want P26 trunc-1 19 want P27 trunc-2 18 want P28 trunc-3 17 # --- V: the two-iovec sender helper ------------------------------------------- # Five bytes of value: the second iovec is padded up to 8 while nfa_len records # the unpadded 4+5. That asymmetry is the whole of what this call does and it # is what a sender relies on. want V01 iovec-nfa "nfa_len=9 nfa_type=42" want V02 iovec-lens "4 8" want V03 iovec-bases "nfa val" want V04 iovec-aligned "nfa_len=8 4 4" # --- D: nfnl_dump_packet ------------------------------------------------------ # It writes straight to stdout and prints pointers, so only the fields that are # the same on every run and every arm are scored. The pointer lines are # deliberately not. check_eq libnfnetlink/dump-payload 60 "$D_PAYLOAD" check_eq libnfnetlink/dump-header \ "nlmsg_type = 256, nlmsg_len = 80, nlmsg_seq = 0 nlmsg_flags = 0x1" "$D_HDR" check_eq libnfnetlink/dump-attr-count 6 "$D_COUNT" # The walk's own order and lengths, which is what the dump exists to show. It # reports NFA_TYPE, so the nest arrives as 6 rather than 0x8006. check_eq libnfnetlink/dump-attr-walk "1:8 2:6 3:5 4:11 5:4 6:20" "$D_WALK" # --- S: the nfnetlink handle -------------------------------------------------- # S01 is the control and must stay green whatever the rest of this group does -- # see the header. It is scored with want(), not want_s(), deliberately: it is # not held by S02, it is what gives S02 its meaning. want S01 rtnetlink-socket "ok" want_s S02 netfilter-socket "ok" want_s S03 nfnl-open "ok" want_s S04 nfnl-fd "ok" want_s S05 nfnl-portid "nonzero" # Subscription state read from the socket the library bound rather than from # the opaque handle: a plain open subscribes to nothing, and the port id the # kernel assigned is the one nfnl_portid reports. want_s S06 nfnl-getsockname "rc=0 family=16 groups=0x00000000 pid_matches=true" want_s S07 subsys-open "ok" # nfnl_fill_hdr at last -- the call that needs the handle. Its FIELDS are # scored and not its bytes: nlmsg_seq is seeded from time(NULL), so a pinned # hex would be a clock read. type 0x0103 is NFNL_SUBSYS_CTNETLINK << 8 | 3. want_s S08 fill-hdr "len=20 type=0x0103 flags=0x0001 pid=0 family=2 version=0 res_id=0" want_s S09 seq-monotonic "monotonic" want_s S10 seq-tracking-off 0 want_s S11 subsys-rebusy "EBUSY" # A subsystem id past the array is refused rather than written past the end of # h->subsys[]. want_s S12 subsys-badid "ENOENT" want_s S13 rcvbuf-grew "grew" want_s S14 nfnl-close 0 # Binding a non-zero group mask needs CAP_NET_ADMIN; the target runs as root # and the native control was taken under `unshare -rn`. 0x5 is # NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY, and the rebind is # the whole of what a subscription does. want_s S15 subscribe "ok" want_s S16 subscribe-groups "rc=0 groups=0x00000005" # --- I: the interface table over NETLINK_ROUTE -------------------------------- # The socket path that DOES work here: nlif_open -> rtnl_open -> socket # (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) -> bind(RTMGRP_LINK) -> sendto # (RTM_GETLINK dump) -> blocking recvmsg loop -> this library's own nlmsghdr # walk into an ifindex table. Every one of those steps is libnfnetlink's code. want I01 nlif-open "ok" want I02 nlif-fd "ok" # The probe sets SO_RCVTIMEO before the dump: rtnl_receive_multi loops on a # BLOCKING recvmsg and leaves it only on NLMSG_DONE or an error, so a kernel # whose dump ended without DONE would hang and take the suite's whole time # bound with it. A stall surfaces as an empty table and a red I06, which # localises, instead of as a suite-timeout, which does not. want I03 nlif-rcvtimeo 0 want I04 nlif-query 1 # if_nametoindex is glibc's ioctl path, so the index is an INDEPENDENT source: # the table below has to agree with something it did not produce. want I05 lo-index "nonzero" want I06 nlif-index2name "rc=1 name=lo" want I07 nlif-ifflags "rc=1 loopback=true" # The differential, and the point of this group -- see the header for why the # flag word itself is not pinned. _i08=${V_I08-} case "$_i08" in agree*) bst_ok libnfnetlink/nlif-flags-agree ;; differ*) bst_notok libnfnetlink/nlif-flags-agree "rtnetlink and SIOCGIFFLAGS disagree on lo: $_i08" ;; ioctl-failed*) bst_notok libnfnetlink/nlif-flags-agree "no ioctl view to compare against: $_i08" ;; *) bst_notok libnfnetlink/nlif-flags-agree "no value on target (probe stopped before I08); want='agree ...'" ;; esac # Index 0 is the documented wildcard and is answered from the code path rather # than from the table. want I09 nlif-index-zero "rc=1 name=*" want I10 nlif-unknown-index "rc=-1 errno=ENOENT" want I11 nlif-flags-zero "rc=-1 errno=ENOENT" rm -f "$pout"