. "$BST_LIB" # BRINTOS-WORKAROUND: brintos/linux-in-the-browser#138 revisit -- the FIRST line a # suite writes after the runner's ==BST-BEGIN== can be swallowed when a kernel # printk lands on the console in between, which cost 79-libevdev a scored case # that vanished from the transcript while the runner's cases= count still # included it. The failure direction is the dangerous one: a harness counting # result lines reads N of N green and is wrong about COVERAGE. This absorbs the # loss with a line nobody scores. Drop it once #138 lands. printf '#diag: libaio suite start (this line is a console-loss absorber, not a case)\n' # libaio 0.3.113 (poky recipes-extended/libaio) -- the userspace half of the # Linux kernel's native asynchronous I/O interface. What ships here is # /usr/lib/libaio.a, /usr/include/libaio.h and the probe this suite scores. The # package installs NO program at all, which is why the probe exists. # # --------------------------------------------------------------------------- # WHAT NEEDS THE KERNEL HERE, AND WHAT DOES NOT. # # A brintOS guest wires none of the five AIO syscalls: arch/wasm32's dispatch # table carries 143 entries and none at 206-210 (io_setup, io_destroy, # io_getevents, io_submit, io_cancel) or 333 (io_pgetevents), so do_syscall's # NULL check answers -ENOSYS. A naive suite would therefore be one red and # nothing else. This one is not, because four of libaio's five layers never # reach a syscall: # # * GROUP A -- THE ABI THE HEADER LAYS OUT. libaio's PADDED/PADDEDptr/PADDEDul # macros exist precisely so struct iocb is 64 bytes and struct io_event 32 # on a 32-bit target exactly as on a 64-bit one; they are the kernel's fixed # structures either way. So every value here must equal the native oracle's, # and a mismatch is a real defect rather than a width difference. This is # also the group that catches the PADDED branch being mis-selected -- the # one thing in this port that would fail silently. # * GROUP B -- THE io_prep_* / io_set_* INLINES. Static inlines in libaio.h # that fill a struct iocb. No fd, no syscall, no kernel. This is where the # opcode, length and offset wiring a submission would carry is asserted. # * GROUP C -- io_queue_init'S OWN REFUSAL. io_queue_init.c returns -EINVAL # for maxevents <= 0 before io_setup is reached. # * GROUP D -- THE aio_ring FAST PATH. io_getevents consults the completion # ring in USER memory first and answers an empty ring under a zero timeout # with 0, having issued no syscall. io_queue_wait and io_queue_run inherit # it. Both branches are exercised: a ring the fast path must accept, and # three it must decline. # * GROUP F -- THE WRAPPER'S ERROR CONTRACT. -errno returned, the caller's # errno restored. Holds whatever the kernel answers. # # * GROUP E -- the ticket's io_setup -> IO_CMD_PWRITE -> IO_CMD_PREAD -> # io_getevents round trip is the one that does need the kernel. It is # scored in full where io_setup succeeds and reported as ONE attributed red # -- brintos/linux#85 -- where it does not, with the probe's handling of # that branch asserted positively beside it. No edit is owed here on the day # the table grows those rows: the suite reads E01 and takes the other branch # by itself. # # --------------------------------------------------------------------------- # THE ORACLE IS A NATIVE x86-64 RUN OF THE SAME PROBE AT THE SAME VERSION. # # Every value pinned in groups A, B, C, D and F below is what a native x86-64 # build of libaio_probe.c against libaio 0.3.113 produced (2026-08-18), and the # E-group values in the kernel-present branch likewise -- that run completes the # whole round trip, so the branch this guest cannot take is pinned from a run # that did take it rather than from reading. # # A15/A16 are the two values that legitimately DIFFER from that oracle: they are # sizeof(long) and sizeof(void *), 8 there and 4 here. They are pinned to 4 # because that is the fact the rest of group A depends on -- the LE-32 arm of # libaio.h's PADDED selection is the one this target takes, and A01..A13 are # what then say it was taken correctly. PROBE=${BST_LIBAIO_PROBE:-/usr/libexec/libaio-tests/libaio-probe} ARCHIVE=/usr/lib/libaio.a HDR=/usr/include/libaio.h if [ ! -f "$ARCHIVE" ]; then bst_skip libaio/present "libaio not installed on this image" exit 0 fi bst_ok libaio/present # --- the package is whole ----------------------------------------------------- check libaio/staticlib test -s "$ARCHIVE" check libaio/header test -s "$HDR" # 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 libaio/no-shared "/usr/lib/libaio.so" "/usr/lib/libaio.so.1" "/usr/lib/libaio.so.1.0.2" # The header must carry the API rather than merely exist. check_out libaio/hdr-syscalls "io_getevents" cat "$HDR" check_out libaio/hdr-prep "io_prep_pwrite" cat "$HDR" check_out libaio/hdr-cmd "IO_CMD_PWRITE" cat "$HDR" check_out libaio/hdr-eventfd "io_set_eventfd" cat "$HDR" check_out libaio/hdr-iocb "struct iocb" cat "$HDR" # 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, so a grep that suppresses # matches in a file containing NULs reports nothing; GNU grep needs -a, busybox # grep as shipped does not. if echo x | grep -aq x 2>/dev/null; then _grepq="grep -aq"; else _grepq="grep -q"; fi for sym in io_setup io_destroy io_submit io_pgetevents \ io_queue_init io_queue_release io_queue_run \ io_getevents_0_4 io_cancel_0_4 io_queue_wait_0_4 \ compat0_1_io_cancel compat0_1_io_getevents compat0_1_io_queue_wait; do if $_grepq "$sym" "$ARCHIVE" 2>/dev/null; then bst_ok "libaio/sym-$sym" else bst_notok "libaio/sym-$sym" "$sym absent from $ARCHIVE" fi done # THE ONE ASSERTION THAT SAYS THE SYMBOL-VERSIONING PATCH TOOK. # A grep for the plain names cannot say it -- "io_getevents" is a substring of # "io_getevents_0_4", so it matches either way. The ELF VERSION TAG is the thing # only the .symver spelling emits: "LIBAIO_0.4" appears in the archive if and # only if DEFSYMVER expanded to .symver, which on this target does not compile at # all. Its ABSENCE is therefore the positive evidence that the alias path was # taken. The probe linking against io_getevents / io_queue_wait / io_cancel is # the other half, and that one is enforced at build time. if $_grepq 'LIBAIO_0\.' "$ARCHIVE" 2>/dev/null; then bst_notok libaio/no-elf-symver "the archive carries an ELF version tag (LIBAIO_0.x) -- .symver was assembled, which this target cannot do" else bst_ok libaio/no-elf-symver fi # --- the probe ---------------------------------------------------------------- if [ ! -x "$PROBE" ]; then bst_skip libaio/probe-runs "$PROBE absent from the image" exit 0 fi pout=${TMPDIR:-/tmp}/bst-libaio-out.$$ "$PROBE" > "$pout" 2>&1 prc=$? if [ "$prc" -eq 0 ]; then bst_ok libaio/probe-runs elif bst_execfail "$(head -c 200 "$pout" 2>/dev/null)"; then bst_unrun libaio/probe-runs "$(head -c 200 "$pout" 2>/dev/null)" else bst_notok libaio/probe-runs "rc=$prc $(head -c 200 "$pout" 2>/dev/null)" fi if grep -q '^PROBE-END$' "$pout" 2>/dev/null; then bst_ok libaio/probe-complete else bst_notok libaio/probe-complete "probe output has no PROBE-END terminator (last line: $(tail -1 "$pout" 2>/dev/null))" fi # val -- the probe's value for one case, empty when the probe stopped # first. Matched on the ID in field 1 rather than taken by position, since the # probe's output is captured with 2>&1 and libaio may write to stderr. val() { awk -v k="$1" '$1==k { $1=""; sub(/^ /,""); print; exit }' "$pout"; } # want -- score one probe value against a pinned answer. # A missing value is reported as the probe having stopped early rather than as a # mismatch against '', which is a different fault and localises to a different # line. want() { _got=$(val "$1") if [ -z "$_got" ]; then bst_notok "libaio/$2" "no value on target (probe stopped before $1); want='$3'" elif [ "$_got" = "$3" ]; then bst_ok "libaio/$2" else bst_notok "libaio/$2" "want:'$3' got:'$_got'" fi } # --- A: the ABI the header lays out ------------------------------------------- # struct iocb is the kernel's 64-byte submission block and struct io_event its # 32-byte completion; libaio's PADDED macros are what keep both true on a 32-bit # target. Sizes and offsets rather than one "layout ok" case, so a field that # moved says WHICH field. want A01 abi-sizeof-iocb 64 want A02 abi-sizeof-io-event 32 want A03 abi-off-lio-opcode 16 want A04 abi-off-reqprio 18 want A05 abi-off-fildes 20 want A06 abi-off-buf 24 want A07 abi-off-nbytes 32 want A08 abi-off-offset 40 want A09 abi-off-flags 56 want A10 abi-off-resfd 60 want A11 abi-off-event-obj 8 want A12 abi-off-event-res 16 want A13 abi-off-event-res2 24 want A14 abi-cmd-enum "0:1:2:3:5:6:7:8" # The two that differ from the x86-64 oracle by construction -- see the header # note. Pinned to 4 because the LE-32 arm of libaio.h's PADDED selection is what # every value above depends on having been chosen. want A15 abi-sizeof-long 4 want A16 abi-sizeof-pointer 4 # --- B: the io_prep_* / io_set_* inlines -------------------------------------- # Each case pre-fills the iocb with 0xAA, so a helper that failed to memset is # caught by B07..B11 rather than by luck. want B01 prep-pwrite-opcode 1 want B02 prep-pwrite-fildes 7 want B03 prep-pwrite-nbytes 4096 want B04 prep-pwrite-offset 8192 want B05 prep-pwrite-buf yes want B06 prep-pwrite-reqprio 0 want B07 prep-pwrite-key 0 want B08 prep-pwrite-flags 0 want B09 prep-pwrite-resfd 0 want B10 prep-pwrite-data yes want B11 prep-pwrite-tail-zeroed yes want B12 prep-pread-opcode 0 want B13 prep-pread-fildes 9 want B14 prep-pread-nbytes 512 want B15 prep-pread-offset 1024 want B16 prep-preadv-opcode 7 want B17 prep-preadv-iovcnt 2 want B18 prep-pwritev-opcode 8 want B19 prep-preadv2-opcode 7 want B20 prep-preadv2-rwflags 17 want B21 prep-preadv2-key 0 want B22 prep-pwritev2-opcode 8 want B23 prep-pwritev2-rwflags 34 want B24 prep-poll-opcode 5 want B25 prep-poll-events 3 want B26 prep-fsync-opcode 2 want B27 prep-fsync-fildes 11 want B28 prep-fdsync-opcode 3 want B29 set-eventfd-flag 1 want B30 set-eventfd-resfd 13 want B31 set-callback-data yes # --- C: libaio's own refusal -------------------------------------------------- # -22 is -EINVAL, returned by io_queue_init.c itself; io_setup is never reached, # which is why these hold on a kernel with no AIO. want C01 queue-init-zero-einval -22 want C02 queue-init-ctx-untouched yes want C03 queue-init-neg-einval -22 # --- D: the aio_ring fast path ------------------------------------------------ # 0 means io_getevents answered an empty completion ring from user memory # without entering the kernel. On this guest the kernel would have answered # -ENOSYS, so a 0 here is unambiguous evidence the fast path ran -- which is # also why the three DECLINE cases below are scored: a fast path that fired on a # ring it had no business trusting would look identical in D01 alone. want D01 ring-empty-getevents 0 want D02 ring-empty-queue-wait 0 want D03 ring-empty-queue-run 0 # Wrong magic, non-empty ring, and a NULL (blocking) timeout: three separate # reasons aio_ring_is_empty must decline, after which the call reaches the # kernel and the kernel must refuse a context it never issued. WHAT it answers # is the platform's business, so the sign is the assertion and the value is a # diagnostic -- pinning -38 here would turn this suite red the day the kernel # grows AIO, which is exactly backwards. want D05 ring-badmagic-declines yes want D07 ring-nonempty-declines yes want D09 ring-blocking-declines yes # io_pgetevents carries the same fast path, and src/io_pgetevents.c is wrapped in # an #ifdef __NR_io_pgetevents -- so a 0 here also says the kernel headers # carried the number (333) at compile time and the call was built at all. want D10 ring-empty-pgetevents 0 printf '#diag: libaio ring-declined errnos: badmagic=%s nonempty=%s blocking=%s (this guest answers -38/ENOSYS; a kernel with AIO answers -22/EINVAL)\n' \ "$(val D04)" "$(val D06)" "$(val D08)" # --- F: the syscall wrapper's error contract ---------------------------------- # _body_io_syscall returns -errno and RESTORES the caller's errno. Both hold # whatever the kernel answers, because io_destroy against a context no kernel # issued fails on every platform. want F01 errno-value-preserved 12345 want F02 errno-preserved yes want F04 returns-negative-errno yes # io_cancel is the third name that exists only as a DEFSYMVER alias # (io_getevents and io_queue_wait are exercised in group D), so these two are # also what keeps the whole of the symbol-versioning patch under test. want F06 cancel-negative-errno yes want F07 cancel-errno-preserved yes printf '#diag: libaio bogus-context errnos: io_destroy=%s io_cancel=%s (this guest answers -38/ENOSYS; a kernel with AIO answers -22/EINVAL and -14/EFAULT)\n' \ "$(val F03)" "$(val F05)" # --- E: the ticket's round trip ----------------------------------------------- # The one group that needs the kernel. E01 is io_setup's return, and it decides # which branch is scored -- so this suite needs no edit on the day the AIO # syscalls are wired. e01=$(val E01) if [ -z "$e01" ]; then bst_notok libaio/round-trip "no value on target (probe stopped before E01)" elif [ "$e01" = "0" ]; then # The kernel has AIO. Score the whole round trip against the native oracle. bst_ok libaio/setup want E02 rt-open yes want E03 rt-submit-write 1 want E04 rt-reap-write 1 want E05 rt-event-obj yes want E06 rt-event-data yes want E07 rt-write-res 4096 want E08 rt-write-res2 0 want E09 rt-submit-read 1 want E10 rt-reap-read 1 want E11 rt-read-res 4096 want E12 rt-read-res2 0 want E13 rt-bytes-roundtrip yes want E14 rt-offset-honoured yes want E15 rt-queue-release 0 want E90 rt-complete OK else # The documented platform gap. ONE attributed red for the capability that is # genuinely missing, and the probe's handling of it asserted positively beside # it -- the ticket asks for the gap to be recorded rather than hung on. bst_notok libaio/round-trip "io_setup returned $e01: the AIO syscalls (206-210) are absent from arch/wasm32/kernel/syscall_table.c, so the io_setup -> IO_CMD_PWRITE -> IO_CMD_PREAD -> io_getevents round trip cannot run -- brintos/linux#85" if [ "$e01" = "-38" ]; then bst_ok libaio/setup-enosys else bst_notok libaio/setup-enosys "io_setup failed with $e01, not -38/ENOSYS -- a different failure from the known platform gap, and not covered by the issue above" fi # The probe took the documented branch: every downstream case is NOSETUP # rather than a hang, a crash or a value invented from an unset context. _bad= for _id in E02 E03 E04 E05 E06 E07 E08 E09 E10 E11 E12 E13 E14 E15; do _v=$(val "$_id") [ "$_v" = "NOSETUP" ] || _bad="$_bad $_id=${_v:-}" done if [ -z "$_bad" ]; then bst_ok libaio/nosetup-uniform else bst_notok libaio/nosetup-uniform "not every downstream E case took the NOSETUP branch:$_bad" fi want E90 nosetup-branch NOSETUP # Where the gap actually is, recorded for whoever closes it: fs/aio.c is # compiled in and initialised (aio_sysctl_init created these two), and only # the syscall-table rows are missing. Reading them returns EINTR -- # brintos/linux#36, a different defect -- so existence is what is reported. printf '#diag: libaio kernel-side AIO: /proc/sys/fs/aio-max-nr %s, /proc/sys/fs/aio-nr %s -- the subsystem is compiled in and only the syscall table rows are absent (brintos/linux#85)\n' \ "$([ -e /proc/sys/fs/aio-max-nr ] && echo present || echo absent)" \ "$([ -e /proc/sys/fs/aio-nr ] && echo present || echo absent)" fi rm -f "$pout"