From f1e8aaa42320e550a05bb7d39d6c6a49bab33a0e Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Sat, 24 Oct 2026 20:31:46 +0200 Subject: [PATCH] fix(build): keep the static liblbug's libstdc++ instantiations private The Linux prebuilt static archive is compiled with gcc-toolset-13 but is linked by consumers whose own code (e.g. the Rust crate's cxx bridge) is compiled with a newer GCC. Both define the same libstdc++ template instantiations (std::format, ...) as COMDAT groups, the linker keeps one copy per group, and code compiled against one libstdc++ layout calls the other. On Ubuntu 26.04 (GCC 15) any COPY ... TO segfaults in Binder::bindCopyToClause (#1381). Add LBUG_LOCALIZE_STATIC_SYMBOLS (default off, ELF only). When set, the bundling step partially links the archive into one object with --force-group-allocation, makes every defined global local except the `lbug::` and C (`lbug_*`) APIs, and weakens the remaining STB_GNU_UNIQUE definitions, which objcopy cannot localize. Enable it for the Linux compat and perf prebuilts. --- .../workflows/precompiled-bin-workflow.yml | 4 ++ CMakeLists.txt | 1 + cmake/BundleStaticLibrary.cmake | 47 +++++++++++++++++++ src/CMakeLists.txt | 7 +++ 4 files changed, 59 insertions(+) diff --git a/.github/workflows/precompiled-bin-workflow.yml b/.github/workflows/precompiled-bin-workflow.yml index cce8cbb877..ce20756e1e 100644 --- a/.github/workflows/precompiled-bin-workflow.yml +++ b/.github/workflows/precompiled-bin-workflow.yml @@ -143,6 +143,9 @@ jobs: export LDFLAGS="-static-libstdc++" # Ensure CMake finds OpenSSL 3 installed by openssl3 packages inside the manylinux container export EXTRA_CMAKE_FLAGS="-DOPENSSL_ROOT_DIR=/usr/lib64/openssl3 -DOPENSSL_INCLUDE_DIR=/usr/include/openssl3 -DOPENSSL_CRYPTO_LIBRARY=/usr/lib64/openssl3/libcrypto.so -DOPENSSL_SSL_LIBRARY=/usr/lib64/openssl3/libssl.so" + # The static archive is linked by consumers with a newer libstdc++; + # keep its own template instantiations private (see BundleStaticLibrary.cmake). + export EXTRA_CMAKE_FLAGS="$EXTRA_CMAKE_FLAGS -DLBUG_LOCALIZE_STATIC_SYMBOLS=TRUE" make GEN=Ninja make install @@ -153,6 +156,7 @@ jobs: CMAKE_CXX_COMPILER_LAUNCHER: ccache CC: clang-21 CXX: clang++-21 + EXTRA_CMAKE_FLAGS: -DLBUG_LOCALIZE_STATIC_SYMBOLS=TRUE run: | make GEN=Ninja make install diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e69d1bfea..c869dee7e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -376,6 +376,7 @@ option(BUILD_SINGLE_FILE_HEADER "Build single file header. Requires Python >= 3. option(BUILD_LBUG "Build Lbug." TRUE) option(BUILD_SHARED_LBUG "Build the shared liblbug library." TRUE) option(BUILD_STATIC_LBUG "Build the bundled static liblbug archive." TRUE) +option(LBUG_LOCALIZE_STATIC_SYMBOLS "Make every symbol of the bundled static liblbug archive except the Ladybug API local, so it can be linked next to code built by a different compiler (ELF only)." FALSE) option(LBUG_API_USE_PRECOMPILED_LIB "Link language bindings against a precompiled static liblbug archive." FALSE) set(LBUG_API_PRECOMPILED_LIB_PATH "" CACHE FILEPATH "Path to the precompiled static liblbug archive used by language bindings.") option(LBUG_NODEJS_USE_PRECOMPILED_LIB "Deprecated alias for LBUG_API_USE_PRECOMPILED_LIB." FALSE) diff --git a/cmake/BundleStaticLibrary.cmake b/cmake/BundleStaticLibrary.cmake index 75b7bb14b7..6fcf699a46 100644 --- a/cmake/BundleStaticLibrary.cmake +++ b/cmake/BundleStaticLibrary.cmake @@ -39,6 +39,53 @@ else() RESULT_VARIABLE BUNDLE_RESULT COMMAND_ERROR_IS_FATAL ANY) file(REMOVE "${MRI_SCRIPT}") + + if(LOCALIZE_SYMBOLS) + # The archive bundles its own instantiations of libstdc++ templates + # (std::format, std::string, ...) as COMDAT groups. A consumer built + # with a different GCC has its own, and the linker keeps only one copy + # per group, so code compiled against one libstdc++ layout ends up + # calling the other (e.g. a segfault in Binder::bindCopyToClause when + # the GCC 13 prebuilt is linked on a GCC 15 system). Give the archive + # private copies of everything that isn't the Ladybug API: + # 1. Partially link it into one object, turning COMDAT group members + # into ordinary sections so the final link can't discard them. + # 2. Make every defined global local except `lbug::` (mangled + # `4lbug`) and the C API (`lbug_*`). + # 3. Weaken the remaining STB_GNU_UNIQUE definitions (function-local + # statics, inline variables), which objcopy cannot localize and + # which would otherwise be duplicate symbols. Only defined symbols: + # a weakened undefined reference would resolve to 0. + set(COMBINED_OBJECT "${OUTPUT}.combined.o") + set(UNIQUE_SYMBOLS_FILE "${OUTPUT}.unique.txt") + execute_process( + COMMAND "${LD_EXECUTABLE}" -r --force-group-allocation + --whole-archive "${BUNDLED_LIBRARY}" -o "${COMBINED_OBJECT}" + COMMAND_ERROR_IS_FATAL ANY) + execute_process( + COMMAND "${OBJCOPY_EXECUTABLE}" --wildcard + "--keep-global-symbol=*4lbug*" + "--keep-global-symbol=lbug_*" + "${COMBINED_OBJECT}" + COMMAND_ERROR_IS_FATAL ANY) + execute_process( + COMMAND "${NM_EXECUTABLE}" -g --defined-only "${COMBINED_OBJECT}" + OUTPUT_VARIABLE GLOBAL_SYMBOLS + COMMAND_ERROR_IS_FATAL ANY) + string(REGEX MATCHALL "[^\n]* u [^\n]*" UNIQUE_LINES "${GLOBAL_SYMBOLS}") + list(TRANSFORM UNIQUE_LINES REPLACE "^.* u " "") + list(JOIN UNIQUE_LINES "\n" UNIQUE_SYMBOLS) + file(WRITE "${UNIQUE_SYMBOLS_FILE}" "${UNIQUE_SYMBOLS}\n") + execute_process( + COMMAND "${OBJCOPY_EXECUTABLE}" "--weaken-symbols=${UNIQUE_SYMBOLS_FILE}" + "${COMBINED_OBJECT}" + COMMAND_ERROR_IS_FATAL ANY) + file(REMOVE "${BUNDLED_LIBRARY}") + execute_process( + COMMAND "${AR_EXECUTABLE}" rcs "${BUNDLED_LIBRARY}" "${COMBINED_OBJECT}" + COMMAND_ERROR_IS_FATAL ANY) + file(REMOVE "${COMBINED_OBJECT}" "${UNIQUE_SYMBOLS_FILE}") + endif() endif() file(RENAME "${BUNDLED_LIBRARY}" "${OUTPUT}") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 91c6153229..75fb802543 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -54,6 +54,9 @@ if(TARGET lbug) list(APPEND LBUG_STATIC_ARCHIVE_PATHS "$") endforeach() set(LBUG_STATIC_ARCHIVE_PATH_FILE "${CMAKE_CURRENT_BINARY_DIR}/lbug_static_archives_$.txt") + if(LBUG_LOCALIZE_STATIC_SYMBOLS AND NOT CMAKE_EXECUTABLE_FORMAT STREQUAL "ELF") + message(FATAL_ERROR "LBUG_LOCALIZE_STATIC_SYMBOLS is only supported for ELF targets") + endif() file(GENERATE OUTPUT "${LBUG_STATIC_ARCHIVE_PATH_FILE}" CONTENT "$\n") @@ -64,6 +67,10 @@ if(TARGET lbug) "-DSTATIC_LIBRARIES_FILE=${LBUG_STATIC_ARCHIVE_PATH_FILE}" "-DAR_EXECUTABLE=${CMAKE_AR}" "-DRANLIB_EXECUTABLE=${CMAKE_RANLIB}" + "-DLOCALIZE_SYMBOLS=${LBUG_LOCALIZE_STATIC_SYMBOLS}" + "-DLD_EXECUTABLE=${CMAKE_LINKER}" + "-DOBJCOPY_EXECUTABLE=${CMAKE_OBJCOPY}" + "-DNM_EXECUTABLE=${CMAKE_NM}" -P "${PROJECT_SOURCE_DIR}/cmake/BundleStaticLibrary.cmake" VERBATIM) endif()