Skip to content

Commit 066f9d5

Browse files
proggeramlugRalph Küpper
andauthored
batch: land #8800, #8801, #8802 (#8805)
* test: normalize method literal GC arms * docs(runtime): pin captured cache hint bounds * runtime: allocate native async-resolution promises in malloc space Native async resolutions (fetch/db/ws/etc.) created their Promise via the nursery arena (js_promise_new), pinned it, and handed the raw pointer to a tokio worker. A copying-minor from-space flip wipes a nursery resident regardless of its pin flag (the flip resets eden/survivor blocks wholesale; only root-reachable pins force the fallback), so the worker's later resolution dereferenced a reclaimed Promise -> SIGSEGV in js_stdlib_process_pending. Allocate these promises via js_promise_new_cross_thread (malloc space, non-moving; both sweep paths honor GC_FLAG_PINNED). Re-export the symbol from perry-runtime and switch every native-binding caller. Found getting the compiled Claude Code CLI to run natively; confirmed to remove the js_stdlib_process_pending fault under PERRY_GC_PROTECT_FROMSPACE. Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF * runtime: root async-from-sync iterator objects across GC safepoints The %AsyncFromSyncIteratorPrototype% helpers created nursery objects (the wrapper, the outer promise, reaction closures, the iter result, the captured sync iterator) and held them as raw pointers across later allocations and JS calls. Under the default-on moving young-gen scavenge those raw pointers are invalidated (evacuated, or swept when unreachable), so a later use dereferenced a stale/poison receiver. Root every live young value in a RuntimeHandleScope and re-read it through the handle after each allocation/JS call; use the long-lived string allocator for the immortal property-name keys. Covers wrap_iterator, install_next/method, next/return/throw, call/call_raw, continue, fulfilled/rejected_value, and iter_result. Claude-Session: https://claude.ai/code/session_01TwxRkALrR9HKSF1zKLSTAF * chore: fmt + raw-handle ceiling for array/iterator.rs (#8801) --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent 71a10f7 commit 066f9d5

28 files changed

Lines changed: 450 additions & 216 deletions

crates/perry-runtime/src/array/iterator.rs

Lines changed: 253 additions & 64 deletions
Large diffs are not rendered by default.

