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
1 change: 1 addition & 0 deletions changelog.d/11486-crypto-sso-args.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**fix(codegen,runtime): crypto natives accept a short (SSO) string argument, so `hash.update(String(n))` no longer segfaults (#11430 follow-up).** The crypto call sites unboxed every string argument with `bits & POINTER_MASK`: 48 codegen arms, plus `arg_ptr` and the open-coded masks in perry-stdlib. An SSO string's inline characters therefore became an address that `bytes_from_ptr` dereferenced. `hash.update(String(7))`, an HMAC key, `pbkdf2Sync` / `hkdfSync` inputs and a cipher's `update` input all crashed. The new `js_ffi_arg_ptr` copies an SSO argument into a per-thread ring of non-GC `StringHeader` slots and leaves every other value's unboxing unchanged. The new gap test is `test_gap_11430_crypto_short_string_args`.
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::types::{DOUBLE, I64};

use super::{
emit_string_literal_global, lower_expr, nanbox_pointer_inline, nanbox_string_inline,
unbox_str_handle, unbox_to_i64, FnCtx,
unbox_ffi_str_arg, unbox_str_handle, FnCtx,
};

mod crypto_hash;
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-codegen/src/expr/calls/crypto_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,13 @@ pub(crate) fn arm_crypto_hash_chain(
}
}
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
// Allocate the handle. Both helpers return f64 already
// NaN-boxed with POINTER_TAG, suitable as the receiver
// for `js_native_call_method`.
let recv = if create_method == "createHmac" || create_method == "Hmac" {
let key_box = key_box_opt.expect("createHmac needs a key arg");
let key_handle = unbox_to_i64(blk, &key_box);
let key_handle = unbox_ffi_str_arg(blk, &key_box);
blk.call(
DOUBLE,
"js_crypto_create_hmac",
Expand Down Expand Up @@ -337,7 +337,7 @@ pub(crate) fn arm_crypto_create_hash(
// #2013/#3146: reject a non-string algorithm before unboxing.
emit_validate_string_arg(ctx, &alg_box, "algorithm");
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
// Returns an already-NaN-boxed f64 (POINTER_TAG + handle id).
if let Some(options_box) = options_box {
Ok(blk.call(
Expand Down
48 changes: 24 additions & 24 deletions crates/perry-codegen/src/expr/calls/crypto_kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub(crate) fn arm_crypto_argon2_sync(
let alg_box = lower_expr(ctx, &args[0])?;
let params_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let buf_handle = blk.call(
I64,
"js_crypto_argon2_sync",
Expand All @@ -40,7 +40,7 @@ pub(crate) fn arm_crypto_argon2(
let params_box = lower_expr(ctx, &args[1])?;
let cb_box = lower_expr(ctx, &args[2])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
Ok(blk.call(
DOUBLE,
"js_crypto_argon2_async",
Expand All @@ -63,10 +63,10 @@ pub(crate) fn arm_crypto_hkdf_sync_alg(
let info_box = lower_expr(ctx, &args[3])?;
let len_box = lower_expr(ctx, &args[4])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let ikm_handle = unbox_to_i64(blk, &ikm_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let info_handle = unbox_to_i64(blk, &info_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let ikm_handle = unbox_ffi_str_arg(blk, &ikm_box);
let salt_handle = unbox_ffi_str_arg(blk, &salt_box);
let info_handle = unbox_ffi_str_arg(blk, &info_box);
let buf_handle = blk.call(
I64,
"js_crypto_hkdf_bytes_alg",
Expand Down Expand Up @@ -97,10 +97,10 @@ pub(crate) fn arm_crypto_hkdf_async_alg(
let len_box = lower_expr(ctx, &args[4])?;
let cb_box = lower_expr(ctx, &args[5])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let ikm_handle = unbox_to_i64(blk, &ikm_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let info_handle = unbox_to_i64(blk, &info_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let ikm_handle = unbox_ffi_str_arg(blk, &ikm_box);
let salt_handle = unbox_ffi_str_arg(blk, &salt_box);
let info_handle = unbox_ffi_str_arg(blk, &info_box);
Ok(blk.call(
DOUBLE,
"js_crypto_hkdf_async_alg",
Expand Down Expand Up @@ -137,8 +137,8 @@ pub(crate) fn arm_crypto_scrypt(
};
let cb_box = lower_expr(ctx, cb_expr)?;
let blk = ctx.block();
let pwd_handle = unbox_to_i64(blk, &pwd_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let pwd_handle = unbox_ffi_str_arg(blk, &pwd_box);
let salt_handle = unbox_ffi_str_arg(blk, &salt_box);
Ok(blk.call(
DOUBLE,
"js_crypto_scrypt_async",
Expand Down Expand Up @@ -180,10 +180,10 @@ pub(crate) fn arm_crypto_pbkdf2_sync(
emit_validate_string_arg(ctx, db, "digest");
}
let blk = ctx.block();
let pwd_handle = unbox_to_i64(blk, &pwd_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let pwd_handle = unbox_ffi_str_arg(blk, &pwd_box);
let salt_handle = unbox_ffi_str_arg(blk, &salt_box);
let digest_handle = match &digest_box {
Some(b) => unbox_to_i64(blk, b),
Some(b) => unbox_ffi_str_arg(blk, b),
None => "0".to_string(),
};
let buf_handle = blk.call(
Expand Down Expand Up @@ -216,9 +216,9 @@ pub(crate) fn arm_crypto_pbkdf2_async(
let alg_box = lower_expr(ctx, &args[4])?;
let cb_box = lower_expr(ctx, &args[5])?;
let blk = ctx.block();
let pwd_handle = unbox_to_i64(blk, &pwd_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let alg_handle = unbox_to_i64(blk, &alg_box);
let pwd_handle = unbox_ffi_str_arg(blk, &pwd_box);
let salt_handle = unbox_ffi_str_arg(blk, &salt_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
Ok(blk.call(
DOUBLE,
"js_crypto_pbkdf2_async_alg",
Expand Down Expand Up @@ -253,8 +253,8 @@ pub(crate) fn arm_crypto_scrypt_sync(
// #2013/#3146: node validates keylen as an integer in [0, 2^31-1].
emit_validate_integer_arg(ctx, &keylen_box, "keylen", 0.0, i32::MAX as f64);
let blk = ctx.block();
let pwd_handle = unbox_to_i64(blk, &pwd_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let pwd_handle = unbox_ffi_str_arg(blk, &pwd_box);
let salt_handle = unbox_ffi_str_arg(blk, &salt_box);
let buf_handle = blk.call(
I64,
"js_crypto_scrypt_bytes",
Expand Down Expand Up @@ -283,10 +283,10 @@ pub(crate) fn arm_crypto_hkdf_sync(
let info_box = lower_expr(ctx, &args[3])?;
let keylen_box = lower_expr(ctx, &args[4])?;
let blk = ctx.block();
let digest_handle = unbox_to_i64(blk, &digest_box);
let ikm_handle = unbox_to_i64(blk, &ikm_box);
let salt_handle = unbox_to_i64(blk, &salt_box);
let info_handle = unbox_to_i64(blk, &info_box);
let digest_handle = unbox_ffi_str_arg(blk, &digest_box);
let ikm_handle = unbox_ffi_str_arg(blk, &ikm_box);
let salt_handle = unbox_ffi_str_arg(blk, &salt_box);
let info_handle = unbox_ffi_str_arg(blk, &info_box);
let buf_handle = blk.call(
I64,
"js_crypto_hkdf_sync",
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-codegen/src/expr/calls/crypto_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub(crate) fn arm_crypto_create_sign_verify_legacy(
};
let alg_box = lower_expr(ctx, &args[0])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let fname = if property == "createSign" || property == "Sign" {
"js_crypto_create_sign"
} else {
Expand All @@ -44,7 +44,7 @@ pub(crate) fn arm_crypto_create_ecdh(
}
let curve_box = lower_expr(ctx, &args[0])?;
let blk = ctx.block();
let curve_handle = unbox_to_i64(blk, &curve_box);
let curve_handle = unbox_ffi_str_arg(blk, &curve_box);
Ok(blk.call(DOUBLE, "js_crypto_create_ecdh", &[(I64, &curve_handle)]))
}

Expand Down Expand Up @@ -129,7 +129,7 @@ pub(crate) fn arm_crypto_generate_key_pair_async(
let options = lower_expr(ctx, &args[1])?;
let callback = lower_expr(ctx, &args[2])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
Ok(blk.call(
DOUBLE,
"js_crypto_generate_key_pair_async",
Expand Down
36 changes: 18 additions & 18 deletions crates/perry-codegen/src/expr/calls/crypto_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub(crate) fn arm_crypto_create_hmac(
emit_validate_string_arg(ctx, &alg_box, "hmac");
emit_validate_crypto_key_arg(ctx, &key_box, "key");
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let key_handle = unbox_to_i64(blk, &key_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let key_handle = unbox_ffi_str_arg(blk, &key_box);
Ok(blk.call(
DOUBLE,
"js_crypto_create_hmac",
Expand Down Expand Up @@ -59,9 +59,9 @@ pub(crate) fn arm_crypto_create_cipheriv(
double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
};
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let key_handle = unbox_to_i64(blk, &key_box);
let iv_handle = unbox_to_i64(blk, &iv_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let key_handle = unbox_ffi_str_arg(blk, &key_box);
let iv_handle = unbox_ffi_str_arg(blk, &iv_box);
let fname = if property == "createCipheriv" {
"js_crypto_create_cipheriv"
} else {
Expand Down Expand Up @@ -144,7 +144,7 @@ pub(crate) fn arm_crypto_create_sign_verify(
}
let alg_box = lower_expr(ctx, &args[0])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let fname = if property == "createSign" {
"js_crypto_create_sign"
} else {
Expand Down Expand Up @@ -413,8 +413,8 @@ pub(crate) fn arm_crypto_sign(
None
};
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let data_handle = unbox_to_i64(blk, &data_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let data_handle = unbox_ffi_str_arg(blk, &data_box);
if let Some(callback_box) = callback_box {
return Ok(blk.call(
DOUBLE,
Expand Down Expand Up @@ -454,9 +454,9 @@ pub(crate) fn arm_crypto_verify(
None
};
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let data_handle = unbox_to_i64(blk, &data_box);
let sig_handle = unbox_to_i64(blk, &sig_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let data_handle = unbox_ffi_str_arg(blk, &data_box);
let sig_handle = unbox_ffi_str_arg(blk, &sig_box);
if let Some(callback_box) = callback_box {
return Ok(blk.call(
DOUBLE,
Expand Down Expand Up @@ -505,7 +505,7 @@ pub(crate) fn arm_crypto_public_private_crypt(
_ => unreachable!(),
};
let key_handle = blk.call(I64, key_converter, &[(DOUBLE, &key_box)]);
let data_handle = unbox_to_i64(blk, &data_box);
let data_handle = unbox_ffi_str_arg(blk, &data_box);
let fname = match property {
"publicEncrypt" => "js_crypto_public_encrypt",
"privateDecrypt" => "js_crypto_private_decrypt",
Expand Down Expand Up @@ -533,9 +533,9 @@ pub(crate) fn arm_crypto_create_secret_key(
None
};
let blk = ctx.block();
let key_handle = unbox_to_i64(blk, &key_box);
let key_handle = unbox_ffi_str_arg(blk, &key_box);
let enc_handle = if let Some(enc) = enc_box {
unbox_to_i64(blk, &enc)
unbox_ffi_str_arg(blk, &enc)
} else {
"0".to_string()
};
Expand All @@ -559,7 +559,7 @@ pub(crate) fn arm_crypto_generate_key_sync(
let alg_box = lower_expr(ctx, &args[0])?;
let options_box = lower_expr(ctx, &args[1])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
let buf_handle = blk.call(
I64,
"js_crypto_generate_key_sync",
Expand All @@ -581,7 +581,7 @@ pub(crate) fn arm_crypto_generate_key_async(
let options_box = lower_expr(ctx, &args[1])?;
let cb_box = lower_expr(ctx, &args[2])?;
let blk = ctx.block();
let alg_handle = unbox_to_i64(blk, &alg_box);
let alg_handle = unbox_ffi_str_arg(blk, &alg_box);
Ok(blk.call(
DOUBLE,
"js_crypto_generate_key_async",
Expand Down Expand Up @@ -609,9 +609,9 @@ pub(crate) fn arm_crypto_generate_key_pair_sync(
None
};
let blk = ctx.block();
let type_handle = unbox_to_i64(blk, &type_box);
let type_handle = unbox_ffi_str_arg(blk, &type_box);
let opts_handle = match &opts_box {
Some(b) => unbox_to_i64(blk, b),
Some(b) => unbox_ffi_str_arg(blk, b),
None => "0".to_string(),
};
// Returns an already-NaN-boxed object (POINTER_TAG).
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/src/expr/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,14 @@ pub(crate) fn unbox_to_i64(blk: &mut LlBlock, boxed: &str) -> String {
blk.and(I64, &bits, POINTER_MASK_I64)
}

/// `unbox_to_i64` for an argument a native entry reads as a
/// `*const StringHeader` (#11430): an SSO string is copied into a scratch
/// header by `js_ffi_arg_ptr` instead of being masked into a garbage address.
/// Every other value unboxes exactly as `unbox_to_i64` does.
pub(crate) fn unbox_ffi_str_arg(blk: &mut LlBlock, boxed: &str) -> String {
blk.call(I64, "js_ffi_arg_ptr", &[(DOUBLE, boxed)])
}

/// Built-in constructor / namespace names that the runtime pre-populates
/// on the globalThis singleton (`populate_global_this_builtins` in
/// crates/perry-runtime/src/object.rs). Used by codegen to decide whether
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ pub(crate) use helpers::{
expr_produces_fresh_heap_allocation, expr_produces_non_pointer_bits_by_construction,
is_global_this_builtin_function_name, is_global_this_builtin_name,
lower_expr_with_expected_type, lower_js_args_array, store_needs_string_addref,
unbox_str_handle, unbox_to_i64,
unbox_ffi_str_arg, unbox_str_handle, unbox_to_i64,
};
pub(crate) use i32_fast_path::{
can_lower_expr_as_i32, can_lower_expr_as_i32_in_current_region,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
// RegExp.escape(str) — #2899. Takes/returns NaN-boxed f64 (string).
module.declare_function("js_regexp_escape", DOUBLE, &[DOUBLE]);
module.declare_function("js_get_string_pointer_unified", I64, &[DOUBLE]);
module.declare_function("js_ffi_arg_ptr", I64, &[DOUBLE]);
// Strict-equality (`===`) compare for switch case dispatch.
module.declare_function("js_switch_strict_equals", I32, &[DOUBLE, DOUBLE]);
module.declare_function("js_value_to_str_ptr_for_ffi", I64, &[DOUBLE]);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ pub use handle::{
// ----- Basic NaN-box pack / unpack FFI -----
pub(crate) use nanbox::nanbox_string_key;
pub use nanbox::{
js_checkpoint, js_debug_val, js_get_string_pointer_unified, js_nanbox_bigint,
js_checkpoint, js_debug_val, js_ffi_arg_ptr, js_get_string_pointer_unified, js_nanbox_bigint,
js_nanbox_get_bigint, js_nanbox_get_pointer, js_nanbox_get_string_pointer, js_nanbox_is_bigint,
js_nanbox_is_pointer, js_nanbox_is_string, js_nanbox_pointer, js_nanbox_string,
};
Expand Down
68 changes: 68 additions & 0 deletions crates/perry-runtime/src/value/nanbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,71 @@ pub extern "C" fn js_nanbox_is_string(value: f64) -> i32 {
0
}
}

/// Scratch copies of SSO string arguments for native entry points that take a
/// `*const StringHeader` as an `i64` (#11430).
///
/// An SSO value keeps its characters inline in the NaN-box, so the usual
/// `bits & POINTER_MASK` unboxing turns it into a garbage address. Those
/// entries (crypto: `createHmac(alg, key)`, `pbkdf2Sync(password, salt, …)`,
/// `createCipheriv(alg, key, iv)`, …) only read the argument's bytes for the
/// duration of the call, so the characters are copied into a small per-thread
/// ring of NON-GC `StringHeader`-shaped slots rather than materialized onto the
/// GC heap: a heap copy would be an unrooted temporary that the next argument's
/// materialization could move or free before the call runs.
const FFI_SSO_SLOTS: usize = 16;

#[repr(C)]
struct FfiSsoSlot {
header: crate::string::StringHeader,
bytes: [u8; crate::value::SHORT_STRING_MAX_LEN],
}

crate::perry_thread_local! {
static FFI_SSO_RING: std::cell::UnsafeCell<(usize, Box<[FfiSsoSlot]>)> = std::cell::UnsafeCell::new((
0,
(0..FFI_SSO_SLOTS)
.map(|_| FfiSsoSlot {
header: crate::string::StringHeader {
utf16_len: 0,
byte_len: 0,
capacity: 0,
refcount: 0,
flags: 0,
},
bytes: [0; crate::value::SHORT_STRING_MAX_LEN],
})
.collect::<Vec<_>>()
.into_boxed_slice(),
));
}

/// Unbox `value` to the raw pointer a native entry expects, copying an SSO
/// string into a scratch `StringHeader` first (see [`FFI_SSO_SLOTS`]). Every
/// other value unboxes exactly as `bits & POINTER_MASK` does.
#[no_mangle]
pub extern "C" fn js_ffi_arg_ptr(value: f64) -> i64 {
let jsval = JSValue::from_bits(value.to_bits());
if !jsval.is_short_string() {
return (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64;
}
let mut buf = [0u8; crate::value::SHORT_STRING_MAX_LEN];
let n = jsval.short_string_to_buf(&mut buf);
FFI_SSO_RING.with(|cell| {
// SAFETY: thread-local, and no reference escapes this closure; the
// slot's address is handed out as an integer.
let (next, slots) = unsafe { &mut *cell.get() };
let slot = &mut slots[*next];
*next = (*next + 1) % FFI_SSO_SLOTS;
slot.bytes[..n].copy_from_slice(&buf[..n]);
// SSO holds ASCII only, so UTF-16 length equals byte length.
slot.header = crate::string::StringHeader {
utf16_len: n as u32,
byte_len: n as u32,
capacity: n as u32,
refcount: 0,
flags: 0,
};
&slot.header as *const crate::string::StringHeader as i64
})
}
Loading
Loading