From 6428e2a716c8c4ccc77f7e5e6787cc664dd2ba14 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 10:02:41 +0000 Subject: [PATCH 1/2] fix(crypto): decode inline SSO string args instead of reading them as StringHeader pointers (#11481) A runtime-built string of <= 5 bytes is an inline SSO value (SHORT_STRING_TAG); its low 48 bits are length + payload, not an address. Crypto read string/bytes arguments by masking to 48 bits and handing the result to `bytes_from_ptr`, which dereferenced the payload as a `StringHeader*` and segfaulted. Two layers did this: - stdlib, NaN-boxed f64 args (hash/hmac `update` data and input encoding, `digest` encoding, cipher `update`/`setAuthTag`/`setAAD`, sign/verify `update`, ECDH args, PEM key inputs): new `bytes_from_value(f64)` decodes the SSO bytes directly and otherwise keeps the masked-pointer path. `arg_bytes`, `decode_hash_update_value`, `decode_crypto_value`, `decode_ecdh_input` and the direct masks route through it; the now-unused `arg_ptr` is removed. - codegen, i64 FFI args (`createHash`/`createHmac` algorithm and key, pbkdf2/scrypt/hkdf/argon2 inputs, `createCipheriv`, `createSign`, `createECDH`, `createSecretKey`, `generateKey*`, one-shot sign/verify and RSA encrypt/decrypt data): unbox via `unbox_str_handle` (`js_get_string_pointer_unified`, which materializes SSO to a heap header) instead of the raw `unbox_to_i64` mask. The generateKeyPairSync options object keeps the plain mask. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LdEUAyNTC8iuUa5RDkLyny --- .../src/expr/calls/crypto_hash.rs | 6 +- .../src/expr/calls/crypto_kdf.rs | 48 +++++------ .../src/expr/calls/crypto_keys.rs | 6 +- .../src/expr/calls/crypto_misc.rs | 34 ++++---- crates/perry-stdlib/src/crypto/cipher.rs | 11 +-- crates/perry-stdlib/src/crypto/ecdh.rs | 23 +++--- .../perry-stdlib/src/crypto/hash_handles.rs | 81 ++++++++++++++++++- crates/perry-stdlib/src/crypto/random.rs | 5 +- crates/perry-stdlib/src/crypto/util.rs | 27 +++++-- crates/perry-stdlib/src/crypto/x509.rs | 14 ++-- .../test_gap_11481_crypto_sso_string_args.ts | 43 ++++++++++ 11 files changed, 207 insertions(+), 91 deletions(-) create mode 100644 test-files/test_gap_11481_crypto_sso_string_args.ts diff --git a/crates/perry-codegen/src/expr/calls/crypto_hash.rs b/crates/perry-codegen/src/expr/calls/crypto_hash.rs index 1dde185e46..8810a576fb 100644 --- a/crates/perry-codegen/src/expr/calls/crypto_hash.rs +++ b/crates/perry-codegen/src/expr/calls/crypto_hash.rs @@ -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_str_handle(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_str_handle(blk, &key_box); blk.call( DOUBLE, "js_crypto_create_hmac", @@ -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_str_handle(blk, &alg_box); // Returns an already-NaN-boxed f64 (POINTER_TAG + handle id). if let Some(options_box) = options_box { Ok(blk.call( diff --git a/crates/perry-codegen/src/expr/calls/crypto_kdf.rs b/crates/perry-codegen/src/expr/calls/crypto_kdf.rs index 8fbc2e4f18..3def7aa7f0 100644 --- a/crates/perry-codegen/src/expr/calls/crypto_kdf.rs +++ b/crates/perry-codegen/src/expr/calls/crypto_kdf.rs @@ -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_str_handle(blk, &alg_box); let buf_handle = blk.call( I64, "js_crypto_argon2_sync", @@ -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_str_handle(blk, &alg_box); Ok(blk.call( DOUBLE, "js_crypto_argon2_async", @@ -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_str_handle(blk, &alg_box); + let ikm_handle = unbox_str_handle(blk, &ikm_box); + let salt_handle = unbox_str_handle(blk, &salt_box); + let info_handle = unbox_str_handle(blk, &info_box); let buf_handle = blk.call( I64, "js_crypto_hkdf_bytes_alg", @@ -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_str_handle(blk, &alg_box); + let ikm_handle = unbox_str_handle(blk, &ikm_box); + let salt_handle = unbox_str_handle(blk, &salt_box); + let info_handle = unbox_str_handle(blk, &info_box); Ok(blk.call( DOUBLE, "js_crypto_hkdf_async_alg", @@ -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_str_handle(blk, &pwd_box); + let salt_handle = unbox_str_handle(blk, &salt_box); Ok(blk.call( DOUBLE, "js_crypto_scrypt_async", @@ -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_str_handle(blk, &pwd_box); + let salt_handle = unbox_str_handle(blk, &salt_box); let digest_handle = match &digest_box { - Some(b) => unbox_to_i64(blk, b), + Some(b) => unbox_str_handle(blk, b), None => "0".to_string(), }; let buf_handle = blk.call( @@ -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_str_handle(blk, &pwd_box); + let salt_handle = unbox_str_handle(blk, &salt_box); + let alg_handle = unbox_str_handle(blk, &alg_box); Ok(blk.call( DOUBLE, "js_crypto_pbkdf2_async_alg", @@ -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_str_handle(blk, &pwd_box); + let salt_handle = unbox_str_handle(blk, &salt_box); let buf_handle = blk.call( I64, "js_crypto_scrypt_bytes", @@ -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_str_handle(blk, &digest_box); + let ikm_handle = unbox_str_handle(blk, &ikm_box); + let salt_handle = unbox_str_handle(blk, &salt_box); + let info_handle = unbox_str_handle(blk, &info_box); let buf_handle = blk.call( I64, "js_crypto_hkdf_sync", diff --git a/crates/perry-codegen/src/expr/calls/crypto_keys.rs b/crates/perry-codegen/src/expr/calls/crypto_keys.rs index cc0c22187e..082acfbad3 100644 --- a/crates/perry-codegen/src/expr/calls/crypto_keys.rs +++ b/crates/perry-codegen/src/expr/calls/crypto_keys.rs @@ -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_str_handle(blk, &alg_box); let fname = if property == "createSign" || property == "Sign" { "js_crypto_create_sign" } else { @@ -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_str_handle(blk, &curve_box); Ok(blk.call(DOUBLE, "js_crypto_create_ecdh", &[(I64, &curve_handle)])) } @@ -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_str_handle(blk, &alg_box); Ok(blk.call( DOUBLE, "js_crypto_generate_key_pair_async", diff --git a/crates/perry-codegen/src/expr/calls/crypto_misc.rs b/crates/perry-codegen/src/expr/calls/crypto_misc.rs index adcb45142b..040b98c139 100644 --- a/crates/perry-codegen/src/expr/calls/crypto_misc.rs +++ b/crates/perry-codegen/src/expr/calls/crypto_misc.rs @@ -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_str_handle(blk, &alg_box); + let key_handle = unbox_str_handle(blk, &key_box); Ok(blk.call( DOUBLE, "js_crypto_create_hmac", @@ -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_str_handle(blk, &alg_box); + let key_handle = unbox_str_handle(blk, &key_box); + let iv_handle = unbox_str_handle(blk, &iv_box); let fname = if property == "createCipheriv" { "js_crypto_create_cipheriv" } else { @@ -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_str_handle(blk, &alg_box); let fname = if property == "createSign" { "js_crypto_create_sign" } else { @@ -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_str_handle(blk, &alg_box); + let data_handle = unbox_str_handle(blk, &data_box); if let Some(callback_box) = callback_box { return Ok(blk.call( DOUBLE, @@ -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_str_handle(blk, &alg_box); + let data_handle = unbox_str_handle(blk, &data_box); + let sig_handle = unbox_str_handle(blk, &sig_box); if let Some(callback_box) = callback_box { return Ok(blk.call( DOUBLE, @@ -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_str_handle(blk, &data_box); let fname = match property { "publicEncrypt" => "js_crypto_public_encrypt", "privateDecrypt" => "js_crypto_private_decrypt", @@ -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_str_handle(blk, &key_box); let enc_handle = if let Some(enc) = enc_box { - unbox_to_i64(blk, &enc) + unbox_str_handle(blk, &enc) } else { "0".to_string() }; @@ -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_str_handle(blk, &alg_box); let buf_handle = blk.call( I64, "js_crypto_generate_key_sync", @@ -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_str_handle(blk, &alg_box); Ok(blk.call( DOUBLE, "js_crypto_generate_key_async", @@ -609,7 +609,7 @@ 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_str_handle(blk, &type_box); let opts_handle = match &opts_box { Some(b) => unbox_to_i64(blk, b), None => "0".to_string(), diff --git a/crates/perry-stdlib/src/crypto/cipher.rs b/crates/perry-stdlib/src/crypto/cipher.rs index dabff10f88..17ece5377f 100644 --- a/crates/perry-stdlib/src/crypto/cipher.rs +++ b/crates/perry-stdlib/src/crypto/cipher.rs @@ -724,10 +724,7 @@ pub unsafe fn dispatch_cipher(handle: i64, method: &str, args: &[f64]) -> f64 { let str_bytes = string_bytes_from_arg(args[0]); decode_string_bytes_with_tag(&str_bytes, in_tag) } - None => { - let ptr = (args[0].to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - bytes_from_ptr(ptr) - } + None => bytes_from_value(args[0]), }; let previous_len = state.buffer.len(); state.buffer.extend_from_slice(&bytes); @@ -1160,8 +1157,7 @@ pub unsafe fn dispatch_cipher(handle: i64, method: &str, args: &[f64]) -> f64 { if state.encrypt || !state.kind.is_gcm() { return nanbox_undefined(); } - let ptr = (args[0].to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - let tag = bytes_from_ptr(ptr); + let tag = bytes_from_value(args[0]); state.auth_tag = Some(tag); nanbox_pointer_f64(handle as usize) } @@ -1172,8 +1168,7 @@ pub unsafe fn dispatch_cipher(handle: i64, method: &str, args: &[f64]) -> f64 { if args.is_empty() { state.aad.clear(); } else { - let ptr = (args[0].to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - state.aad = bytes_from_ptr(ptr); + state.aad = bytes_from_value(args[0]); } nanbox_pointer_f64(handle as usize) } diff --git a/crates/perry-stdlib/src/crypto/ecdh.rs b/crates/perry-stdlib/src/crypto/ecdh.rs index bfbf19f477..038131395c 100644 --- a/crates/perry-stdlib/src/crypto/ecdh.rs +++ b/crates/perry-stdlib/src/crypto/ecdh.rs @@ -53,7 +53,7 @@ pub unsafe extern "C" fn js_crypto_create_diffie_hellman( let second_string = if second_val.is_finite() { String::new() } else { - String::from_utf8(bytes_from_ptr(arg_ptr(second_val))).unwrap_or_default() + String::from_utf8(bytes_from_value(second_val)).unwrap_or_default() }; let (prime_encoding, generator_value, generator_encoding) = if matches!( second_string.as_str(), @@ -110,8 +110,7 @@ pub unsafe extern "C" fn js_crypto_ecdh_convert_key( output_encoding_val: f64, format_val: f64, ) -> f64 { - let curve_ptr = arg_ptr(curve_val); - let curve = String::from_utf8(bytes_from_ptr(curve_ptr)) + let curve = String::from_utf8(bytes_from_value(curve_val)) .unwrap_or_default() .to_ascii_lowercase(); if !matches!(curve.as_str(), "prime256v1" | "secp256r1" | "p-256") { @@ -119,11 +118,11 @@ pub unsafe extern "C" fn js_crypto_ecdh_convert_key( } let input_encoding = - String::from_utf8(bytes_from_ptr(arg_ptr(input_encoding_val))).unwrap_or_default(); + String::from_utf8(bytes_from_value(input_encoding_val)).unwrap_or_default(); let output_encoding = - String::from_utf8(bytes_from_ptr(arg_ptr(output_encoding_val))).unwrap_or_default(); - let format = String::from_utf8(bytes_from_ptr(arg_ptr(format_val))).unwrap_or_default(); - let key_bytes = decode_ecdh_input(arg_ptr(key_val), &input_encoding); + String::from_utf8(bytes_from_value(output_encoding_val)).unwrap_or_default(); + let format = String::from_utf8(bytes_from_value(format_val)).unwrap_or_default(); + let key_bytes = decode_ecdh_input(key_val, &input_encoding); let public = match P256PublicKey::from_sec1_bytes(&key_bytes) { Ok(public) => public, Err(_) => return f64::from_bits(0x7FFC_0000_0000_0001), @@ -159,8 +158,7 @@ pub unsafe fn dispatch_sign(handle: i64, method: &str, args: &[f64]) -> f64 { } match method { "update" if !args.is_empty() => { - let ptr = (args[0].to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - let bytes = bytes_from_ptr(ptr); + let bytes = bytes_from_value(args[0]); h.data.lock().unwrap().extend_from_slice(&bytes); f64::from_bits(0x7FFD_0000_0000_0000u64 | ((handle as u64) & 0x0000_FFFF_FFFF_FFFF)) } @@ -281,7 +279,7 @@ pub unsafe fn dispatch_ecdh(handle: i64, method: &str, args: &[f64]) -> f64 { }; let input_encoding = arg_string(args, 1); let output_encoding = arg_string(args, 2); - let public_bytes = decode_ecdh_input(arg_ptr(args[0]), &input_encoding); + let public_bytes = decode_ecdh_input(args[0], &input_encoding); let public = match P256PublicKey::from_sec1_bytes(&public_bytes) { Ok(public) => public, Err(_) => return f64::from_bits(0x7FFC_0000_0000_0001), @@ -473,8 +471,7 @@ pub unsafe fn dispatch_verify(handle: i64, method: &str, args: &[f64]) -> f64 { } match method { "update" if !args.is_empty() => { - let ptr = (args[0].to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - let bytes = bytes_from_ptr(ptr); + let bytes = bytes_from_value(args[0]); h.data.lock().unwrap().extend_from_slice(&bytes); f64::from_bits(0x7FFD_0000_0000_0000u64 | ((handle as u64) & 0x0000_FFFF_FFFF_FFFF)) } @@ -491,7 +488,7 @@ pub unsafe fn dispatch_verify(handle: i64, method: &str, args: &[f64]) -> f64 { encoding_tag_from_arg(args.get(2).copied()).unwrap_or(EncodingTag(0)); decode_string_bytes_with_tag(signature.as_bytes(), encoding) } else { - bytes_from_ptr(arg_ptr(args[1])) + bytes_from_value(args[1]) }; let pem = match crypto_key_input_to_public_pem(key_bits) { Some(pem) => pem, diff --git a/crates/perry-stdlib/src/crypto/hash_handles.rs b/crates/perry-stdlib/src/crypto/hash_handles.rs index 569820d69a..df99afba65 100644 --- a/crates/perry-stdlib/src/crypto/hash_handles.rs +++ b/crates/perry-stdlib/src/crypto/hash_handles.rs @@ -485,8 +485,7 @@ pub unsafe fn dispatch_hash(handle: i64, method: &str, args: &[f64]) -> f64 { { output_encoding.to_ascii_lowercase() } else { - let enc_ptr = (args[0].to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - let enc_bytes = bytes_from_ptr(enc_ptr); + let enc_bytes = bytes_from_value(args[0]); std::str::from_utf8(&enc_bytes) .unwrap_or("hex") .to_ascii_lowercase() @@ -744,8 +743,7 @@ pub unsafe fn dispatch_hmac(handle: i64, method: &str, args: &[f64]) -> f64 { let buf = alloc_buffer_from_slice(&digest); f64::from_bits(0x7FFD_0000_0000_0000u64 | ((buf as u64) & 0x0000_FFFF_FFFF_FFFF)) } else { - let enc_ptr = (args[0].to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - let enc_bytes = bytes_from_ptr(enc_ptr); + let enc_bytes = bytes_from_value(args[0]); let enc = std::str::from_utf8(&enc_bytes) .unwrap_or("hex") .to_ascii_lowercase(); @@ -867,3 +865,78 @@ pub unsafe fn dispatch_hmac_property(handle: i64, property: &str) -> f64 { // `final()` has run. For decrypt-side GCM, `setAuthTag(buf)` must be called // before `final()` so the verifier can authenticate. // --------------------------------------------------------------------------- + +#[cfg(test)] +mod sso_arg_tests { + use super::*; + + /// A runtime-built short string is an inline SSO value, not a heap + /// `StringHeader`. Assert the fixture really is one, or every check + /// below would pass on the heap path and prove nothing. + fn sso(s: &str) -> f64 { + let v = perry_runtime::string::js_string_new_sso(s.as_ptr(), s.len() as u32); + assert!( + JSValue::from_bits(v.to_bits()).is_short_string(), + "fixture {s:?} must be an inline SSO value" + ); + v + } + + fn heap_str(s: &str) -> f64 { + let p = js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(JSValue::string_ptr(p).bits()) + } + + fn handle_of(boxed: f64) -> i64 { + (boxed.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 + } + + fn result_string(v: f64) -> String { + unsafe { string_from_jsvalue(v.to_bits()) }.expect("digest must return a string") + } + + /// #11481: `createHash("sha1").update("x" + 3).digest("hex")` segfaulted + /// in `bytes_from_ptr` because the SSO `update` argument was masked and + /// dereferenced as a `StringHeader*`. The digest encoding is SSO too. + #[test] + fn hash_update_and_digest_accept_sso_strings() { + unsafe { + let h = handle_of(js_crypto_create_hash( + js_string_from_bytes(b"sha1".as_ptr(), 4) as i64, + )); + dispatch_hash(h, "update", &[sso("x3")]); + let out = dispatch_hash(h, "digest", &[sso("hex")]); + assert_eq!( + result_string(out), + "15da3daa68966ce00bc4d1103f0561573cd36b8a" + ); + + // SSO data with an SSO input encoding (`update(str, "hex")`). + let h = handle_of(js_crypto_create_hash( + js_string_from_bytes(b"sha1".as_ptr(), 4) as i64, + )); + dispatch_hash(h, "update", &[sso("7833"), sso("hex")]); + let out = dispatch_hash(h, "digest", &[heap_str("hex")]); + assert_eq!( + result_string(out), + "15da3daa68966ce00bc4d1103f0561573cd36b8a" + ); + } + } + + #[test] + fn hmac_update_and_digest_accept_sso_strings() { + unsafe { + let h = handle_of(js_crypto_create_hmac( + js_string_from_bytes(b"sha256".as_ptr(), 6) as i64, + js_string_from_bytes(b"k".as_ptr(), 1) as i64, + )); + dispatch_hmac(h, "update", &[sso("x3")]); + let out = dispatch_hmac(h, "digest", &[sso("hex")]); + assert_eq!( + result_string(out), + "12fb7723251890c7dd9b9db6bb12f0130b60f52a4c4afd07999254f672d45835" + ); + } + } +} diff --git a/crates/perry-stdlib/src/crypto/random.rs b/crates/perry-stdlib/src/crypto/random.rs index 4dfbc60836..da5c94fddf 100644 --- a/crates/perry-stdlib/src/crypto/random.rs +++ b/crates/perry-stdlib/src/crypto/random.rs @@ -295,7 +295,7 @@ pub unsafe extern "C" fn js_crypto_native_dispatch( undefined } }; - // SSO-safe StringHeader pointer (matches `unbox_to_i64` on the direct path). + // SSO-safe StringHeader pointer (matches `unbox_str_handle` on the direct path). let str_ptr = |n: usize| -> i64 { perry_runtime::js_get_string_pointer_unified(arg(n)) as i64 }; // A buffer-or-string arg's raw pointer (bytes_from_ptr handles both). let bytes_ptr = |n: usize| -> i64 { @@ -714,8 +714,7 @@ pub(super) unsafe fn crypto_value_bytes(bits: f64) -> Vec { n.to_be_bytes().to_vec() }; } - let ptr = (raw & 0x0000_FFFF_FFFF_FFFF) as i64; - bytes_from_ptr(ptr) + bytes_from_value(bits) } pub(super) fn bytes_to_u128(bytes: &[u8]) -> Option { diff --git a/crates/perry-stdlib/src/crypto/util.rs b/crates/perry-stdlib/src/crypto/util.rs index de22559015..19e6cfc8fe 100644 --- a/crates/perry-stdlib/src/crypto/util.rs +++ b/crates/perry-stdlib/src/crypto/util.rs @@ -77,6 +77,22 @@ pub(super) unsafe fn bytes_from_ptr(ptr: i64) -> Vec { std::slice::from_raw_parts(data, len).to_vec() } +/// Extract the raw bytes of a NaN-boxed argument value. An inline short +/// string (SSO, `SHORT_STRING_TAG`) carries its bytes in the value itself and +/// has no heap `StringHeader`, so masking it down to 48 bits and handing the +/// result to [`bytes_from_ptr`] dereferences its payload as an address +/// (#11481). Decode that representation directly; every other value keeps +/// the existing masked-pointer path (Buffer / heap string / raw pointer). +pub(super) unsafe fn bytes_from_value(value: f64) -> Vec { + let js = JSValue::from_bits(value.to_bits()); + if js.is_short_string() { + let mut buf = [0u8; perry_runtime::value::SHORT_STRING_MAX_LEN]; + let n = js.short_string_to_buf(&mut buf); + return buf[..n].to_vec(); + } + bytes_from_ptr((value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64) +} + /// Allocate a new Buffer, copy `bytes` into it, return the registered pointer. pub(super) unsafe fn alloc_buffer_from_slice( bytes: &[u8], @@ -393,8 +409,7 @@ pub(super) unsafe fn string_bytes_from_arg(arg: f64) -> Vec { if let Some(s) = string_from_jsvalue(arg.to_bits()) { return s.into_bytes(); } - let ptr = (arg.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - bytes_from_ptr(ptr) + bytes_from_value(arg) } pub(super) fn js_bool(b: bool) -> f64 { @@ -516,7 +531,7 @@ pub(super) unsafe fn bytes_from_value_bits(bits: u64) -> Option> { if ptr < 0x1000 { return None; } - Some(bytes_from_ptr(ptr as i64)) + Some(bytes_from_value(f64::from_bits(bits))) } pub(super) unsafe fn object_field_bytes(obj_bits: u64, name: &[u8]) -> Option> { @@ -749,8 +764,7 @@ pub(super) unsafe fn crypto_key_input_to_private_pem(value_bits: u64) -> Option< if matches!(format.as_deref(), Some(f) if f.eq_ignore_ascii_case("jwk")) { return jwk_rsa_private_to_pem(value_bits).or_else(|| jwk_ec_private_to_pem(value_bits)); } - let ptr = (value_bits & 0x0000_FFFF_FFFF_FFFF) as i64; - String::from_utf8(bytes_from_ptr(ptr)).ok() + String::from_utf8(bytes_from_value(f64::from_bits(value_bits))).ok() } pub(super) unsafe fn crypto_key_input_to_public_pem(value_bits: u64) -> Option { @@ -786,8 +800,7 @@ pub(super) unsafe fn crypto_key_input_to_public_pem(value_bits: u64) -> Option(ptr: *mut T) -> f64 { f64::from_bits(0x7FFD_0000_0000_0000u64 | ((ptr as u64) & 0x0000_FFFF_FFFF_FFFF)) } -pub(super) fn arg_ptr(arg: f64) -> i64 { - (arg.to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64 -} - pub(super) unsafe fn arg_bytes(args: &[f64], idx: usize) -> Vec { args.get(idx) - .map(|arg| bytes_from_ptr(arg_ptr(*arg))) + .map(|arg| bytes_from_value(*arg)) .unwrap_or_default() } @@ -1293,8 +1289,8 @@ pub(super) unsafe fn ecdh_output(bytes: &[u8], encoding: Option<&str>) -> f64 { nanbox_ptr(alloc_buffer_from_slice(bytes)) } -pub(super) unsafe fn decode_ecdh_input(ptr: i64, encoding: &str) -> Vec { - let bytes = bytes_from_ptr(ptr); +pub(super) unsafe fn decode_ecdh_input(value: f64, encoding: &str) -> Vec { + let bytes = bytes_from_value(value); if encoding.eq_ignore_ascii_case("hex") { let s = String::from_utf8(bytes).unwrap_or_default(); return perry_hex::decode(s).unwrap_or_default(); @@ -1309,11 +1305,11 @@ pub(super) unsafe fn decode_ecdh_input(ptr: i64, encoding: &str) -> Vec { } pub(super) unsafe fn decode_crypto_value(value: f64, encoding: &str) -> Vec { - decode_ecdh_input(arg_ptr(value), encoding) + decode_ecdh_input(value, encoding) } pub(super) unsafe fn decode_hash_update_value(value: f64, encoding: &str) -> Vec { - let bytes = bytes_from_ptr(arg_ptr(value)); + let bytes = bytes_from_value(value); if encoding.eq_ignore_ascii_case("hex") { let s = String::from_utf8(bytes).unwrap_or_default(); return perry_hex::decode(s).unwrap_or_default(); diff --git a/test-files/test_gap_11481_crypto_sso_string_args.ts b/test-files/test_gap_11481_crypto_sso_string_args.ts new file mode 100644 index 0000000000..3e7a4cd9f5 --- /dev/null +++ b/test-files/test_gap_11481_crypto_sso_string_args.ts @@ -0,0 +1,43 @@ +// #11481: crypto string arguments built at runtime and short enough to be +// stored inline (SSO) were masked to 48 bits and dereferenced as a heap +// `StringHeader*`, segfaulting in `bytes_from_ptr`. Every value below is +// computed at runtime so none of them can be folded into a heap literal. +import * as crypto from "node:crypto"; + +const n = 3; +const short = "x" + (n & 7); // "x3" +const hexEnc = "he" + String.fromCharCode(120); // "hex" +const alg = "sha" + (n - 2); // "sha1" +const key = "k" + (n & 1); // "k1" +const hexData = "78" + (30 + n); // "7833" === hex of "x3" + +// hash: update data, update input encoding, digest encoding, algorithm. +console.log(crypto.createHash("sha1").update(short).digest("hex")); +console.log(crypto.createHash("sha1").update(short).digest(hexEnc)); +console.log(crypto.createHash(alg).update(short).digest("hex")); +console.log(crypto.createHash("sha1").update(hexData, hexEnc).digest("hex")); + +// hmac: key, data, digest encoding. +console.log(crypto.createHmac("sha256", key).update(short).digest("hex")); +console.log(crypto.createHmac(alg, key).update(short).digest(hexEnc)); + +// kdfs: password/salt. +console.log(crypto.pbkdf2Sync(key, short, 1, 16, alg).toString("hex")); +console.log(crypto.scryptSync(key, short, 16).toString("hex")); + +// cipher.update with no input encoding. +const cipher = crypto.createCipheriv( + "aes-128-cbc", + "0123456789abcdef", + "fedcba9876543210", +); +const ct = Buffer.concat([cipher.update(short), cipher.final()]); +console.log(ct.toString("hex")); +const decipher = crypto.createDecipheriv( + "aes-128-cbc", + "0123456789abcdef", + "fedcba9876543210", +); +console.log( + Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8"), +); From bdb0c62366c7f940478b618f06a8ff8217e36a8e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 10:03:15 +0000 Subject: [PATCH 2/2] docs: changelog fragment for #11513 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LdEUAyNTC8iuUa5RDkLyny --- changelog.d/11513-crypto-sso-string-args.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/11513-crypto-sso-string-args.md diff --git a/changelog.d/11513-crypto-sso-string-args.md b/changelog.d/11513-crypto-sso-string-args.md new file mode 100644 index 0000000000..2703d3627f --- /dev/null +++ b/changelog.d/11513-crypto-sso-string-args.md @@ -0,0 +1 @@ +**crypto: short runtime-built strings no longer segfault crypto calls (#11481).** A string of 5 bytes or fewer built at runtime is stored inline (SSO, `SHORT_STRING_TAG`); its low 48 bits are length + payload, not an address. Crypto masked every string/bytes argument to 48 bits and handed it to `bytes_from_ptr`, which dereferenced the payload as a `StringHeader*` — so `createHash("sha1").update("x" + n)`, `digest(enc)` with a computed encoding, `createHash(alg)` / `createHmac(alg, key)` with a computed algorithm or key, and pbkdf2/scrypt/hkdf password/salt/digest all segfaulted. Two layers are fixed: the stdlib's f64-argument readers now go through a new `bytes_from_value` (`crypto/util.rs`) that decodes SSO bytes directly, and the codegen crypto arms (`expr/calls/crypto_{hash,kdf,keys,misc}.rs`) unbox string/bytes arguments with `unbox_str_handle` instead of the raw `unbox_to_i64` mask. Covered by `sso_arg_tests` in `crypto/hash_handles.rs` and `test-files/test_gap_11481_crypto_sso_string_args.ts`.