fix(runtime): cache Web Crypto method closures instead of reallocating per read - #10643
proggeramlug wants to merge 2 commits into
Conversation
…g per read globalThis.crypto.randomUUID/getRandomValues and crypto.subtle's KEM methods (encapsulateBits/decapsulateBits/encapsulateKey/decapsulateKey) allocated a fresh closure on every property read via plain js_closure_alloc, so the method had no stable identity (crypto.randomUUID === crypto.randomUUID was false) and every read allocated. Use the existing func-ptr-keyed js_closure_alloc_singleton cache instead, matching how other builtin methods stay identity-stable across reads.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe runtime now caches Web Crypto and SubtleCrypto method closures by function pointer. New tests verify stable identities for namespaces and methods, cross-expression access, node:crypto imports, and method behavior. ChangesWebCrypto identity
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The reported lint concern is unsupported by this repository’s configured checks. The WebCrypto identity change is ready to merge. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The Web Crypto requirement in [ Full details: Docstring CoverageExplanation Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Landed in merge train 222 (#10732), released as v0.5.1601 — main is now Closing rather than merging is how trains work here: the eight PRs were cherry-picked onto one tree, validated together, and landed under the train's own commit, so GitHub cannot mark this one merged even though your change is on main. Close-keywords in a source PR body never fire under this scheme, so the issues this train resolved were closed from the train's body instead. The tree passed: all nine cheap gates, |
Summary
Reading a Web Crypto method off
globalThis.crypto(orcrypto.subtle) allocated a fresh closure on every access, so the method had no stable identity:crypto.randomUUID === crypto.randomUUIDwasfalse, and each read allocated a newClosureHeader.Root cause
globalThis.cryptois backed by aNATIVE_MODULE_CLASS_IDnamespace object (crypto.webcrypto), socrypto.randomUUIDresolves throughvt_get_own_field→webcrypto_method_value(crates/perry-runtime/src/object/global_this/ctor_thunks.rs). That function (and itscrypto.subtlecounterpart,subtle_crypto_method_value, for the KEM methodsencapsulateBits/decapsulateBits/encapsulateKey/decapsulateKey) called plainjs_closure_alloc(func_ptr, 0)on every call — a brand-new closure each time a property was read, not just each time the method was called.This is the same closure-identity contract PR #10630 (
Fixes #10554) traced back tojs_closure_alloc_singleton: every user-function value materializes into a heap closure, and the singleton allocator (crates/perry-runtime/src/closure/alloc.rs) caches that materialization by func_ptr, giving the sameClosureHeaderback on every call for a given code pointer.webcrypto_method_value/subtle_crypto_method_value's thunks take no captures, so the func_ptr already is the method's identity — using the singleton cache instead of a fresh allocation was a direct drop-in.Fix
crates/perry-runtime/src/object/global_this/ctor_thunks.rs:webcrypto_method_valueandsubtle_crypto_method_valuenow calljs_closure_alloc_singleton(func_ptr)instead ofjs_closure_alloc(func_ptr, 0). Two lines changed, plus explanatory comments.Which
globalThis.cryptomembers were checkedrandomUUID,getRandomValues— both routed through the same brokenwebcrypto_method_value; both fixed by this change.crypto.subtle(the namespace object itself) — already stable:"crypto.subtle"is inshould_cache_native_module_namespace's allow-list, sojs_create_native_module_namespacereturns the same cached object on every read. Unaffected, verified as a control.crypto.subtle.encapsulateBits/decapsulateBits/encapsulateKey/decapsulateKey— routed throughsubtle_crypto_method_value, the same defect. Fixed by the same change.crypto.subtle.digest/encrypt/decrypt/sign/verify/deriveBits/deriveKey/exportKey/generateKey/importKey/unwrapKey/wrapKey— these resolve throughis_native_module_callable_export→bound_native_callable_export_value, a different, already-correctly-cached mechanism (NATIVE_CALLABLE_EXPORTS, keyed bymodule\0property). Already stable; verified as controls, unaffected either way.node:crypto'srandomUUID/randomBytes(default and named imports) — also resolve throughbound_native_callable_export_value. Already stable (this is what the issue's own repro showed as already-true); verified as controls.So the fix covers every member of
globalThis.cryptothat had the defect (randomUUID,getRandomValues, andcrypto.subtle's 4 KEM methods); everything else in the Web Crypto /node:cryptosurface was already correct via the separatebound_native_callable_export_valuecache and is unaffected.Tests
New gap test
test-files/test_gap_10427_webcrypto_method_identity.ts, oracle Node 26.5.1. Covers: the issue's own repro (randomUUID === randomUUID, a 3-iterationSetcollectingsize),getRandomValuesidentity, thecrypto/crypto.subtlenamespace objects' own identity (controls), all 4 KEM method identities, 3 already-correctsubtle.*methods as controls, cross-read identity (globalThis.crypto.randomUUID === crypto.randomUUID, two different expressions resolving the same property),node:cryptodefault-vs-named-import identity andrandomBytesstability as controls, and a functional sanity check (UUID v4 shape via regex, distinct values across calls,getRandomValuesoutput length) — none of which print an actual random value, so the test is fully deterministic.Before/after: on the pristine baseline (commit
68a545439),randomUUID stable: false,getRandomValues stable: false,seen.size: 3(byte-for-byte matching the issue's own "Actual (Perry)" block), and all 4 KEM-method identity lines arefalse; every already-correct control stayedtrue. On this branch, output is byte-identical to Node. Harness:PERRY_SKIP_BUILD=1 ./run_parity_tests.sh --filter test_gap_10427→ PARITY_FAIL on the baseline binary, PASS on this branch.Regression sweep (all still 100% pass, same binary):
test_gap_crypto_*(3),test_gap_webcrypto_async_threadpool,test_issue_561_webcrypto,test_crypto_subtle_*(3).Validation
cargo test --release -p perry-runtime --lib(RUST_TEST_THREADS=1): 4022 passed, 2 failed, 4 ignored. The 2 failures (gc::tests::copy_slot_decode::sabotaged_remembering_arm_is_refused_by_the_coverage_cross_check,gc::tests::heap_generation::a_free_or_move_outside_every_scope_is_caught_in_debug_builds) are pre-existing on the pristine baseline — confirmed by running the same two tests directly against the unmodified baseline tree (/root/claude-fix-10426-10427-baseline@68a545439): identical failures, identical messages. GC internals unrelated to this change (this PR touches none ofgc/).cargo fmt --all -- --check: clean../scripts/run_lint_gates.sh(SKIP_COMPILE_GATES=1): 76/77 passed; the one red (Public benchmark evidence freshness) is the pre-existing, repo-wide red documented in the workflow notes — not touched by this change.Gap suite: targeted filters above (all pass); full local suite not run (this change is two lines in two small thunk functions, not a hot lowering/runtime path used by most programs).
Performance:
perf stat -e instructions,task-clock, 3 runs each, baseline vs fixed, release binaries (a 2,000,000-iteration loop readingglobalThis.crypto.randomUUIDas a value each time — the directly-affected path; the same loop readingglobalThis.crypto.subtleinstead — an already-cached namespace read — as a control):crypto.randomUUIDread loop (directly affected)crypto.subtleread loop (control, unaffected)The directly-affected path is ~26.6% fewer instructions, not slower: the baseline paid for a full
js_closure_alloc(nursery allocation + GC bookkeeping) on every read; the fix's singleton-cache lookup is a thread-local hashmap probe. The control is unchanged within noise (<0.1%), confirming the fix adds no cost to a path it doesn't touch.Package check: not applicable — the issue's repro is a synthetic identity pattern (uuid 14.0.1 was named in the issue's profiling context, not as a reproducible package-level test).
What I did not verify
js_closure_alloc_singleton's cache is unconditionally correct for zero-capture closures per its own doc comment (used elsewhere for the same purpose); I didn't re-audit that mechanism itself, only applied it.Fixes #10427
Summary by CodeRabbit
Bug Fixes
Tests