Skip to content

Fix Linux extension load failures: libstdc++ ABI floor and dynamic Boost dependency - #107

Merged
SicongLiu2000 merged 2 commits into
mainfrom
dev/sicongliu/ubuntu-minimal-fix
Aug 10, 2026
Merged

SicongLiu2000 merged 2 commits into
mainfrom
dev/sicongliu/ubuntu-minimal-fix

Conversation

@SicongLiu2000

@SicongLiu2000 SicongLiu2000 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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.

Before After
Ubuntu 22.04 56 failed 0
Ubuntu 24.04 18 failed 0
Windows no regression

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-scope std::ios_base_library_init() reference,
versioned GLIBCXX_3.4.32, in every translation unit that includes it. Ubuntu 22.04 tops out
at GLIBCXX_3.4.30 and the check is eager at dlopen, so all three extensions failed to load
there. 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.
(fwrite rather than fputs wherever the payload is a std::string, since captured
user-script output can contain an embedded NUL.)

R cannot drop the include — Rcpp.h / RInside.h pull it in transitively — so
R/src/linux/IosBaseSentinel_linux.cpp defines the symbol locally instead, together with a
namespace-scope std::ios_base::Init to preserve the stream initialisation the sentinel was
guarding. That constructor is GLIBCXX_3.4, so it does not raise the floor.

Defect 2 — Boost linked dynamically

libPythonExtension.so carried DT_NEEDED libboost_python312.so.1.83.0, which the SQL
container 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_library did not
honour it: the b2 stage directory holds both the archive and the shared object, and
CMAKE_FIND_LIBRARY_SUFFIXES prefers .so, so the shared copy won silently.

The .a is now named explicitly with NO_DEFAULT_PATH, behind a FATAL_ERROR guard so a
mis-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.32 at all and would
need 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.

I am relaying the R 4.5 / C23 claim from cdae89d; I have not independently verified it
against R 4.5's headers. If it is softer than stated, the toolchain option reopens.

Platform scope

The CMake changes are Linux-only — both Boost blocks sit inside the linux branch.

