From 91c0c6ddd872942c69c947007dc5f63e685d545d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 20 Aug 2026 13:09:53 +0900 Subject: [PATCH] _ctypes, signal, _multiprocessing: the #1357 review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _ctypes ------- `X_set` allocates the new BSTR first and frees what the slot held only immediately before the store, so a value it refuses leaves the previous string readable. Four stores released the slot before the conversion that can fail — `value_setter` and the struct-field, array-element and pointer-element stores in `metaclass.rs` — which frees the string and leaves the freed pointer in the slot. It reads back correctly until the allocator hands the block to someone else: b = BSTR("xy"); b.value = 1 # TypeError [BSTR("ab") for _ in range(64)] # same-size churn reclaims the block b.value # 'zz' here, 'xy' under CPython `SysAllocStringLen` answering null now raises `MemoryError` rather than storing a null pointer. `_build_callargs` set its `outmask`/`inoutmask` bits with an unbounded `1 << i`. The masks are the C `int` pair and `_build_result` reads back only the first 32 bits, so `param_bit` stops at the same width. The same loop held the values it had collected in a plain `Vec` across `out_parameter`, which instantiates the argtype and so runs Python; they are pinned as they are collected and read back out of their root slots. `PyErr_SetExcFromWindowsErrWithFilenameObjects` reads `GetLastError()` when the code it is handed is 0, so an explicit zero no longer names `ERROR_SUCCESS`: SetLastError(5); pythonapi.PyErr_SetFromWindowsErr(0) # .winerror was 0, is 5 signal ------ The Windows handler flagged the signal and returned, so `set_wakeup_fd` was a record and no byte was ever written. `signals.c:145-167` writes on both platforms — the `#ifdef _WIN32` only changes the type of `res` — and `PYPYSIG_USE_SEND` selects `send` for a descriptor that answered the `getsockopt` probe. That probe already ran in `set_wakeup_fd`; its verdict now reaches the handler, which writes the signal-number byte with `send` or the C runtime's `write`, keeps the EINTR retry and the warn_on_full_buffer drop, and restores the caller's errno. `rsocket_rffi` gained `WSAEWOULDBLOCK` for the drop test, and `builtins` a `set_crt_errno` beside `clear_crt_errno`. Both paths now answer as CPython does: signal.set_wakeup_fd(sock.fileno()); raise_signal(SIGINT) # b'\x02' signal.set_wakeup_fd(pipe_w); raise_signal(SIGINT) # b'\x02' `SIGBREAK` was spelled 21 twice in one file. _multiprocessing ---------------- `semlock_acquire` saturated a Windows timeout at `u32::MAX`; `interp_semaphore.py:272-273` refuses one at half of `INFINITE` with `OverflowError("timeout is too large")`, and rounds with `int(timeout + 0.5)` rather than upwards. A wait that returned `WAIT_OBJECT_0` then ran `checksignals_now()`, which can raise after the count has already been taken; `interp_semaphore.py:311-315` reports the acquisition before anything that can raise, and a signal pending at that moment is delivered at the next checkpoint. _thread ------- `interrupt_main` reported `PyLong_AsLong`'s message for an `i32::try_from`. It stages through the platform's C `long` first, so MSVC — where a `long` is 32 bits — keeps that one message for everything out of range, and an LP64 build gets the two `getargs.c` gives its `'i'` conversion. .gitignore ---------- `@test_*_tmp*` is unanchored: the working directory a stdlib test leaves its scratch files in is wherever the run started, not the repo root. Assisted-by: Claude --- .gitignore | 5 +- pyre/pyre-interpreter/src/builtins.rs | 15 +++ .../src/module/_ctypes/cdata.rs | 11 +- .../src/module/_ctypes/funcptr.rs | 30 ++++- .../src/module/_ctypes/metaclass.rs | 6 +- .../src/module/_multiprocessing/mod.rs | 14 +- .../src/module/_socket/rsocket_rffi.rs | 4 + .../src/module/signal/interp_signal.rs | 19 ++- .../src/module/signal/signalstate.rs | 120 ++++++++++++++++-- .../pyre-interpreter/src/module/thread/mod.rs | 13 +- 10 files changed, 212 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 9df1759211d..e2aa664adcd 100644 --- a/.gitignore +++ b/.gitignore @@ -114,5 +114,6 @@ CLAUDE.md # The scratch files `lib-python/3/test` writes into the working directory # (`test.support.TESTFN` is `@test__tmp`), left behind whenever a -# run dies before its `tearDown`. -/@test_*_tmp* +# run dies before its `tearDown`. Unanchored: the working directory is +# wherever the run was started from, not necessarily the repo root. +@test_*_tmp* diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index d6d9fa2b7f8..c3e8bdd0c53 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -6871,6 +6871,21 @@ pub(crate) fn clear_crt_errno() { } } +/// Put back an errno read earlier, so a call made in between leaves the cell +/// as the surrounding code left it. Windows-only for the same reason +/// [`clear_crt_errno`] is. +#[cfg(windows)] +pub(crate) fn set_crt_errno(value: i32) { + #[cfg(feature = "host_env")] + { + rustpython_host_env::os::set_errno(value); + } + #[cfg(not(feature = "host_env"))] + { + let _ = value; + } +} + /// The errno the last C runtime call reported. pub(crate) fn crt_errno() -> i32 { #[cfg(all(windows, feature = "host_env"))] diff --git a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs index 0aefbabe659..4afe4075092 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/cdata.rs @@ -505,11 +505,13 @@ fn value_setter(args: &[PyObjectRef]) -> Result { let _roots = pyre_object::gc_roots::push_roots(); let value_slot = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(value); - release_bstr_slot(&tc, cdata_addr(obj).unwrap_or(0)); let mut bytes = encode_value_into(&tc, value, obj, "0")?; if unsafe { crate::baseobjspace::lookup_in_type(cls, "_swappedbytes_") }.is_some() { bytes.reverse(); } + // `BSTR_set` frees what the slot held only once the new string exists, so + // a conversion that refuses its value leaves the previous one readable. + release_bstr_slot(&tc, cdata_addr(obj).unwrap_or(0)); cdata_write(obj, 0, &bytes); if matches!(tc.as_str(), "z" | "Z" | "O") { let d = crate::baseobjspace::getdict_native(obj); @@ -1299,6 +1301,10 @@ pub(super) fn decode_slot(tc: &str, bytes: &[u8]) -> PyObjectRef { /// every caller has — the object's buffer for a `value` store, that buffer /// plus the field offset for a struct or array slot, and the item address for /// a pointer store. +/// +/// Call it only once the replacement bytes exist: `X_set` allocates first and +/// frees the previous contents immediately before the store, so a value it +/// refuses leaves the slot as it was. #[cfg(windows)] pub(super) fn release_bstr_slot(tc: &str, addr: usize) { if tc != "X" || addr == 0 { @@ -1468,6 +1474,9 @@ pub(super) fn encode_value(tc: &str, obj: PyObjectRef) -> Result, crate: let bstr = unsafe { windows_sys::Win32::Foundation::SysAllocStringLen(units.as_ptr(), len) }; + if bstr.is_null() { + return Err(crate::PyError::memory_error("")); + } bstr as usize } else { return Err(crate::PyError::type_error(format!( diff --git a/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs b/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs index 7dcc87e713d..687056215a5 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/funcptr.rs @@ -1109,6 +1109,11 @@ fn build_callargs( return Ok(plain(passed.to_vec())); } let mut out = plain(Vec::with_capacity(argtypes.len())); + // `out_parameter` instantiates the argtype, which is arbitrary Python, so + // every value already collected lives in a root slot across it; the list + // is read back out of those slots once the loop is done. + let _roots = pyre_object::gc_roots::push_roots(); + let base = pyre_object::gc_roots::shadow_stack_len(); let mut index = 0; for (i, &at) in argtypes.iter().enumerate() { let malformed = @@ -1135,7 +1140,7 @@ fn build_callargs( // A locale id never comes from the call. PARAMFLAG_FIN_FLCID => defval.unwrap_or_else(|| pyre_object::w_int_new(0)), PARAMFLAG_FOUT => { - out.outmask |= 1 << i; + out.outmask |= param_bit(i); out.numretvals += 1; match defval { Some(defval) => defval, @@ -1144,17 +1149,28 @@ fn build_callargs( } direction => { if direction == PARAMFLAG_FIN_FOUT { - out.inoutmask |= 1 << i; + out.inoutmask |= param_bit(i); out.numretvals += 1; } get_arg(&mut index, name.as_deref(), defval, passed, kwargs)? } }; + pyre_object::gc_roots::pin_root(value); out.args.push(value); } + for (i, arg) in out.args.iter_mut().enumerate() { + *arg = pyre_object::gc_roots::shadow_stack_get(base + i); + } Ok(out) } +/// The `1 << i` bit `_build_callargs` sets in its two `int` masks. A +/// parameter past the width of that word has no bit of its own, which is the +/// range [`build_result`] reads back. +fn param_bit(i: usize) -> u32 { + 1u32.checked_shl(i as u32).unwrap_or(0) +} + /// `_get_arg` — the next positional argument, else the keyword of that name, /// else the declared default. fn get_arg( @@ -1457,9 +1473,17 @@ fn internal_pybytes_fromstringandsize(args: &[PyObjectRef]) -> Result`, which is what `test_windows_message` reads. #[cfg(windows)] fn internal_pyerr_setfromwindowserr(args: &[PyObjectRef]) -> Result { + // `PyErr_SetExcFromWindowsErrWithFilenameObjects` reads `GetLastError()` + // when the code it is handed is 0, so an explicit zero says the same + // thing as no argument at all rather than naming `ERROR_SUCCESS`. let code = match args.first() { Some(&arg) => crate::baseobjspace::int_w(arg)? as i32, - None => std::io::Error::last_os_error().raw_os_error().unwrap_or(0), + None => 0, + }; + let code = if code == 0 { + std::io::Error::last_os_error().raw_os_error().unwrap_or(0) + } else { + code }; Err(crate::PyError::os_error_win32_syscall2( code, diff --git a/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs b/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs index b0939ea473f..cf06210309b 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/metaclass.rs @@ -1316,11 +1316,11 @@ fn cfield_set(args: &[PyObjectRef]) -> PyResult { cdata::cdata_write(obj, offset, &bytes); return Ok(pyre_object::w_none()); } - cdata::release_bstr_slot(&tc, cdata::cdata_addr(obj).unwrap_or(0) + offset); let mut bytes = cdata::encode_instance_or_value(&tc, value, obj, &index.to_string())?; if field_needs_swap(obj, proto, size) { bytes.reverse(); } + cdata::release_bstr_slot(&tc, cdata::cdata_addr(obj).unwrap_or(0) + offset); cdata::cdata_write(obj, offset, &bytes); if cdata::is_cdata_instance(value) { cdata::keep_ref(obj, &index.to_string(), value); @@ -1953,8 +1953,8 @@ fn array_set_index(obj: PyObjectRef, meta: &ArrayMeta, idx: usize, value: PyObje "simple" => { let tc = cdata::type_code_of(meta.proto) .ok_or_else(|| crate::PyError::type_error("element has no '_type_'"))?; - cdata::release_bstr_slot(&tc, cdata::cdata_addr(obj).unwrap_or(0) + offset); let bytes = cdata::encode_instance_or_value(&tc, value, obj, &idx.to_string())?; + cdata::release_bstr_slot(&tc, cdata::cdata_addr(obj).unwrap_or(0) + offset); cdata::cdata_write(obj, offset, &bytes); if cdata::is_cdata_instance(value) { cdata::keep_ref(obj, &idx.to_string(), value); @@ -2442,8 +2442,8 @@ fn pointer_setitem(args: &[PyObjectRef]) -> PyResult { "simple" => { let tc = cdata::type_code_of(proto) .ok_or_else(|| crate::PyError::type_error("element has no '_type_'"))?; - cdata::release_bstr_slot(&tc, addr); let bytes = cdata::encode_instance_or_value(&tc, value, obj, &index.to_string())?; + cdata::release_bstr_slot(&tc, addr); unsafe { host_ctypes::copy_bytes_to_address(addr, &bytes, element_size) }; if cdata::is_cdata_instance(value) { cdata::keep_ref(obj, &index.to_string(), value); diff --git a/pyre/pyre-interpreter/src/module/_multiprocessing/mod.rs b/pyre/pyre-interpreter/src/module/_multiprocessing/mod.rs index 4a97f7f5ed6..9f43f0e438c 100644 --- a/pyre/pyre-interpreter/src/module/_multiprocessing/mod.rs +++ b/pyre/pyre-interpreter/src/module/_multiprocessing/mod.rs @@ -218,7 +218,14 @@ fn semlock_acquire( (false, _) => Some(0), (true, None) => None, (true, Some(seconds)) => { - Some((seconds * 1000.0).ceil().clamp(0.0, f64::from(u32::MAX)) as u32) + // `interp_semaphore.py:268-275` — a negative timeout is a poll, + // and one at half of `INFINITE` (about 25 days) is refused rather + // than saturated, so no wait silently becomes a different one. + let msecs = (seconds * 1000.0).max(0.0); + if msecs >= 0.5 * f64::from(u32::MAX) { + return Err(crate::PyError::overflow_error("timeout is too large")); + } + Some((msecs + 0.5) as u32) } }; loop { @@ -227,8 +234,11 @@ fn semlock_acquire( let _blocked = crate::module::thread::before_external_block(); host_mp::wait_for_single_object(handle, slice) }; + // `interp_semaphore.py:311-315` — the wait has taken the count, so it + // is reported before anything that can raise. A signal pending at + // this moment is delivered at the next checkpoint like any other; + // raising here would consume the semaphore without handing it over. if status == host_mp::wait_object_0() { - crate::module::signal::interp_signal::checksignals_now()?; return Ok(true); } if status != host_mp::wait_timeout() { diff --git a/pyre/pyre-interpreter/src/module/_socket/rsocket_rffi.rs b/pyre/pyre-interpreter/src/module/_socket/rsocket_rffi.rs index 42840652af2..424a84e8ea3 100644 --- a/pyre/pyre-interpreter/src/module/_socket/rsocket_rffi.rs +++ b/pyre/pyre-interpreter/src/module/_socket/rsocket_rffi.rs @@ -100,6 +100,10 @@ pub const SO_ERROR: libc::c_int = ws::SO_ERROR; /// The code a call about a descriptor that is not a socket comes back with. #[cfg(windows)] pub const WSAENOTSOCK: i32 = ws::WSAENOTSOCK; +/// The code a send that would have blocked comes back with — a buffer with no +/// room left, which is the one failure a caller may choose to drop. +#[cfg(windows)] +pub const WSAEWOULDBLOCK: i32 = ws::WSAEWOULDBLOCK; /// The code an expired wait reports, so a timeout this module times itself /// reads back the same as one the host produced. #[cfg(windows)] diff --git a/pyre/pyre-interpreter/src/module/signal/interp_signal.rs b/pyre/pyre-interpreter/src/module/signal/interp_signal.rs index 1d06163a35a..00360171853 100644 --- a/pyre/pyre-interpreter/src/module/signal/interp_signal.rs +++ b/pyre/pyre-interpreter/src/module/signal/interp_signal.rs @@ -217,11 +217,14 @@ fn check_signum_in_range(signum: i64) -> Result<(), crate::PyError> { } } +/// The runtime's own signal, absent from the libc crate. +#[cfg(windows)] +const SIGBREAK: i32 = 21; + /// Whether the runtime has a signal under this number at all. `SIGBREAK` is /// its own, so the set is spelled out rather than taken from `libc`. #[cfg(windows)] fn windows_handles_signal(signum: i32) -> bool { - const SIGBREAK: i32 = 21; matches!( signum, libc::SIGINT @@ -642,6 +645,13 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { "set_wakeup_fd() requires an argument", )); }; + // `PYPYSIG_USE_SEND` — set by the Windows probe below, which is + // the only place a descriptor is asked whether it is a socket. + #[cfg_attr( + not(all(windows, not(feature = "sandbox"))), + expect(unused_mut) + )] + let mut use_send = false; // interp_signal.py:343-360 — a real fd is validated with // `os.fstat` then `get_status_flags`: a bad fd is a ValueError // and the fd must already be in non-blocking mode. @@ -691,6 +701,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { &raw mut len, ) }; + use_send = queried == 0; if queried != 0 { let code = rffi::last_error_code(); // `WSAENOTSOCK` is the descriptor answering that it @@ -724,7 +735,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // interp_signal.py:376 — `pypysig_set_wakeup_fd`. The OS // handler writes the signal-number byte to this fd so a // select/poll loop blocked elsewhere wakes up. - let prev = signalstate::set_wakeup_fd(fd, warn_on_full_buffer); + let prev = signalstate::set_wakeup_fd(fd, warn_on_full_buffer, use_send); Ok(pyre_object::w_int_new(prev as i64)) }), ); @@ -1439,14 +1450,12 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { // `Signals(2)` answer from here. #[cfg(windows)] { - // `SIGBREAK` is the runtime's own, absent from the libc crate. - const SIGBREAK: i64 = 21; crate::module_ns_store(ns, "SIGINT", pyre_object::w_int_new(libc::SIGINT as i64)); crate::module_ns_store(ns, "SIGILL", pyre_object::w_int_new(libc::SIGILL as i64)); crate::module_ns_store(ns, "SIGFPE", pyre_object::w_int_new(libc::SIGFPE as i64)); crate::module_ns_store(ns, "SIGSEGV", pyre_object::w_int_new(libc::SIGSEGV as i64)); crate::module_ns_store(ns, "SIGTERM", pyre_object::w_int_new(libc::SIGTERM as i64)); - crate::module_ns_store(ns, "SIGBREAK", pyre_object::w_int_new(SIGBREAK)); + crate::module_ns_store(ns, "SIGBREAK", pyre_object::w_int_new(SIGBREAK.into())); crate::module_ns_store(ns, "SIGABRT", pyre_object::w_int_new(libc::SIGABRT as i64)); crate::module_ns_store(ns, "CTRL_C_EVENT", pyre_object::w_int_new(0)); crate::module_ns_store(ns, "CTRL_BREAK_EVENT", pyre_object::w_int_new(1)); diff --git a/pyre/pyre-interpreter/src/module/signal/signalstate.rs b/pyre/pyre-interpreter/src/module/signal/signalstate.rs index 662ba2462bd..3ab8d78ec45 100644 --- a/pyre/pyre-interpreter/src/module/signal/signalstate.rs +++ b/pyre/pyre-interpreter/src/module/signal/signalstate.rs @@ -40,6 +40,15 @@ static WAKEUP_FD: AtomicI32 = AtomicI32::new(-1); /// `signal.set_wakeup_fd(fd, warn_on_full_buffer=...)`; defaults to true. static WAKEUP_WARN_ON_FULL: AtomicBool = AtomicBool::new(true); +/// `signals.c:42 PYPYSIG_USE_SEND` — whether the wakeup descriptor is a +/// WinSock socket, which takes `send` rather than the runtime's `write` and +/// reports through `WSAGetLastError` rather than errno. Decided by the +/// `getsockopt` probe of `signals.c:250-267`, which lives at the +/// `set_wakeup_fd` call site because that is where WinSock has been started +/// up. +#[cfg(windows)] +static WAKEUP_USE_SEND: AtomicBool = AtomicBool::new(false); + /// `signals.c:132 pypysig_wakeup_fd_write_errno` — errno of a failed /// wakeup-fd write, stashed by the async handler (which cannot report it) /// and surfaced at the next interpreter checkpoint. 0 means none pending. @@ -121,9 +130,15 @@ pub(crate) fn has_pending_signals() -> bool { /// `PYPYSIG_WITH_NUL_BYTE` default only matters before any fd is set, /// during which `WAKEUP_FD` is -1 and nothing is written anyway). /// `warn_on_full` records whether a later full-pipe write should stash -/// its errno (`PYPYSIG_NO_WARN_FULL` cleared) or be dropped silently. -pub fn set_wakeup_fd(fd: i32, warn_on_full: bool) -> i32 { +/// its errno (`PYPYSIG_NO_WARN_FULL` cleared) or be dropped silently, and +/// `use_send` whether the descriptor answered the socket probe +/// (`PYPYSIG_USE_SEND`). +pub fn set_wakeup_fd(fd: i32, warn_on_full: bool, use_send: bool) -> i32 { WAKEUP_WARN_ON_FULL.store(warn_on_full, Ordering::SeqCst); + #[cfg(windows)] + WAKEUP_USE_SEND.store(use_send, Ordering::SeqCst); + #[cfg(not(windows))] + let _ = use_send; WAKEUP_FD.swap(fd, Ordering::SeqCst) } @@ -416,15 +431,104 @@ fn install_handler(signum: i32, handler: libc::sighandler_t) -> bool { previous != libc::SIG_ERR as libc::sighandler_t } -/// The OS signal handler. It flags the signal for the next checkpoint and -/// puts itself back: the runtime resets a handler to the default before -/// running it, so a second delivery would otherwise end the process. -/// -/// The wakeup descriptor the POSIX handler also writes to is a socket here, -/// which this context cannot write to, so `set_wakeup_fd` stays a record. +/// The wakeup byte on a descriptor that answered the socket probe +/// (`signals.c:156-157`). WinSock reports through `WSAGetLastError`, which +/// is the code returned alongside the result. +#[cfg(all(windows, not(feature = "sandbox")))] +fn wakeup_send(fd: i32, byte: &u8) -> (isize, i32) { + use crate::module::_socket::rsocket_rffi as rffi; + let res = unsafe { + rffi::send( + rffi::socket_from_i64(i64::from(fd)), + (byte as *const u8).cast(), + 1, + 0, + ) + }; + (res, rffi::last_error_code()) +} + +/// Unreachable under `sandbox`: the `getsockopt` probe that sets +/// `WAKEUP_USE_SEND` is compiled out there, so no descriptor is ever a socket. +#[cfg(all(windows, feature = "sandbox"))] +fn wakeup_send(_fd: i32, _byte: &u8) -> (isize, i32) { + (-1, 0) +} + +/// `signals.c:145-167` — the signal-number byte the handler puts on the +/// wakeup descriptor, so a `select`/`poll` loop blocked elsewhere wakes up. +/// A socket takes `send`; anything else is a descriptor the C runtime's +/// `write` and errno answer for. The caller's errno is restored either way, +/// so the interrupted code sees no change. +#[cfg(windows)] +fn write_wakeup_byte(signum: libc::c_int) { + let fd = WAKEUP_FD.load(Ordering::SeqCst); + if fd == -1 { + return; + } + let use_send = WAKEUP_USE_SEND.load(Ordering::SeqCst); + let byte = signum as u8; + let saved = crate::builtins::crt_errno(); + loop { + let (res, code) = if use_send { + wakeup_send(fd, &byte) + } else { + let res = crate::builtins::crt_call!(libc::write( + fd, + (&byte as *const u8).cast::(), + 1 + )); + (res as isize, crate::builtins::crt_errno()) + }; + if res < 0 { + if wakeup_error_is_interrupted(code, use_send) { + continue; + } + // `signals.c:160-166` — a full-buffer write is dropped silently + // when warn_on_full_buffer is false; any other error is always + // stashed for the next checkpoint to report. + let warn = WAKEUP_WARN_ON_FULL.load(Ordering::SeqCst); + if warn || !wakeup_error_is_full(code, use_send) { + WAKEUP_FD_WRITE_ERRNO.store(code, Ordering::SeqCst); + } + } + break; + } + crate::builtins::set_crt_errno(saved); +} + +/// `EINTR`, spelled `WSAEINTR` on the socket path. +#[cfg(windows)] +fn wakeup_error_is_interrupted(code: i32, use_send: bool) -> bool { + match use_send { + #[cfg(not(feature = "sandbox"))] + true => crate::module::_socket::rsocket_rffi::error_is_interrupted(code), + #[cfg(feature = "sandbox")] + true => false, + false => code == libc::EINTR, + } +} + +/// A buffer with no room left, spelled `WSAEWOULDBLOCK` on the socket path. +#[cfg(windows)] +fn wakeup_error_is_full(code: i32, use_send: bool) -> bool { + match use_send { + #[cfg(not(feature = "sandbox"))] + true => code == crate::module::_socket::rsocket_rffi::WSAEWOULDBLOCK, + #[cfg(feature = "sandbox")] + true => false, + false => code == libc::EAGAIN || code == libc::EWOULDBLOCK, + } +} + +/// The OS signal handler. It flags the signal for the next checkpoint, +/// writes the wakeup byte and puts itself back: the runtime resets a handler +/// to the default before running it, so a second delivery would otherwise end +/// the process. #[cfg(windows)] extern "C" fn signal_setflag_handler(signum: libc::c_int) { signal_pushback(signum); + write_wakeup_byte(signum); install_handler(signum, signal_setflag_handler as *const () as usize); } diff --git a/pyre/pyre-interpreter/src/module/thread/mod.rs b/pyre/pyre-interpreter/src/module/thread/mod.rs index 535b0742d38..540ddec35dc 100644 --- a/pyre/pyre-interpreter/src/module/thread/mod.rs +++ b/pyre/pyre-interpreter/src/module/thread/mod.rs @@ -1980,7 +1980,18 @@ fn interrupt_main(args: &[PyObjectRef]) -> Result { err } })?; - i32::try_from(value).map_err(|_| c_long_overflow())? + // `PyLong_AsLong` reports the width of the platform's C `long` + // first — 32 bits under MSVC, so everything out of range is that + // one message there — and `getargs.c`'s `'i'` then range-checks + // the result against a C `int` with two messages of its own. + let value = std::ffi::c_long::try_from(value).map_err(|_| c_long_overflow())?; + i32::try_from(value).map_err(|_| { + crate::PyError::overflow_error(if value > 0 { + "signed integer is greater than maximum" + } else { + "signed integer is less than minimum" + }) + })? } #[cfg(not(target_arch = "wasm32"))] None => libc::SIGINT,