AWS-LC uses ELF symbol versioning for its shared libraries on UNIX systems (Linux and BSDs). Symbol versioning provides:
- ABI Stability Tracking: Each exported symbol is assigned to a specific version namespace
- Backward Compatibility: Applications link to specific symbol versions, preventing breakage
- Concurrent Installation: Multiple AWS-LC versions can coexist on the same system
- Distribution Packaging: Standard practice for system libraries in Linux distributions
Symbol versioning is enabled by default in distribution packaging mode
(-DENABLE_DIST_PKG=1), but it is an independent option: see
Enabling Symbol Versioning.
AWS-LC assigns every exported symbol to a version node. Both libcrypto and libssl share the same symbol version namespace:
- AWS_LC_1.0 (current baseline - ~3,000 libcrypto symbols, ~640 libssl symbols)
When you link an application against AWS-LC, the linker records which symbol versions your application uses. At runtime, the dynamic linker ensures your application gets the correct symbol versions.
// Your application code
#include <openssl/evp.h>
int main() {
EVP_MD_CTX *ctx = EVP_MD_CTX_new(); // Uses EVP_MD_CTX_new@@AWS_LC_1.0
// ...
}When compiled and linked:
$ gcc myapp.c -o myapp -lcrypto-awslc
$ nm -D myapp | grep EVP_MD_CTX_new
U EVP_MD_CTX_new@@AWS_LC_1.0The @@AWS_LC_1.0 suffix indicates your application requires the AWS_LC_1.0 version of EVP_MD_CTX_new.
- UNIX system (Linux or BSD)
- GNU ld or compatible linker
- CMake 3.0+
- Ninja or Make
Symbol versioning is controlled by ENABLE_SYMBOL_VERSIONING, which defaults to
the value of ENABLE_DIST_PKG and can be set explicitly to override that:
# Distribution packaging: versioning on by default.
cmake -GNinja -B build -DBUILD_SHARED_LIBS=ON -DENABLE_DIST_PKG=ON
# Versioned symbols WITHOUT the rest of distribution packaging: standard SONAME,
# headers left in include/openssl, bssl not renamed.
cmake -GNinja -B build -DBUILD_SHARED_LIBS=ON \
-DENABLE_PRE_SONAME_BUILD=OFF -DENABLE_SYMBOL_VERSIONING=ON
# Distribution packaging layout WITHOUT versioned symbols.
cmake -GNinja -B build -DBUILD_SHARED_LIBS=ON \
-DENABLE_DIST_PKG=ON -DENABLE_SYMBOL_VERSIONING=OFFVersioning is independent of the other distribution-packaging behaviors
(COHABITANT_HEADERS, COHABITANT_BINARIES, SONAME), so it can be adopted as an
isolated step. The second example passes -DENABLE_PRE_SONAME_BUILD=OFF only
because a shared library you distribute should carry a SONAME and that flag is
currently the sole way to get one without ENABLE_DIST_PKG; the deprecation
warning it prints is expected, and versioning itself does not require a SONAME.
ENABLE_SYMBOL_VERSIONING requires a shared library build
(-DBUILD_SHARED_LIBS=ON) on a non-Apple UNIX platform. Requesting it explicitly
elsewhere is a configure error; when merely inherited from ENABLE_DIST_PKG it is
silently left off.
A full distribution-packaging build, which enables versioning by default:
cmake -GNinja -B build \
-DBUILD_SHARED_LIBS=ON \
-DENABLE_DIST_PKG=ON \
-DCMAKE_BUILD_TYPE=Release
ninja -C buildThis produces versioned shared libraries whose file name carries the full
library version (SOFTWARE_VERSION) and whose SONAME carries the ABI version
(ABI_VERSION). For example, on AWS-LC 5.1.0 with ABI_VERSION=1:
build/crypto/libcrypto-awslc.so.5.1.0— real file, withAWS_LC_1.0versioned symbolsbuild/ssl/libssl-awslc.so.5.1.0— real file, withAWS_LC_1.0versioned symbols
along with the usual symlinks: libcrypto-awslc.so.1 (SONAME) → libcrypto-awslc.so.5.1.0, and the linker/dev symlink libcrypto-awslc.so.
Note: the symbol version node (
AWS_LC_1.0) is independent of the file version. The node only changes when new API is added or an ABI break starts a new series; the file version tracks each release.
Verify symbol versioning is applied (use the libcrypto-awslc.so dev symlink so
the commands do not depend on a specific version string):
# Check version definitions
readelf --version-info build/crypto/libcrypto-awslc.so
# List versioned symbols
nm -D build/crypto/libcrypto-awslc.so | grep @AWS_LC_1.0 | head -10
# Verify all symbols are versioned
nm -D build/crypto/libcrypto-awslc.so | grep ' T ' | grep -v '@'
# (should be empty - all symbols should have a version suffix)Symbol versioning is driven by two per-library files that are checked into the tree:
- Registry (source of truth):
crypto/libcrypto.txt,ssl/libssl.txt - Version script (generated):
crypto/libcrypto.map,ssl/libssl.map
Each registry line records a symbol, its version node, and its visibility:
AES_encrypt AWS_LC_1.0 PUBLIC
CRYPTO_once AWS_LC_1.0 PRIVATE
ssl_cert_check_key_usage AWS_LC_1.0 PRIVATE_CXX
Visibility values:
PUBLIC— public API frominclude/openssl/*.h; can never be removedPRIVATE— internal API with C linkage fromcrypto/**/*.h; may be removedPRIVATE_CXX— internal API with C++ linkage fromssl/**/*.h; may be removed
The .map files are auto-generated from the registry and must not be edited by
hand. CMake attaches the .map to each library via apply_version_script()
(see cmake/GenerateVersionScript.cmake).
The registry and version scripts are managed by Go tools and shell wrappers in util/:
| Tool | Purpose |
|---|---|
util/read_public_symbols |
Extracts exported symbols from headers and classifies visibility (PUBLIC / PRIVATE / PRIVATE_CXX). |
util/generate_version_script |
Generates a .map version script from a registry .txt. Deterministic. -namespace rewrites the version node prefix. |
util/generate_initial_version_scripts.sh |
Bootstraps both registries and .map files from scratch (used once to establish the baseline). |
util/update_symbol_version.sh |
Registers newly introduced API and regenerates the .map files. --current adds to the open node (the common case); passing a version opens a new node. |
AWS_LC_1.0 {
global:
AES_encrypt;
AES_decrypt;
EVP_MD_CTX_new;
/* ... all symbols in this node ... */
local:
*; /* Hide all other symbols */
};
This defines:
- global: Symbols exported with version
AWS_LC_1.0 - local: *: Hide all other symbols (internal implementation)
When more than one version node exists, each node inherits from its predecessor
(e.g. AWS_LC_1.1 { global: ...; } AWS_LC_1.0;). generate_version_script
emits this inheritance automatically; only the oldest (base) node carries the
local: *; catch-all.
New public API is registered in the open version node -- the newest node in
the registry, which is currently AWS_LC_1.0. Adding to it is the normal case
and does not require a new node:
# Build so the new OPENSSL_EXPORT symbols exist in the headers, then:
./util/update_symbol_version.sh --currentThis extracts the current symbol set from the headers, identifies symbols not yet
in the registry, appends them to the open node with their visibility, re-sorts the
registry, and regenerates crypto/libcrypto.map and ssl/libssl.map. Commit the
updated .txt and .map files together.
Always use update_symbol_version.sh rather than editing the registry or .map
files by hand. A symbol that is missing from the registry is hidden by
local: *;, which means it is compiled into the library but applications cannot
link against it -- see Silently dropped symbols.
The version node is always an explicit argument; the script has no default. This is deliberate, so that adding to the open node and opening a new one are both conscious choices.
Opening a node closes the previous one: once a node is closed, no further API may be added to it, because packaging metadata derived from it (see Package Management) would otherwise be unable to distinguish a library that has the new API from one that does not.
Closing a node is a release-level decision, not something an individual API-adding PR does. Don't open a node just because the current one has shipped; raise it with maintainers first. When it is the right call:
./util/update_symbol_version.sh AWS_LC_1.1The script refuses a version that already exists in the registry, and points at
--current instead -- so a mistyped version number cannot silently land API in
the wrong node.
Note: nothing currently enforces that a closed node stays closed; it is a convention.
--currentalways resolves to the newest node in the registry.
- Format:
AWS_LC_<MAJOR>.<MINOR> - Increment: Bump minor version for each API addition
- Examples:
AWS_LC_1.0- Initial releaseAWS_LC_1.1- First update with new symbolsAWS_LC_1.2- Second update with new symbolsAWS_LC_2.0- After ABI break (new SONAME)
The AWS_LC prefix in the node name is configurable via
-DSYMBOL_VERSION_NAMESPACE=<prefix>:
cmake -GNinja -B build -DBUILD_SHARED_LIBS=ON \
-DENABLE_SYMBOL_VERSIONING=ON -DSYMBOL_VERSION_NAMESPACE=MYCORP
# => nodes are named MYCORP_1.0 instead of AWS_LC_1.0The prefix must be a valid linker identifier ([A-Za-z_][A-Za-z0-9_]*), and
setting a non-default namespace while symbol versioning is disabled is a configure
error rather than being silently ignored. Only node names are rewritten; symbol
names are untouched, and the checked-in .map files are left alone -- the renamed
script is written into the build tree (<build>/crypto/libcrypto.map,
<build>/ssl/libssl.map).
This changes the ABI contract. An application linked against a
MYCORP_1.0build records a dependency onMYCORP_1.0and will not resolve against a stockAWS_LC_1.0library, or vice versa. A custom namespace is therefore only appropriate for a privately distributed libcrypto. Do not use it for anything published as AWS-LC.
The same rename is available from the generator directly:
go run ./util/generate_version_script \
-in crypto/libcrypto.txt -out /tmp/libcrypto.map -namespace MYCORPSymbol prefixing (see "Building with Prefixed Symbols" in BUILDING.md) and a custom version node namespace both keep a binary from linking against the wrong library, but they rename different things:
BORINGSSL_PREFIX |
SYMBOL_VERSION_NAMESPACE |
|
|---|---|---|
| Renames | Every exported symbol (SSL_new -> awslc_SSL_new) |
Only version node names (AWS_LC_1.0 -> MYCORP_1.0) |
| Mechanism | Generated #define headers compiled into library and consumers |
GNU ld version script, applied at link time |
| Consumer impact | Token-level rewrite of consumer code; also hits same-named identifiers in unrelated C++ namespaces | None; consumer source is unchanged |
| Applies to | All build types and platforms | Shared ELF libraries only |
| Isolation via | Names differ across builds | Dynamic linker refuses to bind SSL_new@AWS_LC_1.0 to a library defining only SSL_new@MYCORP_1.0 |
Use prefixing when renaming the symbols is acceptable; use a version namespace
when symbol names must stay standard (drop-in OpenSSL-API consumers, dlsym()
of standard names). The two are mutually exclusive: the version scripts
reference unprefixed names, so a prefixed build would hide every symbol behind
local: *. Configuring both is rejected at configure time; a prefixed
ENABLE_DIST_PKG build must set -DENABLE_SYMBOL_VERSIONING=OFF.
Removing a PUBLIC symbol breaks ABI compatibility and should be avoided. If absolutely necessary:
-
Increment
ABI_VERSIONinCMakeLists.txt(this drives the SONAME):set(ABI_VERSION 2) # Was 1
-
Start a new version series by assigning symbols to a new major node:
./util/update_symbol_version.sh AWS_LC_2.0
-
SONAME changes accordingly:
- Old:
libcrypto-awslc.so.1 - New:
libcrypto-awslc.so.2
- Old:
-
Document the breaking change: release notes must prominently document this.
AWS-LC CI monitors the symbol registry and version scripts on every PR via
.github/workflows/abidiff.yml, and validates the built libraries in the
dist-pkg-install-tests-* jobs of
.github/workflows/linux-multi-arch-omnibus.yml.
Six registry jobs run. None of them build a library, so they are fast:
- libcrypto symbol check (incremental) — compares the registry between the PR base and head; flags additions (warning) and PUBLIC removals (error).
- libssl symbol check (incremental) — same for libssl.
- libcrypto symbol check (baseline) — extracts symbols from the headers and verifies every one is present in the registry (catches unregistered new API).
- libssl symbol check (baseline) — same for libssl.
- libcrypto version script drift check — regenerates
crypto/libcrypto.mapfrom the registry and verifies it matches the committed.mapbyte-for-byte. - libssl version script drift check — same for libssl.
These are implemented by
.github/docker_images/symbol_check/check_symbols.sh
in incremental, baseline, and mapcheck modes.
The six jobs above all derive their view of the world from
util/read_public_symbols. If that extractor ever missed an OPENSSL_EXPORT
symbol, the symbol would be absent from the registry and hidden by the version
script, and every registry-based check would still pass -- because none of them
look at what the compiler actually exported.
tests/ci/run_symbol_version_test.sh,
run by the dist-pkg-install-tests-* jobs, closes that hole: it builds the same
sources a second time without ENABLE_DIST_PKG (so no version script is
applied) and diffs the exported symbol sets of the two libraries. Anything
exported by the unversioned build but missing from the versioned one was hidden
by local: *;. This is the backstop, not the first line of defense -- it costs two
full shared builds, so the cheap registry checks above should catch problems
first.
- Symbol additions:
⚠️ Warning (allowed, but verify intentional) - PUBLIC symbol removals: ❌ Error (blocks the build — ABI break)
- PRIVATE / PRIVATE_CXX removals:
⚠️ Warning (allowed) - Unregistered new API (baseline): ❌ Error (run
update_symbol_version.sh --current) .mapout of sync with registry (drift): ❌ Error (regenerate the.map)- Exported symbol hidden by the version script: ❌ Error (register the symbol)
❌ UNREGISTERED SYMBOLS (1):
CRYPTO_tls13_hkdf_expand_label
🛑 New symbols are not in the registry. Unregistered symbols are hidden
by the version script, so applications cannot link against them.
Register them in the current version node:
./util/update_symbol_version.sh --current
Action: run ./util/update_symbol_version.sh --current and commit the updated .txt/.map.
Reported by the dist-pkg-install-tests-* jobs:
✗ FAIL: libcrypto: 5 exported symbol(s) silently hidden by the version script
INFO: These are exported by the compiler but missing from the registry/.map:
EC_group_brainpoolP224r1
...
This is the same root cause as an unregistered-symbol failure, seen from the
built library instead of the headers: the listed functions are compiled into
libcrypto but demoted to local symbols, so an application linking against a
ENABLE_DIST_PKG build fails with an undefined reference. Confirm with:
nm -D --defined-only build/crypto/libcrypto-awslc.so | grep <symbol> # absent
nm build/crypto/libcrypto-awslc.so | grep <symbol> # present, as 't'Action: run ./util/update_symbol_version.sh --current and commit the updated .txt/.map. Expect the baseline check to be failing for the same reason.
❌ PUBLIC SYMBOLS REMOVED FROM REGISTRY (2):
OldFunction1
OldFunction2
🛑 ABI BREAK: removing public symbols from the registry breaks compatibility.
Action: restore the symbols if possible; otherwise follow the ABI break procedure above.
❌ crypto/libcrypto.map is out of sync with crypto/libcrypto.txt (diff above).
🛑 The version script is auto-generated and must not drift.
Action: regenerate the .map with go run ./util/generate_version_script -in crypto/libcrypto.txt -out crypto/libcrypto.map (or util/generate_initial_version_scripts.sh) and commit it.
No action needed. Symbol versioning is transparent.
- Add new
OPENSSL_EXPORTfunctions to headers and implement them. - Register the new symbols and regenerate the version scripts:
./util/update_symbol_version.sh --current
- Verify the symbols are actually exported and versioned:
A symbol that shows up as
cmake -GNinja -B build -DBUILD_SHARED_LIBS=ON -DENABLE_DIST_PKG=ON ninja -C build nm -D --defined-only build/crypto/libcrypto-awslc.so | grep MyNewFunction # expect: T MyNewFunction@@AWS_LC_1.0
tunder plainnm, and not at all undernm -D, was not registered in step 2. - Commit the updated registry (
crypto/libcrypto.txt,ssl/libssl.txt) and version scripts (crypto/libcrypto.map,ssl/libssl.map) together with the header change.
Step 2 is not optional. Skipping it produces a library that compiles, installs
cleanly, and passes the whole test suite, but whose new API cannot be called by
any application linking against the shared library. The tests do not catch it
because the version script is only applied when ENABLE_DIST_PKG is set, and
those builds are configured with -DBUILD_TESTING=OFF.
To rebuild the registries and version scripts from the current headers (rarely needed):
./util/generate_initial_version_scripts.sh
git add crypto/libcrypto.txt crypto/libcrypto.map ssl/libssl.txt ssl/libssl.map
git commit -m "Regenerate symbol registry and version scripts"Applications using AWS_LC_1.0 symbols work with AWS_LC_1.1 libraries because version inheritance ensures all AWS_LC_1.0 symbols remain available.
Applications using AWS_LC_1.1 symbols require AWS_LC_1.1 or later. They won't work with AWS_LC_1.0-only libraries.
Package managers can enforce version requirements:
# Application package metadata
Requires: libcrypto-awslc.so.1(AWS_LC_1.1)
This ensures users have a compatible AWS-LC version installed.
- Linux: All distributions (Amazon Linux, Ubuntu, Fedora, Debian, RHEL, etc.)
- BSD: FreeBSD, OpenBSD, NetBSD (requires GNU ld or compatible)
- macOS: Uses different versioning mechanism (compatibility_version/current_version)
- Windows: PE format uses DEF files for exports
- Static libraries: Symbol versioning only applies to shared libraries
On unsupported platforms, ENABLE_DIST_PKG builds libraries without symbol
versioning; requesting -DENABLE_SYMBOL_VERSIONING=ON explicitly there is a
configure error.
Cause: The .map version script file is missing.
Solution:
# Regenerate registries and version scripts
./util/generate_initial_version_scripts.shCause: Application was built against a newer library version than is installed.
Solution: Install AWS-LC 1.1 or later, or rebuild the application against the installed version.
See When CI Fails above for the specific remediation per check.
- GNU ld version scripts: https://sourceware.org/binutils/docs/ld/VERSION.html
- Symbol versioning in glibc: https://developers.redhat.com/blog/2019/08/01/how-the-gnu-c-library-handles-backward-compatibility
- Symbol registry:
crypto/libcrypto.txt,ssl/libssl.txt - Build documentation:
BUILDING.md
Potential improvements to symbol versioning:
- Automatic registry updates in CI
- Symbol visibility analysis tool
- Historical symbol database across all versions
- Deprecation warnings for old symbol versions
- Symbol aliasing for renamed functions