From e46918ca99a260557883fe4b6c1c5ff2d20dd993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 20 Aug 2026 06:32:18 +0200 Subject: [PATCH 1/2] fix(runtime): own punycode string bytes before decoding --- .../8424-punycode-owned-string-bytes.md | 3 +++ crates/perry-runtime/src/punycode.rs | 23 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 changelog.d/8424-punycode-owned-string-bytes.md diff --git a/changelog.d/8424-punycode-owned-string-bytes.md b/changelog.d/8424-punycode-owned-string-bytes.md new file mode 100644 index 0000000000..a00ee837b5 --- /dev/null +++ b/changelog.d/8424-punycode-owned-string-bytes.md @@ -0,0 +1,3 @@ +Fixed a moving-GC safety hazard in `punycode.ucs2.decode` by copying string +payload bytes before decoding them instead of exposing the heap payload through +an unbounded borrowed slice. diff --git a/crates/perry-runtime/src/punycode.rs b/crates/perry-runtime/src/punycode.rs index 201c713893..e47f4d7a44 100644 --- a/crates/perry-runtime/src/punycode.rs +++ b/crates/perry-runtime/src/punycode.rs @@ -331,11 +331,14 @@ fn string_ptr_if_string(value: f64) -> Option<*const StringHeader> { None } -fn string_bytes(ptr: *const StringHeader) -> &'static [u8] { +/// Copy a heap string's payload before any subsequent runtime allocation can +/// move it. A slice into the inline payload cannot safely escape this function: +/// the collector rewrites rooted pointer slots, not borrows derived from them. +fn copy_string_bytes(ptr: *const StringHeader) -> Vec { if !crate::string::is_valid_string_ptr(ptr) { - return &[]; + return Vec::new(); } - unsafe { slice::from_raw_parts(string_data(ptr), (*ptr).byte_len as usize) } + unsafe { slice::from_raw_parts(string_data(ptr), (*ptr).byte_len as usize).to_vec() } } fn push_utf16_units_from_scalar(units: &mut Vec, cp: u32) { @@ -425,7 +428,8 @@ pub extern "C" fn js_punycode_ucs2_decode(value: f64) -> f64 { return f64::from_bits(crate::value::JSValue::array_ptr(arr).bits()); }; - let units = decode_wtf8_to_utf16_units(string_bytes(str_ptr)); + let input = copy_string_bytes(str_ptr); + let units = decode_wtf8_to_utf16_units(&input); let mut arr = crate::array::js_array_alloc(units.len() as u32); let mut i = 0usize; while i < units.len() { @@ -496,6 +500,17 @@ pub extern "C" fn js_punycode_ucs2_encode(value: f64) -> f64 { mod ucs2_tests { use super::*; + #[test] + fn heap_string_bytes_are_copied_into_owned_storage() { + let source = b"owned-before-allocation"; + let ptr = crate::string::js_string_from_bytes(source.as_ptr(), source.len() as u32); + + let copied = copy_string_bytes(ptr); + unsafe { string_data(ptr).cast_mut().write(b'X') }; + + assert_eq!(copied, source); + } + #[test] fn wtf8_decode_preserves_lone_surrogates() { assert_eq!( From 644ebc53e82845f4130a1ad918961d96e9460522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 20 Aug 2026 06:33:07 +0200 Subject: [PATCH 2/2] chore: key changelog fragment to PR 8440 --- ...-owned-string-bytes.md => 8440-punycode-owned-string-bytes.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{8424-punycode-owned-string-bytes.md => 8440-punycode-owned-string-bytes.md} (100%) diff --git a/changelog.d/8424-punycode-owned-string-bytes.md b/changelog.d/8440-punycode-owned-string-bytes.md similarity index 100% rename from changelog.d/8424-punycode-owned-string-bytes.md rename to changelog.d/8440-punycode-owned-string-bytes.md