QVAC-25104 fix: name fabric's ELF version node only where it owns the runtime - #4566
Merged
Merged
Conversation
… runtime 0.16.0 named the version node for every ELF target, so the Android module defined QVAC_FABRIC_ABI_1 and stamped it on every export. Consumers then recorded a DT_VERNEED on it, which bionic refuses on dlopen: every addon failed to load with `ADDON_NOT_FOUND: Cannot find addon '.'` before any model work. The name exists to keep the host's libstdc++ from answering for the C++ ABI this module exports, so it now follows the same condition as the ABI block it protects -- the one Linux link that embeds libc++. Android links libc++_shared.so and the ASan build links libc++.so.1, and both export no runtime for a consumer to pin to. symbols.map therefore ships the node anonymous and those links pass it to the linker unchanged; the Linux link splices in the ABI block and stamps the name from CMakeLists.txt, which is now its only definition. The export surface and its `local: *;` narrowing are unchanged on every platform, and the generated Linux script is byte-identical to 0.16.0's. Verified against a local arm64-android link: no .gnu.version_d section, and only bionic's own LIBC version needs, matching 0.15.0. The same tree built from main defines the node on 4296 exports. Also registers the script as a LINK_DEPENDS on every ELF path. It was only tracked on the path that generates one, so editing symbols.map did not relink the Android or ASan module.
Contributor
Review StatusCurrent Status: ❌ PENDING Pending reviews: Needs 1 Management or Team Lead, and 1 more from Management, Team Lead, or Member. |
Contributor
License compliance — cleanNo new dependency license findings in this PR. Warn-only (shadow) mode — this check does not block merges yet. Updated automatically by the canonical license compliance workflow. NOTICE presence (advisory)Missing NOTICE (advisory, does not block):
|
gianni-cor
previously approved these changes
Sep 17, 2026
GustavoA1604
previously approved these changes
Sep 17, 2026
The changelog and symbols.map claimed Android's loader refuses versioned imports. It does not: bionic has had symbol versioning since API 23, and the app targets SDK 36. What the run establishes is that the node's name is the only ELF difference between a consumer that loads and one that does not. The loader's own objection is not in evidence -- bare walks its addon candidates and discards each dlerror, so the failing run's logcat carries no linker diagnostic for our libraries, only ADDON_NOT_FOUND. Both plausible bionic paths are recorded instead, since either ends in a failed dlopen: it resolves a version need through the DT_SONAME of the dependency that declares it, and on device the addon's fabric dependency is not a file -- the APK stages it under another name and it resolves only because bare already loaded it. So bionic either finds no dependency matching the verneed, or finds no such version there and demotes the requirement to unversioned definitions, which no export has under a named node. The fix is unchanged and rests on the name being inert on those links.
jpgaribotti
dismissed stale reviews from GustavoA1604 and gianni-cor
via
September 17, 2026 21:34
7319b59
gianni-cor
approved these changes
Sep 17, 2026
iancris
approved these changes
Sep 18, 2026
This was referenced Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 What problem does this PR solve?
barereportsAddonError: ADDON_NOT_FOUND: Cannot find addon '.'from the addon'sbinding.js, before any model work, so the failure hits the firstloadin every suite.QVAC_FABRIC_ABI_1and stamps it on all 4296 exports, and a consumer linked against it records aDT_VERNEEDon that version — 153 pinned imports inllm-llamacpp's case. With thatDT_VERNEEDpresent the addon'sdlopenfails, and bare's resolver reports the addon as missing.DT_SONAMEof the dependency that declares it, and on device an addon's fabric dependency is not a file at all: the APK stages fabric aslibqvac__fabric.0.16.0.sowhile theDT_NEEDEDand theverneedboth nameqvac__fabric@0.bare, which resolves only because bare has already loaded it. Either bionic finds no dependency matching theverneedand fails thedlopenoutright, or it finds no such version there and demotes the requirement to unversioned definitions — which, under a named node, no export has. Both end in a faileddlopen. Bare walks its candidates and discards eachdlerror, so the loader's own objection is not recoverable from the run: the full logcat has no linker diagnostic for our libraries, onlyADDON_NOT_FOUND.tts-ggml0.2.2 hit the same signature from unresolvableUNDsymbols, which is what fixes its meaning as "thedlopenfailed".@qvac/embed-llamacpp@0.41.1(fabric 0.15.0) passes the Android suite, and0.41.2(fabric 0.16.0, no source change to the addon's load path) fails identically to thellm-llamacppmigration branch in QVAC-22415 feat[api]: migrate @qvac/llm-llamacpp to the shared @qvac/fabric runtime #4454, which is where this surfaced. The node's name is the only ELF-visible difference between the two fabrics on Android: diffed with comments stripped, 0.15.0's and 0.16.0's version scripts differ by one line,{againstQVAC_FABRIC_ABI_1 {, with an identical export list.on-pr-fabric.📝 How does it solve it?
libstdc++from answering for the C++ ABI fabric exports — so it now follows the same condition as the ABI block it protects: the one Linux link that embeds libc++. Android linkslibc++_shared.soand the ASan build linkslibc++.so.1; neither exports a runtime for a consumer to pin to, so there theDT_VERNEEDguarded an export set that does not exist and only cost thedlopen. The fix rests on that — the name buys nothing on those links — rather than on which of the two loader paths above we hit.symbols.mapships the node anonymous, and those links pass it to the linker unchanged. The Linux link is the one that transforms it: the ABI block is spliced in at its marker and the name stamped at its own, both plain string replacements with a fail-on-miss guard, so dropping either is a configure error rather than a module that links and runs with nothing pinned.QVAC_FABRIC_ABI_1was declared in two places that a configure-time assertion kept in agreement. CMakeLists.txt is now its only definition — the script and the package config are both stamped from it — so there is nothing left to drift.local: *;narrowing are unchanged on every platform: only the node header differs between the two links. The generated Linux script is byte-identical to 0.16.0's, so nothing about the Linux fix moves.LINK_DEPENDSon every ELF path, matching howqvac-addon.cmakehandlesaddon-symbols.map. It was only tracked on the path that generates a script, so editingsymbols.mapdid not relink the Android or ASan module — which is how I noticed, doing the A/B below.package.jsongoes to0.16.1and the changelog entry is written as## [0.16.1], so the release can be cut straight from arelease-fabric-0.16.1branch. The lockfile needs no regeneration — consumers resolve fabric aslink:../fabricagainst^0.16.0, which0.16.1satisfies.🧪 How was it tested?
A/B on a local
arm64-androidlink (NDK 29,ANDROID_STL=c++_shared), same tree and toolchain, differing only in this commit:.gnu.version_dmain(0.16.0 shape)QVAC_FABRIC_ABI_1LIBC/LIBC_Nfromlibc.so,libm.soandlibdl.so— the shape 0.15.0 had, and the shape that loads on device. With no version defined, a consumer records noDT_VERNEEDon fabric, so none of the machinery above runs.llama_*/ggml_*.symbols-linux.mapis generated in the Android tree, the link line points straight atpackages/fabric/symbols.map, andqvac-fabricConfig.cmakereportsQVAC_FABRIC_OWNS_CXX_RUNTIME "OFF"with an emptyQVAC_FABRIC_ABI_VERSION.I did not re-link Linux locally — its vcpkg configure wants the ROCm SDK for
hip-backend. The generated Linux script is byte-identical (modulo comments) to the one that built and passed #4514's end-to-end validation, so CI's Linux leg is the check there.This PR needs the
prebuildslabel soon-pr-fabricpublishes this branch's fabric and the six consumers build against it. The on-device confirmation is the Android suite for #4454 re-run against a consumer built on this fabric; I'll post the run here.💥 Breaking Changes
No JS/TypeScript API change, and no change to fabric's Linux ABI:
QVAC_FABRIC_ABI_1and theDT_VERNEEDconsumers record on it are exactly as 0.16.0 shipped them, so Linux binaries built against 0.16.0 keep working.Android consumers must be rebuilt, which is what makes this a real upgrade rather than a drop-in: an Android module built against 0.16.0 carries versioned imports of
QVAC_FABRIC_ABI_1, and this fabric no longer defines that version. Whether such a module happens to load against it is exactly the loader question above that the run cannot answer, so it is not something to rely on either way — the six packages released against 0.16.0 today need a patch release off this one regardless.