Add opt-in QUIC_OPENSSL_SYMBOL_PREFIX to namespace bundled OpenSSL symbols (Linux)#6031
Conversation
…mbols
When MsQuic is statically linked into a process that also pulls in another
copy of OpenSSL (e.g. a system libcrypto.so.3 brought in transitively by an
unrelated dependency), the two OpenSSL copies share global C symbols and
the dynamic linker resolves every reference to whichever was loaded first.
The result is silent state-sharing between two OpenSSLs whose headers and
ABIs may not match - typically a crash in OPENSSL_init_crypto, spurious SSL
handshake failures, or worse.
This change introduces an opt-in cache variable, QUIC_OPENSSL_SYMBOL_PREFIX,
that when non-empty:
1. Generates a redefine-syms file from the bundled libssl.a and
libcrypto.a using 'nm --defined-only --extern-only', mapping each
<sym> to <prefix><sym>.
2. Produces prefixed copies of both archives via
'objcopy --redefine-syms=<file>'.
3. Applies the same redefine-syms to libmsquic_platform.a as a POST_BUILD
step so MsQuic's own undefined references to OpenSSL get rewritten too.
4. Routes the existing OpenSSL interface target at the prefixed archives.
The resulting libmsquic.{a,so} has no externally-visible OpenSSL symbols
that match a normal OpenSSL build, so a second OpenSSL in the same process
cannot resolve against it (or vice versa).
Constraints
-----------
- Linux only. macOS would need llvm-objcopy >= 13 (untested); PE/COFF lacks
a flat-namespace symbol table and would need an entirely different
approach. Rejected with FATAL_ERROR on other platforms.
- Bundled OpenSSL only. External/system OpenSSL is owned by the caller and
cannot be renamed; rejected with FATAL_ERROR if combined with
QUIC_USE_EXTERNAL_OPENSSL, QUIC_OPENSSL_INCLUDE_DIR, QUIC_OPENSSL_LIB_DIR,
QUIC_OPENSSL_ROOT_DIR, or QUIC_USE_SYSTEM_LIBCRYPTO.
- Cross-compile aware: ${CMAKE_NM} and ${CMAKE_OBJCOPY} are honored.
Backward compatibility
----------------------
QUIC_OPENSSL_SYMBOL_PREFIX defaults to empty. When empty, no new code runs
and the build is byte-for-byte identical to the prior behavior.
Files
-----
- cmake/openssl-prefix-rename.sh helper script (gen-syms / apply)
- cmake/PrefixOpenSSLArchives.cmake helper function
- CMakeLists.txt option + plumbing
- src/platform/CMakeLists.txt POST_BUILD rename on libmsquic_platform.a
- docs/OpenSSLSymbolPrefix.md motivation, usage, caveats
Verification
------------
Local build with -DQUIC_OPENSSL_SYMBOL_PREFIX=msqtest_ -DQUIC_TLS_LIB=quictls
-DQUIC_BUILD_SHARED=OFF on Linux x86_64:
- libssl.a: 0 unprefixed defined globals (all prefixed with msqtest_).
- libcrypto.a: 0 unprefixed defined globals.
- libmsquic_platform.a: 0 unprefixed OpenSSL undefs, 161 prefixed undefs.
Local build with no option set: behavior unchanged, openssl-prefixed/ dir
not created.
The same prefix-rename technique has been deployed in production in
Microsoft's meru codebase (consumer of msquic) across x86_64 Release,
ASAN, UBSAN, TSAN, clang-tidy, and aarch64 cross-compile configurations.
* CMakeLists.txt: add outer-most QUIC_OPENSSL_SYMBOL_PREFIX validation that
errors on non-quictls/openssl TLS_LIB (instead of silently ignoring the
value), enforces a ^[A-Za-z_][A-Za-z0-9_]*$ charset/length on the prefix,
and blocks the QUIC_BUILD_SHARED=ON path with FATAL_ERROR until the
prefixed IMPORTED target is wired into install(EXPORT msquic).
* cmake/openssl-prefix-rename.sh:
- command -v check on NM/OBJCOPY for clearer config errors;
- awk symbol-type set broadened to include I (STT_GNU_IFUNC, used by
OpenSSL 3.x AES-NI/SHA-NI dispatch resolvers) and switched to NF>=3
so non-IFUNC nm output with extra columns still matches;
- stop swallowing nm stderr;
- empty-syms-file guard so a misconfigured nm fails loudly instead of
letting objcopy --redefine-syms run as a no-op;
- apply mode now does atomic tmp+rename with a trap when in_ar != out_ar.
* cmake/PrefixOpenSSLArchives.cmake:
- drop the file(WRITE "") placeholder libssl.a/libcrypto.a, which masked
real failures from gen-syms/apply;
- broaden the libssl/libcrypto INTERFACE_LINK_LIBRARIES fatal-error to
surface the actual property value and explain the genex limitation;
- document the single-config build-tree path assumption.
* src/platform/CMakeLists.txt:
- fix misleading comment (rename source location);
- mirror the helper's conditional NM=/OBJCOPY= env forwarding so unset
CMAKE_NM/CMAKE_OBJCOPY does not forward empty strings.
* docs/OpenSSLSymbolPrefix.md:
- replace mymsquic_ examples with <your_prefix> placeholder and call out
the charset requirement;
- add a Changing the prefix value section documenting the platform.a
POST_BUILD staleness and the clean-rebuild workaround;
- add BUILD_SHARED_LIBS=OFF and prefix-change-requires-rebuild rows to
the constraints table;
- update Verification recipe: use 'nm -g' (the script does not demangle),
mirror the script's awk symbol-type set including I, use grep -v|wc -l
to avoid grep -vc's non-zero exit on count=0, and use the actual
build/linux/<arch>_<tls>/bin path.
Build + symbol-rename verified with -DQUIC_TLS_LIB=quictls
-DQUIC_OPENSSL_SYMBOL_PREFIX=msqtest_ -DQUIC_BUILD_SHARED=OFF on Linux x86_64:
0 unprefixed defined-extern globals in libssl.a, 0 unprefixed OpenSSL undefs
in libmsquic_platform.a.
There was a problem hiding this comment.
Pull request overview
This PR introduces an opt-in build mechanism to namespace-prefix all bundled OpenSSL symbols (Linux, bundled OpenSSL only) to avoid global symbol collisions when MsQuic is statically linked into processes that also load another OpenSSL copy.
Changes:
- Adds a new CMake cache variable
QUIC_OPENSSL_SYMBOL_PREFIXwith validation and Linux-only enforcement in the bundled-OpenSSL build path. - Implements CMake + shell helpers to generate an
objcopy --redefine-symsmap, produce prefixedlibssl.a/libcrypto.a, and rewrite MsQuic’s OpenSSL references inlibmsquic_platform.a. - Adds documentation describing motivation, constraints, usage, and verification steps.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
CMakeLists.txt |
Adds QUIC_OPENSSL_SYMBOL_PREFIX, validates constraints, and wires in the prefixing flow for bundled OpenSSL. |
cmake/PrefixOpenSSLArchives.cmake |
New CMake helper to generate syms map and produce renamed OpenSSL static archives + exported paths. |
cmake/openssl-prefix-rename.sh |
New helper script that generates the rename map with nm and applies it with objcopy. |
src/platform/CMakeLists.txt |
Adds a POST_BUILD step to rewrite OpenSSL references inside libmsquic_platform.a when prefixing is enabled. |
docs/OpenSSLSymbolPrefix.md |
New documentation for the feature, constraints, and verification. |
- openssl-prefix-rename.sh: always use tmp-file + atomic rename in apply mode, even when in_ar and out_ar resolve to the same path. POSIX rename(2) over an existing file is atomic, so the POST_BUILD step on libmsquic_platform.a is now crash-safe. - docs/OpenSSLSymbolPrefix.md: Usage example now passes -DQUIC_BUILD_SHARED=OFF explicitly so the documented command line matches the FATAL_ERROR guard. - docs/OpenSSLSymbolPrefix.md: Verification recipe targets libmsquic_platform.a (the static output that this feature actually produces) instead of libmsquic.so, and uses the full set of OpenSSL symbol prefixes (X509_, OPENSSL_, RAND_, RSA_, EC_, BIO_, ASN1_, PEM_, CRYPTO_) so the check is not falsely green when only SSL_/EVP_/BN_/ERR_ references are rewritten.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6031 +/- ##
==========================================
+ Coverage 85.03% 85.44% +0.40%
==========================================
Files 60 60
Lines 18792 18798 +6
==========================================
+ Hits 15980 16062 +82
+ Misses 2812 2736 -76 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@leikong We are currently working toward making OpenSSL 3.5 the default option for distributions where it is available, with dynamic linking instead of static linking (which is an artifact of what was needed for QuicTLS). |
OpenSSL 3.5 is not available on Ubuntu versions older than Ubuntu 26.04, and my team will not be able to move to 26.04 or newer any time soon. We are trying to enable static builds to avoid the need to copy MsQuic so files, in our environment, loading MsQuic so files from non-standard folders will require root, which we are trying to avoid. |
Address Copilot review: the doc previously said the result is a `libmsquic.so` (or static archive). Since QUIC_OPENSSL_SYMBOL_PREFIX forces BUILD_SHARED_LIBS=OFF (enforced by the FATAL_ERROR guard added in this PR), the prefixed-symbol mode never produces libmsquic.so directly. Reword to describe the output in terms of libmsquic.a plus the prefixed libssl.a/libcrypto.a, with shared-library packaging explicitly noted as the consumer's responsibility.
|
EDIT: Delete the previous comment, I was not getting the right scenario. This is to run with OpenSSL 3.5 on a Linux distro without OpenSSL 3.5, and with the intention to statically link MsQuic + another dependency that dynamically link the system OpenSSL. Unless it is possible for the project to dynamically link MsQuic (which solves the issue), this symbol renaming is needed. |
guhetier
left a comment
There was a problem hiding this comment.
The approach seems ok.
It would be good to find a way to factor the logic around OpenSSL in its own cmake file, as a dependency of the platform, since it has grown in complexity so much, but that isn't in scope for this PR.
…, target-prop wiring - docs: add a Scope section clarifying the feature only applies when the app links MsQuic statically AND MsQuic bundled OpenSSL statically; note the upcoming move to dynamic system-OpenSSL linking - docs: link openssl/openssl#20766 in Future direction and state plainly that upstream closed it declining a prefix mechanism, so this is a standing workaround - cmake: trim the inline rationale at the platform POST_BUILD call site and the historical NOTE blocks in PrefixOpenSSLArchives.cmake - cmake: replace the CACHE INTERNAL syms/script paths with target properties (PREFIX_RENAME_SYMS_FILE / PREFIX_RENAME_SCRIPT) read via get_target_property at the consumer site
guhetier
left a comment
There was a problem hiding this comment.
Seems good to me.
As a disclaimer, please treat this as an unstable feature we will feel free to do breaking changes between versions as we improve the way we ingest OpenSSL.
|
@guhetier |
|
Done, thanks for the PR! |

