crypto: report external memory usage to GC for node:crypto wrapper classes#32653
Conversation
…asses Most of the node:crypto C++ wrapper classes (KeyObject, Hash, Hmac, Cipher, ECDH, Sign, Verify) own heap-allocated OpenSSL contexts and/or arbitrary-length key/digest buffers, but did not report that memory to JSC via reportExtraMemoryAllocated / reportExtraMemoryVisited. Only DiffieHellman and DiffieHellmanGroup did. The practical effect is that allocating many of these objects (e.g. a loop creating SecretKeyObjects with large keys, or shake256 hashes with a large outputLength) grows the native heap without the GC knowing, so collection is not triggered by that pressure. This wires up the reporting: - JSKeyObject (and its Secret/Public/Private subclasses via inheritance): sizeof(KeyObjectData) + symmetric key bytes + EVP_PKEY_size() - JSHash: sizeof(EVP_MD_CTX) + output length (the user-controllable XOF size) - JSHmac: sizeof(HMAC_CTX) - JSCipher: sizeof(EVP_CIPHER_CTX) - JSECDH: field byte size from EC_GROUP_get_degree - JSSign / JSVerify: sizeof(EVP_MD_CTX) BoringSSL exposes the ctx struct definitions in its public headers so sizeof() is the real struct size, not an estimate.
|
Updated 6:08 PM PT - Jun 23rd, 2026
❌ @robobun, your commit b0fc20b has 1 failures in 🧪 To try this PR locally: bunx bun-pr 32653That installs a local version of the PR into your bun-32653 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughSeven Node.js crypto JSC wrapper classes ( ChangesGC Extra-Memory Accounting for Node Crypto Wrappers
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/jsc/bindings/node/crypto/JSHash.cpp`:
- Around line 132-133: The m_sizeForGC assignment in JSHash only accounts for
m_mdLen but fails to include the memory allocated for m_zigHasher which is owned
and destroyed by JSHash in its destructor. Modify the m_sizeForGC calculation to
add the size of the m_zigHasher allocation alongside m_mdLen so that the garbage
collector accurately tracks the total memory pressure from this object, ensuring
non-XOF Zig hashers do not have invisible native allocations.
In `@src/jsc/bindings/node/crypto/JSHmac.cpp`:
- Around line 103-105: The m_sizeForGC field is set to sizeof(HMAC_CTX) and
memory is reported to the garbage collector, but when the HMAC context is freed
in the digest() method (on both success and failure paths), m_sizeForGC is not
being cleared. This causes finalized Hmac wrappers to continue reporting freed
native memory as allocated. In the digest() method where m_ctx is freed, you
need to also reset m_sizeForGC to 0 and call reportExtraMemoryAllocated again
with a negative or zero value to properly deregister the previously allocated
memory with the garbage collector.
In `@src/jsc/bindings/node/crypto/JSSign.cpp`:
- Around line 211-214: In the signWithKey() method of JSSign, when m_mdCtx is
moved out of the object, the m_sizeForGC tracker is not being reset, causing the
garbage collector to continue reporting memory that no longer belongs to this
JSSign object. After the move operation transfers ownership of m_mdCtx, reset
m_sizeForGC to 0 to ensure accurate memory accounting and prevent the garbage
collector from reporting freed native memory when the Sign object is eventually
finalized.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f7abb350-9a82-4336-84a8-f05ac03dcc53
📒 Files selected for processing (15)
src/jsc/bindings/node/crypto/JSCipher.cppsrc/jsc/bindings/node/crypto/JSCipher.hsrc/jsc/bindings/node/crypto/JSECDH.cppsrc/jsc/bindings/node/crypto/JSECDH.hsrc/jsc/bindings/node/crypto/JSHash.cppsrc/jsc/bindings/node/crypto/JSHash.hsrc/jsc/bindings/node/crypto/JSHmac.cppsrc/jsc/bindings/node/crypto/JSHmac.hsrc/jsc/bindings/node/crypto/JSKeyObject.cppsrc/jsc/bindings/node/crypto/JSKeyObject.hsrc/jsc/bindings/node/crypto/JSSign.cppsrc/jsc/bindings/node/crypto/JSSign.hsrc/jsc/bindings/node/crypto/JSVerify.cppsrc/jsc/bindings/node/crypto/JSVerify.htest/js/node/crypto/crypto-extra-memory.test.ts
When digest()/final()/sign()/verify() free or move out the owned OpenSSL context, stop reporting its size in visitChildren so live-but-finalized wrappers do not keep counting freed native memory.
There was a problem hiding this comment.
I didn't find any bugs — the change faithfully follows the existing JSDiffieHellman pattern and has good test coverage — but it touches GC integration across 7 JSC wrapper classes in the crypto bindings, so it's worth a quick human look before merging.
Extended reasoning...
Overview
This PR wires up reportExtraMemoryAllocated / reportExtraMemoryVisited for 7 node:crypto JSC wrapper classes (JSCipher, JSECDH, JSHash, JSHmac, JSKeyObject, JSSign, JSVerify) so the GC sees the native OpenSSL contexts and key buffers they own. Each class gains a size_t m_sizeForGC field, reports it once at allocation/init time, echoes it in visitChildrenImpl, and zeroes it when the native context is released. A new test (crypto-extra-memory.test.ts) asserts heapStats().extraMemorySize grows proportionally for each object type. 16 files changed (15 C++ source/header + 1 test).
Security risks
None identified. The change is purely GC bookkeeping — no cryptographic operations, key handling, or data flow are altered. The size estimates are conservative approximations used only as GC pressure hints; over- or under-reporting affects collection timing, not correctness or confidentiality.
Level of scrutiny
Medium. The implementation is a mechanical application of the existing JSDiffieHellman pattern (verified at JSDiffieHellman.cpp:25-36) and the bug hunter found no issues. However, it spans 7 classes in the JSC bindings and adds new visitChildren implementations that run on GC threads concurrently with mutator writes to m_sizeForGC. While size_t reads/writes are effectively atomic on supported platforms and this matches the established pattern, GC-integration changes in C++ bindings are the kind of thing where a maintainer familiar with Bun's JSC heap conventions should sign off.
Other factors
- CodeRabbit's three minor comments (clear
m_sizeForGCon context release for Hmac/Sign/Verify) were addressed in commit 96d75a8; the remaining Zig-hasher comment was withdrawn after a reasonable explanation. KeyObjectDataisThreadSafeRefCountedand may be shared across multipleJSKeyObjectwrappers, so each will report the full key size — a benign over-report that errs toward more GC pressure.- No CODEOWNERS cover these paths.
- The new test spawns isolated subprocesses with generous lower-bound thresholds, so it should be stable, but heap-stat-delta tests can occasionally be sensitive to allocator/GC behavior across platforms.
|
CI status: the diff is green everywhere it ran. The remaining red is unrelated infrastructure and flake across two builds (#64316 and the retrigger #64345):
The new |
What
Wire up
reportExtraMemoryAllocated/reportExtraMemoryVisitedfor thenode:cryptoC++ wrapper classes that were missing it.Why
Of the 12 JS-visible wrapper classes in
src/jsc/bindings/node/crypto/, onlyJSDiffieHellmanandJSDiffieHellmanGroupreported their external memory to JSC. The rest own heap-allocated OpenSSL contexts and/or arbitrary-length buffers that the GC knew nothing about:JSSecretKeyObjectVector<uint8_t> symmetricKeyJSPublic/PrivateKeyObjectEVP_PKEY*JSHashEVP_MD_CTX*+ digest bufferoutputLength)JSHmacHMAC_CTX*JSCipherEVP_CIPHER_CTX*JSECDHEC_KEY*JSSign/JSVerifyEVP_MD_CTX*Without reporting, a loop creating
SecretKeyObjects with large keys orshake256hashes with a largeoutputLengthgrows the native heap while the JS heap looks tiny, so GC never kicks in from that pressure.How
Follows the existing
JSDiffieHellmanpattern: storem_sizeForGCon the wrapper, callvm.heap.reportExtraMemoryAllocated(this, size)once the native allocation is known, andvisitor.reportExtraMemoryVisited(size)invisitChildrenImpl.JSKeyObjectreports infinishCreation(key data is set at construction). Subclasses inherit viaBase::finishCreation/Base::visitChildren.JSHash/JSHmacreport ininit()/initZig()after the ctx is allocated andm_mdLenis known.JSSign/JSVerifyreport in their prototypeinitafter the ctx is stored.JSCipher/JSECDHreport infinishCreation(ctx/key passed to constructor).For the opaque OpenSSL contexts, BoringSSL exposes the struct definitions in
<openssl/cipher.h>/<openssl/digest.h>/<openssl/hmac.h>, sosizeof(EVP_CIPHER_CTX)etc. are real sizes rather than hardcoded estimates.Verification
New test at
test/js/node/crypto/crypto-extra-memory.test.tsspawns isolated subprocesses that measureheapStats().extraMemorySizebefore/after allocating N live wrappers and asserts the delta reflects the native memory.With the released bun, 100
SecretKeyObjects holding 64KB keys each (6.4MB total) bumpextraMemorySizeby ~78KB. With this change, the delta is >6.4MB.Existing crypto suites (
crypto.test.ts,crypto.key-objects.test.ts,crypto.hmac.test.ts,ecdh.test.ts, sign/verify tests) all pass.