From 41afa5c6dce6f171f76290cc3eccd48c8339fd0a Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 14 Aug 2026 04:09:52 +0900 Subject: [PATCH 1/8] gc: root every SRE group selector before slicing --- pyre/extra_tests/snippets/stdlib_re.py | 9 +++++++++ pyre/pyre-interpreter/src/module/_sre/interp_sre.rs | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pyre/extra_tests/snippets/stdlib_re.py b/pyre/extra_tests/snippets/stdlib_re.py index 8613ddd30fc..eff3ef40bf0 100644 --- a/pyre/extra_tests/snippets/stdlib_re.py +++ b/pyre/extra_tests/snippets/stdlib_re.py @@ -62,6 +62,15 @@ assert re.compile("(a)(bc)").match("abc")[1] == "a" assert re.compile("a(b)(?Pc)d").match("abcd").groupdict() == {"a": "c"} +# Keep later dynamically-created selectors alive while each earlier group +# slice allocates. Repetition crosses the moving nursery boundary, exercising +# the gateway argument-rooting path rather than only immortal integer indices. +named_match = re.compile("(?Pa)(?Pbc)").match("abc") +for i in range(4096): + left = ("left" + str(i))[:4] + right = ("right" + str(i))[:5] + assert named_match.group(left, right) == ("a", "bc") + # test op branch assert re.compile(r"((?=\d|\.\d)(?P\d*)|a)").match("123.2132").group() == "123" diff --git a/pyre/pyre-interpreter/src/module/_sre/interp_sre.rs b/pyre/pyre-interpreter/src/module/_sre/interp_sre.rs index 452d1c0666f..6f62cf60ee2 100644 --- a/pyre/pyre-interpreter/src/module/_sre/interp_sre.rs +++ b/pyre/pyre-interpreter/src/module/_sre/interp_sre.rs @@ -1743,9 +1743,18 @@ fn sre_match_group(args: &[PyObjectRef]) -> Result return Ok(unsafe { slice_w(m, span, w_none()) }); } let _roots = pyre_object::gc_roots::push_roots(); - let m = RootedObject::pin(m as PyObjectRef); + // Publish the match and every selector as one live set before performing + // any forwarding query. Besides matching RPython's `args_w` liveness, + // this avoids a foreign collection entering between sequential pins while + // a later dynamically-created group name is still unpublished. + let args_base = pyre_object::gc_roots::pin_roots(args); + let m = RootedObject(args_base); + // RPython's GC transform keeps every entry in `args_w` live across each + // `slice_w` allocation. The gateway's native argument copy is not a GC + // root, so read selectors back from that live set after every allocation. let mut results: Vec = Vec::with_capacity(group_args.len()); - for &w_arg in group_args { + for i in 0..group_args.len() { + let w_arg = pyre_object::gc_roots::shadow_stack_get(args_base + 1 + i); let span = do_span(m.get() as *const W_SRE_Match, Some(w_arg))?; results.push(RootedObject::pin(unsafe { slice_w(m.get() as *const W_SRE_Match, span, w_none()) From d39dcbfadd3e1b3579c447e5eca29e9717e88495 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 14 Aug 2026 04:40:05 +0900 Subject: [PATCH 2/8] gc: preserve GIL across sandbox heap dumps --- pyre/pyre-interpreter/src/host_seam.rs | 27 +++++++ pyre/pyre-interpreter/src/module/gc/mod.rs | 2 +- pyre/pyre-sandbox/tests/e2e_interact.rs | 93 +++++++++++++++++++--- 3 files changed, 111 insertions(+), 11 deletions(-) diff --git a/pyre/pyre-interpreter/src/host_seam.rs b/pyre/pyre-interpreter/src/host_seam.rs index dd321ab6824..cb608558822 100644 --- a/pyre/pyre-interpreter/src/host_seam.rs +++ b/pyre/pyre-interpreter/src/host_seam.rs @@ -510,6 +510,33 @@ declare_seam! { sleep(seconds: f64) -> unit = "ll_time.ll_time_sleep"; } +/// The sandbox trampoline for `inspector.py:89 raw_os_write`, whose +/// `_nowrapper=True` declaration deliberately keeps the GIL held. Heap +/// dumping calls this while it owns the collector's `&mut MiniMarkGC` and, +/// when another mutator exists, the stop-the-world guard. The ordinary +/// [`ops::write`] releases the GIL around the controller round trip; a waiting +/// mutator can then acquire it and park at the active STW safepoint while still +/// holding it, preventing the dump owner from reacquiring the GIL forever. +/// +/// Keep this boundary private to the collector hook. Every ordinary host +/// operation must continue to use the release-GIL seam generated above. +#[cfg(feature = "sandbox")] +pub(crate) fn raw_heap_dump_write(fd: i32, data: &[u8]) -> SeamResult { + let args = [ + MarshalValue::Int(fd as i64), + MarshalValue::Str(data.to_vec()), + ]; + let result = client::syscall( + "ll_os.ll_os_write", + &args, + pyre_sandbox::protocol::ResultKind::Int, + )?; + match result { + SyscallResult::Int(written) => Ok(written), + _ => Err(SeamError::Runtime), + } +} + /// Read one process-environment value through the host seam. /// /// Keep this target-independent entry point at the module root: Charon's diff --git a/pyre/pyre-interpreter/src/module/gc/mod.rs b/pyre/pyre-interpreter/src/module/gc/mod.rs index 770be5ffea4..81fcaf76838 100644 --- a/pyre/pyre-interpreter/src/module/gc/mod.rs +++ b/pyre/pyre-interpreter/src/module/gc/mod.rs @@ -997,7 +997,7 @@ fn gc_call_method( #[cfg(feature = "sandbox")] fn heap_dump_write_via_host(fd: i32, bytes: &[u8]) -> Result { - crate::host_seam::ops::write(fd, bytes) + crate::host_seam::raw_heap_dump_write(fd, bytes) .map(|written| written as isize) // A non-OS seam failure still needs an errno. Use the collector's code // for targets and failure modes that cannot supply one. diff --git a/pyre/pyre-sandbox/tests/e2e_interact.rs b/pyre/pyre-sandbox/tests/e2e_interact.rs index b8eef8f9c8c..ae051e95e95 100644 --- a/pyre/pyre-sandbox/tests/e2e_interact.rs +++ b/pyre/pyre-sandbox/tests/e2e_interact.rs @@ -31,10 +31,32 @@ fn workspace_root() -> PathBuf { p } -/// Build the sandbox `pyre` binary once and return its path. Cargo skips the -/// work when the binary is already current, so repeated runs are cheap. -fn build_sandbox_pyre() -> PathBuf { +/// Build a trusted, non-sandbox controller and the sandbox child, then return +/// both paths. A sandbox-linked `pyre` cannot act as its own controller: even +/// its startup environment reads are protocol trampolines. The dedicated +/// dynasm binary stays linked to the real host while the plain `pyre` binary is +/// rebuilt as the untrusted child. +fn build_sandbox_pyre() -> (PathBuf, PathBuf) { let root = workspace_root(); + let controller_status = Command::new(env!("CARGO")) + .current_dir(&root) + .args([ + "build", + "--release", + "-p", + "pyrex", + "--bin", + "pyre-dynasm", + "--no-default-features", + "--features", + "dynasm", + ]) + .status() + .expect("spawn cargo build for sandbox controller"); + assert!( + controller_status.success(), + "building sandbox controller failed" + ); let status = Command::new(env!("CARGO")) .current_dir(&root) .args([ @@ -50,16 +72,27 @@ fn build_sandbox_pyre() -> PathBuf { .status() .expect("spawn cargo build"); assert!(status.success(), "building sandbox pyre failed"); + let controller = root.join("target/release/pyre-dynasm"); let bin = root.join("target/release/pyre"); + assert!( + controller.is_file(), + "sandbox controller missing at {}", + controller.display() + ); assert!(bin.is_file(), "sandbox pyre missing at {}", bin.display()); - bin + (controller, bin) } /// Run `pyre interact --tmp -c