Skip to content

fix: isolate bundled OpenSSL so it can't collide with another copy - #1070

Merged
boorad merged 6 commits into
mainfrom
fix/openssl-symbol-isolation
Aug 14, 2026
Merged

fix: isolate bundled OpenSSL so it can't collide with another copy#1070
boorad merged 6 commits into
mainfrom
fix/openssl-symbol-isolation

Conversation

@boorad

@boorad boorad commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Closes #1059.

Two libraries in one app can each bring their own OpenSSL — most commonly @op-engineering/op-sqlite with sqlcipher: true. Today that breaks both platforms, in different ways.

Android — both ship libcrypto.so/libssl.so, so mergeNativeLibs fails outright. pickFirst "fixes" the build by dropping one copy, leaving whichever library lost the coin toss bound to a version it wasn't compiled against.

iOS — QuickCrypto is source-built, so its OpenSSL references resolve when the app links. Anything that statically embeds OpenSSL wins first-come resolution and QuickCrypto silently runs on a foreign OpenSSL with different struct layouts. Reported in #1059 as EXC_BAD_ACCESS in EVP_KEYMGMT_get0_name on the first subtle.generateKey().

Worth correcting one assumption from the issue thread: iOS was not statically linking OpenSSL. OpenSSL-Universal 3.6.2000 ships a dynamic framework, so "make ours static" alone would have changed nothing there.

Android

Links OpenSSL statically from a new openssl-static prefab and localizes the symbols with -Wl,--exclude-libs,ALL. No libcrypto.so ships at all, so there is nothing left to collide.

Required two upstream PRs: ronickg/ndkports#5 (the port) and ronickg/ndkports#6 (a hyphen in the prefab package name produced an invalid Java package in the AAR manifest, which AGP rejects). Published as io.github.ronickg:openssl-static:3.6.2-2.

iOS

Vendors a prebuilt static OpenSSL whose every global symbol is renamed to rnqc_*, with the originals demoted to non-external. Nothing else defines those names, so the two copies cannot see each other in either direction. OpenSSL-Universal is dropped.

The rename happens at link level (ld -r + -alias_list + -unexported_symbols_list), not via a BoringSSL-style -include prefix header. That matters: a prefix header cannot rewrite .globl _sha256_block_data_order, so every perlasm symbol would stay exposed.

