. "$BST_LIB" # BST-TIMEOUT: 600 # 227 cases, 59s measured on a host running three concurrent # # builds. The 60s default is NOT clear of that — the run this # # number came off would have tripped it — and almost all of the # # cost is one 3.5 MB wasm probe instantiation, which is exactly # # what a loaded host stretches. # libmodbus is the Modbus protocol library: the code an industrial client or # server uses to speak Modbus/TCP over a socket or Modbus/RTU over a serial # line -- read/write coils, discrete inputs and holding/input registers, plus # the exception replies the protocol defines for a bad address or function. # # The distro is static-only and this package installs NO PROGRAM AT ALL. What # the image gets is /usr/lib/libmodbus.a, four headers under # /usr/include/modbus/ and libmodbus.pc. Nothing here is reachable from a # shell, and a payload-only suite could asserts only that an archive and some # headers exist -- which would pass just as happily if every entry point in the # archive returned garbage or trapped on first call. # # --------------------------------------------------------------------------- # WHY THIS SUITE IS A PROBE AND NOT A PAYLOAD LIST # # BRINTOS-217 is the precedent: oniguruma COMPILED and PACKAGED perfectly on # this arm and then returned silently wrong offsets to every caller. libmodbus # is badly placed for that failure mode, because it is a PROTOCOL library -- it # does not crash when it goes wrong, it puts a subtly wrong byte on the wire # and the far side accepts it. A register read that returns the right count and # the wrong values, or a coil unpacking that reverses a byte, is invisible to # anything but a value comparison. # # So data/libmodbus-probe is cross-linked with wasmcc-glibc against THE VERY # ARCHIVE THAT SHIPS (/usr/lib/libmodbus.a), staged to $TMPDIR and run here. # Its source is src/libmodbus_probe.c, where each group is documented. # # THE TRANSPORT IS A socketpair, NOT A PEER. Upstream's own unit test is a # CLIENT BINARY AND A SERVER BINARY that talk over a real TCP socket -- two # processes and a peer, neither of which this image has, and a spawn on wasm32 # is a fresh module instantiation costing seconds (BRINTOS-333 spent seven # minutes on ~150 of them). modbus_set_socket lets a context adopt an fd it did # not open, so the probe holds both ends of a socketpair in ONE process and the # two halves speak real Modbus/TCP to each other. The precedent on this image is # libassuan's probe, which drives both protocol halves the same way. # # --------------------------------------------------------------------------- # THE ORACLE IS A NATIVE x86_64 CONTROL # # data/libmodbus-native.txt is the same probe's output against a build of the # IDENTICAL source on x86_64 -- meta-oe's git SRCREV 2cbafa31 with the same four # CVE-2024-10918 patches, configured --disable-shared as the distro configures # it -- generated by src/libmodbus_mknative.sh. The probe prints only what # cannot legitimately differ between two machines: counts, wire bytes as hex, # booleans, errno NAMES and literals. No pointer, no pid, no clock reading, no # sizeof. So every MB- line must match byte for byte, and the values that DO # legitimately differ -- the pointer width, and the timing/errno diagnostics -- # are printed with an MBX- prefix, kept out of the control, and asserted # separately below. # # The control is reproducible over 12 regenerations, which is the check that # matters here: this probe drives a real socketpair and a real timeout, so if # any line were load-sensitive it would be the wire and clock groups. # # --------------------------------------------------------------------------- # THE GROUPS ARE SPLIT SO A RED NAMES A MECHANISM # # A version, protocol constants, error strings no I/O # B context lifecycle, setters/getters no I/O # C the data-access macros and coil packing no I/O # D the mapping allocator no I/O # E the wire: real request/response exchanges socketpair # F exceptions and the CVE-2024-10918 guards socketpair # G client-side decode against a staged response socketpair # H the response timeout THE CLOCK, and only this # # A red in A-D is libmodbus's or the compiler's. A red in E-G is the socket # layer's or libmodbus's. A red in H ALONE is the platform's clock -- # brintos/linux-in-the-browser#120 names select/poll timeout accuracy on this # arm -- which is why H is one group of its own and asserts nothing else. A # reader can therefore tell "libmodbus is broken here" from "the clock is". # # --------------------------------------------------------------------------- # THE CVE PATCHES ARE COVERAGE, NOT BACKGROUND # # meta-oe carries four CVE-2024-10918 patches: length checks that reject a # truncated or over-long ADU before it is indexed. Group F drives exactly those # paths (F20-F25), so those cases report OK only against a patched library -- # and the native control is built WITH the same four patches, so the comparison # cannot be satisfied by both arms being unpatched together. lib=/usr/lib/libmodbus.a incd=/usr/include/modbus pc=/usr/lib/pkgconfig/libmodbus.pc DATA=$BST_DATA NATIVE=$DATA/libmodbus-native.txt PROBE=$DATA/libmodbus-probe TMP=${TMPDIR:-/tmp} nl=' ' if [ ! -f "$lib" ]; then bst_skip libmodbus/present "libmodbus not installed on this image" exit 0 fi bst_ok libmodbus/present hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } # --- payload shape ----------------------------------------------------------- hasfile libmodbus/archive "$lib" hasfile libmodbus/header-modbus "$incd/modbus.h" hasfile libmodbus/header-rtu "$incd/modbus-rtu.h" hasfile libmodbus/header-tcp "$incd/modbus-tcp.h" hasfile libmodbus/header-version "$incd/modbus-version.h" hasfile libmodbus/pkgconfig "$pc" # The distro is static-only: upstream packages ${libdir}/libmodbus.so.*, which # must match nothing here. A shared object means --disable-shared stopped being # honoured and the image grew a package it does not want. if [ -e /usr/lib/libmodbus.so ] || [ -e /usr/lib/libmodbus.so.5 ]; then bst_notok libmodbus/no-shared-object "a libmodbus shared object shipped on a static-only distro" else bst_ok libmodbus/no-shared-object fi # The .pc is what a consumer actually compiles against, so its two load-bearing # fields are asserted rather than assumed: a -I that does not name the modbus/ # subdir sends every #include to nothing. pcbody=$(cat "$pc" 2>/dev/null) case "$nl$pcbody$nl" in *"${nl}Libs: -L\${libdir} -lmodbus$nl"*) bst_ok libmodbus/pkgconfig-libs ;; *) bst_notok libmodbus/pkgconfig-libs "no '-lmodbus' Libs line in $pc" ;; esac case "$pcbody" in *"-I\${includedir}/modbus"*) bst_ok libmodbus/pkgconfig-cflags ;; *) bst_notok libmodbus/pkgconfig-cflags "Cflags does not name the modbus/ subdir in $pc" ;; esac pcver=$(awk -F': *' '/^Version:/ { print $2; exit }' "$pc" 2>/dev/null) check_eq libmodbus/pkgconfig-version 3.1.10 "$pcver" # The shipped header's version macros must agree with the archive the probe # runs (asserted against the runtime value further down). A -dev packaged from # a different build than its archive fails exactly this pairing. # upstream spells these parenthesised -- `#define LIBMODBUS_VERSION_MAJOR (3)` -- # so the parens come off before the parts are joined. hdrver=$(awk ' { gsub(/[()]/, "") } /^#define[ \t]+LIBMODBUS_VERSION_MAJOR/ { maj = $3 } /^#define[ \t]+LIBMODBUS_VERSION_MINOR/ { min = $3 } /^#define[ \t]+LIBMODBUS_VERSION_MICRO/ { mic = $3 } END { if (maj != "") printf "%s.%s.%s", maj, min, mic } ' "$incd/modbus-version.h" 2>/dev/null) check_eq libmodbus/header-version-value 3.1.10 "$hdrver" # --- the library RUNNING, via the cross-linked probe ------------------------- if [ ! -s "$NATIVE" ]; then bst_notok libmodbus/native-present "$NATIVE missing -- no oracle to compare against" exit 0 fi bst_ok libmodbus/native-present # The probe must be the shipped archive's own consumer. do_install stages data/* # at 0644 and the deployed rootfs is read-only at browser runtime, so it cannot # be executed where it lies. _p=$TMP/bst-libmodbus-probe.$$ cp "$PROBE" "$_p" 2>/dev/null && chmod 0755 "$_p" 2>/dev/null if [ ! -x "$_p" ]; then bst_notok libmodbus/probe-exec "could not stage an executable copy of $PROBE" rm -f "$_p" exit 0 fi bst_ok libmodbus/probe-exec out=$TMP/bst-libmodbus.$$ timeout 240 "$_p" > "$out.out" 2>"$out.err"; prc=$? rm -f "$_p" check_eq libmodbus/probe-rc 0 "$prc" # A truncated run must be distinguishable from a run that legitimately produced # these lines: with no terminator, a wedge half way through would silently # reduce the number of compared cases rather than fail one. Every absent tag is # reported by the comparison below, so a wedge names the group it stopped in. if [ "$(tail -n 1 "$out.out" 2>/dev/null)" = "MB-Z01-end ALL-LIBMODBUS-DONE" ]; then bst_ok libmodbus/probe-complete else bst_notok libmodbus/probe-complete "no terminator; last saw: $(_bst_1line "$(tail -n 2 "$out.out" 2>/dev/null)")" fi # The transport is the probe's own precondition: if socketpair(2) fails, groups # E-H are absent and every one of their comparisons below reports "absent from # target output". Naming it once here is what tells a reader those ~60 reds are # one platform fact and not sixty libmodbus defects. if grep -q '^MB-E00-socketpair 1$' "$out.out" 2>/dev/null; then bst_ok libmodbus/socketpair else bst_notok libmodbus/socketpair "socketpair() unavailable: $(_bst_1line "$(grep '^MBX-socketpair-errno ' "$out.out" 2>/dev/null)")" fi # --- tag-by-tag against the native control ----------------------------------- # One awk over both files rather than a shell loop of ~199 awk calls: process # spawn is expensive on this target and the whole comparison is in-memory. awk ' NR == FNR { if ($0 !~ /^MB-/) next k = $1; $1 = ""; sub(/^ /, ""); W[k] = $0; order[++n] = k; next } /^MB-/ { k = $1; $1 = ""; sub(/^ /, ""); G[k] = $0; seen[k] = 1 } END { for (i = 1; i <= n; i++) { k = order[i]; t = substr(k, 4) if (!(k in seen)) { printf "BST not ok libmodbus/cmp-%s : absent from target output\n", t; continue } if (G[k] == W[k]) printf "BST ok libmodbus/cmp-%s\n", t else printf "BST not ok libmodbus/cmp-%s : want:%s got:%s\n", t, W[k], G[k] } } ' "$NATIVE" "$out.out" # --- the values that legitimately differ, asserted rather than ignored ------- mb_get() { awk -v k="$1" '$1==k { $1=""; sub(/^ /,""); print; exit }' "$out.out"; } check_eq libmodbus/probe-pointer-width 32 "$(mb_get MBX-pointer-width)" # The runtime version, tied to the header that SHIPS. cmp-A01 already compares # the runtime value against the control; this ties the same value to the -dev # package beside it, which nothing else here can do. check_eq libmodbus/run-version-vs-header "$hdrver" "$(mb_get MB-A01-version)" # H's elapsed time is a diagnostic, not a comparison -- but a 300 ms timeout # that returns in 0 ms did not wait, and one that never returns is a hang. The # probe bounds it; this reports the number so a red H is readable. ms=$(mb_get MBX-timeout-elapsed-ms) case "$ms" in ''|*[!0-9]*) bst_notok libmodbus/timeout-elapsed-reported "no MBX-timeout-elapsed-ms in probe output" ;; *) bst_ok libmodbus/timeout-elapsed-reported ;; esac # --- anchored: values the Modbus SPEC fixes, not the control ----------------- # These duplicate comparisons above ON PURPOSE. They are the constants a # consumer actually depends on, written as literals, so that a control # regenerated against a broken build could not quietly move them. Each is fixed # by the Modbus application protocol itself, not by libmodbus's opinion. check_eq libmodbus/anchor-max-pdu 253 "$(mb_get MB-A10-max-pdu)" check_eq libmodbus/anchor-max-adu 260 "$(mb_get MB-A11-max-adu)" check_eq libmodbus/anchor-max-read-bits 2000 "$(mb_get MB-A12-max-read-bits)" check_eq libmodbus/anchor-max-read-regs 125 "$(mb_get MB-A14-max-read-registers)" # The wire itself. A read-holding-registers response is FC 3, a byte count, then # the registers big-endian: 03 08 0001 ffff 8000 1234. If this byte string is # right the library framed, sent, parsed and answered a real request correctly, # which is the single most load-bearing thing it does. check_eq libmodbus/anchor-read-holding-pdu 03080001ffff80001234 "$(mb_get MB-E08-read-holding-pdu)" # An exception reply sets the high bit of the function code: FC 3 -> 0x83, and # 02 is ILLEGAL DATA ADDRESS. check_eq libmodbus/anchor-illegal-address-pdu 8302 "$(mb_get MB-F01-illegal-address-pdu)" # The CVE-2024-10918 guard: a write-single-coil request whose ADU is short must # be REFUSED with exception 03 (illegal data value), not indexed into. check_eq libmodbus/anchor-cve-short-coil 8503 "$(mb_get MB-F22-cve-write-bit-short-pdu)" # Coil unpacking, LSB-first: 0xCD,0x01 over 10 bits is 1,0,1,1,0,0,1,1,1,0. # A reversed byte still has the right popcount, so the ORDER is the assertion. check_eq libmodbus/anchor-coil-unpack 01000101000001010100 "$(mb_get MB-G21-read-bits-values)" rm -f "$out.out" "$out.err"