Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions crates/perry-runtime/src/buffer/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ fn external_crypto_keys() -> &'static Mutex<HashMap<usize, CryptoKeyMeta>> {
EXTERNAL_CRYPTO_KEY_META_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}

/// Called by the GC's buffer sweep when a CryptoKey-flagged `BufferHeader`
/// dies, so perry-stdlib can drop the matching entry from its own
/// `addr -> CryptoKeyMaterial` map. Registered by
/// `js_set_crypto_key_death_hook` at startup; stays null when stdlib isn't
/// linked. Must not allocate — it runs inside the sweep.
pub type CryptoKeyDeathHookFn = extern "C" fn(usize);
static CRYPTO_KEY_DEATH_HOOK: std::sync::atomic::AtomicPtr<()> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());

/// Install the dead-CryptoKey callback (called by perry-stdlib at startup —
/// this crate can't call into perry-stdlib, which depends on it). Same
/// contract as the `js_set_native_*_dispatch` family in `value::handle`.
#[no_mangle]
pub extern "C" fn js_set_crypto_key_death_hook(func: CryptoKeyDeathHookFn) {
CRYPTO_KEY_DEATH_HOOK.store(func as *mut (), std::sync::atomic::Ordering::SeqCst);
}

fn notify_crypto_key_death(addr: usize) {
let ptr = CRYPTO_KEY_DEATH_HOOK.load(std::sync::atomic::Ordering::SeqCst);
if ptr.is_null() {
return;
}
let hook: CryptoKeyDeathHookFn = unsafe { std::mem::transmute(ptr) };
hook(addr);
}

pub type CryptoKeyMeta = (u8, u8, u8, bool, u32);

