. "$BST_LIB" # BST-TIMEOUT: 900 # TWO python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps the output of one of those two. # # # # The two are: a bare `import' (the platform gate) and the # # probe, which takes ONE start for a hundred-odd # # measurements. # # # # There is no third. The recipe declares no # # `[project.scripts]' and the package installs no # # executable, so there is nothing to reach through a # # shebang; `no-bin-script' asserts that absence rather than # # leaving it unstated. # # # # The work inside the probe is constant reads, two # # struct.calcsize calls and at most four REFUSED syscalls. # # A refused syscall is a table lookup and a return -- # # microseconds beside an interpreter start on this arm, # # which is tens of seconds before the first line of the # # program runs. # # # # 900 is 450s per start, roughly seven times the ~60s a # # python3 start has measured at here. It is BELOW the 1200 # # that 99-python3-crc32c and 99-python3-xxhash declare # # because those need three starts and this needs two. # # # # A suite-timeout red is a batch-wide cost, so if the two # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # python3-pyfanotify 0.2.2 (meta-openembedded/meta-python # recipes-devtools/python/python3-pyfanotify_0.2.2.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # A Python wrapper for Linux fanotify: `fanotify_init(2)' opens a notification # group, `fanotify_mark(2)' attaches an event mask to an inode, a mount or a # filesystem, and `read(2)' on the group fd returns # `struct fanotify_event_metadata' records. The property that distinguishes # fanotify from inotify is that each record carries an OPEN FILE DESCRIPTOR for # the object that caused the event, not a name relative to a watch. # # ============================================================================ # THIS IS A C EXTENSION BEHIND A FACADE, SO `import' IS A dlopen. # ============================================================================ # # `pyfanotify/__init__.py:19' does `from . import ext' at MODULE SCOPE, and # lines 21-70 immediately read forty-odd constants off it # (`FanoRule = ext.FanoRule', `FAN_ACCESS = ext.FAN_ACCESS', ...). So the 17 kB # of Python is a facade over `ext.cpython-312.so' and delivers nothing without # it: `import pyfanotify' IS the dlopen. # # That gate was SHUT until 2026-09-11. Two of the extension's 99 `env' imports -- # `_PyArg_ParseTuple_SizeT' and `_PyArg_ParseTupleAndKeywords_SizeT' -- were # names the interpreter defined and did not export, both PY_SSIZE_T_CLEAN rename # targets (`src/ext.c:5' defines the macro before including Python.h, and the # module has five PyArg_ParseTupleAndKeywords call sites and one # PyArg_ParseTuple). BRINTOS-2504 fixed the export derivation in # `python3_%.bbappend' (meta-brintos 0a8afc71) and the interpreter went from # 1715 exported names to 1734. `import' below is where that stays fixed. # # ============================================================================ # THE TICKET'S ACTUAL SUBJECT, AND WHY MOST OF THIS SUITE SKIPS. # ============================================================================ # # Of every interface in this round, fanotify is the one the wasm32 defconfig # went out of its way to enable: upstream's Kconfig gives CONFIG_FANOTIFY # `default n' and `arch/wasm32/configs/wasm32_defconfig' sets it `=y' by hand, # naming the reason in its own comment -- "fs-notification (rsync/tracker-class # tools); pure VFS". Nothing in the image had exercised it, so this port is the # first evidence either way that the decision holds at RUNTIME. # # IT DOES NOT, AND THE REASON IS ONE LAYER BELOW THE ONE THE TICKET EXPECTED. # The ticket predicted a capability answer -- EPERM under CAP_SYS_ADMIN with # CONFIG_USER_NS off. What is actually there is a DISPATCH answer: the subsystem # is compiled in and its UAPI numbers exist on this arch # (`arch/wasm32/include/uapi/asm/unistd.h' wires __NR_fanotify_init 300 and # __NR_fanotify_mark 301, and `asm/hwjs_syscalls.h' even declares both # HWJS_SUSPENDS), but `arch/wasm32/kernel/syscall_table.c' has NO ROW for either # number. `sys_call_table[300]' and `[301]' are therefore NULL, `do_syscall' # returns -ENOSYS on the NULL check, and the host side agrees: the kernel's own # `wasm32_syscall_implemented(300)' answers 0, so hardwarejs does not route the # number to the kernel and its responder has no case for it either. # # So the whole event-flow half of this suite -- the mark, the four event # classes, the `/proc/self/fd' readlink, the permission pair, the mark removal, # the ENOENT on a missing path -- CANNOT RUN. It is not omitted: the probe # records `gate_open=no' with the errno that closed it, and every one of those # cases is reported `skip' carrying that reason. A skip with an attributed cause # is the honest report; fifty reds would bury the one fact that matters. # # A SECOND CONFIG FACT SITS BEHIND THE FIRST and would bite the moment the table # rows land. `FAN_OPEN_PERM' and friends need CONFIG_FANOTIFY_ACCESS_PERMISSIONS, # which `depends on SECURITY'; neither is in the wasm32 defconfig, so # `fanotify_mark' rejects FANOTIFY_PERM_EVENTS with EINVAL at # `fs/notify/fanotify/fanotify_user.c:1735'. `mark-perm-errno' below is written # to record that by errno rather than to demand a pass, so wiring the syscalls # alone will not silently turn this case green on a wrong premise. # # ============================================================================ # WHAT THIS SUITE STILL PROVES, ON A KERNEL THAT REFUSES THE SYSCALLS. # ============================================================================ # # Three things, and they are the reason the port is on the image rather than # held back: # # * THE EXTENSION LOADS AND RUNS. `import' is the dlopen, and `fanorule-ok' # drives `FanoRule_init' -- a `PyArg_ParseTupleAndKeywords' over # "y*|Okz*z*z*b" and nothing else. That is the very machinery BRINTOS-2504's # two missing `_SizeT' names lived in, exercised with no syscall at all. A # module that merely loaded would pass `import'; only this passes both. # # * THE CONSTANTS AGREE WITH THE KERNEL. Every value below is the one in # `include/uapi/linux/fanotify.h' at the pinned LINUX_BRINTOS_SRCREV, not the # one the library echoed back. `src/ext.c' publishes 0 for anything its # build-time header lacked, so a 0 is ambiguous by construction -- and # comparing against the kernel's own value is what disambiguates it. # # * THE STRUCT GEOMETRY IS RIGHT FOR A 32-BIT ARM. `mask' is `__aligned_u64', # so `struct fanotify_event_metadata' is 24 bytes with the mask at offset 8, # not 22 bytes with it at offset 6. A consumer that got this wrong would read # plausible garbage rather than fail, which is exactly why it is asserted. # # ============================================================================ # The suite proper. # ============================================================================ # # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not run". P=python3-pyfanotify bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/pyfanotify SO=$PKG/ext.cpython-312.so INIT=$PKG/__init__.py DI=$SITE/pyfanotify-0.2.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-pyfanotify.list PROBE=$BST_DATA/python3-pyfanotify-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pyfanotify.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE FILES LAND. These run before any import, so a red # here says the .deb is wrong rather than that the module misbehaves -- which # matters more than usual for this port, since a missing .so and an unloadable # one both end at the same ImportError. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/extension-present test -f "$SO" check $P/extension-nonempty test -s "$SO" check $P/init-present test -f "$INIT" check $P/init-nonempty test -s "$INIT" # THE PYTHON HALF IS A FACADE, AND THIS IS WHERE THAT IS ASSERTED RATHER THAN # ASSUMED. Line 19 is the module-scope `from . import ext'; the constant block # after it is what makes the import unconditional. A release that grew a fallback # would change what this port is, and would show up here first. check_out $P/init-imports-ext-at-module-scope "from . import ext" cat "$INIT" check_out $P/init-reads-ext-constants "FAN_ACCESS = ext.FAN_ACCESS" cat "$INIT" # THE SONAME CARRIES NO PLATFORM TRIPLE. Upstream names an extension # `ext.cpython-312-.so'; poky's python3 reports an empty EXT_SUFFIX # triple on this arm, so the file is plain `ext.cpython-312.so'. The import # resolves through importlib.machinery.EXTENSION_SUFFIXES, so the two must agree # -- and if a future poky restored the triple, this case reds before the import # does and says why. check_eq $P/extension-name "ext.cpython-312.so" \ "$(cd "$PKG" 2>/dev/null && echo ext.cpython-312*.so)" # EXACTLY ONE SHARED OBJECT SHIPS. A second .so would mean the build produced # something this port did not expect and did not measure. check_eq $P/pkg-one-so "1" \ "$(ls "$PKG"/*.so 2>/dev/null | wc -l | tr -d ' ')" # NO EXECUTABLE. setup.py declares no console_scripts, so nothing lands in # /usr/bin. Asserted so a future release that started shipping one is noticed. check $P/no-bin-script test ! -e /usr/bin/pyfanotify # --------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.2.2" cat "$DI/METADATA" check_out $P/dist-info-name "Name: pyfanotify" cat "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" # NOT PURELIB, stated by the packaging itself. `Root-Is-Purelib: false' is the # wheel's own claim that it contains something architecture-specific -- the # opposite of what every pure-Python port here asserts, and the one-line summary # of why this port is different. check_out $P/dist-info-not-purelib "Root-Is-Purelib: false" cat "$DI/WHEEL" # ------------------------------------------------------------ the .deb file --- # The installed file list, read once into $W. The copy is what the loop below # redirects from: a stdin redirect the top-level shell performs on the virtiofs # root makes dup2(2) return 513 and ENDS the suite silently (brintos/linux#73), # and $W is on tmpfs. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # Every payload path is under site-packages and is the .so, a .py/.pyc, or a # dist-info file. _bad="" while IFS= read -r _f; do case "$_f" in ''|/usr/lib/python3.12/site-packages) continue ;; */) continue ;; esac [ -d "$_f" ] && continue case "$_f" in *.so|*.py|*.pyc) ;; */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE|*/LICENSE.*) ;; */top_level.txt|*/entry_points.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-expected "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-pyfanotify$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE DEPENDENCY SET IS THE PORT'S CLAIM ABOUT WHAT IT WAITS ON. The recipe adds # python3-crypt, python3-datetime, python3-logging and python3-multiprocessing -- # all INTERPRETER SUBPACKAGES, so nothing outside python3 is required -- and the # wasm shlibs pass derived nothing further, the extension's dylink.0 declaring no # NEEDED entry. Each is asserted by name: a control stanza that had lost one # would surface as an ImportError from deep inside the facade, which reads as # this port's fault rather than as a packaging one. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" for _d in python3-core python3-crypt python3-datetime python3-logging \ python3-multiprocessing; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done # ------------------------------------------------------------------ import --- # THE PLATFORM GATE, and the first of two python3 starts. See the header: this is # a dlopen of a side module whose 99 `env' imports must resolve against a # statically linked main, and two of them did not until BRINTOS-2504 landed. check $P/import python3 -c 'import pyfanotify' # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" exit 0 fi bst_ok $P/probe-present # THE SECOND START, and the one that carries the suite. Bounded at 600s inside # the 900s suite budget so a hung probe is reported as a probe overrun with the # last line it printed, rather than as a suite timeout that says nothing about # where it stopped. # # The probe EXITS 0 even when the import fails, so this case passing means "the # probe ran", not "the module works" -- `import-ok' below is what carries that. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' because # a key is emitted once and a duplicate would otherwise concatenate; `cut -f2-' # because a value may legitimately contain `='. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The probe reached its end. Every case below reads a value it printed, so a # probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" # ------------------------------------------------------- the gate's verdict --- # ONE case for the platform fact, carrying the exception if there is one. _imp=$(pv import_ok) check_eq $P/import-ok "yes" "$_imp" if [ "$_imp" != "yes" ]; then # TWO DIFFERENT FACTS REACH HERE AND THEY NEED DIFFERENT WORDS. The probe # flushes every line, so if it emitted NOTHING at all it never reached its # first statement or it died taking the interpreter with it -- a wasm # indirect-call signature trap arrives as SIGILL (`rc=132') rather than as an # exception, and `probe-run' above carries that. Reporting such a death as # "the extension did not load" names the wrong call and sends the reader to # the loader: measured on BRINTOS-2519, where `ext.create()' trapped and the # whole run read as an import failure although `import' itself was green. if [ -z "$(pv import_ok)$(pv import_error)" ]; then _why="the probe produced no output -- see probe-run for how it died; this is NOT an import failure (the separate \`import' case above stands on its own)" else _why="pyfanotify/ext.cpython-312.so did not load: $(pv import_error)" fi for _c in version package-is-package ext-is-extension has-fanotify \ has-fanotifyclient has-fanorule has-fanotifydata all-tuple \ const-access const-modify const-attrib const-close-write \ const-close-nowrite const-open const-moved-from const-moved-to \ const-create const-delete const-delete-self const-move-self \ const-open-exec const-q-overflow const-open-perm \ const-access-perm const-open-exec-perm const-event-on-child \ const-rename const-ondir const-close const-move const-cloexec \ const-nonblock const-class-notif const-class-content \ const-class-pre-content const-unlimited-queue \ const-unlimited-marks const-enable-audit const-report-tid \ const-report-fid const-report-dir-fid const-report-name \ const-report-dfid-name const-mark-add const-mark-remove \ const-mark-dont-follow const-mark-onlydir \ const-mark-ignored-mask const-mark-ignored-surv-modify \ const-mark-flush const-mark-inode const-mark-mount \ const-mark-filesystem const-metadata-version const-allow \ const-deny const-audit const-nofd const-at-fdcwd \ no-shimmed-zero meta-size meta-mask-offset meta-size-native \ u64-align-native resp-size resp-size-native evt-to-str-modify evt-to-str-open-close \ evt-to-str-zero fanorule-ok fanorule-type fanorule-noparam \ fanorule-badpids create-ok mark-badfd-errno init-errno \ ctor-errno ctor-strerror gate-open gate-reason \ mark-path-ok mark-missing-errno read-errno events-mask-seen \ events-with-fd events-readlink-hit events-vers unmark-ok \ events-after-unmark mark-perm-errno \ array-no-fromstring client-uses-fromstring probe-count; do bst_skip $P/$_c "$_why" done exit 0 fi # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this every value below would be a statement about # whatever was first on sys.path. check_eq $P/version "0.2.2" "$(pv version)" check_eq $P/package-is-package "yes" "$(pv package_is_package)" check_eq $P/ext-is-extension "yes" "$(pv ext_is_extension)" check_eq $P/has-fanotify "yes" "$(pv has_fanotify)" check_eq $P/has-fanotifyclient "yes" "$(pv has_fanotifyclient)" check_eq $P/has-fanorule "yes" "$(pv has_fanorule)" check_eq $P/has-fanotifydata "yes" "$(pv has_fanotifydata)" check_eq $P/all-tuple "__version__,Fanotify,FanotifyClient,FanoRule" \ "$(pv all_tuple)" # ------------------------------------------ the constants, against the kernel --- # EVERY EXPECTED VALUE HERE IS THE KERNEL'S OWN, read out of # `include/uapi/linux/fanotify.h' at the pinned LINUX_BRINTOS_SRCREV # (64cb291e) -- not out of this library, which would agree with itself whatever # it had been compiled against. # # That distinction is load-bearing for THIS package specifically. `src/ext.c' # wraps most constants in `#ifdef' and publishes a literal 0 when its build-time # header did not define them (FAN_ENABLE_AUDIT, every FAN_REPORT_*, FAN_OPEN_EXEC # and its _PERM, the Linux 5.1 dirent events, FAN_RENAME, FAN_MARK_FILESYSTEM). # So a 0 from the module means EITHER "the kernel's value is 0" -- true of # FAN_CLASS_NOTIF and FAN_MARK_INODE, and of nothing else here -- OR "the sysroot # header the recipe compiled against was older than the kernel this image runs". # Those are a correct port and a silently wrong one, and only comparing against # the kernel's value tells them apart. check_eq $P/const-access "1" "$(pv c_fan_access)" check_eq $P/const-modify "2" "$(pv c_fan_modify)" check_eq $P/const-attrib "4" "$(pv c_fan_attrib)" check_eq $P/const-close-write "8" "$(pv c_fan_close_write)" check_eq $P/const-close-nowrite "16" "$(pv c_fan_close_nowrite)" check_eq $P/const-open "32" "$(pv c_fan_open)" check_eq $P/const-moved-from "64" "$(pv c_fan_moved_from)" check_eq $P/const-moved-to "128" "$(pv c_fan_moved_to)" check_eq $P/const-create "256" "$(pv c_fan_create)" check_eq $P/const-delete "512" "$(pv c_fan_delete)" check_eq $P/const-delete-self "1024" "$(pv c_fan_delete_self)" check_eq $P/const-move-self "2048" "$(pv c_fan_move_self)" check_eq $P/const-open-exec "4096" "$(pv c_fan_open_exec)" check_eq $P/const-q-overflow "16384" "$(pv c_fan_q_overflow)" check_eq $P/const-open-perm "65536" "$(pv c_fan_open_perm)" check_eq $P/const-access-perm "131072" "$(pv c_fan_access_perm)" check_eq $P/const-open-exec-perm "262144" "$(pv c_fan_open_exec_perm)" check_eq $P/const-event-on-child "134217728" "$(pv c_fan_event_on_child)" check_eq $P/const-rename "268435456" "$(pv c_fan_rename)" check_eq $P/const-ondir "1073741824" "$(pv c_fan_ondir)" # The two composites the header builds from the above. They are asserted because # `FAN_CLOSE' is defined in the HEADER and re-derived in `__init__.py:52' from # the library's own two values -- so a shimmed 0 in either half would produce a # composite that is wrong in a way neither half's own case would catch. check_eq $P/const-close "24" "$(pv c_fan_close)" check_eq $P/const-move "192" "$(pv c_fan_move)" check_eq $P/const-cloexec "1" "$(pv c_fan_cloexec)" check_eq $P/const-nonblock "2" "$(pv c_fan_nonblock)" # FAN_CLASS_NOTIF is legitimately 0 in the header. It is the one value in this # block a shim could imitate, and it is asserted anyway so the block is total. check_eq $P/const-class-notif "0" "$(pv c_fan_class_notif)" check_eq $P/const-class-content "4" "$(pv c_fan_class_content)" check_eq $P/const-class-pre-content "8" "$(pv c_fan_class_pre_content)" check_eq $P/const-unlimited-queue "16" "$(pv c_fan_unlimited_queue)" check_eq $P/const-unlimited-marks "32" "$(pv c_fan_unlimited_marks)" check_eq $P/const-enable-audit "64" "$(pv c_fan_enable_audit)" check_eq $P/const-report-tid "256" "$(pv c_fan_report_tid)" check_eq $P/const-report-fid "512" "$(pv c_fan_report_fid)" check_eq $P/const-report-dir-fid "1024" "$(pv c_fan_report_dir_fid)" check_eq $P/const-report-name "2048" "$(pv c_fan_report_name)" check_eq $P/const-report-dfid-name "3072" "$(pv c_fan_report_dfid_name)" check_eq $P/const-mark-add "1" "$(pv c_fan_mark_add)" check_eq $P/const-mark-remove "2" "$(pv c_fan_mark_remove)" check_eq $P/const-mark-dont-follow "4" "$(pv c_fan_mark_dont_follow)" check_eq $P/const-mark-onlydir "8" "$(pv c_fan_mark_onlydir)" check_eq $P/const-mark-ignored-mask "32" "$(pv c_fan_mark_ignored_mask)" check_eq $P/const-mark-ignored-surv-modify "64" \ "$(pv c_fan_mark_ignored_surv_modify)" check_eq $P/const-mark-flush "128" "$(pv c_fan_mark_flush)" # FAN_MARK_INODE is 0 in the header, like FAN_CLASS_NOTIF. check_eq $P/const-mark-inode "0" "$(pv c_fan_mark_inode)" check_eq $P/const-mark-mount "16" "$(pv c_fan_mark_mount)" check_eq $P/const-mark-filesystem "256" "$(pv c_fan_mark_filesystem)" # THE EVENT RECORD'S OWN VERSION BYTE. `FANOTIFY_METADATA_VERSION' is 3 in the # header, and the kernel writes it into every record's `vers' field; a consumer # that read a different number would be parsing a layout it does not know. check_eq $P/const-metadata-version "3" "$(pv c_fanotify_metadata_version)" check_eq $P/const-allow "1" "$(pv c_fan_allow)" check_eq $P/const-deny "2" "$(pv c_fan_deny)" check_eq $P/const-audit "16" "$(pv c_fan_audit)" check_eq $P/const-nofd "-1" "$(pv c_fan_nofd)" check_eq $P/const-at-fdcwd "-100" "$(pv c_at_fdcwd)" # THE SHIM CENSUS, stated once as a number. Every constant `src/ext.c' guards # with an `#ifdef' has a nonzero value in this kernel's header, so the count of # shimmed zeros must be zero. This is the summary of the block above and would # red on its own if a future sysroot header regressed -- which is the case where # fifty individual rows are harder to read than one count. check_eq $P/no-shimmed-zero "0" "$(pv shimmed_zero_count)" # ------------------------------------------------------- the struct geometry --- # `struct fanotify_event_metadata' is what `read(2)' on the group fd returns: # # __u32 event_len; __u8 vers; __u8 reserved; __u16 metadata_len; # __aligned_u64 mask; __s32 fd; __s32 pid; # # THE `__aligned_u64' IS THE WHOLE POINT ON A 32-BIT ARM. Without the alignment # attribute a naive 4-byte-aligned reading puts `mask' at offset 8 anyway on # LP64 but at offset 8 only by luck on ILP32 -- and a consumer that packed it at # 6 would read a mask made of two halves of the wrong fields, which is plausible # garbage rather than an error. So the runtime geometry is asserted rather than # inferred from a value that came back looking reasonable. check_eq $P/meta-size "24" "$(pv meta_size)" check_eq $P/meta-mask-offset "8" "$(pv meta_mask_offset)" # THE ONE THAT SAYS SOMETHING ABOUT THE ARM. The two above read the WIRE layout # (`=', standard size, no alignment), which is the same number on every platform # and therefore cannot be evidence about this one. This reads the NATURAL C # layout of the same fields and requires it to agree: no padding is inserted # anywhere the kernel does not expect it. check_eq $P/meta-size-native "24" "$(pv meta_size_native)" # The arm's natural `unsigned long long' alignment, RECORDED rather than # demanded: 8 on LP64, 4 on i386, and either leaves the 24-byte layout intact -- # which is exactly why the kernel writes `__aligned_u64' and not `__u64'. The # case reds only on a third answer, which would mean the platform is not one this # parse was reasoned about. _al=$(pv u64_align_native) case "$_al" in 4|8) bst_ok $P/u64-align-native ;; *) bst_notok $P/u64-align-native "natural u64 alignment is '$_al', neither 4 nor 8" ;; esac # `struct fanotify_response' is `__s32 fd; __u32 response;' -- 8 bytes, and the # write a permission answer is made of. check_eq $P/resp-size "8" "$(pv resp_size)" check_eq $P/resp-size-native "8" "$(pv resp_size_native)" # The library's own mask-to-name table, spot-checked. `evt_to_str(0)' is the # empty string because `_EVT_MASKS' carries a `0: ""' row that a bitwise `&' # never selects -- an implementation detail worth pinning, since a release that # dropped the row would make the same call raise instead. check_eq $P/evt-to-str-modify "modify" "$(pv evt_to_str_modify)" check_eq $P/evt-to-str-open-close "close_write|open" "$(pv evt_to_str_open_close)" check_eq $P/evt-to-str-zero "" "$(pv evt_to_str_zero)" # --------------------------------------------- the extension RUNS, not loads --- # `FanoRule_init' is a `PyArg_ParseTupleAndKeywords' over "y*|Okz*z*z*b" and # nothing else -- no syscall anywhere in it. That makes it the strongest # evidence this suite can produce on a kernel that refuses the fanotify calls: # it drives the module's argument-parsing machinery, which is exactly where # BRINTOS-2504's two missing `_SizeT' names lived. A module that had merely # loaded would pass `import' above; only this passes as well. check_eq $P/fanorule-ok "yes" "$(pv fanorule_ok)" check_eq $P/fanorule-type "FanoRule" "$(pv fanorule_type)" # The REFUSAL paths, which are the half a regressed parser would get wrong in the # permissive direction: a rule with no rule parameter, and one whose `pids' is # not a set/list/tuple. Both are documented in `FanoRule__doc__'. check_eq $P/fanorule-noparam "ValueError" "$(pv fanorule_noparam)" check_eq $P/fanorule-badpids "TypeError" "$(pv fanorule_badpids)" # ================================================================= THE GATE === # `ext.create()' is a calloc and three field stores -- no syscall -- so it # separates "the module cannot allocate" from "the kernel refuses". check_eq $P/create-ok "yes" "$(pv create_ok)" # THE TICKET'S QUESTION, ANSWERED BY ERRNO, TWICE. # # `mark-badfd-errno' is `fanotify_mark' on a context whose `fano_fd' is still -1 # -- exactly what `Fanotify.__init__' does at `__init__.py:162' before it ever # calls init, and a call the library TOLERATES an EBADF from. So the errno here # discriminates three states that would otherwise all read as "fanotify is # broken": # # EBADF the syscall is wired and rejected the fd -- fanotify_mark IS # reachable, and this suite's skips below would be wrong # ENOSYS the number is not dispatched at all # EPERM the syscall is wired and the caller lacks CAP_SYS_ADMIN # # ENOSYS is what this kernel gives, and the mechanism is read out of the tree # rather than guessed: `arch/wasm32/kernel/syscall_table.c' has no row for 300 or # 301, so `sys_call_table' holds NULL there and `do_syscall' returns -ENOSYS on # the NULL check. The UAPI numbers exist and the subsystem is compiled in # (CONFIG_FANOTIFY=y); only the dispatch row is missing. # # EPERM WOULD BE A DIFFERENT TICKET. The ticket predicted it -- fanotify needs # CAP_SYS_ADMIN, CONFIG_USER_NS is off and everything runs as global root under # one shared cred -- and it is NOT what happened. Pinning ENOSYS here rather than # "not zero" is what makes the day the table rows land visible: this case reds, # and the red says the platform moved. check_eq $P/mark-badfd-errno "ENOSYS" "$(pv mark_badfd_errno_name)" check_eq $P/init-errno "ENOSYS" "$(pv init_errno_name)" # THE SAME QUESTION THROUGH THE CLASS THE TICKET NAMES. `Fanotify()' runs the # mark/init pair above, and upstream has a FIRST-CLASS path for this exact # platform state: `__init__.py:165-167' appends `: No fanotify!' to the strerror # when the errno is ENOSYS. So the port does not merely fail here -- it reports # this kernel's state in the library's own documented words, which is the # difference between a broken port and a working one on a kernel that says no. check_eq $P/ctor-errno "ENOSYS" "$(pv ctor_errno_name)" check_out $P/ctor-strerror "No fanotify!" sh -c "grep '^ctor_strerror=' '$OUT'" # AN INDEPENDENT SECOND READING, so the errno above is not taken on the # extension's word alone. `raw-init-errno' goes through ctypes to # `syscall(300, 0, 0)' with no pyfanotify code in the path at all. ctypes needs # libffi and a working dlopen on this arm, so it may legitimately be # unavailable -- and the case accepts EITHER the same errno OR an explicit # `unavailable', because a detector that cannot run must say so rather than have # its silence read as a measurement. What it must never do is disagree. _raw=$(pv raw_init_errno_name) case "$_raw" in ENOSYS) bst_ok $P/raw-init-errno ;; unavailable|ImportError|OSError|AttributeError|TypeError) bst_skip $P/raw-init-errno "ctypes route unavailable: $_raw" ;; *) bst_notok $P/raw-init-errno \ "independent syscall(300) reading disagrees with the extension: $_raw" ;; esac # ==================================================== BEHIND THE CLOSED GATE === # Everything the ticket asks for beyond this point needs a group fd, and there is # none. The probe says so in one place and this is where that is turned into a # report: `gate-open' is the case that carries the fact, and every case below is # SKIPPED with the errno attached rather than red. A skip with an attributed # cause is the honest report of a platform boundary; a wall of reds would bury # the one line that matters and would make this port look broken when it is the # kernel's dispatch table that is incomplete. _gate=$(pv gate_open) if [ "$_gate" = "yes" ]; then bst_ok $P/gate-open # THE GATE OPENED. That is a real change in the platform and the suite must not # pass silently through it: these expectations were written against a kernel # that refuses, and the cases above (`init-errno', `mark-badfd-errno') will # already have reddened to say so. What follows is the ticket's event flow, # measured for the first time. # # HOW MUCH OF THIS BRANCH HAS EVER RUN, stated plainly because a branch that # cannot execute on the target is a PLAN rather than a guard, and a green suite # must not be read as having exercised it. On 2026-09-22 the block was run # verbatim on an x86-64 control against an unprivileged FAN_CLASS_NOTIF + # FAN_REPORT_FID group -- the only group that account could open -- and every # statement executed. Eight of the ten literals below were confirmed there: # `mark-path-ok' yes, `mark-missing-errno' ENOENT, `read-errno' OK, # `events-vers' 3, `unmark-ok' yes, `events-after-unmark' 0, # `mark-perm-errno' EINVAL, and `events-mask-seen' for that run's own two-event # mask (10 = FAN_MODIFY|FAN_CLOSE_WRITE, against the 43 this suite's four-event # mask asks for). The two that could NOT be confirmed are named at their own # cases. check_eq $P/mark-path-ok "yes" "$(pv mark_path_ok)" check_eq $P/mark-missing-errno "ENOENT" "$(pv mark_missing_errno_name)" check_eq $P/read-errno "OK" "$(pv read_errno_name)" # 43 = FAN_OPEN|FAN_ACCESS|FAN_MODIFY|FAN_CLOSE_WRITE = 32|1|2|8. check_eq $P/events-mask-seen "43" "$(pv events_mask_seen)" # THESE TWO ARE THE ONLY LITERALS IN THIS BRANCH NOT VALIDATED AGAINST A REAL # KERNEL -- see the branch header. An unprivileged group is FANOTIFY_UNPRIV and # reports neither fd nor pid, so the x86-64 control could reach every other # case here and could not reach these. They are the ticket's central property # and are written from the man page. check $P/events-with-fd test "$(pv events_with_fd)" -gt 0 # THE PROPERTY THAT DISTINGUISHES FANOTIFY FROM INOTIFY: the record carries an # open descriptor for the object that caused the event, so its # `/proc/self/fd/' readlink names the file rather than a watch-relative # name. This is the case most likely to break on a fresh arm. check_eq $P/events-readlink-hit "yes" "$(pv events_readlink_hit)" check_eq $P/events-vers "3" "$(pv events_vers)" check_eq $P/unmark-ok "yes" "$(pv unmark_ok)" check_eq $P/events-after-unmark "0" "$(pv events_after_unmark)" # THE PERMISSION CLASS IS EXPECTED TO STAY REFUSED even once the dispatch rows # land, and EINVAL has TWO routes to it here -- worth separating, because the # obvious one is not the only one and a reader who assumes it would draw the # wrong conclusion from a later green: # # 1. THE CONFIG. FAN_OPEN_PERM needs CONFIG_FANOTIFY_ACCESS_PERMISSIONS, # which `depends on SECURITY'; neither is in the wasm32 defconfig, so # `fanotify_mark' drops FANOTIFY_PERM_EVENTS from `valid_mask' at # `fs/notify/fanotify/fanotify_user.c:1735' and rejects the mask. # 2. THE GROUP'S CLASS. A perm mask is refused on a FAN_CLASS_NOTIF group # whatever the config says. Measured on an x86-64 control whose kernel # HAS the option: an unprivileged FAN_CLASS_NOTIF group still answered # EINVAL. # # This suite's group is FAN_CLASS_CONTENT (pyfanotify's `_INIT_FLAGS'), so # route 2 does not apply to it and route 1 is what a red would be about. The # distinction is recorded because enabling the option is the change this case # is meant to make visible, and it can only do that if the cause is named # correctly. check_eq $P/mark-perm-errno "EINVAL" "$(pv mark_perm_errno_name)" else bst_notok $P/gate-open "$(pv gate_reason)" _why="no fanotify group: $(pv gate_reason) -- arch/wasm32 sys_call_table has no row for __NR_fanotify_init 300 / __NR_fanotify_mark 301, so the numbers are undispatched although CONFIG_FANOTIFY=y" for _c in mark-path-ok mark-missing-errno read-errno events-mask-seen \ events-with-fd events-readlink-hit events-vers unmark-ok \ events-after-unmark mark-perm-errno; do bst_skip $P/$_c "$_why" done fi # --------------------------------------------- the upstream py3.12 break ------ # `FanotifyClient._recv_data' calls `array.array.fromstring' (`__init__.py:487'), # which CPython REMOVED in 3.9. So even on a kernel that answered every syscall # above, the client's event path -- the SCM_RIGHTS half the ticket names -- would # raise AttributeError on this interpreter. It is upstream pyfanotify's defect, # not this platform's, and per `no-public-upstream-reports' we do not report it # outward; it is pinned here so that a reader does not conclude the port is whole # the day the dispatch rows land. check_eq $P/array-no-fromstring "no" "$(pv array_has_fromstring)" check_eq $P/client-uses-fromstring "yes" "$(pv client_uses_fromstring)"