The source changes are not. All three extensions glob ${SRC_DIR}/*.cpp, so the logger
files 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 endl and still
flush, while R's debug Log used a bare cout << with no flush and correspondingly does not.
IosBaseSentinel_linux.cpp is under src/linux/ and is Linux-only.
This is why Windows was validated as well.

Validation

Binary. All three shipped .so files: max GLIBC 2.33–2.34, max GLIBCXX_3.4.29, no Boost
DT_NEEDED, and no _ZSt4cout / _ZSt4cerr / _ZSt21ios_base_library_initv.

Linux — PVS boards on an mssql-server drop, holding shelveset, test filter and quality
gate constant and varying only the drop:

Board Before After
Ubuntu 22.04 56 failed 0 / 7027
Ubuntu 24.04 18 failed 0 / 7129

Counts are actual Failed outcomes; totalTests - passedTests reads 76 because it also
absorbs NotExecuted. By signature on 22.04: Java script errors 15 → 0, R 11 → 0, Python
11 → 0, cascading KeyNotFoundException 17 → 0. ThirdParty.PythonExtension.* went 6/22
passed → 604/604, and executed tests rose 4579 → 7027 as suites that used to abort ran to
completion. RExtension-test passes 89/89 in debug and release.

Windows — the -windows package built from this same payload, consumed by DsMainDev with
the pin moved to it, against a baseline arm on the currently shipped pin. The two arms differ
by exactly that one line.

Arm Package Tests Failed
Baseline 1.0.0-CI-master-20260729.1 17318 0
This change 1.0.0-CI-ubuntu-minimal-fix-validate-20260807.1 17276 0

The 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_priority fix, had one failure on each
Windows arm — Extensibility.Launchpad.LaunchpadTest on ours and an unrelated AiFunctions
suite 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.

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>
@SicongLiu2000
SicongLiu2000 force-pushed the dev/sicongliu/ubuntu-minimal-fix branch from 8434493 to 4bf2c4e Compare August 6, 2026 19:31
@SicongLiu2000
SicongLiu2000 marked this pull request as ready for review August 7, 2026 17:59
Copilot AI lite review requested due to automatic review settings August 7, 2026 17:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 raising GLIBCXX requirements.
  • 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.

Comment thread language-extensions/R/src/Logger.cpp Outdated
Comment thread language-extensions/R/src/Logger.cpp Outdated
Comment thread language-extensions/R/src/linux/IosBaseSentinel_linux.cpp Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Aniruddh Munde (@Aniruddh25) — fair question, the description gives the count without the detail. Both failures are the same suite: Extensibility.Launchpad.LaunchpadTest, Setup and Cleanup, failing with System.ServiceProcess.TimeoutException after 325 s and 302 s — a Windows Service Control Manager timeout, not an assertion.

They are not attributable to this change, and the evidence is:

run payload Setup
fix arm, retail leg identical bits Passed, 7 s
fix arm, debug leg identical bits Failed, 325 s
baseline arm, both legs shipped pin Passed, 13 s / 9 s
unrelated run 230021086, both legs someone else's change Passed, 14 s / 8 s

The same payload both passed and failed, so it is not deterministic.

PVS's own causality analysis agrees. With DetermineTestFailureCausality=true it re-runs each failing job 3x with the change and 3x without, and reported:

  • this changeExtensibility.Launchpad.LaunchpadTest: failed 1/3 with, 0/3 without
  • baseline armDW…AiFunctions.QueryInsights.DwExtensibilityQiAggregationTests: failed 1/3 with, 0/3 without

The baseline arm contains no change at all — it is the currently shipped pin on unmodified master — and it produced the identical ratio on a different suite. So 1/3 with / 0/3 without is this board's noise floor rather than a causal signal. That comparison is the only reason the number is interpretable, and it is why I ran a baseline arm rather than reporting our arm alone.

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 #includes — they add no static initialiser and no service-start work. The one file that does add a static initialiser, IosBaseSentinel_linux.cpp, is Linux-only and never compiled on Windows.

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 init_priority fix, and post the fresh numbers here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, as long as Justin's comment about initialization priority is addressed.

@SicongLiu2000

Copy link
Copy Markdown
Contributor Author

Re-validation against 833fc0f is complete — both gates, all four arms clean.

Gate Arm Tests Failed
Linux Ubuntu 22.04 7027 0
Linux Ubuntu 24.04 7129 0
Windows baseline (1.0.0-CI-master-20260729.1) 17318 0
Windows this change (…-validate-20260807.1) 17276 0

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 succeeded rather than partiallySucceeded.

That is also the cleanest possible confirmation that the earlier Extensibility.Launchpad.LaunchpadTest failures were flaky rather than caused by this change. The argument at the time rested on the same payload passing on one leg and failing on the other, plus the baseline arm — which contains no change at all — producing the identical 1/3 with / 0/3 without signature on a different suite. Neither reproduced on a fresh run of a superset payload, which is the evidence I would have wanted rather than the inference.

Justin M (@JustinMDotNet) — your init_priority fix costs nothing measurable: it compiles with no warning (101 clears the reserved 0–100 range), RExtension-test still passes 89/89, and the ELF profile is unchanged — all three binaries at max GLIBCXX_3.4.29, no _ZSt21ios_base_library_initv, no _ZSt4cout/_ZSt4cerr, and no libboost_* in DT_NEEDED.

Provenance for the numbers above, verified end to end rather than assumed: payload 833fc0f → packages 1.0.0-CI-ubuntu-minimal-fix-validate-20260807.1 → drop 18.0.246-2-1771 (mssqlCommit cdd2b37f4) → boards. Both Linux boards confirmed running PlatformLinux suites and both Windows arms PlatformWindows, since the platform is selected by quality gate and is worth checking rather than trusting.

The description has been updated with these numbers.

One caveat I would rather state than leave implicit: this validates 833fc0f as it stands, not the post-merge result. main has since gained #106 (Windows Boost caching) and #95 (C# extension XEvent logging). Neither touches any file that enters the Linux package, so the Linux numbers are unaffected by a merge. #95 is Windows-compiled, so the Windows arm was measured without it.

@SicongLiu2000
SicongLiu2000 added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 9bbfaf5 Aug 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants