node:crypto: fix null deref when worker.terminate() lands during an EC JWK private key import - #37443
node:crypto: fix null deref when worker.terminate() lands during an EC JWK private key import#37443robobun wants to merge 2 commits into
Conversation
…"d" parameter
decodeJwkString() returns null when constructFromEncoding() leaves an
exception pending. In getKeyObjectHandleFromJwk every other call site
checks for that; the EC private key path called span() on the result
directly. RETURN_IF_EXCEPTION is also where a worker's termination trap
is serviced, so worker.terminate() landing while the worker was inside
createPrivateKey({ format: "jwk" }) for an EC key dereferenced null and
took the whole process down (Segmentation fault at address 0x10).
Adds the missing RETURN_IF_EXCEPTION and a regression test that
terminates workers spinning on an EC JWK import.
|
Warning Review limit reached
Next review available in: 30 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 12:59 AM PT - Aug 11th, 2026
✅ @robobun, your commit f2f65a411a296c44b8966699d2233af4ffcdbb5e passed in 🧪 To try this PR locally: bunx bun-pr 37443That installs a local version of the PR into your bun-37443 --bun |
|
Status: ready for review, CI running (build 92056). Reproduced on stock bun with the repro in the description: The new case in Automated review came back with no requested changes; no open review threads. |
There was a problem hiding this comment.
LGTM — one-line RETURN_IF_EXCEPTION after the EC d decode, matching every sibling decodeJwkString call in the same function.
What was reviewed:
- Confirmed every other
decodeJwkStringcall site ingetKeyObjectHandleFromJwk(x/y/OKP/AKP/RSA) already has this check; this was the lone omission. - Test follows the file's existing terminate()-race pattern: concurrent pipe drains, error/exit wired to reject,
slow-scaled rounds, combined{stdout, stderr, exitCode}assertion. - The one other unguarded
decodeJwkString-shaped site (CryptoHkdf.cpp) is called out as intentionally left to #32258.
Extended reasoning...
Overview
Single-line addition of RETURN_IF_EXCEPTION(scope, {}) after decodeJwkString(globalObject, scope, dView, "key.d"_s) in the EC private key branch of KeyObject::getKeyObjectHandleFromJwk (src/jsc/bindings/node/crypto/KeyObject.cpp:1423). Every other decodeJwkString call in this function — xBuf, yBuf, the OKP dataBuf, the AKP pubBuf/privBuf, and all six RSA fields via getDecodedJwkStringBuf — is already followed by exactly this line. The one for EC d was missing, so a TerminationException surfaced by constructFromEncoding returned null and dBuf->span() dereferenced it.
A new regression test in test/js/web/workers/worker-terminate-lifetime.test.ts spins workers on createPrivateKey({ key: jwk, format: "jwk" }) for an EC key and terminates them mid-import. The d scalar is padded with 768KB of leading zeros (which BN_bin2bn discards, so the key still imports) to make the base64 decode dominate wall-clock and hit the window reliably.
Security risks
None. The added line is a standard exception-propagation guard that returns early with an empty KeyObject when an exception (including the sticky worker TerminationException) is already pending. It does not change validation, key material handling, or any success-path behavior. If anything it closes a process-crash DoS reachable from user JS via worker termination.
Level of scrutiny
Low. This is the canonical "missing RETURN_IF_EXCEPTION after a call that can throw" fix that REVIEW.md calls out explicitly. The correct behavior is unambiguous — it must match the immediately adjacent xBuf/yBuf decodes three lines above — and there is no design decision involved.
Other factors
- The PR description addresses the "fix the whole class" rule: the only other unguarded site with this shape (
CryptoHkdf.cppprepareKey) is named and deferred to open PR #32258, which is an acceptable exclusion per the review guidance. - The test follows the exact conventions of the neighboring terminate() races in the same file:
await using proc,Promise.all([stdout.text(), stderr.text(), proc.exited]), combined-object assertion,ready()wiresmessage/error/exitto resolve/reject, and iteration counts scale on the file-localslowflag with the sharedtimeout. - Verification in the description shows fail-before (5/5 UBSan, 6/6 SIGSEGV with
USE_SYSTEM_BUN=1) and pass-after (5/5), satisfying the "test must fail on system bun" requirement. - No CODEOWNERS entries cover this path.
…ect tests The test exercises createPrivateKey's JWK import, so it belongs in crypto.key-objects.test.ts. It now drives the workers through the Worker global instead of node:worker_threads, which more than halves the per round cost on debug builds, and uses a single self-spawning fixture like the original report.
There was a problem hiding this comment.
LGTM — one-line missing RETURN_IF_EXCEPTION after the EC d decode, matching every sibling decodeJwkString call in the same function.
What was reviewed:
- Confirmed all other
decodeJwkStringcalls ingetKeyObjectHandleFromJwk(x/y/OKP/AKP/RSA) already carry the check; this was the lone gap. - Test follows harness conventions (tempDir, concurrent pipe drain, combined-object assert,
isDebug||isASANround scaling) and awaits worker close events rather than sleeping. - Checked that the padded
d(leading zero bytes) still imports as a valid scalar viaBignumPointer, so the pre-postMessageimport guards against a vacuous test.
Extended reasoning...
Overview
One production line in src/jsc/bindings/node/crypto/KeyObject.cpp: add RETURN_IF_EXCEPTION(scope, {}) immediately after decodeJwkString(..., dView, "key.d"_s) in the EC private-key branch of getKeyObjectHandleFromJwk. Every other decodeJwkString call in this function already carries this guard; the missing one meant dBuf->span() dereferenced null when constructFromEncoding returned empty with an exception (in practice, a worker TerminationException) pending. Plus a regression test in test/js/node/crypto/crypto.key-objects.test.ts (moved from the worker-terminate file per commit f2f65a4).
Security risks
None. The change only adds an early-return-on-pending-exception; no new parsing, validation loosening, or trust boundary change. The padded d in the test is worker-local and never leaves the process.
Level of scrutiny
Low-to-moderate. This is native crypto binding code, but the change is a mechanical pattern-match to the immediately adjacent lines (xBuf/yBuf decodes two statements above use the identical guard). The failure mode is a null-deref crash, and the fix is the canonical JSC exception-check macro at exactly the site the REVIEW.md "Exception checks after every call that can enter JS" rule requires.
Other factors
The test is well-constructed: it widens the race window by padding d with ~768KB of leading zero bytes (which BN_bin2bn drops, so the scalar is unchanged and the import still succeeds), imports once before postMessage so a broken JWK fails loudly rather than silently passing, awaits worker close events, drains pipes concurrently, and scales rounds down on debug/ASAN. The PR description notes it fails 5/5 without the fix and passes 5/5 with it, and fails under USE_SYSTEM_BUN=1. The description's mention of worker-terminate-lifetime.test.ts is stale (the test was moved to crypto.key-objects.test.ts in the tip commit), but the actual placement next to other KeyObject tests is appropriate. The sibling CryptoHkdf.cpp gap is explicitly deferred to open PR #32258, which is a reasonable scope boundary.
Symptom
worker.terminate()while the worker is insidecrypto.createPrivateKey({ key, format: "jwk" })for an EC key kills the whole process. Stockbun(1.4.0 canary, 9008ae7) dies on the first or second worker with:Debug/ASAN builds report the same site directly:
Repro (crashes every run on main, usually within the first few workers):
Cause
decodeJwkString()returns null whenconstructFromEncoding()leaves an exception pending. Every other call ingetKeyObjectHandleFromJwkfollows it withRETURN_IF_EXCEPTION; the EC private key branch called->span()on the result of decodingdwithout checking.That is reachable in normal use because
RETURN_IF_EXCEPTIONis also where a worker's termination trap gets serviced (vm.hasExceptionsAfterHandlingTraps()): onceterminate()has fired the trap, the next check insideconstructFromEncodingthrows the TerminationException and the decode returns null. Any terminate that lands between theydecode's check and theddecode, a window that includessetPublicKeyRaw, hits it, which is why the repro fires so quickly.Fix
Add the missing
RETURN_IF_EXCEPTION(scope, {})after decodingd, matching thex/y/RSA/OKP/AKP branches of the same function. #33752 (EC scalar validation) happens to carry the same line as part of a larger change; this PR is just the crash fix.The same pattern exists in
CryptoHkdf.cppprepareKey(string key path), which the open #32258 covers; not touched here.Verification
New test in
test/js/node/crypto/crypto.key-objects.test.ts. A self-spawning fixture terminates rounds of four workers spinning on an EC JWK import;dcarries leading zero bytes (same scalar, still imports) so the decode dominates each call and the trap lands inside it nearly every time.Segmentation fault at address 0x10).no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/crypto/crypto.key-objects.test.ts