# BST-TIMEOUT: 300 # MEASURED: see the ==BST-SUITE== line on any run. The # # scoring forks almost nothing -- every probe case below # # is a shell string compare against values awk lifted # # from ONE probe run -- so the cost is that one exec plus # # the T group's handful of nfacct invocations. 300 is the # # bound the ticket gates and the nightly pass. . "$BST_LIB" # libnetfilter_acct 1.0.3 and nfacct 1.0.2 -- the userspace library for the # kernel's extended accounting infrastructure and the command-line tool built # on it. The library composes, parses and prints the NFNL_SUBSYS_ACCT messages # that create, read and delete named packet/byte counters; nfacct is the # program that sends them. Two recipes, one port, one suite: the ticket's pass # criterion is written against the TOOL, and everything the tool cannot reach # on this image is where the library lives. # # --------------------------------------------------------------------------- # THE SPLIT, AND WHY IT IS NOT THE SIBLING PORTS' SPLIT. # # libnetfilter_conntrack (79-libnetfilter-conntrack) and libnetfilter_log # (79-libnetfilter-log) own a socket themselves, so their framing and # transport halves are both inside the library. THIS library owns nothing: it # has no open, no handle and no descriptor anywhere in src/, and libmnl # appears in it only to build and walk messages. The socket belongs to the # CONSUMER. # # * THE LIBRARY (Z/A/H/B/P/X, 111 of the probe's 127 lines) is pure framing # and every case runs here, on a kernel with no netfilter at all. # * THE TRANSPORT (S in the probe, T through the real nfacct binary) needs # socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER). This kernel does not # provide it: wasm32_defconfig sets CONFIG_NET=y, CONFIG_INET=y, # CONFIG_UNIX=y and NO CONFIG_NETFILTER of any kind, so # net/netfilter/nfnetlink.c is not built and netlink protocol 12 is never # registered. brintos/linux#86 (https://brintos.io/brintos/linux/issues/86), # filed under the sibling port BRINTOS-963. # * THE CONTROL (K) opens NETLINK_ROUTE, protocol 0, registered # unconditionally under CONFIG_NET. K green with S red says "protocol 12 # is unregistered"; BOTH red would say "AF_NETLINK is unavailable", which # is a different fault in a different place that S alone cannot separate. # # S AND T NEED MORE THAN #86, AND THIS IS THE PART THAT IS EASY TO GET WRONG. # Protocol 12 alone gets no ANSWER: NFNL_SUBSYS_ACCT is registered by # net/netfilter/nfnetlink_acct.c under CONFIG_NETFILTER_NETLINK_ACCT, and # #86's proposed diff turns on CONFIG_NETFILTER + CONFIG_NETFILTER_NETLINK # only -- it says so itself, that neither pulls in "any hook, table or # conntrack". So the day #86 lands, S01/S02/S03 go green and everything that # needs the accounting subsystem to answer stays red. That is NOT this port # regressing: it is the second, separately-named gap surfacing, which is # exactly what the ticket asked this suite to establish. _attrib below # switches its wording by itself on the day, with no edit to this file. # # --------------------------------------------------------------------------- # WHY THE TICKET'S LAST STEP IS SCORED BY MESSAGE AND NOT BY EXIT STATUS. # # The criterion ends "...and `nfacct delete` removing it so that a following # `get` fails non-zero." On THIS image a following get fails non-zero -- and # so does the get before it, and the add, and the delete, because every one of # them dies at mnl_socket_open. Scoring T11 on its exit status alone would # report the ticket's hardest case GREEN on an image where not one accounting # object was ever created, which is the worst outcome available here: a # criterion that passes for a reason unrelated to the criterion. Every T case # therefore pins the exit status AND a substring of the tool's own output, # measured on a configured kernel. # # --------------------------------------------------------------------------- # THE ORACLE. # # Every value below is pinned from a native x86-64 build of these same sources # against libmnl 1.0.5, run under `unshare -rn` on the build host against a # kernel that HAS nfnetlink_acct -- so the transport expectations are MEASURED # rather than asserted as what a configured kernel ought to do. A second arm, # the same binaries in the init namespace unprivileged, answers ok for the # socket and EPERM past it; that is what lets the attribution below tell # PERMISSION from ABSENCE. Both arms produce BYTE-IDENTICAL Z/A/H/B/P/X # output, the control saying the framing layer depends on neither. # # Netlink's TLV encoding is a wire format and this library puts its counters # on it in NETWORK byte order (htobe64), so a 32- and a 64-bit build MUST # agree exactly and a hex disagreement is a real defect, never an expected # architecture difference. The P group parses bytes CAPTURED FROM A REAL # KERNEL rather than bytes this library composed: set-then-get through the # library's own writer and reader proves only that they agree with each other. # # --------------------------------------------------------------------------- # THREE UPSTREAM BEHAVIOURS PINNED HERE RATHER THAN REPORTED. Per the # project's no-public-upstream-reports rule none is filed against netfilter. # # 1. libnetfilter_acct.pc HAS AN EMPTY `Requires:` while the library needs # libmnl. src/Makefile.am sets `libnetfilter_acct_la_LIBADD = ${LIBMNL_LIBS}`, # which records the dependency in a shared object and loses it in an # archive -- and this distro is --disable-shared, so NEITHER # `pkg-config --libs` NOR `pkg-config --static --libs` mentions libmnl and # a consumer built from either fails to link every mnl_* symbol. This is # NOT the sibling ports' finding, whose .pc files DO name their # dependencies (under Requires.private, which only --static expands). # Checked against the installed file here rather than carried over. # # 2. nfacct_snprintf DOES NOT RETURN THE LENGTH IT WROTE, in the XML form. # nfacct_snprintf_xml returns the LAST snprintf's value -- strlen("") # -- and never its running total, so X05 is 6 against a 100-character # string. A consumer sizing a buffer from that return value gets it wrong. # # 3. nfacct_nlmsg_parse_payload's GUARD AND ITS BODY DISAGREE. The guard is # `if (!tb[NAME] && !tb[PKTS] && !tb[BYTES]) return -1`, so a message # carrying ANY ONE of the three passes it -- and the body then dereferences # ALL THREE unconditionally through mnl_attr_get_str/get_u64. A reply with # only NFACCT_PKTS is a NULL dereference in the library. P14 pins the half # that is safe to drive (a message with NONE of the three is rejected with # -1); the crashing half is DOCUMENTED AND NOT EXECUTED, because a probe # that segfaulted would take every case after it with it. The kernel never # sends such a reply, which is why this has survived upstream. # # --------------------------------------------------------------------------- # WHAT THIS SUITE DELIBERATELY DOES NOT DO. It does not drive `nfacct # monitor`, which blocks forever in a recv() loop waiting for overquota # notifications and has no way to self-terminate; and it does not exercise # the counters INCREMENTING, which needs an iptables/nftables rule with an # `-m nfacct` match and therefore a kernel with the whole netfilter hook # machinery, two gaps beyond what this port is about. R=${BST_LIBNETFILTER_ACCT_ROOT:-} PROBE=${BST_LIBNETFILTER_ACCT_PROBE:-$R/usr/libexec/libnetfilter-acct-tests/libnetfilter-acct-probe} NFACCT=${BST_NFACCT:-$R/usr/sbin/nfacct} ARCHIVE=$R/usr/lib/libnetfilter_acct.a PCFILE=$R/usr/lib/pkgconfig/libnetfilter_acct.pc INC=$R/usr/include/libnetfilter_acct # Asserted against the .pc rather than assumed: a recipe bumped without # regenerating the pinned bytes below must go red on ONE named case instead of # silently changing the subject of the other hundred and thirty. WANT_VERSION=1.0.3 WANT_NFACCT_VERSION=1.0.2 if [ ! -f "$ARCHIVE" ]; then bst_skip libnetfilter-acct/present "libnetfilter-acct not installed on this image" exit 0 fi bst_ok libnetfilter-acct/present check libnetfilter-acct/staticlib test -s "$ARCHIVE" check libnetfilter-acct/pkgconfig test -s "$PCFILE" check libnetfilter-acct/hdr-main test -s "$INC/libnetfilter_acct.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 libnetfilter-acct/no-shared "$R/usr/lib/libnetfilter_acct.so" "$R/usr/lib/libnetfilter_acct.so.1" # THE IMAGE SHIPS NO KERNEL UAPI HEADERS AT ALL, and for this package that is a # fact a consumer has to know rather than a detail. Everything the accounting # attribute numbers are defined in lives in the kernel's # linux/netfilter/nfnetlink_acct.h, which is not here -- the package's own # bundled copy is reached only by building against its source tree, and it is # STALE (no NFACCT_PAD; see kattr-max below). A consumer on this image must # therefore vendor the definitions. # # Asserted ABSENT deliberately. If this ever goes red the image has GAINED # kernel headers, which is an improvement -- and the thing to do then is not to # delete this case but to re-pin kattr-max to 8, because the real header will # start winning the include order. The case exists to make that coupling # impossible to miss. if [ -e "$R/usr/include/linux/netfilter/nfnetlink_acct.h" ]; then bst_notok libnetfilter-acct/no-kernel-uapi-header "the image now ships linux/netfilter/nfnetlink_acct.h -- re-pin kattr-max to 8, the real UAPI header has NFACCT_PAD and this suite's 7 comes from the package's stale bundled copy" else bst_ok libnetfilter-acct/no-kernel-uapi-header fi pcver=$(sed -n 's/^Version: *//p' "$PCFILE" 2>/dev/null | head -1) check_eq libnetfilter-acct/pkgconfig-version "$WANT_VERSION" "$pcver" check_out libnetfilter-acct/pkgconfig-libs "-lnetfilter_acct" cat "$PCFILE" # Upstream behaviour 1 above, pinned as its own case. The field is present and # EMPTY; a release that filled it in would fix a real consumer problem, and # this case is what makes that a visible change rather than a silent one. _pcreq() { awk '/^Requires:/ { sub(/^Requires:[ \t]*/, ""); v = $0 while (v ~ /\\$/) { sub(/[ \t]*\\$/, "", v); if ((getline nx) <= 0) break sub(/^[ \t]+/, "", nx); v = v " " nx } print v; exit }' "$1" 2>/dev/null } check_eq libnetfilter-acct/pkgconfig-requires-empty "" "$(_pcreq "$PCFILE")" # The transport attribution, defined HERE rather than beside the S group so the # T group can use it too: the tool and the probe hit the same wall and a red # from either must name what holds it. NF_SOCK is filled in from the probe's # S01 once it has run, and an empty one is a legitimate state (no probe on the # image) that _attrib reports as unattributed rather than guessing. # # IT KEYS ON THE FAILING CASE'S OWN ERRNO FIRST, and only then on S01. Keying # on S01 alone is the obvious way to write this and it is WRONG: run the same # probe natively without a namespace and S01 opens fine while every case past # it answers EPERM, so an S01-only rule prints "protocol 12 is registered, # therefore the accounting subsystem is missing" over a red whose actual cause # is that we may not use it. That is worse than no attribution -- it sends the # reader to a kernel .config for a permissions problem. It is not # hypothetical: that arm is one of the port's three measured arms. NF_SOCK= _attrib() { case "$1" in EPERM|EACCES) printf '%s' "held by PERMISSION, not absence: the kernel answered $1, so netlink protocol 12 AND the accounting subsystem are both present and this process lacks CAP_NET_ADMIN in its net namespace -- do not attribute this to brintos/linux#86" return ;; esac case "$NF_SOCK" in ok) printf '%s' "held by the ACCOUNTING SUBSYSTEM, not by the protocol: S01 opened netlink protocol 12, so brintos/linux#86 has landed, but net/netfilter/nfnetlink_acct.c (CONFIG_NETFILTER_NETLINK_ACCT) does not register NFNL_SUBSYS_ACCT -- the second gap, which #86's own diff states it does not close" ;; EPERM|EACCES) printf '%s' "held by PERMISSION at the socket: S01 answered $NF_SOCK, so protocol 12 IS registered and this process lacks CAP_NET_ADMIN -- do not attribute this to brintos/linux#86" ;; '') printf '%s' "unattributed: the probe emitted no S01, so the transport was never reached and the cause is unknown" ;; *) printf '%s' "held by S01 netfilter-socket ($NF_SOCK): wasm32_defconfig sets no CONFIG_NETFILTER, so netlink protocol 12 is unregistered (brintos/linux#86)" ;; esac } # --------------------------------------------------------------------------- # The symbol census. This library exports fourteen entry points -- few enough # to be EXHAUSTIVE rather than a sample, so a red names the exact entry point # that did not ship instead of saying that one arbitrary member of the # subsystem went missing. # grep -a: the archive is binary and some greps refuse it without it. if echo x | grep -aq x 2>/dev/null; then _grepq="grep -aq"; else _grepq="grep -q"; fi for sym in nfacct_alloc nfacct_free \ nfacct_attr_set nfacct_attr_set_str nfacct_attr_set_u64 \ nfacct_attr_unset \ nfacct_attr_get nfacct_attr_get_str nfacct_attr_get_u64 \ nfacct_snprintf \ nfacct_nlmsg_build_hdr nfacct_nlmsg_build_payload \ nfacct_nlmsg_parse_payload; do if $_grepq "$sym" "$ARCHIVE" 2>/dev/null; then bst_ok "libnetfilter-acct/sym-$sym" else bst_notok "libnetfilter-acct/sym-$sym" "$sym absent from $ARCHIVE" fi done # --------------------------------------------------------------------------- # The probe. Both it and the archive are this ticket's deliverable, so a skip # on either is a broken port and not a legitimate absence -- but an exec the # shell could not stage at all is platform noise, and bst_unrun tells those # two apart. if [ ! -x "$PROBE" ]; then bst_skip libnetfilter-acct/probe-runs "$PROBE absent from the image" else probe_scored=yes tmp=${TMPDIR:-/tmp} pout=$tmp/bst-libnetfilter-acct-out.$$ "$PROBE" > "$pout" 2>&1 prc=$? if [ "$prc" -eq 0 ]; then bst_ok libnetfilter-acct/probe-runs elif bst_execfail "$(head -c 200 "$pout" 2>/dev/null)"; then # NOT `exit 0`: the T group below drives the ticket's pass criterion through # the nfacct binary and does not depend on this probe at all, so a guest that # could not stage THIS exec must not take that half of the suite with it. bst_unrun libnetfilter-acct/probe-runs "$(head -c 200 "$pout" 2>/dev/null)" probe_scored=no else bst_notok libnetfilter-acct/probe-runs "probe exited $prc: $(_bst_1line "$(head -c 300 "$pout")")" fi if [ "$probe_scored" = yes ]; then # A probe killed part-way still emits usable lines, so the run is scored either # way -- but PROBE-END is what separates "these values are the whole story" # from "the rest is missing", and want() reports an absent value as its own red # rather than as a mismatch. if grep -q '^PROBE-END$' "$pout" 2>/dev/null; then bst_ok libnetfilter-acct/probe-complete else bst_notok libnetfilter-acct/probe-complete "probe output has no PROBE-END terminator (last line: $(tail -1 "$pout" 2>/dev/null))" fi # One awk pass lifts every case line into a V_ shell variable, so the # hundred-odd comparisons below fork nothing. First occurrence wins. eval "$(awk ' function q(v) { gsub(/'"'"'/, "'"'"'\\'"'"''"'"'", v); return "'"'"'" v "'"'"'" } $1 ~ /^[ZAHBPXKS][0-9][0-9]$/ && seen[$1]++ == 0 { id = $1; v = $0; sub(/^[^ ]+ ?/, "", v); print "V_" id "=" q(v) } ' "$pout")" # want : the strict comparison. An absent # value is its own message -- "the probe stopped before here" and "the probe # answered differently" are different findings and a bare mismatch would hide # the first behind an empty got. NF_SOCK=${V_S01-} want() { eval "_got=\${V_$1-}" if [ -z "$_got" ]; then bst_notok "libnetfilter-acct/$2" "no value on target (probe stopped before $1); want='$3'" elif [ "$_got" = "$3" ]; then bst_ok "libnetfilter-acct/$2" else bst_notok "libnetfilter-acct/$2" "want:'$3' got:'$_got'" fi } # --------------------------------------------------------------------------- # Z -- THE WIRE FORMAT AND THE ENUMS. Scored first because a disagreement here # explains every hex mismatch after it. Z26-Z30 are the ones most worth # reading: the LIBRARY's nfacct_attr_type enum is NOT the kernel's NFACCT_* # attribute numbering (Z12-Z19) -- it starts at 0, has no USE and no FILTER -- # so a consumer that passed a kernel constant to nfacct_attr_set would set the # wrong field silently. Both numberings are pinned for that reason. want Z01 sizeof-nlmsghdr '16' want Z02 sizeof-nfgenmsg '4' want Z03 sizeof-nlattr '4' want Z04 mnl-alignto '4' want Z05 nfacct-name-max '32' want Z06 subsys-acct '7' want Z07 msgtype-new '0' want Z08 msgtype-get '1' want Z09 msgtype-get-ctrzero '2' want Z10 msgtype-del '3' want Z11 msgtype-overquota '4' want Z12 kattr-name '1' want Z13 kattr-pkts '2' want Z14 kattr-bytes '3' want Z15 kattr-use '4' want Z16 kattr-flags '5' want Z17 kattr-quota '6' want Z18 kattr-filter '7' # 7, NOT the 8 every distro with kernel headers reports, and the two cases above # and below are equal FOR A REASON: the package ships its own stale copy of # linux/netfilter/nfnetlink_acct.h, predating the kernel's NFACCT_PAD, so its # __NFACCT_MAX is one short and NFACCT_MAX collapses onto NFACCT_FILTER. On a # normal distro the real UAPI header wins and this reads 8. On THIS image there # is no kernel header at all (see no-kernel-uapi-header above), so the bundled # copy is the only definition that exists and 7 is what every consumer here # sees -- including the library itself, whose own build puts # -I$(top_srcdir)/include first, so nothing on this image disagrees with # anything else. # # This case was pinned at 8 first and went red on target. THE GUARD WAS WRONG, # NOT THE PORT: the x86-64 oracle probe had been built without the package's # include dir, so it picked up the host's real UAPI header while the oracle's # own library used the bundled one -- an inconsistency the target does not # have. Rebuilding the oracle probe the way the recipe does changed EXACTLY # this one line of 127 and nothing else, which is what says the rest of the # pins are sound. Recorded rather than quietly corrected: a control that was # once wrong is the one a later reader most needs to know the shape of. want Z19 kattr-max '7' want Z20 flag-quota-pkts '1' want Z21 flag-quota-bytes '2' want Z22 flag-overquota '4' want Z23 htonl-32 '01020304' want Z24 htons-16 '0102' want Z25 htobe64-64 '0102030405060708' want Z26 libattr-name '0' want Z27 libattr-pkts '1' want Z28 libattr-bytes '2' want Z29 libattr-flags '3' want Z30 libattr-quota '4' want Z31 snprintf-t-plain '0' want Z32 snprintf-t-xml '1' want Z33 snprintf-t-json '2' want Z34 snprintf-f-full '1' want Z35 snprintf-f-time '2' # --------------------------------------------------------------------------- # A -- THE ACCOUNTING OBJECT. alloc, set, get, unset. No descriptor is touched # anywhere in this group, so all of it runs on a kernel with no netfilter. # # A02-A04 are the case a consumer depends on and nothing else here covers: a # FRESH object reports every attribute unset and the u64 accessor answers 0 # rather than reading uninitialised memory. A08/A09 drive the full 64-bit # range because a port that truncated a counter to 32 bits would round-trip # every small value correctly and fail only above 4 G. # # A12/A13 pin a behaviour rather than endorse it: unset clears the presence # bit and DOES NOT clear the field, so a consumer reading only the u64 # accessor cannot tell "unset" from "set to zero". That is why A12 asks # nfacct_attr_get (which answers NULL) and A13 asks the u64 accessor # (which answers 0) as two separate cases. want A01 obj-alloc 'ok' want A02 fresh-name-unset 'unset' want A03 fresh-pkts-unset 'unset' want A04 fresh-pkts-u64 '0' want A05 set-get-name 'bst-acct' want A06 set-get-pkts '7' want A07 set-get-bytes '1234' want A08 set-get-bytes-wide '72623859790382856' want A09 set-get-bytes-max '18446744073709551615' want A10 set-get-flags '2' want A11 set-get-quota '4096' want A12 unset-quota-get 'unset' want A13 unset-quota-u64 '0' # NFACCT_NAME_MAX is 32 INCLUDING the NUL the library forces at # name[NFACCT_NAME_MAX-1], so a 40-character name comes back as 31 characters. want A14 name-truncated '0123456789abcdefghijklmnopqrstu' want A15 name-truncated-len '31' want A16 obj-free 'ok' # --------------------------------------------------------------------------- # H -- nfacct_nlmsg_build_hdr. The 20 bytes every message of this protocol # opens with, and the only place the subsystem id reaches the wire. H04/H11/ # H14/H15 pin the (NFNL_SUBSYS_ACCT << 8 | cmd) shift across the WHOLE command # enum rather than at one point, because a shift that was wrong for one # command only would still produce a valid-looking header for the others. want H01 build-hdr-rc 'ok' want H02 build-hdr-bytes '1400000000070504000000000000000000000000' want H03 build-hdr-len '20' want H04 build-hdr-type-new '1792' want H05 build-hdr-flags '1029' want H06 build-hdr-seq '0' want H07 build-hdr-pid '0' want H08 nfgen-family '0' want H09 nfgen-version '0' want H10 nfgen-resid '0' want H11 build-hdr-type-get '1793' want H12 build-hdr-flags-dump '769' want H13 build-hdr-seq-set '287454020' want H14 build-hdr-type-del '1795' want H15 build-hdr-type-ctrzero '1794' # --------------------------------------------------------------------------- # B -- nfacct_nlmsg_build_payload, THE COMPOSE HALF OF THE TICKET'S PASS # CRITERION as bytes. `nfacct add`, `nfacct get` and `nfacct delete` are # exactly B02, B11 and B12, byte for byte, and this half runs on a kernel with # no netfilter because composing a buffer touches no descriptor. It is where # the LIBRARY is actually proven, since the tool that would send these # messages never gets to. # # B03/B04 are not decoration. A composed buffer whose bytes all match can # still carry a length field one word short, which reproduces every hex case # here and fails only when a receiver tries to walk it -- so mnl_nlmsg_ok is # asserted true at the stated length and FALSE one byte below it. # # B13/B14 are the empty object: `nfacct flush` and a whole-table `nfacct list` # both send a bare header with no attributes, and a bitset bug would break # that case first. # # B15/B16 put both counters at the top of the 64-bit range in network order. # A port that byte-swapped only 32 bits reproduces every case above and fails # exactly here. want B01 add-len '60' want B02 add-bytes '3c000000000705040000000000000000000000000d0001006273742d61636374000000000c00020000000000000000000c0003000000000000000000' want B03 nlmsg-ok '1' want B04 nlmsg-ok-truncated '0' want B05 full-len '80' want B06 full-bytes '50000000000705040000000000000000000000000d0001006273742d61636374000000000c00020000000000000000070c00030000000000000004d208000500000000020c0006000000000000001000' want B07 first-attr-type '1' want B08 first-attr-len '13' want B09 first-attr-str 'bst-acct' want B10 get-len '36' want B11 get-bytes '24000000010705000000000000000000000000000d0001006273742d6163637400000000' want B12 del-bytes '24000000030705000000000000000000000000000d0001006273742d6163637400000000' want B13 empty-len '20' want B14 empty-bytes '1400000003070500000000000000000000000000' want B15 wide-len '52' want B16 wide-bytes '340000000007010400000000000000000000000006000100780000000c00020001020304050607080c000300ffffffffffffffff' # --------------------------------------------------------------------------- # P -- nfacct_nlmsg_parse_payload against bytes CAPTURED FROM A REAL KERNEL # (Linux 5.4 NFNL_MSG_ACCT_GET dump replies, taken on the build host under # `unshare -rn`), not against a message this same library composed. # # P07/P08 are the pair a consumer depends on: a reply with no quota must leave # FLAGS and QUOTA UNSET rather than writing zeros, because "no quota # configured" and "quota of zero" are different states and the u64 accessor # reports both as 0. # # P13 reads NFACCT_USE straight off the wire because THE LIBRARY HAS NO # ACCESSOR FOR IT -- there is no NFACCT_ATTR_USE in the public enum at all -- # so a consumer on this image that wants the refcount has to do exactly this. # # P14 is the safe half of upstream behaviour 3 in the header: a message with # NONE of NAME/PKTS/BYTES is rejected with -1. The unsafe half (exactly one of # the three present, which the guard admits and the body then NULL-derefs) is # documented and deliberately not driven. want P01 parse-msg-ok '1' want P02 parse-msg-type '1792' want P03 parse-plain-rc '0' want P04 parse-plain-name 'bst-plain' want P05 parse-plain-pkts '7' want P06 parse-plain-bytes '1234' want P07 parse-plain-flags-unset 'unset' want P08 parse-plain-quota-unset 'unset' want P09 parse-quota-rc '0' want P10 parse-quota-name 'bst-oracle' want P11 parse-quota-flags '2' want P12 parse-quota-quota '4096' want P13 parse-use-onwire '1' want P14 parse-empty-rejected '-1' # --------------------------------------------------------------------------- # X -- nfacct_snprintf, all three formats. This is what `nfacct list` PRINTS, # so it is the half of the ticket's criterion a reader actually sees -- and # none of it is reachable on this image through the tool, which never reaches # its receive callback. # # X02 is the exact line the ticket asks to see: the named object with zeroed # counters, in the library's own words. (Here with 7/1234 rather than 0/0, so # that a formatter which printed a constant zero could not pass.) # # X05 IS 6, NOT 100, and that is upstream behaviour 2 in the header rather # than a defect of this port: nfacct_snprintf_xml returns the last snprintf's # value -- strlen("") -- and never its running total. Pinned by number # beside X06 (the real length) so the two are visibly different. # # X10/X11 pin that JSON without F_FULL writes NOTHING and returns 0; upstream # says so in a comment ("doesn't seem to make sense in JSON"), and a contract # that is only in a comment is one a consumer discovers by accident. # # X14 is a real gap rather than a formatting quirk: the XML form has NO quota # branch at all, so an object with a quota prints identically to one without # and an XML consumer cannot see one. It is pinned equal to X07's body for # exactly that reason. # # X15 IS NOT VALID JSON, and it is upstream's, not ours: the quota branch # writes `"mode" = "byte"` and `"overquota" = 0` with `=` where JSON requires # `:`. Pinned as emitted so that a consumer piping `nfacct list json` into a # parser knows the quota form will not parse. want X01 snprintf-plain-rc '73' want X02 snprintf-plain '{ pkts = 00000000000000000007, bytes = 00000000000000001234 } = bst-acct;' want X03 snprintf-plain-nofull-rc '9' want X04 snprintf-plain-nofull-len '9' want X05 snprintf-xml-rc '6' want X06 snprintf-xml-len '100' want X07 snprintf-xml 'bst-acct0000000000000000000700000000000000001234' want X08 snprintf-json-rc '53' want X09 snprintf-json ' { "pkts" : 7, "bytes" : 1234, "name" : "bst-acct" }' want X10 snprintf-json-nofull-rc '0' want X11 snprintf-json-nofull-len '0' want X12 snprintf-bad-type '-1' want X13 snprintf-plain-quota '{ pkts = 00000000000000000007, bytes = 00000000000000001234, quota = 00000000000000004096, mode = byte, overquota = no } = bst-acct;' want X14 snprintf-xml-quota 'bst-acct0000000000000000000700000000000000001234' want X15 snprintf-json-quota ' { "pkts" : 7, "bytes" : 1234, "quota" : 4096, "mode" = "byte", "overquota" = 0, "name" : "bst-acct" }' # --------------------------------------------------------------------------- # K -- THE CONTROL, AND IT MUST STAY GREEN. NETLINK_ROUTE is protocol 0, # registered unconditionally under CONFIG_NET. See the header for why S alone # could not distinguish "protocol 12 is unregistered" from "AF_NETLINK is # unavailable". want K01 rtnetlink-socket 'ok' want K02 rtnetlink-bind 'ok' want K03 rtnetlink-portid '1' # --------------------------------------------------------------------------- # S -- THE TRANSPORT, and the ticket's pass criterion driven over libmnl with # every message composed by THIS LIBRARY and every request ACKed, so the value # each case reports is the KERNEL'S -- lifted out of NLMSG_ERROR -- and not a # local success that only says the write left the process. # # THE ATTRIBUTION KEYS ON THE FAILING CASE'S OWN ERRNO FIRST, and only then on # S01. Keying on S01 alone is the obvious way to write this and it is WRONG: # run the same probe natively without a namespace and S01 opens fine while # every case past it answers EPERM, so an S01-only rule prints "protocol 12 is # registered, therefore the accounting subsystem is missing" over a red whose # actual cause is that we may not use it. That is worse than no attribution -- # it sends the reader to a kernel .config for a permissions problem. It is not # hypothetical: that arm is one of the port's three measured arms. want_s() { eval "_got=\${V_$1-}" if [ -z "$_got" ]; then bst_notok "libnetfilter-acct/$2" "no value on target (probe stopped before $1); want='$3' -- $(_attrib '')" elif [ "$_got" = "$3" ]; then bst_ok "libnetfilter-acct/$2" else bst_notok "libnetfilter-acct/$2" "want:'$3' got:'$_got' -- $(_attrib "$_got")" fi } # Opening and binding. S03 asserts the kernel assigned a portid, which is what # separates "bind returned 0" from "bind actually happened". want_s S01 netfilter-socket 'ok' want_s S02 netfilter-bind 'ok' want_s S03 netfilter-portid '1' # THE TICKET'S PASS CRITERION, in order and in its own words. # "nfacct add" ... want_s S04 acct-new 'ok' # ... and the case that makes S04 mean something: adding the same name twice # must FAIL. MEASURED as EBUSY on the oracle, not guessed -- EEXIST is the # errno this case looks like it ought to return, and does not. want_s S05 acct-new-duplicate 'EBUSY' # "...`nfacct list` showing the named object with zeroed counters, `nfacct # get` reading it back..." -- read back through the library's own parser. want_s S06 acct-get-name 'bst-probe' want_s S07 acct-get-pkts '0' want_s S08 acct-get-bytes '0' # "...and `nfacct delete` removing it..." want_s S09 acct-del 'ok' # "...so that a following `get` fails non-zero." THE case the criterion turns # on: without it, S09's delete could be an accepted no-op and every case above # would still be green. want_s S10 acct-get-after-del 'ENOENT' want_s S11 acct-del-again 'ENOENT' # A message type one past the end of the enum, dispatched to no callback. # Pinned so "rejected" and "accepted as a no-op" stay distinguishable, which a # consumer cannot tell apart without asking. want_s S12 acct-bad-msgtype 'EINVAL' fi rm -f "$pout" fi # --------------------------------------------------------------------------- # T -- THE nfacct TOOL, which is what the ticket's pass criterion is literally # written against. The probe above drives the same sequence through the # library; this group drives it through the program that ships. # # EVERY CASE PINS THE EXIT STATUS **AND** A SUBSTRING OF THE TOOL'S OWN # OUTPUT. See the header: on this image every socket-touching command fails # non-zero at mnl_socket_open, so an exit-status-only score would report the # criterion's hardest case (T11, "a following get fails non-zero") GREEN on an # image where no accounting object was ever created. # # T01-T05 are the commands that never open a socket, and they are the tool's # GREENS on this image: the port is not merely "installed", it runs. if [ ! -x "$NFACCT" ]; then bst_skip nfacct/present "$NFACCT absent from the image" else bst_ok nfacct/present # t_case -- # The attribution is the same _attrib as the S group, keyed on what the tool # itself printed, so a T red never reaches a reviewer without naming what # holds it. _tattrib() { case "$1" in *"mnl_socket_open"*) printf '%s' "$(_attrib "${V_S01-}")" ;; *"Operation not permitted"*) printf '%s' "$(_attrib EPERM)" ;; *) printf '%s' "$(_attrib '')" ;; esac } # An EMPTY means the tool must print NOTHING, not "any output # will do" -- `nfacct add` and `nfacct delete` succeed silently, and a # substring test against "" matches every string including an error message. t_case() { _tid=$1; _twrc=$2; _tsub=$3; shift 3 _tout=$("$@" 2>&1); _trc=$? if [ -z "$_tsub" ]; then [ -z "$_tout" ] && _thit=1 || _thit=0 else case "$_tout" in *"$_tsub"*) _thit=1 ;; *) _thit=0 ;; esac fi if [ "$_trc" -eq "$_twrc" ] && [ "$_thit" -eq 1 ]; then bst_ok "nfacct/$_tid" elif bst_execfail "$_tout"; then bst_unrun "nfacct/$_tid" "$_tout" else bst_notok "nfacct/$_tid" "rc=$_trc want-rc=$_twrc want:'${_tsub:-}' got:'$(_bst_1line "$_tout")' -- $(_tattrib "$_tout")" fi } t_case version 0 "nfacct v$WANT_NFACCT_VERSION" "$NFACCT" version t_case help 0 "Usage:" "$NFACCT" help # No arguments at all: main() prints the usage and exits EXIT_FAILURE. t_case no-args 1 "Usage:" "$NFACCT" t_case unknown-command 1 "Unknown command: frobnicate" "$NFACCT" frobnicate # Argument validation happens BEFORE any socket is opened, which is what makes # these two green here and what proves the binary really executes rather than # merely existing. t_case add-missing-name 1 "missing object name" "$NFACCT" add t_case add-bad-quota-kind 1 'argument must "byte" or "packet"' \ "$NFACCT" add bst-t frobnicate 4096 # Leave no object behind from an earlier run: on an image where these work, # a stale bst-t would make the add below answer EBUSY and read as a defect. "$NFACCT" delete bst-t >/dev/null 2>&1 # THE PASS CRITERION, through the tool, in the ticket's own order. t_case add 0 "" "$NFACCT" add bst-t t_case list 0 "{ pkts = 00000000000000000000, bytes = 00000000000000000000 } = bst-t;" \ "$NFACCT" list t_case get 0 "{ pkts = 00000000000000000000, bytes = 00000000000000000000 } = bst-t;" \ "$NFACCT" get bst-t t_case get-xml 0 "bst-t00000000000000000000" \ "$NFACCT" get bst-t xml t_case delete 0 "" "$NFACCT" delete bst-t # "so that a following get fails non-zero" -- and it must fail with ENOENT's # message, not with whatever else happens to fail on this image. t_case get-after-delete 1 "No such file or directory" "$NFACCT" get bst-t t_case delete-again 1 "No such file or directory" "$NFACCT" delete bst-t fi