. "$BST_LIB" # BST-TIMEOUT: 300 # # libnfs 5.0.3 (meta-oe recipes-connectivity/libnfs, SRCREV 4379837) -- the # userspace NFS client library. An RPC/XDR codec, a URL parser, a connection # state machine, and both a synchronous and an asynchronous API over NFSv3 and # NFSv4. # # THE TICKET'S CRITERION IS OFFLINE, AND THAT IS A CHOICE ABOUT WHAT CAN BE # ASSERTED HERE RATHER THAN A GAP. Three halves, and none of them needs an NFS # server: # # 1. nfs_parse_url_full (and its _dir and _incomplete siblings) over # well-formed and malformed URLs, against the expected fields and errors; # 2. the nfs_init_context / nfs_destroy_context lifecycle, and the context # getters the URL arguments drive; # 3. the SYNCHRONOUS API against an unreachable loopback address, returning # the documented error inside its timeout. # # No NFS server is on this image and none is planned; when one lands this suite # can be extended with a live mount, readdir and read on loopback. It does NOT # depend on one to pass, and nothing below should be rewritten to. # # AND NONE OF IT NEEDS A THREAD, which is why this package is in this batch at # all -- and the reason is structural rather than lucky. libnfs does have a # multithreading arm (lib/multithreading.c, README.multithreading, ~110 # HAVE_MULTITHREADING sites across lib/), but that symbol is defined ONLY by the # autoconf build, under --enable-multithreading. cmake/config.h.cmake does not # carry it at all, and this recipe is a CMake build -- so every one of those # sites is compiled out and no pthread is linked. On a platform that refuses a # thread-mode clone (brintos/linux-in-the-browser#85) that is what makes the # package exercisable at all, and `libnfs/no-pthread' below is what says it is # still true. # # THE CRITERION IS DRIVEN THROUGH ONE PROGRAM, AND THE PACKAGE SHIPS NONE OF ITS # OWN. ENABLE_UTILS, ENABLE_EXAMPLES and ENABLE_TESTS all default OFF upstream # and this port leaves them there: all four utils -- nfs-cat, nfs-ls, nfs-cp, # nfs-stat -- open a connection to a live NFS server within a few lines of # main(), so on this image they could only fail to connect. So the deliverable # is an archive and ten headers, and the only way to assert anything about it is # to be a CONSUMER of them. Everything libnfs-probe calls is declared EXTERN in # the installed , so a red from it is a statement about what the # package installs. The probe is built in-recipe against this build's own # archive (libnfs_%.bbappend) and carries the reasoning for each assertion in # its own header comment. # # EVERY EXPECTATION IS PINNED FROM AN x86-64 NATIVE CONTROL, not guessed: the # same tree at the same SRCREV (4379837), with the two meta-oe patches applied # and configured with the same BUILD_SHARED_LIBS=OFF this port sets, built # natively and driven through all three probe modes on 2026-08-20. All 74 cases # were green there, `url', `ctx' and `noconnect' each returning 0. So a red here # is a statement about this platform. # # TWO UPSTREAM DEFECTS ARE PINNED AT THEIR MEASURED (BROKEN) VALUES, and a # reader must not mistake them for reds -- see section E. Both reproduce on # x86-64, so neither is a wasm32 artifact. # # THE PROBE IS EXEC'D IN PLACE, never copied into /tmp first. It is a # whole-program static link of libnfs; staging one through the tmpfs heap # fragments it past binfmt_wasm's WASM32_IMG_MAX_SEGS=1024 cap and the exec is # refused -ENOMEM (brintos/linux#44). 88-nopoll measured that both ways on target. require /usr/libexec/libnfs-tests/libnfs-probe libnfs/present PROBE=/usr/libexec/libnfs-tests/libnfs-probe # The library THIS arm shipped. cmake builds one kind at a time, so a converted # build has no archive and every question below is answered against the shared # object instead. grep(1) works on either for the reason section B gives: a # symbol name is a plain string in both containers, and the image ships no ar. # # bst_ps_lib rather than a loop of our own: it prefers the archive where both # shipped, so the static arm keeps reading exactly the bytes it always read, and # the suite-archive gate reads a row handed to it as arm-aware (BRINTOS-1930). NFSLIB=$(bst_ps_lib /usr/lib/libnfs.a /usr/lib/libnfs.so /usr/lib/libnfs.so.*) : "${NFSLIB:=/usr/lib/libnfs.a}" # =========================================================================== # A. THE DELIVERABLE ITSELF # # The archive and the public headers, asserted before anything is run against # them, so "the port did not install" stays distinguishable from "the port does # not work". check libnfs/archive test -f "$NFSLIB" check libnfs/header-main test -f /usr/include/nfsc/libnfs.h check libnfs/header-raw test -f /usr/include/nfsc/libnfs-raw.h check libnfs/header-zdr test -f /usr/include/nfsc/libnfs-zdr.h # The seven per-protocol raw headers. They are installed by an explicit # install(FILES) list in the top-level CMakeLists rather than by the # install(DIRECTORY include/nfsc) above it, so they are a separate thing that can # separately stop happening -- and they are what a caller of the raw RPC layer # (rather than the convenience API) actually includes. for h in mount nfs nfs4 nlm nsm portmap rquota; do check "libnfs/header-raw-$h" test -f "/usr/include/nfsc/libnfs-raw-$h.h" done # THE PORT'S CENTRAL BUILD DECISION, ASSERTED ON THE IMAGE. This distro links no # shared objects, so BUILD_SHARED_LIBS is denied. Upstream defaults it ON with # SOVERSION 11.2.0, so a red means the switch stopped applying and the package # grew a shared object that could not be loaded here anyway. check_eq libnfs/no-shared-library "" "$(ls /usr/lib/libnfs.so* 2>/dev/null)" # THE ARCHIVE IS SELF-CONTAINED, WHICH IS THE OTHER HALF OF THAT DECISION. # libnfs's seven protocol directories are CMake OBJECT libraries spliced into the # `nfs' target, so libnfs.a must carry their symbols -- a consumer links -lnfs # and nothing else. Had they been ordinary static libraries, the PRIVATE linkage # lib/CMakeLists.txt uses would not have merged them and every nfs_mount call # would be an undefined symbol. One symbol per protocol directory, so a # directory that stopped being merged is named rather than lumped in. # # grep(1) over the archive rather than ar(1) or nm(1): the image ships neither # binutils nor a wasm32-aware ar, and elfutils' eu-ar is written against ELF # members while these are wasm objects. The `ar' container format is # architecture-neutral and a symbol name is a plain string in it, so a binary # grep answers the question with a tool the image is certain to have. for sym in rpc_mount3_null_async rpc_nfs3_null_async rpc_nfs4_null_async \ rpc_nlm4_null_async rpc_nsm1_null_async rpc_pmap2_null_async \ rpc_rquota1_null_async; do check_eq "libnfs/archive-has-$sym" "yes" \ "$(grep -qa "$sym" "$NFSLIB" && echo yes)" done # AND THE SYNCHRONOUS API IS IN IT. libnfs-sync.c is where nfs_mount and the # rest of the blocking interface live, and section D drives it -- so its # presence is asserted before that section rather than inferred from it. check_eq libnfs/archive-has-sync "yes" \ "$(grep -qa nfs_mount "$NFSLIB" && echo yes)" # NO PTHREAD IS LINKED. The structural fact from the header comment, asserted # rather than assumed: HAVE_MULTITHREADING is an autoconf-only symbol, so a CMake # build compiles the whole multithreading arm out. A red means the arm came back # -- which on a platform that refuses a thread-mode clone # (brintos/linux-in-the-browser#85) turns this package from portable into a # runtime failure, and would need diagnosing before anything else here is read. # nfs_mt_get_tid is the arm's one non-static entry point (multithreading.c), so # it is a symbol rather than a guess; pthread_create is the dependency the arm # would drag in. Both must be absent. Asserted separately: the first says the # arm was compiled out, the second that nothing else on the link line wants # threads either. check_eq libnfs/no-multithreading-arm "" \ "$(grep -oa 'nfs_mt_get_tid' "$NFSLIB" 2>/dev/null | head -1)" check_eq libnfs/no-pthread "" \ "$(grep -oa 'pthread_create' "$NFSLIB" 2>/dev/null | head -1)" # THE PACKAGING METADATA. libnfs.pc is what a downstream recipe finds the package # through, and it is generated at configure time from the resolved install paths # -- so it is not merely another installed file, it is what says those paths came # out right. check libnfs/pkgconfig test -f /usr/lib/pkgconfig/libnfs.pc check_eq libnfs/pkgconfig-names-lib "yes" \ "$(grep -q -- '-lnfs' /usr/lib/pkgconfig/libnfs.pc && echo yes)" # THE GENERATED CMAKE PACKAGE CONFIG IS DELIBERATELY ABSENT. The stock recipe # removes libnfs-config.cmake in its own do_install:append, and it is right to: # under BUILD_SHARED_LIBS=OFF the export set names the seven OBJECT-library # targets, which no installed tree can resolve, so a find_package() consumer # would fail on a file that describes targets that were never installed. The # version file and FindNFS.cmake are shipped and are what a consumer uses. check_eq libnfs/no-broken-cmake-config "" "$(ls /usr/lib/cmake/libnfs/libnfs-config.cmake 2>/dev/null)" check libnfs/cmake-version-file test -f /usr/lib/cmake/libnfs/libnfs-config-version.cmake check libnfs/cmake-findnfs test -f /usr/lib/cmake/libnfs/FindNFS.cmake # AND THE UTILS ARE DELIBERATELY ABSENT TOO, which is worth a case rather than a # silence: their absence is a decision this port made (see the header comment), # not an accident, and if a future recipe change turned ENABLE_UTILS on then four # programs that can only fail to connect would start shipping unnoticed. for u in nfs-cat nfs-ls nfs-cp nfs-stat; do check_eq "libnfs/no-util-$u" "" "$(ls "/usr/bin/$u" 2>/dev/null)" done check libnfs/probe-installed test -x "$PROBE" # score : ok iff the probe printed " ok". Defined once and # given the captured output by the caller, because all three modes below score # the same way and a per-mode copy is three chances to diverge. score() { _out=$1; _id=$2 _line=$(printf '%s\n' "$_out" | grep "^$_id " | head -1) case "$_line" in "$_id ok"*) bst_ok "libnfs/$_id" ;; "$_id FAIL"*) bst_notok "libnfs/$_id" "$(_bst_1line "$_line")" ;; *) bst_notok "libnfs/$_id" "the probe printed no line for this id -- $(_bst_1line "$_out")" ;; esac } # =========================================================================== # B. THE TICKET'S FIRST CRITERION -- THE URL PARSER # # The probe's `url' mode drives nfs_parse_url_full, _dir and _incomplete over # well-formed and malformed URLs. Each assertion is re-scored INDIVIDUALLY below # rather than collapsed into the exit status, because "the port was left glued to # the hostname" and "a malformed URL was accepted" are different findings and a # single green would hide which of them held. URL=$("$PROBE" url 2>&1); URL_RC=$? if bst_execfail "$URL"; then bst_unrun libnfs/url "$URL" else # The well-formed case, field by field. url/full-* is the canonical # nfs://server/export/file; the FQDN and dotted-quad arms say the same thing # for the two host forms a real caller passes. for id in url/full-parsed url/full-server url/full-path url/full-file \ url/fqdn-server url/fqdn-path url/fqdn-file url/ipv4-server; do score "$URL" "$id" done # A PORT IN THE AUTHORITY IS CONSUMED RATHER THAN LEFT IN `server'. libnfs # exposes no getter for the parsed NFS port, so what is assertable is the half # that reaches the caller -- and a red means a consumer would try to resolve a # hostname with ":2049" glued to it. for id in url/port-stripped url/port-path; do score "$URL" "$id" done # THE TWO SHAPES THAT SURPRISE A READER OF THE HEADER, pinned so they stay # true: `file' keeps its leading slash, and a one-component path parses to an # EMPTY path rather than to NULL or to the component itself. A consumer that # tests `path' for NULL rather than for emptiness behaves differently here. for id in url/one-component-path url/one-component-file; do score "$URL" "$id" done # THE %-UNESCAPE, which is the only string decoding the parser does itself. # unescape-space is the positive case; the other two are the bounds. On this # target a read past the end of the buffer is a silent read of adjacent wasm # linear memory rather than a fault, so a trailing bare "%" -- where the # decoder must not look at strp[2] -- is worth its own case. for id in url/unescape-space url/unescape-bad-left-alone \ url/unescape-trailing-percent; do score "$URL" "$id" done # THE URL ARGUMENTS, WHICH ARE SIDE EFFECTS ON THE CONTEXT rather than fields # in the returned struct: the parser calls setters as it walks the query # string. `version' is the argument with a public getter, so it is the one that # can be asserted end to end; arg-two-* says the '&' loop runs past the first. for id in url/arg-version-default url/arg-version-applied url/arg-not-in-file \ url/arg-two-survives url/arg-two-file; do score "$URL" "$id" done # THE THREE PARSE MODES ARE DISTINCT. _dir does not split the file off; the # _incomplete cases are the section-C refusals re-run in the mode whose # contract is to tolerate them, so a red separates "the tolerant mode stopped # tolerating" from "the strict mode stopped refusing". for id in url/dir-path url/dir-file-null \ url/incomplete-no-path url/incomplete-no-path-path \ url/incomplete-bare url/incomplete-bare-server \ url/incomplete-rootish-server; do score "$URL" "$id" done # =========================================================================== # C. THE MALFORMED URLS, AND THE ERRORS THEY MUST SET. # # Two cases per URL, deliberately: that the parser refused it, and that # nfs_get_error() names WHY. The second is not decoration -- libnfs # distinguishes "this is not an nfs:// URL" from "the server is missing" from # "the URL is incomplete", and a caller reporting the failure to a user needs # those to stay distinct. A single "it returned NULL" case would stay green if # every refusal collapsed onto one message. # # The expected strings are pinned verbatim from lib/libnfs.c and confirmed on # the native control -- including that one of them carries a trailing full stop # and the others do not. That inconsistency is matched exactly rather than # tidied, because the point of the case is to detect change. for id in url/reject-scheme url/reject-scheme-err \ url/reject-empty url/reject-empty-err \ url/reject-no-server url/reject-no-server-err \ url/reject-query-only url/reject-query-only-err \ url/reject-no-path url/reject-no-path-err; do score "$URL" "$id" done # nfs_destroy_url(NULL) is the natural cleanup path on every refusal above, so # a caller written the obvious way calls it. On this target the alternative is # not a signal but a trap in linear memory. score "$URL" url/destroy-null-safe # =========================================================================== # E. TWO UPSTREAM DEFECTS, PINNED AT THEIR MEASURED (BROKEN) VALUES. # # READ THESE BEFORE CALLING EITHER A REGRESSION. Both reproduce on x86-64 with # gcc, so neither is a wasm32 or a static-link artifact, and a red means the # BEHAVIOUR CHANGED -- most likely because upstream fixed it, which is good # news and means the pin should be updated. We file nothing against # third-party public repositories # (wasm_linux docs/conventions/no-public-upstream-reports.md). # # 1. A ONE-CHARACTER HOSTNAME IS SILENTLY ERASED. nfs_parse_url ends with # `if (urls->server && strlen(urls->server) <= 1) { free(...); server = # NULL; }'. The intent is to normalise the EMPTY server that the # incomplete-parse arm can produce; "<= 1" also catches a server of # length one. So "nfs://h/export/file" returns a NON-NULL struct, with no # error set, and server == NULL. The rest of the URL parses correctly, # which is what makes it quiet. "< 1" would be correct. Single-label # hostnames are legal and do occur (a short internal alias, an /etc/hosts # entry), and a caller then connects to nothing or dereferences NULL. # url/two-char-server-kept is here to show it is an off-by-one rather # than a wholesale failure on short hostnames. # # 2. AN UNSUPPORTED version= IS ACCEPTED BY THE PARSER. nfs_set_context_args # returns -1 for version=99 and nfs_parse_url DISCARDS that return value # (the call is a bare statement), so the URL parses fine and the context # keeps version 3 while an error string has been set behind the caller's # back. A caller that trusts the URL it was handed therefore silently # talks NFSv3 to a server it asked for v99 of. for id in url/one-char-server-upstream-bug url/one-char-server-path-intact \ url/two-char-server-kept \ url/arg-bad-version-tolerated-upstream-bug; do score "$URL" "$id" done # THE MODE RAN TO THE END. Without this the runner counts the lines it saw and # reports a complete green, so a probe that died half way through would look # like a pass with fewer cases. Absence of this marker is the only signal. score "$URL" url/complete check_eq libnfs/url-exit-status 0 "$URL_RC" fi # =========================================================================== # F. THE TICKET'S SECOND CRITERION -- THE CONTEXT LIFECYCLE # # "create/destroy" is one line of code, so this mode is built around the two # things that can actually go wrong with it on a new target: the allocation # itself under repetition, and whether the getters report the documented # defaults rather than uninitialised memory. The latter is the more valuable # half here -- a struct being read at the wrong offset on a new ABI shows up as a # wrong default long before it shows up as a crash. CTX=$("$PROBE" ctx 2>&1); CTX_RC=$? if bst_execfail "$CTX"; then bst_unrun libnfs/ctx "$CTX" else score "$CTX" ctx/init-nonnull # THE DOCUMENTED DEFAULTS ON A FRESH CONTEXT. NFSv3 is the documented default; # timeout -1 is "no timeout"; fd -1 rather than 0 matters because 0 is a valid # descriptor and a context reporting it would make a caller poll(2) on stdin; # readmax/writemax are learned from the server at mount time, so before a mount # they must be 0 and not a compiled-in guess a caller would size a buffer from. for id in ctx/default-version ctx/default-timeout ctx/unconnected-fd \ ctx/unconnected-readmax ctx/unconnected-writemax; do score "$CTX" "$id" done # nfs_get_error() ON A FRESH CONTEXT RETURNS "" AND NEVER NULL. Documented to # be safe to print, and every red in this suite that reports an error string # depends on it -- so it is asserted rather than assumed. for id in ctx/fresh-error-nonnull ctx/fresh-error-empty; do score "$CTX" "$id" done # THE TIMEOUT ROUND TRIP -- the value section G depends on. If the setter does # not take, that section's bound is not being enforced and its green means # nothing. score "$CTX" ctx/timeout-roundtrip # THE VERSION SETTER, INCLUDING ITS REFUSAL. set(4) must take; set(2) must be # refused, must say so, and must LEAVE THE PREVIOUS VALUE ALONE -- a rejected # setter that had already written would leave the context claiming a version # libnfs cannot speak. for id in ctx/set-version-4-rc ctx/set-version-4-took \ ctx/set-version-2-refused ctx/set-version-2-left-alone \ ctx/set-version-2-err; do score "$CTX" "$id" done # THE LIFECYCLE UNDER REPETITION, which is the half of "create/destroy" that # can actually fail. One iteration proves the code path; a thousand is what # catches a leak or a double free, and on this target it is also what would # catch a heap that fragments. Every context is fully initialised (init # allocates its rpc context and buffers) and then destroyed, so the loop is not # a no-op. for id in ctx/destroy-returns ctx/lifecycle-1000 ctx/destroy-unconnected; do score "$CTX" "$id" done # A PROPERTY OF THE SHIPPED HEADER, recorded rather than hidden: libnfs.h # declares struct nfs_stat_64 with three struct timeval members and does not # include the header that defines it, so a consumer including only # fails to compile. The probe includes first; this # case is how that requirement stays visible to whoever writes the next # consumer. score "$CTX" ctx/header-needs-sys-time score "$CTX" ctx/complete check_eq libnfs/ctx-exit-status 0 "$CTX_RC" fi # =========================================================================== # G. THE TICKET'S THIRD CRITERION -- THE SYNCHRONOUS API, UNREACHABLE ADDRESS # # Two distinct claims, and the second is the one that needs a clock: the call # must return the documented error, and it must do so INSIDE ITS TIMEOUT. A # library that returned the right error after blocking forever would satisfy a # return-value assertion and be useless. # # WHY LOOPBACK RATHER THAN A ROUTABLE ADDRESS. 127.0.0.1 with no NFS server on # it refuses the connection immediately (ECONNREFUSED) rather than dropping the # SYN, so the case is deterministic, fast, and does not require the target to # have outbound networking at all -- which matters here, because the browser # guest's networking is not something this suite should depend on. A blackholed # address would exercise the timeout path instead, but its duration would be a # property of the platform's TCP stack rather than of libnfs. # # The probe sets autoreconnect to 0 first, and that is load-bearing: libnfs's # default is to retry a failed session, so without it the call is entitled to # keep retrying and the "inside its timeout" half could not be asserted at all. NOC=$("$PROBE" noconnect 2>&1); NOC_RC=$? if bst_execfail "$NOC"; then bst_unrun libnfs/noconnect "$NOC" else # THE CRITERION: an error, not a success and not a hang -- and bounded. for id in noconnect/mount-failed noconnect/within-timeout; do score "$NOC" "$id" done # THE DOCUMENTED ERROR IS SET, AND ITS TEXT. A failure with no error string # leaves a caller with nothing to report. The exact text is pinned because it # says WHICH LAYER reported the refusal -- libnfs surfaces it through its # service loop -- so a change there is visible even while the criterion holds. for id in noconnect/error-set noconnect/error-text; do score "$NOC" "$id" done # NO DESCRIPTOR IS LEFT BEHIND, and the failure is repeatable on the same # context, and the context is still destroyable afterwards. These three are the # states a real caller leaves the context in -- retry, then give up and clean # up -- and each is where a half-torn-down connection would fault. for id in noconnect/no-fd-left noconnect/repeatable \ noconnect/destroy-after-failure; do score "$NOC" "$id" done # A SECOND ADDRESS on a fresh context, so the result is not a property of one # address; and THE SAME THING WITH THE PORTMAPPER BYPASSED. Without nfsport and # mountport set the mount first talks to the portmapper on port 111; with them # set it goes straight to 2049/20048. Both are refused here, but they are # different code paths through libnfs, and asserting only the first would leave # the direct path unexercised on this target. for id in noconnect/second-address noconnect/no-portmap; do score "$NOC" "$id" done # THE EXACT RETURN VALUE, PINNED SEPARATELY FROM THE CRITERION ABOVE. -5 is # what the native control returns. A change means the failure is being # reported through a different path, which is worth seeing -- but read it as # information, not as a defect, while noconnect/mount-failed is green. score "$NOC" noconnect/mount-rc-value # AND FAST, scored separately from `within-timeout' for the same reason: the # criterion is bounded, this is the measured shape. Native control ~0.002s. # A red here with within-timeout green means the refusal is arriving far slower # on target than natively -- worth diagnosing, not a criterion failure. score "$NOC" noconnect/prompt score "$NOC" noconnect/complete check_eq libnfs/noconnect-exit-status 0 "$NOC_RC" fi