Built by scripts/build-openssl-apple.sh (landed in #1068), published as openssl-apple-3.6.2 and pinned by SHA-256 in the podspec.

Verification

Both platforms built against the published artifacts, not local ones.

Android — all 4 ABIs build; APK contains 0 libcrypto.so/libssl.so; libQuickCrypto.so exports 0 OpenSSL symbols in its dynamic table while keeping JNI_OnLoad and the Nitro entry points; OpenSSL code present but local (t).

iOS — example app compiles and links with SODIUM_ENABLED=1; shipped binary has 10,924 rnqc_* symbols and 0 unprefixed OpenSSL symbols exported; _X509_free, _d2i_X509, _EVP_MD_CTX_new all present as non-external.

Runtime verification

The example app's test suite passes on both the iOS simulator and the Android emulator. Artifact inspection confirms those runs used the new static builds rather than stale ones:

  • iOSQuickCryptoExample.debug.dylib: 10,924 rnqc_* symbols, 0 unprefixed OpenSSL symbols exported, 0 links to OpenSSL.framework, and the framework is gone from the bundle.
  • Android — the native build links jetified-openssl-static-3.6.2-2/.../libcrypto.a; the APK contains 0 libcrypto.so/libssl.so and libQuickCrypto.so exports 0 OpenSSL symbols.

Size: iOS grows ~1.3 MB net (6.4 MB static vs the 5.1 MB embedded dylib it replaces), and the app no longer embeds or code-signs OpenSSL.framework.

Also supersedes #1069, which carries the same prefix-header fix for main standalone — whichever merges first makes the other a no-op.

🤖 Generated with Claude Code

boorad added 5 commits August 12, 2026 18:25
Two libraries in one app can each bring their own OpenSSL — commonly
op-sqlite with sqlcipher enabled. Today that breaks in two ways:

Android: both ship libcrypto.so/libssl.so, so mergeNativeLibs fails, and
pickFirst 'fixes' the build by leaving one library bound to a version it
was not compiled against.

iOS: QuickCrypto is source-built, so its OpenSSL references resolve at
*app* link time. Anything that statically embeds OpenSSL wins first-come
resolution and QuickCrypto silently runs on a foreign OpenSSL with
different struct layouts (EXC_BAD_ACCESS in EVP_KEYMGMT_get0_name).

Android now links OpenSSL statically and localizes those symbols with
-Wl,--exclude-libs,ALL, so no libcrypto.so ships at all.

iOS vendors a prebuilt static OpenSSL whose every global symbol has been
renamed to rnqc_* with the originals demoted to non-external, built by
scripts/build-openssl-apple.sh. The rename happens at link level rather
than through a -include prefix header so that it also covers perlasm
symbols, which the preprocessor cannot reach.

Both changes depend on artifacts that must be published first:
- io.github.ronickg:openssl-static:3.6.2-1 (ndkports)
- the openssl-apple-3.6.2 release, whose checksum goes in the podspec
Sets openssl_sha256 to the checksum from the openssl-apple-3.6.2 release
(CI-built, verified: all 7 slices export only rnqc_* symbols, and the
macOS slice runs SHA-256 and P-256 keygen through them).

Also narrows the exclude_files pattern. CocoaPods runs exclude_files
against vendored_frameworks as well as sources — paths_for_attribute
passes spec_consumer.exclude_files as :exclude_patterns — so the previous
'ios/openssl/**/*' silently dropped the .xcframework from the pod and the
library was never linked. pod install succeeded and looked clean; only
the missing -l"QuickCryptoOpenSSL" in the app's OTHER_LDFLAGS gave it
away.
The prefix header was generated by intersecting the renamed symbols with
identifiers found in the text of OpenSSL's public headers. Much of the
public API never appears as literal text: DECLARE_ASN1_FUNCTIONS(X509)
token-pastes X509_free/d2i_X509/i2d_X509, DECLARE_PEM_write_bio produces
PEM_write_bio_X509. 1100 names were dropped — the whole ASN.1 and PEM
surface — so QuickCrypto kept calling the original names, which are
non-external in the archive.

Collect the identifiers from clang -E output instead. 5379 -> 6479
defines, and the example app now links.

Caught by building the example app; every prior check passed, since the
test consumer only exercised EVP digests and keygen and never touched
X509 or PEM.
The openssl-apple-3.6.2 release was rebuilt from the fixed prefix-header
generation; the archives were always correct, but the header ships inside
the zip so the checksum moved.
openssl-static 3.6.2-1 was unusable — ndkports derives the AAR's manifest
package from the prefab package name, and 'openssl-static' is not a valid
Java identifier, so AGP rejected it before compiling anything. Fixed
upstream in ronickg/ndkports#6 and republished as 3.6.2-2, which names the
prefab package opensslstatic (the Maven coordinate is unchanged).

Verified against the published artifact: builds all 4 ABIs, the APK ships
no libcrypto.so or libssl.so, libQuickCrypto.so exports no OpenSSL symbols
while keeping JNI_OnLoad and the Nitro entry points, and the OpenSSL code
it does contain is local.
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-native-quick-crypto Ready Ready Preview Aug 14, 2026 10:32pm

Request Review

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🤖 End-to-End Test Results - iOS

Status: ✅ Passed
Platform: iOS
Run: 31846980217

📸 Final Test Screenshot

Maestro Test Results - ios

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 End-to-End Test Results - Android

Status: ✅ Passed
Platform: Android
Run: 31846980227

📸 Final Test Screenshot

Maestro Test Results - android

Screenshot automatically captured from End-to-End tests and will expire in 30 days


This comment is automatically updated on each test run.

@boorad boorad self-assigned this Aug 14, 2026
@boorad
boorad merged commit d387f00 into main Aug 14, 2026
6 of 7 checks passed
@boorad
boorad deleted the fix/openssl-symbol-isolation branch August 14, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💭 🔧 Android: libcrypto.so collision when using SQLCipher-based libraries alongside react-native-quick-crypto

1 participant