Fix Linux extension load failures: libstdc++ ABI floor and dynamic Boost dependency - #107
Conversation
Two defects made the Linux language extensions fail to load inside SQL Server. On Ubuntu 22.04 this took the extensibility suite from 56 failures to 0; on Ubuntu 24.04 from 18 to 0. libstdc++ ABI floor: GCC 13's <iostream> emits a namespace-scope reference to std::ios_base_library_init() in every translation unit that includes it. That symbol is versioned GLIBCXX_3.4.32, above the ceiling of the libstdc++ shipped on Ubuntu 22.04, so all three extensions failed to load there and every external script failed with them. The include alone is sufficient - six of the changed files never used a stream. Removed the include from the Linux-compiled files and replaced the stream writes with stdio. fwrite wherever the payload is a std::string, since captured user-script output can carry an embedded NUL at which fputs and "%s" stop. R cannot drop the include: Rcpp.h and RInside.h pull it in and every R translation unit reaches them through Common.h. IosBaseSentinel_linux.cpp defines the symbol locally. The sentinel is never called - the compiler emits the name with no call site, purely so the resulting version dependency refuses to load against a libstdc++ that would leave the standard streams unconstructed - so the same file declares a namespace-scope std::ios_base::Init to perform that initialisation at dlopen. Boost linked dynamically: libPythonExtension.so carried DT_NEEDED libboost_python312.so.1.83.0, which the SQL container images do not ship. The file already declared static Boost for both platforms, but the Linux find_library did not honour it: the b2 stage directory holds both the archive and the shared object, and CMAKE_FIND_LIBRARY_SUFFIXES is set just above to prefer .so. Now resolved by naming the .a explicitly with NO_DEFAULT_PATH, preceded by unset(... CACHE) so a reused CMakeCache.txt cannot retain a shared-Boost path, and guarded by FATAL_ERROR so a missing archive fails at configure time rather than silently shipping a package that cannot load. The test target takes the same archives, otherwise the process would hold two Boost.Python copies with independent type registries. The CMake changes are Linux-only. The source changes compile on Windows too and now use stdio there as well; output bytes, ordering and flush semantics are unchanged. Signed-off-by: Sicong Liu <sicongliu@microsoft.com>
8434493 to
4bf2c4e
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes Linux SQL Server language extension load failures by (1) lowering the libstdc++/GLIBCXX symbol version floor triggered by <iostream> and (2) ensuring Boost.Python/Boost.NumPy are linked statically so the shipped Python extension doesn’t require runtime libboost_*.so presence.
Changes:
- Remove
<iostream>from Linux-compiled sources and route diagnostic output through stdio (fwrite/fputs) to avoid raisingGLIBCXXrequirements. - Add an R/Linux-only iostream initialization sentinel override (
IosBaseSentinel_linux.cpp) to avoid importing the host’s versioned symbol while still initializing standard streams. - Update Linux CMake logic (extension + tests) to explicitly resolve and fail-fast on static Boost archives (
.a) to prevent silent dynamic linkage.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| language-extensions/R/src/RTypeUtils.cpp | Drops <iostream> include to avoid pulling in newer GLIBCXX references. |
| language-extensions/R/src/RExtension.cpp | Drops <iostream> include for the same ABI-floor reason. |
| language-extensions/R/src/Logger.cpp | Switches debug/verbose logging from iostreams to stdio. |
| language-extensions/R/src/linux/IosBaseSentinel_linux.cpp | Adds a Linux-only local definition/initialization approach to avoid importing the host’s iostream sentinel symbol. |
| language-extensions/python/test/src/CMakeLists.txt | Forces tests to link the same static Boost archives as the extension (and fail-fast if not found). |
| language-extensions/python/src/PythonSession.cpp | Writes captured Python stdout/stderr via stdio instead of cout/cerr. |
| language-extensions/python/src/PythonLibrarySession.cpp | Drops <iostream> include to avoid ABI-floor impact. |
| language-extensions/python/src/PythonExtension.cpp | Drops <iostream> include to avoid ABI-floor impact. |
| language-extensions/python/src/Logger.cpp | Replaces iostream-based logging with stdio (fwrite/fputs). |
| language-extensions/python/src/CMakeLists.txt | Forces static Boost archive resolution on Linux with configure-time failure if mis-resolved. |
| language-extensions/python/include/Common.h | Removes <iostream> from common headers to prevent widespread ABI-floor inflation. |
| language-extensions/java/src/Logger.cpp | Replaces iostream-based logging with stdio. |
| language-extensions/java/include/JniTypeHelper.inl | Drops <iostream> include (keeps <sstream>). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Aniruddh Munde (Aniruddh25)
left a comment
There was a problem hiding this comment.
This change 1.0.0-CI-ubuntu-minimal-fix-validate-20260806.1 17290 2
What are the 2 failures mentioned in the description?
…comment - IosBaseSentinel_linux.cpp: give g_iosBaseInit init_priority(101). Defining _ZSt21ios_base_library_initv locally removes the GLIBCXX guard, and dynamic initialisation order between translation units is unspecified, so at the default priority an Rcpp/RInside global constructor could touch a stream before it is constructed - the exact case the sentinel was guarding. libstdc++ uses priority 90 for its own centralised initialiser; 0-100 are reserved and warn, so 101 is the lowest usable. Comment updated to explain why the attribute is load-bearing rather than cosmetic. - R Logger::Log: drop fflush(stdout). The original was 'cout << msg;' with no endl and therefore no flush, so the explicit flush was a behaviour change. Python and Java are left flushing because their originals used endl, which does flush - the PR's claim of unchanged flush semantics only broke for R. - R Logger::LogToStdErr: the comment claimed an R-specific error function was used when R is initialised; no such branch exists and none was removed. Rewritten to describe what the code does.
|
Aniruddh Munde (@Aniruddh25) — fair question, the description gives the count without the detail. Both failures are the same suite: They are not attributable to this change, and the evidence is:
The same payload both passed and failed, so it is not deterministic. PVS's own causality analysis agrees. With
The baseline arm contains no change at all — it is the currently shipped pin on unmodified Mechanically there is also no path from this change to a service-start timeout: the Windows-compiled files here only swap stream writes for stdio and drop For completeness, the baseline arm was not clean either — 1 failure to our 2 — and every extension suite (Python, R, Java, ThirdParty, ColumnStore, boundary and parameter) passed on the change arm, which is where a stdio-for-stream regression would actually surface. I will re-run both gates against 833fc0f, which picks up Justin M (@JustinMDotNet)'s |
Aniruddh Munde (Aniruddh25)
left a comment
There was a problem hiding this comment.
Approved, as long as Justin's comment about initialization priority is addressed.
|
Re-validation against
Aniruddh Munde (@Aniruddh25) — this answers your question directly. The two failures are gone, and so is the baseline's one. Neither Windows arm produced a PVS causality warning this time; both runs returned That is also the cleanest possible confirmation that the earlier Justin M (@JustinMDotNet) — your Provenance for the numbers above, verified end to end rather than assumed: payload The description has been updated with these numbers. One caveat I would rather state than leave implicit: this validates |
Fix Linux extension load failures: libstdc++ ABI floor and dynamic Boost dependency
Summary
Two defects stopped the Linux language extensions loading inside SQL Server, so every external
script failed. Both are fixed here.
Neither defect was visible in source review or behind a green build — both were found by
measuring the shipped binary. Most of the changed files compile on Windows too, so this was
validated on both platforms; see Validation.
Defect 1 — libstdc++ ABI floor
GCC 13's
<iostream>emits a namespace-scopestd::ios_base_library_init()reference,versioned
GLIBCXX_3.4.32, in every translation unit that includes it. Ubuntu 22.04 tops outat
GLIBCXX_3.4.30and the check is eager atdlopen, so all three extensions failed to loadthere. The include alone is sufficient — six of the changed files never used a stream.
Removed
<iostream>from 11 Linux-compiled files and moved the stream writes to stdio.(
fwriterather thanfputswherever the payload is astd::string, since captureduser-script output can contain an embedded NUL.)
R cannot drop the include —
Rcpp.h/RInside.hpull it in transitively — soR/src/linux/IosBaseSentinel_linux.cppdefines the symbol locally instead, together with anamespace-scope
std::ios_base::Initto preserve the stream initialisation the sentinel wasguarding. That constructor is
GLIBCXX_3.4, so it does not raise the floor.Defect 2 — Boost linked dynamically
libPythonExtension.socarriedDT_NEEDED libboost_python312.so.1.83.0, which the SQLcontainer images do not ship, so every external Python script failed on both distros.
Static Boost was already declared for both platforms, but the Linux
find_librarydid nothonour it: the b2 stage directory holds both the archive and the shared object, and
CMAKE_FIND_LIBRARY_SUFFIXESprefers.so, so the shared copy won silently.The
.ais now named explicitly withNO_DEFAULT_PATH, behind aFATAL_ERRORguard so amis-resolved Boost fails at configure time instead of shipping a package that cannot load —
that silent success is exactly how this reached a published package. The test target takes the
same archives, otherwise the process would hold two Boost.Python copies with independent type
registries. This defect is independent of the toolchain.
Why GCC 13 has to stay
The cheaper fix for defect 1 would be GCC 11, which emits no
GLIBCXX_3.4.32at all and wouldneed no source changes. That option is closed: #96 moved Python to GCC 11 and #97
reverted it the next day, because R 4.5 uses C23 fixed-underlying-type enums in public headers
that GCC 11 cannot parse. A mixed toolchain does not help — R would still emit the sentinel and
still need the local definition added here.
So the dependency has to be removed at the source level. This change reaches
GLIBCXX_3.4.29,the same floor GCC 11 would have produced, while keeping GCC 13.
Platform scope
The CMake changes are Linux-only — both Boost blocks sit inside the
linuxbranch.The source changes are not. All three extensions glob
${SRC_DIR}/*.cpp, so the loggerfiles compile on Windows too and now use stdio there as well. Output bytes and ordering are
unchanged, and flush behaviour matches each original: Java and Python used
endland stillflush, while R's debug
Logused a barecout <<with no flush and correspondingly does not.IosBaseSentinel_linux.cppis undersrc/linux/and is Linux-only.This is why Windows was validated as well.
Validation
Binary. All three shipped
.sofiles: max GLIBC 2.33–2.34, maxGLIBCXX_3.4.29, no BoostDT_NEEDED, and no_ZSt4cout/_ZSt4cerr/_ZSt21ios_base_library_initv.Linux — PVS boards on an
mssql-serverdrop, holding shelveset, test filter and qualitygate constant and varying only the drop:
Counts are actual
Failedoutcomes;totalTests - passedTestsreads 76 because it alsoabsorbs
NotExecuted. By signature on 22.04: Java script errors 15 → 0, R 11 → 0, Python11 → 0, cascading
KeyNotFoundException17 → 0.ThirdParty.PythonExtension.*went 6/22passed → 604/604, and executed tests rose 4579 → 7027 as suites that used to abort ran to
completion.
RExtension-testpasses 89/89 in debug and release.Windows — the
-windowspackage built from this same payload, consumed by DsMainDev withthe pin moved to it, against a baseline arm on the currently shipped pin. The two arms differ
by exactly that one line.
1.0.0-CI-master-20260729.11.0.0-CI-ubuntu-minimal-fix-validate-20260807.1The package builds, restores and binplaces, and every extension suite passed — Python, R,
Java, ThirdParty, ColumnStore, boundary and parameter suites. That is where a stdio-for-stream
regression would surface.
All four arms are clean and neither Windows arm produced a PVS causality warning.
An earlier round of this validation, before the
init_priorityfix, had one failure on eachWindows arm —
Extensibility.Launchpad.LaunchpadTeston ours and an unrelated AiFunctionssuite on the baseline — both returning 1/3 with the change, 0/3 without. Since the
baseline arm contains no change at all, that ratio was the board's noise floor rather than a
causal signal; neither reproduced here, which confirms it.