QVAC-25104 fix: pin consumers' C++ runtime with a named ELF version node - #4514
Merged
Merged
Conversation
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):
|
Contributor
🧪 C++ Test Coverage ReportCoverage: 📊 Detailed Coverage |
6 tasks
Contributor
🧪 C++ Test Coverage ReportCoverage: 📊 Detailed Coverage |
gianni-cor
previously approved these changes
Sep 16, 2026
0.15.0 gave fabric the C++ runtime to export, but nothing reached it. bare's executable links GNU libstdc++.so.6, so libstdc++ sits in the process' global lookup scope, which the dynamic linker searches before a dlopen'd module's own DT_NEEDED chain. A consumer linked with -nostdlib++ resolved __cxa_throw, the personality routine and the std:: typeinfo objects from libstdc++ while taking only the libc++-only names from fabric, and std::exception_ptr split down that seam: a captured exception was re-raised with a foreign exception class that GNU's personality routine can only match against catch (...). Every native error from load reached JS as "Unknown error", not only the three config-validation cases. Name the version node QVAC_FABRIC_ABI_1, so consumers record a DT_VERNEED that libstdc++ cannot satisfy. The node covers the whole export surface: an anonymous node cannot coexist with a named one, and the spliced C++ ABI block has to stay inside the same global list to keep its precedence over `local: *;`. It pins fabric's own internal references too, which were being interposed the same way. Publish QVAC_FABRIC_ABI_VERSION and QVAC_FABRIC_OWNS_CXX_RUNTIME through the package config, so a consumer's build can assert it pinned the runtime instead of trusting its link line, and can tell a deliberately shared libc++ (Android, ASan) from a fabric too old to pin at all. Configure fails if symbols.map and the declared node name drift apart. Consumers must be rebuilt, and a mixed pairing is worse than either half and silent: pinning fabric's internal references removes the accident that both sides previously resolved the runtime from libstdc++ and so agreed on one, so a consumer that is not rebuilt loses the typed catch it used to get -- and still loads, because an unversioned reference binds to a default-versioned definition. Ship as a minor; a caret on 0.x locks the minor, so ^0.15.0 keeps already-published consumers away from it. Verified on linux-x64 against a locally built fabric: a rebuilt llm-llamacpp pins 378 imports to QVAC_FABRIC_ABI_1, and the load-mode, repeat-penalty and device errors surface their real messages under bare. The addon-template half, which asserts the pin at build time and narrows addon exports to the bare C entry points, lands separately after this is released to npm.
Folds the version bump into the fix PR so the release can be cut straight from a release-fabric-0.16.0 branch: release-merge-guard only requires package.json to match the branch version, and create-github-release reads the [0.16.0] section out of the changelog. Minor, not patch: a consumer that is not rebuilt against the version node loses a typed catch it used to get and still loads, so ^0.15.0 has to keep already-published consumers away from this release. The six in-tree consumer ranges move in the same commit. pnpm-workspace's linkWorkspacePackages resolves from the registry once the workspace version stops satisfying the range, so bumping fabric alone flips all six from link:../fabric to the published 0.15.0 -- which is both a lockfile change and a silent downgrade of what CI builds them against. Their own versions are untouched; nothing here releases a consumer. pnpm-lock.yaml is regenerated with the pinned pnpm 11.17.0 rather than hand-edited, so it also drops two orphaned @qvac/diffusion-cpp@0.23.0 entries that nothing has referenced since #4439 moved sdk and inference to the workspace copy.
…hips The six consumer ranges cannot move in the same commit as the bump after all. The pr-head-ts-checks jobs run a standalone npm install per package rather than through the pnpm workspace, so an unpublished ^0.16.0 fails resolution outright -- ETARGET, no matching version -- and took six jobs down with it. #4477 hit the same ordering constraint for the 0.15.0 floor: ranges move once the release is on npm, which is what #4519 does. The cost is that pnpm stops linking the workspace fabric into the six consumers, because 0.16.0 no longer satisfies ^0.15.0, so the regenerated lockfile records the published 0.15.0 for them. That is the same in-between state main sat in after #4481 and before #4477, and it is content-identical for those builds: the published 0.15.0 is the fabric they link today. Native CI is unaffected either way, since overlay-local-fabric stages this branch's own fabric into consumer node_modules when the fabric stack changes, which is what the prebuilds label drives. The window closes when
gianni-cor
approved these changes
Sep 16, 2026
iancris
approved these changes
Sep 17, 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?
-nostdlib++did not actually give a consumer addon fabric's C++ runtime.bare's executable links GNUlibstdc++.so.6, so libstdc++ sits in the process' global lookup scope, which the dynamic linker searches before adlopen'd module's ownDT_NEEDEDchain.__cxa_throw,__gxx_personality_v0and thestd::typeinfo objects from libstdc++, while still resolving the libc++-only names from fabric.std::exception_ptrsplits along exactly that seam, becausestd::current_exceptionandstd::rethrow_exceptionhave identical mangled names in both libraries: libstdc++ captures the throw, libc++abi re-raises it stampedCLNGC++, and GNU's personality routine will only match a foreign exception class againstcatch (...).JSCATCHreported"Unknown error". That is not limited to the three known config-validation cases — every native error out ofloadcame back asINTERNAL_ERROR/"Unknown error", including genuine load failures.📝 How does it solve it?
QVAC_FABRIC_ABI_1. Consumers then record aDT_VERNEEDon it, and libstdc++ cannot satisfy a versioned reference to a version it does not define. This one change is the fix; it also pins fabric's own internal references, which were being interposed the same way.anonymous version tag cannot be combined with other version tags), and the spliced C++ ABI block must stay inside the sameglobal:list to keep precedence overlocal: *;.DT_VERNEEDon its own, so the in-tree consumers pick the fix up simply by being rebuilt against this fabric — which is what CI does here.QVAC_FABRIC_ABI_VERSIONandQVAC_FABRIC_OWNS_CXX_RUNTIMEthroughqvac-fabricConfig.cmake, and fail configure ifsymbols.mapdoes not declare the node fabric advertises, so the two cannot drift.QVAC_FABRIC_OWNS_CXX_RUNTIMEis what lets a consumer's build tell a deliberately shared libc++ (Android'slibc++_shared.so, the ASan build) apart from a fabric that is simply too old to pin; the template half in QVAC-25104 fix: assert addons pin fabric's C++ runtime and narrow exports #4519 uses both to assert the pin at build time.symbols.map,symbols-linux-cxx-runtime.mapand the QIP. All three described the cause as two static libc++ copies compared by typeinfo address, which is true but omits the interposition that actually shipped. Also corrects the claim that thedlopen'd ggml backends resolveggml_*from fabric: all 16 linux-x64 and all 9 android-arm64 backends link ggml statically and import zeroggml_*, which is why the version node cannot affect them.package.jsongoes to0.16.0and the changelog entry is written as## [0.16.0], so the release can be cut straight from arelease-fabric-0.16.0branch instead of via a separate release PR as in QVAC-25104 chore: release @qvac/fabric v0.15.0 #4481:release-merge-guardonly requirespackage.jsonto match the branch version, andcreate-github-releasereads that changelog section for the release notes.^0.15.0here and move in QVAC-25104 fix: assert addons pin fabric's C++ runtime and narrow exports #4519 after the release is on npm, which is the ordering QVAC-25104 fix: let Linux addons catch exceptions thrown by fabric #4477 used for the 0.15.0 floor. Moving them in this PR was tried and does not work: thepr-head-ts-checksjobs install each package standalone with npm rather than through the pnpm workspace, so^0.16.0fails resolution withETARGET — no matching version foundwhile 0.16.0 is unpublished, taking six jobs down.linkWorkspacePackages: truestops linking the workspace fabric into those six, because0.16.0no longer satisfies^0.15.0, sopnpm-lock.yamlis regenerated to record the published0.15.0for them. This is the same in-between statemainsat in after QVAC-25104 chore: release @qvac/fabric v0.15.0 #4481 and before QVAC-25104 fix: let Linux addons catch exceptions thrown by fabric #4477, and it is content-identical for those builds — the published 0.15.0 is the fabric they link today. Native CI is unaffected regardless, sinceoverlay-local-fabricstages this branch's own fabric into consumernode_moduleswhen the fabric stack changes, which is what theprebuildslabel drives. The window closes when QVAC-25104 fix: assert addons pin fabric's C++ runtime and narrow exports #4519 lands.pnpm-lock.yamlis regenerated with the pinned pnpm 11.17.0 rather than hand-edited, so it also drops two orphaned@qvac/diffusion-cpp@0.23.0entries that nothing has referenced since QVAC-24821 fix[bc]: integrate Fabric 10549.1.0 consumers in SDK #4439 movedsdkandinferenceonto the workspace copy.🧪 How was it tested?
Locally on linux-x64, with fabric built from this branch and
llm-llamacpp(the migration in #4454) built against it:readelfon the rebuilt addon:DT_VERNEEDonQVAC_FABRIC_ABI_1, 378 C++ runtime imports pinned to it,current_exceptionandrethrow_exceptionboth resolving to fabric, and fabric still self-contained (noDT_NEEDEDonlibc++.so.1).bare, the three previously failing scenarios surface their real messages instead of"Unknown error":load-mode: bogus->[ LLM :: InvalidArgument ] load-mode must be one of 'auto', 'none', ...repeat_penalty: -1.1->[ LLM :: InvalidArgument ] commonParamsParse: error while handling argument "--repeat-penalty" ...device: invalid->[ General :: InvalidArgument ] preferredDeviceFromString: wrong device specified ...[ LLM :: UnableToLoadModel ].dlsymon the plain names still resolves fabric's now-versionedbare_*entry points, which is how bare locates a module.exception_ptrround-trip printsUNKNOWN ERROR; with a named version node the typed catch matches.This PR needs the
prebuildslabel soon-pr-fabricpublishes this branch's fabric and the consumers build against it. CI then covers the six already-migrated consumers (classification-ggml,embed-llamacpp,model-fit,ocr-ggml,translation-nmtcpp,vla-ggml) on the platforms I cannot build locally.💥 Breaking Changes
No JS/TypeScript API change. This is a change to fabric's Linux native ABI:
Consumers must be rebuilt against this release, so it has to ship as a minor (
0.16.0), never a patch. A mixed pairing is worse than either half, and silent. Measured on the minimal repro (a host linking libstdc++ asbaredoes,dlopening fabricRTLD_GLOBALand the moduleRTLD_LOCAL):exception_ptrround-tripcatch (...)catch (...)catch (...)Before the node, both sides resolved the runtime from libstdc++ and so accidentally agreed on one, which is why a direct throw was catchable and only the
exception_ptrpath failed. Pinning fabric's internal references removes that accident, so a consumer that is not rebuilt now genuinely disagrees with fabric and loses the typed catch it used to get. It still loads — an unversioned reference binds to a default-versioned definition — so the regression would be silent. A caret on a0.xversion locks the minor, so^0.15.0is what keeps already-published consumers away from it; a0.15.1would reach them and degrade them.The node name
QVAC_FABRIC_ABI_1is itself part of the contract: renaming it is a rebuild of every consumer, and a consumer built against the wrong node does not fail to load, it reverts to the host runtime.Android exports the same set, now version-stamped. Its consumers share
libc++_shared.sorather than importing the runtime from fabric, so they are unaffected. Darwin, iOS and Windows use no version script and are untouched.The in-tree consumers are built from source in CI and so are covered here. Their npm ranges move to
^0.16.0in QVAC-25104 fix: assert addons pin fabric's C++ runtime and narrow exports #4519, once this release is published — see above for why they cannot move before then.