thread_local! {
Expand Down Expand Up @@ -490,6 +516,50 @@ pub(crate) fn finalize_collected_dead_buffer(addr: usize) {
BUFFER_AB_ALIAS.with(|r| {
r.borrow_mut().remove(&addr);
});
// The WebCrypto/KeyObject side tables were missing from this list. They are
// plain `addr -> metadata` maps that do not root the `BufferHeader`, so a
// collected CryptoKey/secret-key buffer left its entries behind forever.
// Two consequences, both real:
//
// * an unbounded leak — every CryptoKey ever created kept an entry in the
// thread-local map AND in the process-global one (a 60k-key run leaked
// 59,998 of them);
// * the #6080 ABA class this very function exists to prevent: the old
// arena resets a fully-empty block's offset to 0 while keeping its base
// pointer (`arena_reset_empty_blocks` + the block-reuse forward scan in
// `Arena::alloc`), so a recycled address inherits CryptoKey identity.
// `crypto_key_meta`/`is_secret_key` gate `instanceof CryptoKey`,
// `util.types.isCryptoKey`/`isKeyObject`, the `[object CryptoKey]` tag,
// the `.algorithm`/`.type`/`.usages` property surface, `KeyObject.from`
// and `.export()` — an unrelated fresh Buffer landing on a dead key's
// address would answer to all of them.
CRYPTO_KEY_META_REGISTRY.with(|r| {
r.borrow_mut().remove(&addr);
});
SECRET_KEY_REGISTRY.with(|r| {
r.borrow_mut().remove(&addr);
});
UINT8ARRAY_FROM_CTOR.with(|r| {
r.borrow_mut().remove(&addr);
});
Comment on lines +519 to +544

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "ASYMMETRIC_KEY_REGISTRY" -C4 --type=rust
rg -n "fn mark_as_asymmetric_key|fn is_asymmetric_key|asymmetric_key_meta" -C6 --type=rust

Repository: PerryTS/perry

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files and references to key registries / asymmetric key helpers.
git ls-files | rg 'crates/perry-runtime/src/(buffer|crypto|webcrypto|key|mod).*\.rs$|crates/perry-runtime/src/.+\.rs$'
rg -n "ASYMMETRIC_KEY_REGISTRY|CRYPTO_KEY_META_REGISTRY|SECRET_KEY_REGISTRY|UINT8ARRAY_FROM_CTOR|mark_as_asymmetric_key|is_asymmetric_key|asymmetric" crates/perry-runtime/src -C 3

# Inspect the buffer header cleanup area and the asymmetric-key related definitions/usages if any.
ast-grep outline crates/perry-runtime/src/buffer/header.rs --view expanded
ast-grep outline crates/perry-runtime/src/buffer/mod.rs --view expanded

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the prior search finds no asymmetric registry symbol, inspect the key-related API surface to see
# whether any asymmetric identity/type checks are implemented under different names.
rg -n "KeyObject|CryptoKey|secret key|asymmetric key|public key|private key|export\\(|instanceof CryptoKey|isCryptoKey|isKeyObject|util.types.isCryptoKey|util.types.isKeyObject" crates/perry-runtime/src -C 2

# Show the cleanup function around the cited lines.
sed -n '500,560p' crates/perry-runtime/src/buffer/header.rs

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any other address-keyed registries in buffer/header.rs and adjacent modules.
rg -n "thread_local!|addr ->|remove\\(&addr\\)|insert\\(addr|HashMap<usize|HashMap<u64|BTreeMap<usize|BTreeMap<u64" crates/perry-runtime/src/buffer -C 2

# Enumerate nearby files in the buffer module.
find crates/perry-runtime/src/buffer -maxdepth 2 -type f | sort

Repository: PerryTS/perry

Length of output: 15415


Remove ASYMMETRIC_KEY_REGISTRY in finalize_collected_dead_buffer
mark_as_asymmetric_key populates the same addr-keyed side table, and it gates KeyObject brand/type/property dispatch (instanceof, util.types.isKeyObject, toCryptoKey, .export(), type/asymmetricKeyType/asymmetricKeyDetails). Leaving it behind leaks entries and lets a recycled address inherit stale KeyObject identity.

🤖 Prompt for 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.

In `@crates/perry-runtime/src/buffer/header.rs` around lines 519 - 544, Update
finalize_collected_dead_buffer to remove the address from
ASYMMETRIC_KEY_REGISTRY alongside CRYPTO_KEY_META_REGISTRY and
SECRET_KEY_REGISTRY. Ensure mark_as_asymmetric_key entries are cleared when the
buffer is finalized, preventing stale KeyObject metadata on recycled addresses.

// `js_buffer_mark_as_crypto_key_external` writes all three global maps, and
// `is_registered_buffer`/`is_uint8array_buffer` consult them, so a dead
// external key buffer has to be dropped from every one of them.
if let Ok(mut r) = external_buffers().lock() {
r.remove(&addr);
}
if let Ok(mut r) = external_uint8arrays().lock() {
r.remove(&addr);
}
if let Ok(mut r) = external_crypto_keys().lock() {
r.remove(&addr);
}
// perry-stdlib keeps its own `addr -> CryptoKeyMaterial` map (the primary
// one `lookup_crypto_key` consults; the runtime table above is only its
// fallback), and this crate cannot call into perry-stdlib. Notify it
// through the hook it installs at startup. The callback only removes a
// HashMap entry — no allocation, so it is safe to run inside the sweep.
notify_crypto_key_death(addr);
Comment on lines +545 to +562

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

New unconditional global-mutex locks + cross-crate FFI hook run on every dead buffer, not just CryptoKeys.

Lines 548-556 add three brand-new external_buffers()/external_uint8arrays()/external_crypto_keys() .lock() calls to finalize_collected_dead_buffer, and notify_crypto_key_death(addr) unconditionally invokes the perry-stdlib hook (which itself locks CRYPTO_KEY_REGISTRY) — for every dead buffer, including the overwhelming majority that were never CryptoKeys. This runs on the GC sweep's per-dead-object hot path.

is_registered_buffer already shows the cheaper pattern for exactly this: it gates the external_buffers().lock() behind the EXTERNAL_BUFFERS_NONEMPTY atomic latch to skip the lock entirely when the map has never been populated. None of that reuse happens here, and the removals' own return values (HashMap::removeOption, HashSet::removebool) are discarded even though they'd tell you for free whether this address was ever a crypto key — which is exactly the condition needed to skip the stdlib hook call.

⚡ Proposed fix — skip the expensive paths for non-crypto buffers
-    CRYPTO_KEY_META_REGISTRY.with(|r| {
-        r.borrow_mut().remove(&addr);
-    });
-    SECRET_KEY_REGISTRY.with(|r| {
-        r.borrow_mut().remove(&addr);
-    });
+    let was_crypto_key_meta =
+        CRYPTO_KEY_META_REGISTRY.with(|r| r.borrow_mut().remove(&addr).is_some());
+    let was_secret_key = SECRET_KEY_REGISTRY.with(|r| r.borrow_mut().remove(&addr));
     UINT8ARRAY_FROM_CTOR.with(|r| {
         r.borrow_mut().remove(&addr);
     });
     // `js_buffer_mark_as_crypto_key_external` writes all three global maps, and
     // `is_registered_buffer`/`is_uint8array_buffer` consult them, so a dead
     // external key buffer has to be dropped from every one of them.
-    if let Ok(mut r) = external_buffers().lock() {
-        r.remove(&addr);
+    if EXTERNAL_BUFFERS_NONEMPTY.load(std::sync::atomic::Ordering::Acquire) {
+        if let Ok(mut r) = external_buffers().lock() {
+            r.remove(&addr);
+        }
     }
     if let Ok(mut r) = external_uint8arrays().lock() {
         r.remove(&addr);
     }
-    if let Ok(mut r) = external_crypto_keys().lock() {
-        r.remove(&addr);
-    }
-    // perry-stdlib keeps its own `addr -> CryptoKeyMaterial` map ...
-    notify_crypto_key_death(addr);
+    let was_external_crypto_key = external_crypto_keys()
+        .lock()
+        .map(|mut r| r.remove(&addr).is_some())
+        .unwrap_or(false);
+    // perry-stdlib keeps its own `addr -> CryptoKeyMaterial` map ...
+    if was_crypto_key_meta || was_secret_key || was_external_crypto_key {
+        notify_crypto_key_death(addr);
+    }

This still fully clears every table for actual CryptoKey buffers; it just avoids the mutex/FFI overhead for the common case of ordinary Buffers.

🤖 Prompt for 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.

In `@crates/perry-runtime/src/buffer/header.rs` around lines 545 - 562, Optimize
finalize_collected_dead_buffer by gating each external_* mutex lock with its
corresponding nonempty atomic latch, following the pattern used by
is_registered_buffer. Capture the removal result from external_crypto_keys() and
call notify_crypto_key_death(addr) only when an entry was actually removed,
while continuing to clear all relevant tables for real CryptoKey buffers.

super::detach::remove_detached_entry_for_dead_buffer(addr);
super::view::remove_entries_for_dead_buffer(addr);
}
Expand Down
52 changes: 49 additions & 3 deletions crates/perry-runtime/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ pub use header::{
asymmetric_key_meta, buffer_ab_alias, buffer_alloc, buffer_backing_array_buffer,
buffer_byte_offset, buffer_data, buffer_data_mut, crypto_key_meta, ensure_buffer_ab_alias,
is_any_array_buffer, is_array_buffer, is_data_view, is_registered_buffer, is_secret_key,
is_shared_array_buffer, is_uint8array_buffer, mark_as_array_buffer, mark_as_asymmetric_key,
mark_as_crypto_key, mark_as_data_view, mark_as_secret_key, mark_as_shared_array_buffer,
mark_as_uint8array, register_buffer, resolve_buffer_ab_alias, set_buffer_ab_alias,
is_shared_array_buffer, is_uint8array_buffer, js_set_crypto_key_death_hook,
mark_as_array_buffer, mark_as_asymmetric_key, mark_as_crypto_key, mark_as_data_view,
mark_as_secret_key, mark_as_shared_array_buffer, mark_as_uint8array, register_buffer,
resolve_buffer_ab_alias, set_buffer_ab_alias, CryptoKeyDeathHookFn,
};
pub(crate) use header::{
collect_dead_registered_buffers_post_trace, finalize_collected_dead_buffer,
Expand Down Expand Up @@ -146,6 +147,51 @@ pub use iter::{
mod tests {
use super::*;

/// The GC buffer sweep must drop the CryptoKey/secret-key side tables
/// along with the buffer identity ones. They are plain `addr -> metadata`
/// maps that never rooted the `BufferHeader`, so leaving them behind both
/// leaked an entry per key and let a recycled address inherit CryptoKey
/// identity (`crypto_key_meta`/`is_secret_key` gate `instanceof CryptoKey`,
/// `util.types.isCryptoKey`, `KeyObject.from`, `.export()` …) — the #6080
/// ABA class this finalizer exists to prevent.
#[test]
fn test_dead_buffer_finalize_prunes_crypto_key_side_tables() {
let buf = buffer_alloc(32);
assert!(!buf.is_null());
let addr = buf as usize;

// Shape a WebCrypto secret CryptoKey: HMAC / SHA-256 / secret.
mark_as_uint8array(addr);
mark_as_crypto_key(addr, 1, 2, 1);
mark_as_secret_key(addr);

assert!(crypto_key_meta(addr).is_some(), "meta registered");
assert!(is_secret_key(addr), "secret-key flag registered");
assert!(is_uint8array_buffer(addr), "uint8array flag registered");
assert!(is_registered_buffer(addr), "buffer registered");

// Exactly what the sweep subphase runs once the header is proven dead.
finalize_collected_dead_buffer(addr);

assert!(
crypto_key_meta(addr).is_none(),
"dead buffer must not keep CryptoKey metadata — a recycled address \
would answer to instanceof CryptoKey / KeyObject.from()"
);
assert!(
!is_secret_key(addr),
"dead buffer must not keep the secret-key flag"
);
assert!(
!is_uint8array_buffer(addr),
"dead buffer must not keep the uint8array flag"
);
assert!(
!is_registered_buffer(addr),
"dead buffer must not stay registered"
);
}

#[test]
fn test_small_buffer_slab_unique_addresses() {
// Every allocation must occupy a distinct address (no overlap).
Expand Down
83 changes: 77 additions & 6 deletions crates/perry-runtime/src/object/native_module_crypto_key_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,99 @@ fn invalid_key(value: f64) -> ! {
crate::fs::validate::throw_type_error_with_code(&message, "ERR_INVALID_ARG_TYPE")
}

/// A CryptoKey Perry recognises but cannot turn into a KeyObject must still
/// fail loudly. Returning `undefined` from a factory like this is what #6302
/// was filed about.
fn conversion_failed(detail: &str) -> ! {
let message = format!("KeyObject.from() failed: {detail}");
crate::fs::validate::throw_error_with_code(&message, "ERR_CRYPTO_OPERATION_FAILED")
}

/// `crypto.KeyObject.from(cryptoKey)` (#2565; asymmetric + unregistered-buffer
/// keys #6302).
///
/// Node converts *any* CryptoKey — secret, public, or private — into the
/// matching KeyObject, and throws `ERR_INVALID_ARG_TYPE` for everything else.
/// Perry models the two KeyObject shapes differently:
///
/// * **secret** — the key bytes in a `BufferHeader` flagged by
/// `mark_as_secret_key`, which is what the KeyObject property/method surface
/// keys off (`type`, `symmetricKeySize`, `export`).
/// * **asymmetric** — a PEM (RSA/EC) or internal Ed/X surrogate *string*
/// flagged by `mark_as_asymmetric_key`.
///
/// The secret conversion is a buffer copy and happens here; the asymmetric one
/// needs the key encoders (RSA PKCS#8/SPKI, P-256/384/521, Ed25519, X25519)
/// that live in perry-stdlib, so it is routed through the WebCrypto dispatch
/// hook — the same bridge `keyObject.toCryptoKey()` uses in the other
/// direction (`native_call_method/string_methods.rs`).
///
/// Pre-#6302 this function demanded `kind == 1 && is_registered_buffer(addr)`
/// and sent everything else to `invalid_key`, so a public/private CryptoKey —
/// or a secret CryptoKey whose backing buffer was not in *this* thread's buffer
/// registry — never produced a KeyObject at all.
pub(super) unsafe fn key_object_from(value: f64) -> f64 {
let addr = value_addr(value);
let Some((_algo, _hash, kind, _extractable, _usages)) = crate::buffer::crypto_key_meta(addr)
else {
invalid_key(value);
};
if kind != 1 || !crate::buffer::is_registered_buffer(addr) {
invalid_key(value);
match kind {
1 => secret_key_object(addr),
2 | 3 => asymmetric_key_object(addr),
_ => invalid_key(value),
}
}

/// Copy the CryptoKey's bytes into a fresh secret-key Buffer.
///
/// The old `is_registered_buffer(addr)` precondition was wrong: CryptoKey
/// metadata is only ever attached to a `BufferHeader` allocated by the
/// WebCrypto key factories, and that metadata is *also* kept in a
/// process-global registry, so the address is readable even when the current
/// thread's (thread-local) buffer registry has no entry for it. Requiring the
/// registry hit turned such keys into a silent `invalid_key` throw.
unsafe fn secret_key_object(addr: usize) -> f64 {
let src = addr as *const crate::buffer::BufferHeader;
let len = (*src).length as usize;
let out = crate::buffer::buffer_alloc(len as u32);
if !out.is_null() {
if out.is_null() {
conversion_failed("could not allocate the secret key buffer");
}
if len > 0 {
std::ptr::copy_nonoverlapping(
crate::buffer::buffer_data(src),
crate::buffer::buffer_data_mut(out),
len,
);
(*out).length = len as u32;
crate::buffer::mark_as_uint8array(out as usize);
crate::buffer::mark_as_secret_key(out as usize);
}
(*out).length = len as u32;
crate::buffer::mark_as_uint8array(out as usize);
crate::buffer::mark_as_secret_key(out as usize);
f64::from_bits(JSValue::pointer(out as *const u8).bits())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Public/private CryptoKey → PEM/surrogate-string KeyObject, encoded by
/// perry-stdlib's WebCrypto bridge (it owns the RSA/EC/Ed/X key writers and
/// the CryptoKey material registry).
unsafe fn asymmetric_key_object(addr: usize) -> f64 {
let ptr = crate::value::JS_NATIVE_WEBCRYPTO_DISPATCH.load(std::sync::atomic::Ordering::SeqCst);
if ptr.is_null() {
conversion_failed("the WebCrypto runtime is not available");
}
let dispatch: unsafe extern "C" fn(*const u8, usize, *const f64, usize) -> f64 =
std::mem::transmute(ptr);
// Hand the bridge a properly NaN-boxed pointer: a Buffer reaching
// `KeyObject.from` can arrive as a raw f64-bitcast pointer (module-level
// storage convention), which the stdlib pointer-strip helper would read as
// a tagged primitive and reject.
let args = [f64::from_bits(JSValue::pointer(addr as *const u8).bits())];
let method = "keyObjectFromCryptoKey";
let result = dispatch(method.as_ptr(), method.len(), args.as_ptr(), args.len());
// The bridge throws for key material it cannot model; a bare `undefined`
// return would be the exact silent failure #6302 is about.
if JSValue::from_bits(result.to_bits()).is_undefined() {
conversion_failed("unsupported asymmetric key type");
}
result
}
5 changes: 5 additions & 0 deletions crates/perry-stdlib/src/common/dispatch/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,11 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() {
perry_runtime::js_set_native_crypto_dispatch(crate::crypto::js_crypto_native_dispatch);
#[cfg(feature = "crypto")]
perry_runtime::js_set_native_webcrypto_dispatch(crate::webcrypto::js_webcrypto_native_dispatch);
// Prune the stdlib CryptoKey-material map when the GC sweeps a key's
// backing buffer (otherwise it leaks an entry per key and a recycled
// address inherits the dead key's material).
#[cfg(feature = "crypto")]
perry_runtime::buffer::js_set_crypto_key_death_hook(crate::webcrypto::crypto_key_buffer_died);
#[cfg(feature = "compression")]
perry_runtime::js_set_native_zlib_dispatch(crate::zlib::js_zlib_native_dispatch);
perry_runtime::js_set_native_querystring_dispatch(
Expand Down
12 changes: 8 additions & 4 deletions crates/perry-stdlib/src/crypto/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,18 @@ pub(super) const ED25519_PUBLIC_PREFIX: &str = "PERRY-ED25519-PUBLIC:";
pub(super) const X25519_PRIVATE_PREFIX: &str = "PERRY-X25519-PRIVATE:";
pub(super) const X25519_PUBLIC_PREFIX: &str = "PERRY-X25519-PUBLIC:";

pub(super) fn ed25519_private_surrogate(key: &ed25519_dalek::SigningKey) -> String {
/// `pub(crate)` (not `pub(super)`): the WebCrypto bridge builds the very same
/// surrogate strings when `KeyObject.from()` converts an asymmetric CryptoKey
/// back into a KeyObject (#6302), and both sides must agree byte-for-byte or
/// the `parse_*_surrogate` readers below reject the result.
pub(crate) fn ed25519_private_surrogate(key: &ed25519_dalek::SigningKey) -> String {
let secret = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key.to_bytes());
let public =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key.verifying_key().to_bytes());
format!("{ED25519_PRIVATE_PREFIX}{secret}.{public}")
}

pub(super) fn ed25519_public_surrogate(key: &ed25519_dalek::VerifyingKey) -> String {
pub(crate) fn ed25519_public_surrogate(key: &ed25519_dalek::VerifyingKey) -> String {
let public = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key.to_bytes());
format!("{ED25519_PUBLIC_PREFIX}{public}")
}
Expand All @@ -217,12 +221,12 @@ pub(crate) fn parse_ed25519_public_surrogate(value: &str) -> Option<ed25519_dale
ed25519_dalek::VerifyingKey::from_bytes(&public).ok()
}

pub(super) fn x25519_private_surrogate(secret: &[u8; 32]) -> String {
pub(crate) fn x25519_private_surrogate(secret: &[u8; 32]) -> String {
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(secret);
format!("{X25519_PRIVATE_PREFIX}{encoded}")
}

pub(super) fn x25519_public_surrogate(public: &[u8; 32]) -> String {
pub(crate) fn x25519_public_surrogate(public: &[u8; 32]) -> String {
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public);
format!("{X25519_PUBLIC_PREFIX}{encoded}")
}
Expand Down
10 changes: 10 additions & 0 deletions crates/perry-stdlib/src/webcrypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub use self::{
aes::*, digest::*, encapsulation::*, hmac::*, jwk::*, kdf::*, keys::*, supports::*, wrap::*,
};

// GC buffer-sweep callback for dead CryptoKey buffers, installed by
// `js_stdlib_init_dispatch`. Re-exported here so `util` stays a private module.
pub(crate) use self::util::crypto_key_buffer_died;

/// Dispatcher for captured/dynamic `crypto.subtle.*` calls. Static
/// `crypto.subtle.method(...)` call sites still lower directly in codegen;
/// this keeps namespace property reads such as `const subtle = crypto.subtle`
Expand Down Expand Up @@ -106,6 +110,12 @@ pub unsafe extern "C" fn js_webcrypto_native_dispatch(
"keyObjectToCryptoKey" if args_len >= 4 => {
js_webcrypto_key_object_to_crypto_key(arg(0), arg(1), arg(2), arg(3))
}
// Internal bridge for `crypto.KeyObject.from(<asymmetric CryptoKey>)`
// (#6302) — the runtime owns the secret-key shape but the asymmetric
// key encoders live here. Not a `crypto.subtle` method name.
"keyObjectFromCryptoKey" if args_len >= 1 => {
js_webcrypto_key_object_from_crypto_key(arg(0))
}
"supports" if args_len >= 2 => js_webcrypto_supports(arg(0), arg(1), arg(2)),
"encapsulateBits" => promise_to_value(js_webcrypto_encapsulate_bits(args_ptr, args_len)),
"decapsulateBits" => promise_to_value(js_webcrypto_decapsulate_bits(args_ptr, args_len)),
Expand Down
Loading
Loading