Skip to content

fix: copy symmetric key on export to prevent JSI ArrayBuffer GC corruption#933

Merged
boorad merged 2 commits intomainfrom
fix/symmetric-key-export-gc
Feb 16, 2026
Merged

fix: copy symmetric key on export to prevent JSI ArrayBuffer GC corruption#933
boorad merged 2 commits intomainfrom
fix/symmetric-key-export-gc

Conversation

@boorad
Copy link
Collaborator

@boorad boorad commented Feb 15, 2026

Summary

Fixes an intermittent bug where subtle.exportKey('raw') returns corrupted key data (trailing zeros) for symmetric keys. The root cause is a JSI ArrayBuffer lifetime issue: GetSymmetricKey() returned the internal shared_ptr<ArrayBuffer> directly, sharing memory with the JSI wrapper. When the JSI wrapper was garbage collected, the underlying memory could be freed while JavaScript still held a Uint8Array view over it.

Changes

  • C++ (HybridKeyObjectHandle.cpp): Return a copy via ToNativeArrayBuffer() instead of the internal key buffer directly, preventing GC corruption and protecting internal key material from mutation
  • TypeScript (classes.ts): Revert to Buffer.from(key) since the C++ layer now handles the copy (no need for double-copy via Uint8Array)
  • Test (import_export.ts): Add 200-iteration stress test that generates, exports, re-imports, and round-trips AES-CBC keys to validate the fix deterministically

Testing

Run the subtle/import_export test suite in the example app. The new #645 AES-CBC generateKey/exportKey/importKey stress test exercises the fix with 200 iterations.

Fixes #645

…ption (#645)

exportKey() for SECRET keys returned the internal shared_ptr<ArrayBuffer>
directly. When this crossed JSI to JS, Buffer.from(jsiArrayBuffer) created
a zero-copy view. If GC reclaimed the JSI ArrayBuffer wrapper before the
data was consumed, the view would see partially zeroed memory — explaining
the intermittent trailing-zeros corruption reported in #645.

Fix: Return a memcpy'd NativeArrayBuffer from C++ (matching the pattern
used by all asymmetric key export paths), and force a copy in
SecretKeyObject.export() via Buffer.from(new Uint8Array(key)) as
defense-in-depth.

Includes a 200-iteration stress test for generateKey → exportKey →
importKey → encrypt → decrypt round-trips.
Remove redundant double-copy in TS (C++ already copies), trim verbose
comment, and replace heuristic zero-byte test assertion with
deterministic round-trip validation.
@boorad boorad self-assigned this Feb 15, 2026
@github-actions
Copy link
Contributor

🤖 End-to-End Test Results - iOS

Status: ✅ Passed
Platform: iOS
Run: 22045489514

📸 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
Contributor

🤖 End-to-End Test Results - Android

Status: ✅ Passed
Platform: Android
Run: 22045489502

📸 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 merged commit 8e4c9e7 into main Feb 16, 2026
7 checks passed
@boorad boorad deleted the fix/symmetric-key-export-gc branch February 16, 2026 00:13
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.

🐛 Key with empty bytes vs importKey

1 participant

Comments