Description
This PR adds an opt-in CMake cache variable,
QUIC_OPENSSL_SYMBOL_PREFIX, that namespace-prefixes every globally-visible symbol in the bundled OpenSSL static archives (and rewrites MsQuic's own undefined references to match). WhenQUIC_OPENSSL_SYMBOL_PREFIXis left empty (the default), no new code runs and the build is byte-for-byte identical to today.Motivation
When MsQuic is statically linked into a process that also pulls in another copy of OpenSSL — for example, a system
libcrypto.so.3brought in transitively by an unrelated dependency (a logging library, a database client, any C++ library that itself uses OpenSSL) — the two OpenSSL copies share the same global C symbols (SSL_CTX_new,EVP_*,BN_*,ERR_*, the per-module init constructors, …). The dynamic linker resolves every reference to the first definition loaded, so all callers — including MsQuic — silently end up sharing one OpenSSL's state machine while their headers and ABI assumptions came from the other. Typical symptoms:OPENSSL_init_crypto/RAND_load_filewhen one OpenSSL's per-module init runs against the other's global registries.SSL_CTXsee the other's vtable layout.3.0.x(systemlibcrypto.so.3on Ubuntu 22.04 / RHEL 9) and MsQuic's bundled OpenSSL is3.5.x(required forSSL_set_quic_tls_cbs).Note that MsQuic's bundled-OpenSSL path always links
libssl.a/libcrypto.astatically into MsQuic, regardless ofQUIC_BUILD_SHARED—SHAREDjust controls whether the result islibmsquic.soorlibmsquic.a. BuildingSHAREDwith--exclude-libs=ALLtherefore strips the (statically linked) OpenSSL symbols fromlibmsquic.so's dynamic export table, which is sufficient when the consumerdlopens/links againstlibmsquic.soitself. It does not help:libmsquic.ainto a final binary — symbols flow straight into the executable's global table.libmsquic.ainto their own.so— same exposure unless they also--exclude-libsit.libcryptos coexist in one address space and OpenSSL's process-global init/registries (OPENSSL_init_crypto, error/atexit tables,RANDstate) can still collide.How it works
When
QUIC_OPENSSL_SYMBOL_PREFIX=<prefix>is passed at CMake configure time, the build:libssl.aandlibcrypto.a.nm --defined-only --extern-onlyand writes a redefine-syms file mapping each<sym>to<prefix><sym>.objcopy --redefine-syms=<file>(touches both definitions and undefined references inside each member object).--redefine-symsstep as aPOST_BUILDaction onlibmsquic_platform.aso MsQuic's own undefined references to OpenSSL (fromtls_openssl.c,tls_quictls.c,crypt_openssl.c,selfsign_openssl.c) get rewritten to match.OpenSSLinterface target at the prefixed archives, so the rest of the build is unchanged.The result is a
libmsquic.{a,so}whose only externally-visible OpenSSL symbols are the prefixed ones. The dynamic linker has no reason to resolve them against any other OpenSSL copy present in the same process.Constraints
CX_PLATFORM=linux)objcopy --redefine-syms. macOS would needllvm-objcopy>= 13 (untested); PE/COFF lacks a flat-namespace symbol table. Rejected withFATAL_ERRORon other platforms.FATAL_ERRORif combined withQUIC_USE_EXTERNAL_OPENSSL,QUIC_OPENSSL_INCLUDE_DIR,QUIC_OPENSSL_LIB_DIR,QUIC_OPENSSL_ROOT_DIR, orQUIC_USE_SYSTEM_LIBCRYPTO.${CMAKE_NM}/${CMAKE_OBJCOPY}are forwarded to the helper script, soaarch64-linux-gnu-objcopyetc. are used when configured.Files
cmake/openssl-prefix-rename.sh— helper script (gen-syms/applymodes; honorsNM/OBJCOPYenv vars).cmake/PrefixOpenSSLArchives.cmake— helper functionprefix_openssl_archives(PREFIX … INPUT_TARGET … OUTPUT_TARGET …).CMakeLists.txt— new option + validation + plumbing in the bundled-OpenSSL branch.src/platform/CMakeLists.txt—POST_BUILDrename onlibmsquic_platform.a.docs/OpenSSLSymbolPrefix.md— motivation, usage, caveats, verification recipe.Future direction
The cleanest long-term solution is for OpenSSL itself to expose a configure-time
--symbol-prefix=option that compiles every public symbol with the prefix baked in (an analog of BoringSSL'sBSSL_NAMESPACEor LibreSSL's recurring discussion). I plan to file that issue upstream with this PR linked as concrete prior art demonstrating consumer demand. Until that lands, this CMake helper provides an equivalent at link time without requiring an OpenSSL fork.Testing
Manual verification (local, this PR's branch)
Built on Linux x86_64 with
-G Ninja -DQUIC_TLS_LIB=quictls -DQUIC_BUILD_SHARED=OFF:With
-DQUIC_OPENSSL_SYMBOL_PREFIX=msqtest_:Without the option (default build):
libmsquic_platform.a.build/openssl-prefixed/directory not created.Production validation
The same prefix-rename technique has been deployed in Microsoft's meru codebase (a consumer of MsQuic). It is currently validated across this matrix:
Releasex86_64Release_ASAN,Debug_ASAN,Debug_UBSAN,Debug_TSANx86_64arm_debug_crosscompile(aarch64)clang_tidySee microsoft/meru-common#4011 for the consumer-side integration that ports the same helper script + CMake module pattern that this PR upstreams.
MsQuic CI coverage
The default (option-empty) path is unchanged, so existing CI should be unaffected. The option-set path is Linux-only and opt-in, so it does not need to enter the default matrix. Happy to add a single Linux CI leg that exercises
-DQUIC_OPENSSL_SYMBOL_PREFIX=msquic_if maintainers think that's worthwhile.Documentation
New file:
docs/OpenSSLSymbolPrefix.mddocuments motivation, usage, constraints, and a verification recipe.