Add opt-in QUIC_OPENSSL_SYMBOL_PREFIX to namespace bundled OpenSSL symbols (Linux)#6029
Add opt-in QUIC_OPENSSL_SYMBOL_PREFIX to namespace bundled OpenSSL symbols (Linux)#6029leikong wants to merge 2 commits into
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.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6029 +/- ##
==========================================
+ Coverage 85.03% 85.55% +0.51%
==========================================
Files 60 60
Lines 18792 18792
==========================================
+ Hits 15980 16077 +97
+ Misses 2812 2715 -97 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds an opt-in CMake cache variable (QUIC_OPENSSL_SYMBOL_PREFIX) to namespace-prefix all globally visible symbols in MsQuic’s bundled OpenSSL static archives on Linux, and rewrites MsQuic’s OpenSSL undefined references to match. This mitigates symbol collisions when a process contains another OpenSSL copy.
Changes:
- Introduces
QUIC_OPENSSL_SYMBOL_PREFIXoption with Linux-only / bundled-OpenSSL-only validation and wiring. - Adds CMake + shell helpers to generate an
nm-derived symbol map and applyobjcopy --redefine-symsto produce prefixed archives. - Applies a
POST_BUILDrename step tolibmsquic_platform.aso MsQuic’s OpenSSL references resolve against the prefixed archives; adds new documentation.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
CMakeLists.txt |
Adds the cache option, validation, module include, and routes OpenSSL interface target to prefixed archives when enabled. |
src/platform/CMakeLists.txt |
Adds a POST_BUILD step to rewrite OpenSSL references inside libmsquic_platform.a when the option is enabled. |
cmake/PrefixOpenSSLArchives.cmake |
New CMake helper that generates a redefine-syms map and creates prefixed copies of libssl.a/libcrypto.a. |
cmake/openssl-prefix-rename.sh |
New helper script implementing gen-syms and apply modes via nm and objcopy. |
docs/OpenSSLSymbolPrefix.md |
New documentation describing motivation, usage, constraints, and verification steps. |
leikong
left a comment
There was a problem hiding this comment.
Deep-mode code review (Claude Opus 4.7 extra-high + GPT-5.5 blind parallel Pass 1s, merged by Claude Opus 4.7 high reasoning). 2 major + 7 minor + 1 grouped nit. Highest-impact summary:
- major Configure-time zero-byte placeholders for the renamed
.afiles can shadow real outputs when source archives are restored from a CI cache with older mtimes — link silently consumes empty archives.cmake/PrefixOpenSSLArchives.cmakelines 156-167. - major
BUILD_SHARED_LIBS=ON+QUIC_OPENSSL_SYMBOL_PREFIX=<set>breaksinstall(EXPORT msquic)becauseOpenSSL's link interface gains an unexportedOpenSSLQuicPrefixed(declaredINTERFACE IMPORTED GLOBAL). Either reject the combination, dropIMPORTEDand addOpenSSLQuicPrefixedto the export set, or skip the OpenSSL-targets export when prefix is set. - minor (both passes) The nm filter
[TDRBWVC]omits IFUNC typesi/I; not exercised by the current bundled OpenSSL config but trivially-safe defensive coverage for future submodule bumps. - minor (both passes)
QUIC_OPENSSL_SYMBOL_PREFIXis unvalidated and is interpolated both as a symbol prefix and as a build-tree path component — needs a^[A-Za-z_][A-Za-z0-9_]*$guard. - minor Several other items: nm stderr swallowed, dlsym-based OpenSSL plug-ins escape rename (docs), POST_BUILD lacks OBJECT_DEPENDS on the syms file,
INTERFACE_LINK_LIBRARIESregex is fragile,add_dependencieson anINTERFACE IMPORTEDtarget is a no-op, docs verification commands havegrep -vcexit-code traps. - nit (grouped) Thin-archive guard, LTO incompatibility callout,
readlink -fportability, warn when prefix is set withQUIC_TLS_LIB=schannel,CMAKE_MODULE_PATHpollution inadd_subdirectory, two documentation accuracy nits.
No blockers. Default-disabled path is byte-identical, agreed.
leikong
left a comment
There was a problem hiding this comment.
Code review — deep mode (Opus-extra-high + GPT-5.5 blind parallel Pass 1s; Opus high-reasoning merge & audit)
Five major findings, mostly around the POST_BUILD step's fragility in CMake's dependency graph and missing validation. The opt-in design and Linux-only gate are sound; concerns are about edge cases that silently produce broken/un-prefixed binaries — exactly the failure modes this PR is meant to prevent.
Highlights (by severity)
- major — POST_BUILD rename is outside the dependency graph. The rename on
libmsquic_platform.awon't re-fire when onlyQUIC_OPENSSL_SYMBOL_PREFIX,redefine.syms, oropenssl-prefix-rename.shchanges — and won't re-fire after an interruption betweenarandobjcopy. Reconfiguring with a new prefix on an existing build tree produces a corrupted incremental build that silently leaks unprefixed OpenSSL symbols (the exact failure mode this PR exists to prevent). Restructure as a trackedadd_custom_command(OUTPUT ...)that produces a separatelibmsquic_platform.prefixed.aand route consumers at that artifact. - major — Prefix charset/length validation missing. The value is interpolated straight into
objcopy --redefine-symslines and into theopenssl-prefixed/<PREFIX>/path. Whitespace,#, leading digit,/,.., shell metachars all silently corrupt the rename map or build tree. Reject prefixes not matching^[A-Za-z_][A-Za-z0-9_]*$withFATAL_ERROR. - major — Validation block silently ignored with
QUIC_TLS_LIB=schannel. The whole prefix block lives insideif(QUIC_TLS_LIB STREQUAL quictls OR ...openssl). SettingQUIC_OPENSSL_SYMBOL_PREFIXwithschannel(or unset on Windows) is silently no-op'd — no FATAL_ERROR, no warning. Lift the prefix-set guard out so it errors loudly with an incompatible TLS lib. - major —
install(EXPORT msquic)broken when prefixing is enabled. WithBUILD_SHARED_LIBS=ON,OpenSSL's INTERFACE_LINK_LIBRARIES now referencesOpenSSLQuicPrefixed(INTERFACE IMPORTED GLOBAL), which can't be exported. The generatedshare/msquic/msquic-targets.cmakewill fail at install or reference build-tree-only paths. - major —
awkregex^[TDRBWVC]$missesI(IFUNC) andu(unique-global). OpenSSL 3.x on x86_64 Linux does ship IFUNCs (e.g. AES-NI/SHA-NI dispatch resolvers). Those defined externs won't be in the rename map → MsQuic's undefs to those names don't get renamed at POST_BUILD → unprefixed OpenSSL symbols leak through, again defeating the PR's purpose.
Minor findings
gen-symsswallowsnmstderr with2>/dev/nulland treats a 0-line syms file as success. Real errors (bad cross-compile nm, malformed archive) are hidden, and a silently-empty syms file produces a no-op rename — build looks green, symbols are not prefixed.applymode doescp→objcopyin place on the destination with no trap. Ctrl-C between the two leaves a fresh, valid copy of the un-renamed input with a newer mtime than the source → next build skips the rename.file(WRITE "")placeholder pattern inPrefixOpenSSLArchives.cmakeis unnecessary and masks a failure mode: if the custom_command silently fails to run, consumers see a 0-byte archive, whichar/ldaccept as valid-but-empty — final link succeeds with NO OpenSSL implementation, deferring failure to runtime.cmake/PrefixOpenSSLArchives.cmake_out_dirparsesOpenSSLQuic'sINTERFACE_LINK_LIBRARIESvia a hardcodedlibssl\.a$/libcrypto\.a$regex. Fragile against any future genexpr wrapping of the Linux paths — the FATAL_ERROR doesn't include the genexpr content, making diagnosis hard.- Inconsistent env-var forwarding:
PrefixOpenSSLArchives.cmakeconditionally forwardsNM=/OBJCOPY=only when set;src/platform/CMakeLists.txtalways forwards them, soCMAKE_NM=""(empty) silently re-defaults to hostnmeven on cross-builds. - Documentation under-states the reconfigure caveat: changing
QUIC_OPENSSL_SYMBOL_PREFIXin an existing build tree requires a clean /--freshconfigure.
Nits
src/platform/CMakeLists.txtcomment referencessubmodules/CMakeLists.txtfor the rename wiring; the rename actually lives incmake/PrefixOpenSSLArchives.cmakeinvoked from the top-levelCMakeLists.txt.docs/OpenSSLSymbolPrefix.mdverification recipe usesnm -gC(demangling) but the rename script doesn't — false-positive risk if downstream consumers contain any C++ TUs.- Doc example uses
mymsquic_literally while the same doc tells users to pick a globally-unique prefix; copy-paste produces collisions across projects. awk 'NF==3'strict-equality filter is fragile vs. nm-wrappers that emit<member>: <addr> <type> <name>(4 fields).- No
command -vexistence check on$NM/$OBJCOPYbefore invocation. - The directory
openssl-prefixed/<OLD_PREFIX>/is orphaned in the build tree when the prefix changes — disk leak.
(Multi-model deep review; merge by Claude Opus 4.7 high-reasoning. Inline comments below mark the exact lines.)
* 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.
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.