diff --git a/changelog.d/PENDING-buffer-b2c-b4.md b/changelog.d/PENDING-buffer-b2c-b4.md new file mode 100644 index 0000000000..3136982171 --- /dev/null +++ b/changelog.d/PENDING-buffer-b2c-b4.md @@ -0,0 +1,78 @@ +Route native byte consumers through scoped byte access or owner-thread pins. Crypto, WebCrypto, SQLite, TLS, HTTP/2, ethers, web streams/BYOB, StringDecoder, querystring, filesystem results, TextEncoder/TextDecoder, V8/VM and structured-clone copies no longer derive byte addresses from buffer headers. `copy_value` roots a source before destination allocation and resolves its bytes afterward. + +Compatible B4 changes move detached state into the owner header (deleting `DETACHED_BUFFER_REGISTRY` and `EVER_DETACHED`), replace `INLINE_OWNING_U32_CACHE` with a current-header admission check, and prefix all three persistent-symbol factories with an eight-byte leaf GC header. Detached state uses bit 14, leaving bits 3–5 for GC survival age; the existing bounded pin counter uses bits 9–13 (31 simultaneous pins per owner, returning `PinLimit` on overflow). The source gate also checks old raw byte-helper calls and emitted offsets; `run_lint_gates.sh` discovers it through the existing lint workflow. Child-process sabotages exercise the detector and the runtime/consumer contracts. + +Boundary still pending: generated code directly links `PERRY_U8_INLINE_CACHE` and `PERRY_TA_KIND_CACHE`, and reads typed-array metadata at +8/+10. The proposed unified layout puts a data/owner pointer at +8. Removing those exported caches or replacing those fields requires the B4c codegen switch, explicitly excluded from this lane until #12023. The six-table deletion and unified 16-byte layout therefore cannot all be completed within that boundary. No placement policy, versions, fetch bodies, zlib implementation, or runtime node_stream files are changed. + +The remaining B2c work includes native-addon APIs that return raw pointers, private Array.sort byte access, and typed-array creation paths coupled to the old layout. Their pointer lifetimes or representation must be adapted before the source gate can become a zero-debt invariant. This delivery is the compatible conversion subset, not completion of all B2c/B4 requirements. Zlib retains B1's wrapper and output witness. + +The exact closed census rows are in `scripts/buffer_b4_census_closed.tsv`: 73 rows (42 runtime, 27 stdlib, 2 updater, 1 ext-http, 1 ext-net), comprising 35 creation, 28 size-assumption, 8 unscoped-borrow and 2 pointer-across-GC sites. Other ext producers already use B1's C ABI wrapper. The expanded source gate has 284 existing sites and zero additions; the same detector found 444 at the earlier B1 baseline, with 161 removed by this lane, one by upstream #12094, and two subsequent upstream verifier/test reads added. It is a ratchet, not the still-pending zero-debt layout invariant. + +| Machinery | Deleted | Still pending | +|---|---|---| +| Address-keyed tables | `DETACHED_BUFFER_REGISTRY` | `VIEW_REGISTRY`, `BACKING_TO_VIEWS`, `RESIZABLE_BUFFER_MAX`, `BUFFER_AB_ALIAS`, `TYPED_ARRAY_VIEW_META` | +| Latches | `EVER_DETACHED` | `RESIZABLE_BUFFER_EVER_MARKED`, `BUFFER_AB_ALIAS_EVER_SET` | +| Address caches | `INLINE_OWNING_U32_CACHE` | `PERRY_U8_INLINE_CACHE`, `PERRY_TA_KIND_CACHE` | + +| Contract | Witness and sabotage | Result | +|---|---|---| +| T1/T2: moving collection; borrow allocation forbidden | B1 byte tests; native-store pin remains rooted while a young object actually moves; allocation inside no_gc aborts | Native contract PASS / RED; the forced young-byte B6-knob variant is not run | +| T3: detach during operation | Deflate data listener transfers the input owner; native last-pin lifetime witness with B1 `detach_free` and B4 `detach_mark` sabotages | Node parity PASS on main and head; Native ASan witness PASS / RED; the TS stream itself is not run under ASan | +| T4: worker transfer | 32 MiB transfer preserves backing pointer, sender length zero, receiver contents and backing count; `transfer_copy` | PASS / RED | +| T5: large concat and nested views | 300 × 18,000 bytes; only the nested view roots its owner across minor and full GC; `view_edge` | Content/identity/owner lifetime PASS / RED; the edge still uses the legacy registry; the placement-counter variant awaits B3 | +| T6: owner access checks | u8, i32 and DataView share writes; shrink/OOB/grow/detach; `owner_check` | PASS / RED | +| T7: native outputs | Crypto sizes 0, 1, 255, 256, 257 and 1 MiB; random fill bounds; `crypto_output`, `crypto_borrow`, `random_fill_range`; B1 C ABI producer contracts | Consumer contracts PASS / RED; the physical-placement test with INLINE_MAX forced to 256 awaits B3/B4 | +| T8: Buffer parameter view | Main's `test_gap_12094_buffer_param_views` | Main and head PASS | +| T9: whole-module invariant | Source gate; owner-edge witness; header and root-holder gates | Ratchet PASS, nine source sabotages RED; address-table-free invariant pending B4c | +| Symbol header and u32 admission | Three persistent-symbol factories; current-header u32 admission; `symbol_header`, `u32_admission` | PASS / RED | + +Verification baseline: origin/main `81e65f33ebdc72d75431b03427dcbfb2f153d7b1`; tested B1 `f3e59107868b737b3b5646a7d0b19dc4f9193f8f`. The final required refresh rebases onto B1 `16b16c96f3cb1f4f81a4bee62dc78c94be59cf83`; its additions are documentation and cfg(test) only, with no production change. The seven FFI buffer tests, including the added native-input consumer and its RED sabotage, pass with `runtime-link`. The final release builds, all 54 crate-test binaries, gap comparison, output checks and measurements include current main's iterator-close and from-space verifier changes, plus B1's process-shared pin fix. Measured production sources are those of `48205175a7`; later changes are harnesses, evidence and cfg(test) witnesses. + +| Crate | Main passed / failed / ignored | Head passed / failed / ignored | New failures | +|---|---|---|---| +| runtime | 5,156 / 0 / 5 | 5,169 / 0 / 5 | 0 | +| stdlib | 251 / 2 / 0 | 258 / 2 / 0 | 0 | +| codegen | 2,564 / 0 / 1 | 2,564 / 0 / 1 | 0 | + +The identical stdlib failures are `runtime_thread_exit_tests::symbols_tests::thread_exit_releases_the_threads_symbol_side_table_entries` and `runtime_thread_exit_tests::thread_exit_releases_the_threads_closure_side_table_entries`. The requested gap union contains 173 cases. Raw main results are 158 pass / 15 parity failures; raw head results are 157 / 16. The single difference is `test_gap_webcrypto_async_threadpool`: head's Node oracle printed `subtle.digest crosses macrotask: false`, while Perry printed true. Ten isolated, interleaved Node comparisons (five per arm) all passed. The original discrepancy is retained as Node timing variation, with zero persistent regressions established. The remaining 15 parity failures reproduce on both arms. Both #12094 and the new detach-during-zlib fixture pass. + +All 14 program/kernel output checks pass against Node. Forced-GC focused tests pass: five B4, eight B1 byte-access, one transfer and three crypto tests, with six B4, seven B1 and three crypto child sabotages red. ASan's instrumented runtime with the system allocator passes 14 native tests (eight byte-access, five B4, one transfer), including the runtime child sabotages; leak detection is disabled for intentional process-lifetime objects. The TS zlib stream's output is checked against Node; that stream executable is not run under ASan. + +All 26 distinct byte-contract/source sabotages turn red (16 runtime/crypto, nine source-gate, one FFI). Final source-layout, header-constant, root-holder and pin-custody checks pass, as do their applicable self-tests and the lint-runner inventory self-test. The full lint tier is not claimed: the file-size gate has three unchanged main violations (`dynamic_dispatch.rs`, `delete_rest.rs`, `method_site.rs`). Exact crate summaries, raw gap outcomes, rechecks, ASan summaries and output statuses are recorded in `scripts/fixtures/buffer_b4_verification.json`; interleaved trials and medians are in `scripts/fixtures/buffer_b4_measurements.json`. + +Measurements use qb6 CPUs 56–63 under the shared lock, ASLR disabled, separate targets, Node 26.5.1 output checks and n=5 interleaved trials with identical-main-binary controls. Both arms use the same full prebuilt archives and forced http/net/ws/zlib wrappers. GC counts come from separate `PERRY_GC_TRACE=1` runs; instruction/RSS runs have tracing disabled. Kernel elapsed-time fields and Effect's two elapsed-time fields are normalized; semantic output matches exactly. + +| Program | Instructions main → head | Delta / instruction noise (%) | RSS KiB main → head | Fulls main → head | Minors main → head | +|---|---|---|---|---|---| +| tsc | 9,971,062,650 → 9,971,526,952 | +0.00466 / 0.02131 | 218,628 → 217,104 | 0 → 0 | 3 → 3 | +| zod5k | 14,680,305,791 → 14,689,230,535 | +0.06079 / 0.06157 | 52,428 → 52,812 | 0 → 0 | 97 → 97 | +| qsparse | 23,501,408,052 → 23,499,678,046 | -0.00736 / 0.03111 | 57,688 → 57,644 | 0 → 0 | 131 → 131 | +| qsstr | 60,415,603,345 → 60,418,221,141 | +0.00433 / 0.04021 | 57,532 → 57,532 | 0 → 0 | 439 → 439 | +| commander | 7,562,974,564 → 7,562,572,144 | -0.00532 / 0.02436 | 53,032 → 52,908 | 0 → 0 | 32 → 32 | +| hello | 1,390,682 → 1,390,347 | -0.02409 / 0.03012 | 16,104 → 16,112 | 0 → 0 | 0 → 0 | +| fastify | 6,628,074,014 → 6,630,180,962 | +0.03179 / 0.25620 | 133,384 → 133,152 | 0 → 0 | 2 → 2 | +| effect | 24,903,168,156 → 24,909,199,781 | +0.02422 / 0.10273 | 191,596 → 190,816 | 2 → 2 | 17 → 17 | +| buffer_heavy | 10,673,094,122 → 10,670,258,459 | -0.02657 / 0.00600 | 94,016 → 94,012 | 36 → 36 | 0 → 0 | +| worker_heavy | 2,127,316,944 → 2,113,661,119 | -0.64193 / 1.30558 | 227,908 → 226,132 | 42 → 42 | 0 → 0 | +| matmul | 1,735,709,990 → 1,735,708,592 | -0.00008 / 0.00000 | 20,300 → 20,696 | 0 → 0 | 0 → 0 | +| prime_sieve | 25,201,751 → 25,201,070 | -0.00270 / 0.00031 | 18,164 → 18,176 | 0 → 0 | 0 → 0 | +| bench_buffer_readwrite | 101,604,457 → 101,603,876 | -0.00057 / 0.00002 | 18,164 → 18,172 | 0 → 0 | 0 → 0 | +| ecs_u32 | 681,978,773 → 681,978,340 | -0.00006 / 0.00000 | 18,140 → 17,692 | 0 → 0 | 0 → 0 | + +All real-program instruction changes are within their current control noise floor except buffer-heavy, which improves. Buffer-heavy uses uninitialized factories before copying native output, removing redundant zero writes; an AVX2 explanatory profile (Valgrind cannot decode the normal driver's AVX512 masked instruction) confirms fewer memset and finalizer instructions. The removed detached table no longer incurs a removal probe per finalized byte cell. Hello's refreshed final-binary profile confirms 420 fewer dynamic instructions in arena teardown after deleting the 64-entry admission-cache scan; all other Perry function counts are unchanged. The smaller total reduction includes ELF/libc startup-layout effects. The kernel instruction decreases are at most 0.00270%, consistent with the same startup/teardown removal; no kernel loop code is changed by this lane. + +Worker full-count trials are main [42, 42, 41, 42, 42], head [42, 41, 43, 40, 42]; both medians are 42. Concurrent transfer scheduling changes simultaneously live stores and pressure-triggered collection timing. Its RSS delta is inside the 12,856 KiB same-binary control variation, and its instruction delta is inside 1.30558% noise. Buffer-heavy retains exactly 36 full collections per arm. All full/minor medians match across the two arms. + +Small RSS movements are bounded changes in linked code/data residency and allocator page granularity, inferred from the companion mapping samples and unchanged collection regime. Zod's +384 KiB, commander’s -124 KiB, hello’s +8 KiB, fastify’s -232 KiB and the kernels’ +396/+12/+8/-448 KiB accompany only small anonymous working-set changes (0–16 KiB in those samples). File-resident shifts are +180/+4/+160/+76 KiB for Zod/commander/hello/fastify, and +72/+8/+8/-128 KiB for matmul/prime_sieve/buffer-readwrite/ECS. These companion snapshots are not taken at the primary run’s exact RSS peak, so they establish the page/layout mechanism without accounting for every primary KiB. No Buffer placement rule or pacing rule changed. tsc’s -1,524 KiB is within its 2,048 KiB RSS control variation and has a 2,036 KiB anonymous companion step; the fresh THP-off check below removes that RSS difference. + +Fresh current-main THP-off companion results (n=5 interleaved, `PR_SET_THP_DISABLE`, verified AnonHugePages=0 in every companion): + +| Program | RSS KiB main → head | RSS control noise KiB | Instruction delta / noise (%) | Fulls / minors main → head | +|---|---|---|---|---| +| tsc | 177,448 → 177,448 | 0 | -0.00091 / 0.01627 | 0/3 → 0/3 | +| qsstr | 29,440 → 29,456 | 0 | +0.01357 / 0.05293 | 0/439 → 0/439 | +| effect | 147,724 → 147,520 | 3,116 | +0.01711 / 0.32972 | 2/17 → 2/17 | + +With THP disabled, tsc's median RSS is identical across arms, consistent with the primary difference coming from huge-page granularity. qs-stringify's +16 KiB accompanies unchanged sampled anonymous RSS and +80 KiB file residency; this is bounded linked-image/page residency rather than increased byte-store allocation. Effect's -204 KiB is inside 3,116 KiB RSS control variation; its sampled anonymous/file deltas are -676/+120 KiB. All three THP-off instruction deltas are inside their control noise floors, and their full/minor collection medians remain unchanged. These companion snapshots retain the peak-accounting limitation above. + +The additional eight bytes per small Buffer have not been introduced by this compatible subset; their RSS effect is unmeasured and belongs to the unified-layout change. Persistent symbols gained eight bytes each. diff --git a/crates/perry-ext-http/src/server/http2_server/session.rs b/crates/perry-ext-http/src/server/http2_server/session.rs index 9560f86374..3553992493 100644 --- a/crates/perry-ext-http/src/server/http2_server/session.rs +++ b/crates/perry-ext-http/src/server/http2_server/session.rs @@ -303,16 +303,11 @@ unsafe fn pem_bytes(value: JsValue) -> Option> { if let Some(text) = jsvalue_to_owned_string(f64::from_bits(value.bits())) { return Some(text.into_bytes()); } - extern "C" { - fn js_value_buffer_or_typedarray_data(value: f64, out_len: *mut u32) -> *const u8; - } - let mut len = 0u32; - let data = js_value_buffer_or_typedarray_data(f64::from_bits(value.bits()), &mut len); - if data.is_null() || len == 0 { - None - } else { - Some(std::slice::from_raw_parts(data, len as usize).to_vec()) - } + perry_ffi::bytes::no_gc(|scope| { + perry_ffi::bytes::borrow(value, scope) + .filter(|bytes| !bytes.is_empty()) + .map(<[u8]>::to_vec) + }) } /// `(host, port, host:port)` for an `http2.connect` authority. diff --git a/crates/perry-ext-net/src/tls.rs b/crates/perry-ext-net/src/tls.rs index aa2811af1c..028af3556a 100644 --- a/crates/perry-ext-net/src/tls.rs +++ b/crates/perry-ext-net/src/tls.rs @@ -136,20 +136,7 @@ unsafe fn value_bytes(value: f64) -> Option> { if js.is_any_string() { return crate::jsvalue_to_socket_bytes(value); } - // Read through the canonical runtime registry. `perry-ext-net` is a - // separately linked archive, so perry-ffi's local Buffer registry cannot - // see Buffers allocated by the program runtime (notably `ca`, `cert`, and - // `key` values returned by fs.readFileSync). - extern "C" { - fn js_value_buffer_or_typedarray_data(value: f64, out_len: *mut u32) -> *const u8; - } - let mut len = 0u32; - let data = js_value_buffer_or_typedarray_data(value, &mut len); - if data.is_null() { - None - } else { - Some(std::slice::from_raw_parts(data, len as usize).to_vec()) - } + perry_ffi::bytes::no_gc(|scope| perry_ffi::bytes::borrow(js, scope).map(<[u8]>::to_vec)) } unsafe fn material_list(value: f64) -> Option>> { diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 621bcff4ad..95e610f093 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -699,15 +699,12 @@ pub(crate) fn buffer_receiver_as_uint8_typed_array( // Copy the bytes out BEFORE allocating: `typed_array_alloc` can collect, // and a raw payload pointer read across it is exactly the borrowed-heap- // slice shape rooting cannot fix. - let buf = addr as *const crate::buffer::BufferHeader; - let bytes: Vec = unsafe { - let len = (*buf).length as usize; - if len == 0 { - Vec::new() - } else { - std::slice::from_raw_parts(crate::buffer::buffer_data(buf), len).to_vec() - } - }; + let bytes = crate::buffer::bytes::no_gc(|scope| { + let value = crate::value::js_nanbox_pointer(addr as i64); + crate::buffer::bytes::bytes(value, scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }); let ta = crate::typedarray::typed_array_alloc(crate::typedarray::KIND_UINT8, bytes.len() as u32); for (i, byte) in bytes.iter().enumerate() { diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 8f03608c80..ff951ab6f1 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -896,19 +896,21 @@ pub extern "C" fn js_array_at(arr: *const ArrayHeader, index: f64) -> f64 { ); } if crate::buffer::is_registered_buffer(addr) { - let buf = addr as *const crate::buffer::BufferHeader; - unsafe { - let length = (*buf).length as i64; - let mut idx = index as i64; - if idx < 0 { - idx += length; - } - if idx < 0 || idx >= length { + return crate::buffer::bytes::no_gc(|scope| { + let Ok(data) = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(addr as i64), scope) + else { return f64::from_bits(crate::value::TAG_UNDEFINED); - } - let data = crate::buffer::buffer_data(buf as *const crate::buffer::BufferHeader); - return *data.add(idx as usize) as f64; - } + }; + let length = data.len() as i64; + let idx = if (index as i64) < 0 { + index as i64 + length + } else { + index as i64 + }; + data.get(idx as usize) + .map_or(f64::from_bits(crate::value::TAG_UNDEFINED), |n| *n as f64) + }); } unsafe { let length = (*arr).length as i64; diff --git a/crates/perry-runtime/src/atomics.rs b/crates/perry-runtime/src/atomics.rs index b2f47eeebd..d85be32910 100644 --- a/crates/perry-runtime/src/atomics.rs +++ b/crates/perry-runtime/src/atomics.rs @@ -161,25 +161,22 @@ impl AtomicView { /// table key that lets cross-thread `wait`/`notify` rendezvous (#4913). /// Returns 0 if the backing pointer can't be resolved. fn slot_addr(&self, index: i32) -> usize { - match self { - AtomicView::TypedArray { ptr, kind } => unsafe { - let base = crate::typedarray::typed_array_bytes(*ptr) - .map(|b| b.as_ptr() as usize) - .unwrap_or(0); - if base == 0 { - return 0; - } - base + (index.max(0) as usize) * atomic_elem_size(*kind) - }, - AtomicView::Uint8ArrayBuffer(ptr) => { - let base = - crate::buffer::buffer_data(*ptr as *const crate::buffer::BufferHeader) as usize; - if base == 0 { - return 0; - } - base + index.max(0) as usize - } - } + let (value, elem) = match self { + AtomicView::TypedArray { ptr, kind } => ( + crate::value::js_nanbox_pointer(*ptr as i64), + atomic_elem_size(*kind), + ), + AtomicView::Uint8ArrayBuffer(ptr) => (crate::value::js_nanbox_pointer(*ptr as i64), 1), + }; + // This address is a futex identity, not a borrowed byte reference. + // Wait/notify only admits process-lifetime shared stores. + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope) + .ok() + .map_or(0, |bytes| { + bytes.as_ptr() as usize + index.max(0) as usize * elem + }) + }) } fn get_bigint_bits(&self, index: i32) -> u64 { @@ -370,15 +367,16 @@ fn typed_array_bigint_bits(ta: *const TypedArrayHeader, index: i32) -> u64 { if index as u32 >= (*ta).length { return 0; } - let data = crate::typedarray::typed_array_bytes(ta).unwrap_or(&[]); - let off = (index as usize).saturating_mul((*ta).elem_size as usize); - let bytes = data.get(off..off + 8).unwrap_or(&[]); - if bytes.len() != 8 { - return 0; - } - u64::from_ne_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]) + crate::buffer::bytes::no_gc(|scope| { + let data = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(ta as i64), scope) + .unwrap_or(&[]); + let off = (index as usize).saturating_mul((*ta).elem_size as usize); + let Some(bytes) = data.get(off..off + 8) else { + return 0; + }; + u64::from_ne_bytes(bytes.try_into().unwrap()) + }) } } @@ -396,13 +394,17 @@ fn typed_array_set_bigint_bits(ta: *mut TypedArrayHeader, index: i32, value: u64 if index as u32 >= (*ta).length { return; } - let Some(data) = crate::typedarray::typed_array_bytes_mut(ta) else { - return; - }; - let off = (index as usize).saturating_mul((*ta).elem_size as usize); - if let Some(slot) = data.get_mut(off..off + 8) { - slot.copy_from_slice(&value.to_ne_bytes()); - } + crate::buffer::bytes::no_gc(|scope| { + let Ok(data) = + crate::buffer::bytes::bytes_mut(crate::value::js_nanbox_pointer(ta as i64), scope) + else { + return; + }; + let off = (index as usize).saturating_mul((*ta).elem_size as usize); + if let Some(slot) = data.get_mut(off..off + 8) { + slot.copy_from_slice(&value.to_ne_bytes()); + } + }); } } diff --git a/crates/perry-runtime/src/bigint/convert.rs b/crates/perry-runtime/src/bigint/convert.rs index d2236956b6..c86935b396 100644 --- a/crates/perry-runtime/src/bigint/convert.rs +++ b/crates/perry-runtime/src/bigint/convert.rs @@ -403,10 +403,13 @@ pub extern "C" fn js_bigint_to_buffer( let limbs = bigint_limbs_or_zero(a); let length = if length <= 0 { 32 } else { length as usize }; - let result = crate::buffer::buffer_alloc(length as u32); + let (value, pin) = crate::buffer::bytes::new_bytes( + crate::buffer::bytes::Brand::Buffer, + length, + crate::buffer::bytes::Init::Uninit, + ); unsafe { - (*result).length = length as u32; - let data = crate::buffer::buffer_data_mut(result); + let data = pin.as_mut_ptr(); // Extract bytes from the pre-allocation limb snapshot // (little-endian in memory) and write in big-endian order. @@ -418,7 +421,9 @@ pub extern "C" fn js_bigint_to_buffer( *data.add(length - 1 - i) = byte; } } - result + crate::value::JSValue::from_bits(value.to_bits()) + .as_pointer::() + .cast_mut() } /// Convert BigInt to f64 (may lose precision) diff --git a/crates/perry-runtime/src/buffer/backing.rs b/crates/perry-runtime/src/buffer/backing.rs index 2da4b49f53..abe892b3ab 100644 --- a/crates/perry-runtime/src/buffer/backing.rs +++ b/crates/perry-runtime/src/buffer/backing.rs @@ -91,6 +91,12 @@ impl TransferredBacking { }; Backing::copy(data, self.length) }); + #[cfg(test)] + let backing = if super::bytes::b4_sabotage("transfer_copy") { + Backing::copy(backing.data(), self.length) + } else { + backing + }; *self.backing.get_mut().unwrap() = Some(backing); } @@ -145,12 +151,12 @@ mod tests { let _guard = setup(); let before = count(); let scope = RuntimeHandleScope::new(); - let source = js_array_buffer_new(8 * 1024 * 1024); + let source = js_array_buffer_new(32 * 1024 * 1024); let source_root = scope.root_raw_mut_ptr(source); let original = buffer_data(source) as usize; unsafe { *(original as *mut u8) = 37; - *(original as *mut u8).add(8 * 1024 * 1024 - 1) = 91; + *(original as *mut u8).add(32 * 1024 * 1024 - 1) = 91; let message = serialize_message( JSValue::pointer(source.cast()).bits(), &[source as usize], @@ -176,12 +182,19 @@ mod tests { original, "transfer must move the original allocation" ); - assert_eq!((*received).length, 8 * 1024 * 1024); + assert_eq!((*received).length, 32 * 1024 * 1024); drop(message); collect(); let received = (root.get_nanbox_u64() & POINTER_MASK) as *const BufferHeader; assert_eq!(*buffer_data(received), 37); - assert_eq!(*buffer_data(received).add(8 * 1024 * 1024 - 1), 91); + crate::buffer::bytes::no_gc(|scope| { + let data = crate::buffer::bytes::bytes( + crate::value::js_nanbox_pointer(received as i64), + scope, + ) + .unwrap(); + assert_eq!(data.last(), Some(&91)); + }); }) .join() .unwrap(); diff --git a/crates/perry-runtime/src/buffer/bytes.rs b/crates/perry-runtime/src/buffer/bytes.rs index 1c39a223ed..0c23bb0a21 100644 --- a/crates/perry-runtime/src/buffer/bytes.rs +++ b/crates/perry-runtime/src/buffer/bytes.rs @@ -80,6 +80,11 @@ pub(crate) fn assert_allocation_allowed() { NO_GC_DEPTH.with(|n| assert_eq!(n.get(), 0, "allocation inside bytes::no_gc")); } +#[cfg(test)] +pub(crate) fn b4_sabotage(fault: &str) -> bool { + std::env::var("PERRY_B4_SABOTAGE").ok().as_deref() == Some(fault) +} + pub(crate) struct Span { pub ptr: *mut u8, pub len: usize, @@ -94,7 +99,13 @@ pub(crate) fn span(value: f64, writable: bool) -> Result { let addr = value.as_pointer::() as usize; let (ptr, len, owner) = if super::is_registered_buffer(addr) { let owner = super::view::backing_of(addr); - if super::is_detached_buffer(owner) || super::view::is_out_of_bounds_view(addr) { + #[cfg(test)] + let skip_owner_check = b4_sabotage("owner_check"); + #[cfg(not(test))] + let skip_owner_check = false; + if !skip_owner_check + && (super::is_detached_buffer(owner) || super::view::is_out_of_bounds_view(addr)) + { return Err(NotBytes::Detached); } let b = addr as *const super::BufferHeader; @@ -106,8 +117,13 @@ pub(crate) fn span(value: f64, writable: bool) -> Result { } else if crate::typedarray::lookup_typed_array_kind(addr).is_some() { let ta = addr as *const crate::typedarray::TypedArrayHeader; let owner = if let Some(meta) = crate::typedarray_view::view_meta_of(addr) { - if super::is_detached_buffer(meta.backing) - || crate::typedarray_view::is_view_out_of_bounds(addr) + #[cfg(test)] + let skip_owner_check = b4_sabotage("owner_check"); + #[cfg(not(test))] + let skip_owner_check = false; + if !skip_owner_check + && (super::is_detached_buffer(meta.backing) + || crate::typedarray_view::is_view_out_of_bounds(addr)) { return Err(NotBytes::Detached); } @@ -179,11 +195,12 @@ pub(crate) unsafe fn write_admitted_inline_byte(addr: usize, index: usize, byte: }); } -// Bits 9..14 are unused by current byte cells and NativeArena owners. Bit 15 +// Bits 9..13 hold up to 31 nested byte pins. Bit 14 is detached state for +// byte-family cells (unused by NativeArena owners). Bit 15 // preserves a pre-existing permanent GC pin. Nested byte pins share the owner; // no address registry or latch is introduced. Overflow is refused, never wraps. const PIN_ONE: u16 = 1 << 9; -const PIN_MASK: u16 = 0x7e00; +const PIN_MASK: u16 = 0x3e00; const WAS_PINNED: u16 = 0x8000; pub(crate) fn has_pins(owner: usize) -> bool { @@ -382,3 +399,18 @@ pub fn from_slice(brand: Brand, input: &[u8]) -> f64 { }); value } + +/// Copy a byte value into a new store. Root the input before allocating; +/// resolve its span after allocation so no derived pointer crosses GC. +pub fn copy_value(brand: Brand, input: f64) -> Result { + let handles = crate::gc::RuntimeHandleScope::new(); + let input = handles.root_nanbox_f64(input); + let len = no_gc(|scope| bytes(input.get_nanbox_f64(), scope).map(<[u8]>::len))?; + let (output, pin) = new_bytes(brand, len, Init::Uninit); + no_gc(|scope| { + let source = bytes(input.get_nanbox_f64(), scope)?; + unsafe { std::slice::from_raw_parts_mut(pin.as_mut_ptr(), pin.len()) } + .copy_from_slice(source); + Ok(output) + }) +} diff --git a/crates/perry-runtime/src/buffer/detach.rs b/crates/perry-runtime/src/buffer/detach.rs index aa1e7414bb..c449453810 100644 --- a/crates/perry-runtime/src/buffer/detach.rs +++ b/crates/perry-runtime/src/buffer/detach.rs @@ -16,38 +16,18 @@ //! zeros), so the only observable effect is RSS dropping. use super::*; -use crate::fast_hash::{new_ptr_hash_set, PtrHashSet}; -use std::cell::RefCell; +// Per-buffer header bit; disjoint from byte pins and the storage-layout bits. +pub(crate) const DETACHED: u16 = 1 << 14; -crate::perry_thread_local! { - /// Buffers detached via `transfer`/`transferToFixedLength`/structuredClone - /// transfer. A detached buffer also has `length == capacity == 0`, but that - /// alone cannot be the probe: `new ArrayBuffer(0)` is empty yet NOT - /// detached. - static DETACHED_BUFFER_REGISTRY: RefCell> = - RefCell::new(new_ptr_hash_set()); -} - -/// Monotone "an ArrayBuffer has been detached in this process" latch — nothing -/// detached ⟹ nothing to find. See `crate::registry_latch`. -static EVER_DETACHED: crate::registry_latch::RegistryLatch = - crate::registry_latch::RegistryLatch::new(); - -/// `ArrayBuffer.prototype.detached` — true after a successful transfer. +/// Detached state is born and dies with the store owner, never its address. #[inline] pub fn is_detached_buffer(addr: usize) -> bool { - if EVER_DETACHED.is_idle() { + if !super::is_registered_buffer(addr) { return false; } - DETACHED_BUFFER_REGISTRY.with(|r| r.borrow().contains(&addr)) -} - -/// Drop the detached mark when the buffer dies — a recycled address would -/// otherwise inherit detached-ness (the #6080 ABA class). -pub(crate) fn remove_detached_entry_for_dead_buffer(addr: usize) { - DETACHED_BUFFER_REGISTRY.with(|r| { - r.borrow_mut().remove(&addr); - }); + unsafe { + (*crate::gc::header_from_trusted_user_ptr(addr as *const u8))._reserved & DETACHED != 0 + } } /// DetachArrayBuffer(buffer): idempotent. @@ -62,13 +42,18 @@ pub fn detach_array_buffer(addr: usize) { (*buf).length = 0; (*buf).capacity = 0; } - // Arm before the insert — see `crate::registry_latch`. - EVER_DETACHED.arm(); - DETACHED_BUFFER_REGISTRY.with(|r| { - let mut r = r.borrow_mut(); - r.insert(addr); - r.insert(backing); - }); + #[cfg(test)] + let mark_detached = !super::bytes::b4_sabotage("detach_mark"); + #[cfg(not(test))] + let mark_detached = true; + if mark_detached { + unsafe { + (*crate::gc::header_from_trusted_user_ptr(backing as *const u8).cast_mut()) + ._reserved |= DETACHED; + (*crate::gc::header_from_trusted_user_ptr(addr as *const u8).cast_mut())._reserved |= + DETACHED; + } + } // Buffer-shaped views (`new Uint8Array(ab)`, DataView slices): zero their // own header lengths so `.length`/`.byteLength` report 0 and every indexed // access is out-of-bounds, matching Node's view-over-detached semantics. diff --git a/crates/perry-runtime/src/buffer/header.rs b/crates/perry-runtime/src/buffer/header.rs index ce75b6781e..a6071aaf24 100644 --- a/crates/perry-runtime/src/buffer/header.rs +++ b/crates/perry-runtime/src/buffer/header.rs @@ -1123,7 +1123,6 @@ pub(crate) fn finalize_collected_dead_buffer(addr: usize) { // `GC_TYPE_TYPED_ARRAY` cell, so that path never sees it (#9347). crate::typedarray_props::typed_array_clear_own_props(addr); crate::typedarray_props::typed_array_clear_no_extend(addr); - super::detach::remove_detached_entry_for_dead_buffer(addr); super::view::remove_entries_for_dead_buffer(addr); // #9342: drop the dead address from the inline-read admission cache before // its block can be reset and re-issued — a stale hit would read the next diff --git a/crates/perry-runtime/src/buffer/view.rs b/crates/perry-runtime/src/buffer/view.rs index c09eb4a0b7..1e1f73bfc8 100644 --- a/crates/perry-runtime/src/buffer/view.rs +++ b/crates/perry-runtime/src/buffer/view.rs @@ -309,6 +309,10 @@ fn register(view_ptr: usize, backing_ptr: usize, offset: u32) { /// Backings are old/non-moving; exposing a stable slot also makes the edge /// visible to the collector's rewrite and verification walks. pub(crate) fn visit_backing_slot(addr: usize, mut visit: impl FnMut(*mut u64)) { + #[cfg(test)] + if super::bytes::b4_sabotage("view_edge") { + return; + } let updated = VIEW_REGISTRY.with(|r| { r.borrow_mut().get_mut(&addr).and_then(|rec| { let previous = rec.info.backing; diff --git a/crates/perry-runtime/src/builtins/formatting.rs b/crates/perry-runtime/src/builtins/formatting.rs index 8eefd00a89..a1c7c23df3 100644 --- a/crates/perry-runtime/src/builtins/formatting.rs +++ b/crates/perry-runtime/src/builtins/formatting.rs @@ -885,42 +885,44 @@ unsafe fn format_buffer_value(buf_ptr: *const crate::buffer::BufferHeader) -> St if buf_ptr.is_null() { return "".to_string(); } - let len = (*buf_ptr).length as usize; - let data = crate::buffer::buffer_data(buf_ptr as *const crate::buffer::BufferHeader); - let bytes = std::slice::from_raw_parts(data, len); - - // If this buffer was created via `new Uint8Array(...)`, format it Node-style - // as `Uint8Array(N) [ a, b, c ]` rather than ``. - if crate::buffer::is_uint8array_buffer(buf_ptr as usize) { - if len == 0 { - return "Uint8Array(0) []".to_string(); - } - let mut out = format!("Uint8Array({}) [", len); - for (i, b) in bytes.iter().enumerate() { - if i == 0 { - out.push(' '); - } else { - out.push_str(", "); + crate::buffer::bytes::no_gc(|scope| { + let value = crate::value::js_nanbox_pointer(buf_ptr as i64); + let bytes = crate::buffer::bytes::bytes(value, scope).unwrap_or(&[]); + let len = bytes.len(); + + // If this buffer was created via `new Uint8Array(...)`, format it Node-style + // as `Uint8Array(N) [ a, b, c ]` rather than ``. + if crate::buffer::is_uint8array_buffer(buf_ptr as usize) { + if len == 0 { + return "Uint8Array(0) []".to_string(); + } + let mut out = format!("Uint8Array({}) [", len); + for (i, b) in bytes.iter().enumerate() { + if i == 0 { + out.push(' '); + } else { + out.push_str(", "); + } + out.push_str(&format!("{}", *b)); } - out.push_str(&format!("{}", *b)); + out.push_str(" ]"); + return out; } - out.push_str(" ]"); - return out; - } - // Node caps at 50 bytes then shows "... N more bytes" - let display_len = len.min(50); - let mut out = String::with_capacity(9 + display_len * 3); - out.push_str(" display_len { - out.push_str(&format!(" ... {} more bytes", len - display_len)); - } - out.push('>'); - out + // Node caps at 50 bytes then shows "... N more bytes" + let display_len = len.min(50); + let mut out = String::with_capacity(9 + display_len * 3); + out.push_str(" display_len { + out.push_str(&format!(" ... {} more bytes", len - display_len)); + } + out.push('>'); + out + }) } fn format_proxy_value(value: f64, depth: usize, json: bool) -> String { diff --git a/crates/perry-runtime/src/builtins/formatting/array_buffer.rs b/crates/perry-runtime/src/builtins/formatting/array_buffer.rs index 0f01467c96..ee0290e258 100644 --- a/crates/perry-runtime/src/builtins/formatting/array_buffer.rs +++ b/crates/perry-runtime/src/builtins/formatting/array_buffer.rs @@ -5,21 +5,23 @@ pub(super) unsafe fn format_array_buffer_value( if buf_ptr.is_null() { return format!("{label} {{ [Uint8Contents]: <>, [byteLength]: 0 }}"); } - let len = (*buf_ptr).length as usize; - let data = crate::buffer::buffer_data(buf_ptr as *const crate::buffer::BufferHeader); - let bytes = std::slice::from_raw_parts(data, len); - let display_len = len.min(50); - let mut contents = String::new(); - for (i, b) in bytes[..display_len].iter().enumerate() { - if i > 0 { - contents.push(' '); + crate::buffer::bytes::no_gc(|scope| { + let value = crate::value::js_nanbox_pointer(buf_ptr as i64); + let bytes = crate::buffer::bytes::bytes(value, scope).unwrap_or(&[]); + let len = bytes.len(); + let display_len = len.min(50); + let mut contents = String::new(); + for (i, b) in bytes[..display_len].iter().enumerate() { + if i > 0 { + contents.push(' '); + } + contents.push_str(&format!("{:02x}", b)); } - contents.push_str(&format!("{:02x}", b)); - } - if len > display_len { - contents.push_str(&format!(" ... {} more bytes", len - display_len)); - } - format!("{label} {{ [Uint8Contents]: <{contents}>, [byteLength]: {len} }}") + if len > display_len { + contents.push_str(&format!(" ... {} more bytes", len - display_len)); + } + format!("{label} {{ [Uint8Contents]: <{contents}>, [byteLength]: {len} }}") + }) } pub(super) unsafe fn format_data_view_value(buf_ptr: *const crate::buffer::BufferHeader) -> String { diff --git a/crates/perry-runtime/src/builtins/formatting/typed_array_equality.rs b/crates/perry-runtime/src/builtins/formatting/typed_array_equality.rs index f165ec59d9..573a83f88a 100644 --- a/crates/perry-runtime/src/builtins/formatting/typed_array_equality.rs +++ b/crates/perry-runtime/src/builtins/formatting/typed_array_equality.rs @@ -1,7 +1,6 @@ -struct TypedArrayView { +struct TypedArrayView<'s> { kind: u8, - data: *const u8, - byte_len: usize, + data: &'s [u8], } fn value_pointer_addr(value: f64) -> Option { @@ -18,47 +17,33 @@ fn value_pointer_addr(value: f64) -> Option { None } -fn typed_array_view(value: f64) -> Option { +fn typed_array_view<'s>( + value: f64, + scope: &'s crate::buffer::bytes::NoGc<'s>, +) -> Option> { let addr = value_pointer_addr(value)?; - if let Some(kind) = crate::typedarray::lookup_typed_array_kind(addr) { - let ta = addr as *const crate::typedarray::TypedArrayHeader; - let bytes = unsafe { crate::typedarray::typed_array_bytes(ta)? }; - return Some(TypedArrayView { - kind, - data: bytes.as_ptr(), - byte_len: bytes.len(), - }); - } - if crate::buffer::is_registered_buffer(addr) && crate::buffer::is_uint8array_buffer(addr) { - let buf = addr as *const crate::buffer::BufferHeader; - let byte_len = unsafe { (*buf).length as usize }; - return Some(TypedArrayView { - kind: crate::typedarray::KIND_UINT8, - data: crate::buffer::buffer_data(buf), - byte_len, - }); - } - None + let kind = if let Some(kind) = crate::typedarray::lookup_typed_array_kind(addr) { + kind + } else if crate::buffer::is_registered_buffer(addr) && crate::buffer::is_uint8array_buffer(addr) + { + crate::typedarray::KIND_UINT8 + } else { + return None; + }; + let data = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(addr as i64), scope).ok()?; + Some(TypedArrayView { kind, data }) } pub(super) fn deep_strict_typed_array_equal(left: f64, right: f64) -> Option { - let left_view = typed_array_view(left); - let right_view = typed_array_view(right); - match (left_view, right_view) { - (Some(left_view), Some(right_view)) => { - if left_view.kind != right_view.kind || left_view.byte_len != right_view.byte_len { - return Some(false); - } - if left_view.byte_len == 0 { - return Some(true); - } - unsafe { - let left_bytes = std::slice::from_raw_parts(left_view.data, left_view.byte_len); - let right_bytes = std::slice::from_raw_parts(right_view.data, right_view.byte_len); - Some(left_bytes == right_bytes) - } + crate::buffer::bytes::no_gc(|scope| { + match ( + typed_array_view(left, scope), + typed_array_view(right, scope), + ) { + (Some(left), Some(right)) => Some(left.kind == right.kind && left.data == right.data), + (Some(_), None) | (None, Some(_)) => Some(false), + (None, None) => None, } - (Some(_), None) | (None, Some(_)) => Some(false), - (None, None) => None, - } + }) } diff --git a/crates/perry-runtime/src/builtins/globals.rs b/crates/perry-runtime/src/builtins/globals.rs index 656c21a41f..c783759db9 100644 --- a/crates/perry-runtime/src/builtins/globals.rs +++ b/crates/perry-runtime/src/builtins/globals.rs @@ -613,19 +613,11 @@ fn clone_buffer_header(addr: usize, detach_source: bool) -> f64 { // merely marking an inline buffer as a DataView would make the numeric // setter interpret its first bytes as that cache pointer. if crate::buffer::is_data_view(addr) { - let backing = crate::buffer::buffer_alloc(src_len); - unsafe { - (*backing).length = src_len; - if src_len > 0 { - std::ptr::copy_nonoverlapping( - crate::buffer::buffer_data(src), - crate::buffer::buffer_data_mut(backing), - src_len as usize, - ); - } - } - crate::buffer::mark_as_array_buffer(backing as usize); - let backing_value = crate::value::js_nanbox_pointer(backing as i64); + let backing_value = crate::buffer::bytes::copy_value( + crate::buffer::bytes::Brand::ArrayBuffer, + crate::value::js_nanbox_pointer(addr as i64), + ) + .expect("live DataView bytes"); let cloned = crate::buffer::js_data_view_new(backing_value, 0.0, src_len as f64); if detach_source { let cloned_addr = pointer_addr(cloned).unwrap_or(0); @@ -634,17 +626,14 @@ fn clone_buffer_header(addr: usize, detach_source: bool) -> f64 { return cloned; } - let dst = crate::buffer::buffer_alloc(src_len); - unsafe { - (*dst).length = src_len; - if src_len > 0 { - std::ptr::copy_nonoverlapping( - crate::buffer::buffer_data(src), - crate::buffer::buffer_data_mut(dst), - src_len as usize, - ); - } - } + let value = crate::buffer::bytes::copy_value( + crate::buffer::bytes::Brand::Buffer, + crate::value::js_nanbox_pointer(addr as i64), + ) + .expect("live buffer bytes"); + let dst = JSValue::from_bits(value.to_bits()) + .as_pointer::() + .cast_mut(); let dst_addr = dst as usize; if crate::buffer::is_array_buffer(addr) { diff --git a/crates/perry-runtime/src/bun_compat/mod.rs b/crates/perry-runtime/src/bun_compat/mod.rs index 54badd1710..9ce7e4ab05 100644 --- a/crates/perry-runtime/src/bun_compat/mod.rs +++ b/crates/perry-runtime/src/bun_compat/mod.rs @@ -187,12 +187,12 @@ fn payload_bytes(value: f64) -> Result, f64> { if is_string_value(value) { return Ok(value_to_string(value).into_bytes()); } - let mut binary_len = 0u32; - let binary_ptr = unsafe { - crate::buffer::js_value_buffer_or_typedarray_data(value, &mut binary_len as *mut u32) - }; - if !binary_ptr.is_null() { - return Ok(unsafe { std::slice::from_raw_parts(binary_ptr, binary_len as usize) }.to_vec()); + if let Some(bytes) = crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) { + return Ok(bytes); } if heap_ptr_from_value(value).is_some() { if let Some(path_value) = object_field(value, BUN_FILE_PATH_KEY) { @@ -376,15 +376,7 @@ fn array_buffer_from_bytes(bytes: &[u8]) -> f64 { } fn uint8_array_from_bytes(bytes: &[u8]) -> f64 { - let buffer = crate::buffer::js_uint8array_alloc(bytes.len() as i32); - unsafe { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buffer), - bytes.len(), - ); - } - f64::from_bits(JSValue::pointer(buffer as *const u8).bits()) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Uint8Array, bytes) } /// `Bun.file(path)` — BunFile-like lazy handle. diff --git a/crates/perry-runtime/src/bun_ffi/call.rs b/crates/perry-runtime/src/bun_ffi/call.rs index eb8b2d003f..86cc773bf1 100644 --- a/crates/perry-runtime/src/bun_ffi/call.rs +++ b/crates/perry-runtime/src/bun_ffi/call.rs @@ -362,30 +362,11 @@ pub(crate) unsafe fn value_to_f64_num(v: f64) -> f64 { /// always targets the ultimate backing bytes (#6515) — see the module doc /// for the resulting read-back caveat on views. pub(crate) unsafe fn value_buffer_span(v: f64) -> Option<(*mut u8, usize)> { - let jv = JSValue::from_bits(v.to_bits()); - if !jv.is_pointer() { - return None; - } - let addr = crate::value::js_nanbox_get_pointer(f64::from_bits(jv.bits())) as usize; - if addr == 0 { - return None; - } - if crate::buffer::is_registered_buffer(addr) - || crate::buffer::is_any_array_buffer(addr) - || crate::buffer::is_data_view(addr) - || crate::buffer::is_uint8array_buffer(addr) - { - let buf = addr as *const crate::buffer::BufferHeader; - let data = crate::buffer::view::resolve_data_ptr(buf); - return Some((data as *mut u8, (*buf).length as usize)); - } - if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = - crate::typedarray::clean_ta_ptr(addr as *const crate::typedarray::TypedArrayHeader); - let bytes = crate::typedarray::typed_array_bytes(ta)?; - return Some((bytes.as_ptr() as *mut u8, bytes.len())); - } - None + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(v, scope) + .ok() + .map(|bytes| (bytes.as_ptr() as *mut u8, bytes.len())) + }) } fn describe_value_for_error(jv: JSValue) -> &'static str { diff --git a/crates/perry-runtime/src/bun_ffi/memory.rs b/crates/perry-runtime/src/bun_ffi/memory.rs index 029c02571e..c287cc2ed7 100644 --- a/crates/perry-runtime/src/bun_ffi/memory.rs +++ b/crates/perry-runtime/src/bun_ffi/memory.rs @@ -144,14 +144,15 @@ pub(crate) unsafe fn node_view_value( let copy_jv = JSValue::from_bits(copy_arg.to_bits()); let copy = copy_jv.is_undefined() || crate::value::js_is_truthy(copy_arg) != 0; let buffer = if copy { - let buffer = crate::buffer::buffer_alloc(length); - std::ptr::copy_nonoverlapping( - address as *const u8, - crate::buffer::buffer_data_mut(buffer), - length as usize, - ); - (*buffer).length = length; - buffer + JSValue::from_bits( + crate::buffer::bytes::from_slice( + crate::buffer::bytes::Brand::Buffer, + std::slice::from_raw_parts(address as *const u8, length as usize), + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut() } else { crate::buffer::buffer_alloc_foreign(address as *mut u8, length) }; diff --git a/crates/perry-runtime/src/child_process/v8_serde.rs b/crates/perry-runtime/src/child_process/v8_serde.rs index 9f2deeaac3..dee68e6bf2 100644 --- a/crates/perry-runtime/src/child_process/v8_serde.rs +++ b/crates/perry-runtime/src/child_process/v8_serde.rs @@ -412,9 +412,11 @@ impl Serializer { let mut le: Vec = if be_buf.is_null() { Vec::new() } else { - let data = crate::buffer::buffer_data(be_buf); - let len = unsafe { (*be_buf).length } as usize; - let mut v = unsafe { std::slice::from_raw_parts(data, len) }.to_vec(); + let mut v = crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(be_buf as i64), scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }); v.reverse(); v }; @@ -444,32 +446,28 @@ impl Serializer { } fn write_host_buffer(&mut self, value: f64) { - let data = crate::buffer::js_native_buffer_data_ptr(value); - let len = crate::buffer::js_native_buffer_byte_len(value); - let bytes: &[u8] = if data.is_null() || len == 0 { - &[] - } else { - unsafe { std::slice::from_raw_parts(data, len) } - }; - self.write_host_view(NODE_BUFFER_VIEW_INDEX, bytes); + crate::buffer::bytes::no_gc(|scope| { + let bytes = crate::buffer::bytes::bytes(value, scope).unwrap_or(&[]); + self.write_host_view(NODE_BUFFER_VIEW_INDEX, bytes); + }); } fn write_array_buffer(&mut self, buffer: *const crate::buffer::BufferHeader) { - let len = unsafe { (*buffer).length as usize }; - let data = crate::buffer::buffer_data(buffer); - self.out.push(TAG_ARRAY_BUFFER); - self.write_varint(len as u64); - if !data.is_null() && len != 0 { - self.out - .extend_from_slice(unsafe { std::slice::from_raw_parts(data, len) }); - } + crate::buffer::bytes::no_gc(|scope| { + let bytes = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buffer as i64), scope) + .unwrap_or(&[]); + self.out.push(TAG_ARRAY_BUFFER); + self.write_varint(bytes.len() as u64); + self.out.extend_from_slice(bytes); + }); } fn write_host_typed_array(&mut self, value: f64, kind: u8) { - let raw = (value.to_bits() & crate::value::POINTER_MASK) as usize; - let ta = raw as *const crate::typedarray::TypedArrayHeader; - let bytes = unsafe { crate::typedarray::typed_array_bytes(ta) }.unwrap_or(&[]); - self.write_host_view(v8_index_for_kind(kind), bytes); + crate::buffer::bytes::no_gc(|scope| { + let bytes = crate::buffer::bytes::bytes(value, scope).unwrap_or(&[]); + self.write_host_view(v8_index_for_kind(kind), bytes); + }); } fn write_dense_array(&mut self, arr: *mut crate::array::ArrayHeader) { @@ -999,15 +997,7 @@ impl<'a> Deserializer<'a> { fn read_array_buffer(&mut self) -> Option { let len = self.read_varint()? as usize; let bytes = self.read_raw(len)?; - let buffer = crate::buffer::js_array_buffer_new(len as i32); - if buffer.is_null() { - return Some(cp_undefined()); - } - let data = crate::buffer::buffer_data_mut(buffer); - if !data.is_null() && len != 0 { - unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, len) }; - } - let v = cp_box_ptr(buffer as *const u8); + let v = crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::ArrayBuffer, bytes); self.id_table.push(v); Some(v) } @@ -1268,9 +1258,13 @@ fn make_typed_array(kind: u8, bytes: &[u8]) -> f64 { if ta.is_null() { return cp_undefined(); } - if let Some(dst) = unsafe { crate::typedarray::typed_array_bytes_mut(ta) } { - let n = dst.len().min(bytes.len()); - dst[..n].copy_from_slice(&bytes[..n]); - } + crate::buffer::bytes::no_gc(|scope| unsafe { + if let Ok(dst) = + crate::buffer::bytes::bytes_mut(crate::value::js_nanbox_pointer(ta as i64), scope) + { + let n = dst.len().min(bytes.len()); + dst[..n].copy_from_slice(&bytes[..n]); + } + }); cp_box_ptr(ta as *const u8) } diff --git a/crates/perry-runtime/src/child_process/value_util.rs b/crates/perry-runtime/src/child_process/value_util.rs index f0558fd4bb..63de88c14a 100644 --- a/crates/perry-runtime/src/child_process/value_util.rs +++ b/crates/perry-runtime/src/child_process/value_util.rs @@ -122,22 +122,12 @@ pub(crate) fn cp_value_to_bytes(value: f64) -> Vec { if JSValue::from_bits(bits).is_pointer() { let raw = (bits & crate::value::POINTER_MASK) as usize; if raw >= 0x10000 { - if crate::buffer::is_registered_buffer(raw) { - let buf = raw as *const crate::buffer::BufferHeader; - unsafe { - let len = (*buf).length as usize; - let data = - crate::buffer::buffer_data(buf as *const crate::buffer::BufferHeader); - return std::slice::from_raw_parts(data, len).to_vec(); - } - } - if crate::typedarray::lookup_typed_array_kind(raw).is_some() { - let ta = raw as *const crate::typedarray::TypedArrayHeader; - unsafe { - if let Some(bytes) = crate::typedarray::typed_array_bytes(ta) { - return bytes.to_vec(); - } - } + if let Some(bytes) = crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) { + return bytes; } } } diff --git a/crates/perry-runtime/src/dgram/net.rs b/crates/perry-runtime/src/dgram/net.rs index 0940cbdfc3..056de2af93 100644 --- a/crates/perry-runtime/src/dgram/net.rs +++ b/crates/perry-runtime/src/dgram/net.rs @@ -183,19 +183,15 @@ fn message_bytes_inner(value: f64, allow_list: bool) -> Option> { return Some(text.into_bytes()); } let raw = raw_ptr_from_value(value); - if raw >= 0x10000 && crate::buffer::is_registered_buffer(raw) { - let buf = raw as *const crate::buffer::BufferHeader; - unsafe { - let len = (*buf).length as usize; - let data = crate::buffer::buffer_data(raw as *const crate::buffer::BufferHeader); - return Some(std::slice::from_raw_parts(data, len).to_vec()); - } - } - if raw >= 0x10000 && crate::typedarray::lookup_typed_array_kind(raw).is_some() { - return unsafe { - crate::typedarray::typed_array_bytes(raw as *const crate::typedarray::TypedArrayHeader) + if raw >= 0x10000 + && (crate::buffer::is_registered_buffer(raw) + || crate::typedarray::lookup_typed_array_kind(raw).is_some()) + { + return crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(raw as i64), scope) + .ok() .map(<[u8]>::to_vec) - }; + }); } if crate::array::js_array_is_array(value).to_bits() == crate::value::TAG_TRUE { // Node accepts one top-level buffer list, but each list element must diff --git a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs index 9cb471e111..94065f55b1 100644 --- a/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs +++ b/crates/perry-runtime/src/fs/dir_glob_watch/watch.rs @@ -382,17 +382,7 @@ fn with_watcher_uncaught_trap(f: F) { fn filename_arg_value(filename: &str, encoding: &str) -> f64 { let bytes = filename.as_bytes(); if encoding == "buffer" { - let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); - if !buf.is_null() && !bytes.is_empty() { - unsafe { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buf), - bytes.len(), - ); - } - } - boxed_ptr(buf as *const u8) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Buffer, bytes) } else { let ptr = encoded_string_ptr(bytes, encoding); f64::from_bits(crate::value::JSValue::string_ptr(ptr).bits()) diff --git a/crates/perry-runtime/src/fs/dirent.rs b/crates/perry-runtime/src/fs/dirent.rs index a7bcf7c4d8..6499ed268a 100644 --- a/crates/perry-runtime/src/fs/dirent.rs +++ b/crates/perry-runtime/src/fs/dirent.rs @@ -291,15 +291,7 @@ pub(crate) unsafe fn options_string_field(options_value: f64, field: &[u8]) -> O } pub(crate) fn buffer_value_from_bytes(bytes: &[u8]) -> f64 { - let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); - if !buf.is_null() { - unsafe { - let data = crate::buffer::buffer_data_mut(buf); - std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len()); - (*buf).length = bytes.len() as u32; - } - } - f64::from_bits(crate::value::JSValue::pointer(buf as *const u8).bits()) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Buffer, bytes) } pub(crate) fn bytes_to_readdir_value(bytes: &[u8], as_buffer: bool) -> f64 { diff --git a/crates/perry-runtime/src/fs/fd_ops.rs b/crates/perry-runtime/src/fs/fd_ops.rs index ea33643c8d..d01d5e727d 100644 --- a/crates/perry-runtime/src/fs/fd_ops.rs +++ b/crates/perry-runtime/src/fs/fd_ops.rs @@ -267,34 +267,42 @@ pub(crate) fn read_sync_result( if buf.is_null() { return Ok(0.0); } - FD_REGISTRY.with(|r| { - let mut reg = r.borrow_mut(); - let Some(file) = reg.get_mut(&fd) else { - return Ok(0.0); - }; - let restore_pos = position.and_then(|_| file.stream_position().ok()); - if let Some(pos) = position { - let _ = file.seek(SeekFrom::Start(pos)); - } - unsafe { - let cap = (*buf).length as usize; - if offset >= cap { - if let Some(pos) = restore_pos { - let _ = file.seek(SeekFrom::Start(pos)); - } + crate::buffer::bytes::no_gc(|scope| { + FD_REGISTRY.with(|r| { + let mut reg = r.borrow_mut(); + let Some(file) = reg.get_mut(&fd) else { return Ok(0.0); - } - let n = length.min(cap - offset); - let data = crate::buffer::buffer_data_mut(buf).add(offset); - let result = match file.read(std::slice::from_raw_parts_mut(data, n)) { - Ok(read) => Ok(read as f64), - Err(err) => Err(err), }; - if let Some(pos) = restore_pos { + let restore_pos = position.and_then(|_| file.stream_position().ok()); + if let Some(pos) = position { let _ = file.seek(SeekFrom::Start(pos)); } - result - } + unsafe { + let Ok(bytes) = crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(buf as i64), + scope, + ) else { + return Ok(0.0); + }; + let cap = bytes.len(); + if offset >= cap { + if let Some(pos) = restore_pos { + let _ = file.seek(SeekFrom::Start(pos)); + } + return Ok(0.0); + } + let n = length.min(cap - offset); + let data = bytes.as_mut_ptr().add(offset); + let result = match file.read(std::slice::from_raw_parts_mut(data, n)) { + Ok(read) => Ok(read as f64), + Err(err) => Err(err), + }; + if let Some(pos) = restore_pos { + let _ = file.seek(SeekFrom::Start(pos)); + } + result + } + }) }) } @@ -426,33 +434,40 @@ pub(crate) fn write_buffer_sync_result( if buf.is_null() { return Ok(0.0); } - FD_REGISTRY.with(|r| { - let mut reg = r.borrow_mut(); - let Some(file) = reg.get_mut(&fd) else { - return Ok(0.0); - }; - let restore_pos = position.and_then(|_| file.stream_position().ok()); - if let Some(pos) = position { - let _ = file.seek(SeekFrom::Start(pos)); - } - unsafe { - let cap = (*buf).length as usize; - if offset >= cap { + crate::buffer::bytes::no_gc(|scope| { + FD_REGISTRY.with(|r| { + let mut reg = r.borrow_mut(); + let Some(file) = reg.get_mut(&fd) else { + return Ok(0.0); + }; + let restore_pos = position.and_then(|_| file.stream_position().ok()); + if let Some(pos) = position { + let _ = file.seek(SeekFrom::Start(pos)); + } + unsafe { + let Ok(bytes) = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf as i64), scope) + else { + return Ok(0.0); + }; + let cap = bytes.len(); + if offset >= cap { + if let Some(pos) = restore_pos { + let _ = file.seek(SeekFrom::Start(pos)); + } + return Ok(0.0); + } + let n = length.min(cap - offset); + let data = bytes.as_ptr().add(offset); + let result = file + .write(std::slice::from_raw_parts(data, n)) + .map(|written| written as f64); if let Some(pos) = restore_pos { let _ = file.seek(SeekFrom::Start(pos)); } - return Ok(0.0); - } - let n = length.min(cap - offset); - let data = crate::buffer::buffer_data(buf).add(offset); - let result = file - .write(std::slice::from_raw_parts(data, n)) - .map(|written| written as f64); - if let Some(pos) = restore_pos { - let _ = file.seek(SeekFrom::Start(pos)); + result } - result - } + }) }) } @@ -564,6 +579,8 @@ pub extern "C" fn js_fs_readv_sync(fd_value: f64, buffers_value: f64, position_v if buffers.is_null() { return 0.0; } + let handles = crate::gc::RuntimeHandleScope::new(); + let buffers = handles.root_raw_const_ptr(buffers); FD_REGISTRY.with(|r| { let mut reg = r.borrow_mut(); let Some(file) = reg.get_mut(&fd) else { @@ -575,38 +592,47 @@ pub extern "C" fn js_fs_readv_sync(fd_value: f64, buffers_value: f64, position_v } let mut total = 0usize; unsafe { - let len = crate::array::js_array_length(buffers); + let len = crate::array::js_array_length(buffers.get_raw_const_ptr()); for i in 0..len { - let value = crate::array::js_array_get_f64(buffers, i); + let value = crate::array::js_array_get_f64(buffers.get_raw_const_ptr(), i); let buf = buffer_ptr_from_value(value); if buf.is_null() { continue; } - let cap = (*buf).length as usize; - if cap == 0 { - continue; - } - let data = crate::buffer::buffer_data_mut(buf); - // Node's readv fills each iovec completely (short read only - // at EOF). Use `read` in a loop so we don't return partially - // filled buffers when the kernel splits the read. - let mut filled = 0usize; - let mut eof = false; - while filled < cap { - let slice = std::slice::from_raw_parts_mut(data.add(filled), cap - filled); - match file.read(slice) { - Ok(0) => { - eof = true; - break; - } - Ok(n) => filled += n, - Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, - Err(_) => { - eof = true; - break; + let (filled, eof) = crate::buffer::bytes::no_gc(|scope| { + let Ok(bytes) = crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(buf as i64), + scope, + ) else { + return (0, false); + }; + let cap = bytes.len(); + if cap == 0 { + return (0, false); + } + let data = bytes.as_mut_ptr(); + // Node's readv fills each iovec completely (short read only + // at EOF). Use `read` in a loop so we don't return partially + // filled buffers when the kernel splits the read. + let mut filled = 0usize; + let mut eof = false; + while filled < cap { + let slice = std::slice::from_raw_parts_mut(data.add(filled), cap - filled); + match file.read(slice) { + Ok(0) => { + eof = true; + break; + } + Ok(n) => filled += n, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(_) => { + eof = true; + break; + } } } - } + (filled, eof) + }); total += filled; if eof { break; @@ -649,6 +675,8 @@ pub(crate) fn writev_sync_inner(fd: i32, buffers_value: f64, position_value: f64 if buffers.is_null() { return 0.0; } + let handles = crate::gc::RuntimeHandleScope::new(); + let buffers = handles.root_raw_const_ptr(buffers); FD_REGISTRY.with(|r| { let mut reg = r.borrow_mut(); let Some(file) = reg.get_mut(&fd) else { @@ -659,26 +687,24 @@ pub(crate) fn writev_sync_inner(fd: i32, buffers_value: f64, position_value: f64 let _ = file.seek(SeekFrom::Start(pos)); } let mut total = 0usize; - unsafe { - let len = crate::array::js_array_length(buffers); - for i in 0..len { - let value = crate::array::js_array_get_f64(buffers, i); - let buf = buffer_ptr_from_value(value); - if buf.is_null() { - continue; - } - let cap = (*buf).length as usize; - if cap == 0 { - continue; - } - let data = crate::buffer::buffer_data(buf); - // Node guarantees each iovec is fully written before the - // next; use `write_all` semantics to match. - let slice = std::slice::from_raw_parts(data, cap); - if file.write_all(slice).is_err() { - break; - } - total += cap; + let len = crate::array::js_array_length(buffers.get_raw_const_ptr()); + for i in 0..len { + let value = crate::array::js_array_get_f64(buffers.get_raw_const_ptr(), i); + let buf = buffer_ptr_from_value(value); + if buf.is_null() { + continue; + } + let result = crate::buffer::bytes::no_gc(|scope| { + let Ok(bytes) = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf as i64), scope) + else { + return Ok(0); + }; + file.write_all(bytes).map(|()| bytes.len()) + }); + match result { + Ok(count) => total += count, + Err(_) => break, } } if let Some(pos) = restore_pos { diff --git a/crates/perry-runtime/src/fs/filehandle.rs b/crates/perry-runtime/src/fs/filehandle.rs index b1d892b50d..abd06d0deb 100644 --- a/crates/perry-runtime/src/fs/filehandle.rs +++ b/crates/perry-runtime/src/fs/filehandle.rs @@ -387,19 +387,7 @@ fn webstream_auto_close(closure: *const ClosureHeader) -> bool { } fn allocate_uint8array_chunk(bytes: &[u8]) -> f64 { - let buf = crate::buffer::buffer_alloc(bytes.len() as u32); - crate::buffer::mark_as_uint8array(buf as usize); - unsafe { - (*buf).length = bytes.len() as u32; - if !bytes.is_empty() { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buf), - bytes.len(), - ); - } - } - f64::from_bits(crate::value::JSValue::pointer(buf as *const u8).bits()) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Uint8Array, bytes) } fn read_filehandle_webstream_chunk(fd: i32) -> Option> { @@ -1274,19 +1262,9 @@ pub(crate) extern "C" fn filehandle_read_file_impl( return promise_rejected_fs(unsafe { build_fs_error_value_no_path(&err, "read") }); } if read_file_encoding(encoding).is_none() { - let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); - if !buf.is_null() { - unsafe { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buf), - bytes.len(), - ); - (*buf).length = bytes.len() as u32; - } - } - promise_value_fs(f64::from_bits( - crate::value::JSValue::pointer(buf as *const u8).bits(), + promise_value_fs(crate::buffer::bytes::from_slice( + crate::buffer::bytes::Brand::Buffer, + &bytes, )) } else { let s = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); @@ -1353,8 +1331,12 @@ pub(crate) extern "C" fn filehandle_read_impl( let actual_buffer = options_field_value(buffer, b"buffer") .map(|v| f64::from_bits(v.bits())) .unwrap_or_else(|| { - let buf = crate::buffer::js_buffer_alloc(16 * 1024, 0); - f64::from_bits(crate::value::JSValue::pointer(buf as *const u8).bits()) + crate::buffer::bytes::new_bytes( + crate::buffer::bytes::Brand::Buffer, + 16 * 1024, + crate::buffer::bytes::Init::Zero, + ) + .0 }); let buffer_len = buffer_len_from_value(actual_buffer) as f64; let actual_offset = options_number_field(buffer, b"offset").unwrap_or(0.0); diff --git a/crates/perry-runtime/src/fs/mod.rs b/crates/perry-runtime/src/fs/mod.rs index 2a7aebcd5a..8aea4615a5 100644 --- a/crates/perry-runtime/src/fs/mod.rs +++ b/crates/perry-runtime/src/fs/mod.rs @@ -1237,11 +1237,12 @@ pub(crate) unsafe fn decode_path_value_named(path_value: f64, arg_name: &str) -> if buf.is_null() { return None; } - let bytes = - std::slice::from_raw_parts(crate::buffer::buffer_data(buf), (*buf).length as usize); - return std::str::from_utf8(bytes) - .ok() - .map(|s| reject_null_bytes(s.to_string(), arg_name)); + let path = crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf as i64), scope) + .ok() + .and_then(|bytes| std::str::from_utf8(bytes).ok().map(str::to_owned)) + }); + return path.map(|s| reject_null_bytes(s, arg_name)); } if jsval.is_pointer() { let obj = jsval.as_pointer::(); diff --git a/crates/perry-runtime/src/fs/stream.rs b/crates/perry-runtime/src/fs/stream.rs index 27533a2f2b..9680b5c5be 100644 --- a/crates/perry-runtime/src/fs/stream.rs +++ b/crates/perry-runtime/src/fs/stream.rs @@ -259,22 +259,15 @@ pub(crate) fn path_from_value(v: f64) -> String { /// BufferHeader values. pub(crate) fn bytes_from_value(v: f64) -> Vec { unsafe { - if crate::buffer::js_buffer_is_buffer(v.to_bits() as i64) == 1 { - let buf = buffer_ptr_from_value(v); - if !buf.is_null() { - let len = (*buf).length as usize; - let data = crate::buffer::buffer_data(buf); - return std::slice::from_raw_parts(data, len).to_vec(); - } - } - // #10694: the brand probes read the cell's header, so only a POINTER - // payload or an allocator-owned raw word is an address here. let addr = crate::value::addr_class::object_ref_addr(v); - if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *const crate::typedarray::TypedArrayHeader; - if let Some(bytes) = crate::typedarray::typed_array_bytes(ta) { - return bytes.to_vec(); - } + if crate::buffer::is_registered_buffer(addr) + || crate::typedarray::lookup_typed_array_kind(addr).is_some() + { + return crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(addr as i64), scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }); } // Both string representations; empty for anything that is not a // string (`extract_string_ptr` is heap-`STRING_TAG` only, #8122). @@ -322,15 +315,15 @@ fn encoding_tag_from_options(options_value: f64) -> i32 { } fn bytes_from_buffer_value(value: f64) -> Vec { - unsafe { - let buf = buffer_ptr_from_value(value); - if buf.is_null() { - return Vec::new(); - } - let len = (*buf).length as usize; - let data = crate::buffer::buffer_data(buf); - std::slice::from_raw_parts(data, len).to_vec() + let buf = buffer_ptr_from_value(value); + if buf.is_null() { + return Vec::new(); } + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf as i64), scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }) } fn bytes_from_string_value(value: f64, encoding_tag: i32) -> Vec { @@ -338,11 +331,7 @@ fn bytes_from_string_value(value: f64, encoding_tag: i32) -> Vec { if buf.is_null() { return Vec::new(); } - unsafe { - let len = (*buf).length as usize; - let data = crate::buffer::buffer_data(buf); - std::slice::from_raw_parts(data, len).to_vec() - } + bytes_from_buffer_value(crate::value::js_nanbox_pointer(buf as i64)) } mod write_file_input; diff --git a/crates/perry-runtime/src/fs/stream/write_file_input.rs b/crates/perry-runtime/src/fs/stream/write_file_input.rs index ecc38ca11e..3c92e4b46f 100644 --- a/crates/perry-runtime/src/fs/stream/write_file_input.rs +++ b/crates/perry-runtime/src/fs/stream/write_file_input.rs @@ -68,21 +68,20 @@ fn write_file_chunk_bytes(value: f64, encoding_tag: i32) -> Result, f64> // payload or an allocator-owned raw word is an address here. let addr = crate::value::addr_class::object_ref_addr(value); if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *const crate::typedarray::TypedArrayHeader; - if let Some(bytes) = unsafe { crate::typedarray::typed_array_bytes(ta) } { - return Ok(bytes.to_vec()); - } - return Ok(Vec::new()); + return Ok(crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(addr as i64), scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + })); } if crate::array::js_array_is_array(value).to_bits() == crate::value::TAG_TRUE { let buf = crate::buffer::js_buffer_from_value(value.to_bits() as i64, encoding_tag); if buf.is_null() { return Ok(Vec::new()); } - return Ok(unsafe { - std::slice::from_raw_parts(crate::buffer::buffer_data(buf), (*buf).length as usize) - .to_vec() - }); + return Ok(bytes_from_buffer_value(crate::value::js_nanbox_pointer( + buf as i64, + ))); } Err(write_file_data_type_error(value)) } diff --git a/crates/perry-runtime/src/gc/census.rs b/crates/perry-runtime/src/gc/census.rs index 6f68ab6911..8bdc6ce3b0 100644 --- a/crates/perry-runtime/src/gc/census.rs +++ b/crates/perry-runtime/src/gc/census.rs @@ -341,6 +341,26 @@ struct ClassAcc { unshaped: u64, } +// Rust's array Default impl stops at 32 entries. Keep per-kind census counts +// sized by the type inventory, including the persistent-symbol leaf kind. +struct TypeCounts([Acc; GC_TYPE_MAX as usize + 1]); +impl Default for TypeCounts { + fn default() -> Self { + Self(std::array::from_fn(|_| Acc::default())) + } +} +impl std::ops::Deref for TypeCounts { + type Target = [Acc; GC_TYPE_MAX as usize + 1]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for TypeCounts { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + #[derive(Default)] struct Census { // per space: 0..5 arenas (walk order), 5 = malloc @@ -350,9 +370,9 @@ struct Census { space_stub_live: [Acc; 6], space_stub_dead: [Acc; 6], space_late: [Acc; 6], - type_live: [Acc; GC_TYPE_MAX as usize + 1], - type_dead: [Acc; GC_TYPE_MAX as usize + 1], - type_late: [Acc; GC_TYPE_MAX as usize + 1], + type_live: TypeCounts, + type_dead: TypeCounts, + type_late: TypeCounts, /// Pass-1 reachable set (sorted header addresses); `None` = trust marks. pass1: Option>, size_hist: [Acc; 21], diff --git a/crates/perry-runtime/src/gc/tests/buffer_b4.rs b/crates/perry-runtime/src/gc/tests/buffer_b4.rs new file mode 100644 index 0000000000..d667912cbd --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/buffer_b4.rs @@ -0,0 +1,219 @@ +//! Compatible B4 contracts while B4c still owns the emitted layout switch. +use super::super::*; +use super::support::*; +use crate::buffer::{ + self, + bytes::{self, Brand, Init}, +}; +use crate::value::JSValue; + +fn bits(p: *const T) -> f64 { + crate::value::js_nanbox_pointer(p as i64) +} +fn fault(name: &str) -> bool { + std::env::var("PERRY_B4_SABOTAGE").ok().as_deref() == Some(name) +} + +#[test] +fn views_observe_owner_resize_and_detach_after_a_live_collection() { + let _guard = CopyingNurseryTestGuard::new(1); + let _force = ForcedEvacuationTestGuard::on(); + gc_register_named_mutable_root_scanner("pinned", crate::gc::pin::scan_pinned_object_roots_mut); + let options = crate::object::js_object_alloc(0, 1); + let key = crate::string::js_string_from_bytes(b"maxByteLength".as_ptr(), 13); + crate::object::js_object_set_field_by_name(options, key, 16.0); + let ab = buffer::js_array_buffer_new_with_options(8.0, bits(options)); + let u8 = buffer::js_buffer_slice(ab, 0, 8); + let i32view = crate::typedarray_view::js_typed_array_view( + crate::typedarray::KIND_INT32 as i32, + bits(ab), + 0.0, + 2.0, + ); + let dv = buffer::js_data_view_new(bits(ab), 0.0, 8.0); + let values = [bits(u8), bits(i32view), dv]; + for (i, value) in values.into_iter().enumerate() { + bytes::no_gc(|scope| unsafe { + bytes::bytes_mut(value, scope).unwrap()[i] = (41 + i) as u8; + }); + } + let pin = bytes::pin(bits(ab)).unwrap(); + let young = young_leaf(); + js_shadow_slot_set(0, string_bits(young)); + let before = gc_total_collection_count(); + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); + assert!( + gc_total_collection_count() > before, + "collection must run while bytes are held" + ); + for value in values { + bytes::no_gc(|scope| assert_eq!(&bytes::bytes(value, scope).unwrap()[..3], &[41, 42, 43])); + } + buffer::array_buffer_resize(ab as usize, &[4.0]); + for value in values { + assert!( + bytes::no_gc(|scope| bytes::bytes(value, scope).is_err()), + "fixed view must check its resized owner" + ); + } + buffer::array_buffer_resize(ab as usize, &[8.0]); + for value in values { + bytes::no_gc(|scope| { + let data = bytes::bytes(value, scope).unwrap(); + assert_eq!(&data[..3], &[41, 42, 43]); + assert_eq!(&data[4..], &[0; 4]); + }); + } + buffer::detach_array_buffer(ab as usize); + assert!(buffer::is_detached_buffer(ab as usize)); + for value in values { + assert!(bytes::no_gc(|scope| bytes::bytes(value, scope).is_err())); + } + drop(pin); +} + +#[test] +fn detached_bit_and_nested_pin_count_do_not_overlap() { + let _guard = CopyingNurseryTestGuard::new(0); + let value = bytes::from_slice(Brand::ArrayBuffer, &[0x25; 1024]); + let owner = JSValue::from_bits(value.to_bits()).as_pointer::() as usize; + let pins: Vec<_> = (0..31).map(|_| bytes::pin(value).unwrap()).collect(); + assert!(!buffer::is_detached_buffer(owner)); + assert!(matches!(bytes::pin(value), Err(bytes::NotBytes::PinLimit))); + buffer::detach_array_buffer(owner); + assert!(buffer::is_detached_buffer(owner)); + for pin in &pins { + unsafe { + assert_eq!(*pin.as_ptr(), 0x25); + } + } + drop(pins); + assert!(buffer::is_detached_buffer(owner)); +} + +#[test] +fn large_concat_and_nested_views_preserve_one_visible_window() { + let _guard = CopyingNurseryTestGuard::new(1); + let _force = ForcedEvacuationTestGuard::on(); + let (nested, owner) = { + let handles = RuntimeHandleScope::new(); + let array = handles.root_raw_mut_ptr(crate::array::js_array_alloc(300)); + let (part, pin) = bytes::new_bytes(Brand::Buffer, 18_000, Init::Uninit); + unsafe { + std::ptr::write_bytes(pin.as_mut_ptr(), 0x25, pin.len()); + } + for _ in 0..300 { + crate::array::js_array_push_f64(array.get_raw_mut_ptr(), part); + } + let concat = buffer::js_buffer_concat(array.get_raw_mut_ptr()); + let concat = handles.root_raw_mut_ptr(concat); + let view = buffer::js_buffer_slice(concat.get_raw_mut_ptr(), 4, 5_400_000); + let view = handles.root_raw_mut_ptr(view); + let nested = + handles.root_raw_mut_ptr(buffer::js_buffer_slice(view.get_raw_mut_ptr(), 4, 12)); + assert_eq!( + buffer::buffer_backing_array_buffer( + concat.get_raw_mut_ptr::() as usize + ), + buffer::buffer_backing_array_buffer( + nested.get_raw_mut_ptr::() as usize + ) + ); + let holder = crate::array::js_array_alloc(1); + crate::array::js_array_push_f64( + holder, + bits(nested.get_raw_mut_ptr::()), + ); + js_shadow_slot_set(0, ptr_bits(holder as usize)); + ( + nested.get_raw_mut_ptr::(), + concat.get_raw_mut_ptr::() as usize, + ) + }; + let before = gc_total_collection_count(); + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); + assert!(gc_total_collection_count() > before); + let _ = + gc_collect_full_mark_sweep_with_trigger(GcTriggerSnapshot::capture(GcTriggerKind::Direct)); + assert!( + unsafe { crate::value::addr_class::try_read_tracked_gc_header(owner) }.is_some(), + "the nested view must retain its concat owner without a separate owner root" + ); + assert_eq!( + buffer::js_buffer_read_uint32_be(bits(nested), 0), + 0x25252525_u32 as f64 + ); + bytes::no_gc(|scope| assert_eq!(bytes::bytes(bits(nested), scope).unwrap(), &[0x25; 8])); +} + +#[test] +fn persistent_symbols_have_a_leaf_header_at_p_minus_eight() { + let _guard = CopyingNurseryTestGuard::new(0); + let key = crate::string::js_string_from_bytes(b"b4-header".as_ptr(), 9); + let registered = + unsafe { crate::symbol::js_symbol_for(crate::value::js_nanbox_string(key as i64)) }; + let values = [ + bits(crate::symbol::well_known_symbol("iterator")), + registered, + crate::symbol::intl_legacy_constructed_symbol(), + ]; + for value in values { + let p = JSValue::from_bits(value.to_bits()).as_pointer::(); + let h = unsafe { &*header_from_trusted_user_ptr(p) }; + assert_eq!(h.obj_type, GC_TYPE_SYMBOL); + assert_eq!( + h.size as usize, + GC_HEADER_SIZE + std::mem::size_of::() + ); + assert_ne!(h.gc_flags & GC_FLAG_PINNED, 0); + assert_eq!(unsafe { crate::symbol::js_is_symbol(value) }, 1); + } +} + +#[test] +fn each_compatible_b4_sabotage_turns_its_witness_red() { + assert!(!fault("unused")); + for (fault, witness) in [ + ( + "symbol_header", + "gc::tests::buffer_b4::persistent_symbols_have_a_leaf_header_at_p_minus_eight", + ), + ( + "detach_mark", + "gc::tests::buffer_bytes::detach_defers_native_free_until_the_last_pin", + ), + ( + "owner_check", + "gc::tests::buffer_b4::views_observe_owner_resize_and_detach_after_a_live_collection", + ), + ( + "transfer_copy", + "buffer::backing::tests::transfer_receiver_uses_original_pointer_after_sender_gc", + ), + ( + "view_edge", + "gc::tests::buffer_b4::large_concat_and_nested_views_preserve_one_visible_window", + ), + ( + "u32_admission", + "typedarray::tests::owning_u32_admission_reads_current_header", + ), + ] { + let child = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", witness, "--nocapture"]) + .env("PERRY_B4_SABOTAGE", fault) + .output() + .unwrap(); + assert!( + String::from_utf8_lossy(&child.stdout).contains("running 1 test"), + "witness must actually run: {witness}" + ); + assert!( + !child.status.success(), + "sabotage {fault} left {witness} green" + ); + eprintln!("B4 sabotage {fault}: RED"); + } +} diff --git a/crates/perry-runtime/src/gc/tests/buffer_side_tables.rs b/crates/perry-runtime/src/gc/tests/buffer_side_tables.rs index 3cefa94e74..ccb6aa4a91 100644 --- a/crates/perry-runtime/src/gc/tests/buffer_side_tables.rs +++ b/crates/perry-runtime/src/gc/tests/buffer_side_tables.rs @@ -268,7 +268,7 @@ fn test_buffer_own_props_table_drains_after_owners_die() { /// pointer-lifetime contract hands `ptr(view)` to native code and documents /// the address as stable for the lifetime of the JS object /// (`bun_ffi/mod.rs`); `VIEW_REGISTRY`, -/// `BACKING_TO_VIEWS`, `DETACHED_BUFFER_REGISTRY` and the identity registries +/// `BACKING_TO_VIEWS` and the remaining identity registries /// above are all keyed by that address; and #9611 publishes /// `WebAssembly.Memory.prototype.buffer` as a foreign-backed wrapper whose /// address the wasm binding table keys. Flipping either type to `movable` diff --git a/crates/perry-runtime/src/gc/tests/mod.rs b/crates/perry-runtime/src/gc/tests/mod.rs index 06fff1f72a..4097066ff5 100644 --- a/crates/perry-runtime/src/gc/tests/mod.rs +++ b/crates/perry-runtime/src/gc/tests/mod.rs @@ -131,3 +131,5 @@ mod external_buffer; mod eden_entry_residency; mod buffer_bytes; + +mod buffer_b4; diff --git a/crates/perry-runtime/src/gc/types.rs b/crates/perry-runtime/src/gc/types.rs index f656b6064c..2e9143d7da 100644 --- a/crates/perry-runtime/src/gc/types.rs +++ b/crates/perry-runtime/src/gc/types.rs @@ -135,7 +135,10 @@ pub const GC_TYPE_BUFFER_SECRET_KEY: u8 = 30; /// A WebCrypto `CryptoKey`: Uint8Array storage holding the key material; its /// algorithm/usages metadata is `buffer::header::crypto_key_meta`. pub const GC_TYPE_BUFFER_CRYPTO_KEY: u8 = 31; -pub const GC_TYPE_MAX: u8 = GC_TYPE_BUFFER_CRYPTO_KEY; +/// Process-lifetime symbols carry a readable leaf header even though their +/// allocation is deliberately outside the collecting heap. +pub const GC_TYPE_SYMBOL: u8 = 32; +pub const GC_TYPE_MAX: u8 = GC_TYPE_SYMBOL; /// Is `obj_type` a `BufferHeader` cell of any flavor (Buffer, Uint8Array, /// ArrayBuffer, SharedArrayBuffer, DataView, KeyObject, CryptoKey)? @@ -998,6 +1001,21 @@ pub(super) static GC_TYPE_INFO_BY_ID: [Option; MALLOC_KIND_BUCKET_CO GC_TYPE_BUFFER_CRYPTO_KEY, "buffer_crypto_key", )), + Some(gc_type_info_entry( + GC_TYPE_SYMBOL, + "symbol", + GcAllocationPolicy::ArenaOrMalloc, + true, + GcRewriteDescriptorKind::Leaf, + GcLayoutSlotKind::None, + false, + GcExternalBytePolicy::InlinePayload, + GcLargeObjectPolicy::MallocTracked, + true, + GcMoveHookKind::None, + GcRewriteHookKind::None, + GcFinalizeHookKind::None, + )), ]; /// One `GcTypeInfo` for every buffer-family flavor: the flavors differ only in @@ -1592,8 +1610,9 @@ pub const OBJ_FLAG_PLAIN_ORDINARY: u16 = 0x200; /// | 13 | available | `GC_LAYOUT_ALL_POINTERS` | closure/array layout only | /// | 14..15 | available | `GC_LAYOUT_STATE_MASK` | closure/array layout only | /// -/// Byte families, TypedArray and NativeArena owner only: B1 uses 9..14 for -/// the active byte-pin count, 15 to preserve a pre-existing permanent pin. +/// Byte families, TypedArray and NativeArena owner only: byte access uses +/// the active byte-pin count in 9..13, 14 is buffer detached state, and 15 +/// preserves a pre-existing permanent pin. /// These types have no dynamic layout-slot state; no header or cell grows. /// /// Object layout is a ShapeId fact. Bits 7 and 12..15 have no object diff --git a/crates/perry-runtime/src/json/stringify_buffer.rs b/crates/perry-runtime/src/json/stringify_buffer.rs index 35f11966f1..14c10d16ba 100644 --- a/crates/perry-runtime/src/json/stringify_buffer.rs +++ b/crates/perry-runtime/src/json/stringify_buffer.rs @@ -18,57 +18,59 @@ use super::*; /// dispatch to the wrong arm (or panic when `is_object_pointer` deref's a /// bogus `keys_array` pointer). pub(crate) unsafe fn stringify_buffer(ptr: *const u8, buf: &mut String) { - let buf_ptr = ptr as *const crate::buffer::BufferHeader; - if buf_ptr.is_null() { - buf.push_str("null"); - return; - } - // #8149: an `ArrayBuffer` / `SharedArrayBuffer` / `DataView` is a - // registered buffer, but it is NOT a `Buffer` and NOT a `Uint8Array`. - // Neither `Buffer.prototype.toJSON` nor the integer-indexed own-property - // shape applies: node serializes both as `{}` because they have no own - // enumerable properties at all. Perry answered - // `{"type":"Buffer","data":[…]}` — a shape node never produces for these, - // and one that leaks the backing bytes. Asked ABOVE the - // Buffer/`Uint8Array` split, which claims every registered buffer. - // - // Own expandos (`dv.foo = 1`, which node WOULD serialize) are not emitted: - // that needs the generic object serializer, and this arm exists to stop the - // byte leak. `{}` is node's answer for every `DataView`/`ArrayBuffer` that - // carries none, which is all of them in practice. - if crate::buffer::is_non_indexed_buffer_view(ptr as usize) { - buf.push_str("{}"); - return; - } - let len = (*buf_ptr).length as usize; - let data = crate::buffer::buffer_data(buf_ptr as *const crate::buffer::BufferHeader); - let bytes = std::slice::from_raw_parts(data, len); + crate::buffer::bytes::no_gc(|scope| { + let buf_ptr = ptr as *const crate::buffer::BufferHeader; + if buf_ptr.is_null() { + buf.push_str("null"); + return; + } + // #8149: an `ArrayBuffer` / `SharedArrayBuffer` / `DataView` is a + // registered buffer, but it is NOT a `Buffer` and NOT a `Uint8Array`. + // Neither `Buffer.prototype.toJSON` nor the integer-indexed own-property + // shape applies: node serializes both as `{}` because they have no own + // enumerable properties at all. Perry answered + // `{"type":"Buffer","data":[…]}` — a shape node never produces for these, + // and one that leaks the backing bytes. Asked ABOVE the + // Buffer/`Uint8Array` split, which claims every registered buffer. + // + // Own expandos (`dv.foo = 1`, which node WOULD serialize) are not emitted: + // that needs the generic object serializer, and this arm exists to stop the + // byte leak. `{}` is node's answer for every `DataView`/`ArrayBuffer` that + // carries none, which is all of them in practice. + if crate::buffer::is_non_indexed_buffer_view(ptr as usize) { + buf.push_str("{}"); + return; + } + let bytes = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf_ptr as i64), scope) + .unwrap_or(&[]); - if crate::buffer::is_uint8array_buffer(ptr as usize) { - buf.push('{'); - for (i, b) in bytes.iter().enumerate() { - if i > 0 { - buf.push(','); + if crate::buffer::is_uint8array_buffer(ptr as usize) { + buf.push('{'); + for (i, b) in bytes.iter().enumerate() { + if i > 0 { + buf.push(','); + } + buf.push('"'); + let mut idx_buf = itoa::Buffer::new(); + buf.push_str(idx_buf.format(i)); + buf.push_str("\":"); + let mut byte_buf = itoa::Buffer::new(); + buf.push_str(byte_buf.format(*b)); } - buf.push('"'); - let mut idx_buf = itoa::Buffer::new(); - buf.push_str(idx_buf.format(i)); - buf.push_str("\":"); - let mut byte_buf = itoa::Buffer::new(); - buf.push_str(byte_buf.format(*b)); - } - buf.push('}'); - } else { - buf.push_str(r#"{"type":"Buffer","data":["#); - for (i, b) in bytes.iter().enumerate() { - if i > 0 { - buf.push(','); + buf.push('}'); + } else { + buf.push_str(r#"{"type":"Buffer","data":["#); + for (i, b) in bytes.iter().enumerate() { + if i > 0 { + buf.push(','); + } + let mut byte_buf = itoa::Buffer::new(); + buf.push_str(byte_buf.format(*b)); } - let mut byte_buf = itoa::Buffer::new(); - buf.push_str(byte_buf.format(*b)); + buf.push_str("]}"); } - buf.push_str("]}"); - } + }); } /// Issue #5111: serialize a `TypedArrayHeader`-backed typed array (`Int8Array` @@ -147,81 +149,84 @@ pub(crate) unsafe fn stringify_buffer_pretty( indent: &str, depth: usize, ) { - let buf_ptr = ptr as *const crate::buffer::BufferHeader; - if buf_ptr.is_null() { - buf.push_str("null"); - return; - } - // #8149: see `stringify_buffer` — an `ArrayBuffer` / `SharedArrayBuffer` / - // `DataView` is neither a `Buffer` nor a `Uint8Array`, and node serializes - // all three as `{}`. - if crate::buffer::is_non_indexed_buffer_view(ptr as usize) { - buf.push_str("{}"); - return; - } - let len = (*buf_ptr).length as usize; - let data = crate::buffer::buffer_data(buf_ptr as *const crate::buffer::BufferHeader); - let bytes = std::slice::from_raw_parts(data, len); + crate::buffer::bytes::no_gc(|scope| { + let buf_ptr = ptr as *const crate::buffer::BufferHeader; + if buf_ptr.is_null() { + buf.push_str("null"); + return; + } + // #8149: see `stringify_buffer` — an `ArrayBuffer` / `SharedArrayBuffer` / + // `DataView` is neither a `Buffer` nor a `Uint8Array`, and node serializes + // all three as `{}`. + if crate::buffer::is_non_indexed_buffer_view(ptr as usize) { + buf.push_str("{}"); + return; + } + let bytes = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf_ptr as i64), scope) + .unwrap_or(&[]); + let len = bytes.len(); - let push_indent = |buf: &mut String, levels: usize| { - for _ in 0..levels { - buf.push_str(indent); + let push_indent = |buf: &mut String, levels: usize| { + for _ in 0..levels { + buf.push_str(indent); + } + }; + + if len == 0 { + // Empty Uint8Array -> "{}"; empty Buffer -> {"type":"Buffer","data":[]}. + if crate::buffer::is_uint8array_buffer(ptr as usize) { + buf.push_str("{}"); + } else { + buf.push_str("{\n"); + push_indent(buf, depth + 1); + buf.push_str("\"type\": \"Buffer\",\n"); + push_indent(buf, depth + 1); + buf.push_str("\"data\": []\n"); + push_indent(buf, depth); + buf.push('}'); + } + return; } - }; - if len == 0 { - // Empty Uint8Array -> "{}"; empty Buffer -> {"type":"Buffer","data":[]}. if crate::buffer::is_uint8array_buffer(ptr as usize) { - buf.push_str("{}"); + // Plain Uint8Array: { "0": b0, "1": b1, ... } + buf.push_str("{\n"); + for (i, b) in bytes.iter().enumerate() { + push_indent(buf, depth + 1); + let mut idx_buf = itoa::Buffer::new(); + buf.push('"'); + buf.push_str(idx_buf.format(i)); + buf.push_str("\": "); + let mut byte_buf = itoa::Buffer::new(); + buf.push_str(byte_buf.format(*b)); + if i + 1 < len { + buf.push(','); + } + buf.push('\n'); + } + push_indent(buf, depth); + buf.push('}'); } else { + // Buffer: { "type": "Buffer", "data": [ b0, b1, ... ] } buf.push_str("{\n"); push_indent(buf, depth + 1); buf.push_str("\"type\": \"Buffer\",\n"); push_indent(buf, depth + 1); - buf.push_str("\"data\": []\n"); + buf.push_str("\"data\": [\n"); + for (i, b) in bytes.iter().enumerate() { + push_indent(buf, depth + 2); + let mut byte_buf = itoa::Buffer::new(); + buf.push_str(byte_buf.format(*b)); + if i + 1 < len { + buf.push(','); + } + buf.push('\n'); + } + push_indent(buf, depth + 1); + buf.push_str("]\n"); push_indent(buf, depth); buf.push('}'); } - return; - } - - if crate::buffer::is_uint8array_buffer(ptr as usize) { - // Plain Uint8Array: { "0": b0, "1": b1, ... } - buf.push_str("{\n"); - for (i, b) in bytes.iter().enumerate() { - push_indent(buf, depth + 1); - let mut idx_buf = itoa::Buffer::new(); - buf.push('"'); - buf.push_str(idx_buf.format(i)); - buf.push_str("\": "); - let mut byte_buf = itoa::Buffer::new(); - buf.push_str(byte_buf.format(*b)); - if i + 1 < len { - buf.push(','); - } - buf.push('\n'); - } - push_indent(buf, depth); - buf.push('}'); - } else { - // Buffer: { "type": "Buffer", "data": [ b0, b1, ... ] } - buf.push_str("{\n"); - push_indent(buf, depth + 1); - buf.push_str("\"type\": \"Buffer\",\n"); - push_indent(buf, depth + 1); - buf.push_str("\"data\": [\n"); - for (i, b) in bytes.iter().enumerate() { - push_indent(buf, depth + 2); - let mut byte_buf = itoa::Buffer::new(); - buf.push_str(byte_buf.format(*b)); - if i + 1 < len { - buf.push(','); - } - buf.push('\n'); - } - push_indent(buf, depth + 1); - buf.push_str("]\n"); - push_indent(buf, depth); - buf.push('}'); - } + }); } diff --git a/crates/perry-runtime/src/node_api_host/buffers.rs b/crates/perry-runtime/src/node_api_host/buffers.rs index 064d3eb495..b115a7b341 100644 --- a/crates/perry-runtime/src/node_api_host/buffers.rs +++ b/crates/perry-runtime/src/node_api_host/buffers.rs @@ -80,12 +80,16 @@ unsafe fn initialize_buffer( Ok(length) => length, Err(status) => return status, }; - let buffer = crate::buffer::buffer_alloc(length); - (*buffer).length = length; + let (value, pin) = crate::buffer::bytes::new_bytes( + crate::buffer::bytes::Brand::Buffer, + length as usize, + crate::buffer::bytes::Init::Uninit, + ); if !data.is_null() { - *data = crate::buffer::buffer_data_mut(buffer).cast(); + *data = pin.as_mut_ptr().cast(); } - write_pointer_handle(env, buffer.cast(), result) + let buffer = JSValue::from_bits(value.to_bits()).as_pointer::(); + write_pointer_handle(env, buffer.cast_mut().cast(), result) } #[no_mangle] @@ -205,11 +209,16 @@ pub unsafe extern "C" fn napi_create_arraybuffer( ) } }; - let buffer = crate::buffer::js_array_buffer_new(byte_length); + let (value, pin) = crate::buffer::bytes::new_bytes( + crate::buffer::bytes::Brand::ArrayBuffer, + byte_length as usize, + crate::buffer::bytes::Init::Zero, + ); if !data.is_null() { - *data = crate::buffer::buffer_data_mut(buffer).cast(); + *data = pin.as_mut_ptr().cast(); } - write_pointer_handle(env, buffer.cast(), result) + let buffer = JSValue::from_bits(value.to_bits()).as_pointer::(); + write_pointer_handle(env, buffer, result) } #[no_mangle] diff --git a/crates/perry-runtime/src/node_stream/native_hooks.rs b/crates/perry-runtime/src/node_stream/native_hooks.rs index 87666731a5..902f710980 100644 --- a/crates/perry-runtime/src/node_stream/native_hooks.rs +++ b/crates/perry-runtime/src/node_stream/native_hooks.rs @@ -457,12 +457,11 @@ unsafe fn chunk_bytes(chunk: f64) -> (*const u8, usize) { } return (crate::string::string_data(ptr), (*ptr).byte_len as usize); } - let raw = raw_ptr_from_value(chunk); - if raw >= 0x10000 && crate::buffer::is_registered_buffer(raw) { - let buf = raw as *const crate::buffer::BufferHeader; - return (crate::buffer::buffer_data(buf), (*buf).length as usize); - } - (std::ptr::null(), 0) + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(chunk, scope) + .map(|b| (b.as_ptr(), b.len())) + .unwrap_or((std::ptr::null(), 0)) + }) } /// The one step loop for every hooked stream (see the module docs). diff --git a/crates/perry-runtime/src/node_submodules/blob.rs b/crates/perry-runtime/src/node_submodules/blob.rs index abc9999a57..95868dece8 100644 --- a/crates/perry-runtime/src/node_submodules/blob.rs +++ b/crates/perry-runtime/src/node_submodules/blob.rs @@ -330,12 +330,12 @@ fn captured_blob_bytes(closure: *const ClosureHeader) -> Vec { if raw < 0x10000 || !crate::buffer::is_registered_buffer(raw) { return Vec::new(); } - unsafe { - let buf = raw as *const crate::buffer::BufferHeader; - let len = (*buf).length as usize; - let data = crate::buffer::buffer_data(buf); - std::slice::from_raw_parts(data, len).to_vec() - } + crate::buffer::bytes::no_gc(|scope| { + let value = crate::value::js_nanbox_pointer(raw as i64); + crate::buffer::bytes::bytes(value, scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }) } fn set_named_value(obj: *mut ObjectHeader, name: &[u8], value: f64) { diff --git a/crates/perry-runtime/src/node_submodules/consumers.rs b/crates/perry-runtime/src/node_submodules/consumers.rs index daffb74f41..b27b0f6f7b 100644 --- a/crates/perry-runtime/src/node_submodules/consumers.rs +++ b/crates/perry-runtime/src/node_submodules/consumers.rs @@ -28,24 +28,16 @@ pub(crate) fn buffer_from_bytes( mark_array_buffer: bool, mark_uint8_array: bool, ) -> *mut crate::buffer::BufferHeader { - let buf = crate::buffer::buffer_alloc(bytes.len() as u32); - unsafe { - (*buf).length = bytes.len() as u32; - if !bytes.is_empty() { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buf), - bytes.len(), - ); - } - } - if mark_array_buffer { - crate::buffer::mark_as_array_buffer(buf as usize); - } - if mark_uint8_array { - crate::buffer::mark_as_uint8array(buf as usize); - } - buf + let brand = if mark_array_buffer { + crate::buffer::bytes::Brand::ArrayBuffer + } else if mark_uint8_array { + crate::buffer::bytes::Brand::Uint8Array + } else { + crate::buffer::bytes::Brand::Buffer + }; + crate::value::JSValue::from_bits(crate::buffer::bytes::from_slice(brand, bytes).to_bits()) + .as_pointer::() + .cast_mut() } pub(crate) fn bytes_to_buffer_value(bytes: &[u8]) -> f64 { @@ -276,12 +268,12 @@ fn append_buffer_value_bytes(raw: usize, out: &mut Vec) { if raw < 0x10000 || !crate::buffer::is_registered_buffer(raw) { return; } - unsafe { - let buf = raw as *const crate::buffer::BufferHeader; - let len = (*buf).length as usize; - let data = crate::buffer::buffer_data(buf); - out.extend_from_slice(std::slice::from_raw_parts(data, len)); - } + crate::buffer::bytes::no_gc(|scope| { + let value = crate::value::js_nanbox_pointer(raw as i64); + if let Ok(bytes) = crate::buffer::bytes::bytes(value, scope) { + out.extend_from_slice(bytes); + } + }); } fn append_number_chunk(value: f64, jsval: JSValue, out: &mut Vec) { diff --git a/crates/perry-runtime/src/node_v8.rs b/crates/perry-runtime/src/node_v8.rs index b32a29ada4..047b51ab5e 100644 --- a/crates/perry-runtime/src/node_v8.rs +++ b/crates/perry-runtime/src/node_v8.rs @@ -114,31 +114,11 @@ unsafe fn build_object(pairs: &[(&str, f64)]) -> f64 { /// `Uint8Array` / other TypedArrays, and `ArrayBuffer`. Returns `None` for /// anything else (caller throws `ERR_INVALID_ARG_TYPE` like Node). unsafe fn input_bytes(value: f64) -> Option> { - let jsv = JSValue::from_bits(value.to_bits()); - if !jsv.is_pointer() { - return None; - } - let addr = (value.to_bits() & crate::value::POINTER_MASK) as usize; - if addr < 0x10000 { - return None; - } - if crate::buffer::is_registered_buffer(addr) { - let data = crate::buffer::js_native_buffer_data_ptr(value); - let len = crate::buffer::js_native_buffer_byte_len(value); - if data.is_null() || len == 0 { - return Some(Vec::new()); - } - return Some(std::slice::from_raw_parts(data, len).to_vec()); - } - if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *const crate::typedarray::TypedArrayHeader; - return Some( - crate::typedarray::typed_array_bytes(ta) - .map(|b| b.to_vec()) - .unwrap_or_default(), - ); - } - None + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) } fn is_valid_heap_snapshot_options(value: f64) -> bool { diff --git a/crates/perry-runtime/src/node_vm.rs b/crates/perry-runtime/src/node_vm.rs index 029f5fff0a..07daccbbbf 100644 --- a/crates/perry-runtime/src/node_vm.rs +++ b/crates/perry-runtime/src/node_vm.rs @@ -11,7 +11,6 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use crate::array::ArrayHeader; -use crate::buffer::BufferHeader; use crate::closure::ClosureHeader; use crate::object::{ObjectHeader, PropertyAttrs}; use crate::string::StringHeader; @@ -319,10 +318,6 @@ fn array_value(arr: *mut ArrayHeader) -> f64 { crate::value::js_nanbox_pointer(arr as i64) } -fn buffer_value(buf: *mut BufferHeader) -> f64 { - crate::value::js_nanbox_pointer(buf as i64) -} - fn raw_addr_from_value(value: f64) -> usize { let bits = value.to_bits(); let jv = JSValue::from_bits(bits); @@ -523,16 +518,11 @@ fn validate_produce_cached_data(options: f64) -> bool { } fn typed_array_or_buffer_bytes(value: f64) -> Option> { - let mut len = 0_u32; - let ptr = unsafe { crate::buffer::js_value_buffer_or_typedarray_data(value, &mut len) }; - if !ptr.is_null() { - return Some(unsafe { std::slice::from_raw_parts(ptr, len as usize).to_vec() }); - } - let addr = raw_addr_from_value(value); - if addr != 0 && crate::buffer::is_data_view(addr) { - return Some(Vec::new()); - } - None + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) } fn validate_cached_data_option(options: f64) -> Option> { @@ -580,16 +570,7 @@ fn validate_one_of_string( fn cached_data_buffer(kind: u8, hash: u64) -> f64 { let bytes = cached_data_bytes(kind, hash); - let buf = crate::buffer::buffer_alloc(bytes.len() as u32); - unsafe { - (*buf).length = bytes.len() as u32; - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buf), - bytes.len(), - ); - } - buffer_value(buf) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Buffer, &bytes) } fn validate_module_source(source: &str) { diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs index dcff1c46a1..aa5efcb09d 100644 --- a/crates/perry-runtime/src/object/buffer_dispatch.rs +++ b/crates/perry-runtime/src/object/buffer_dispatch.rs @@ -374,11 +374,12 @@ unsafe fn buffer_secret_export_format(bits: f64) -> Option { } unsafe fn secret_key_jwk_object(buf_ptr: *mut crate::buffer::BufferHeader) -> f64 { - let bytes = std::slice::from_raw_parts( - crate::buffer::buffer_data(buf_ptr as *const crate::buffer::BufferHeader), - (*buf_ptr).length as usize, - ); - let encoded = perry_base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes); + let encoded = crate::buffer::bytes::no_gc(|scope| { + let bytes = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf_ptr as i64), scope) + .expect("live secret key"); + perry_base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) + }); let obj = js_object_alloc(0, 2); let kty_key = crate::string::js_string_from_bytes(b"kty".as_ptr(), 3); let kty_val = crate::string::js_string_from_bytes(b"oct".as_ptr(), 3); @@ -461,17 +462,10 @@ unsafe fn secret_to_crypto_key(addr: usize, algorithm_bits: f64) -> f64 { // and the KeyObject keeps being one. A buffer's flavor is its GC type // (#10694), so the key bytes go into a fresh cell carrying the CryptoKey // brand rather than re-branding the KeyObject's own cell. - let src = addr as *const crate::buffer::BufferHeader; - let len = (*src).length; - let out = crate::buffer::buffer_alloc(len); - (*out).length = len; - if len > 0 { - std::ptr::copy_nonoverlapping( - crate::buffer::buffer_data(src), - crate::buffer::buffer_data_mut(out), - len as usize, - ); - } + let input = crate::value::js_nanbox_pointer(addr as i64); + let value = crate::buffer::bytes::copy_value(crate::buffer::bytes::Brand::Buffer, input) + .expect("live key bytes"); + let out = JSValue::from_bits(value.to_bits()).as_pointer::(); crate::buffer::mark_as_crypto_key(out as usize, algo_id, hash_id, 1); f64::from_bits(JSValue::pointer(out as *mut u8).bits()) } @@ -769,13 +763,11 @@ pub unsafe fn dispatch_buffer_method( let final_ = if args.len() >= 3 { rel(args[2]) } else { len }; let count = (final_ - from).min(len - to); if count > 0 { - let data = crate::buffer::buffer_data_mut(buf_ptr); - let block: Vec = (0..count as usize) - .map(|i| *data.add(from as usize + i)) - .collect(); - for (i, b) in block.into_iter().enumerate() { - *data.add(to as usize + i) = b; - } + crate::buffer::bytes::no_gc(|scope| { + if let Ok(data) = crate::buffer::bytes::bytes_mut(buf_f64, scope) { + data.copy_within(from as usize..(from + count) as usize, to as usize); + } + }); } buf_f64 } @@ -861,23 +853,11 @@ pub unsafe fn dispatch_buffer_method( if matches!(format.as_deref(), Some("jwk")) { return secret_key_jwk_object(buf_ptr); } - let bytes = std::slice::from_raw_parts( - crate::buffer::buffer_data(buf_ptr as *const crate::buffer::BufferHeader), - (*buf_ptr).length as usize, - ); - let out = crate::buffer::buffer_alloc(bytes.len() as u32); - if !out.is_null() { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(out), - bytes.len(), - ); - (*out).length = bytes.len() as u32; - // Node returns a `Buffer` (Uint8Array subclass) here so - // `instanceof Uint8Array` must hold on the result. - crate::buffer::mark_as_uint8array(out as usize); - } - f64::from_bits(JSValue::pointer(out as *mut u8).bits()) + crate::buffer::bytes::copy_value( + crate::buffer::bytes::Brand::Uint8Array, + crate::value::js_nanbox_pointer(addr as i64), + ) + .expect("live key bytes") } "toCryptoKey" if crate::buffer::is_secret_key(addr) && !args.is_empty() => { secret_to_crypto_key(addr, args[0]) diff --git a/crates/perry-runtime/src/object/global_this_webassembly.rs b/crates/perry-runtime/src/object/global_this_webassembly.rs index 740a778deb..8f0f2b6db9 100644 --- a/crates/perry-runtime/src/object/global_this_webassembly.rs +++ b/crates/perry-runtime/src/object/global_this_webassembly.rs @@ -844,13 +844,17 @@ fn wasm_memory_grow_on(this: f64, delta: f64) -> Result { )); } if old_bytes > 0 { - unsafe { - std::ptr::copy_nonoverlapping( - crate::buffer::buffer_data_mut(buf), - crate::buffer::buffer_data_mut(new_buf), - old_bytes, - ); - } + crate::buffer::bytes::no_gc(|scope| unsafe { + let source = + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(buf as i64), scope) + .expect("live wasm store"); + let target = crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(new_buf as i64), + scope, + ) + .expect("new wasm store"); + target[..old_bytes].copy_from_slice(source); + }); } js_object_set_field_by_name( this_obj, diff --git a/crates/perry-runtime/src/object/native_module_crypto_key_object.rs b/crates/perry-runtime/src/object/native_module_crypto_key_object.rs index a93c977c94..ae536f3c74 100644 --- a/crates/perry-runtime/src/object/native_module_crypto_key_object.rs +++ b/crates/perry-runtime/src/object/native_module_crypto_key_object.rs @@ -71,21 +71,10 @@ pub(super) unsafe fn key_object_from(value: f64) -> f64 { /// 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() { - 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); + let input = crate::value::js_nanbox_pointer(addr as i64); + let value = crate::buffer::bytes::copy_value(crate::buffer::bytes::Brand::Uint8Array, input) + .unwrap_or_else(|_| conversion_failed("could not read the secret key buffer")); + let out = JSValue::from_bits(value.to_bits()).as_pointer::(); crate::buffer::mark_as_secret_key(out as usize); f64::from_bits(JSValue::pointer(out as *const u8).bits()) } diff --git a/crates/perry-runtime/src/object/native_module_crypto_random.rs b/crates/perry-runtime/src/object/native_module_crypto_random.rs index d1509b9a28..ed0472c535 100644 --- a/crates/perry-runtime/src/object/native_module_crypto_random.rs +++ b/crates/perry-runtime/src/object/native_module_crypto_random.rs @@ -75,36 +75,28 @@ pub(super) unsafe fn random_fill_sync(target: f64, offset_bits: f64, size_bits: use rand::Rng; let addr = value_addr(target); - if crate::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *mut crate::typedarray::TypedArrayHeader; - if let Some(data) = crate::typedarray::typed_array_bytes_mut(ta) { - let elem_size = (*ta).elem_size as usize; - let len = if elem_size == 0 { - 0 - } else { - data.len() / elem_size - }; - let (start_elem, count_elem) = range(len, offset_bits, size_bits); - let start = start_elem.saturating_mul(elem_size); - let end = start - .saturating_add(count_elem.saturating_mul(elem_size)) - .min(data.len()); - if end > start { - rand::rng().fill_bytes(&mut data[start..end]); - } - return target; - } - invalid_buf(target); - } - if crate::buffer::is_registered_buffer(addr) { - let buf = addr as *mut crate::buffer::BufferHeader; - let total = (*buf).length as usize; - let (start, count) = range(total, offset_bits, size_bits); - if count > 0 { - let data = crate::buffer::buffer_data_mut(buf); - rand::rng().fill_bytes(std::slice::from_raw_parts_mut(data.add(start), count)); - } - return target; - } - invalid_buf(target); + let elem_size = if let Some(kind) = crate::typedarray::lookup_typed_array_kind(addr) { + crate::typedarray::elem_size_for_kind(kind).max(1) + } else if crate::buffer::is_registered_buffer(addr) { + 1 + } else { + invalid_buf(target) + }; + let value = crate::value::js_nanbox_pointer(addr as i64); + let byte_len = crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope).map(<[u8]>::len) + }) + .unwrap_or_else(|_| invalid_buf(target)); + let (start, count) = range(byte_len / elem_size, offset_bits, size_bits); + let start = start.saturating_mul(elem_size); + let count = count + .saturating_mul(elem_size) + .min(byte_len.saturating_sub(start)); + crate::buffer::bytes::no_gc(|scope| { + let data = crate::buffer::bytes::bytes_mut(value, scope)?; + rand::rng().fill_bytes(&mut data[start..start + count]); + Ok::<_, crate::buffer::bytes::NotBytes>(()) + }) + .unwrap_or_else(|_| invalid_buf(target)); + target } diff --git a/crates/perry-runtime/src/object/shape_rule3.rs b/crates/perry-runtime/src/object/shape_rule3.rs index 8d9e65af4b..4ed9055645 100644 --- a/crates/perry-runtime/src/object/shape_rule3.rs +++ b/crates/perry-runtime/src/object/shape_rule3.rs @@ -311,6 +311,12 @@ pub(crate) const RULE3_KINDS: &[(u8, &str, &str, Rule3Word)] = &[ "capacity: u32 (bytes), as `GC_TYPE_BUFFER`", Rule3Word::BoundedBelowRange, ), + ( + gc::GC_TYPE_SYMBOL, + "SymbolHeader (persistent)", + "registered: u32 — the Symbol.for flag is exactly 0 or 1", + Rule3Word::StructurallySmall, + ), ]; /// Rule 3, checked where the word is written. diff --git a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs index 2ff7cc7276..91ee1d2e5f 100644 --- a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs +++ b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs @@ -348,14 +348,17 @@ unsafe fn uint8_set(addr: usize, index: usize, value: u8) { } unsafe fn uint8_alloc_like(source_addr: usize, len: usize) -> *mut crate::buffer::BufferHeader { - let out = crate::buffer::buffer_alloc(len as u32); - if !out.is_null() { - (*out).length = len as u32; - if crate::buffer::is_uint8array_buffer(source_addr) { - crate::buffer::mark_as_uint8array(out as usize); - } - } - out + let brand = if crate::buffer::is_uint8array_buffer(source_addr) { + crate::buffer::bytes::Brand::Uint8Array + } else { + crate::buffer::bytes::Brand::Buffer + }; + let (value, pin) = + crate::buffer::bytes::new_bytes(brand, len, crate::buffer::bytes::Init::Uninit); + drop(pin); + crate::value::JSValue::from_bits(value.to_bits()) + .as_pointer::() + .cast_mut() } unsafe fn uint8_copy_to_new(source_addr: usize) -> *mut crate::buffer::BufferHeader { diff --git a/crates/perry-runtime/src/os_process_stream_write.rs b/crates/perry-runtime/src/os_process_stream_write.rs index 8064d908f5..fa0c39e9f2 100644 --- a/crates/perry-runtime/src/os_process_stream_write.rs +++ b/crates/perry-runtime/src/os_process_stream_write.rs @@ -36,40 +36,18 @@ use crate::value::JSValue; /// The pointer borrows the live allocation: it is valid until the next /// collection. The callers below neither allocate on the JS heap nor reach a /// safepoint between taking it and finishing the write. -fn binary_chunk_span(chunk: f64) -> Option<(*const u8, usize)> { +fn binary_chunk_span<'s>( + chunk: f64, + scope: &'s crate::buffer::bytes::NoGc<'s>, +) -> Option<&'s [u8]> { if !JSValue::from_bits(chunk.to_bits()).is_pointer() { return None; } - // Everything below keys registries by address; nothing dereferences - // `addr` unless a registry has vouched for it. let addr = (chunk.to_bits() & crate::value::POINTER_MASK) as usize; if crate::buffer::is_any_array_buffer(addr) { return None; } - let mut len = 0_u32; - // SAFETY: `len` is a valid out-pointer. This is the shared native-span - // accessor: it resolves a registered view (subarray / `new T(ab, off, n)` / - // DataView) to its backing window rather than to the view header. - let data = unsafe { crate::buffer::js_value_buffer_or_typedarray_data(chunk, &mut len) }; - if !data.is_null() && len != 0 { - return Some((data, len as usize)); - } - // `(null, 0)` is both "not a binary chunk" and "an empty one" (a - // zero-length or detached view). Only the second is a chunk. - let is_view = crate::buffer::is_registered_buffer(addr) - || crate::typedarray::lookup_typed_array_kind(addr).is_some(); - if !is_view { - return None; - } - // Node re-wraps every view that is not already a `Buffer` - // (`new FastBuffer(chunk.buffer, chunk.byteOffset, chunk.byteLength)`), - // and that construction throws once the ArrayBuffer has been transferred - // away. A detached `Buffer` is written as it is: empty. Only reachable - // for an empty view, so a live chunk never pays for the lookup. - if !crate::buffer::is_node_buffer(addr) && view_backing_is_detached(addr) { - crate::typedarray::throw_type_error(b"Cannot perform Construct on a detached ArrayBuffer"); - } - Some((std::ptr::NonNull::::dangling().as_ptr() as *const u8, 0)) + crate::buffer::bytes::bytes(chunk, scope).ok() } /// Whether the `ArrayBuffer` a view aliases has been detached (`transfer()`). @@ -119,10 +97,21 @@ pub(super) fn with_write_bytes(chunk: f64, encoding: f64, f: impl FnOnce(&[u8 tag => f(&crate::buffer::buffer_string_bytes_for_encoding(text, tag)), }; } - } else if let Some((data, len)) = binary_chunk_span(chunk) { - // SAFETY: see `binary_chunk_span` — `len` readable bytes, stable for - // the duration of `f`, which does not touch the JS heap. - return f(unsafe { std::slice::from_raw_parts(data, len) }); + } else if value.is_pointer() { + let addr = value.as_pointer::() as usize; + let is_view = !crate::buffer::is_any_array_buffer(addr) + && (crate::buffer::is_registered_buffer(addr) + || crate::typedarray::lookup_typed_array_kind(addr).is_some()); + if is_view { + if !crate::buffer::is_node_buffer(addr) && view_backing_is_detached(addr) { + crate::typedarray::throw_type_error( + b"Cannot perform Construct on a detached ArrayBuffer", + ); + } + return crate::buffer::bytes::no_gc(|scope| { + f(binary_chunk_span(chunk, scope).unwrap_or(&[])) + }); + } } let s_ptr = crate::value::js_jsvalue_to_string(chunk); if s_ptr.is_null() { diff --git a/crates/perry-runtime/src/os_process_stream_write_tests.rs b/crates/perry-runtime/src/os_process_stream_write_tests.rs index e248104027..c38927ecf7 100644 --- a/crates/perry-runtime/src/os_process_stream_write_tests.rs +++ b/crates/perry-runtime/src/os_process_stream_write_tests.rs @@ -84,7 +84,9 @@ fn a_wider_typed_array_is_written_as_its_raw_element_bytes() { fn an_empty_view_is_an_empty_chunk_and_an_arraybuffer_is_not_a_chunk() { let empty = js_uint8array_alloc(0); assert_eq!( - binary_chunk_span(pointer_value(empty)).map(|s| s.1), + crate::buffer::bytes::no_gc( + |scope| binary_chunk_span(pointer_value(empty), scope).map(<[u8]>::len) + ), Some(0) ); assert!(written(pointer_value(empty), UNDEFINED).is_empty()); @@ -92,9 +94,19 @@ fn an_empty_view_is_an_empty_chunk_and_an_arraybuffer_is_not_a_chunk() { // Node rejects an ArrayBuffer chunk; perry keeps writing its display text // rather than its bytes, so it must not be classified as binary. let array_buffer = js_array_buffer_new(4); - assert!(binary_chunk_span(pointer_value(array_buffer)).is_none()); - assert!(binary_chunk_span(UNDEFINED).is_none()); - assert!(binary_chunk_span(42.0).is_none()); + assert!(crate::buffer::bytes::no_gc(|scope| binary_chunk_span( + pointer_value(array_buffer), + scope + ) + .is_none())); + assert!(crate::buffer::bytes::no_gc(|scope| binary_chunk_span( + UNDEFINED, scope + ) + .is_none())); + assert!(crate::buffer::bytes::no_gc(|scope| binary_chunk_span( + 42.0, scope + ) + .is_none())); } #[test] diff --git a/crates/perry-runtime/src/os_process_streams.rs b/crates/perry-runtime/src/os_process_streams.rs index 5d3d2536d3..2f9a30897f 100644 --- a/crates/perry-runtime/src/os_process_streams.rs +++ b/crates/perry-runtime/src/os_process_streams.rs @@ -644,35 +644,7 @@ pub fn stdin_chunk_jsvalue(chunk: &[u8]) -> f64 { let decoded = stdin_decode_encoded(chunk).unwrap_or_default(); return string_jsvalue(&decoded); } - let buf = crate::buffer::buffer_alloc(chunk.len() as u32); - unsafe { - // #9399: `buffer_alloc` only reserves CAPACITY — it leaves `length` at - // 0, and every other caller sets the length itself after filling the - // payload. This one never did, so a `data` chunk delivered as a Buffer - // (Node's default, i.e. whenever `setEncoding` has NOT been called) - // arrived with `.length === 0`: the bytes were copied into the payload - // but no consumer could see them. `chunk.toString()` was `""`, - // `Buffer.concat([acc, chunk])` appended nothing, and - // `JSON.stringify(chunk)` reported `{"type":"Buffer","data":[]}`. - // - // That is why claude-code's MCP stdio server answered nothing: its - // transport does `readBuffer.append(chunk)` on raw (unencoded) chunks, - // so the newline-delimited JSON-RPC framer never saw a single byte and - // `readMessage()` returned null forever. The `setEncoding("utf8")` - // branch above was unaffected, which is why the string path looked fine. - (*buf).length = chunk.len() as u32; - let dst = crate::buffer::buffer_data_mut(buf); - if !dst.is_null() && !chunk.is_empty() { - // GC_STORE_AUDIT(POINTER_FREE): raw stdin bytes into a freshly - // allocated Buffer's data area. The payload is bytes, never - // JSValues, so the destination slots hold no GC references and no - // write barrier is required. `buffer_alloc` returns before any - // safepoint, so `dst` cannot have been moved between the - // allocation and this copy. - std::ptr::copy_nonoverlapping(chunk.as_ptr(), dst, chunk.len()); - } - } - f64::from_bits(crate::value::JSValue::pointer(buf as *const u8).bits()) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Buffer, chunk) } /// `process.stdin.setEncoding(enc)`. Was a no-op stub, which forced every `data` diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index 91bdd00094..8a6f63baec 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -145,6 +145,43 @@ pub struct SymbolHeader { pub id: u64, } +/// Box-leaked symbols obey the same p-8 header contract as GC cells. The +/// prefix is not registered with the collector: identity and lifetime remain +/// process-wide, and the payload contains no GC pointers. +#[repr(C)] +struct PersistentSymbol { + header: crate::gc::GcHeader, + symbol: SymbolHeader, +} + +fn leak_symbol(symbol: SymbolHeader) -> *mut SymbolHeader { + let mut cell = Box::new(PersistentSymbol { + header: crate::gc::GcHeader { + obj_type: crate::gc::GC_TYPE_SYMBOL, + gc_flags: crate::gc::GC_FLAG_TENURED, + _reserved: 0, + size: std::mem::size_of::() as u32, + }, + symbol, + }); + // A Box allocation is outside the moving arena. Use the shared setter + // so pin custody remains explicit even for these immortal leaf cells. + unsafe { crate::gc::pin_object(std::ptr::addr_of_mut!(cell.header)) }; + #[cfg(test)] + let cell = if crate::buffer::bytes::b4_sabotage("symbol_header") { + let mut cell = cell; + cell.header.obj_type = crate::gc::GC_TYPE_STRING; + cell + } else { + cell + }; + let raw = Box::into_raw(cell); + unsafe { std::ptr::addr_of_mut!((*raw).symbol) } +} + +const _: () = assert!(std::mem::offset_of!(PersistentSymbol, symbol) == crate::gc::GC_HEADER_SIZE); +const _: () = assert!(std::mem::offset_of!(SymbolHeader, registered) == 4); + // Global registry for Symbol.for(key) — maps key → symbol pointer (as usize). // The symbol pointers stored here are leaked (never freed) so that // `Symbol.for("x") === Symbol.for("x")` always returns the same pointer. @@ -374,13 +411,12 @@ fn well_known_symbol_slow(short_name: &str) -> *mut SymbolHeader { // can't store a real StringHeader pointer here because this allocation may // be made on a worker thread whose arena will later be torn down, while // the SymbolHeader itself is Box-leaked and outlives that arena. - let boxed = Box::new(SymbolHeader { + let sym_ptr = leak_symbol(SymbolHeader { magic: SYMBOL_MAGIC, registered: 0, description: std::ptr::null_mut(), id: next_id(), }); - let sym_ptr = Box::into_raw(boxed); if crate::hot_diag::receiver_repr_on() { crate::hot_diag::receiver_repr_note_constructed( crate::hot_diag::ReceiverReprFamily::SymbolGlobal, @@ -452,28 +488,10 @@ static SYMBOL_EVER_REGISTERED: crate::registry_latch::RegistryLatch = /// classifier that has already ruled a symbol out some other way must still ask /// the authoritative [`is_registered_symbol`]. /// -/// #7850. `gc_pointer_and_type_from_value` — on the path of every dynamic method -/// call — cannot use `GcHeader.obj_type` to rule a symbol out, because three of -/// the five registration sites (`well_known_symbol`, -/// `intl_legacy_constructed_symbol`, `js_symbol_for`) are `Box::into_raw`: -/// process-lifetime allocations with **no `GcHeader` at all**, so `ptr - 8` is -/// foreign allocator bytes that can coincidentally equal any `obj_type`. Trusting -/// the header for those is a silent wrong answer. -/// -/// What every symbol DOES have, whatever its storage, is `SYMBOL_MAGIC` in its -/// own first four bytes — `alloc_symbol` and all three `Box` sites set it, and -/// the field is at offset 0 precisely so cheap discrimination is possible. So -/// one 4-byte load of the object the caller is already about to inspect answers -/// "definitely not a symbol" for everything else. -/// -/// The direction of the guarantee is what makes it safe to use as a screen: -/// **`false` is exact** — no symbol reads `false` — while `true` is merely -/// "ask the registry". A non-symbol whose first word happens to equal -/// `SYMBOL_MAGIC` (a `StringHeader` would need `utf16_len == 0x5359_4D42`, i.e. -/// a 2.8 GB string; an `ObjectHeader`'s first word is `class_id`, and ids are -/// handed out from 1 — #8113 deleted the `object_type` tag that used to sit -/// there, which does not change this argument) simply pays the old probe and -/// gets the old, correct answer. +/// All persistent symbols now carry a GC_TYPE_SYMBOL prefix; fresh symbols +/// retain their existing GC_TYPE_STRING leaf layout. The magic screen stays +/// valid for both kinds until the codegen header migration in B4c. +/// `false` is exact, while `true` still asks the symbol registry. /// /// # Safety /// `ptr` must be readable for 4 bytes. Every caller is one that already @@ -698,13 +716,13 @@ pub fn intl_legacy_constructed_symbol() -> f64 { // realm-global. Description text lives in REGISTERED_SYMBOL_DESCRIPTIONS // (readers materialize a fresh StringHeader on demand), matching the // well-known-symbol contract. - let boxed = Box::new(SymbolHeader { + let sym_ptr = leak_symbol(SymbolHeader { magic: SYMBOL_MAGIC, registered: 0, description: std::ptr::null_mut(), id: next_id(), }); - let sym_ptr = Box::into_raw(boxed) as usize; + let sym_ptr = sym_ptr as usize; if crate::hot_diag::receiver_repr_on() { crate::hot_diag::receiver_repr_note_constructed( crate::hot_diag::ReceiverReprFamily::SymbolGlobal, diff --git a/crates/perry-runtime/src/symbol/constructors.rs b/crates/perry-runtime/src/symbol/constructors.rs index 4474ecd1ab..4616ff2841 100644 --- a/crates/perry-runtime/src/symbol/constructors.rs +++ b/crates/perry-runtime/src/symbol/constructors.rs @@ -100,13 +100,12 @@ pub unsafe extern "C" fn js_symbol_for(key_f64: f64) -> f64 { // the side table and materialize a StringHeader in *their own* arena on // demand, so cross-thread reads are safe even when the originating // worker's arena was torn down. - let boxed = Box::new(SymbolHeader { + let sym_ptr = leak_symbol(SymbolHeader { magic: SYMBOL_MAGIC, registered: 1, description: std::ptr::null_mut(), id: next_id(), }); - let sym_ptr = Box::into_raw(boxed); if crate::hot_diag::receiver_repr_on() { crate::hot_diag::receiver_repr_note_constructed( crate::hot_diag::ReceiverReprFamily::SymbolGlobal, diff --git a/crates/perry-runtime/src/text.rs b/crates/perry-runtime/src/text.rs index 19cb635e5f..c3a7c28d55 100644 --- a/crates/perry-runtime/src/text.rs +++ b/crates/perry-runtime/src/text.rs @@ -27,7 +27,7 @@ use std::sync::Mutex; -use crate::buffer::{buffer_alloc, buffer_data_mut, mark_as_uint8array, BufferHeader}; +use crate::buffer::BufferHeader; use crate::object::{js_object_alloc, js_object_set_field_by_name, ObjectHeader}; use crate::string::{js_string_from_bytes, StringHeader}; @@ -412,22 +412,13 @@ pub extern "C" fn js_text_encoder_encode_llvm(value: f64) -> i64 { (d, l) }; - let buf = buffer_alloc(len as u32); - unsafe { - (*buf).length = len as u32; - if len > 0 { - std::ptr::copy_nonoverlapping(data_ptr, buffer_data_mut(buf), len); - } - } - mark_as_uint8array(buf as usize); - - buf as i64 -} - -#[derive(Clone, Copy)] -enum TextEncoderDest { - Buffer(*mut BufferHeader), - TypedArray(*mut crate::typedarray::TypedArrayHeader), + crate::value::JSValue::from_bits( + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Uint8Array, unsafe { + std::slice::from_raw_parts(data_ptr, len) + }) + .to_bits(), + ) + .as_pointer::() as i64 } fn text_value_pointer_addr(value: f64) -> usize { @@ -510,17 +501,17 @@ fn text_encoder_encode_into_source(source: f64) -> *const StringHeader { ptr } -fn text_encoder_encode_into_dest(dest: f64) -> TextEncoderDest { +fn text_encoder_encode_into_dest(dest: f64) -> f64 { let addr = text_value_pointer_addr(dest); if addr >= 0x1000 { if crate::typedarray::lookup_typed_array_kind(addr) == Some(crate::typedarray::KIND_UINT8) { - return TextEncoderDest::TypedArray(addr as *mut crate::typedarray::TypedArrayHeader); + return dest; } if crate::buffer::is_registered_buffer(addr) && !crate::buffer::is_any_array_buffer(addr) && !crate::buffer::is_data_view(addr) { - return TextEncoderDest::Buffer(addr as *mut BufferHeader); + return dest; } } @@ -581,35 +572,18 @@ pub extern "C" fn js_text_encoder_encode_into_llvm(source: f64, dest: f64) -> i6 let str_ptr = text_encoder_encode_into_source(source); let dest = text_encoder_encode_into_dest(dest); - unsafe { + let (read, written) = crate::buffer::bytes::no_gc(|scope| unsafe { let src_len = (*str_ptr).byte_len as usize; let src_data = (str_ptr as *const u8).add(std::mem::size_of::()); let src = std::slice::from_raw_parts(src_data, src_len); - let dest_len = match dest { - TextEncoderDest::Buffer(dest_ptr) => (*dest_ptr).length as usize, - TextEncoderDest::TypedArray(dest_ptr) => { - crate::typedarray::typed_array_bytes_mut(dest_ptr) - .map(|bytes| bytes.len()) - .unwrap_or(0) - } + let Ok(bytes) = crate::buffer::bytes::bytes_mut(dest, scope) else { + return (0, 0); }; - let (read, written) = text_encoder_prefix_len(src, dest_len); - - match dest { - TextEncoderDest::Buffer(dest_ptr) => { - for (idx, byte) in src.iter().copied().take(written).enumerate() { - crate::buffer::js_buffer_set(dest_ptr, idx as i32, byte as i32); - } - } - TextEncoderDest::TypedArray(dest_ptr) => { - if let Some(bytes) = crate::typedarray::typed_array_bytes_mut(dest_ptr) { - bytes[..written].copy_from_slice(&src[..written]); - } - } - } - - text_encoder_result(read, written) as i64 - } + let (read, written) = text_encoder_prefix_len(src, bytes.len()); + bytes[..written].copy_from_slice(&src[..written]); + (read, written) + }); + text_encoder_result(read, written) as i64 } /// `decoder.decode(buf)` — UTF-8 decode a NaN-boxed `BufferHeader` value. @@ -654,38 +628,28 @@ pub extern "C" fn js_text_decoder_decode_llvm(handle: f64, value: f64) -> i64 { throw_invalid_decode_input(); } - // Route by concrete kind so the byte offset/length is honored and only - // genuine buffer sources are accepted. - let bytes: &[u8] = unsafe { - if crate::typedarray::lookup_typed_array_kind(ptr_usize).is_some() { - // TypedArray view (incl. Uint16Array, sliced subarray, etc.). - match crate::typedarray::typed_array_bytes( - ptr_usize as *const crate::typedarray::TypedArrayHeader, - ) { - Some(b) => b, - None => throw_invalid_decode_input(), - } - } else if crate::buffer::is_data_view(ptr_usize) - || crate::buffer::is_any_array_buffer(ptr_usize) - || crate::buffer::is_registered_buffer(ptr_usize) - { - // DataView, (Shared)ArrayBuffer, or a registered Buffer/Uint8Array - // — all BufferHeader-backed. Their bytes are not necessarily - // INLINE, though: a registered view (a DataView, a `Buffer.from(ab)` - // window, a subarray) keeps a construction-time copy that only - // registry-routed writes refresh, so a multi-byte typed array over - // the same backing decoded as pre-write bytes. Resolve the window - // the way every other native-span consumer does (#6515). - let buf = ptr_usize as *const BufferHeader; - let len = (*buf).length as usize; - std::slice::from_raw_parts(crate::buffer::resolve_span_data_ptr(buf), len) - } else { - // Plain arrays, plain objects, strings — reject like Node. - throw_invalid_decode_input(); + let value = crate::value::js_nanbox_pointer(ptr_usize as i64); + match crate::buffer::bytes::pin(value) { + Ok(pin) => decode_bytes( + unsafe { std::slice::from_raw_parts(pin.as_ptr(), pin.len()) }, + encoding, + fatal, + label, + ), + Err( + crate::buffer::bytes::NotBytes::UnstableForeign + | crate::buffer::bytes::NotBytes::Frozen, + ) => { + // Foreign engines have no retain protocol. Copy in a borrow scope + // before decoding can allocate the returned JavaScript string. + let input = crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope).map(<[u8]>::to_vec) + }) + .unwrap_or_else(|_| throw_invalid_decode_input()); + decode_bytes(&input, encoding, fatal, label) } - }; - - decode_bytes(bytes, encoding, fatal, label) + Err(_) => throw_invalid_decode_input(), + } } fn throw_invalid_decode_input() -> ! { diff --git a/crates/perry-runtime/src/thread/clone_read.rs b/crates/perry-runtime/src/thread/clone_read.rs index add2efd08f..85de3684a5 100644 --- a/crates/perry-runtime/src/thread/clone_read.rs +++ b/crates/perry-runtime/src/thread/clone_read.rs @@ -466,27 +466,22 @@ unsafe fn array_buffer(bytes: &[u8]) -> u64 { } unsafe fn new_buffer(bytes: &[u8]) -> *mut crate::buffer::BufferHeader { - let len = u32::try_from(bytes.len()).expect("cloned buffer exceeds u32::MAX bytes"); - let buffer = crate::buffer::buffer_alloc(len); - (*buffer).length = len; - if !bytes.is_empty() { - ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buffer), - bytes.len(), - ); - } - buffer + JSValue::from_bits( + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Buffer, bytes).to_bits(), + ) + .as_pointer::() + .cast_mut() } unsafe fn owned_typed_array(kind: u8, length: u32, bytes: &[u8]) -> u64 { let ta = crate::typedarray::typed_array_alloc(kind, length); if !bytes.is_empty() { - ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::typedarray::data_ptr_mut(ta), - bytes.len(), - ); + crate::buffer::bytes::no_gc(|scope| { + let value = crate::value::js_nanbox_pointer(ta as i64); + let destination = crate::buffer::bytes::bytes_mut(value, scope) + .expect("fresh typed array must expose its bytes"); + destination[..bytes.len()].copy_from_slice(bytes); + }); } JSValue::pointer(ta as *const u8).bits() } diff --git a/crates/perry-runtime/src/thread/clone_write.rs b/crates/perry-runtime/src/thread/clone_write.rs index 84cf4e7591..8f8ff45554 100644 --- a/crates/perry-runtime/src/thread/clone_write.rs +++ b/crates/perry-runtime/src/thread/clone_write.rs @@ -569,12 +569,13 @@ impl Writer<'_> { if let Err(seen) = self.begin(Some(addr)) { return seen; } - return SerializedValue::Uint8Array(view_bytes( - crate::buffer::buffer_data(header), - length as usize, - )); + return SerializedValue::Uint8Array(crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(addr as i64), scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + })); } - return self.visible_bytes_view(addr, kind, crate::buffer::buffer_data(header), length); + return self.visible_bytes_view(addr, kind, length); } if let Err(seen) = self.begin(Some(addr)) { return seen; @@ -600,7 +601,7 @@ impl Writer<'_> { } if self.mode == CloneMode::Thread { let bytes = length * crate::typedarray::elem_size_for_kind(kind) as u32; - return self.visible_bytes_view(addr, kind, crate::typedarray::data_ptr(ta), bytes); + return self.visible_bytes_view(addr, kind, bytes); } if let Err(seen) = self.begin(Some(addr)) { return seen; @@ -624,7 +625,6 @@ impl Writer<'_> { &mut self, addr: usize, kind: u8, - data: *const u8, byte_len: u32, ) -> SerializedValue { if let Err(seen) = self.begin(Some(addr)) { @@ -638,9 +638,12 @@ impl Writer<'_> { }; SerializedValue::View { kind, - buffer: Box::new(SerializedValue::ArrayBuffer(view_bytes( - data, - byte_len as usize, + buffer: Box::new(SerializedValue::ArrayBuffer(crate::buffer::bytes::no_gc( + |scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(addr as i64), scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }, ))), byte_offset: 0, length: byte_len / elem, @@ -665,17 +668,11 @@ impl Writer<'_> { crate::buffer::TransferredBacking::pending(backing, length), ); } - let bytes = if let Some(kind) = crate::typedarray::lookup_typed_array_kind(backing) { - let ta = backing as *const crate::typedarray::TypedArrayHeader; - let len = (*ta).length as usize * crate::typedarray::elem_size_for_kind(kind); - view_bytes(crate::typedarray::data_ptr(ta), len) - } else { - let header = backing as *const crate::buffer::BufferHeader; - view_bytes( - crate::buffer::buffer_data(header), - (*header).length as usize, - ) - }; + let bytes = crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(backing as i64), scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }); SerializedValue::ArrayBuffer(bytes) } @@ -769,10 +766,3 @@ unsafe fn string_bytes(ptr: *const crate::string::StringHeader) -> Vec { let data = (ptr as *const u8).add(std::mem::size_of::()); std::slice::from_raw_parts(data, len).to_vec() } - -unsafe fn view_bytes(data: *const u8, len: usize) -> Vec { - if len == 0 || data.is_null() { - return Vec::new(); - } - std::slice::from_raw_parts(data, len).to_vec() -} diff --git a/crates/perry-runtime/src/tls.rs b/crates/perry-runtime/src/tls.rs index 21a9b954fb..c92ff63e64 100644 --- a/crates/perry-runtime/src/tls.rs +++ b/crates/perry-runtime/src/tls.rs @@ -298,12 +298,11 @@ fn value_to_bytes(value: f64) -> Option> { if js.is_pointer() && crate::buffer::is_any_array_buffer(js.as_pointer::() as usize) { return None; } - let mut len = 0u32; - let data = unsafe { crate::buffer::js_value_buffer_or_typedarray_data(value, &mut len) }; - if data.is_null() { - return None; - } - Some(unsafe { std::slice::from_raw_parts(data, len as usize) }.to_vec()) + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) } fn value_to_utf8(value: f64) -> Option { diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index 9e22401a91..e94fc45228 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -275,18 +275,6 @@ pub const TA_CACHE_NEGATIVE: u64 = 0xFF; pub static PERRY_TA_KIND_CACHE: [AtomicU64; TA_KIND_CACHE_SLOTS] = [const { AtomicU64::new(0) }; TA_KIND_CACHE_SLOTS]; -// The generic kind cache deliberately uses the exact slot formula duplicated -// by codegen. Large, equal-sized ECS columns can therefore share the same low -// address bits and continually evict one another. Whole-loop admission needs -// the stronger, persistent fact "this exact address is an owning Uint32Array", -// so keep a separate direct cache whose index folds higher address bits too. -// A hit is safe until unregister: a TypedArray header's kind and owning/view -// storage class never change during its lifetime, and unregister clears both -// caches before an address can be reused. -const INLINE_OWNING_U32_CACHE_SLOTS: usize = 64; -static INLINE_OWNING_U32_CACHE: [AtomicU64; INLINE_OWNING_U32_CACHE_SLOTS] = - [const { AtomicU64::new(0) }; INLINE_OWNING_U32_CACHE_SLOTS]; - /// The [`PERRY_TA_KIND_CACHE`] tag for the registered typed array at `ta`: /// its kind, plus [`TA_CACHE_EXTERNAL_STORAGE`] when its elements do not /// follow the header (#10516). @@ -309,7 +297,6 @@ unsafe fn kind_cache_tag(ta: *const TypedArrayHeader, kind: u8) -> u64 { pub(crate) fn note_external_storage(ta: *mut TypedArrayHeader) { unsafe { (*ta).storage = TA_STORAGE_EXTERNAL }; ta_kind_cache_invalidate(ta as usize); - inline_owning_u32_cache_invalidate(ta as usize); } #[inline] @@ -336,50 +323,14 @@ fn ta_kind_cache_invalidate(addr: usize) { } } -#[inline] -fn inline_owning_u32_cache_slot(addr: usize) -> usize { - let word = addr >> 3; - let mixed = word ^ (word >> 6) ^ (word >> 12); - mixed & (INLINE_OWNING_U32_CACHE_SLOTS - 1) -} - -#[inline] -fn inline_owning_u32_cache_get(addr: usize) -> bool { - INLINE_OWNING_U32_CACHE[inline_owning_u32_cache_slot(addr)].load(Ordering::Relaxed) - == addr as u64 -} - -#[inline] -fn inline_owning_u32_cache_store(addr: usize) { - INLINE_OWNING_U32_CACHE[inline_owning_u32_cache_slot(addr)] - .store(addr as u64, Ordering::Relaxed); -} - -#[inline] -fn inline_owning_u32_cache_invalidate(addr: usize) { - let slot = inline_owning_u32_cache_slot(addr); - if INLINE_OWNING_U32_CACHE[slot].load(Ordering::Relaxed) == addr as u64 { - INLINE_OWNING_U32_CACHE[slot].store(0, Ordering::Relaxed); - } -} - -/// Forget process-wide admission facts before a retiring arena block can be -/// reused by another thread (#11463). The per-thread registry disappears at -/// thread exit, but these two atomic caches otherwise outlive its allocations. -/// Do not touch TLS here: its destruction order is not guaranteed. +/// Drop exported kind-cache entries before retiring arena memory is reused. +/// No separate whole-loop admission cache remains. pub(crate) fn invalidate_caches_in_range(start: usize, end: usize) { - for (cache, shift) in [ - (&PERRY_TA_KIND_CACHE[..], 8), - (&INLINE_OWNING_U32_CACHE[..], 0), - ] { - for slot in cache { - let entry = slot.load(Ordering::Relaxed); - let address = (entry >> shift) as usize; - if (start..end).contains(&address) { - // A different thread can replace a colliding slot while we - // inspect it. Clear only the entry from the retiring block. - let _ = slot.compare_exchange(entry, 0, Ordering::Relaxed, Ordering::Relaxed); - } + for slot in &PERRY_TA_KIND_CACHE { + let entry = slot.load(Ordering::Relaxed); + let address = (entry >> 8) as usize; + if (start..end).contains(&address) { + let _ = slot.compare_exchange(entry, 0, Ordering::Relaxed, Ordering::Relaxed); } } } @@ -418,7 +369,6 @@ pub fn register_typed_array(ptr: *const TypedArrayHeader, kind: u8) { pub fn unregister_typed_array(ptr: *const TypedArrayHeader) { let owner = ptr as usize; ta_kind_cache_invalidate(owner); - inline_owning_u32_cache_invalidate(owner); crate::typedarray_view::clear_view_meta(owner); crate::typedarray_props::typed_array_clear_own_props(owner); crate::typedarray_props::typed_array_clear_no_extend(owner); @@ -671,13 +621,8 @@ pub extern "C" fn js_typed_array_masked_window_data_ptr(receiver: f64) -> i64 { data_ptr(addr as *const TypedArrayHeader) as i64 } -/// One-time loop admission primitive for erased ECS component columns. Return -/// the stable owning-header address only for an exact inline `Uint32Array`. -/// Use the admission-specific address cache first, then consult the -/// authoritative registry on a miss. The generic direct-mapped kind cache is -/// intentionally not authority here: sibling columns can collide there, -/// which is harmless for individual accesses but must not make a whole-loop -/// proof spuriously fail forever. +/// Whole-loop admission reads the receiver header as the authority. Views +/// and external stores cannot satisfy the owning-inline proof. #[inline] pub(crate) fn inline_u32_addr(receiver: f64) -> usize { let value = crate::value::JSValue::from_bits(receiver.to_bits()); @@ -685,17 +630,25 @@ pub(crate) fn inline_u32_addr(receiver: f64) -> usize { return 0; } let addr = value.as_pointer::() as usize; - if inline_owning_u32_cache_get(addr) { + if !crate::value::addr_class::is_plausible_heap_addr(addr) { + return 0; + } + #[cfg(test)] + if crate::buffer::bytes::b4_sabotage("u32_admission") { return addr; } - if lookup_typed_array_kind(addr) != Some(KIND_UINT32) - || crate::native_arena::is_native_typed_view(addr as *const TypedArrayHeader) - || crate::typedarray_view::view_meta_of(addr).is_some() - { - return 0; + unsafe { + let h = crate::gc::header_from_trusted_user_ptr(addr as *const u8); + let ta = addr as *const TypedArrayHeader; + if (*h).obj_type == crate::gc::GC_TYPE_TYPED_ARRAY + && (*ta).kind == KIND_UINT32 + && (*ta).storage == TA_STORAGE_INLINE + { + addr + } else { + 0 + } } - inline_owning_u32_cache_store(addr); - addr } #[inline] @@ -1351,23 +1304,23 @@ mod tests { use super::*; #[test] - fn owning_u32_admission_cache_skips_registry_and_invalidates() { + fn owning_u32_admission_reads_current_header() { let ta = typed_array_alloc(KIND_UINT32, 16); let boxed = crate::value::js_nanbox_pointer(ta as i64); - - let before = test_typed_array_registry_probe_count(); assert_eq!(inline_u32_addr(boxed), ta as usize); - let primed = test_typed_array_registry_probe_count(); - assert_eq!(primed, before + 1); - assert_eq!(inline_u32_addr(boxed), ta as usize); - assert_eq!(test_typed_array_registry_probe_count(), primed); - - // Unregistering (what the finalizer does at death) must drop the - // admission, so the next access re-derives it from the header. - unregister_typed_array(ta); - assert!(!inline_owning_u32_cache_get(ta as usize)); + unsafe { + (*ta).storage = TA_STORAGE_EXTERNAL; + } + assert_eq!(inline_u32_addr(boxed), 0); + unsafe { + (*ta).storage = TA_STORAGE_INLINE; + (*ta).kind = KIND_INT32; + } + assert_eq!(inline_u32_addr(boxed), 0); + unsafe { + (*ta).kind = KIND_UINT32; + } assert_eq!(inline_u32_addr(boxed), ta as usize); - assert_eq!(test_typed_array_registry_probe_count(), primed + 1); } #[test] diff --git a/crates/perry-runtime/src/typedarray/thread_exit_tests.rs b/crates/perry-runtime/src/typedarray/thread_exit_tests.rs index 1f79719ebc..87ce190c61 100644 --- a/crates/perry-runtime/src/typedarray/thread_exit_tests.rs +++ b/crates/perry-runtime/src/typedarray/thread_exit_tests.rs @@ -1,4 +1,4 @@ -//! Process-wide admission caches must not outlive a worker's arena. +//! The exported kind cache must not outlive a worker's arena. use super::*; #[test] @@ -18,50 +18,24 @@ fn thread_exit_invalidates_kind_cache() { assert_ne!(ta_kind_cache_get(address), Some(Some(KIND_UINT8))); } -#[test] -fn thread_exit_invalidates_owning_u32_cache() { - let address = std::thread::spawn(|| { - let array = typed_array_alloc(KIND_UINT32, 16); - let address = array as usize; - assert_eq!( - inline_u32_addr(crate::value::js_nanbox_pointer(array as i64)), - address - ); - assert!(inline_owning_u32_cache_get(address)); - address - }) - .join() - .unwrap(); - - // Do not dereference the retired address: only inspect the admission - // cache that would otherwise let a generated loop use it as a Uint32Array. - assert!(!inline_owning_u32_cache_get(address)); -} - #[test] fn retiring_range_preserves_live_cache_entries() { let retired = typed_array_alloc(KIND_UINT32, 16) as usize; - // Both caches are direct-mapped. Keep the control in different slots so + // Keep the control in a different exported cache slot so // priming it cannot evict the entry this test intends to invalidate. let live = (0..64) .map(|_| typed_array_alloc(KIND_UINT32, 16) as usize) - .find(|&address| { - ta_kind_cache_slot(address) != ta_kind_cache_slot(retired) - && inline_owning_u32_cache_slot(address) != inline_owning_u32_cache_slot(retired) - }) - .expect("distinct admission-cache slots"); + .find(|&address| ta_kind_cache_slot(address) != ta_kind_cache_slot(retired)) + .expect("distinct kind-cache slots"); for address in [retired, live] { assert_eq!( inline_u32_addr(crate::value::js_nanbox_pointer(address as i64)), address ); assert_eq!(ta_kind_cache_get(address), Some(Some(KIND_UINT32))); - assert!(inline_owning_u32_cache_get(address)); } invalidate_caches_in_range(retired, retired + 1); assert_eq!(ta_kind_cache_get(retired), None); - assert!(!inline_owning_u32_cache_get(retired)); assert_eq!(ta_kind_cache_get(live), Some(Some(KIND_UINT32))); - assert!(inline_owning_u32_cache_get(live)); } diff --git a/crates/perry-runtime/src/url/node_compat.rs b/crates/perry-runtime/src/url/node_compat.rs index 0f1c7e9c42..fa999b7b13 100644 --- a/crates/perry-runtime/src/url/node_compat.rs +++ b/crates/perry-runtime/src/url/node_compat.rs @@ -471,18 +471,7 @@ pub extern "C" fn js_url_file_url_to_path(url_f64: f64, options_f64: f64) -> f64 pub extern "C" fn js_url_file_url_to_path_buffer(url_f64: f64, options_f64: f64) -> f64 { let windows = options_windows_flag(options_f64); let bytes = file_url_to_path_bytes(url_f64, windows); - let buf = crate::buffer::buffer_alloc(bytes.len() as u32); - unsafe { - (*buf).length = bytes.len() as u32; - if !bytes.is_empty() { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - crate::buffer::buffer_data_mut(buf), - bytes.len(), - ); - } - } - crate::value::js_nanbox_pointer(buf as i64) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::Buffer, &bytes) } /// Percent-encode a file-URL path component (after separator normalization), diff --git a/crates/perry-runtime/src/wasi.rs b/crates/perry-runtime/src/wasi.rs index b2b50e2807..eff20809e4 100644 --- a/crates/perry-runtime/src/wasi.rs +++ b/crates/perry-runtime/src/wasi.rs @@ -1071,11 +1071,18 @@ fn write_u32(buffer: *mut crate::buffer::BufferHeader, offset: f64, value: u32) if offset.checked_add(4).is_none_or(|end| end > len) { return false; } - unsafe { - let target = crate::buffer::buffer_data_mut(buffer).add(offset); - std::ptr::copy_nonoverlapping(value.to_le_bytes().as_ptr(), target, 4); - } - true + crate::buffer::bytes::no_gc(|scope| unsafe { + let Ok(data) = + crate::buffer::bytes::bytes_mut(crate::value::js_nanbox_pointer(buffer as i64), scope) + else { + return false; + }; + let Some(target) = data.get_mut(offset..offset + 4) else { + return false; + }; + target.copy_from_slice(&value.to_le_bytes()); + true + }) } fn write_u64(buffer: *mut crate::buffer::BufferHeader, offset: f64, value: u64) -> bool { @@ -1086,11 +1093,18 @@ fn write_u64(buffer: *mut crate::buffer::BufferHeader, offset: f64, value: u64) if offset.checked_add(8).is_none_or(|end| end > len) { return false; } - unsafe { - let target = crate::buffer::buffer_data_mut(buffer).add(offset); - std::ptr::copy_nonoverlapping(value.to_le_bytes().as_ptr(), target, 8); - } - true + crate::buffer::bytes::no_gc(|scope| unsafe { + let Ok(data) = + crate::buffer::bytes::bytes_mut(crate::value::js_nanbox_pointer(buffer as i64), scope) + else { + return false; + }; + let Some(target) = data.get_mut(offset..offset + 8) else { + return false; + }; + target.copy_from_slice(&value.to_le_bytes()); + true + }) } fn snapshot_values(import: f64, key: &[u8]) -> *mut crate::array::ArrayHeader { @@ -1149,10 +1163,22 @@ fn snapshot_get(import: f64, key: &[u8], pointers: f64, strings: f64) -> f64 { if !write_u32(buffer, pointers as f64, strings as u32) { return 28.0; } - unsafe { - let target = crate::buffer::buffer_data_mut(buffer).add(strings); - std::ptr::copy_nonoverlapping(bytes.as_ptr(), target, bytes.len()); - *target.add(bytes.len()) = 0; + let written = crate::buffer::bytes::no_gc(|scope| unsafe { + let Ok(data) = crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(buffer as i64), + scope, + ) else { + return false; + }; + let Some(target) = data.get_mut(strings..strings + bytes.len() + 1) else { + return false; + }; + target[..bytes.len()].copy_from_slice(&bytes); + target[bytes.len()] = 0; + true + }); + if !written { + return 28.0; } pointers += 4; strings += bytes.len() + 1; @@ -1279,11 +1305,23 @@ pub extern "C" fn js_wasi_import_stub( let seed = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(1, |time| time.as_nanos() as u64); - unsafe { - let target = crate::buffer::buffer_data_mut(buffer).add(offset); - for index in 0..len { - *target.add(index) = (seed >> ((index % 8) * 8)) as u8; + let written = crate::buffer::bytes::no_gc(|scope| unsafe { + let Ok(data) = crate::buffer::bytes::bytes_mut( + crate::value::js_nanbox_pointer(buffer as i64), + scope, + ) else { + return false; + }; + let Some(target) = data.get_mut(offset..offset + len) else { + return false; + }; + for (index, byte) in target.iter_mut().enumerate() { + *byte = (seed >> ((index % 8) * 8)) as u8; } + true + }); + if !written { + return 28.0; } } 0.0 diff --git a/crates/perry-runtime/src/webassembly.rs b/crates/perry-runtime/src/webassembly.rs index b58a284799..1883abc825 100644 --- a/crates/perry-runtime/src/webassembly.rs +++ b/crates/perry-runtime/src/webassembly.rs @@ -83,37 +83,20 @@ fn unbox_pointer(v: f64) -> *mut c_void { /// source. Accepts both `Uint8Array` (TypedArrayHeader, kind=KIND_UINT8) and /// raw ArrayBuffer-style `BufferHeader`. Returns `None` if the JSValue isn't /// a recognised byte buffer. -fn extract_bytes(jsval: f64) -> Option<(*const u8, usize)> { - let ptr = unbox_pointer(jsval); - if ptr.is_null() { - return None; - } - let addr = ptr as usize; - +fn extract_bytes(jsval: f64) -> Option> { + let addr = unbox_pointer(jsval) as usize; if let Some(kind) = crate::typedarray::lookup_typed_array_kind(addr) { - // KIND_UINT8 = 0 per typedarray.rs (Int8=0,Uint8=1 — verify via - // elem_size_for_kind which returns 1 for both byte kinds anyway). - // We accept any single-byte kind for bytes input — wasmi treats it - // as raw u8. - if crate::typedarray::elem_size_for_kind(kind) == 1 { - let header = addr as *const crate::typedarray::TypedArrayHeader; - if let Some(bytes) = unsafe { crate::typedarray::typed_array_bytes(header) } { - return Some((bytes.as_ptr(), bytes.len())); - } + if crate::typedarray::elem_size_for_kind(kind) != 1 { + return None; } + } else if !crate::buffer::is_registered_buffer(addr) { + return None; } - - if crate::buffer::is_registered_buffer(addr) - || crate::buffer::is_array_buffer(addr) - || crate::buffer::is_uint8array_buffer(addr) - { - let header = addr as *const crate::buffer::BufferHeader; - let len = unsafe { (*header).length as usize }; - let data = crate::buffer::buffer_data(header as *const crate::buffer::BufferHeader); - return Some((data, len)); - } - - None + crate::buffer::bytes::no_gc(|scope| { + crate::buffer::bytes::bytes(crate::value::js_nanbox_pointer(addr as i64), scope) + .ok() + .map(<[u8]>::to_vec) + }) } /// Extract a UTF-8 byte view of a JS string. Accepts StringHeader-backed @@ -201,11 +184,12 @@ fn rejected_promise_value(reason: f64) -> f64 { /// delivery: `new WebAssembly.Module` throws synchronously, `compile` / /// `instantiate` reject their promise. fn module_new_value(bytes_jsval: f64) -> Result { - let Some((ptr, len)) = extract_bytes(bytes_jsval) else { + let Some(bytes) = extract_bytes(bytes_jsval) else { return Err(wasm_type_error_value( "WebAssembly.Module: argument must be a Uint8Array or ArrayBuffer", )); }; + let (ptr, len) = (bytes.as_ptr(), bytes.len()); let mut err: *mut c_char = std::ptr::null_mut(); let module = unsafe { perry_wasm_host_module_new(ptr, len, &mut err) }; if module.is_null() { @@ -268,18 +252,18 @@ fn array_value(arr: *mut crate::array::ArrayHeader) -> f64 { } fn array_buffer_from_bytes(data: *const u8, len: usize) -> f64 { - let len_i32 = len.min(i32::MAX as usize) as i32; - let buf = crate::buffer::js_array_buffer_new(len_i32); - if !buf.is_null() && !data.is_null() && len_i32 > 0 { - unsafe { - std::ptr::copy_nonoverlapping( - data, - crate::buffer::buffer_data_mut(buf), - len_i32 as usize, - ); - } + let len = len.min(i32::MAX as usize); + if data.is_null() { + return crate::buffer::bytes::new_bytes( + crate::buffer::bytes::Brand::ArrayBuffer, + len, + crate::buffer::bytes::Init::Zero, + ) + .0; } - crate::value::js_nanbox_pointer(buf as i64) + crate::buffer::bytes::from_slice(crate::buffer::bytes::Brand::ArrayBuffer, unsafe { + std::slice::from_raw_parts(data, len) + }) } fn make_module_object(module: *mut c_void) -> f64 { @@ -380,9 +364,10 @@ fn empty_array_value() -> f64 { /// `WebAssembly.validate(bytes)` — returns boolean. #[no_mangle] pub extern "C" fn js_webassembly_validate(bytes_jsval: f64) -> f64 { - let Some((ptr, len)) = extract_bytes(bytes_jsval) else { + let Some(bytes) = extract_bytes(bytes_jsval) else { return nanbox_bool(false); }; + let (ptr, len) = (bytes.as_ptr(), bytes.len()); let ok = unsafe { perry_wasm_host_validate(ptr, len) } != 0; nanbox_bool(ok) } @@ -1539,11 +1524,12 @@ pub extern "C" fn js_webassembly_instantiate(bytes_jsval: f64, imports_jsval: f6 nanbox_undefined(), )); } - let Some((ptr, len)) = extract_bytes(bytes_jsval) else { + let Some(bytes) = extract_bytes(bytes_jsval) else { return rejected_promise_value(wasm_type_error_value( "WebAssembly.instantiate: argument must be a Uint8Array or ArrayBuffer", )); }; + let (ptr, len) = (bytes.as_ptr(), bytes.len()); let mut err: *mut c_char = std::ptr::null_mut(); let module = unsafe { perry_wasm_host_module_new(ptr, len, &mut err) }; if module.is_null() { diff --git a/crates/perry-stdlib/src/crypto.rs b/crates/perry-stdlib/src/crypto.rs index b87b565266..7b41e10bfd 100644 --- a/crates/perry-stdlib/src/crypto.rs +++ b/crates/perry-stdlib/src/crypto.rs @@ -32,3 +32,6 @@ pub use self::{ certificate::*, cipher::*, ecdh::*, handles::*, hash::*, hash_chain::*, hash_handles::*, kdf::*, keys::*, prime::*, random::*, sign::*, x509::*, }; + +#[cfg(test)] +mod bytes_contract_tests; diff --git a/crates/perry-stdlib/src/crypto/bytes_contract_tests.rs b/crates/perry-stdlib/src/crypto/bytes_contract_tests.rs new file mode 100644 index 0000000000..4310945024 --- /dev/null +++ b/crates/perry-stdlib/src/crypto/bytes_contract_tests.rs @@ -0,0 +1,85 @@ +use super::*; +use perry_runtime::buffer::{ + self, + bytes::{self, Brand}, +}; + +fn value(ptr: *const T) -> f64 { + perry_runtime::value::js_nanbox_pointer(ptr as i64) +} + +#[test] +fn byte_outputs_and_digest_inputs_use_the_visible_window() { + // Both sides of the design's 256-byte test boundary. B3 supplies the + // placement rule; these consumers must work independently of that rule. + for len in [0, 1, 255, 256, 257, 1024 * 1024] { + let input: Vec = (0..len).map(|n| (n % 251) as u8).collect(); + let ptr = unsafe { alloc_buffer_from_slice(&input) }; + bytes::no_gc(|scope| assert_eq!(bytes::bytes(value(ptr), scope).unwrap(), input)); + } + let source = bytes::from_slice(Brand::Buffer, b"prefix-payload-suffix"); + let ptr = JSValue::from_bits(source.to_bits()).as_pointer::(); + let view = buffer::js_buffer_slice(ptr, 7, 14); + let result = unsafe { js_crypto_sha256_bytes(view as i64) }; + let expected = Sha256::digest(b"payload"); + bytes::no_gc(|scope| { + assert_eq!( + bytes::bytes(value(result), scope).unwrap(), + expected.as_slice() + ) + }); +} + +#[test] +fn random_fill_preserves_bytes_outside_the_view_and_range() { + let source = bytes::from_slice(Brand::Buffer, &[0x25; 32]); + let ptr = JSValue::from_bits(source.to_bits()).as_pointer::(); + let view = buffer::js_buffer_slice(ptr, 8, 24); + assert_eq!( + js_crypto_random_fill_sync(value(view), 4.0, 8.0).to_bits(), + value(view).to_bits() + ); + bytes::no_gc(|scope| { + let data = bytes::bytes(source, scope).unwrap(); + assert_eq!(&data[..12], &[0x25; 12]); + assert_eq!(&data[20..], &[0x25; 12]); + }); + for len in [0, 255, 257, 4096] { + let ptr = js_crypto_random_bytes_buffer(len as f64); + bytes::no_gc(|scope| assert_eq!(bytes::bytes(value(ptr), scope).unwrap().len(), len)); + } +} + +#[test] +fn each_b2c_sabotage_turns_its_consumer_witness_red() { + for (fault, witness) in [ + ( + "crypto_output", + "byte_outputs_and_digest_inputs_use_the_visible_window", + ), + ( + "crypto_borrow", + "byte_outputs_and_digest_inputs_use_the_visible_window", + ), + ( + "random_fill_range", + "random_fill_preserves_bytes_outside_the_view_and_range", + ), + ] { + let child = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + &format!("crypto::bytes_contract_tests::{witness}"), + "--nocapture", + ]) + .env("PERRY_B2C_SABOTAGE", fault) + .output() + .unwrap(); + assert!(String::from_utf8_lossy(&child.stdout).contains("running 1 test")); + assert!( + !child.status.success(), + "sabotage {fault} left {witness} green" + ); + eprintln!("B2c sabotage {fault}: RED"); + } +} diff --git a/crates/perry-stdlib/src/crypto/kdf.rs b/crates/perry-stdlib/src/crypto/kdf.rs index 776e617ce3..ece7ce0821 100644 --- a/crates/perry-stdlib/src/crypto/kdf.rs +++ b/crates/perry-stdlib/src/crypto/kdf.rs @@ -170,7 +170,7 @@ pub unsafe extern "C" fn js_crypto_hkdf_bytes_alg( let info = bytes_from_ptr(info_ptr); let len = keylen as usize; if len == 0 || len > 8160 { - return perry_runtime::buffer::buffer_alloc(0); + return alloc_buffer_from_slice(&[]); } let mut out = vec![0u8; len]; let ok = match alg.as_str() { @@ -193,7 +193,7 @@ pub unsafe extern "C" fn js_crypto_hkdf_bytes_alg( if ok { alloc_buffer_from_slice(&out) } else { - perry_runtime::buffer::buffer_alloc(0) + alloc_buffer_from_slice(&[]) } } @@ -314,8 +314,12 @@ unsafe fn argon2_value_bytes(bits: u64) -> Option> { return None; } if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *const perry_runtime::typedarray::TypedArrayHeader; - return perry_runtime::typedarray::typed_array_bytes(ta).map(|bytes| bytes.to_vec()); + return perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }); } if perry_runtime::buffer::is_registered_buffer(addr) { return Some(bytes_from_ptr(addr as i64)); @@ -495,19 +499,13 @@ unsafe fn validate_timing_safe_equal_buffer_source(value: f64, arg_name: &str) - bits as usize } }; - if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { - if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes( - addr as *const perry_runtime::typedarray::TypedArrayHeader, - ) { - return bytes.to_vec(); - } - } - if perry_runtime::buffer::is_registered_buffer(addr) { - let buf = addr as *const perry_runtime::buffer::BufferHeader; - let len = (*buf).length as usize; - let data = - perry_runtime::buffer::buffer_data(buf as *const perry_runtime::buffer::BufferHeader); - return std::slice::from_raw_parts(data, len).to_vec(); + if let Some(bytes) = perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) { + return bytes; } let message = format!( "The \"{}\" argument must be an instance of ArrayBuffer, Buffer, TypedArray, or DataView.", diff --git a/crates/perry-stdlib/src/crypto/random.rs b/crates/perry-stdlib/src/crypto/random.rs index 19055e9f7d..62f2e0273e 100644 --- a/crates/perry-stdlib/src/crypto/random.rs +++ b/crates/perry-stdlib/src/crypto/random.rs @@ -54,18 +54,17 @@ pub extern "C" fn js_crypto_random_bytes_buffer( size: f64, ) -> *mut perry_runtime::buffer::BufferHeader { let size = validate_random_bytes_size(size); - if size == 0 { - return perry_runtime::buffer::buffer_alloc(0); - } - - let buf = perry_runtime::buffer::buffer_alloc(size as u32); + let (value, pin) = perry_runtime::buffer::bytes::new_bytes( + perry_runtime::buffer::bytes::Brand::Buffer, + size, + perry_runtime::buffer::bytes::Init::Uninit, + ); unsafe { - (*buf).length = size as u32; - let data = perry_runtime::buffer::buffer_data_mut(buf); - let bytes = std::slice::from_raw_parts_mut(data, size); - rand::rng().fill_bytes(bytes); + rand::rng().fill_bytes(std::slice::from_raw_parts_mut(pin.as_mut_ptr(), pin.len())); } - buf + JSValue::from_bits(value.to_bits()) + .as_pointer::() + .cast_mut() } /// `crypto.randomBytes(size, callback)` — callback form. @@ -584,47 +583,46 @@ pub extern "C" fn js_crypto_random_fill_sync( offset_bits: f64, size_bits: f64, ) -> f64 { - unsafe { - let raw = raw_addr_from_value(buf_bits); - // TypedArrayHeader path (Uint8Array, Uint32Array, Float32Array, …). - if perry_runtime::typedarray::lookup_typed_array_kind(raw).is_some() { - let ta = raw as *mut perry_runtime::typedarray::TypedArrayHeader; - if let Some(data) = perry_runtime::typedarray::typed_array_bytes_mut(ta) { - let elem_size = (*ta).elem_size as usize; - let len = if elem_size == 0 { - 0 - } else { - data.len() / elem_size - }; - let (start_elem, count_elem) = - validate_random_fill_range(len, offset_bits, size_bits); - let start = start_elem.saturating_mul(elem_size); - let end = start - .saturating_add(count_elem.saturating_mul(elem_size)) - .min(data.len()); - if end > start { - rand::rng().fill_bytes(&mut data[start..end]); - } - return buf_bits; - } - throw_invalid_random_fill_buffer(buf_bits); - } - // BufferHeader / Uint8Array path. - if perry_runtime::buffer::is_registered_buffer(raw) { - let buf = raw as *mut perry_runtime::buffer::BufferHeader; - let total = (*buf).length as usize; - let (start, count) = validate_random_fill_range(total, offset_bits, size_bits); - if count > 0 { - let data = perry_runtime::buffer::buffer_data_mut(buf); - let slice = std::slice::from_raw_parts_mut(data.add(start), count); - rand::rng().fill_bytes(slice); - } - // Hand back the same NaN-boxed value the caller passed. - return buf_bits; + let raw = raw_addr_from_value(buf_bits); + let elem_size = if let Some(kind) = perry_runtime::typedarray::lookup_typed_array_kind(raw) { + perry_runtime::typedarray::elem_size_for_kind(kind).max(1) + } else if perry_runtime::buffer::is_registered_buffer(raw) { + 1 + } else { + throw_invalid_random_fill_buffer(buf_bits); + }; + let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); + let byte_len = perry_runtime::buffer::bytes::no_gc(|scope| { + perry_runtime::buffer::bytes::bytes(value, scope).map(<[u8]>::len) + }) + .unwrap_or_else(|_| throw_invalid_random_fill_buffer(buf_bits)); + let (start_elem, count_elem) = + validate_random_fill_range(byte_len / elem_size, offset_bits, size_bits); + let start = start_elem.saturating_mul(elem_size); + let count = count_elem + .saturating_mul(elem_size) + .min(byte_len.saturating_sub(start)); + #[cfg(test)] + let start = if b2c_sabotage("random_fill_range") { + 0 + } else { + start + }; + // RNG work neither allocates in the JS heap nor calls JS. A scoped borrow + // also supports foreign stores without requiring a retain protocol. + perry_runtime::buffer::bytes::no_gc(|scope| unsafe { + let data = perry_runtime::buffer::bytes::bytes_mut(value, scope)?; + rand::rng().fill_bytes(&mut data[start..start + count]); + #[cfg(test)] + if b2c_sabotage("random_fill_range") && count > 0 { + // Make the wrong-window witness deterministic rather than depend + // on the RNG changing the sentinel byte by chance. + data[start] = 0xff; } - } - - throw_invalid_random_fill_buffer(buf_bits); + Ok::<_, perry_runtime::buffer::bytes::NotBytes>(()) + }) + .unwrap_or_else(|_| throw_invalid_random_fill_buffer(buf_bits)); + buf_bits } fn raw_addr_from_value(value: f64) -> usize { diff --git a/crates/perry-stdlib/src/crypto/sign.rs b/crates/perry-stdlib/src/crypto/sign.rs index 0719c14c9b..b927f8a2e0 100644 --- a/crates/perry-stdlib/src/crypto/sign.rs +++ b/crates/perry-stdlib/src/crypto/sign.rs @@ -48,7 +48,7 @@ pub unsafe extern "C" fn js_crypto_hmac_sha256_bytes( let data = bytes_from_ptr(data_ptr); let mut mac = match HmacSha256::new_from_slice(&key) { Ok(m) => m, - Err(_) => return perry_runtime::buffer::buffer_alloc(0), + Err(_) => return alloc_buffer_from_slice(&[]), }; mac.update(&data); let digest = mac.finalize().into_bytes(); diff --git a/crates/perry-stdlib/src/crypto/util.rs b/crates/perry-stdlib/src/crypto/util.rs index de22559015..ac50a49544 100644 --- a/crates/perry-stdlib/src/crypto/util.rs +++ b/crates/perry-stdlib/src/crypto/util.rs @@ -47,27 +47,39 @@ pub(super) use sha2::{Digest as Sha256Digest, Sha224, Sha256, Sha384, Sha512, Sh // as of sha3 0.12 (RustCrypto/hashes#869). pub(super) use shake::{ExtendableOutput, Shake128, Shake256, XofReader}; +#[cfg(test)] +pub(super) fn b2c_sabotage(fault: &str) -> bool { + std::env::var("PERRY_B2C_SABOTAGE").ok().as_deref() == Some(fault) +} + pub(super) use crate::common::bytes_from_header as string_from_header; -/// Extract the raw bytes from a pointer that might be a Buffer, a -/// StringHeader, or anything that uses the `[u32 byte-length prefix][bytes]` -/// layout. StringHeader has `utf16_len` at offset 0 and `byte_len` at -/// offset 4; BufferHeader has `length` at offset 0 and `capacity` at -/// offset 4. Both have the payload bytes immediately after the 8-byte -/// header, and both store the byte count (in UTF-8 / as raw bytes) in -/// the same u32 slot for our purposes — but we pick the correct field -/// based on whether the pointer is a registered Buffer. +/// Copy a byte value through the scoped API, or read a crypto string argument. pub(super) unsafe fn bytes_from_ptr(ptr: i64) -> Vec { let addr = ptr as usize; if addr < 0x1000 { return Vec::new(); } - if perry_runtime::buffer::is_registered_buffer(addr) { - let buf = ptr as *const perry_runtime::buffer::BufferHeader; - let len = (*buf).length as usize; - let data = - perry_runtime::buffer::buffer_data(buf as *const perry_runtime::buffer::BufferHeader); - return std::slice::from_raw_parts(data, len).to_vec(); + if perry_runtime::buffer::is_registered_buffer(addr) + || perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() + { + let data = perry_runtime::buffer::bytes::no_gc(|scope| { + perry_runtime::buffer::bytes::bytes( + f64::from_bits(JSValue::pointer(addr as *const u8).bits()), + scope, + ) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }); + #[cfg(test)] + let data = if b2c_sabotage("crypto_borrow") && !data.is_empty() { + let mut data = data; + data[0] ^= 0xff; + data + } else { + data + }; + return data; } // Fall back to StringHeader layout — the common case for literal // strings passed to crypto functions. @@ -81,14 +93,21 @@ pub(super) unsafe fn bytes_from_ptr(ptr: i64) -> Vec { pub(super) unsafe fn alloc_buffer_from_slice( bytes: &[u8], ) -> *mut perry_runtime::buffer::BufferHeader { - let buf = perry_runtime::buffer::buffer_alloc(bytes.len() as u32); - if buf.is_null() { - return buf; - } - (*buf).length = bytes.len() as u32; - let dst = perry_runtime::buffer::buffer_data_mut(buf); - std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()); - buf + #[cfg(test)] + let bytes = if b2c_sabotage("crypto_output") && !bytes.is_empty() { + &bytes[..bytes.len() - 1] + } else { + bytes + }; + JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Buffer, + bytes, + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut() } #[derive(Clone, Copy)] diff --git a/crates/perry-stdlib/src/ethers.rs b/crates/perry-stdlib/src/ethers.rs index 1460e3486d..5f229427a4 100644 --- a/crates/perry-stdlib/src/ethers.rs +++ b/crates/perry-stdlib/src/ethers.rs @@ -280,12 +280,10 @@ pub unsafe extern "C" fn js_keccak256_native(buf_ptr: i64) -> *mut StringHeader return js_string_from_bytes(s.as_ptr(), s.len() as u32); } - let len = (*buf_ptr).length as usize; - let data = - perry_runtime::buffer::buffer_data(buf_ptr as *const perry_runtime::buffer::BufferHeader); - let bytes = std::slice::from_raw_parts(data, len); - - let hash = keccak256(bytes); + let hash = perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(perry_runtime::JSValue::pointer(buf_ptr.cast()).bits()); + keccak256(perry_runtime::buffer::bytes::bytes(value, scope).unwrap_or(&[])) + }); // Format as "0x" + 64 hex chars let hex_chars = b"0123456789abcdef"; @@ -308,17 +306,10 @@ pub unsafe extern "C" fn js_keccak256_native_bytes( ) -> *mut perry_runtime::buffer::BufferHeader { let buf_ptr = (buf_ptr as u64 & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::buffer::BufferHeader; - let (data, _len) = if buf_ptr.is_null() { - (&[] as &[u8], 0) - } else { - let len = (*buf_ptr).length as usize; - let data = perry_runtime::buffer::buffer_data( - buf_ptr as *const perry_runtime::buffer::BufferHeader, - ); - (std::slice::from_raw_parts(data, len), len) - }; - - let hash = keccak256(data); + let hash = perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(perry_runtime::JSValue::pointer(buf_ptr.cast()).bits()); + keccak256(perry_runtime::buffer::bytes::bytes(value, scope).unwrap_or(&[])) + }); let result = perry_runtime::value::JSValue::from_bits( perry_runtime::buffer::bytes::from_slice( diff --git a/crates/perry-stdlib/src/querystring.rs b/crates/perry-stdlib/src/querystring.rs index e41617377a..a3a4ec4631 100644 --- a/crates/perry-stdlib/src/querystring.rs +++ b/crates/perry-stdlib/src/querystring.rs @@ -29,7 +29,7 @@ use crate::common::handle::Handle; use std::borrow::Cow; use perry_runtime::array::{js_array_alloc, js_array_length, js_array_push_f64}; -use perry_runtime::buffer::{buffer_alloc, buffer_data_mut, BufferHeader}; +use perry_runtime::buffer::BufferHeader; use perry_runtime::closure::{is_closure_ptr, js_closure_call1, ClosureHeader}; use perry_runtime::{ js_object_alloc_null_proto, js_object_get_field_by_name, js_object_get_own_field_or_undef, @@ -199,15 +199,15 @@ pub unsafe extern "C" fn js_querystring_unescape(str_arg: f64) -> f64 { } unsafe fn buffer_from_bytes(bytes: &[u8]) -> *mut BufferHeader { - let buf = buffer_alloc(bytes.len() as u32); - if buf.is_null() { - return std::ptr::null_mut(); - } - (*buf).length = bytes.len() as u32; - if !bytes.is_empty() { - std::ptr::copy_nonoverlapping(bytes.as_ptr(), buffer_data_mut(buf), bytes.len()); - } - buf + perry_runtime::JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Buffer, + bytes, + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut() } /// `querystring.unescapeBuffer(str, decodeSpaces?)` -> Buffer. diff --git a/crates/perry-stdlib/src/sqlite/backup.rs b/crates/perry-stdlib/src/sqlite/backup.rs index c6fdebb8db..e60b1ddaf5 100644 --- a/crates/perry-stdlib/src/sqlite/backup.rs +++ b/crates/perry-stdlib/src/sqlite/backup.rs @@ -1,7 +1,7 @@ use super::*; use crate::common::{get_handle, Handle}; use perry_runtime::{ - buffer::{buffer_data, is_registered_buffer, BufferHeader}, + buffer::is_registered_buffer, closure::{js_closure_call1, ClosureHeader}, js_get_string_pointer_unified, js_nanbox_pointer, js_object_alloc, js_object_set_field_by_name, js_string_from_bytes, JSValue, ObjectHeader, StringHeader, @@ -193,18 +193,16 @@ pub(crate) unsafe fn bytes_from_path_like(value: f64) -> Option> { if raw < 0x1000 { return None; } - if is_registered_buffer(raw) { - let buffer = raw as *const BufferHeader; - let bytes = std::slice::from_raw_parts(buffer_data(buffer), (*buffer).length as usize); - return Some(bytes.to_vec()); - } - if perry_runtime::typedarray::lookup_typed_array_kind(raw) - == Some(perry_runtime::typedarray::KIND_UINT8) + if is_registered_buffer(raw) + || perry_runtime::typedarray::lookup_typed_array_kind(raw) + == Some(perry_runtime::typedarray::KIND_UINT8) { - let bytes = perry_runtime::typedarray::typed_array_bytes( - raw as *const perry_runtime::typedarray::TypedArrayHeader, - )?; - return Some(bytes.to_vec()); + return perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }); } None } diff --git a/crates/perry-stdlib/src/sqlite/bind.rs b/crates/perry-stdlib/src/sqlite/bind.rs index c7167cf732..4d7b65578d 100644 --- a/crates/perry-stdlib/src/sqlite/bind.rs +++ b/crates/perry-stdlib/src/sqlite/bind.rs @@ -1,10 +1,7 @@ use super::*; use crate::common::{get_handle, Handle}; use perry_runtime::{ - buffer::{ - buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, - is_registered_buffer, mark_as_uint8array, BufferHeader, - }, + buffer::{is_any_array_buffer, is_data_view, is_registered_buffer}, closure::js_closure_call_array, js_array_alloc, js_array_get, js_array_length, js_array_push, js_get_string_pointer_unified, js_object_alloc_null_proto, js_object_get_field_by_name, js_object_set_field, @@ -183,49 +180,23 @@ pub(crate) unsafe fn bind_node_sqlite_value( ffi::sqlite3_bind_double(raw_stmt, index, js.as_number()) } else { let raw = raw_addr_from_value(value); - if perry_runtime::typedarray::lookup_typed_array_kind(raw).is_some() { - let typed_array = raw as *const perry_runtime::typedarray::TypedArrayHeader; - let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(typed_array) else { - throw_type(&format!( - "Provided value cannot be bound to SQLite parameter {}.", - index - )); - }; - let data_ptr = if bytes.is_empty() { - std::ptr::null() - } else { - bytes.as_ptr() as *const c_void - }; - if bytes.is_empty() { + let status = perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); + let bytes = perry_runtime::buffer::bytes::bytes(value, scope).ok()?; + Some(if bytes.is_empty() { ffi::sqlite3_bind_zeroblob(raw_stmt, index, 0) } else { ffi::sqlite3_bind_blob( raw_stmt, index, - data_ptr, + bytes.as_ptr().cast(), bytes.len() as c_int, ffi::SQLITE_TRANSIENT(), ) - } - } else if raw != 0 && is_registered_buffer(raw) { - let buffer = raw as *const BufferHeader; - let len = (*buffer).length as usize; - let data_ptr = if len == 0 { - std::ptr::null() - } else { - buffer_data(buffer) as *const c_void - }; - if len == 0 { - ffi::sqlite3_bind_zeroblob(raw_stmt, index, 0) - } else { - ffi::sqlite3_bind_blob( - raw_stmt, - index, - data_ptr, - len as c_int, - ffi::SQLITE_TRANSIENT(), - ) - } + }) + }); + if let Some(status) = status { + status } else { throw_type(&format!( "Provided value cannot be bound to SQLite parameter {}.", @@ -436,15 +407,19 @@ pub(crate) unsafe fn node_sqlite_column_value( } ffi::SQLITE_BLOB => { let len = ffi::sqlite3_column_bytes(raw_stmt, index) as usize; - let buf = buffer_alloc(len as u32); - (*buf).length = len as u32; - if len > 0 { - let ptr = ffi::sqlite3_column_blob(raw_stmt, index); - if !ptr.is_null() { - std::ptr::copy_nonoverlapping(ptr as *const u8, buffer_data_mut(buf), len); - } - } - JSValue::object_ptr(buf as *mut u8) + let ptr = ffi::sqlite3_column_blob(raw_stmt, index); + let input = if len > 0 && !ptr.is_null() { + std::slice::from_raw_parts(ptr as *const u8, len) + } else { + &[] + }; + JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Buffer, + input, + ) + .to_bits(), + ) } _ => JSValue::null(), } @@ -544,16 +519,19 @@ pub(crate) unsafe fn node_sqlite_value_arg( } ffi::SQLITE_BLOB => { let len = ffi::sqlite3_value_bytes(value) as usize; - let buf = buffer_alloc(len as u32); - (*buf).length = len as u32; - mark_as_uint8array(buf as usize); - if len > 0 { - let ptr = ffi::sqlite3_value_blob(value); - if !ptr.is_null() { - std::ptr::copy_nonoverlapping(ptr as *const u8, buffer_data_mut(buf), len); - } - } - JSValue::object_ptr(buf as *mut u8) + let ptr = ffi::sqlite3_value_blob(value); + let input = if len > 0 && !ptr.is_null() { + std::slice::from_raw_parts(ptr as *const u8, len) + } else { + &[] + }; + JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Uint8Array, + input, + ) + .to_bits(), + ) } _ => JSValue::null(), } @@ -582,22 +560,15 @@ pub(crate) unsafe fn node_sqlite_blob_like_bytes(value: f64) -> Option> if raw < 0x1000 { return None; } - if perry_runtime::typedarray::lookup_typed_array_kind(raw).is_some() { - let ta = raw as *const perry_runtime::typedarray::TypedArrayHeader; - if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ta) { - return Some(bytes.to_vec()); - } - } - if is_registered_buffer(raw) { - if is_any_array_buffer(raw) && !is_data_view(raw) { - return None; - } - let buf = raw as *const BufferHeader; - let len = (*buf).length as usize; - let data = buffer_data(buf); - return Some(std::slice::from_raw_parts(data, len).to_vec()); + if is_registered_buffer(raw) && is_any_array_buffer(raw) && !is_data_view(raw) { + return None; } - None + perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) } pub(crate) unsafe fn sqlite_result_error(ctx: *mut ffi::sqlite3_context, message: &str) { diff --git a/crates/perry-stdlib/src/sqlite/node_db.rs b/crates/perry-stdlib/src/sqlite/node_db.rs index e5033d727d..c0ba224bd0 100644 --- a/crates/perry-stdlib/src/sqlite/node_db.rs +++ b/crates/perry-stdlib/src/sqlite/node_db.rs @@ -2,8 +2,7 @@ use super::*; use crate::common::{get_handle, register_handle, Handle}; use perry_runtime::{ buffer::{ - buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, - is_registered_buffer, is_uint8array_buffer, mark_as_uint8array, BufferHeader, + is_any_array_buffer, is_data_view, is_registered_buffer, is_uint8array_buffer, BufferHeader, }, js_get_string_pointer_unified, js_nanbox_pointer, js_promise_rejected, js_promise_resolved, JSValue, Promise, StringHeader, @@ -142,19 +141,16 @@ unsafe fn node_sqlite_path_bytes(value: f64) -> Option> { if raw < 0x1000 { return None; } - if is_registered_buffer(raw) && is_uint8array_buffer(raw) { - let buffer = raw as *const BufferHeader; - return Some( - std::slice::from_raw_parts(buffer_data(buffer), (*buffer).length as usize).to_vec(), - ); - } - if perry_runtime::typedarray::lookup_typed_array_kind(raw) - == Some(perry_runtime::typedarray::KIND_UINT8) + if (is_registered_buffer(raw) && is_uint8array_buffer(raw)) + || perry_runtime::typedarray::lookup_typed_array_kind(raw) + == Some(perry_runtime::typedarray::KIND_UINT8) { - return perry_runtime::typedarray::typed_array_bytes( - raw as *const perry_runtime::typedarray::TypedArrayHeader, - ) - .map(ToOwned::to_owned); + return perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }); } None } @@ -425,12 +421,15 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_serialize( throw_sqlite_error_from_conn(conn); } let len = size as usize; - let buffer = buffer_alloc(len as u32); - (*buffer).length = len as u32; - mark_as_uint8array(buffer as usize); - if len > 0 { - std::ptr::copy_nonoverlapping(image, buffer_data_mut(buffer), len); - } + let buffer = JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Uint8Array, + std::slice::from_raw_parts(image, len), + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut(); ffi::sqlite3_free(image.cast()); buffer }) @@ -445,18 +444,17 @@ pub unsafe extern "C" fn js_node_sqlite_database_sync_deserialize( let raw = raw_addr_from_value(image_value); let bytes = if perry_runtime::typedarray::lookup_typed_array_kind(raw) == Some(perry_runtime::typedarray::KIND_UINT8) + || (raw >= 0x1000 + && is_registered_buffer(raw) + && !is_any_array_buffer(raw) + && !is_data_view(raw)) { - perry_runtime::typedarray::typed_array_bytes( - raw as *const perry_runtime::typedarray::TypedArrayHeader, - ) - .map(ToOwned::to_owned) - } else if raw >= 0x1000 - && is_registered_buffer(raw) - && !is_any_array_buffer(raw) - && !is_data_view(raw) - { - let buffer = raw as *const BufferHeader; - Some(std::slice::from_raw_parts(buffer_data(buffer), (*buffer).length as usize).to_vec()) + perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(raw as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) } else { None } diff --git a/crates/perry-stdlib/src/sqlite/node_stmt_session.rs b/crates/perry-stdlib/src/sqlite/node_stmt_session.rs index b6a41619cb..14aa031349 100644 --- a/crates/perry-stdlib/src/sqlite/node_stmt_session.rs +++ b/crates/perry-stdlib/src/sqlite/node_stmt_session.rs @@ -1,10 +1,7 @@ use super::*; use crate::common::{get_handle, register_handle, Handle}; use perry_runtime::{ - buffer::{ - buffer_alloc, buffer_data, buffer_data_mut, is_any_array_buffer, is_data_view, - is_registered_buffer, mark_as_uint8array, BufferHeader, - }, + buffer::{is_any_array_buffer, is_data_view, is_registered_buffer, BufferHeader}, closure::{js_closure_call1, ClosureHeader}, js_array_alloc, js_array_push, js_object_alloc_with_shape, js_object_set_field, js_string_from_bytes, ArrayHeader, JSValue, ObjectHeader, StringHeader, @@ -244,19 +241,18 @@ pub unsafe extern "C" fn js_node_sqlite_statement_sync_expanded_sql( pub(crate) unsafe fn changeset_bytes_from_value(value: f64) -> Vec { let addr = raw_addr_from_value(value); - if addr != 0 { - if is_registered_buffer(addr) && !is_any_array_buffer(addr) && !is_data_view(addr) { - let buf = addr as *const BufferHeader; - let bytes = std::slice::from_raw_parts(buffer_data(buf), (*buf).length as usize); - return bytes.to_vec(); - } - if perry_runtime::typedarray::lookup_typed_array_kind(addr) - == Some(perry_runtime::typedarray::KIND_UINT8) - { - let ptr = addr as *const perry_runtime::typedarray::TypedArrayHeader; - if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ptr) { - return bytes.to_vec(); - } + if addr != 0 + && ((is_registered_buffer(addr) && !is_any_array_buffer(addr) && !is_data_view(addr)) + || perry_runtime::typedarray::lookup_typed_array_kind(addr) + == Some(perry_runtime::typedarray::KIND_UINT8)) + { + if let Some(bytes) = perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) { + return bytes; } } throw_type("The \"changeset\" argument must be a Uint8Array."); @@ -311,12 +307,20 @@ pub(crate) unsafe fn sqlite_session_blob( } let len = len.max(0) as usize; - let buffer = buffer_alloc(len as u32); - (*buffer).length = len as u32; - mark_as_uint8array(buffer as usize); - if len > 0 && !data.is_null() { - std::ptr::copy_nonoverlapping(data as *const u8, buffer_data_mut(buffer), len); - } + let input = if len > 0 && !data.is_null() { + std::slice::from_raw_parts(data as *const u8, len) + } else { + &[] + }; + let buffer = JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Uint8Array, + input, + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut(); if !data.is_null() { ffi::sqlite3_free(data); } diff --git a/crates/perry-stdlib/src/streams.rs b/crates/perry-stdlib/src/streams.rs index eff1679113..5b911bfe4f 100644 --- a/crates/perry-stdlib/src/streams.rs +++ b/crates/perry-stdlib/src/streams.rs @@ -860,17 +860,8 @@ unsafe fn build_iter_result(value_bits: u64, done: bool) -> u64 { } pub(crate) unsafe fn alloc_uint8array_from_bytes(bytes: &[u8]) -> u64 { - let buf = perry_runtime::buffer::buffer_alloc(bytes.len() as u32); - perry_runtime::buffer::mark_as_uint8array(buf as usize); - (*buf).length = bytes.len() as u32; - if !bytes.is_empty() { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - perry_runtime::buffer::buffer_data_mut(buf), - bytes.len(), - ); - } - JSValue::object_ptr(buf as *mut u8).bits() + perry_runtime::buffer::bytes::from_slice(perry_runtime::buffer::bytes::Brand::Uint8Array, bytes) + .to_bits() } unsafe fn read_bytes_from_chunk(chunk_bits: u64) -> Option> { @@ -885,17 +876,12 @@ unsafe fn read_bytes_from_chunk(chunk_bits: u64) -> Option> { if addr < 0x1000 { return None; } - if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *const perry_runtime::typedarray::TypedArrayHeader; - return perry_runtime::typedarray::typed_array_bytes(ta).map(|bytes| bytes.to_vec()); - } - if !perry_runtime::buffer::is_registered_buffer(addr) { - return None; - } - let ptr = addr as *const perry_runtime::buffer::BufferHeader; - let len = (*ptr).length as usize; - let data = perry_runtime::buffer::buffer_data(ptr); - Some(std::slice::from_raw_parts(data, len).to_vec()) + perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) } unsafe fn raw_pointer_addr(bits: u64) -> Option { @@ -1959,10 +1945,13 @@ pub unsafe extern "C" fn js_readable_stream_from_iterable(value: f64) -> f64 { && !perry_runtime::buffer::is_any_array_buffer(addr) && !perry_runtime::buffer::is_data_view(addr) { - let buf = addr as *const perry_runtime::buffer::BufferHeader; - let len = (*buf).length as usize; - let data = perry_runtime::buffer::buffer_data(buf); - let chunks = (0..len).map(|i| (*data.add(i) as f64).to_bits()).collect(); + let chunks = perry_runtime::buffer::bytes::no_gc(|scope| { + perry_runtime::buffer::bytes::bytes(value, scope) + .unwrap_or(&[]) + .iter() + .map(|&byte| (byte as f64).to_bits()) + .collect() + }); ReadableFromSource::closed(chunks) } else if let Some(chunks) = chunks_from_sync_iterable(value) { ReadableFromSource::closed(chunks) diff --git a/crates/perry-stdlib/src/streams/byob.rs b/crates/perry-stdlib/src/streams/byob.rs index ce3f8d397a..1a30462716 100644 --- a/crates/perry-stdlib/src/streams/byob.rs +++ b/crates/perry-stdlib/src/streams/byob.rs @@ -102,6 +102,9 @@ struct ViewInfo { byte_len: usize, kind: u8, elem_size: usize, + // Queue processing can allocate a remainder and invoke stream callbacks. + // Keep the owner rooted and the address stable for the whole operation. + _pin: perry_runtime::buffer::bytes::Pinned, } unsafe fn view_info(view_bits: u64) -> Option { @@ -109,31 +112,28 @@ unsafe fn view_info(view_bits: u64) -> Option { if addr < 0x1000 { return None; } - if let Some(kind) = perry_runtime::typedarray::lookup_typed_array_kind(addr) { - let ta = addr as *mut perry_runtime::typedarray::TypedArrayHeader; - let bytes = perry_runtime::typedarray::typed_array_bytes_mut(ta)?; - return Some(ViewInfo { - data: bytes.as_mut_ptr(), - byte_len: bytes.len(), - kind, - elem_size: perry_runtime::typedarray::elem_size_for_kind(kind).max(1), - }); - } - if perry_runtime::buffer::is_registered_buffer(addr) - && !perry_runtime::buffer::is_any_array_buffer(addr) - { - // DataView and Uint8Array/Buffer registrations both carry their - // byte storage in a BufferHeader. - let buf = addr as *mut perry_runtime::buffer::BufferHeader; - let len = (*buf).length as usize; - return Some(ViewInfo { - data: perry_runtime::buffer::buffer_data_mut(buf), - byte_len: len, - kind: 0, // KIND_U8 — fulfilled values surface as Uint8Array - elem_size: 1, - }); - } - None + let (kind, elem_size) = + if let Some(kind) = perry_runtime::typedarray::lookup_typed_array_kind(addr) { + ( + kind, + perry_runtime::typedarray::elem_size_for_kind(kind).max(1), + ) + } else if perry_runtime::buffer::is_registered_buffer(addr) + && !perry_runtime::buffer::is_any_array_buffer(addr) + { + (0, 1) + } else { + return None; + }; + let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + let pin = perry_runtime::buffer::bytes::pin(value).ok()?; + Some(ViewInfo { + data: pin.as_mut_ptr(), + byte_len: pin.len(), + kind, + elem_size, + _pin: pin, + }) } /// `chunk.byteLength` for desiredSize accounting on byte streams; 1.0 for @@ -166,10 +166,13 @@ unsafe fn alloc_view_of_kind(kind: u8, elem_size: usize, bytes: &[u8]) -> u64 { if ta.is_null() { return TAG_UNDEFINED; } - if let Some(dst) = perry_runtime::typedarray::typed_array_bytes_mut(ta) { - let n = dst.len().min(bytes.len()); - dst[..n].copy_from_slice(&bytes[..n]); - } + perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(ta.cast()).bits()); + if let Ok(dst) = perry_runtime::buffer::bytes::bytes_mut(value, scope) { + let n = dst.len().min(bytes.len()); + dst[..n].copy_from_slice(&bytes[..n]); + } + }); JSValue::pointer(ta as *const u8).bits() } diff --git a/crates/perry-stdlib/src/string_decoder.rs b/crates/perry-stdlib/src/string_decoder.rs index b4d139086d..68cd89c915 100644 --- a/crates/perry-stdlib/src/string_decoder.rs +++ b/crates/perry-stdlib/src/string_decoder.rs @@ -27,7 +27,7 @@ //! the next write. `hex` / `latin1` / `ascii` are stateless. use crate::common::handle::{get_handle_mut, with_handle}; -use perry_runtime::buffer::{buffer_data, is_registered_buffer, BufferHeader}; + use perry_runtime::string::js_string_from_wtf8_bytes; use perry_runtime::{js_get_string_pointer_unified, js_string_from_bytes, JSValue, StringHeader}; @@ -708,25 +708,16 @@ unsafe fn bytes_from_write_arg(value: f64) -> Vec { } let addr = raw_addr_from_value(value); - if addr >= 0x1000 { - if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { - let ta = addr as *const perry_runtime::typedarray::TypedArrayHeader; - if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ta) { - return bytes.to_vec(); - } + if addr >= 0x1000 && !perry_runtime::buffer::is_any_array_buffer(addr) { + if let Some(bytes) = perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }) { + return bytes; } } - if addr >= 0x1000 - && is_registered_buffer(addr) - && (!perry_runtime::buffer::is_any_array_buffer(addr) - || perry_runtime::buffer::is_data_view(addr)) - { - let buf = addr as *const BufferHeader; - let len = (*buf).length as usize; - let data = buffer_data(buf); - return std::slice::from_raw_parts(data, len).to_vec(); - } - throw_invalid_buf_arg(value) } @@ -907,16 +898,10 @@ pub unsafe fn dispatch_string_decoder_property(handle: i64, property: &str) -> f match property { "lastNeed" => f64::from(h.utf8.last_need as i32), "lastTotal" => f64::from(h.utf8.last_total as i32), - "lastChar" => { - let buf = perry_runtime::buffer::buffer_alloc(4); - if buf.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - (*buf).length = 4; - let dst = perry_runtime::buffer::buffer_data_mut(buf); - std::ptr::copy_nonoverlapping(h.utf8.last_char.as_ptr(), dst, 4); - f64::from_bits(0x7FFD_0000_0000_0000u64 | ((buf as u64) & 0x0000_FFFF_FFFF_FFFF)) - } + "lastChar" => perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Buffer, + &h.utf8.last_char, + ), "encoding" => { let s = canonical_encoding_name(h.mode); let sh = js_string_from_bytes(s.as_ptr(), s.len() as u32); diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index ac547a5830..6d08549f73 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -821,11 +821,11 @@ unsafe fn jsvalue_to_bytes(value: f64) -> Option> { return value_to_string(value).map(|s| s.into_bytes()); } if v.is_pointer() { - let mut len = 0u32; - let data = perry_runtime::buffer::js_value_buffer_or_typedarray_data(value, &mut len); - if !data.is_null() { - return Some(std::slice::from_raw_parts(data, len as usize).to_vec()); - } + return perry_runtime::buffer::bytes::no_gc(|scope| { + perry_runtime::buffer::bytes::bytes(value, scope) + .ok() + .map(<[u8]>::to_vec) + }); } None } diff --git a/crates/perry-stdlib/src/tls/module_api.rs b/crates/perry-stdlib/src/tls/module_api.rs index eb21ad49aa..99e4ed22b8 100644 --- a/crates/perry-stdlib/src/tls/module_api.rs +++ b/crates/perry-stdlib/src/tls/module_api.rs @@ -239,18 +239,17 @@ pub unsafe extern "C" fn js_tls_convert_alpn_protocols(protocols: f64, out: f64) if perry_runtime::buffer::is_registered_buffer(addr) && !perry_runtime::buffer::is_any_array_buffer(addr) { - let data = perry_runtime::buffer::js_native_buffer_data_ptr(protocols); - let length = perry_runtime::buffer::js_native_buffer_byte_len(protocols); - if !data.is_null() && length != 0 { - encoded.extend_from_slice(std::slice::from_raw_parts(data, length)); - } + perry_runtime::buffer::bytes::no_gc(|scope| { + if let Ok(bytes) = perry_runtime::buffer::bytes::bytes(protocols, scope) { + encoded.extend_from_slice(bytes); + } + }); } else if perry_runtime::typedarray::lookup_typed_array_kind(addr).is_some() { - let mut length = 0u32; - let data = - perry_runtime::buffer::js_value_buffer_or_typedarray_data(protocols, &mut length); - if !data.is_null() && length != 0 { - encoded.extend_from_slice(std::slice::from_raw_parts(data, length as usize)); - } + perry_runtime::buffer::bytes::no_gc(|scope| { + if let Ok(bytes) = perry_runtime::buffer::bytes::bytes(protocols, scope) { + encoded.extend_from_slice(bytes); + } + }); } else { return undefined(); } @@ -266,14 +265,15 @@ pub unsafe extern "C" fn js_tls_convert_alpn_protocols(protocols: f64, out: f64) "ERR_INVALID_ARG_TYPE", ); }; - let buffer = perry_runtime::buffer::js_buffer_alloc(encoded.len() as i32, 0); - if !encoded.is_empty() { - std::ptr::copy_nonoverlapping( - encoded.as_ptr(), - perry_runtime::buffer::buffer_data_mut(buffer), - encoded.len(), - ); - } + let buffer = JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Buffer, + &encoded, + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut(); set_field( out_addr as *mut ObjectHeader, "ALPNProtocols", diff --git a/crates/perry-stdlib/src/webcrypto/key_object.rs b/crates/perry-stdlib/src/webcrypto/key_object.rs index de1464be5f..780ea82a29 100644 --- a/crates/perry-stdlib/src/webcrypto/key_object.rs +++ b/crates/perry-stdlib/src/webcrypto/key_object.rs @@ -172,12 +172,7 @@ pub(super) unsafe fn js_webcrypto_key_object_to_crypto_key( /// metadata resolves through the process-global registry still has readable /// bytes at `addr`. unsafe fn crypto_key_bytes(addr: usize) -> Vec { - let buf = addr as *const BufferHeader; - let len = (*buf).length as usize; - if len == 0 { - return Vec::new(); - } - std::slice::from_raw_parts(buffer_payload(buf), len).to_vec() + buffer_payload_copy(addr) } /// Re-encode an asymmetric CryptoKey's WebCrypto key material (SPKI/PKCS#8 DER diff --git a/crates/perry-stdlib/src/webcrypto/util.rs b/crates/perry-stdlib/src/webcrypto/util.rs index f8790b0687..e86a38a016 100644 --- a/crates/perry-stdlib/src/webcrypto/util.rs +++ b/crates/perry-stdlib/src/webcrypto/util.rs @@ -60,14 +60,12 @@ pub(super) use ml_kem::pkcs8::{ }; pub(super) use perry_runtime::{ - buffer::{buffer_data_mut, is_registered_buffer, BufferHeader}, + buffer::{is_registered_buffer, BufferHeader}, js_object_alloc, js_object_set_field_by_name, js_promise_resolved, JSValue, Promise, StringHeader, }; extern "C" { - fn js_buffer_alloc_unsafe(size: i32) -> *mut BufferHeader; - fn js_buffer_mark_as_uint8array_external(addr: usize); fn js_buffer_mark_as_crypto_key_external( addr: usize, algo: u8, @@ -81,8 +79,13 @@ extern "C" { /// Resolve inline, shared-view, and foreign-backed buffer storage. #[inline] -pub(super) unsafe fn buffer_payload(buf: *const BufferHeader) -> *const u8 { - perry_runtime::buffer::buffer_data(buf as *const perry_runtime::buffer::BufferHeader) +pub(super) unsafe fn buffer_payload_copy(addr: usize) -> Vec { + perry_runtime::buffer::bytes::no_gc(|scope| { + let value = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + perry_runtime::buffer::bytes::bytes(value, scope) + .map(<[u8]>::to_vec) + .unwrap_or_default() + }) } // #854: NaN-boxing tag contract — see CLAUDE.md. `POINTER_TAG`, @@ -515,18 +518,10 @@ pub(super) unsafe fn bytes_from_jsvalue(bits: u64) -> Vec { if raw < 0x1000 { return Vec::new(); } - if is_registered_buffer(raw) { - let buf = raw as *const BufferHeader; - let len = (*buf).length as usize; - return std::slice::from_raw_parts(buffer_payload(buf), len).to_vec(); - } - if let Some(_kind) = perry_runtime::typedarray::lookup_typed_array_kind(raw) { - // BufferSource can be any TypedArray. Native arena views keep their - // bytes out-of-line, so route through the typed-array byte helper. - let ta = raw as *const perry_runtime::typedarray::TypedArrayHeader; - if let Some(bytes) = perry_runtime::typedarray::typed_array_bytes(ta) { - return bytes.to_vec(); - } + if is_registered_buffer(raw) + || perry_runtime::typedarray::lookup_typed_array_kind(raw).is_some() + { + return buffer_payload_copy(raw); } if top16 == 0x7FFF { let hdr = raw as *const StringHeader; @@ -944,20 +939,15 @@ pub(super) fn require_usage( /// Allocate a fresh Buffer marked as Uint8Array (so `instanceof Uint8Array` /// is true and `new Uint8Array(buf)` memcpy's correctly), copy `bytes` in. pub(super) unsafe fn alloc_uint8array_from_slice(bytes: &[u8]) -> *mut BufferHeader { - // Allocate through the runtime provider's C ABI. A separately packaged - // stdlib must not depend on registering a Rust-allocated cell afterwards. - // A negative size makes the runtime raise its ordinary allocation error. - let buf = js_buffer_alloc_unsafe(i32::try_from(bytes.len()).unwrap_or(-1)); - if buf.is_null() { - return buf; - } - (*buf).length = bytes.len() as u32; - if !bytes.is_empty() { - let dst = buffer_data_mut(buf); - std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()); - } - js_buffer_mark_as_uint8array_external(buf as usize); - buf + JSValue::from_bits( + perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Uint8Array, + bytes, + ) + .to_bits(), + ) + .as_pointer::() + .cast_mut() } /// Wrap a heap value (NaN-boxed bits) in an already-resolved Promise. diff --git a/crates/perry-updater/src/core.rs b/crates/perry-updater/src/core.rs index 3d9062e188..02b2c25244 100644 --- a/crates/perry-updater/src/core.rs +++ b/crates/perry-updater/src/core.rs @@ -421,26 +421,21 @@ pub extern "C" fn perry_updater_sha256_buffer(buf_ptr: i64) -> *mut BufferHeader if buf_ptr == 0 { return std::ptr::null_mut(); } - unsafe { - let buf = buf_ptr as *const BufferHeader; - let len = (*buf).length as usize; - let data = - perry_runtime::buffer::buffer_data(buf as *const perry_runtime::buffer::BufferHeader); - let bytes = std::slice::from_raw_parts(data, len); - - let mut hasher = Sha256::new(); - hasher.update(bytes); - let digest = hasher.finalize(); - - let out = perry_runtime::buffer::buffer_alloc(32); - if out.is_null() { - return out; - } - (*out).length = 32; - let dst = perry_runtime::buffer::buffer_data_mut(out); - std::ptr::copy_nonoverlapping(digest.as_ptr(), dst, 32); - out - } + let value = perry_runtime::value::js_nanbox_pointer(buf_ptr); + let digest = perry_runtime::buffer::bytes::no_gc(|scope| { + let bytes = perry_runtime::buffer::bytes::bytes(value, scope).ok()?; + Some(Sha256::digest(bytes)) + }); + let Some(digest) = digest else { + return std::ptr::null_mut(); + }; + let output = perry_runtime::buffer::bytes::from_slice( + perry_runtime::buffer::bytes::Brand::Buffer, + &digest, + ); + perry_runtime::value::JSValue::from_bits(output.to_bits()) + .as_pointer::() + .cast_mut() } #[cfg(test)] diff --git a/scripts/buffer_b4_census_closed.tsv b/scripts/buffer_b4_census_closed.tsv new file mode 100644 index 0000000000..d70715a562 --- /dev/null +++ b/scripts/buffer_b4_census_closed.tsv @@ -0,0 +1,74 @@ +crate file:line category pattern scope snippet +perry-ext-http perry-ext-http/src/server/http2_server/session.rs:310 borrow-unscoped &'static/raw borrow, lifetime unchecked prod let data = js_value_buffer_or_typedarray_data(f64::from_bits(value.bits()), &mut len); +perry-ext-net perry-ext-net/src/tls.rs:147 borrow-unscoped &'static/raw borrow, lifetime unchecked prod let data = js_value_buffer_or_typedarray_data(value, &mut len); +perry-runtime perry-runtime/src/bun_compat/mod.rs:192 borrow-unscoped &'static/raw borrow, lifetime unchecked prod crate::buffer::js_value_buffer_or_typedarray_data(value, &mut binary_len as *mut u32) +perry-runtime perry-runtime/src/node_vm.rs:527 borrow-unscoped &'static/raw borrow, lifetime unchecked prod let ptr = unsafe { crate::buffer::js_value_buffer_or_typedarray_data(value, &mut len) }; +perry-runtime perry-runtime/src/os_process_stream_write.rs:53 borrow-unscoped &'static/raw borrow, lifetime unchecked prod let data = unsafe { crate::buffer::js_value_buffer_or_typedarray_data(chunk, &mut len) }; +perry-runtime perry-runtime/src/tls.rs:302 borrow-unscoped &'static/raw borrow, lifetime unchecked prod let data = unsafe { crate::buffer::js_value_buffer_or_typedarray_data(value, &mut len) }; +perry-runtime perry-runtime/src/bigint/convert.rs:406 creation buffer_alloc prod let result = crate::buffer::buffer_alloc(length as u32); +perry-runtime perry-runtime/src/builtins/globals.rs:616 creation buffer_alloc prod let backing = crate::buffer::buffer_alloc(src_len); +perry-runtime perry-runtime/src/builtins/globals.rs:637 creation buffer_alloc prod let dst = crate::buffer::buffer_alloc(src_len); +perry-runtime perry-runtime/src/bun_ffi/memory.rs:147 creation buffer_alloc prod let buffer = crate::buffer::buffer_alloc(length); +perry-runtime perry-runtime/src/fs/dir_glob_watch/watch.rs:385 creation js_buffer_alloc prod let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); +perry-runtime perry-runtime/src/fs/dirent.rs:294 creation js_buffer_alloc prod let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); +perry-runtime perry-runtime/src/fs/filehandle.rs:1277 creation js_buffer_alloc prod let buf = crate::buffer::js_buffer_alloc(bytes.len() as i32, 0); +perry-runtime perry-runtime/src/fs/filehandle.rs:390 creation buffer_alloc prod let buf = crate::buffer::buffer_alloc(bytes.len() as u32); +perry-runtime perry-runtime/src/node_api_host/buffers.rs:83 creation buffer_alloc prod let buffer = crate::buffer::buffer_alloc(length); +perry-runtime perry-runtime/src/node_submodules/consumers.rs:31 creation buffer_alloc prod let buf = crate::buffer::buffer_alloc(bytes.len() as u32); +perry-runtime perry-runtime/src/node_vm.rs:583 creation buffer_alloc prod let buf = crate::buffer::buffer_alloc(bytes.len() as u32); +perry-runtime perry-runtime/src/object/buffer_dispatch.rs:466 creation buffer_alloc prod let out = crate::buffer::buffer_alloc(len); +perry-runtime perry-runtime/src/object/buffer_dispatch.rs:868 creation buffer_alloc prod let out = crate::buffer::buffer_alloc(bytes.len() as u32); +perry-runtime perry-runtime/src/object/native_module_crypto_key_object.rs:76 creation buffer_alloc prod let out = crate::buffer::buffer_alloc(len as u32); +perry-runtime perry-runtime/src/object/typed_array_proto_thunks.rs:351 creation buffer_alloc prod let out = crate::buffer::buffer_alloc(len as u32); +perry-runtime perry-runtime/src/os_process_streams.rs:647 creation buffer_alloc prod let buf = crate::buffer::buffer_alloc(chunk.len() as u32); +perry-runtime perry-runtime/src/text.rs:415 creation buffer_alloc prod let buf = buffer_alloc(len as u32); +perry-runtime perry-runtime/src/thread/clone_read.rs:470 creation buffer_alloc prod let buffer = crate::buffer::buffer_alloc(len); +perry-runtime perry-runtime/src/atomics.rs:176 pointer-kept-across-gc SAB data addr stashed for Atomics.wait prod crate::buffer::buffer_data(*ptr as *const crate::buffer::BufferHeader) as usize; +perry-runtime perry-runtime/src/typedarray/mod.rs:287 pointer-kept-across-gc INLINE_OWNING_U32_CACHE address cache prod static INLINE_OWNING_U32_CACHE +perry-runtime perry-runtime/src/bigint/convert.rs:408 size-assumption header length write prod (*result).length = length as u32; +perry-runtime perry-runtime/src/builtins/globals.rs:618 size-assumption header length write prod (*backing).length = src_len; +perry-runtime perry-runtime/src/builtins/globals.rs:639 size-assumption header length write prod (*dst).length = src_len; +perry-runtime perry-runtime/src/bun_ffi/memory.rs:153 size-assumption header length write prod (*buffer).length = length; +perry-runtime perry-runtime/src/fs/dirent.rs:299 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-runtime perry-runtime/src/fs/filehandle.rs:1285 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-runtime perry-runtime/src/fs/filehandle.rs:393 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-runtime perry-runtime/src/node_api_host/buffers.rs:84 size-assumption header length write prod (*buffer).length = length; +perry-runtime perry-runtime/src/node_submodules/consumers.rs:33 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-runtime perry-runtime/src/node_vm.rs:585 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-runtime perry-runtime/src/object/buffer_dispatch.rs:467 size-assumption header length write prod (*out).length = len; +perry-runtime perry-runtime/src/object/buffer_dispatch.rs:875 size-assumption header length write prod (*out).length = bytes.len() as u32; +perry-runtime perry-runtime/src/object/native_module_crypto_key_object.rs:87 size-assumption header length write prod (*out).length = len as u32; +perry-runtime perry-runtime/src/object/typed_array_proto_thunks.rs:353 size-assumption header length write prod (*out).length = len as u32; +perry-runtime perry-runtime/src/os_process_streams.rs:663 size-assumption header length write prod (*buf).length = chunk.len() as u32; +perry-runtime perry-runtime/src/text.rs:417 size-assumption header length write prod (*buf).length = len as u32; +perry-runtime perry-runtime/src/thread/clone_read.rs:471 size-assumption header length write prod (*buffer).length = len; +perry-stdlib perry-stdlib/src/tls.rs:826 borrow-unscoped &'static/raw borrow, lifetime unchecked prod let data = perry_runtime::buffer::js_value_buffer_or_typedarray_data(value, &mut len); +perry-stdlib perry-stdlib/src/tls/module_api.rs:250 borrow-unscoped &'static/raw borrow, lifetime unchecked prod perry_runtime::buffer::js_value_buffer_or_typedarray_data(protocols, &mut length); +perry-stdlib perry-stdlib/src/crypto/random.rs:58 creation buffer_alloc prod return perry_runtime::buffer::buffer_alloc(0); +perry-stdlib perry-stdlib/src/crypto/random.rs:61 creation buffer_alloc prod let buf = perry_runtime::buffer::buffer_alloc(size as u32); +perry-stdlib perry-stdlib/src/crypto/util.rs:84 creation buffer_alloc prod let buf = perry_runtime::buffer::buffer_alloc(bytes.len() as u32); +perry-stdlib perry-stdlib/src/querystring.rs:202 creation buffer_alloc prod let buf = buffer_alloc(bytes.len() as u32); +perry-stdlib perry-stdlib/src/sqlite/bind.rs:439 creation buffer_alloc prod let buf = buffer_alloc(len as u32); +perry-stdlib perry-stdlib/src/sqlite/bind.rs:547 creation buffer_alloc prod let buf = buffer_alloc(len as u32); +perry-stdlib perry-stdlib/src/sqlite/node_db.rs:428 creation buffer_alloc prod let buffer = buffer_alloc(len as u32); +perry-stdlib perry-stdlib/src/sqlite/node_stmt_session.rs:314 creation buffer_alloc prod let buffer = buffer_alloc(len as u32); +perry-stdlib perry-stdlib/src/streams.rs:863 creation buffer_alloc prod let buf = perry_runtime::buffer::buffer_alloc(bytes.len() as u32); +perry-stdlib perry-stdlib/src/string_decoder.rs:911 creation buffer_alloc prod let buf = perry_runtime::buffer::buffer_alloc(4); +perry-stdlib perry-stdlib/src/tls/module_api.rs:269 creation js_buffer_alloc prod let buffer = perry_runtime::buffer::js_buffer_alloc(encoded.len() as i32, 0); +perry-stdlib perry-stdlib/src/webcrypto/util.rs:950 creation js_buffer_alloc_unsafe prod let buf = js_buffer_alloc_unsafe(i32::try_from(bytes.len()).unwrap_or(-1)); +perry-stdlib perry-stdlib/src/crypto/random.rs:63 size-assumption header length write prod (*buf).length = size as u32; +perry-stdlib perry-stdlib/src/crypto/util.rs:88 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-stdlib perry-stdlib/src/querystring.rs:206 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-stdlib perry-stdlib/src/sqlite/bind.rs:440 size-assumption header length write prod (*buf).length = len as u32; +perry-stdlib perry-stdlib/src/sqlite/bind.rs:548 size-assumption header length write prod (*buf).length = len as u32; +perry-stdlib perry-stdlib/src/sqlite/node_db.rs:429 size-assumption header length write prod (*buffer).length = len as u32; +perry-stdlib perry-stdlib/src/sqlite/node_stmt_session.rs:315 size-assumption header length write prod (*buffer).length = len as u32; +perry-stdlib perry-stdlib/src/streams.rs:865 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-stdlib perry-stdlib/src/string_decoder.rs:915 size-assumption header length write prod (*buf).length = 4; +perry-stdlib perry-stdlib/src/webcrypto/util.rs:954 size-assumption header length write prod (*buf).length = bytes.len() as u32; +perry-runtime perry-runtime/src/fs/filehandle.rs:1356 creation js_buffer_alloc prod let buf = crate::buffer::js_buffer_alloc(16 * 1024, 0); +perry-stdlib perry-stdlib/src/crypto/kdf.rs:173 creation buffer_alloc prod return perry_runtime::buffer::buffer_alloc(0); +perry-stdlib perry-stdlib/src/crypto/kdf.rs:196 creation buffer_alloc prod perry_runtime::buffer::buffer_alloc(0) +perry-updater perry-updater/src/core.rs:435 creation buffer_alloc prod let out = perry_runtime::buffer::buffer_alloc(32); +perry-updater perry-updater/src/core.rs:439 size-assumption header length write prod (*out).length = 32; +perry-stdlib perry-stdlib/src/crypto/sign.rs:51 creation buffer_alloc prod Err(_) => return perry_runtime::buffer::buffer_alloc(0), diff --git a/scripts/buffer_b4_gap.py b/scripts/buffer_b4_gap.py new file mode 100644 index 0000000000..a18ed2c1f3 --- /dev/null +++ b/scripts/buffer_b4_gap.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Use the canonical parity normalizer with one coherent, forced wrapper set. + +The stock mixed-suite runner switches wrapper cases to auto-optimize. B2c/B4 +instead measures the same full prebuilt package/feature set on both arms, +forcing the archives that satisfy its three pump references on every link. +Only that switch is suppressed in a temporary copy of the canonical runner. +""" +import argparse +import json +import os +import shlex +from pathlib import Path +import subprocess +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from buffer_b4_validate import environment + +KEYWORDS = ['buffer', 'typed', 'dataview', 'arraybuffer', 'zlib', 'crypto', 'tls', 'net', 'http', 'ws'] + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--hostdir', type=Path, required=True) + parser.add_argument('--arm', choices=['main', 'head'], required=True) + parser.add_argument('--keywords', default=','.join(KEYWORDS)) + args = parser.parse_args() + source, target, env = environment(args.hostdir, args.arm) + env.update(PERRY_BIN=str(target/'release/perry'), PERRY_FORCE_WELL_KNOWN='http,net,ws,zlib', + PERRY_RUN_TIMEOUT='30', + RAYON_NUM_THREADS=os.environ.get('PERRY_VERIFY_RAYON_THREADS', '8')) + original = (source/'run_parity_tests.sh').read_text() + switch = 'elif [[ -n "${PERRY_NO_AUTO_OPTIMIZE:-}" && "$TEST_SUITE" == "all" ]] &&' + assert original.count(switch) == 1 + runner = source/f'.buffer-b4-parity-{os.getpid()}.sh' + # The shared host forbids pkill. Enumerate only executables produced in + # this runner's unique scratch directory, then stop them by their PID. + import re + original, replaced = re.subn( + r' if \[\[ "\$HOST_PLATFORM" != "windows" \]\] && command -v pkill.*?\n fi', + ' reap_lane_children', original, flags=re.S) + assert replaced == 2 + cleanup = """ +reap_lane_children() { + python3 - "$PARITY_TMP" <<'PY_REAP' +import os, pathlib, signal, sys +prefix = sys.argv[1] + '/perry_parity_' +for entry in pathlib.Path('/proc').iterdir(): + if not entry.name.isdigit(): continue + try: + if os.readlink(entry/'exe').startswith(prefix): + os.kill(int(entry.name), signal.SIGKILL) + except (OSError, ProcessLookupError): pass +PY_REAP +} +""" + runner.write_text(original.replace(switch, 'elif false &&').replace('cleanup_parity_run() {', cleanup+'\ncleanup_parity_run() {')) + results = {} + folder = args.hostdir/'gap'/args.arm + folder.mkdir(parents=True, exist_ok=True) + keywords = args.keywords.split(',') + selected = sorted(p for p in (source/'test-files').glob('test_gap_*') + if p.suffix in ['.ts', '.cts', '.mts'] + and (any(k in p.stem for k in keywords) or '12094' in p.stem)) + selection = folder/'selection.txt' + selection.write_text(''.join(str(p)+'\n' for p in selected)) + # Run the union once; overlapping keyword sets must not rerun fixtures. + # Keep the canonical selection, skip list, normalizer and status handling. + marker = 'declare -a SELECTED_FILES=()' + text = runner.read_text() + assert text.count(marker) == 1 + text = text.replace(marker, "TEST_FILES=()\nwhile IFS= read -r test_file; do\n" + " TEST_FILES+=(\"$test_file\")\ndone < " + shlex.quote(str(selection)) + + "\n" + marker) + runner.write_text(text) + try: + journal = folder/'subset.jsonl' + with (folder/'subset.log').open('w') as output: + proc = subprocess.run(['bash', str(runner), '--filter', 'test_gap_', + '--journal', str(journal)], cwd=source, env=env, + stdout=output, stderr=subprocess.STDOUT) + if journal.exists(): + for line in journal.read_text().splitlines(): + row = json.loads(line) + if 'status' in row and 'id' in row: results[row['id']] = row['status'] + print(f'{args.arm}/subset: runner exit {proc.returncode}; {len(results)} of {len(selected)} results', flush=True) + finally: + runner.unlink(missing_ok=True) + (folder/'results.json').write_text(json.dumps(results, indent=2)+'\n') + if not results: + raise SystemExit('No tests ran; inspect setup logs') + +if __name__ == '__main__': main() diff --git a/scripts/buffer_b4_validate.py b/scripts/buffer_b4_validate.py new file mode 100644 index 0000000000..d2ab486f36 --- /dev/null +++ b/scripts/buffer_b4_validate.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""B2c/B4 reproducible output checks and interleaved instruction/RSS measurements. + +Builds are separate, external to this script. Compile under CPUs 0-55; invoke +measure through the qb6 measurement lock on CPUs 56-63 with ASLR disabled. +Every artifact is under --hostdir. GC diagnostics run separately so their JSON +formatting is not included in program instruction counts. +""" +import argparse +import ctypes +import json +import os +from pathlib import Path +import re +import statistics +import subprocess +import threading + +PROGRAMS = { + 'tsc': ('tscwork.ts', ['1'], False), + 'zod5k': ('zodwork.ts', ['5000'], False), + 'qsparse': ('qs/parse_nested.ts', ['20000', '1000'], True), + 'qsstr': ('qs/stringify_nested.ts', ['20000', '1000'], True), + 'commander': ('commander/parse_argv.ts', ['5000', '200'], True), + 'hello': ('hello.ts', [], False), + 'fastify': ('fastify/inject.ts', ['500', '30'], True), + 'effect': ('effectwork.ts', [], False), + 'buffer_heavy': ('buffer_heavy.ts', [], False), + 'worker_heavy': ('worker_heavy.ts', [], False), +} +KERNELS = { + 'matmul': 'matmul.ts', + 'prime_sieve': 'prime_sieve.ts', + 'bench_buffer_readwrite': 'bench_buffer_readwrite.ts', + 'ecs_u32': 'ecs_u32.ts', +} +NODE = ['node', '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON', '--experimental-strip-types'] + +def disable_thp(): + if ctypes.CDLL(None, use_errno=True).prctl(41, 1, 0, 0, 0) != 0: + raise OSError(ctypes.get_errno(), 'PR_SET_THP_DISABLE failed') + +def environment(root, arm): + source = root / ('main-src' if arm == 'main' else 'src') + target = root / ('main-target' if arm == 'main' else 'target') + # The no-auto HTTP path rebuilds whenever it finds crate source, even + # when the requested pump features are already in the prebuilt archives. + # Use a source-free workspace marker for compiler invocations so both + # arms consume exactly the coherent archives built outside this script. + prebuilt = root / ('prebuilt-' + arm) + for crate in ['perry-runtime', 'perry-ui-geisterhand']: + (prebuilt / 'crates' / crate).mkdir(parents=True, exist_ok=True) + env = dict(os.environ, CARGO_TARGET_DIR=str(target), PERRY_RUNTIME_DIR=str(target/'release'), + PERRY_WORKSPACE_ROOT=str(prebuilt), RUST_TEST_THREADS='1', CARGO_BUILD_JOBS='8', + PERRY_NO_AUTO_OPTIMIZE='1', PERRY_NO_CACHE='1', PERRY_SKIP_BUILD='1', + PERRY_ALLOW_PERRY_FEATURES='1', TMPDIR=str(root/'tmp'), RAYON_NUM_THREADS=os.environ.get('PERRY_VERIFY_RAYON_THREADS', '8'), + PERRY_MODULE_JOBS='1', PERRY_CODEGEN_UNIT_JOBS='1', + PERRY_FORCE_WELL_KNOWN='http,net,ws,zlib', + PERF_BUILDID_DIR=str(root/'perf-buildid'), XDG_CACHE_HOME=str(root/'cache')) + env.pop('PERRY_GC_DIAG', None) + env.pop('PERRY_GC_TRACE', None) + return source, target, env + +def run(cmd, cwd, env, prefix, timeout=1800): + prefix.parent.mkdir(parents=True, exist_ok=True) + try: + result = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, timeout=timeout, + preexec_fn=disable_thp if env.get('MIMALLOC_ALLOW_THP') == '0' else None) + except subprocess.TimeoutExpired as error: + result = subprocess.CompletedProcess(cmd, 124, error.stdout or b'', error.stderr or b'') + Path(str(prefix)+'.out').write_bytes(result.stdout) + Path(str(prefix)+'.err').write_bytes(result.stderr) + return result + +def normalize_kernel(data): + return re.sub(rb'(?m)^(matrix_multiply|matmul|prime_sieve|buffer_readwrite):\d+', rb'\1: