From 4bf2c4e1fcfa4f2dc1062c5d66014a6b35d4ca89 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Thu, 6 Aug 2026 12:31:30 -0700 Subject: [PATCH 1/2] Fix Linux extension load failures: libstdc++ ABI floor and dynamic Boost 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 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 --- language-extensions/R/src/Logger.cpp | 9 ++--- language-extensions/R/src/RExtension.cpp | 1 - language-extensions/R/src/RTypeUtils.cpp | 1 - .../R/src/linux/IosBaseSentinel_linux.cpp | 34 +++++++++++++++++++ .../java/include/JniTypeHelper.inl | 1 - language-extensions/java/src/Logger.cpp | 25 +++++++++++--- language-extensions/python/include/Common.h | 1 - language-extensions/python/src/CMakeLists.txt | 26 ++++++++++++-- language-extensions/python/src/Logger.cpp | 22 ++++++++++-- .../python/src/PythonExtension.cpp | 1 - .../python/src/PythonLibrarySession.cpp | 1 - .../python/src/PythonSession.cpp | 10 ++++-- .../python/test/src/CMakeLists.txt | 24 +++++++++++-- 13 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 language-extensions/R/src/linux/IosBaseSentinel_linux.cpp diff --git a/language-extensions/R/src/Logger.cpp b/language-extensions/R/src/Logger.cpp index 8a36d2b7..8ff2763c 100644 --- a/language-extensions/R/src/Logger.cpp +++ b/language-extensions/R/src/Logger.cpp @@ -27,7 +27,6 @@ //************************************************************************************************** #include -#include #include #include "Common.h" @@ -75,7 +74,8 @@ void Logger::Log(const string &msg) { #if defined(_DEBUG) || defined(_VERBOSE) string msgWithTimestamp = string(GetCurrentTimestamp()) + msg + "\n"; - cout << msgWithTimestamp; + fwrite(msgWithTimestamp.data(), 1, msgWithTimestamp.size(), stdout); + fflush(stdout); #endif } @@ -136,9 +136,10 @@ const char* Logger::GetCurrentTimestamp() // // Description: // Logs the given message to stderr; if R is initialized uses its error printing function, -// else uses std::cerr. +// else writes through stdio. // void Logger::LogToStdErr(const string &errorMsgWithTimestamp) { - cerr << errorMsgWithTimestamp; + fwrite(errorMsgWithTimestamp.data(), 1, errorMsgWithTimestamp.size(), stderr); + fflush(stderr); } diff --git a/language-extensions/R/src/RExtension.cpp b/language-extensions/R/src/RExtension.cpp index 36e02c87..d97c81d7 100644 --- a/language-extensions/R/src/RExtension.cpp +++ b/language-extensions/R/src/RExtension.cpp @@ -29,7 +29,6 @@ #include "Common.h" #include -#include #include #include diff --git a/language-extensions/R/src/RTypeUtils.cpp b/language-extensions/R/src/RTypeUtils.cpp index ceaba9e7..58b295ea 100644 --- a/language-extensions/R/src/RTypeUtils.cpp +++ b/language-extensions/R/src/RTypeUtils.cpp @@ -28,7 +28,6 @@ #include "Common.h" #include #include -#include #include "RTypeUtils.h" #include "Unicode.h" diff --git a/language-extensions/R/src/linux/IosBaseSentinel_linux.cpp b/language-extensions/R/src/linux/IosBaseSentinel_linux.cpp new file mode 100644 index 00000000..616ae5dd --- /dev/null +++ b/language-extensions/R/src/linux/IosBaseSentinel_linux.cpp @@ -0,0 +1,34 @@ +//************************************************************************************************** +// Copyright (C) Microsoft Corporation. +// +// @File: IosBaseSentinel_linux.cpp +// +// Purpose: +// Defines the initialisation sentinel locally so libRExtension does not import it from +// the host's libstdc++. Recent GCC releases version that symbol above what the oldest supported +// hosts provide, so importing it makes the library fail to load there. +// +// The other extensions drop the include instead. R cannot: Rcpp.h and RInside.h include +// , and every R translation unit reaches them through Common.h. +// +// 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. Defining it here removes that dependency, so this file must also perform +// the initialisation the sentinel was guarding: that is the namespace-scope ios_base::Init below. +// A function-local static would never run, because the function is never called. +// +// extern "C" emits the mangled name verbatim; hidden visibility keeps it out of our exports. +// +//************************************************************************************************** + +#include + +extern "C" __attribute__((visibility("hidden"))) void _ZSt21ios_base_library_initv() +{ +} + +// Constructs std::cin/cout/cerr/clog at dlopen, before any extension code or statically linked +// Rcpp / RInside code can touch a stream. The constructor is refcounted and idempotent, and is old +// enough to be present in every libstdc++ we target, so it does not raise the ABI floor. +// +static std::ios_base::Init g_iosBaseInit; diff --git a/language-extensions/java/include/JniTypeHelper.inl b/language-extensions/java/include/JniTypeHelper.inl index 0d6aec68..48c2fb01 100644 --- a/language-extensions/java/include/JniTypeHelper.inl +++ b/language-extensions/java/include/JniTypeHelper.inl @@ -10,7 +10,6 @@ //********************************************************************* #include #include -#include #include #include // Needed for memcpy in Linux diff --git a/language-extensions/java/src/Logger.cpp b/language-extensions/java/src/Logger.cpp index 8d16694e..0daebc0f 100644 --- a/language-extensions/java/src/Logger.cpp +++ b/language-extensions/java/src/Logger.cpp @@ -9,7 +9,7 @@ // //********************************************************************* #include "Logger.h" -#include +#include #include using namespace std; @@ -22,7 +22,11 @@ using namespace std; // void Logger::LogError(const string &errorMsg) { - cerr << GetCurrentTimestamp() << "Error: " << errorMsg << endl; + fputs(GetCurrentTimestamp(), stderr); + fputs("Error: ", stderr); + fwrite(errorMsg.data(), 1, errorMsg.size(), stderr); + fputc('\n', stderr); + fflush(stderr); } //--------------------------------------------------------------------- @@ -34,7 +38,11 @@ void Logger::LogError(const string &errorMsg) // void Logger::LogException(const exception &e) { - cerr << GetCurrentTimestamp() << "Exception occurred: " << e.what() << endl; + fputs(GetCurrentTimestamp(), stderr); + fputs("Exception occurred: ", stderr); + fputs(e.what(), stderr); + fputc('\n', stderr); + fflush(stderr); } //--------------------------------------------------------------------- @@ -46,7 +54,11 @@ void Logger::LogException(const exception &e) // void Logger::LogJavaException(const string &exceptionMsg) { - cerr << GetCurrentTimestamp() << "Exception occurred in Java: " << exceptionMsg << endl; + fputs(GetCurrentTimestamp(), stderr); + fputs("Exception occurred in Java: ", stderr); + fwrite(exceptionMsg.data(), 1, exceptionMsg.size(), stderr); + fputc('\n', stderr); + fflush(stderr); } //--------------------------------------------------------------------- @@ -58,6 +70,9 @@ void Logger::LogJavaException(const string &exceptionMsg) void Logger::Log(const string &msg) { #ifdef DEBUG - cout << GetCurrentTimestamp() << msg << endl; + fputs(GetCurrentTimestamp(), stdout); + fwrite(msg.data(), 1, msg.size(), stdout); + fputc('\n', stdout); + fflush(stdout); #endif } diff --git a/language-extensions/python/include/Common.h b/language-extensions/python/include/Common.h index aa8da254..91db4fb1 100644 --- a/language-extensions/python/include/Common.h +++ b/language-extensions/python/include/Common.h @@ -18,7 +18,6 @@ #endif #include -#include #include #include #include diff --git a/language-extensions/python/src/CMakeLists.txt b/language-extensions/python/src/CMakeLists.txt index fd9112c4..81a94390 100644 --- a/language-extensions/python/src/CMakeLists.txt +++ b/language-extensions/python/src/CMakeLists.txt @@ -72,8 +72,30 @@ if (${PLATFORM} STREQUAL linux) message(STATUS "Shared Python library not found in standard locations, trying system default search") find_library(PYTHON_LIB python${PYTHON_VERSION}) endif() - find_library(BOOST_PYTHON_LIB boost_python${PYTHON_VERSION_NO_DOT} ${BOOST_PYTHON_ROOT}) - find_library(BOOST_NUMPY_LIB boost_numpy${PYTHON_VERSION_NO_DOT} ${BOOST_PYTHON_ROOT}) + + # Boost must not become a runtime dependency: the SQL container images ship no libboost_*.so, + # so a shared link fails at load time and every external script fails with it. + # + if(NOT BOOST_PYTHON_ROOT) + set(BOOST_PYTHON_ROOT /usr/local/lib/boost_1_90_0/stage/lib) + endif() + + unset(BOOST_PYTHON_LIB CACHE) + unset(BOOST_NUMPY_LIB CACHE) + + find_library(BOOST_PYTHON_LIB NAMES libboost_python${PYTHON_VERSION_NO_DOT}.a + PATHS ${BOOST_PYTHON_ROOT} NO_DEFAULT_PATH) + find_library(BOOST_NUMPY_LIB NAMES libboost_numpy${PYTHON_VERSION_NO_DOT}.a + PATHS ${BOOST_PYTHON_ROOT} NO_DEFAULT_PATH) + + # Fail at configure time rather than silently shipping a package that cannot load. + # + if(NOT BOOST_PYTHON_LIB OR NOT BOOST_NUMPY_LIB) + message(FATAL_ERROR + "libboost_python${PYTHON_VERSION_NO_DOT}.a / libboost_numpy${PYTHON_VERSION_NO_DOT}.a " + "not found under ${BOOST_PYTHON_ROOT}. Override -DBOOST_PYTHON_ROOT= if the " + "build moved them.") + endif() file(TO_CMAKE_PATH ${INCLUDE_ROOT}/python${PYTHON_VERSION} PYTHON_INCLUDE) file(TO_CMAKE_PATH ${INCLUDE_ROOT}/boost BOOST_INCLUDE) diff --git a/language-extensions/python/src/Logger.cpp b/language-extensions/python/src/Logger.cpp index 324fc7cc..929acc22 100644 --- a/language-extensions/python/src/Logger.cpp +++ b/language-extensions/python/src/Logger.cpp @@ -11,6 +11,8 @@ // //************************************************************************************************* +#include + #include "Logger.h" #define TIMESTAMP_LENGTH 35 @@ -26,7 +28,12 @@ char Logger::sm_timestampBuffer[TIMESTAMP_LENGTH] = { 0 }; // void Logger::LogError(const string &errorMsg) { - cerr << GetCurrentTimestamp() << "Error: " << errorMsg << endl; + const string timestamp = GetCurrentTimestamp(); + fwrite(timestamp.data(), 1, timestamp.size(), stderr); + fputs("Error: ", stderr); + fwrite(errorMsg.data(), 1, errorMsg.size(), stderr); + fputc('\n', stderr); + fflush(stderr); } //------------------------------------------------------------------------------------------------- @@ -38,7 +45,12 @@ void Logger::LogError(const string &errorMsg) // void Logger::LogException(const exception &e) { - cerr << GetCurrentTimestamp() << "Exception occurred: " << e.what() << endl; + const string timestamp = GetCurrentTimestamp(); + fwrite(timestamp.data(), 1, timestamp.size(), stderr); + fputs("Exception occurred: ", stderr); + fputs(e.what(), stderr); + fputc('\n', stderr); + fflush(stderr); } //------------------------------------------------------------------------------------------------- @@ -50,7 +62,11 @@ void Logger::LogException(const exception &e) void Logger::Log(const string &msg) { #if defined(_DEBUG) - cout << GetCurrentTimestamp() << msg << endl; + const string timestamp = GetCurrentTimestamp(); + fwrite(timestamp.data(), 1, timestamp.size(), stdout); + fwrite(msg.data(), 1, msg.size(), stdout); + fputc('\n', stdout); + fflush(stdout); #endif } diff --git a/language-extensions/python/src/PythonExtension.cpp b/language-extensions/python/src/PythonExtension.cpp index 7e9d6629..1883d928 100644 --- a/language-extensions/python/src/PythonExtension.cpp +++ b/language-extensions/python/src/PythonExtension.cpp @@ -14,7 +14,6 @@ //************************************************************************************************* #include -#include #include #include diff --git a/language-extensions/python/src/PythonLibrarySession.cpp b/language-extensions/python/src/PythonLibrarySession.cpp index 2d8f0275..f6cae65c 100644 --- a/language-extensions/python/src/PythonLibrarySession.cpp +++ b/language-extensions/python/src/PythonLibrarySession.cpp @@ -12,7 +12,6 @@ //************************************************************************************************* #include -#include #include #include diff --git a/language-extensions/python/src/PythonSession.cpp b/language-extensions/python/src/PythonSession.cpp index 6cf9637f..da476939 100644 --- a/language-extensions/python/src/PythonSession.cpp +++ b/language-extensions/python/src/PythonSession.cpp @@ -11,6 +11,8 @@ // //************************************************************************************************* +#include + #include "Logger.h" #include "PythonExtensionUtils.h" #include "PythonNamespace.h" @@ -211,8 +213,12 @@ void PythonSession::ExecuteWorkflow( string pyStdOut = bp::extract(m_mainNamespace["_temp_out_"]); string pyStdErr = bp::extract(m_mainNamespace["_temp_err_"]); - cout << pyStdOut << endl; - cerr << pyStdErr << endl; + fwrite(pyStdOut.data(), 1, pyStdOut.size(), stdout); + fputc('\n', stdout); + fflush(stdout); + fwrite(pyStdErr.data(), 1, pyStdErr.size(), stderr); + fputc('\n', stderr); + fflush(stderr); // In case of streaming clean up the previous stream batch's output buffers // diff --git a/language-extensions/python/test/src/CMakeLists.txt b/language-extensions/python/test/src/CMakeLists.txt index 317a9049..8339a6a7 100644 --- a/language-extensions/python/test/src/CMakeLists.txt +++ b/language-extensions/python/test/src/CMakeLists.txt @@ -46,8 +46,28 @@ if (${PLATFORM} STREQUAL linux) set(PYTHON_LIB_PATH ${PYTHONHOME}/lib/python${PYTHON_VERSION}/config-${PYTHON_VERSION}-x86_64-linux-gnu) find_library(PYTHON_LIB python${PYTHON_VERSION} ${PYTHON_LIB_PATH}) - find_library(BOOST_PYTHON_LIB boost_python${PYTHON_VERSION_NO_DOT} ${BOOST_PYTHON_ROOT}) - find_library(BOOST_NUMPY_LIB boost_numpy${PYTHON_VERSION_NO_DOT} ${BOOST_PYTHON_ROOT}) + + # Same static archives the extension links - see language-extensions/python/src/CMakeLists.txt. + # The test links libPythonExtension and Boost both, so a shared Boost here would put two + # Boost.Python copies with independent type registries in one process. + # + if(NOT BOOST_PYTHON_ROOT) + set(BOOST_PYTHON_ROOT /usr/local/lib/boost_1_90_0/stage/lib) + endif() + + unset(BOOST_PYTHON_LIB CACHE) + unset(BOOST_NUMPY_LIB CACHE) + + find_library(BOOST_PYTHON_LIB NAMES libboost_python${PYTHON_VERSION_NO_DOT}.a + PATHS ${BOOST_PYTHON_ROOT} NO_DEFAULT_PATH) + find_library(BOOST_NUMPY_LIB NAMES libboost_numpy${PYTHON_VERSION_NO_DOT}.a + PATHS ${BOOST_PYTHON_ROOT} NO_DEFAULT_PATH) + + if(NOT BOOST_PYTHON_LIB OR NOT BOOST_NUMPY_LIB) + message(FATAL_ERROR + "Static Boost.Python/Boost.NumPy not found under ${BOOST_PYTHON_ROOT}. " + "Override -DBOOST_PYTHON_ROOT= if the build moved them.") + endif() file(TO_CMAKE_PATH ${INCLUDE_ROOT}/python${PYTHON_VERSION} PYTHON_INCLUDE) file(TO_CMAKE_PATH ${INCLUDE_ROOT}/boost BOOST_INCLUDE) From 833fc0f6f358f7d1ec126db8dfa0428af3797457 Mon Sep 17 00:00:00 2001 From: Sicong Liu Date: Fri, 7 Aug 2026 13:31:32 -0700 Subject: [PATCH 2/2] Address review: sentinel init priority, R stdout flush parity, stale 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. --- language-extensions/R/src/Logger.cpp | 5 ++--- .../R/src/linux/IosBaseSentinel_linux.cpp | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/language-extensions/R/src/Logger.cpp b/language-extensions/R/src/Logger.cpp index 8ff2763c..299a5f3d 100644 --- a/language-extensions/R/src/Logger.cpp +++ b/language-extensions/R/src/Logger.cpp @@ -75,7 +75,6 @@ void Logger::Log(const string &msg) #if defined(_DEBUG) || defined(_VERBOSE) string msgWithTimestamp = string(GetCurrentTimestamp()) + msg + "\n"; fwrite(msgWithTimestamp.data(), 1, msgWithTimestamp.size(), stdout); - fflush(stdout); #endif } @@ -135,8 +134,8 @@ const char* Logger::GetCurrentTimestamp() // Name: Logger::LogToStdErr // // Description: -// Logs the given message to stderr; if R is initialized uses its error printing function, -// else writes through stdio. +// Writes the given message to stderr and flushes, matching the unit-buffered behaviour of the +// std::cerr insertion this replaced. // void Logger::LogToStdErr(const string &errorMsgWithTimestamp) { diff --git a/language-extensions/R/src/linux/IosBaseSentinel_linux.cpp b/language-extensions/R/src/linux/IosBaseSentinel_linux.cpp index 616ae5dd..cd2c817e 100644 --- a/language-extensions/R/src/linux/IosBaseSentinel_linux.cpp +++ b/language-extensions/R/src/linux/IosBaseSentinel_linux.cpp @@ -11,10 +11,10 @@ // The other extensions drop the include instead. R cannot: Rcpp.h and RInside.h include // , and every R translation unit reaches them through Common.h. // -// 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. Defining it here removes that dependency, so this file must also perform -// the initialisation the sentinel was guarding: that is the namespace-scope ios_base::Init below. +// The sentinel has no call site - the compiler emits the reference purely so the resulting +// version dependency refuses to load against a libstdc++ that would leave the standard streams +// unconstructed. Defining it here removes that dependency, so this file must also perform the +// initialisation the sentinel was guarding: that is the namespace-scope ios_base::Init below. // A function-local static would never run, because the function is never called. // // extern "C" emits the mangled name verbatim; hidden visibility keeps it out of our exports. @@ -27,8 +27,14 @@ extern "C" __attribute__((visibility("hidden"))) void _ZSt21ios_base_library_ini { } -// Constructs std::cin/cout/cerr/clog at dlopen, before any extension code or statically linked -// Rcpp / RInside code can touch a stream. The constructor is refcounted and idempotent, and is old -// enough to be present in every libstdc++ we target, so it does not raise the ABI floor. +// Constructs std::cin/cout/cerr/clog before any other global constructor in this library can touch +// a stream. The constructor is refcounted and idempotent, and is old enough to be present in every +// libstdc++ we target, so it does not raise the ABI floor. // -static std::ios_base::Init g_iosBaseInit; +// init_priority is required, not cosmetic: dynamic initialisation order between translation units +// is unspecified, so at default priority an Rcpp / RInside global constructor could run first and +// touch a stream that is not yet constructed - exactly the case the sentinel we just defined away +// was guarding against. libstdc++ gives its own centralised initialiser priority 90 for this +// reason. Priorities 0-100 are reserved and warn, so use the lowest available, 101. +// +static std::ios_base::Init g_iosBaseInit __attribute__((init_priority(101)));