Linux: statically linked OpenSSL 1.1.1 exports leak from engine libraries and corrupt other OpenSSL users in the same process
The Linux build links the 3rdParty OpenSSL 1.1.1t statically into engine shared libraries with default symbol visibility (no shared libcrypto or libssl ships in the output). The practical result is that libAzFramework.so exports on the order of 1700 OpenSSL symbols, unversioned: on a current development profile build, nm -D --defined-only counts 1733 exports matching the EVP_/SSL_/MD5/SHA/X509/BIO_ families, and EVP_DigestInit, EVP_DigestInit_ex and friends appear as plain T exports with no version suffix. Any other library in the same process that uses a different OpenSSL can have its calls captured by those exports, because the dynamic loader lets an unversioned definition satisfy a versioned reference when it is found earlier in the lookup order.
Minimal reproduction, needing no O3DE runtime, only a stock Linux build and the system python:
LD_PRELOAD=<build>/bin/profile/libAzFramework.so python3 -c "import hashlib; hashlib.md5(b'x')"On a current development build this segfaults through a null pointer inside _hashlib_openssl_md5, the same crash the engine hits through its own embedded python. The loader trace explains it exactly. Under LD_DEBUG=bindings, _hashlib's OpenSSL resolution splits by symbol version: its explicitly versioned OpenSSL 3 requests (for example EVP_MD_get_size [OPENSSL_3.0.0]) bind correctly to /lib64/libcrypto.so.3, while its unversioned requests, EVP_DigestInit_ex and EVP_DigestUpdate among them, bind into libAzFramework.so (on this build, 374 of _hashlib's unversioned OpenSSL imports resolve into the engine library against 11 that stay versioned to libcrypto.so.3). So one hashlib.md5 call takes an EVP_MD handle shaped by OpenSSL 3 and runs it through EVP_DigestInit_ex from 1.1.1t, which reads that handle with the wrong structure layout. Versioning protects only the symbols that carry a version tag; the many unversioned OpenSSL entry points are captured. Abbreviated stack (identical from the one-liner or an AssetBuilder worker):
#0 0x0000000000000000 in ?? ()
#1 _hashlib_HASH (/usr/lib64/python3.14/lib-dynload/_hashlib.cpython-314-x86_64-linux-gnu.so)
#2 _hashlib_openssl_md5
#12 import_find_and_load
#19 EditorPythonBindings::Internal::PythonProxyNotificationHandler::OnEventGenericHook ("OnUpdateManifest")
#27 AZ::SceneAPI::Behaviors::ScriptBuildingNotificationBusHandler::OnUpdateManifestThe same class of capture applies to anything else that meets a second OpenSSL in-process on Linux, and default builds carry at least one concrete candidate: the bundled Qt ships its TLS backend (plugins/tls/libqopensslbackend.so) with no ssl or crypto DT_NEEDED entries, resolving OpenSSL at runtime, and on a typical Linux host that resolution finds the system OpenSSL 3. Any TLS use through QtNetwork inside the Editor or Project Manager therefore brings a second OpenSSL into a process whose global scope already exports the 1.1.1t symbols. We have not built a reproduction on that path, but the loader rules are the same ones behind the python crash above. Beyond Qt, the same applies to distro curl pulled in by a gem, GStreamer, ODBC drivers, or any python extension. Which side wins depends on library load order, so failures are configuration-dependent and show up far from the cause.
Two aggravating factors worth separating:
- Symbol visibility. Even keeping a bundled OpenSSL, linking a static library into shared objects without hiding its symbols turns every engine library into an accidental OpenSSL provider. Hiding them (exclude-libs or a linker version script for the OpenSSL objects) would contain the bundled copy to its intended consumers. One caveat this needs to handle: a few engine libraries (the Multiplayer gem and AutomatedTesting) currently import OpenSSL symbols and depend on libAzFramework, so they may be resolving OpenSSL through AzFramework's re-exports today rather than linking their own copy; a containment change has to give those consumers a direct OpenSSL link so it does not just move the breakage. Sorting that out is part of why this is filed as an issue first.
- The bundled copy itself. Linux used the system default OpenSSL by explicit decision in #8366 (merged 2022-03-23), with the rationale that the distribution keeps it patched. The bundled 1.1.1t association reappeared when the Linux package list was restructured in early June 2025; it entered through the resolution of the merge that landed 2025-06-05 rather than through any change that discussed OpenSSL, so there is no sign the #8366 decision was deliberately revisited. 1.1.1 has been end of life since 2023-09-11. The in-flight OpenSSL 3.6.3 work (3p package already updated, engine-side #19974 in draft) would resolve the EOL half; restoring the system OpenSSL on Linux per #8366 would resolve both halves at once. We have verified the engine compiles against a system OpenSSL 3.5.7 on Linux without source changes and that asset processing and the Editor run normally on that build; TLS-exercising paths (multiplayer certificate flows as in #8366's original testing) have not been re-run and would be part of making this a PR.
A note on the in-flight OpenSSL 3.6.3 bump (#19974): bumping the bundled copy would very likely stop this specific crash, because both sides would then be OpenSSL 3.x and structurally compatible, so the wrong-library binding becomes harmless. It would not remove the symbol leak itself: a statically linked 3.6.3 with default visibility still exports its OpenSSL symbols unversioned and still captures any in-process OpenSSL user, staying benign only as long as everything in the process is on ABI-compatible OpenSSL 3.x. So this is a distinct build-hygiene issue rather than a duplicate of that bump, and worth deciding on its own terms (contain the symbols, or restore the system OpenSSL per #8366).
Happy to provide the full reproduction (the one-liner above is deterministic) or to turn either remedy into a PR.
Source: o3de/o3de