crates/perry-runtime/src/closure/alloc.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,10 @@ pub(crate) fn test_captured_singleton_closure_cache_entries(
488488
/// covers the per-batch fan-out shape (50 promises) found in
489489
/// `benchmarks/app-patterns/kernels/promise_all_chains.ts`.
490490
const MAX_CAPTURED_CLOSURE_SLOTS: usize = 64;
491+
const _: () = assert!(
492+
MAX_CAPTURED_CLOSURE_SLOTS <= u8::MAX as usize,
493+
"hint_indices_plus_one stores an entry index plus one in a u8"
494+
);
491495

492496
/// Per-`func_ptr` cache miss-streak counter for the adaptive bypass.
493497
/// Closures whose captures change every call (per-call boxes for
@@ -581,9 +585,8 @@ pub extern "C" fn js_closure_alloc_with_captures_singleton(
581585
}
582586
crate::promise::bump(&CLOSURE_CAP_SINGLETON_MISS);
583587

584-
// Slow path: allocate, populate captures, insert into cache as
585-
// the most-recent entry. If the slot list is full, drop the
586-
// least-recent (back of the Vec).
588+
// Slow path: allocate, populate captures, and insert with a fresh usage
589+
// timestamp. If the entry list is full, replace its oldest timestamp.
587590
let capture_scope = crate::gc::RuntimeHandleScope::new();
588591
let capture_handles: Vec<_> = captures_slice
589592
.iter()

crates/perry-runtime/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,8 @@ pub use object::{
307307
};
308308
pub use promise::{js_is_promise, js_promise_run_microtasks, js_promise_state, js_promise_value};
309309
pub use promise::{
310-
js_promise_mark_internally_handled, js_promise_new, js_promise_reject, js_promise_rejected,
311-
js_promise_resolve, js_promise_resolved,
310+
js_promise_mark_internally_handled, js_promise_new, js_promise_new_cross_thread,
311+
js_promise_reject, js_promise_rejected, js_promise_resolve, js_promise_resolved,
312312
};
313313
pub use string::js_string_from_bytes;
314314
pub use value::{

crates/perry-stdlib/src/argon2.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ use argon2::{
99
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
1010
Argon2,
1111
};
12-
use perry_runtime::{js_promise_new, js_string_from_bytes, Promise, StringHeader};
12+
use perry_runtime::{js_promise_new_cross_thread, js_string_from_bytes, Promise, StringHeader};
1313

1414
/// argon2.hash(password) -> Promise<string>
1515
///
1616
/// Hash a password using Argon2id with default parameters.
1717
#[no_mangle]
1818
pub unsafe extern "C" fn js_argon2_hash(password_ptr: *const StringHeader) -> *mut Promise {
19-
let promise = js_promise_new();
19+
let promise = js_promise_new_cross_thread();
2020

2121
let password = match string_from_header(password_ptr) {
2222
Some(p) => p,
@@ -77,7 +77,7 @@ pub unsafe extern "C" fn js_argon2_verify(
7777
hash_ptr: *const StringHeader,
7878
password_ptr: *const StringHeader,
7979
) -> *mut Promise {
80-
let promise = js_promise_new();
80+
let promise = js_promise_new_cross_thread();
8181

8282
let hash_str = match string_from_header(hash_ptr) {
8383
Some(h) => h,

crates/perry-stdlib/src/axios.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::common::{
77
get_handle, register_handle, spawn_for_promise, string_from_header_lossy as string_from_header,
88
Handle,
99
};
10-
use perry_runtime::{js_promise_new, js_string_from_bytes, Promise, StringHeader};
10+
use perry_runtime::{js_promise_new_cross_thread, js_string_from_bytes, Promise, StringHeader};
1111

1212
/// #598: read the body argument as a JSON string. Strings pass
1313
/// through as-is; everything else is JSON.stringify'd via the
@@ -49,7 +49,7 @@ unsafe fn request_without_body(
4949
url_ptr: *const StringHeader,
5050
method: reqwest::Method,
5151
) -> *mut Promise {
52-
let promise = js_promise_new();
52+
let promise = js_promise_new_cross_thread();
5353

5454
let url = match string_from_header(url_ptr) {
5555
Some(u) => u,
@@ -119,7 +119,7 @@ pub unsafe extern "C" fn js_axios_options(url_ptr: *const StringHeader) -> *mut
119119
/// axios.post(url, data) -> Promise<AxiosResponse>
120120
#[no_mangle]
121121
pub unsafe extern "C" fn js_axios_post(url_ptr: *const StringHeader, data: f64) -> *mut Promise {
122-
let promise = js_promise_new();
122+
let promise = js_promise_new_cross_thread();
123123

124124
let url = match string_from_header(url_ptr) {
125125
Some(u) => u,
@@ -186,7 +186,7 @@ pub unsafe extern "C" fn js_axios_post(url_ptr: *const StringHeader, data: f64)
186186
/// axios.put(url, data) -> Promise<AxiosResponse>
187187
#[no_mangle]
188188
pub unsafe extern "C" fn js_axios_put(url_ptr: *const StringHeader, data: f64) -> *mut Promise {
189-
let promise = js_promise_new();
189+
let promise = js_promise_new_cross_thread();
190190

191191
let url = match string_from_header(url_ptr) {
192192
Some(u) => u,
@@ -250,7 +250,7 @@ pub unsafe extern "C" fn js_axios_put(url_ptr: *const StringHeader, data: f64) -
250250
/// axios.delete(url) -> Promise<AxiosResponse>
251251
#[no_mangle]
252252
pub unsafe extern "C" fn js_axios_delete(url_ptr: *const StringHeader) -> *mut Promise {
253-
let promise = js_promise_new();
253+
let promise = js_promise_new_cross_thread();
254254

255255
let url = match string_from_header(url_ptr) {
256256
Some(u) => u,
@@ -305,7 +305,7 @@ pub unsafe extern "C" fn js_axios_delete(url_ptr: *const StringHeader) -> *mut P
305305
/// axios.patch(url, data) -> Promise<AxiosResponse>
306306
#[no_mangle]
307307
pub unsafe extern "C" fn js_axios_patch(url_ptr: *const StringHeader, data: f64) -> *mut Promise {
308-
let promise = js_promise_new();
308+
let promise = js_promise_new_cross_thread();
309309

310310
let url = match string_from_header(url_ptr) {
311311
Some(u) => u,

crates/perry-stdlib/src/bcrypt.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub unsafe extern "C" fn js_bcrypt_hash(
1515
password_ptr: *const StringHeader,
1616
salt_rounds: f64,
1717
) -> *mut perry_runtime::Promise {
18-
let promise = perry_runtime::js_promise_new();
18+
let promise = perry_runtime::js_promise_new_cross_thread();
1919
let promise_ptr = promise as usize;
2020

2121
let password = match string_from_header(password_ptr) {
@@ -79,7 +79,7 @@ pub unsafe extern "C" fn js_bcrypt_compare(
7979
password_ptr: *const StringHeader,
8080
hash_ptr: *const StringHeader,
8181
) -> *mut perry_runtime::Promise {
82-
let promise = perry_runtime::js_promise_new();
82+
let promise = perry_runtime::js_promise_new_cross_thread();
8383
let promise_ptr = promise as usize;
8484

8585
let password = match string_from_header(password_ptr) {
@@ -142,7 +142,7 @@ pub unsafe extern "C" fn js_bcrypt_compare(
142142
/// bcrypt.genSalt(rounds) -> Promise<string>
143143
#[no_mangle]
144144
pub unsafe extern "C" fn js_bcrypt_gen_salt(rounds: f64) -> *mut perry_runtime::Promise {
145-
let promise = perry_runtime::js_promise_new();
145+
let promise = perry_runtime::js_promise_new_cross_thread();
146146
let promise_ptr = promise as usize;
147147
let cost = rounds as u32;
148148

crates/perry-stdlib/src/common/async_bridge.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,18 @@ unsafe fn unpin_promise_after_native_resolution(promise_ptr: usize) {
8383
#[inline]
8484
pub unsafe fn js_promise_new_for_native_resolution() -> *mut perry_runtime::Promise {
8585
ensure_gc_scanner_registered();
86-
let p = perry_runtime::js_promise_new();
86+
// #8770: allocate in MALLOC space (non-moving), not the nursery arena. A
87+
// native-resolution promise is handed to a tokio worker as a raw `usize` and,
88+
// until its resolution is queued into PENDING_RESOLUTIONS (which the root
89+
// scanner visits), it is reachable only through that worker-thread capture —
90+
// invisible to the main-thread copying minor. A nursery resident in that
91+
// window is wiped by the from-space flip REGARDLESS of its PIN flag (the flip
92+
// resets eden/survivor blocks wholesale; only root-reachable pins force the
93+
// fallback — see `js_promise_new_cross_thread`). Then `js_stdlib_process_
94+
// pending` unpins/resolves through the stale pointer and faults on the
95+
// reclaimed header. Malloc space is non-moving and both sweep paths honor
96+
// GC_FLAG_PINNED, so the pin actually protects it there.
97+
let p = perry_runtime::js_promise_new_cross_thread();
8798
pin_promise_for_native_resolution(p as usize);
8899
p
89100
}

crates/perry-stdlib/src/container/backend_ctl.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub use types::{
66
};
77

88
pub use backend::{detect_backend, ContainerBackend};
9-
use perry_runtime::{js_promise_new, Promise, StringHeader};
9+
use perry_runtime::{js_promise_new_cross_thread, Promise, StringHeader};
1010
use std::collections::HashMap;
1111
use std::sync::Arc;
1212
use std::sync::OnceLock;
@@ -77,7 +77,7 @@ pub unsafe extern "C" fn js_container_getBackend() -> *const StringHeader {
7777
/// FFI: js_container_detectBackend() -> *mut Promise
7878
#[no_mangle]
7979
pub unsafe extern "C" fn js_container_detectBackend() -> *mut Promise {
80-
let promise = js_promise_new();
80+
let promise = js_promise_new_cross_thread();
8181
crate::common::spawn_for_promise_deferred(
8282
promise as *mut u8,
8383
async move {
@@ -199,7 +199,7 @@ pub unsafe extern "C" fn js_container_selectBackendFor(
199199
/// await setBackends(ready.map(b => b.name));
200200
#[no_mangle]
201201
pub unsafe extern "C" fn js_container_getAvailableBackends() -> *mut Promise {
202-
let promise = js_promise_new();
202+
let promise = js_promise_new_cross_thread();
203203
crate::common::spawn_for_promise_deferred(
204204
promise as *mut u8,
205205
async move {
@@ -249,7 +249,7 @@ pub unsafe extern "C" fn js_container_getBackendPriority() -> *const StringHeade
249249
/// - `"backend probe failed: <reason>"`
250250
#[no_mangle]
251251
pub unsafe extern "C" fn js_container_setBackend(name_ptr: *const StringHeader) -> *mut Promise {
252-
let promise = js_promise_new();
252+
let promise = js_promise_new_cross_thread();
253253
let name = match string_from_header(name_ptr) {
254254
Some(s) => s,
255255
None => {
@@ -328,7 +328,7 @@ pub unsafe extern "C" fn js_container_setBackend(name_ptr: *const StringHeader)
328328
pub unsafe extern "C" fn js_container_setBackends(
329329
names_json_ptr: *const StringHeader,
330330
) -> *mut Promise {
331-
let promise = js_promise_new();
331+
let promise = js_promise_new_cross_thread();
332332
let names_json = match string_from_header(names_json_ptr) {
333333
Some(s) => s,
334334
None => {

crates/perry-stdlib/src/container/compose_ffi.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub use types::{
66
};
77

88
pub use backend::{detect_backend, ContainerBackend};
9-
use perry_runtime::{js_promise_new, Promise, StringHeader};
9+
use perry_runtime::{js_promise_new_cross_thread, Promise, StringHeader};
1010
use std::collections::HashMap;
1111
use std::sync::Arc;
1212
use std::sync::OnceLock;
@@ -19,7 +19,7 @@ pub unsafe extern "C" fn js_container_compose_start(
1919
handle: f64,
2020
services_json_ptr: *const StringHeader,
2121
) -> *mut Promise {
22-
let promise = js_promise_new();
22+
let promise = js_promise_new_cross_thread();
2323
let handle_id = handle_id_from_f64(handle);
2424

2525
let engine = match types::get_compose_handle(handle_id as u64) {
@@ -57,7 +57,7 @@ pub unsafe extern "C" fn js_container_compose_stop(
5757
handle: f64,
5858
services_json_ptr: *const StringHeader,
5959
) -> *mut Promise {
60-
let promise = js_promise_new();
60+
let promise = js_promise_new_cross_thread();
6161
let handle_id = handle_id_from_f64(handle);
6262

6363
let engine = match types::get_compose_handle(handle_id as u64) {
@@ -95,7 +95,7 @@ pub unsafe extern "C" fn js_container_compose_restart(
9595
handle: f64,
9696
services_json_ptr: *const StringHeader,
9797
) -> *mut Promise {
98-
let promise = js_promise_new();
98+
let promise = js_promise_new_cross_thread();
9999
let handle_id = handle_id_from_f64(handle);
100100

101101
let engine = match types::get_compose_handle(handle_id as u64) {
@@ -131,7 +131,7 @@ pub unsafe extern "C" fn js_container_compose_restart(
131131
/// FFI: `js_container_compose_config(handle: f64) -> *mut Promise`
132132
#[no_mangle]
133133
pub unsafe extern "C" fn js_container_compose_config(handle: f64) -> *mut Promise {
134-
let promise = js_promise_new();
134+
let promise = js_promise_new_cross_thread();
135135
let handle_id = handle_id_from_f64(handle);
136136

137137
let engine = match types::get_compose_handle(handle_id as u64) {
@@ -164,7 +164,7 @@ pub unsafe extern "C" fn js_container_compose_config(handle: f64) -> *mut Promis
164164
pub unsafe extern "C" fn js_container_composeUp(
165165
spec_ptr: *const perry_runtime::StringHeader,
166166
) -> *mut Promise {
167-
let promise = js_promise_new();
167+
let promise = js_promise_new_cross_thread();
168168

169169
let spec = match types::parse_compose_spec(spec_ptr) {
170170
Ok(s) => s,
@@ -283,7 +283,7 @@ pub unsafe extern "C" fn js_container_compose_down(
283283
handle: f64,
284284
opts_ptr: *const StringHeader,
285285
) -> *mut Promise {
286-
let promise = js_promise_new();
286+
let promise = js_promise_new_cross_thread();
287287
let handle_id = handle_id_from_f64(handle);
288288

289289
let opts_json = unsafe { string_from_header(opts_ptr) };
@@ -330,7 +330,7 @@ pub unsafe extern "C" fn js_container_compose_down(
330330
/// FFI: `js_container_compose_ps(handle: f64) -> *mut Promise`
331331
#[no_mangle]
332332
pub unsafe extern "C" fn js_container_compose_ps(handle: f64) -> *mut Promise {
333-
let promise = js_promise_new();
333+
let promise = js_promise_new_cross_thread();
334334
let handle_id = handle_id_from_f64(handle);
335335

336336
let engine = match types::get_compose_handle(handle_id as u64) {
@@ -376,7 +376,7 @@ pub unsafe extern "C" fn js_container_compose_logs(
376376
service_ptr: *const StringHeader,
377377
tail: f64,
378378
) -> *mut Promise {
379-
let promise = js_promise_new();
379+
let promise = js_promise_new_cross_thread();
380380
let handle_id = handle_id_from_f64(handle);
381381

382382
let engine = match types::get_compose_handle(handle_id as u64) {
@@ -427,7 +427,7 @@ pub unsafe extern "C" fn js_container_compose_exec(
427427
service_ptr: *const StringHeader,
428428
cmd_json_ptr: *const StringHeader,
429429
) -> *mut Promise {
430-
let promise = js_promise_new();
430+
let promise = js_promise_new_cross_thread();
431431
let handle_id = handle_id_from_f64(handle);
432432

433433
let engine = match types::get_compose_handle(handle_id as u64) {

crates/perry-stdlib/src/container/images.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub use types::{
66
};
77

88
pub use backend::{detect_backend, ContainerBackend};
9-
use perry_runtime::{js_promise_new, Promise, StringHeader};
9+
use perry_runtime::{js_promise_new_cross_thread, Promise, StringHeader};
1010
use std::collections::HashMap;
1111
use std::sync::Arc;
1212
use std::sync::OnceLock;
@@ -19,7 +19,7 @@ use std::sync::OnceLock;
1919
pub unsafe extern "C" fn js_container_pullImage(
2020
reference_ptr: *const StringHeader,
2121
) -> *mut Promise {
22-
let promise = js_promise_new();
22+
let promise = js_promise_new_cross_thread();
2323

2424
let reference = match string_from_header(reference_ptr) {
2525
Some(s) => s,
@@ -52,7 +52,7 @@ pub unsafe extern "C" fn js_container_pullImage(
5252
/// FFI: js_container_listImages() -> *mut Promise
5353
#[no_mangle]
5454
pub unsafe extern "C" fn js_container_listImages() -> *mut Promise {
55-
let promise = js_promise_new();
55+
let promise = js_promise_new_cross_thread();
5656

5757
// Resolves with a JSON-encoded `ImageInfo[]` string.
5858
crate::common::spawn_for_promise_deferred(
@@ -78,7 +78,7 @@ pub unsafe extern "C" fn js_container_build(
7878
spec_ptr: *const StringHeader,
7979
image_name_ptr: *const StringHeader,
8080
) -> *mut Promise {
81-
let promise = js_promise_new();
81+
let promise = js_promise_new_cross_thread();
8282

8383
let spec_json = string_from_header(spec_ptr).unwrap_or_else(|| "{}".to_string());
8484
let image_name = string_from_header(image_name_ptr).unwrap_or_default();
@@ -108,7 +108,7 @@ pub unsafe extern "C" fn js_container_removeImage(
108108
reference_ptr: *const StringHeader,
109109
force: i32,
110110
) -> *mut Promise {
111-
let promise = js_promise_new();
111+
let promise = js_promise_new_cross_thread();
112112

113113
let reference = match string_from_header(reference_ptr) {
114114
Some(s) => s,

0 commit comments

Comments
 (0)