. "$BST_LIB" # BST-TIMEOUT: 300 # MEASURED: see the Self-Test Record in the BRINTOS-1580 # # design doc. The probe is a 2.8 MB wasm module and staging # # it dominates the run; 300 is the same bound the other # # probe-driven suites of this size carry. # # libuio 0.2.1+0.2.2+git at SRCREV 17d96e8f (meta-oe recipes-extended/libuio) -- # Stephan Linz's helper library over the kernel's UIO subsystem. What ships is # /usr/lib/libuio.a, /usr/include/uio_helper.h, /usr/lib/pkgconfig/libuio.pc, # the two tools /usr/bin/lsuio and /usr/bin/rwuio, and the probe this port adds # at /usr/libexec/libuio-tests/uio-probe (built in the recipe against the very # archive that ships -- see libuio_%.bbappend). # # --------------------------------------------------------------------------- # THERE IS NO UIO DEVICE ON THIS MACHINE, AND THAT IS THE DESIGN PROBLEM. # # UIO binds a userspace driver to a device the kernel has enumerated. Nothing # behind this machine's virtio surface registers one, so /sys/class/uio does not # exist and neither does /dev/uio0. The library's device path is a STRING # LITERAL inside the library -- there is no $LIBUIO_SYSFS to redirect -- so the # enumeration and per-device readers can only be driven in their absent # direction here. # # The naive suite writes itself and is worthless. Call uio_find_devices(), get # an empty list, report one green, ship. A libuio.a miscompiled into rubble # would produce THE SAME EMPTY LIST. So this file drives everything that does # NOT need a device, for real, and PROBES the rest: # # ARTIFACT the shipped files, and a header-to-archive completeness check # over every entry point uio_helper.h declares. # TOOLS lsuio and rwuio actually run, with their usage text, their # behaviour on an image with no UIO device, and their exit codes. # PARSER __uio_num_from_filename -- a pure state machine over a string. # READER __uio_line_from_file -- reads ANY path, so it is driven against # real fixtures, INCLUDING the descriptor leak meta-oe's own # fix-fclose-leak-in-uio_line_from_file.patch closes. # GETTERS every reader's documented failure return against a uioN that # cannot exist, plus the fields each does and does not touch. # MMAP the mmap/munmap state machine, driven for real against an # ordinary tmpfs file -- uio_single_mmap() maps whatever descriptor # it is handed and does not care that it is not /dev/uioN. # ENUM probed. The against-a-device half is `skip`, and only on a probe # that confirms the absence: a guest that grows a UIO device is a # HEALTHIER guest and a suite that reds on it would be wrong. # # --------------------------------------------------------------------------- # THE ORACLE IS A NATIVE x86-64 RUN OF THE SAME PROBE AGAINST THE SAME SOURCE. # # Every value pinned below was read off a native build of uio_probe.c against # libuio at SRCREV 17d96e8f with meta-oe's patch applied -- the recipe's own # source -- before this guest ever booted. Two answers differ by word size and # are asserted as wasm32 facts rather than copied from the oracle: sz-long is 8 # there and 4 here. # # THE DESCRIPTOR-LEAK PIN IS THE ONE CASE THAT GUARDS THE RECIPE'S PATCH, and it # is measured by SAMPLING THE LOWEST FREE DESCRIPTOR either side of a 256-call # loop rather than by exhausting the table. That is not a stylistic choice: the # first version exhausted, and the build host allows 1048576 open files, so # 4096 leaked descriptors against the UNPATCHED library came back completely # clean -- a guard that certified a patch it could not see. Sampling the slot # reads 0 patched and 256 unpatched, in 256 iterations, on any limit. # # It also shows the patch's own header understates the defect: it says the # function "fails to close the FILE pointer when fgets() returns NULL", but # pre-patch there was no fclose() on the SUCCESS path either. Both branches # leaked; both are pinned. # # --------------------------------------------------------------------------- # FIVE DECLARED ENTRY POINTS ARE ASSERTED **ABSENT**, ON PURPOSE. # # uio_helper.h declares uio_lib_name, uio_lib_version, uio_lib_ifcurrent, # uio_lib_ifrevision and uio_lib_ifage. NOTHING IN THE TREE DEFINES THEM -- not # a .c file, not a macro, nowhere -- so a consumer that calls one gets a link # failure against a function the installed header promised. The top-level # Makefile.am still passes -DUIO_LIB_NAME/-DUIO_LIB_CUR/-DUIO_LIB_REV/ # -DUIO_LIB_AGE for the implementation that was meant to consume them. # # So sym-complete is asserted over the SEVENTEEN that exist, and sym-lib-absent # pins the five that do not. That is deliberately the opposite of a bug report: # it is a third-party public repository, which # docs/conventions/no-public-upstream-reports.md forbids us filing against, so # the divergence is pinned here instead and a version that fixes it will be # noticed by this suite going red rather than by nobody. # # Text assertions are `case` globs against files read once into shell variables # -- no fork per case, the same reason 99-libsoc.test gives. # --------------------------------------------------------------------------- tmp=${TMPDIR:-/tmp} lib=/usr/lib/libuio.a incd=/usr/include pc=/usr/lib/pkgconfig/libuio.pc probe_src=/usr/libexec/libuio-tests/uio-probe # hasfile -- ok iff is a non-empty regular file. hasfile() { if [ -f "$2" ] && [ -s "$2" ]; then bst_ok "$1" else bst_notok "$1" "missing or empty: $2"; fi } # hasline -- ok iff is a whole line of it. hasline() { case "$_NL$3$_NL" in *"$_NL$2$_NL"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } _NL=' ' # =========================================================================== # ARTIFACT # =========================================================================== hasfile libuio/archive "$lib" hasfile libuio/header "$incd/uio_helper.h" hasfile libuio/pkgconfig "$pc" hasfile libuio/lsuio /usr/bin/lsuio hasfile libuio/rwuio /usr/bin/rwuio hasfile libuio/probe-shipped "$probe_src" check_eq libuio/archive-format '!' "$(head -c 7 "$lib" 2>/dev/null)" # 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 libuio/static-only "/usr/lib/libuio.so" "/usr/lib/libuio.la" pctext=$(cat "$pc" 2>/dev/null) # The module is `libuio` and the link flag is `-luio`: a consumer writes # PKG_CHECK_MODULES([UIO],[libuio]) and then links -luio, which is the reverse # of the libsoc asymmetry next door (module `soc`, flag `-lsoc`). hasline libuio/pc-name 'Name: libuio' "$pctext" hasline libuio/pc-libs 'Libs: -L${libdir} -luio' "$pctext" hasline libuio/pc-cflags 'Cflags: -I${includedir}' "$pctext" hasline libuio/pc-libdir 'libdir=/usr/lib' "$pctext" hasline libuio/pc-incdir 'includedir=/usr/include' "$pctext" # The .pc carries upstream's configure.ac version, which is NOT the recipe's PV # (0.2.1+0.2.2+git): the recipe pins a post-0.2.1 SRCREV whose tree still calls # itself 0.2.2-devel. Pinned because a consumer's version test reads this one. hasline libuio/pc-version 'Version: 0.2.2-devel' "$pctext" hdr=$(cat "$incd/uio_helper.h" 2>/dev/null) hasline libuio/hdr-guard '#define __UIO_HELPER_H__' "$hdr" case "$hdr" in *"#define UIO_MAX_NAME_SIZE 64"*) bst_ok libuio/hdr-name-size ;; *) bst_notok libuio/hdr-name-size "UIO_MAX_NAME_SIZE is not 64" ;; esac case "$hdr" in *"#define MAX_UIO_MAPS 5"*) bst_ok libuio/hdr-max-maps ;; *) bst_notok libuio/hdr-max-maps "MAX_UIO_MAPS is not 5" ;; esac case "$hdr" in *"#define UIO_MMAP_NOT_DONE 0"*) bst_ok libuio/hdr-mmap-verbs ;; *) bst_notok libuio/hdr-mmap-verbs "UIO_MMAP_NOT_DONE is not 0" ;; esac # --- completeness: every DEFINED entry point the header declares is present --- # # Flatten the archive's printable runs to one token per line first: a raw # `grep -o` over binary data reports "Binary file matches" instead of the # matches, which would silently under-count and read as a partial compile. flat=$tmp/bst-libuio-flat.$$ namef=$tmp/bst-libuio-names.$$ libf=$tmp/bst-libuio-lib.$$ fakef=$tmp/bst-libuio-fake.$$ ( tr -c '[:print:]' '\n' < "$lib" > "$flat" 2>/dev/null ) # Declarations start at column 0; the doc comments do not. The `(` in # `typedef void* (*uio_single_mmap_fp) (...)` falls outside the character class, # so the two function-pointer typedefs are excluded by construction rather than # by a second filter. grep -hoE '^[a-zA-Z_][a-zA-Z_0-9 *]*\**uio_[a-z_0-9]+[[:space:]]*\(' "$incd"/uio_helper.h 2>/dev/null \ | sed 's/(.*//' | awk '{print $NF}' | sed 's/^\**//' \ | sort -u > "$libf" grep -v '^uio_lib_' "$libf" > "$namef" # -x: the WHOLE LINE must equal the name. The archive's string data contains # runs like `uio_munmapinfoi` where a symbol name abuts neighbouring text, and a # substring match would count that as uio_munmap. found=$(grep -xFf "$namef" "$flat" 2>/dev/null | sort -u) ndecl=$(grep -c . "$namef"); nfound=$(printf '%s' "$found" | grep -c .) # The floor guards the check itself: a header that lost its declarations, or an # extraction that silently matched nothing, would otherwise pass vacuously with # zero names to look up. This tree declares 22 and defines 17. if [ "$ndecl" -ge 15 ] && [ "$nfound" -eq "$ndecl" ]; then bst_ok libuio/sym-complete else bst_notok libuio/sym-complete "declared=$ndecl found=$nfound"; fi nall=$(grep -c . "$libf") if [ "$nall" -eq 22 ]; then bst_ok libuio/sym-declared-count else bst_notok libuio/sym-declared-count "uio_helper.h declares $nall entry points, expected 22"; fi # The five upstream declares and never defines. Asserted ABSENT: see the header. misf=$tmp/bst-libuio-mis.$$ printf '%s\n' uio_lib_name uio_lib_version uio_lib_ifcurrent uio_lib_ifrevision uio_lib_ifage > "$misf" nmis=$(grep -xFf "$misf" "$flat" 2>/dev/null | sort -u | grep -c .) if [ "$nmis" -eq 0 ]; then bst_ok libuio/sym-lib-absent else bst_notok libuio/sym-lib-absent "$nmis of the five uio_lib_* names now resolve -- upstream defined them; re-read the suite header"; fi # The two the library exports but the installed header does not declare: a # consumer cannot obtain their prototypes, yet libuio's own .c files reach them # by hand-written extern -- which is how the probe drives them too. Pinned so a # version that promotes or drops them is noticed. intf=$tmp/bst-libuio-int.$$ printf '%s\n' __uio_line_from_file __uio_num_from_filename > "$intf" nint=$(grep -xFf "$intf" "$flat" 2>/dev/null | sort -u | grep -c .) if [ "$nint" -eq 2 ]; then bst_ok libuio/sym-internal-exported else bst_notok libuio/sym-internal-exported "expected 2 undeclared exports, found $nint"; fi case "$hdr" in *__uio_line_from_file*|*__uio_num_from_filename*) bst_notok libuio/hdr-no-internal "uio_helper.h now declares an internal helper" ;; *) bst_ok libuio/hdr-no-internal ;; esac # Matcher self-check: names that do not exist, each one character short of one # that does. `-x` must reject every one; a matcher that matched any would be # reporting presence from a prefix, which is the failure the whole-line form # exists to prevent -- and `uio_munmapinfoi` above is a live example of the # adjacency that produces it. printf '%s\n' uio_find_device uio_get_nam uio_mma uio_munma uio_free_inf \ uio_single_mmap_r uio_get_all_inf > "$fakef" nfake=$(grep -xFf "$fakef" "$flat" 2>/dev/null | sort -u | grep -c .) if [ "$nfake" -eq 0 ]; then bst_ok libuio/sym-no-prefix-inflation else bst_notok libuio/sym-no-prefix-inflation "matcher claimed $nfake non-existent names"; fi # A named sample as well, so a failure names the entry point that went missing # rather than only reporting a count. Matched against $found -- the lookup that # already ran -- so this costs no second pass over the archive. for s in uio_find_devices uio_find_devices_byname uio_get_all_info \ uio_get_device_attributes uio_get_event_count uio_get_mem_addr \ uio_get_mem_size uio_get_name uio_get_version uio_free_info \ uio_free_dev_attrs uio_mmap uio_mmap_ro uio_munmap \ uio_single_mmap uio_single_mmap_ro uio_single_munmap; do hasline "libuio/sym-$s" "$s" "$found" done rm -f "$flat" "$namef" "$libf" "$fakef" "$misf" "$intf" # =========================================================================== # TOOLS -- lsuio and rwuio really run # =========================================================================== # # THE EXIT CODE WHEN /sys/class/uio IS ABSENT IS ZERO, and that is the answer # the ticket asks to pin. Both tools print "No UIO devices found." and exit 0: # main() ends in exit(0) unconditionally once the option pass is done, and an # empty device list is not treated as an error anywhere. A caller that scripts # lsuio and tests its status learns nothing about whether a device was found. lshelp=$(lsuio --help 2>&1); lsrc=$? check_eq libuio/lsuio-help-rc 0 "$lsrc" case "$lshelp" in *"- List UIO devices."*) bst_ok libuio/lsuio-help-banner ;; *) bst_notok libuio/lsuio-help-banner "got:'$(_bst_1line "$lshelp")'" ;; esac # program_name is argv[0] VERBATIM. The comment above it in lsuio.c says the # name is "stripped of any leading path" and NOTHING STRIPS IT -- so the banner # echoes whatever the caller typed. Invoked through PATH the shell execs with # argv[0] = "lsuio" and the banner is bare; invoked by absolute path the whole # path comes back. Both are asserted, because the pair is the finding: a script # matching this banner must not assume either spelling. # # (This is also where a first draft of this suite was wrong: it pinned # "/usr/bin/lsuio - ..." for the bare call, having read argv[0] off a libtool # WRAPPER SCRIPT on the build host, which re-execs the real binary by full path. # It would have gone red on target for a reason that has nothing to do with the # port.) case "$lshelp" in "lsuio - List UIO devices."*) bst_ok libuio/lsuio-help-argv0-bare ;; *) bst_notok libuio/lsuio-help-argv0-bare "banner does not open with the invoked name: '$(_bst_1line "$lshelp")'" ;; esac case "$(/usr/bin/lsuio --help 2>&1)" in "/usr/bin/lsuio - List UIO devices."*) bst_ok libuio/lsuio-help-argv0-abs ;; *) bst_notok libuio/lsuio-help-argv0-abs "banner does not echo the absolute path it was invoked with" ;; esac for o in help mmap uiodev verbose version; do case "$lshelp" in *"--$o"*) bst_ok "libuio/lsuio-help-opt-$o" ;; *) bst_notok "libuio/lsuio-help-opt-$o" "--$o absent from usage" ;; esac done lsver=$(lsuio -V 2>&1); check_eq libuio/lsuio-version '0.2.2-devel' "$lsver" check_rc libuio/lsuio-version-rc 0 lsuio -V # =========================================================================== # ENUM -- probed, never assumed # =========================================================================== if [ -d /sys/class/uio ]; then have_sysfs=yes; else have_sysfs=no; fi if [ -e /dev/uio0 ]; then have_dev=yes; else have_dev=no; fi lsout=$(lsuio 2>&1); lsrc=$? if [ "$have_sysfs" = no ]; then check_eq libuio/lsuio-nodev-out 'No UIO devices found.' "$lsout" check_eq libuio/lsuio-nodev-rc 0 "$lsrc" bst_skip libuio/lsuio-listing "no /sys/class/uio on this machine -- nothing to list" else bst_skip libuio/lsuio-nodev-out "/sys/class/uio exists on this machine" bst_skip libuio/lsuio-nodev-rc "/sys/class/uio exists on this machine" case "$lsout" in uio*) bst_ok libuio/lsuio-listing ;; *) bst_notok libuio/lsuio-listing "a uio device exists but lsuio listed none: $(_bst_1line "$lsout")" ;; esac fi # ARGUMENT VALIDATION IS DIAGNOSED AND NOT ENFORCED. decode_switches() has no # default: arm, so getopt_long's '?' falls through the switch and the tool # carries on and exits 0. The message reaches stderr; the status does not # reflect it. That is upstream's behaviour and it is pinned, not corrected. lsbad_err=$(lsuio -z 2>&1 >/dev/null); lsbad_rc=$? case "$lsbad_err" in *"invalid option"*) bst_ok libuio/lsuio-badopt-diagnosed ;; *) bst_notok libuio/lsuio-badopt-diagnosed "no diagnostic on stderr: '$(_bst_1line "$lsbad_err")'" ;; esac check_eq libuio/lsuio-badopt-rc 0 "$lsbad_rc" if [ "$have_sysfs" = no ]; then check_eq libuio/lsuio-badopt-continues 'No UIO devices found.' "$(lsuio -z 2>/dev/null)" else bst_skip libuio/lsuio-badopt-continues "/sys/class/uio exists -- the listing is not empty" fi # -u takes a device number; atoi() of a non-numeric argument is 0, so this is a # filter for uio0 rather than an error. Still exit 0. check_rc libuio/lsuio-u-nonnumeric 0 lsuio -u notanumber rwhelp=$(rwuio --help 2>&1); rwrc=$? check_eq libuio/rwuio-help-rc 0 "$rwrc" # rwuio's usage banner says "List UIO devices." -- copied verbatim from lsuio.c. # rwuio does not list anything; it reads and writes a mapping. Upstream's, and # pinned so a corrected banner is noticed rather than silently breaking a match. case "$rwhelp" in *"- List UIO devices."*) bst_ok libuio/rwuio-help-banner-is-lsuios ;; *) bst_notok libuio/rwuio-help-banner-is-lsuios "the banner changed: '$(_bst_1line "$rwhelp")'" ;; esac for o in read write map byte word dword lword; do case "$rwhelp" in *"--$o"*) bst_ok "libuio/rwuio-help-opt-$o" ;; *) bst_notok "libuio/rwuio-help-opt-$o" "--$o absent from usage" ;; esac done check_eq libuio/rwuio-version '0.2.2-devel' "$(rwuio -V 2>&1)" # THE ONE ENFORCED VALIDATION, AND ITS EXIT CODE. main() rejects a negative # offset with usage(-EINVAL), and exit(-22) leaves the shell reading (-22 & 255) # = 234. It is checked BEFORE uio_find_devices(), so it is reached on a machine # with no UIO device at all -- which is why it can be asserted here for real. rwneg_err=$(rwuio -r -1 2>&1 >/dev/null); rwneg_rc=$? check_eq libuio/rwuio-negoff-rc 234 "$rwneg_rc" check_eq libuio/rwuio-negoff-msg 'Negative offsets are not supported.' "$rwneg_err" case "$(rwuio -r -1 2>/dev/null)" in *"Usage: rwuio [OPTIONS]"*) bst_ok libuio/rwuio-negoff-usage ;; *) bst_notok libuio/rwuio-negoff-usage "the refusal did not print usage on stdout" ;; esac # The same path via the long option, and via a hex offset, so the pin is on the # offset value rather than on one spelling of the flag. check_rc libuio/rwuio-negoff-long 234 rwuio --read=-16 check_rc libuio/rwuio-negoff-write 234 rwuio -w -1:0 check_rc libuio/rwuio-zerooff 0 rwuio -r 0 if [ "$have_sysfs" = no ]; then check_eq libuio/rwuio-nodev-out 'No UIO devices found.' "$(rwuio -r 0 2>&1)" else bst_skip libuio/rwuio-nodev-out "/sys/class/uio exists on this machine" fi # =========================================================================== # THE PROBE # =========================================================================== # # A probe is not run in place: do_install ships it 0755 but the rootfs is # read-only at browser runtime, so it is copied to tmpfs first. Never assert # `test -x` on the shipped path -- see the packager README. probe=$tmp/bst-uio-probe.$$ if cp "$probe_src" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then out=$("$probe" 2>&1); prc=$? else out=; prc=127 fi pget() { printf '%s\n' "$out" | sed -n "s/^$1=//p" | head -n 1; } case "$out" in *uio-probe-start*) bst_ok libuio/probe-started ;; *) if bst_execfail "$out"; then bst_unrun libuio/probe-started "$out" else bst_notok libuio/probe-started "probe produced no start marker: '$(_bst_1line "$out")'"; fi ;; esac case "$out" in *uio-probe-end*) bst_ok libuio/probe-complete ;; *) bst_notok libuio/probe-complete "probe did not reach its end marker -- it died mid-run" ;; esac check_eq libuio/probe-rc 0 "$prc" # --- ABI: wasm32 is ILP32, and that is load-bearing here --------------------- # # uio_get_mem_size() does fscanf(file,"0x%lx",&info->maps[n].size) into an # `int`, and uio_get_event_count() does fscanf(file,"%d",...) into an # `unsigned long`. Both are type errors upstream. They are HARMLESS ON THIS ARM # ONLY BECAUSE sizeof(long) == sizeof(int) == 4 here; on an LP64 target the # first is an eight-byte store into a four-byte field. Asserting the word size # is what makes that reasoning checkable rather than asserted. check_eq libuio/abi-sizeof-int 4 "$(pget sz-int)" check_eq libuio/abi-sizeof-long 4 "$(pget sz-long)" check_eq libuio/abi-max-maps 5 "$(pget sz-maps)" check_eq libuio/abi-name-size 64 "$(pget sz-name)" # --- PARSER: __uio_num_from_filename ----------------------------------------- check_eq libuio/nf-uio0 0 "$(pget nf-uio0)" check_eq libuio/nf-uio7 7 "$(pget nf-uio7)" check_eq libuio/nf-uio12 12 "$(pget nf-uio12)" check_eq libuio/nf-uio255 255 "$(pget nf-uio255)" # leading zeros are accumulated, not rejected: "uio007" is device 7. check_eq libuio/nf-uio007 7 "$(pget nf-uio007)" # "uio" alone ends in the digit state having read no digit, so num is still -1. check_eq libuio/nf-bare -1 "$(pget nf-bare)" check_eq libuio/nf-empty -1 "$(pget nf-empty)" check_eq libuio/nf-foo0 -1 "$(pget nf-foo0)" check_eq libuio/nf-trail -1 "$(pget nf-trail)" check_eq libuio/nf-uuio0 -1 "$(pget nf-uuio0)" # "." and ".." are what scandir hands it first on any real /sys/class/uio. check_eq libuio/nf-dot -1 "$(pget nf-dot)" # --- READER: __uio_line_from_file -------------------------------------------- check_eq libuio/lf-missing -1 "$(pget lf-missing)" check_eq libuio/lf-empty -2 "$(pget lf-empty)" check_eq libuio/lf-line-rc 0 "$(pget lf-line-rc)" check_eq libuio/lf-line-val 'brintos-uio' "$(pget lf-line-val)" # A line longer than UIO_MAX_NAME_SIZE arrives TRUNCATED to 63 characters with a # success return -- not an error. A caller reading a long sysfs name gets a # silently short answer. check_eq libuio/lf-long-rc 0 "$(pget lf-long-rc)" check_eq libuio/lf-long-len 63 "$(pget lf-long-len)" # --- the descriptor leak meta-oe's patch closes ------------------------------ check_eq libuio/lf-empty-iters 256 "$(pget lf-empty-iters)" check_eq libuio/lf-empty-rc -2 "$(pget lf-empty-rc)" check_eq libuio/lf-ok-iters 256 "$(pget lf-ok-iters)" check_eq libuio/lf-ok-rc 0 "$(pget lf-ok-rc)" # The sampler first: a fd_slot() that always answered the same number would # report every leak as absent, so the two pins below would pass vacuously. check_eq libuio/lf-sampler-sees-leaks 4 "$(pget lf-sampler-growth)" check_eq libuio/lf-noleak-empty 0 "$(pget lf-empty-fd-growth)" check_eq libuio/lf-noleak-ok 0 "$(pget lf-ok-fd-growth)" # --- GETTERS against a uioN that cannot exist -------------------------------- check_eq libuio/gt-name -1 "$(pget gt-name)" check_eq libuio/gt-version -1 "$(pget gt-version)" check_eq libuio/gt-event -1 "$(pget gt-event)" check_eq libuio/gt-event-zeroed 0 "$(pget gt-event-val)" check_eq libuio/gt-size -1 "$(pget gt-size)" # UIO_INVALID_SIZE, written before the open is attempted. check_eq libuio/gt-size-invalid -1 "$(pget gt-size-val)" check_eq libuio/gt-addr -1 "$(pget gt-addr)" check_eq libuio/gt-addr-invalid 'yes' "$(pget gt-addr-invalid)" # The bounds check returns BEFORE touching the struct, unlike the open failure # above which invalidates the field on its way out. Two different -1s. check_eq libuio/gt-size-oob -1 "$(pget gt-size-oob)" check_eq libuio/gt-addr-oob -1 "$(pget gt-addr-oob)" check_eq libuio/gt-oob-untouched 4321 "$(pget gt-oob-untouched)" check_eq libuio/gt-devattrs -1 "$(pget gt-devattrs)" check_eq libuio/gt-devattrs-null 'yes' "$(pget gt-devattrs-null)" check_eq libuio/gt-all-null -1 "$(pget gt-all-null)" check_eq libuio/gt-all-neg -1 "$(pget gt-all-neg)" check_eq libuio/gt-all-over -1 "$(pget gt-all-over)" # THE ANSWER A CALLER SEES AND THE CODE DOES NOT JUSTIFY: uio_get_all_info() # returns 0 as soon as its range check passes, whatever its five sub-reads did. # Here every one of them failed and the struct came back empty, and the caller # was still told success. Pinned because a consumer that checks only this return # believes it has a populated device. check_eq libuio/gt-all-inrange-returns-0 0 "$(pget gt-all-inrange)" check_eq libuio/gt-all-name-empty 'yes' "$(pget gt-all-name-empty)" check_eq libuio/gt-all-size0 -1 "$(pget gt-all-size0)" # --- MMAP: driven for real against tmpfs ------------------------------------- check_eq libuio/mm-open 'ok' "$(pget mm-open)" # fd 0 is rejected BEFORE mmap_result is touched, so a sentinel survives: a # caller whose device really is on descriptor 0 gets a silent no-op and a stale # state field, not a mapping and not an error. check_eq libuio/mm-fd0-ret 'null' "$(pget mm-fd0-ret)" check_eq libuio/mm-fd0-untouched 7 "$(pget mm-fd0-result)" # size <= 0 is NOT_DONE, not FAILED -- the two are distinguishable and the # distinction is what lsuio -m reports as "N/A" rather than "FAILED". check_eq libuio/mm-size0-ret 'null' "$(pget mm-size0-ret)" check_eq libuio/mm-size0-notdone 0 "$(pget mm-size0-result)" # WHICH mmap shapes this runtime honours -- a 2x2 varying ONE bit at a time. # # THIS IS WHERE A FIRST DRAFT OF THIS SUITE GOT THE CAUSE WRONG, and it is worth # the space because the mistake is invisible in the result. The probe originally # compared MAP_SHARED|PROT_READ|PROT_WRITE against MAP_PRIVATE|PROT_READ, saw # the first refused and the second honoured, and reported "MAP_SHARED of a # regular file is unsupported". Two variables moved between those arms. The # refusal is on **PROT_WRITE**, not on MAP_SHARED: MAP_SHARED|PROT_READ is # honoured here, which brintos/linux#92 also shows in passing. Naming the wrong # bit would have put a false cause in front of the next reader AND hidden real # coverage -- see the uio_single_mmap_ro block below, which the wrong reading # had skipping for no reason. # # ENODEV on the PROT_WRITE arm is brintos/linux#30 -- open, filed 2026-07-28 # against gdbm, filesystem-independent (tmpfs and virtiofs alike). Nothing is # filed from here; it is an existing platform defect, and this suite attributes # to it rather than re-reporting it. check_eq libuio/mm-anon-supported 'yes' "$(pget mm-anon-supported)" # The two MAP_PRIVATE arms and the shared read-only arm are asserted, not # probed: if any of those stopped working, mapping is broken far beyond #30 and # a red is the right answer. check_eq libuio/mm-private-rw-supported 'yes' "$(pget mm-pv-rw-supported)" check_eq libuio/mm-private-ro-supported 'yes' "$(pget mm-pv-ro-supported)" shro=$(pget mm-sh-ro-supported); shrw=$(pget mm-sh-rw-supported) if [ "$shro" = yes ]; then bst_ok libuio/mm-shared-ro-supported else bst_notok libuio/mm-shared-ro-supported "MAP_SHARED|PROT_READ refused too (errno=$(pget mm-sh-ro-errno)) -- wider than brintos/linux#30, which is PROT_WRITE only"; fi # The one expected non-pass in this file, and it is a SKIP with a named owner. if [ "$shrw" = yes ]; then bst_ok libuio/mm-shared-rw-supported else bst_skip libuio/mm-shared-rw-supported "MAP_SHARED|PROT_WRITE of a regular file: errno=$(pget mm-sh-rw-errno) -- brintos/linux#30, open; PROT_READ on the same descriptor is honoured, so the refusal is on the write bit" fi # --- uio_single_mmap(): MAP_SHARED|PROT_READ|PROT_WRITE, so #30 gates it ----- if [ "$shrw" = yes ]; then check_eq libuio/mm-ok-ret 'nonnull' "$(pget mm-ok-ret)" check_eq libuio/mm-ok-result 1 "$(pget mm-ok-result)" check_eq libuio/mm-ok-addr-matches 'yes' "$(pget mm-ok-addr-matches)" # 0x5a written through the mapping and read back through it. check_eq libuio/mm-ok-readback 90 "$(pget mm-ok-readback)" check_eq libuio/mm-munmap-resets 0 "$(pget mm-munmap-result)" check_eq libuio/mm-all-m0 1 "$(pget mm-all-m0)" check_eq libuio/mm-all-m1 1 "$(pget mm-all-m1)" else # libuio is NOT reached in these cases -- the platform refuses the mapping # before uio_single_mmap() can record a result -- so they are skips against # #30 and not statements about the port. What libuio does with the refusal IS # asserted, immediately below. for c in mm-ok-ret mm-ok-result mm-ok-addr-matches mm-ok-readback \ mm-munmap-resets mm-all-m0 mm-all-m1; do bst_skip "libuio/$c" "MAP_SHARED|PROT_WRITE refused (errno=$(pget mm-sh-rw-errno), brintos/linux#30) -- uio_single_mmap() cannot succeed on this platform" done # And this is the assertion that replaces them: handed a mapping the kernel # refuses, uio_single_mmap() must report UIO_MMAP_FAILED and return NULL # rather than hand back MAP_FAILED as if it were an address. It does, and the # same values are what mm-badfd asserts unconditionally below. check_eq libuio/mm-refused-is-failed 2 "$(pget mm-ok-result)" check_eq libuio/mm-refused-returns-null 'null' "$(pget mm-ok-ret)" fi # --- uio_single_mmap_ro(): MAP_SHARED|PROT_READ, which #30 does NOT gate ------ # # This is the half the wrong diagnosis was hiding. The read-only mapping is # honoured on this platform, so libuio's whole mmap path -- the size check, the # mmap call, the MAP_FAILED test, the internal_addr store and the UIO_MMAP_OK # transition -- is driven FOR REAL here, on an ordinary file, with a readback # through the mapping. It is the strongest evidence in this suite that the port # works, and it needs no UIO device. if [ "$shro" = yes ]; then check_eq libuio/mm-ro-ret 'nonnull' "$(pget mm-ro-ret)" check_eq libuio/mm-ro-result 1 "$(pget mm-ro-result)" # The byte is seeded by pwrite(2) BEFORE the mapping, so this case is # independent of the read-write arm #30 refuses. It read 0 on the first # target run for exactly that reason -- a defect in the guard, not in the # read-only path -- and the seed is what makes it mean something. check_eq libuio/mm-ro-seed 1 "$(pget mm-ro-seed)" check_eq libuio/mm-ro-read 90 "$(pget mm-ro-read)" else for c in mm-ro-ret mm-ro-result mm-ro-seed mm-ro-read; do bst_skip "libuio/$c" "MAP_SHARED|PROT_READ refused (errno=$(pget mm-sh-ro-errno)) -- wider than brintos/linux#30" done fi # These hold whether or not mmap() works, so they are asserted unconditionally. check_eq libuio/mm-badfd-ret 'null' "$(pget mm-badfd-ret)" check_eq libuio/mm-badfd-failed 2 "$(pget mm-badfd-result)" check_eq libuio/mm-all-fd0-noop 7 "$(pget mm-all-fd0)" # map 4 has size 0, so the walker leaves it NOT_DONE while mapping 0 and 1. check_eq libuio/mm-all-m4-notdone 0 "$(pget mm-all-m4)" check_eq libuio/mm-all-after-munmap 0 "$(pget mm-all-after-munmap)" # uio_single_munmap() calls munmap() unconditionally, including on a map that # was never mapped (NULL, length 0). It must not crash, and it must still reset # the state -- the probe reaching its end marker is half this assertion. check_eq libuio/mm-munmap-virgin 0 "$(pget mm-munmap-virgin)" # --- FREE -------------------------------------------------------------------- check_eq libuio/fr-null-ok 'yes' "$(pget fr-null-ok)" check_eq libuio/fr-devattrs-null 'yes' "$(pget fr-devattrs-null)" check_eq libuio/fr-list-ok 'yes' "$(pget fr-list-ok)" check_eq libuio/fr-alloc 'ok' "$(pget fr-alloc)" # --- ENUMERATION: the probe's own view must agree with the suite's ----------- check_eq libuio/en-sysfs-agrees "$have_sysfs" "$(pget en-sysfs-present)" check_eq libuio/en-devnode-agrees "$have_dev" "$(pget en-devnode-present)" if [ "$have_sysfs" = no ]; then # scandir("/sys/class/uio") fails, so both finders return NULL. This is the # refuses-cleanly assertion, and it is real: the alternative to NULL here is a # crash or a bogus list, and a rubble build would give one of those. check_eq libuio/en-find-all-null 'null' "$(pget en-find-all)" bst_skip libuio/en-find-all-populated "no /sys/class/uio on this machine -- probed, not assumed" bst_skip libuio/en-getters-real "no /sys/class/uio on this machine -- probed, not assumed" bst_skip libuio/en-mmap-device "no /dev/uio0 on this machine -- probed, not assumed" else bst_skip libuio/en-find-all-null "/sys/class/uio exists on this machine" check_eq libuio/en-find-all-populated 'nonnull' "$(pget en-find-all)" # SKIP, not `not ok`. A guest that grows a UIO device is a HEALTHIER guest, # and reddening the nightly for a platform improvement is exactly the failure # this file's header argues against. The detail is the notice that the device # half of this suite was never written -- because no machine had one to write # it against -- and now can be. bst_skip libuio/en-getters-real "a UIO device is present: this suite's against-a-device half was never written, because no machine had one -- extend it" if [ "$have_dev" = yes ]; then bst_skip libuio/en-mmap-device "a /dev/uio node is present: extend this suite to mmap it" else bst_skip libuio/en-mmap-device "no /dev/uio0 on this machine -- probed, not assumed" fi fi # A name no device can carry is NULL either way. check_eq libuio/en-find-byname-absent 'null' "$(pget en-find-byname)" rm -f "$probe"