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
3 changes: 3 additions & 0 deletions changelog.d/8439-gc-safe-string-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- `String.prototype.toWellFormed()` and `structuredClone()` now keep source strings rooted while allocating their copies, preventing moving garbage collection from reading stale string payload pointers.
8 changes: 5 additions & 3 deletions crates/perry-runtime/src/builtins/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,9 +747,11 @@ fn js_structured_clone_inner(value: f64, depth: usize) -> f64 {
return value;
}
unsafe {
let len = (*str_ptr).byte_len as usize;
let data = (str_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
let new_str = js_string_from_bytes(data, len as u32);
let len = (*str_ptr).byte_len;
let utf16_len = (*str_ptr).utf16_len;
let flags = (*str_ptr).flags;
// Root and refresh the source across the destination allocation.
let new_str = crate::string::string_copy_range(str_ptr, 0, len, utf16_len, flags);
let new_bits = 0x7FFF_0000_0000_0000u64 | (new_str as u64 & 0x0000_FFFF_FFFF_FFFF);
f64::from_bits(new_bits)
}
Expand Down
8 changes: 5 additions & 3 deletions crates/perry-runtime/src/string/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -777,11 +777,13 @@ pub extern "C" fn js_string_to_well_formed(s: *const StringHeader) -> *mut Strin
}
let flags = unsafe { (*s).flags };
let blen = unsafe { (*s).byte_len } as usize;
let data = string_data(s);
if flags & STRING_FLAG_HAS_LONE_SURROGATES == 0 {
// Well-formed UTF-8: return a copy without scanning
return js_string_from_bytes(data, blen as u32);
// Well-formed UTF-8: return a copy without scanning. The destination
// allocation can move `s`, so refresh its payload pointer afterwards.
let utf16_len = unsafe { (*s).utf16_len };
return string_copy_range(s, 0, blen as u32, utf16_len, flags);
}
let data = string_data(s);
// Scan raw bytes and replace every WTF-8 lone-surrogate sequence with U+FFFD.
// WTF-8 surrogate: first byte = 0xED, second = 0xA0..=0xBF, third = 0x80..=0xBF.
let bytes = unsafe { slice::from_raw_parts(data, blen) };
Expand Down
19 changes: 19 additions & 0 deletions test-files/test_gap_gc_string_copy_source_rooting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// #8423: both of these operations used to take a raw pointer into a source
// string's inline payload and then allocate the destination before copying it.
// Keep the two paths under allocation pressure together so moving alloc-point
// collections can exercise the source-rooting contract as that GC path evolves.
// parity-env: PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=0 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_PROTECT_FROMSPACE=1

const astral = "🦆";

let badWellFormed = 0;
let badClone = 0;
for (let i = 0; i < 1000; i++) {
// A fresh, non-SSO source gives the collector a young heap cell each time.
const source = `well-formed-${astral}-${i}-`.repeat(16) + "tail";
if (source.toWellFormed() !== source) badWellFormed++;
if (structuredClone(source) !== source) badClone++;
}

console.log("toWellFormed copies:", badWellFormed === 0);
console.log("structuredClone copies:", badClone === 0);
Loading