diff --git a/pyre/bench/synth/global_store_plain_dict_globals.wasm.jitstats b/pyre/bench/synth/global_store_plain_dict_globals.wasm.jitstats index 4e6eded23bb..67945696705 100644 --- a/pyre/bench/synth/global_store_plain_dict_globals.wasm.jitstats +++ b/pyre/bench/synth/global_store_plain_dict_globals.wasm.jitstats @@ -11,5 +11,5 @@ field_pos_spec_misplaced=0 guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=6 +loops_compiled=4 retraces_compiled=0 diff --git a/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats b/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats index 65633b9393d..191a4bff22e 100644 --- a/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats +++ b/pyre/bench/synth/pickle_terminal_raise_resume.wasm.jitstats @@ -11,5 +11,5 @@ field_pos_spec_misplaced=0 guard_failures=339 internal_compile_panics=0 loops_aborted=9 -loops_compiled=71 +loops_compiled=69 retraces_compiled=0 diff --git a/pyre/extra_tests/parity_tests/lsprof_enable_frees_tool_id_on_failure.py b/pyre/extra_tests/parity_tests/lsprof_enable_frees_tool_id_on_failure.py new file mode 100644 index 00000000000..b4a4a65e43d --- /dev/null +++ b/pyre/extra_tests/parity_tests/lsprof_enable_frees_tool_id_on_failure.py @@ -0,0 +1,51 @@ +# CPython-suite gap: test_cprofile and test_profile only ever pass real bools +# to `enable`, so neither exercises the failing-conversion path, and the leak +# they would expose is process-wide rather than per-test. +# parity-tests reason: the tool id is a process singleton, so the damage shows +# up on a *later, unrelated* profiler; that cross-object effect is what this +# checks, on both backends, on every leg. + +"""A failed `Profiler.enable` must not keep the profiler tool id. + +`enable` takes `subcalls` and `builtins` through the index/bool protocol, so +an argument whose `__bool__` raises makes the call fail. The tool id is a +single process-wide slot: if the failed call has already claimed it and +nothing releases it, `disable` cannot help -- it is a no-op while the +profiler never became enabled -- and every later `enable`, on any profiler +object, reports that another tool is active. + +The discriminator is therefore a *second, independent* profiler enabling +successfully after the first one's `enable` raised. +""" + +import _lsprof + + +class Raises: + def __bool__(self): + raise ValueError("no truth value") + + +def main(): + first = _lsprof.Profiler() + try: + first.enable(Raises()) + except ValueError: + pass + else: + raise AssertionError("enable() accepted an argument whose __bool__ raises") + + # The failed call must have left the tool id free. + second = _lsprof.Profiler() + second.enable() + second.disable() + + # And the id must still be reusable after a clean enable/disable pair. + third = _lsprof.Profiler() + third.enable() + third.disable() + + print("OK") + + +main() diff --git a/pyre/extra_tests/parity_tests/simplequeue_put_defaults_replay.py b/pyre/extra_tests/parity_tests/simplequeue_put_defaults_replay.py new file mode 100644 index 00000000000..9770f765ae6 --- /dev/null +++ b/pyre/extra_tests/parity_tests/simplequeue_put_defaults_replay.py @@ -0,0 +1,73 @@ +# CPython-suite gap: test_queue reaches this only through +# `CSimpleQueueTest.test_reentrancy`, which reads as a finalizer-ordering test +# and reports it as a 10000-element list diff -- it names neither the replayed +# call nor the argument shape that causes it. +# parity-tests reason: the defect is a miscompile, so it needs a fixture that +# runs the loop hot on both backends and checks a count that the interpreter +# and the compiled trace must agree on. + +"""A hot loop must call its producer exactly once per iteration. + +`SimpleQueue.put` is declared `put(item, block=True, timeout=None)`; the two +trailing arguments are accepted and ignored, because an unbounded queue never +blocks. Writing `put(v)` therefore leaves the call site to fill both defaults +in, and that is the shape this guards. + +When such a call is compiled into a hot loop, the call feeding it must not be +re-executed. `Counter.next` is deliberately side-effecting, so a replay shows +up twice over: as a producer count that exceeds the iteration count, and as a +value that is generated but never queued -- which shifts every later result by +one rather than reordering a pair. + +A plain function call in the producer position does not reproduce it; the call +has to go through the method path, which is why this uses a bound method. + +The count is small enough to stay fast and large enough for the loop to be +compiled and left at least once. +""" + +import queue + +LIMIT = 1500 + + +class Counter: + def __init__(self): + self.n = 0 + + def next(self): + value = self.n + self.n += 1 + return value + + +def main(): + q = queue.SimpleQueue() + counter = Counter() + results = [] + + while True: + q.put(counter.next()) + results.append(q.get()) + if results[-1] >= LIMIT: + break + + assert counter.n == len(results), ( + "the producer ran more often than the loop body", + counter.n, + len(results), + ) + expected = list(range(LIMIT + 1)) + assert results == expected, ( + "a produced value never reached the queue", + next( + (i, results[i], expected[i]) + for i in range(len(results)) + if results[i] != expected[i] + ), + ) + + print("OK") + + +main() diff --git a/pyre/pyre-interpreter/src/_structseq.rs b/pyre/pyre-interpreter/src/_structseq.rs index 0e32c780707..b85a1dc4dbf 100644 --- a/pyre/pyre-interpreter/src/_structseq.rs +++ b/pyre/pyre-interpreter/src/_structseq.rs @@ -339,7 +339,7 @@ fn structseq_setattr(args: &[PyObjectRef]) -> Result { /// dict])` constructor. The first `n_sequence_fields` items fill the /// tuple body; any surplus positional items, then the optional dict, then /// `None` defaults, fill the named-only extra fields. -fn structseq_descr_new(args: &[PyObjectRef]) -> Result { +pub(crate) fn structseq_descr_new(args: &[PyObjectRef]) -> Result { if args.len() < 2 || args[1].is_null() { return Err(PyError::type_error("structseq() requires class + sequence")); } diff --git a/pyre/pyre-interpreter/src/cpyext/capsule.rs b/pyre/pyre-interpreter/src/cpyext/capsule.rs index 5de4ae15e0d..9278cb05bfe 100644 --- a/pyre/pyre-interpreter/src/cpyext/capsule.rs +++ b/pyre/pyre-interpreter/src/cpyext/capsule.rs @@ -22,7 +22,7 @@ const DESTRUCTOR_KEY: &str = "__pyre_destructor__"; static CAPSULE_TYPE: OnceLock = OnceLock::new(); -fn capsule_type() -> PyObjectRef { +pub(crate) fn capsule_type() -> PyObjectRef { *CAPSULE_TYPE.get_or_init(|| { let tp = crate::typedef::make_builtin_type("PyCapsule", |ns| unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index 67bab1e7af0..7a221b314df 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -205,17 +205,24 @@ pub fn install_current_frame_tls_only(frame: &mut PyFrame) -> CurrentFrameGuard /// Pushing the frame onto the shadow stack lets the root walker forward it in /// place during the collection; `live()` reads the forwarded pointer back. /// This mirrors the JIT eval layer's `FrameRoot`. -pub(crate) struct FrameAnchor { +pub struct FrameAnchor { depth: usize, + /// The shadow stack is per-thread, so a depth taken on one thread names a + /// different slot on another. The marker is what keeps an anchor from + /// being sent or shared across threads now that the type is public. + _not_send: std::marker::PhantomData<*const ()>, } impl FrameAnchor { - pub(crate) fn new(frame: &mut PyFrame) -> Self { + pub fn new(frame: &mut PyFrame) -> Self { let depth = majit_gc::shadow_stack::push(majit_ir::GcRef(frame as *mut PyFrame as usize)); - Self { depth } + Self { + depth, + _not_send: std::marker::PhantomData, + } } - pub(crate) fn live(&self) -> *mut PyFrame { + pub fn live(&self) -> *mut PyFrame { majit_gc::shadow_stack::get(self.depth).0 as *mut PyFrame } } @@ -2276,6 +2283,19 @@ pub(crate) fn finalize_failed_attr_receiver_now(obj: PyObjectRef) -> bool { impl SharedOpcodeHandler for PyFrame { type Value = PyObjectRef; + type Anchor = FrameAnchor; + + fn anchor(&mut self) -> Self::Anchor { + FrameAnchor::new(self) + } + + fn push_anchored(anchor: &Self::Anchor, value: Self::Value) -> Result<(), PyError> { + // A JIT-created frame lives in the nursery and the allocating step may + // have relocated it; push onto the forwarded live frame. + unsafe { &mut *anchor.live() }.push(value); + Ok(()) + } + fn push_value(&mut self, value: Self::Value) -> Result<(), PyError> { self.push(value); Ok(()) diff --git a/pyre/pyre-interpreter/src/host_seam.rs b/pyre/pyre-interpreter/src/host_seam.rs index 578957fb57b..b4f691a93a6 100644 --- a/pyre/pyre-interpreter/src/host_seam.rs +++ b/pyre/pyre-interpreter/src/host_seam.rs @@ -101,7 +101,7 @@ pub mod sys { #[cfg(all(target_os = "linux", target_env = "gnu"))] pub use ::libc::RTLD_DEEPBIND; #[cfg(any(target_os = "linux", target_os = "android"))] - pub use ::libc::{SCHED_BATCH, SCHED_IDLE}; + pub use ::libc::{SCHED_BATCH, SCHED_DEADLINE, SCHED_IDLE, SCHED_NORMAL, SCHED_RESET_ON_FORK}; #[cfg(not(any(target_os = "macos", target_os = "ios")))] pub use ::libc::{SCHED_FIFO, SCHED_OTHER, SCHED_RR}; // `SEEK_HOLE`/`SEEK_DATA` carry the same host list as the table that diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index aff719ea05c..89e0828e874 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -754,6 +754,9 @@ pub fn install_builtin_modules() { crate::module::array::startup_array_module, ); register_builtin_module("_csv", crate::module::_csv::init); + register_builtin_module("_queue", crate::module::_queue::init); + register_builtin_module("_statistics", crate::module::_statistics::init); + register_builtin_module("_types", crate::module::_types::init); register_builtin_module("_json", crate::module::_json::init); register_builtin_module("_tokenize", crate::module::_tokenize::init); // `_scproxy` is built only on macOS, and `urllib.request` reaches it only diff --git a/pyre/pyre-interpreter/src/lib.rs b/pyre/pyre-interpreter/src/lib.rs index 777881dbae5..a2099afcaa1 100644 --- a/pyre/pyre-interpreter/src/lib.rs +++ b/pyre/pyre-interpreter/src/lib.rs @@ -1171,41 +1171,44 @@ pub fn all_subclass_range_aliases() -> Vec()), subclass_range_alias(177, typed::()), subclass_range_alias(178, typed::()), - // Native-only posix aliases 179 and 180 preserve `build_gc`'s rclass + // `_queue.SimpleQueue` is unconditional and carries a native FIFO, so + // it closes the ungated aliases ahead of the target-gated ones. + subclass_range_alias(179, typed::()), + // Native-only posix aliases 180 and 181 preserve `build_gc`'s rclass // registration order after the unconditional aliases. #[cfg(not(target_arch = "wasm32"))] - subclass_range_alias(179, typed::()), + subclass_range_alias(180, typed::()), #[cfg(not(target_arch = "wasm32"))] - subclass_range_alias(180, typed::()), + subclass_range_alias(181, typed::()), // The rustls-backed `_ssl` aliases preserve `build_gc`'s registration // order for `W_SSLContext`, `W_MemoryBIO`, and `W_SSLSession`. #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] - subclass_range_alias(181, typed::()), + subclass_range_alias(182, typed::()), #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] - subclass_range_alias(182, typed::()), + subclass_range_alias(183, typed::()), #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] - subclass_range_alias(183, typed::()), + subclass_range_alias(184, typed::()), #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] - subclass_range_alias(184, typed::()), + subclass_range_alias(185, typed::()), #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] - subclass_range_alias(185, typed::()), + subclass_range_alias(186, typed::()), // `mmap.mmap` follows the optional SSL tail on ordinary Unix builds. // A sandbox build has no `mmap` module at all (`module/mod.rs`), so it // contributes no alias rather than sliding into the vacated SSL slot. #[cfg(all(any(unix, windows), not(feature = "sandbox")))] - subclass_range_alias(186, typed::()), + subclass_range_alias(187, typed::()), // Windows asyncio's Overlapped owner follows mmap at the native tail. // It is a non-subclassable builtin in Python, but still participates // in the rclass hierarchy because its managed header and retained // buffer/result fields are traced by the ordinary object marker. #[cfg(all(windows, feature = "host_env", not(feature = "sandbox")))] - subclass_range_alias(187, typed::()), + subclass_range_alias(188, typed::()), // `_winapi.Overlapped` follows it: a second record of the same kind, // owning its own event and transfer buffer rather than retained // Python objects, so nothing of it is traced beyond the header. #[cfg(all(windows, feature = "host_env", not(feature = "sandbox")))] subclass_range_alias( - 188, + 189, typed::(), ), ] diff --git a/pyre/pyre-interpreter/src/module/_bz2/mod.rs b/pyre/pyre-interpreter/src/module/_bz2/mod.rs index 54b0066d815..d9c89637382 100644 --- a/pyre/pyre-interpreter/src/module/_bz2/mod.rs +++ b/pyre/pyre-interpreter/src/module/_bz2/mod.rs @@ -158,8 +158,17 @@ mod decompressor_methods { "Decompressor is unusable after a previous error", )); } + // A cap too large for the platform's index type is an error, not + // silently unlimited -- only a negative value means unlimited. + let max_length = if max_length < 0 { + None + } else { + Some(usize::try_from(max_length).map_err(|_| { + crate::PyError::overflow_error("Python int too large to convert to C ssize_t") + })?) + }; decompressor - .decompress(&data, usize::try_from(max_length).ok()) + .decompress(&data, max_length) .map_err(bz2_error) } diff --git a/pyre/pyre-interpreter/src/module/_lsprof/mod.rs b/pyre/pyre-interpreter/src/module/_lsprof/mod.rs index 4b9277dd2db..8238b17f145 100644 --- a/pyre/pyre-interpreter/src/module/_lsprof/mod.rs +++ b/pyre/pyre-interpreter/src/module/_lsprof/mod.rs @@ -721,18 +721,33 @@ mod profiler_methods { if self.is_enabled { return Ok(()); } - // `_lsprof.c profiler_enable` claims the profiler tool id before - // touching any of its own state, so a second profiler's `enable` - // reports the conflict and leaves the first one installed. + // `subcalls` and `builtins` are declared `bool`, so their truth + // value is taken while the arguments are parsed -- before + // `profiler_enable` reaches `use_tool_id`. Converting them after + // the claim instead would leave the tool id held when `__bool__` + // raises, and nothing could release it: `disable` returns early + // while `is_enabled` is still false. + let flag = |w: PyObjectRef| -> Result, crate::PyError> { + if unsafe { pyre_object::is_none(w) } { + Ok(None) + } else { + Ok(Some(crate::baseobjspace::is_true(w)?)) + } + }; + let subcalls = flag(w_subcalls)?; + let builtins = flag(w_builtins)?; + // The tool id is claimed before any of the profiler's own state is + // touched, so a second profiler's `enable` reports the conflict and + // leaves the first one installed. crate::module::sys::vm::monitoring_use_tool_id( crate::module::sys::vm::MONITORING_PROFILER_ID, w_str_new("cProfile"), )?; - if !unsafe { pyre_object::is_none(w_subcalls) } { - self.subcalls = crate::baseobjspace::is_true(w_subcalls)?; + if let Some(value) = subcalls { + self.subcalls = value; } - if !unsafe { pyre_object::is_none(w_builtins) } { - self.builtins = crate::baseobjspace::is_true(w_builtins)?; + if let Some(value) = builtins { + self.builtins = value; } self.is_enabled = true; self.total_real_time -= read_real_time(); diff --git a/pyre/pyre-interpreter/src/module/_lzma/mod.rs b/pyre/pyre-interpreter/src/module/_lzma/mod.rs index 1712af2fd0a..753835edec1 100644 --- a/pyre/pyre-interpreter/src/module/_lzma/mod.rs +++ b/pyre/pyre-interpreter/src/module/_lzma/mod.rs @@ -455,8 +455,17 @@ mod decompressor_methods { "Already at end of stream", )); } + // A cap too large for the platform's index type is an error, not + // silently unlimited -- only a negative value means unlimited. + let max_length = if max_length < 0 { + None + } else { + Some(usize::try_from(max_length).map_err(|_| { + crate::PyError::overflow_error("Python int too large to convert to C ssize_t") + })?) + }; decompressor - .decompress(&data, usize::try_from(max_length).ok()) + .decompress(&data, max_length) .map_err(lzma_error) } diff --git a/pyre/pyre-interpreter/src/module/_queue/mod.rs b/pyre/pyre-interpreter/src/module/_queue/mod.rs new file mode 100644 index 00000000000..bede827b3b9 --- /dev/null +++ b/pyre/pyre-interpreter/src/module/_queue/mod.rs @@ -0,0 +1,244 @@ +//! `_queue` accelerator module. + +use pyre_object::*; +use std::collections::VecDeque; +use std::sync::{Condvar, Mutex, MutexGuard}; +use std::time::{Duration, Instant}; + +#[crate::pyre_class("_queue.SimpleQueue")] +#[derive(Default)] +pub struct W_SimpleQueue { + pub map: *const u8, + pub storage: *mut pyre_object::object_array::ItemsBlock, + queue: Mutex>, + not_empty: Condvar, +} + +const _: () = assert!( + std::mem::offset_of!(W_SimpleQueue, map) + == std::mem::offset_of!(pyre_object::objectobject::W_ObjectObject, map), + "W_SimpleQueue must keep W_ObjectObject's map offset" +); +const _: () = assert!( + std::mem::offset_of!(W_SimpleQueue, storage) + == std::mem::offset_of!(pyre_object::objectobject::W_ObjectObject, storage), + "W_SimpleQueue must keep W_ObjectObject's storage offset" +); + +fn queue_lock<'a>( + mutex: &'a Mutex>, +) -> MutexGuard<'a, VecDeque> { + if let Ok(guard) = mutex.try_lock() { + return guard; + } + let blocked = crate::module::thread::before_external_block(); + let guard = mutex + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + drop(blocked); + guard +} + +/// `_queue_SimpleQueue_get_impl` reads `timeout` only on the blocking path: +/// `block=False` is answered from the queue immediately, so the argument is +/// neither converted nor range-checked there. +fn parse_timeout(block: bool, timeout: PyObjectRef) -> Result, crate::PyError> { + if !block { + return Ok(None); + } + if timeout.is_null() || unsafe { pyre_object::is_none(timeout) } { + return Ok(None); + } + let seconds = crate::baseobjspace::float_w(timeout)?; + if seconds < 0.0 { + return Err(crate::PyError::value_error( + "'timeout' must be a non-negative number", + )); + } + Ok(Some(seconds)) +} + +fn deadline_from_timeout(timeout: Option) -> Option { + timeout.and_then(|seconds| { + if seconds.is_infinite() && seconds.is_sign_positive() { + None + } else if seconds <= 0.0 || seconds.is_nan() { + Some(Instant::now()) + } else { + let capped = seconds.min((i64::MAX / 1_000_000_000) as f64); + Some(Instant::now() + Duration::from_secs_f64(capped)) + } + }) +} + +fn empty_error() -> crate::PyError { + let mut err = crate::PyError::runtime_error(""); + if let Some(cls) = crate::builtins::lookup_exc_class("_queue.Empty") + && let Ok(exc) = crate::builtins::exc_exception_new(&[cls]) + { + err.exc_object = exc; + } + err +} + +fn simplequeue_put(queue: &W_SimpleQueue, item: PyObjectRef) -> PyObjectRef { + // Taking the lock can drop the GIL and rooting is itself a collection + // point, so `item` is read back out of its shadow-stack slot rather than + // from the argument, which a move would have left stale. + let roots = pyre_object::gc_roots::push_roots(); + let base = pyre_object::gc_roots::shadow_stack_len(); + roots.pin_root(item); + let mut guard = queue_lock(&queue.queue); + guard.push_back(pyre_object::gc_roots::shadow_stack_get(base)); + drop(guard); + queue.not_empty.notify_one(); + w_none() +} + +fn simplequeue_get( + queue: &W_SimpleQueue, + block: bool, + timeout: PyObjectRef, +) -> Result { + let timeout = parse_timeout(block, timeout)?; + let mut guard = queue_lock(&queue.queue); + if !block { + return guard.pop_front().ok_or_else(empty_error); + } + let deadline = deadline_from_timeout(timeout); + loop { + if let Some(item) = guard.pop_front() { + return Ok(item); + } + let blocked = crate::module::thread::before_external_block(); + if let Some(deadline) = deadline { + let now = Instant::now(); + if now >= deadline { + drop(blocked); + return Err(empty_error()); + } + let (next_guard, result) = queue + .not_empty + .wait_timeout(guard, deadline - now) + .unwrap_or_else(std::sync::PoisonError::into_inner); + guard = next_guard; + drop(blocked); + if result.timed_out() && guard.is_empty() { + return Err(empty_error()); + } + } else { + guard = queue + .not_empty + .wait(guard) + .unwrap_or_else(std::sync::PoisonError::into_inner); + drop(blocked); + } + } +} + +mod simplequeue_methods { + use super::*; + + #[crate::pyre_methods(weakrefable, unhashable)] + impl W_SimpleQueue { + #[staticmethod] + fn __new__(cls: PyObjectRef, args: &[PyObjectRef]) -> Result { + if args.len() > 1 { + return Err(crate::PyError::type_error( + "_queue.SimpleQueue() takes no arguments", + )); + } + crate::typedef::check_user_subclass(type_object(), cls)?; + let obj = Self::allocate_stable(Self::default()); + unsafe { (*obj).w_class = cls }; + Ok(obj) + } + + /// `block` and `timeout` are accepted and ignored: the queue is + /// unbounded, so a put never blocks. They are named without a leading + /// underscore because the keyword a caller may bind is taken from the + /// parameter's own identifier, and `put(item, block=True, + /// timeout=None)` is the signature. + fn put( + &self, + item: PyObjectRef, + #[default(true)] block: bool, + #[default(w_none())] timeout: PyObjectRef, + ) -> PyObjectRef { + let _ = (block, timeout); + simplequeue_put(self, item) + } + + fn put_nowait(&self, item: PyObjectRef) -> PyObjectRef { + simplequeue_put(self, item) + } + + fn get( + &self, + #[default(true)] block: bool, + #[default(w_none())] timeout: PyObjectRef, + ) -> Result { + simplequeue_get(self, block, timeout) + } + + fn get_nowait(&self) -> Result { + simplequeue_get(self, false, w_none()) + } + + fn empty(&self) -> bool { + queue_lock(&self.queue).is_empty() + } + + fn qsize(&self) -> i64 { + queue_lock(&self.queue).len() as i64 + } + + #[classmethod] + fn __class_getitem__( + cls: PyObjectRef, + item: PyObjectRef, + ) -> Result { + crate::_pypy_generic_alias::generic_alias_class_getitem(&[cls, item]) + } + } +} + +/// Drop the Rust-owned queue storage. +/// +/// # Safety +/// `obj` must be a GC-dead `W_SimpleQueue`. +pub unsafe fn w_simplequeue_dealloc(obj: PyObjectRef) { + unsafe { std::ptr::drop_in_place(obj as *mut W_SimpleQueue) }; +} + +/// Walk the queued items owned by a `W_SimpleQueue`. +/// +/// # Safety +/// `obj_addr` must point at a live `W_SimpleQueue`. +pub unsafe fn w_simplequeue_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { + let queue = unsafe { &mut *(obj_addr as *mut W_SimpleQueue) }; + // `get_mut` is sound because the deque is only ever mutated while holding + // the GIL, so collection cannot overlap a mutation. A thread parked inside + // `queue_lock` or `Condvar::wait` may hold the mutex, but it is not + // touching the deque; it rejoins the RUNNING census before touching it + // again. + let items = queue + .queue + .get_mut() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let (front, back) = items.as_mut_slices(); + for item in front.iter_mut().chain(back.iter_mut()) { + f(item as *mut PyObjectRef as *mut majit_ir::GcRef); + } +} + +crate::py_module! { + "_queue", + interpleveldefs: { + "SimpleQueue" => simplequeue_methods::type_object(), + }, + exceptions: { + "Empty" => crate::builtins::lookup_exc_class("Exception") + .expect("Exception must be installed before _queue init"), + }, +} diff --git a/pyre/pyre-interpreter/src/module/_statistics/mod.rs b/pyre/pyre-interpreter/src/module/_statistics/mod.rs new file mode 100644 index 00000000000..e1cbd5773ca --- /dev/null +++ b/pyre/pyre-interpreter/src/module/_statistics/mod.rs @@ -0,0 +1,134 @@ +//! `_statistics` accelerator module. + +use pyre_object::*; + +fn normal_dist_inv_cdf_impl(p: f64, mu: f64, sigma: f64) -> Result { + // `_statisticsmodule.c` refuses `p` outside the open unit interval and + // checks nothing else -- `sigma` of 0 or below is accepted, and a NaN `p` + // fails both comparisons and comes back out of the approximation as NaN. + // `statistics.py`'s own copy has no check at all, because the only caller + // there, `NormalDist.inv_cdf`, raises before reaching it. + if p <= 0.0 || p >= 1.0 { + return Err(crate::PyError::value_error( + "inv_cdf undefined for these parameters", + )); + } + let q = p - 0.5; + + if q.abs() <= 0.425 { + let r = 0.180625 - q * q; + let num = (((((((2.50908_09287_30122_6727e+3 * r + 3.34305_75583_58812_8105e+4) * r + + 6.72657_70927_00870_0853e+4) + * r + + 4.59219_53931_54987_1457e+4) + * r + + 1.37316_93765_50946_1125e+4) + * r + + 1.97159_09503_06551_4427e+3) + * r + + 1.33141_66789_17843_7745e+2) + * r + + 3.38713_28727_96366_6080e+0) + * q; + let den = ((((((5.22649_52788_52854_5610e+3 * r + 2.87290_85735_72194_2674e+4) * r + + 3.93078_95800_09271_0610e+4) + * r + + 2.12137_94301_58659_5867e+4) + * r + + 5.39419_60214_24751_1077e+3) + * r + + 6.87187_00749_20579_0830e+2) + * r + + 4.23133_30701_60091_1252e+1) + * r + + 1.0; + let x = num / den; + return Ok(mu + (x * sigma)); + } + + let mut r = if q <= 0.0 { p } else { 1.0 - p }; + r = (-r.ln()).sqrt(); + let num; + let den; + if r <= 5.0 { + r -= 1.6; + num = ((((((7.74545_01427_83414_07640e-4 * r + 2.27238_44989_26918_45833e-2) * r + + 2.41780_72517_74506_11770e-1) + * r + + 1.27045_82524_52368_38258e+0) + * r + + 3.64784_83247_63204_60504e+0) + * r + + 5.76949_72214_60691_40550e+0) + * r + + 4.63033_78461_56545_29590e+0) + * r + + 1.42343_71107_49683_57734e+0; + den = ((((((1.05075_00716_44416_84324e-9 * r + 5.47593_80849_95344_94600e-4) * r + + 1.51986_66563_61645_71966e-2) + * r + + 1.48103_97642_74800_74590e-1) + * r + + 6.89767_33498_51000_04550e-1) + * r + + 1.67638_48301_83803_84940e+0) + * r + + 2.05319_16266_37758_82187e+0) + * r + + 1.0; + } else { + r -= 5.0; + num = ((((((2.01033_43992_92288_13265e-7 * r + 2.71155_55687_43487_57815e-5) * r + + 1.24266_09473_88078_43860e-3) + * r + + 2.65321_89526_57612_30930e-2) + * r + + 2.96560_57182_85048_91230e-1) + * r + + 1.78482_65399_17291_33580e+0) + * r + + 5.46378_49111_64114_36990e+0) + * r + + 6.65790_46435_01103_77720e+0; + den = ((((((2.04426_31033_89939_78564e-15 * r + 1.42151_17583_16445_88870e-7) * r + + 1.84631_83175_10054_68180e-5) + * r + + 7.86869_13114_56132_59100e-4) + * r + + 1.48753_61290_85061_48525e-2) + * r + + 1.36929_88092_27358_05310e-1) + * r + + 5.99832_20655_58879_37690e-1) + * r + + 1.0; + } + + let mut x = num / den; + if q < 0.0 { + x = -x; + } + + Ok(mu + (x * sigma)) +} + +#[crate::pyre_function] +fn _normal_dist_inv_cdf(p: f64, mu: f64, sigma: f64) -> Result { + normal_dist_inv_cdf_impl(p, mu, sigma) +} + +pub fn init(ns: PyObjectRef) { + crate::module_ns_store( + ns, + "_normal_dist_inv_cdf", + crate::gateway::with_module( + "_statistics", + crate::make_module_builtin_function_with_arity_and_maybe_sig( + "_normal_dist_inv_cdf", + _normal_dist_inv_cdf, + _normal_dist_inv_cdf_pyre_arity(), + _normal_dist_inv_cdf_pyre_sig(), + ), + ), + ); +} diff --git a/pyre/pyre-interpreter/src/module/_types/mod.rs b/pyre/pyre-interpreter/src/module/_types/mod.rs new file mode 100644 index 00000000000..651f37ada00 --- /dev/null +++ b/pyre/pyre-interpreter/src/module/_types/mod.rs @@ -0,0 +1,160 @@ +//! `_types` native type-object exports. + +use pyre_object::*; +#[cfg(not(all( + feature = "cpyext", + not(feature = "sandbox"), + any(target_os = "macos", target_os = "linux") +)))] +use std::sync::OnceLock; + +fn store(ns: PyObjectRef, name: &str, ty: PyObjectRef) { + crate::module_ns_store(ns, name, ty); +} + +#[cfg(all( + feature = "cpyext", + not(feature = "sandbox"), + any(target_os = "macos", target_os = "linux") +))] +fn capsule_type() -> PyObjectRef { + crate::cpyext::capsule::capsule_type() +} + +#[cfg(not(all( + feature = "cpyext", + not(feature = "sandbox"), + any(target_os = "macos", target_os = "linux") +)))] +fn capsule_type() -> PyObjectRef { + static CAPSULE_TYPE: OnceLock = OnceLock::new(); + *CAPSULE_TYPE.get_or_init(|| crate::typedef::make_builtin_type("PyCapsule", |_| {}) as usize) + as PyObjectRef +} + +pub fn init(ns: PyObjectRef) { + let function_type = crate::typedef::gettypeobject(&crate::function::FUNCTION_TYPE); + store( + ns, + "AsyncGeneratorType", + crate::typedef::gettypeobject(&pyre_object::generator::ASYNC_GENERATOR_TYPE), + ); + store( + ns, + "BuiltinFunctionType", + crate::typedef::gettypeobject(&crate::function::BUILTIN_FUNCTION_TYPE), + ); + store( + ns, + "BuiltinMethodType", + crate::typedef::gettypeobject(&crate::function::BUILTIN_FUNCTION_TYPE), + ); + store(ns, "CapsuleType", capsule_type()); + store( + ns, + "CellType", + crate::typedef::gettypeobject(&pyre_object::nestedscope::CELL_TYPE), + ); + store( + ns, + "ClassMethodDescriptorType", + crate::typedef::gettypeobject(&crate::function::CLASSMETHOD_DESCRIPTOR_TYPE), + ); + store( + ns, + "CodeType", + crate::typedef::gettypeobject(&crate::pycode::CODE_TYPE), + ); + store( + ns, + "CoroutineType", + crate::typedef::gettypeobject(&pyre_object::generator::COROUTINE_TYPE), + ); + store( + ns, + "EllipsisType", + crate::typedef::gettypeobject(&pyre_object::ELLIPSIS_TYPE), + ); + store( + ns, + "FrameType", + crate::typedef::gettypeobject(&crate::pyframe::FRAME_TYPE), + ); + store(ns, "FunctionType", function_type); + store( + ns, + "GeneratorType", + crate::typedef::gettypeobject(&pyre_object::generator::GENERATOR_TYPE), + ); + store( + ns, + "GenericAlias", + crate::typedef::gettypeobject(&pyre_object::GENERIC_ALIAS_TYPE), + ); + store( + ns, + "GetSetDescriptorType", + crate::typedef::gettypeobject(&pyre_object::typedef::GETSET_DESCRIPTOR_TYPE), + ); + store(ns, "LambdaType", function_type); + store( + ns, + "MappingProxyType", + crate::typedef::gettypeobject(&pyre_object::MAPPING_PROXY_TYPE), + ); + store( + ns, + "MemberDescriptorType", + crate::typedef::gettypeobject(&pyre_object::typedef::MEMBER_TYPE), + ); + store( + ns, + "MethodDescriptorType", + crate::typedef::gettypeobject(&crate::function::METHOD_DESCRIPTOR_TYPE), + ); + store( + ns, + "MethodType", + crate::typedef::gettypeobject(&pyre_object::function::METHOD_TYPE), + ); + store( + ns, + "MethodWrapperType", + crate::typedef::gettypeobject(&crate::function::METHOD_WRAPPER_TYPE), + ); + store( + ns, + "ModuleType", + crate::typedef::gettypeobject(&pyre_object::MODULE_TYPE), + ); + store( + ns, + "NoneType", + crate::typedef::gettypeobject(&pyre_object::NONE_TYPE), + ); + store( + ns, + "NotImplementedType", + crate::typedef::gettypeobject(&pyre_object::NOTIMPLEMENTED_TYPE), + ); + store( + ns, + "SimpleNamespace", + crate::module::sys::vm::simple_namespace_type(), + ); + store( + ns, + "TracebackType", + crate::typedef::gettypeobject(&crate::pytraceback::PYTRACEBACK_TYPE), + ); + store( + ns, + "UnionType", + crate::typedef::gettypeobject(&pyre_object::UNION_TYPE), + ); + store( + ns, + "WrapperDescriptorType", + crate::typedef::gettypeobject(&crate::function::SLOT_WRAPPER_TYPE), + ); +} diff --git a/pyre/pyre-interpreter/src/module/mod.rs b/pyre/pyre-interpreter/src/module/mod.rs index 3e67f68c857..a0cbbe3d9ba 100644 --- a/pyre/pyre-interpreter/src/module/mod.rs +++ b/pyre/pyre-interpreter/src/module/mod.rs @@ -69,6 +69,8 @@ pub mod _posixsubprocess; #[allow(non_snake_case)] pub mod _pypy_generic_alias; #[allow(non_snake_case)] +pub mod _queue; +#[allow(non_snake_case)] pub mod _random; #[allow(non_snake_case)] #[cfg(not(feature = "sandbox"))] @@ -80,11 +82,15 @@ pub mod _ssl; #[allow(non_snake_case)] pub mod _stat; #[allow(non_snake_case)] +pub mod _statistics; +#[allow(non_snake_case)] pub mod _symtable; #[allow(non_snake_case)] pub mod _template; pub mod _tokenize; #[allow(non_snake_case)] +pub mod _types; +#[allow(non_snake_case)] pub mod _typing; pub mod _warnings; pub mod _weakref; diff --git a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs index 0b0e4deb2ef..f155230f03f 100644 --- a/pyre/pyre-interpreter/src/module/posix/interp_posix.rs +++ b/pyre/pyre-interpreter/src/module/posix/interp_posix.rs @@ -527,8 +527,10 @@ fn waitid_result_seq_type() -> PyObjectRef { } /// `posix.sched_param` structseq — the single field `app_posix.py:140-147` -/// declares. Its `__new__` takes the priority itself rather than a sequence, -/// which is what `_structseq.py:102-107` already gives every 1-field structseq. +/// declares. `_structseq.py:102-107` already wraps the scalar a 1-field +/// structseq is handed, so `__new__` only has to name the argument; +/// `__reduce__` has to be replaced outright, because the generic one hands +/// back `(tuple(self), self.__dict__)` and this `__new__` takes one argument. #[cfg(all( unix, any( @@ -541,10 +543,80 @@ fn waitid_result_seq_type() -> PyObjectRef { fn sched_param_seq_type() -> PyObjectRef { static T: std::sync::OnceLock = std::sync::OnceLock::new(); *T.get_or_init(|| { - crate::_structseq::make_struct_seq("posix.sched_param", &["sched_priority"]) as usize + let _roots = pyre_object::gc_roots::push_roots(); + let ty = crate::_structseq::make_struct_seq("posix.sched_param", &["sched_priority"]); + pyre_object::gc_roots::pin_root(ty); + let ty_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + + let new_descr = crate::typedef::make_new_descr_with_signature( + crate::_structseq::structseq_descr_new, + crate::gateway::Signature::new(vec!["cls", "sched_priority"], None, None, 0, 1), + ); + pyre_object::gc_roots::pin_root(new_descr); + let new_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + let reduce = crate::make_builtin_function_with_arity("__reduce__", sched_param_reduce, 1); + pyre_object::gc_roots::pin_root(reduce); + let reduce_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + + unsafe { + // A store can resize the namespace and collect, which moves the + // type and the function objects, so every one of them is read back + // out of its slot and `ns` is re-derived per store. + let ns = || { + pyre_object::w_type_get_dict_ptr(pyre_object::gc_roots::shadow_stack_get(ty_slot)) + as PyObjectRef + }; + pyre_object::w_dict_setitem_str_no_proxy( + ns(), + "__new__", + pyre_object::gc_roots::shadow_stack_get(new_slot), + ); + pyre_object::w_dict_setitem_str_no_proxy( + ns(), + "__reduce__", + pyre_object::gc_roots::shadow_stack_get(reduce_slot), + ); + crate::baseobjspace::mutated(pyre_object::gc_roots::shadow_stack_get(ty_slot), None); + pyre_object::gc_roots::shadow_stack_get(ty_slot) as usize + } }) as PyObjectRef } +/// `os_sched_param_reduce` — `(type(self), (self[0],))`, the one shape this +/// type's own `__new__` can be called back with. +#[cfg(all( + unix, + any( + target_os = "android", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd" + ) +))] +fn sched_param_reduce(args: &[PyObjectRef]) -> Result { + let Some(&inst) = args.first().filter(|inst| !inst.is_null()) else { + return Err(crate::PyError::type_error( + "sched_param.__reduce__ missing self", + )); + }; + let cls = unsafe { (*inst).w_class }; + let priority = + unsafe { pyre_object::w_tuple_getitem(inst, 0) }.unwrap_or_else(pyre_object::w_none); + // Both tuple allocations can collect, so the class and the element are + // published first and each one is read back out of its slot at the point + // it is stored. + let _roots = pyre_object::gc_roots::push_roots(); + let base = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(cls); + pyre_object::gc_roots::pin_root(priority); + let inner = pyre_object::w_tuple_new(vec![pyre_object::gc_roots::shadow_stack_get(base + 1)]); + pyre_object::gc_roots::pin_root(inner); + Ok(pyre_object::w_tuple_new(vec![ + pyre_object::gc_roots::shadow_stack_get(base), + pyre_object::gc_roots::shadow_stack_get(base + 2), + ])) +} + /// The `w_param` argument `sched_setparam` and `sched_setscheduler` share. /// `interp_posix.py:3086-3092` refuses anything that is not a `sched_param`, /// reads field 0 through the sequence protocol, and refuses a priority the C @@ -1546,6 +1618,14 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ("SCHED_BATCH", libc::SCHED_BATCH as i64), #[cfg(any(target_os = "linux", target_os = "android"))] ("SCHED_IDLE", libc::SCHED_IDLE as i64), + // `` names these two; `SCHED_RESET_ON_FORK` is a + // flag OR-ed into a policy rather than a policy of its own. + #[cfg(any(target_os = "linux", target_os = "android"))] + ("SCHED_NORMAL", libc::SCHED_NORMAL as i64), + #[cfg(any(target_os = "linux", target_os = "android"))] + ("SCHED_DEADLINE", libc::SCHED_DEADLINE as i64), + #[cfg(any(target_os = "linux", target_os = "android"))] + ("SCHED_RESET_ON_FORK", libc::SCHED_RESET_ON_FORK as i64), ] { crate::module_ns_store(ns, name, pyre_object::w_int_new(val)); } @@ -1751,24 +1831,85 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // `moduledef.py:152-157` registers each only where the host has it. #[cfg(all(unix, not(feature = "sandbox")))] { - // A device number is a `dev_t`, which is wider than a C int where the - // pair is more than two bytes and signed where it is not — so the - // argument is narrowed to that type rather than to `c_int`, and a value - // that does not fit says so instead of wrapping. + fn device_u64_w(value: PyObjectRef) -> Result { + let value = if unsafe { + pyre_object::is_bool(value) + || pyre_object::is_int(value) + || pyre_object::is_long(value) + } { + value + } else { + crate::baseobjspace::space_index(value)? + }; + crate::baseobjspace::uint_w(value) + } + fn device_value_w(value: PyObjectRef) -> Result { + let indexed = crate::baseobjspace::space_index(value)?; + // Reading the sentinel must not be fallible: a device number above + // `i64::MAX` has no machine-word form, so propagating `int_w`'s + // overflow here would refuse a value `uint_w` below accepts. + #[cfg(all(target_os = "linux", not(target_env = "musl")))] + if matches!(crate::baseobjspace::int_w(indexed), Ok(-1)) { + return Ok(-1i64 as libc::dev_t); + } + let value = crate::baseobjspace::uint_w(indexed)?; + // `dev_t` is signed on some targets and unsigned on others, so the + // ceiling comes from the type rather than from its signedness. + let max = u64::try_from(libc::dev_t::MAX).unwrap_or(u64::MAX); + if value > max { + return Err(crate::PyError::overflow_error( + "Python int too large to convert to C dev_t", + )); + } + Ok(value as libc::dev_t) + } fn device_w(args: &[PyObjectRef]) -> Result { let Some(&value) = args.first() else { return Err(crate::PyError::type_error("device is required")); }; - libc::dev_t::try_from(crate::baseobjspace::int_w(value)?).map_err(|_| { - crate::PyError::overflow_error("Python int too large to convert to C dev_t") - }) + device_value_w(value) + } + fn major_minor_result(value: i64) -> PyObjectRef { + #[cfg(all(target_os = "linux", not(target_env = "musl")))] + if value == -1 || value == libc::c_uint::MAX as i64 { + return pyre_object::w_int_new(-1); + } + pyre_object::w_int_new(value) + } + fn major_minor_arg(value: PyObjectRef) -> Result { + // Where `NODEV` is spelled -1 that one value passes through, rather + // than being rejected as out of range for an unsigned field. + #[cfg(all(target_os = "linux", not(target_env = "musl")))] + let value = { + let indexed = crate::baseobjspace::space_index(value)?; + if crate::baseobjspace::int_w(indexed)? == -1 { + return Ok(-1i64 as libc::dev_t); + } + crate::baseobjspace::uint_w(indexed)? + }; + #[cfg(not(all(target_os = "linux", not(target_env = "musl"))))] + let value = device_u64_w(value).map_err(|err| { + if err.kind == crate::PyErrorKind::OverflowError { + crate::PyError::overflow_error( + "Python int too large to convert to C unsigned int", + ) + } else { + err + } + })?; + if value > libc::c_uint::MAX as u64 { + return Err(crate::PyError::overflow_error( + "Python int too large to convert to C unsigned int", + )); + } + Ok(value as libc::dev_t) } crate::module_ns_store( ns, "major", crate::make_builtin_function_with_arity( "major", - |args| Ok(pyre_object::w_int_new(libc::major(device_w(args)?) as i64)), + |args| Ok(major_minor_result(libc::major(device_w(args)?) as i64)), 1, ), ); @@ -1777,7 +1918,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "minor", crate::make_builtin_function_with_arity( "minor", - |args| Ok(pyre_object::w_int_new(libc::minor(device_w(args)?) as i64)), + |args| Ok(major_minor_result(libc::minor(device_w(args)?) as i64)), 1, ), ); @@ -1791,8 +1932,8 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { [major, minor, ..] => (*major, *minor), _ => return Err(crate::PyError::type_error("makedev takes 2 arguments")), }; - let major = crate::baseobjspace::c_int_w(major)?; - let minor = crate::baseobjspace::c_int_w(minor)?; + let major = major_minor_arg(major)?; + let minor = major_minor_arg(minor)?; Ok(pyre_object::w_int_new( libc::makedev(major as _, minor as _) as i64, )) @@ -2204,6 +2345,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { /// where an exact-type test would reject it and a raw payload read would /// interpret the instance's first word as the descriptor. fn unwrap_fd(value: PyObjectRef, allowed_types: &str) -> Result { + if unsafe { pyre_object::is_bool(value) } { + crate::warn::warn_category("bool is used as a file descriptor", "RuntimeWarning", 1)?; + } let result = crate::baseobjspace::c_int_w(value).map_err(|err| { if err.kind == crate::PyErrorKind::OverflowError { err @@ -5951,6 +6095,62 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ), ); + // `getgroups(2)` reports at most `NGROUPS_MAX` entries, so a process in + // more groups than that gets a truncated list. `` aliases the + // name to an unlimited variant under `_DARWIN_C_SOURCE`, which is the + // one the reference implementation is compiled against; the `libc` + // binding names the capped symbol, so the alias is declared here. + #[cfg(target_vendor = "apple")] + unsafe extern "C" { + #[link_name = "getgroups$DARWIN_EXTSN"] + fn getgroups_unlimited( + gidsetsize: libc::c_int, + grouplist: *mut libc::gid_t, + ) -> libc::c_int; + } + + /// The group list, sized by the count the kernel reports first. + #[cfg(target_vendor = "apple")] + fn host_getgroups() -> std::io::Result> { + let count = unsafe { getgroups_unlimited(0, std::ptr::null_mut()) }; + if count < 0 { + return Err(std::io::Error::last_os_error()); + } + let mut groups = Vec::::with_capacity(count as usize); + let filled = unsafe { getgroups_unlimited(count, groups.as_mut_ptr()) }; + if filled < 0 { + return Err(std::io::Error::last_os_error()); + } + // A `gidsetsize` of 0 asks for the count instead of the list, so + // a process that was in no groups at the first call and is in + // some by the second gets back a count with nothing written. + let filled = (filled as usize).min(groups.capacity()); + unsafe { groups.set_len(filled) }; + Ok(groups) + } + + /// Elsewhere one symbol answers the question and `host_env` names it. + #[cfg(not(target_vendor = "apple"))] + fn host_getgroups() -> std::io::Result> { + host_posix::getgroups() + } + + /// Replace the supplementary group list. The `host_env` binding for + /// this call is gated off on the apple targets, which do have it. + #[cfg(target_vendor = "apple")] + fn host_setgroups(groups: &[libc::gid_t]) -> std::io::Result<()> { + let ret = unsafe { libc::setgroups(groups.len() as _, groups.as_ptr()) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + } + + #[cfg(not(target_vendor = "apple"))] + fn host_setgroups(groups: &[libc::gid_t]) -> std::io::Result<()> { + host_posix::setgroups_raw(groups) + } + // os.getgroups() -> list[int] crate::module_ns_store( ns, @@ -5958,7 +6158,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { crate::make_builtin_function_with_arity( "getgroups", |_| { - let gs = host_posix::getgroups().map_err(|e| io_err(e, ""))?; + let gs = host_getgroups().map_err(|e| io_err(e, ""))?; let items: Vec<_> = gs .into_iter() .map(|g| pyre_object::w_int_new(g as i64)) @@ -5969,6 +6169,31 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ), ); + // os.setgroups(list) -> None + crate::module_ns_store( + ns, + "setgroups", + crate::make_builtin_function_with_arity( + "setgroups", + |args| { + let Some(&w_list) = args.first() else { + return Err(crate::PyError::type_error("setgroups() requires 1 argument")); + }; + // interp_posix.py:1053-1064 — the list is unpacked as any + // iterable and each element read with `c_uid_t_w`, which is + // what lets -1 name `(gid_t)-1` instead of being refused. + let items = crate::builtins::collect_iterable(w_list)?; + let mut groups: Vec = Vec::with_capacity(items.len()); + for w_gid in items { + groups.push(crate::baseobjspace::c_uid_t_w(w_gid)?); + } + host_setgroups(&groups).map_err(|e| io_err(e, ""))?; + Ok(pyre_object::w_none()) + }, + 1, + ), + ); + // os.sched_get_priority_max(policy) -> int crate::module_ns_store( ns, @@ -7822,23 +8047,18 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { Some(w) => crate::baseobjspace::is_true(w)?, None => true, }; - // `linkat` is reached only for what plain `link` cannot say: - // a name to resolve against a descriptor, or a source symlink - // to link rather than follow. - let ret = if src_dir_fd != libc::AT_FDCWD || dst_dir_fd != libc::AT_FDCWD || !follow - { - let flags = if follow { libc::AT_SYMLINK_FOLLOW } else { 0 }; - unsafe { - libc::linkat( - src_dir_fd, - c_src.as_ptr(), - dst_dir_fd, - c_dst.as_ptr(), - flags, - ) - } - } else { - unsafe { libc::link(c_src.as_ptr(), c_dst.as_ptr()) } + // Whether plain `link` follows a source symlink is left to the + // implementation and the hosts disagree, so both answers are + // spelled out through `linkat` rather than taken from it. + let flags = if follow { libc::AT_SYMLINK_FOLLOW } else { 0 }; + let ret = unsafe { + libc::linkat( + src_dir_fd, + c_src.as_ptr(), + dst_dir_fd, + c_dst.as_ptr(), + flags, + ) }; if ret < 0 { return Err(fs_err_with_filename2( @@ -8739,15 +8959,32 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { }), ); - // os.posix_spawn(path, argv, env, *, file_actions=None) -> pid - // os.posix_spawnp(file, argv, env, *, file_actions=None) -> pid - // Currently supports path/argv/env + the file_actions sequence - // ((POSIX_SPAWN_OPEN, fd, path, flags, mode) | (POSIX_SPAWN_CLOSE, - // fd) | (POSIX_SPAWN_DUP2, fd, newfd)). Other CPython kwargs - // (setpgroup, setsid, setsigmask, setsigdef, resetids, scheduler) - // are not yet plumbed. - #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))] + // os.posix_spawn(path, argv, env, *, file_actions=None, setpgroup=None, + // resetids=False, setsid=False, setsigmask=(), setsigdef=(), + // scheduler=None) -> pid + // os.posix_spawnp(file, argv, env, *, file_actions=None, ...) -> pid + #[cfg(all( + any(target_os = "linux", target_os = "freebsd", target_os = "macos"), + not(feature = "sandbox") + ))] { + struct SpawnScheduler { + policy: Option, + param: libc::sched_param, + } + + /// The `POSIX_SPAWN_SETSID` spawn attribute, or `None` where the + /// platform has no such flag — which is what makes `setsid=True` + /// report an unavailable argument rather than being ignored. + #[cfg(target_os = "linux")] + const POSIX_SPAWN_SETSID: Option = Some(libc::POSIX_SPAWN_SETSID); + /// `` defines the flag, but the `libc` binding for + /// this target does not export it. + #[cfg(target_vendor = "apple")] + const POSIX_SPAWN_SETSID: Option = Some(0x0400); + #[cfg(not(any(target_os = "linux", target_vendor = "apple")))] + const POSIX_SPAWN_SETSID: Option = None; + fn build_posix_spawn( args: &[pyre_object::PyObjectRef], spawnp: bool, @@ -8789,11 +9026,43 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { let c_path = std::ffi::CString::new(path.as_bytes.as_slice()).map_err(|_| { crate::PyError::value_error("posix_spawn: embedded null in path") })?; - let argv = collect_cstring_seq(positional[1], "posix_spawn", "argv")?; + let argv = collect_cstring_seq(positional[1], func, "argv")?; // posixmodule.c parses `env` through the same keys/values // snapshot used by execve, then filesystem-encodes paired // elements into `key=value`. let env = collect_spawn_env(positional[2])?; + let setpgroup = match crate::builtins::kwarg_get(kwargs, "setpgroup") { + Some(value) if !unsafe { pyre_object::is_none(value) } => { + let value = crate::baseobjspace::space_index(value)?; + let value = crate::baseobjspace::int_w(value)?; + Some(libc::pid_t::try_from(value).map_err(|_| { + crate::PyError::overflow_error( + "Python int too large to convert to C pid_t", + ) + })?) + } + _ => None, + }; + let resetids = crate::builtins::kwarg_get(kwargs, "resetids") + .map(crate::baseobjspace::is_true) + .transpose()? + .unwrap_or(false); + let setsid = crate::builtins::kwarg_get(kwargs, "setsid") + .map(crate::baseobjspace::is_true) + .transpose()? + .unwrap_or(false); + if setsid && POSIX_SPAWN_SETSID.is_none() { + return Err(argument_unavailable(func, "setsid")); + } + let setsigmask = match crate::builtins::kwarg_get(kwargs, "setsigmask") { + Some(value) => Some(sigset_arg(value)?), + None => None, + }; + let setsigdef = match crate::builtins::kwarg_get(kwargs, "setsigdef") { + Some(value) => Some(sigset_arg(value)?), + None => None, + }; + let scheduler = parse_spawn_scheduler(func, kwargs)?; let file_actions_obj = crate::builtins::kwarg_get(kwargs, "file_actions"); let actions: Vec = if let Some(fa) = file_actions_obj { @@ -8805,19 +9074,20 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } else { Vec::new() }; - let config = rustpython_host_env::posix::PosixSpawnConfig { + let config = LocalPosixSpawnConfig { path: c_path.as_c_str(), args: &argv, env: &env, file_actions: &actions, - setsigdef: None, - setpgroup: None, - resetids: false, - setsid: false, - setsigmask: None, + setsigdef: setsigdef.as_deref(), + setpgroup, + resetids, + setsid, + setsigmask: setsigmask.as_deref(), + scheduler: scheduler.as_ref(), spawnp, }; - let pid = host_posix::posix_spawn(config) + let pid = local_posix_spawn(config) .map_err(|e| io_err_with_filename(e, path.w_path()))?; Ok(pyre_object::w_int_new(pid as i64)) } @@ -8900,68 +9170,72 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ) -> Result, crate::PyError> { use rustpython_host_env::posix::PosixSpawnFileAction; - let len = if unsafe { pyre_object::is_list(obj) } { - unsafe { pyre_object::w_list_len(obj) } - } else if unsafe { pyre_object::is_tuple(obj) } { - unsafe { pyre_object::w_tuple_len(obj) } - } else { - return Err(crate::PyError::type_error( - "posix_spawn: file_actions must be a list or tuple", - )); - }; + let items = + crate::builtins::sequence_fast(obj, "file_actions must be a sequence or None")?; // Every field of a `file_actions` entry is an `int` argument of // `os.posix_spawn`, so it is converted rather than read as a // payload: the caller controls the tuple's contents, and an // `int` subclass or a plain non-int would otherwise be // reinterpreted as a descriptor, flag set or mode. - let field = |entry: PyObjectRef, index: i64| -> Result { + // + // That conversion reaches `__index__`, and an OPEN path reaches + // `__fspath__`, so reading one field can collect and move the + // entries not yet read. The sequence is published once and each + // entry read back out of its slot at every use, the way + // `collect_cstring_seq` above does. + let field = |slot: usize, index: i64| -> Result { + let entry = pyre_object::gc_roots::shadow_stack_get(slot); let value = unsafe { pyre_object::w_tuple_getitem(entry, index) }.ok_or_else(|| { - crate::PyError::value_error("posix_spawn: file_actions entry too short") + crate::PyError::type_error( + "Each file_actions element must be a non-empty tuple", + ) })?; crate::baseobjspace::c_int_w(value) }; - let mut out = Vec::with_capacity(len); - for i in 0..len { - let entry = if unsafe { pyre_object::is_list(obj) } { - unsafe { pyre_object::w_list_getitem(obj, i as i64) } + let _seq_roots = pyre_object::gc_roots::push_roots(); + let items_base = pyre_object::gc_roots::pin_roots(&items); + let mut out = Vec::with_capacity(items.len()); + for offset in 0..items.len() { + let slot = items_base + offset; + let entry = pyre_object::gc_roots::shadow_stack_get(slot); + let tlen = if unsafe { pyre_object::is_tuple(entry) } { + unsafe { pyre_object::w_tuple_len(entry) } } else { - unsafe { pyre_object::w_tuple_getitem(obj, i as i64) } - } - .ok_or_else(|| { - crate::PyError::value_error("posix_spawn: file_actions entry missing") - })?; - if unsafe { !pyre_object::is_tuple(entry) } { return Err(crate::PyError::type_error( - "posix_spawn: each file_actions entry must be a tuple", + "Each file_actions element must be a non-empty tuple", )); - } - let tlen = unsafe { pyre_object::w_tuple_len(entry) }; - if tlen < 2 { - return Err(crate::PyError::value_error( - "posix_spawn: file_actions entry too short", + }; + if tlen == 0 { + return Err(crate::PyError::type_error( + "Each file_actions element must be a non-empty tuple", )); } - let op = field(entry, 0)?; + let op = field(slot, 0)?; match op { 0 => { // POSIX_SPAWN_OPEN: (op, fd, path, flags, mode) - if tlen < 5 { - return Err(crate::PyError::value_error( - "posix_spawn: OPEN action requires fd, path, flags, mode", + if tlen != 5 { + return Err(crate::PyError::type_error( + "A open file_action tuple must have 5 elements", )); } - let fd = field(entry, 1)?; - let path_obj = - unsafe { pyre_object::w_tuple_getitem(entry, 2).unwrap() }; + let fd = field(slot, 1)?; + let path_obj = unsafe { + pyre_object::w_tuple_getitem( + pyre_object::gc_roots::shadow_stack_get(slot), + 2, + ) + .unwrap() + }; let path = extract_path(path_obj)?; let cpath = std::ffi::CString::new(path).map_err(|_| { crate::PyError::value_error( "posix_spawn: embedded null in OPEN path", ) })?; - let oflag = field(entry, 3)?; - let mode = field(entry, 4)? as u32; + let oflag = field(slot, 3)?; + let mode = field(slot, 4)? as u32; out.push(PosixSpawnFileAction::Open { fd, path: cpath, @@ -8971,29 +9245,315 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } 1 => { // POSIX_SPAWN_CLOSE: (op, fd) - let fd = field(entry, 1)?; + if tlen != 2 { + return Err(crate::PyError::type_error( + "A close file_action tuple must have 2 elements", + )); + } + let fd = field(slot, 1)?; out.push(PosixSpawnFileAction::Close { fd }); } 2 => { // POSIX_SPAWN_DUP2: (op, fd, newfd) - if tlen < 3 { - return Err(crate::PyError::value_error( - "posix_spawn: DUP2 action requires fd, newfd", + if tlen != 3 { + return Err(crate::PyError::type_error( + "A dup2 file_action tuple must have 3 elements", )); } - let fd = field(entry, 1)?; - let newfd = field(entry, 2)?; + let fd = field(slot, 1)?; + let newfd = field(slot, 2)?; out.push(PosixSpawnFileAction::Dup2 { fd, newfd }); } _ => { - return Err(crate::PyError::value_error( - "posix_spawn: unknown file_actions opcode", + return Err(crate::PyError::type_error( + "Unknown file_actions identifier", )); } } } Ok(out) } + + fn sigset_arg(value: PyObjectRef) -> Result, crate::PyError> { + let items = crate::builtins::collect_iterable(value)?; + let mut sigs = Vec::with_capacity(items.len()); + for item in items { + let item = crate::baseobjspace::space_index(item)?; + let signum = crate::baseobjspace::int_w(item)?; + if !(1..crate::module::signal::signalstate::NSIG as i64).contains(&signum) { + return Err(crate::PyError::value_error(format!( + "signal number {signum} out of range [1; {}]", + crate::module::signal::signalstate::NSIG - 1 + ))); + } + sigs.push(signum as i32); + } + Ok(sigs) + } + + fn parse_spawn_scheduler( + func: &str, + kwargs: Option, + ) -> Result, crate::PyError> { + let Some(value) = crate::builtins::kwarg_get(kwargs, "scheduler") else { + return Ok(None); + }; + if unsafe { pyre_object::is_none(value) } { + return Ok(None); + } + if unsafe { !pyre_object::is_tuple(value) } { + return Err(crate::PyError::type_error(format!( + "{func}: scheduler must be a tuple or None" + ))); + } + if unsafe { pyre_object::w_tuple_len(value) } != 2 { + return Err(crate::PyError::type_error( + "A scheduler tuple must have two elements", + )); + } + + #[cfg(all(target_os = "linux", not(target_env = "musl")))] + { + let policy_obj = unsafe { pyre_object::w_tuple_getitem(value, 0).unwrap() }; + let param_obj = unsafe { pyre_object::w_tuple_getitem(value, 1).unwrap() }; + let priority = sched_priority_w(param_obj)?; + let mut param: libc::sched_param = + unsafe { core::mem::zeroed::() }; + param.sched_priority = priority; + let policy = if unsafe { pyre_object::is_none(policy_obj) } { + None + } else { + let policy = crate::baseobjspace::space_index(policy_obj)?; + let policy = crate::baseobjspace::int_w(policy)?; + Some(libc::c_int::try_from(policy).map_err(|_| { + crate::PyError::overflow_error( + "Python int too large to convert to C int", + ) + })?) + }; + Ok(Some(SpawnScheduler { policy, param })) + } + + #[cfg(any(not(target_os = "linux"), target_env = "musl"))] + { + Err(crate::PyError::not_implemented( + "The scheduler option is not supported in this system.", + )) + } + } + + struct LocalPosixSpawnConfig<'a> { + path: &'a std::ffi::CStr, + args: &'a [std::ffi::CString], + env: &'a [std::ffi::CString], + file_actions: &'a [rustpython_host_env::posix::PosixSpawnFileAction], + setsigdef: Option<&'a [i32]>, + setpgroup: Option, + resetids: bool, + setsid: bool, + setsigmask: Option<&'a [i32]>, + scheduler: Option<&'a SpawnScheduler>, + spawnp: bool, + } + + fn errno_result(ret: libc::c_int) -> std::io::Result<()> { + if ret == 0 { + Ok(()) + } else { + Err(std::io::Error::from_raw_os_error(ret)) + } + } + + unsafe fn fill_sigset(set: *mut libc::sigset_t, sigs: &[i32]) -> std::io::Result<()> { + if unsafe { libc::sigemptyset(set) } != 0 { + return Err(std::io::Error::last_os_error()); + } + for signum in sigs { + if unsafe { libc::sigaddset(set, *signum) } != 0 { + return Err(std::io::Error::last_os_error()); + } + } + Ok(()) + } + + fn build_spawn_file_actions( + actions: &[rustpython_host_env::posix::PosixSpawnFileAction], + ) -> std::io::Result> { + use rustpython_host_env::posix::PosixSpawnFileAction; + if actions.is_empty() { + return Ok(None); + } + let mut raw = unsafe { core::mem::zeroed::() }; + errno_result(unsafe { libc::posix_spawn_file_actions_init(&mut raw) })?; + for action in actions { + let result = match action { + PosixSpawnFileAction::Open { + fd, + path, + oflag, + mode, + } => unsafe { + libc::posix_spawn_file_actions_addopen( + &mut raw, + *fd, + path.as_ptr(), + *oflag, + *mode as libc::mode_t, + ) + }, + PosixSpawnFileAction::Close { fd } => unsafe { + libc::posix_spawn_file_actions_addclose(&mut raw, *fd) + }, + PosixSpawnFileAction::Dup2 { fd, newfd } => unsafe { + libc::posix_spawn_file_actions_adddup2(&mut raw, *fd, *newfd) + }, + }; + if let Err(error) = errno_result(result) { + unsafe { libc::posix_spawn_file_actions_destroy(&mut raw) }; + return Err(error); + } + } + Ok(Some(raw)) + } + + fn build_spawn_attrs( + config: &LocalPosixSpawnConfig<'_>, + ) -> std::io::Result { + let mut raw = unsafe { core::mem::zeroed::() }; + errno_result(unsafe { libc::posix_spawnattr_init(&mut raw) })?; + let mut flags = 0i32; + if let Some(pgid) = config.setpgroup { + if let Err(error) = + errno_result(unsafe { libc::posix_spawnattr_setpgroup(&mut raw, pgid) }) + { + unsafe { libc::posix_spawnattr_destroy(&mut raw) }; + return Err(error); + } + flags |= libc::POSIX_SPAWN_SETPGROUP as i32; + } + if config.resetids { + flags |= libc::POSIX_SPAWN_RESETIDS as i32; + } + if config.setsid { + let Some(setsid_flag) = POSIX_SPAWN_SETSID else { + unsafe { libc::posix_spawnattr_destroy(&mut raw) }; + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "posix_spawn: setsid unavailable on this platform", + )); + }; + flags |= setsid_flag as i32; + } + if let Some(sigs) = config.setsigmask { + let mut set = unsafe { core::mem::zeroed::() }; + if let Err(error) = unsafe { fill_sigset(&mut set, sigs) }.and_then(|_| { + errno_result(unsafe { libc::posix_spawnattr_setsigmask(&mut raw, &set) }) + }) { + unsafe { libc::posix_spawnattr_destroy(&mut raw) }; + return Err(error); + } + flags |= libc::POSIX_SPAWN_SETSIGMASK as i32; + } + if let Some(sigs) = config.setsigdef { + let mut set = unsafe { core::mem::zeroed::() }; + if let Err(error) = unsafe { fill_sigset(&mut set, sigs) }.and_then(|_| { + errno_result(unsafe { libc::posix_spawnattr_setsigdefault(&mut raw, &set) }) + }) { + unsafe { libc::posix_spawnattr_destroy(&mut raw) }; + return Err(error); + } + flags |= libc::POSIX_SPAWN_SETSIGDEF as i32; + } + if let Some(scheduler) = config.scheduler { + #[cfg(target_os = "linux")] + { + if let Some(policy) = scheduler.policy { + if let Err(error) = errno_result(unsafe { + libc::posix_spawnattr_setschedpolicy(&mut raw, policy) + }) { + unsafe { libc::posix_spawnattr_destroy(&mut raw) }; + return Err(error); + } + flags |= libc::POSIX_SPAWN_SETSCHEDULER as i32; + } + if let Err(error) = errno_result(unsafe { + libc::posix_spawnattr_setschedparam(&mut raw, &scheduler.param) + }) { + unsafe { libc::posix_spawnattr_destroy(&mut raw) }; + return Err(error); + } + flags |= libc::POSIX_SPAWN_SETSCHEDPARAM as i32; + } + } + if let Err(error) = + errno_result(unsafe { libc::posix_spawnattr_setflags(&mut raw, flags as _) }) + { + unsafe { libc::posix_spawnattr_destroy(&mut raw) }; + return Err(error); + } + Ok(raw) + } + + fn local_posix_spawn( + config: LocalPosixSpawnConfig<'_>, + ) -> std::io::Result { + let mut actions = build_spawn_file_actions(config.file_actions)?; + // `actions` is initialized C state, not a Rust value a drop + // reclaims, so a later failure has to destroy it explicitly. + let mut attrs = match build_spawn_attrs(&config) { + Ok(attrs) => attrs, + Err(error) => { + if let Some(actions) = actions.as_mut() { + unsafe { libc::posix_spawn_file_actions_destroy(actions) }; + } + return Err(error); + } + }; + let mut argv: Vec<*mut libc::c_char> = config + .args + .iter() + .map(|arg| arg.as_ptr() as *mut libc::c_char) + .collect(); + argv.push(std::ptr::null_mut()); + let mut env: Vec<*mut libc::c_char> = config + .env + .iter() + .map(|entry| entry.as_ptr() as *mut libc::c_char) + .collect(); + env.push(std::ptr::null_mut()); + let actionsp = actions + .as_mut() + .map_or(std::ptr::null(), |actions| actions as *mut _ as *const _); + let mut pid: libc::pid_t = 0; + let ret = crate::module::thread::call_external_function(|| unsafe { + if config.spawnp { + libc::posix_spawnp( + &mut pid, + config.path.as_ptr(), + actionsp, + &attrs, + argv.as_ptr(), + env.as_ptr(), + ) + } else { + libc::posix_spawn( + &mut pid, + config.path.as_ptr(), + actionsp, + &attrs, + argv.as_ptr(), + env.as_ptr(), + ) + } + }) + .0; + unsafe { libc::posix_spawnattr_destroy(&mut attrs) }; + if let Some(actions) = actions.as_mut() { + unsafe { libc::posix_spawn_file_actions_destroy(actions) }; + } + errno_result(ret)?; + Ok(pid) + } crate::module_ns_store( ns, "posix_spawn", @@ -9327,25 +9887,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { if args.is_empty() { return Err(crate::PyError::type_error("sysconf() requires name")); } - // interp_posix.py:2388-2397 confname_w: symbolic names - // resolve through the same dictionary exported as - // `sysconf_names`; every other value follows space.int_w. - let name = if unsafe { pyre_object::is_str(args[0]) } { - let key = crate::baseobjspace::text_w(args[0])?; - sysconf_names() - .iter() - .find_map(|(name, value)| (*name == key).then_some(*value)) - .ok_or_else(|| { - crate::PyError::value_error("unrecognized configuration name") - })? - } else { - // Narrowed, not truncated — see `confname_arg`. - i32::try_from(crate::baseobjspace::int_w(args[0])?).map_err(|_| { - crate::PyError::overflow_error( - "Python int too large to convert to C int", - ) - })? - }; + let name = confname_arg(args[0], sysconf_names())?; let v = host_posix::sysconf(name).map_err(|e| io_err(e, ""))?; Ok(pyre_object::w_int_new(v as i64)) }, diff --git a/pyre/pyre-interpreter/src/module/signal/interp_signal.rs b/pyre/pyre-interpreter/src/module/signal/interp_signal.rs index d2009b05e14..7a94270e9f4 100644 --- a/pyre/pyre-interpreter/src/module/signal/interp_signal.rs +++ b/pyre/pyre-interpreter/src/module/signal/interp_signal.rs @@ -118,6 +118,15 @@ pub fn set_handler(signum: i32, handler: PyObjectRef) { unsafe { pyre_object::w_dict_setitem(d, signum as i64, handler) }; } +/// Drop every registered handler, at the point in teardown where running +/// app-level code is no longer sound. A signal recorded after this reaches +/// `report_signal` with nothing to call, which is the case that returns +/// without running anything. +pub fn clear_handlers() { + let d = handlers_dict(); + unsafe { pyre_object::w_dict_clear(d) }; +} + /// GC root walker over the signal-handler table and its value slots. /// /// The HANDLERS dict pointer itself is visited as a root so the GC can diff --git a/pyre/pyre-interpreter/src/module/signal/signalstate.rs b/pyre/pyre-interpreter/src/module/signal/signalstate.rs index 20dd4fe09f9..be2dd506efd 100644 --- a/pyre/pyre-interpreter/src/module/signal/signalstate.rs +++ b/pyre/pyre-interpreter/src/module/signal/signalstate.rs @@ -358,13 +358,22 @@ fn async_signal_set() -> libc::sigset_t { } } +/// The signals the process already had blocked before the routing below +/// started, captured on the original thread. A mask inherited across `exec` +/// — what `posix_spawn`'s `setsigmask` installs — has to outlive the routing, +/// so those signals are never unblocked on the interpreter thread. +#[cfg(unix)] +static INHERITED_BLOCKED: std::sync::OnceLock = std::sync::OnceLock::new(); + /// Block the async signals on the calling thread — called on the process's /// original thread before the interpreter thread is spawned. #[cfg(unix)] pub fn block_async_signals_on_origin_thread() { unsafe { let set = async_signal_set(); - libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()); + let mut previous: libc::sigset_t = std::mem::zeroed(); + libc::pthread_sigmask(libc::SIG_BLOCK, &set, &mut previous); + let _ = INHERITED_BLOCKED.set(previous); } } @@ -374,7 +383,14 @@ pub fn block_async_signals_on_origin_thread() { #[cfg(unix)] pub fn unblock_async_signals_on_interp_thread() { unsafe { - let set = async_signal_set(); + let mut set = async_signal_set(); + if let Some(inherited) = INHERITED_BLOCKED.get() { + for signum in 1..NSIG { + if libc::sigismember(inherited, signum) == 1 { + libc::sigdelset(&mut set, signum); + } + } + } libc::pthread_sigmask(libc::SIG_UNBLOCK, &set, std::ptr::null_mut()); } } diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 38154a4ad77..a6901adb00d 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -376,7 +376,7 @@ fn simple_namespace_init(args: &[PyObjectRef]) -> crate::PyResult { /// full rich-comparison surface, pickle reducer and `__replace__`. Storage /// remains PyPy-shaped: the values live in the instance dict, not a side /// table or a second native mapping. -fn simple_namespace_type() -> PyObjectRef { +pub(crate) fn simple_namespace_type() -> PyObjectRef { static TYPE: OnceLock = OnceLock::new(); let raw = *TYPE.get_or_init(|| { let tp = crate::typedef::make_builtin_type("types.SimpleNamespace", |ns| { diff --git a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs index dacec04e004..4ffa5e93f3c 100644 --- a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs +++ b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs @@ -623,6 +623,17 @@ unsafe fn is_lsprof_mapdict_layout(obj: PyObjectRef) -> bool { } } +#[inline] +unsafe fn is_queue_mapdict_layout(obj: PyObjectRef) -> bool { + use pyre_object::lltype::PyreClassPyTypeOf; + unsafe { + pyre_object::py_type_check( + obj, + &*::PYTYPE, + ) + } +} + /// Whether `obj`'s physical allocation carries the slots supplied by /// `MapdictStorageMixin` (`mapdict.py:748-761, 905-910`). Ordinary instances /// and `_random.Random` keep the historical prefix. The generated tuple/int/str @@ -646,6 +657,7 @@ pub unsafe fn has_mapdict_layout(obj: PyObjectRef) -> bool { || unsafe { is_mmap_mapdict_layout(obj) } || unsafe { is_zlib_mapdict_layout(obj) } || unsafe { is_lsprof_mapdict_layout(obj) } + || unsafe { is_queue_mapdict_layout(obj) } { return true; } diff --git a/pyre/pyre-interpreter/src/pyopcode.rs b/pyre/pyre-interpreter/src/pyopcode.rs index f6fbff6c35c..63686a6e530 100644 --- a/pyre/pyre-interpreter/src/pyopcode.rs +++ b/pyre/pyre-interpreter/src/pyopcode.rs @@ -712,8 +712,10 @@ pub fn opcode_swap( pub fn opcode_get_iter(handler: &mut H) -> Result<(), PyError> { let iterable = handler.pop_value()?; + // A user-defined `__iter__` allocates, so the push goes through the anchor. + let anchor = handler.anchor(); let iterator = handler.iter_value(iterable)?; - handler.push_value(iterator) + H::push_anchored(&anchor, iterator) } pub fn opcode_for_iter( @@ -1151,10 +1153,13 @@ pub trait OpcodeStepExecutor: SharedOpcodeHandler { Self: SharedOpcodeHandler + NamespaceOpcodeHandler, { let obj = self.pop_value()?; + // The lookup runs descriptor code, so it can allocate and relocate a + // moving frame; both pushes go through the anchor. + let anchor = self.anchor(); let attr = SharedOpcodeHandler::load_special_attr(self, obj, name)?; - self.push_value(attr)?; + Self::push_anchored(&anchor, attr)?; let null = self.null_value()?; - self.push_value(null) + Self::push_anchored(&anchor, null) } fn store_attr(&mut self, name: &str) -> Result<(), PyError> diff --git a/pyre/pyre-interpreter/src/shared_opcode.rs b/pyre/pyre-interpreter/src/shared_opcode.rs index eb50f4c1141..86be932bbab 100644 --- a/pyre/pyre-interpreter/src/shared_opcode.rs +++ b/pyre/pyre-interpreter/src/shared_opcode.rs @@ -5,6 +5,24 @@ type OpcodeResult = Result; pub trait SharedOpcodeHandler { type Value: Copy; + /// Keeps the handler reachable across a call that may allocate. + /// + /// A handler stored in a moving heap block is relocated by a minor + /// collection, which leaves the `&mut Self` these helpers hold aimed at + /// the abandoned copy. Every opcode that allocates and then pushes takes + /// one of these first and pushes through it, so the result lands on the + /// relocated handler rather than on dead memory. + type Anchor; + + /// Take an anchor for the current handler. Called *before* the allocating + /// step, while `self` is still known good. + fn anchor(&mut self) -> Self::Anchor; + + /// Push onto the anchored handler. Deliberately takes no `self`: after the + /// allocating step the caller's `&mut Self` may name the abandoned copy, + /// so the anchor is the only sound way back to the live one. + fn push_anchored(anchor: &Self::Anchor, value: Self::Value) -> OpcodeResult<()>; + fn push_value(&mut self, value: Self::Value) -> OpcodeResult<()>; fn pop_value(&mut self) -> OpcodeResult; fn peek_at(&mut self, depth: usize) -> OpcodeResult; @@ -57,8 +75,9 @@ fn pop_n( pub fn opcode_make_function(handler: &mut H) -> OpcodeResult<()> { let code_obj = handler.pop_value()?; + let anchor = handler.anchor(); let func = handler.make_function(code_obj)?; - handler.push_value(func) + H::push_anchored(&anchor, func) } pub fn opcode_call( @@ -73,23 +92,26 @@ pub fn opcode_call( 0 => { let _null_or_self = handler.pop_value()?; let callable = handler.pop_value()?; + let anchor = handler.anchor(); let result = handler.call_callable(callable, &[])?; - handler.push_value(result) + H::push_anchored(&anchor, result) } 1 => { let a0 = handler.pop_value()?; let _null_or_self = handler.pop_value()?; let callable = handler.pop_value()?; + let anchor = handler.anchor(); let result = handler.call_callable(callable, &[a0])?; - handler.push_value(result) + H::push_anchored(&anchor, result) } 2 => { let a1 = handler.pop_value()?; let a0 = handler.pop_value()?; let _null_or_self = handler.pop_value()?; let callable = handler.pop_value()?; + let anchor = handler.anchor(); let result = handler.call_callable(callable, &[a0, a1])?; - handler.push_value(result) + H::push_anchored(&anchor, result) } 3 => { let a2 = handler.pop_value()?; @@ -97,15 +119,17 @@ pub fn opcode_call( let a0 = handler.pop_value()?; let _null_or_self = handler.pop_value()?; let callable = handler.pop_value()?; + let anchor = handler.anchor(); let result = handler.call_callable(callable, &[a0, a1, a2])?; - handler.push_value(result) + H::push_anchored(&anchor, result) } _ => { let args = pop_n(handler, nargs)?; let _null_or_self = handler.pop_value()?; let callable = handler.pop_value()?; + let anchor = handler.anchor(); let result = handler.call_callable(callable, &args)?; - handler.push_value(result) + H::push_anchored(&anchor, result) } } } @@ -115,8 +139,9 @@ pub fn opcode_build_list( size: usize, ) -> OpcodeResult<()> { let items = pop_n(handler, size)?; + let anchor = handler.anchor(); let list = handler.build_list(&items)?; - handler.push_value(list) + H::push_anchored(&anchor, list) } pub fn opcode_build_tuple( @@ -124,8 +149,9 @@ pub fn opcode_build_tuple( size: usize, ) -> OpcodeResult<()> { let items = pop_n(handler, size)?; + let anchor = handler.anchor(); let tuple = handler.build_tuple(&items)?; - handler.push_value(tuple) + H::push_anchored(&anchor, tuple) } pub fn opcode_build_map( @@ -133,8 +159,9 @@ pub fn opcode_build_map( size: usize, ) -> OpcodeResult<()> { let items = pop_n(handler, size * 2)?; + let anchor = handler.anchor(); let dict = handler.build_map(&items)?; - handler.push_value(dict) + H::push_anchored(&anchor, dict) } pub fn opcode_store_subscr(handler: &mut H) -> OpcodeResult<()> { @@ -161,9 +188,10 @@ pub fn opcode_unpack_sequence( count: usize, ) -> OpcodeResult<()> { let seq = handler.pop_value()?; + let anchor = handler.anchor(); let items = handler.unpack_sequence(seq, count)?; for item in items.into_iter().rev() { - handler.push_value(item)?; + H::push_anchored(&anchor, item)?; } Ok(()) } @@ -173,8 +201,9 @@ pub fn opcode_load_attr( name: &str, ) -> OpcodeResult<()> { let obj = handler.pop_value()?; + let anchor = handler.anchor(); let attr = handler.load_attr(obj, name)?; - handler.push_value(attr) + H::push_anchored(&anchor, attr) } pub fn opcode_store_attr( diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index 0391846894b..828ddb877a6 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -779,6 +779,14 @@ unsafe fn lsprof_profiler_destructor(obj_addr: usize) { }; } +unsafe fn queue_simplequeue_destructor(obj_addr: usize) { + unsafe { + pyre_interpreter::module::_queue::w_simplequeue_dealloc( + obj_addr as pyre_object::PyObjectRef, + ) + }; +} + #[cfg(all(windows, not(feature = "sandbox")))] unsafe fn overlapped_destructor(obj_addr: usize) { unsafe { @@ -895,6 +903,12 @@ unsafe fn lsprof_profiler_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut m unsafe { pyre_interpreter::module::_lsprof::w_profiler_custom_trace(obj_addr, f) }; } +/// `_queue.SimpleQueue` is subclassable and owns a FIFO of Python objects. +unsafe fn queue_simplequeue_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { + unsafe { object_object_custom_trace(obj_addr, f) }; + unsafe { pyre_interpreter::module::_queue::w_simplequeue_custom_trace(obj_addr, f) }; +} + /// `_ssl._SSLSocket` owns its context, transport endpoints, cached unbound /// socket methods, public owner, and hostname directly on the typed object. #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] @@ -3862,6 +3876,30 @@ fn build_gc() -> Box { as pyre_object::lltype::PyreClassPyTypeOf>::DESCRIPTOR, ); + // `_queue.SimpleQueue` is unconditional, so it registers ahead of the + // target-gated `posix` rclasses below and keeps one id on every target. + let simplequeue_descr = ::DESCRIPTOR; + let simplequeue_tid = gc.register_type( + TypeInfo::object_subclass_with_custom_trace( + simplequeue_descr.object_size, + object_tid, + queue_simplequeue_custom_trace, + ) + .with_destructor_fn(queue_simplequeue_destructor), + ); + simplequeue_descr.gc_type_id.set(simplequeue_tid); + majit_gc::GcAllocator::register_vtable_for_type( + &mut gc, + simplequeue_descr.pytype_ptr as usize, + simplequeue_tid, + ); + pytype_to_tid.insert(simplequeue_descr.pytype_ptr as usize, simplequeue_tid); + pyre_object::gc_hook::register_pyre_class_offsets( + simplequeue_descr.pytype_ptr as usize, + simplequeue_descr.ptr_offsets, + ); + // Register `posix.DirEntry`'s four inline GC edges and // `posix.ScandirIterator`'s entries-list edge. The entries in // `SUBCLASS_RANGE_HIERARCHY` and `all_subclass_range_aliases` are diff --git a/pyre/pyre-object/src/pyobject.rs b/pyre/pyre-object/src/pyobject.rs index 080773af713..63b9374da0d 100644 --- a/pyre/pyre-object/src/pyobject.rs +++ b/pyre/pyre-object/src/pyobject.rs @@ -677,19 +677,20 @@ pub const SUBCLASS_RANGE_HIERARCHY: &[(u32, Option)] = &[ (176, Some(0)), (177, Some(0)), (178, Some(0)), - // Native-only type IDs 179 and 180 represent `posix.DirEntry` and - // `posix.ScandirIterator`, matching `build_gc`'s registration order. - #[cfg(not(target_arch = "wasm32"))] + // `_queue.SimpleQueue` owns a native FIFO and is unconditional, so it + // closes the ungated block rather than joining the target-gated tail. (179, Some(0)), + // Native-only type IDs 180 and 181 represent `posix.DirEntry` and + // `posix.ScandirIterator`, matching `build_gc`'s registration order. #[cfg(not(target_arch = "wasm32"))] (180, Some(0)), + #[cfg(not(target_arch = "wasm32"))] + (181, Some(0)), // rustls `_ssl` context, MemoryBIO, and session native payloads. These // extend the append-only native rclass tail; wasm omits the host TLS // module and therefore the hierarchy entries as well. Sandbox filtering // belongs to pyre-interpreter, which owns that module configuration. #[cfg(not(target_arch = "wasm32"))] - (181, Some(0)), - #[cfg(not(target_arch = "wasm32"))] (182, Some(0)), #[cfg(not(target_arch = "wasm32"))] (183, Some(0)), @@ -697,6 +698,8 @@ pub const SUBCLASS_RANGE_HIERARCHY: &[(u32, Option)] = &[ (184, Some(0)), #[cfg(not(target_arch = "wasm32"))] (185, Some(0)), + #[cfg(not(target_arch = "wasm32"))] + (186, Some(0)), // `mmap.mmap` owns its native mapping payload — the duplicated fd on POSIX // and the file handle on Windows — and follows the optional SSL tail // wherever the module is compiled. The gate must match the alias gate in @@ -705,16 +708,16 @@ pub const SUBCLASS_RANGE_HIERARCHY: &[(u32, Option)] = &[ // A sandbox build has no `mmap` module either, so // `active_subclass_range_hierarchy` drops this entry along with SSL's. #[cfg(any(unix, windows))] - (186, Some(0)), + (187, Some(0)), // `_overlapped.Overlapped` owns the Windows OVERLAPPED record and its // retained Python buffers. pyre-interpreter supplies the vtable alias; // the object layer owns only the append-only hierarchy slot. #[cfg(windows)] - (187, Some(0)), + (188, Some(0)), // `_winapi.Overlapped` owns a second Windows OVERLAPPED record, the one // waited on through an event of its own rather than a completion port. #[cfg(windows)] - (188, Some(0)), + (189, Some(0)), ]; /// Compute subclass IDs from [`SUBCLASS_RANGE_HIERARCHY`] and write every diff --git a/pyre/pyrex/src/lib.rs b/pyre/pyrex/src/lib.rs index 45f0a5a0baf..832d56328eb 100644 --- a/pyre/pyrex/src/lib.rs +++ b/pyre/pyrex/src/lib.rs @@ -1464,6 +1464,10 @@ fn finalize_runtime(canonical: pyre_object::PyObjectRef, ec_ptr: *const PyExecut // may still start threads; reject new starts only when module/finalizer // teardown is actually about to begin. pyre_interpreter::module::thread::set_finalizing(); + // Past this point a handler would run against a half-torn-down module + // graph, so the teardown below reports signals instead of delivering + // them. atexit ran above and may legitimately have used signals. + pyre_interpreter::module::signal::interp_signal::clear_handlers(); // baseobjspace.py:498-501 `finish()` runs every started module's shutdown // hook; `_io`'s (moduledef.py:37-40) flushes the streams that are still // alive. The per-global teardown below reaches only the ones `__main__`