Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Lib/test/test_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import unittest
from test import support
from test.support import (
is_apple, is_apple_mobile, os_helper, threading_helper
force_not_colorized, is_apple, is_apple_mobile, os_helper, threading_helper
)
from test.support.script_helper import assert_python_ok, spawn_python
try:
Expand Down Expand Up @@ -355,6 +355,7 @@ def check_signum(signals):

@unittest.skipIf(_testcapi is None, 'need _testcapi')
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
@force_not_colorized

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revert the non-marker test edit

Adding @force_not_colorized and its import changes test execution metadata, but this repository explicitly limits test-file edits to adding or removing expectedFailure markers. Revert this synchronization and validate the runtime behavior without modifying the CPython test in this way.

AGENTS.md reference: AGENTS.md:L280-L282

Useful? React with 👍 / 👎.

def test_wakeup_write_error(self):
# Issue #16105: write() errors in the C signal handler should not
# pass silently.
Expand Down
35 changes: 30 additions & 5 deletions crates/host_env/src/crt_fd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,9 +437,30 @@ pub fn as_handle(fd: Borrowed<'_>) -> io::Result<BorrowedHandle<'_>> {

fn _write(fd: Raw, buf: &[u8]) -> io::Result<usize> {
let count = cmp::min(buf.len(), MAX_RW);
write_errno(fd, &buf[..count]).map_err(|errno| {
cfg_select! {
windows => crate::os::io_error_from_errno(errno),
_ => io::Error::from_raw_os_error(errno),
}
})
}

/// `_Py_write_noraise`: retry on EINTR and return the errno on failure
/// without allocating, so the OS signal handler can use it.
pub fn write_noraise(fd: Raw, buf: &[u8]) -> Result<usize, i32> {
let count = cmp::min(buf.len(), MAX_RW);
loop {
match write_errno(fd, &buf[..count]) {
Err(libc::EINTR) => continue,
result => return result,
}
}
}

fn write_errno(fd: Raw, buf: &[u8]) -> Result<usize, i32> {
#[cfg(windows)]
{
let mut count = count;
let mut count = buf.len();
loop {
unsafe { c::_set_doserrno(0) };
let n = unsafe { suppress_iph!(c::write(fd, buf.as_ptr() as _, count as _)) };
Expand All @@ -450,18 +471,22 @@ fn _write(fd: Raw, buf: &[u8]) -> io::Result<usize> {
// The CRT reports a successful zero-byte WriteFile as ENOSPC.
// A nonblocking pipe may still accept a smaller prefix.
if errno != libc::ENOSPC || dos_error() != 0 {
return Err(crate::os::io_error_from_errno(errno));
return Err(errno);
}
count /= 2;
if count == 0 {
return Err(crate::os::io_error_from_errno(libc::EAGAIN));
return Err(libc::EAGAIN);
}
}
}
#[cfg(not(windows))]
{
let n = cvt(unsafe { suppress_iph!(c::write(fd, buf.as_ptr() as _, count as _)) })?;
Ok(n as usize)
let n = unsafe { suppress_iph!(c::write(fd, buf.as_ptr() as _, buf.len() as _)) };
if n < 0 {
Err(crate::os::get_errno())
} else {
Ok(n as usize)
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions crates/host_env/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ macro_rules! suppress_iph {
#[cfg(all(windows, target_env = "msvc"))]
macro_rules! __suppress_iph_impl {
($e:expr) => {{
let old = $crate::__macro_private::_set_thread_local_invalid_parameter_handler(
let old = $crate::__macro_private::_set_thread_local_invalid_parameter_handler(Some(
$crate::__macro_private::silent_iph_handler,
);
));
let ret = $e;
$crate::__macro_private::_set_thread_local_invalid_parameter_handler(old);
ret
Expand Down Expand Up @@ -43,8 +43,8 @@ pub mod __macro_private {
#[cfg(target_env = "msvc")]
unsafe extern "C" {
pub fn _set_thread_local_invalid_parameter_handler(
pNew: InvalidParamHandler,
) -> InvalidParamHandler;
pNew: Option<InvalidParamHandler>,
) -> Option<InvalidParamHandler>;
}

#[cfg(target_env = "msvc")]
Expand Down
50 changes: 37 additions & 13 deletions crates/host_env/src/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,18 +395,33 @@ pub fn wakeup_fd_is_socket(fd: libc::SOCKET) -> io::Result<bool> {
Ok(false)
}

#[cfg(windows)]
pub struct WakeupSendError(pub i32);
#[cfg(any(unix, windows))]
pub enum WakeupError {
Write(i32),
#[cfg(windows)]
Send(i32),
}

#[cfg(windows)]
impl WakeupSendError {
#[cfg(any(unix, windows))]
impl WakeupError {
pub fn is_would_block(&self) -> bool {
self.0 == windows_sys::Win32::Networking::WinSock::WSAEWOULDBLOCK
match *self {
Self::Write(errno) => errno == libc::EAGAIN || errno == libc::EWOULDBLOCK,
#[cfg(windows)]
Self::Send(error) => error == windows_sys::Win32::Networking::WinSock::WSAEWOULDBLOCK,
}
}

// Only convert to an allocating error outside the OS signal handler.
pub fn into_io_error(self) -> io::Error {
io::Error::from_raw_os_error(self.0)
match self {
#[cfg(unix)]
Self::Write(errno) => io::Error::from_raw_os_error(errno),
#[cfg(windows)]
Self::Write(errno) => crate::os::io_error_from_errno(errno),
#[cfg(windows)]
Self::Send(error) => io::Error::from_raw_os_error(error),
}
}
}

Expand All @@ -416,7 +431,7 @@ pub fn notify_signal(
wakeup_fd: libc::SOCKET,
wakeup_is_socket: bool,
sigint_event: Option<isize>,
) -> Option<WakeupSendError> {
) -> Option<WakeupError> {
if signum == libc::SIGINT
&& let Some(handle) = sigint_event
{
Expand All @@ -440,21 +455,30 @@ pub fn notify_signal(
)
};
if result < 0 {
Some(WakeupSendError(unsafe {
Some(WakeupError::Send(unsafe {
windows_sys::Win32::Networking::WinSock::WSAGetLastError()
}))
} else {
None
}
} else {
unsafe {
let _ = libc::write(wakeup_fd as _, &sigbyte as *const u8 as *const _, 1);
}
None
crate::crt_fd::write_noraise(wakeup_fd as _, &[sigbyte])
.err()
.map(WakeupError::Write)
}
}

#[cfg(any(unix, target_os = "wasi"))]
#[cfg(unix)]
pub fn notify_signal(signum: i32, wakeup_fd: i32) -> Option<WakeupError> {
if wakeup_fd == -1 {
return None;
}
crate::crt_fd::write_noraise(wakeup_fd, &[signum as u8])
.err()
.map(WakeupError::Write)
}

#[cfg(target_os = "wasi")]
pub fn notify_signal(signum: i32, wakeup_fd: i32) {
if wakeup_fd == -1 {
return;
Expand Down
84 changes: 52 additions & 32 deletions crates/vm/src/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,48 +134,68 @@ fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> {
}
let _guard = SignalHandlerGuard;

#[cfg(all(feature = "host_env", windows))]
if vm.is_main_thread() && vm.state.is_main_interpreter() {
// Report before a Python signal handler can raise out of the current
// error-reporting scope.
crate::stdlib::_signal::_signal::report_wakeup_send_errors(vm);
} else if crate::stdlib::_signal::_signal::wakeup_send_errors_pending() {
// A different interpreter/thread must leave the report for the main one.
set_triggered();
}

let signal_handlers = vm
.signal_handlers
.get()
.expect("should never fail since we check above");
let can_handle_signals = vm.is_main_thread() && vm.state.is_main_interpreter();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject subinterpreter signal registrations

When _interpreters.exec() runs a subinterpreter on the process main OS thread, signal.signal() still passes its existing guard because that guard only checks vm.is_main_thread(), and it stores the callable in the subinterpreter's signal_handlers. This new predicate then refuses to drain that table; once the main interpreter resumes, it consumes the trigger against a different handler table, so the registered callable is never invoked and the process signal may be silently swallowed. Add the missing is_main_interpreter() rejection to signal() before restricting delivery this way.

Useful? React with 👍 / 👎.

if can_handle_signals {
let signal_handlers = vm
.signal_handlers
.get()
.expect("should never fail since we check above");

for (signum, trigger) in TRIGGERS.iter().enumerate().skip(1) {
let triggered = trigger.swap(false, Ordering::Relaxed);
if !triggered {
continue;
}

for (signum, trigger) in TRIGGERS.iter().enumerate().skip(1) {
let triggered = trigger.swap(false, Ordering::Relaxed);
if !triggered {
continue;
// SAFETY: TRIGGERS has the same length as the signal_handlers
let signum = unsafe { SignalNum::new_unchecked(signum as i32) };

// Read the handler out and drop the borrow before running it. A
// handler is free to call signal.signal(), which takes the same cell
// mutably, and a live read borrow turns that into a panic.
let handler = signal_handlers.borrow()[signum].clone();

if let Some(handler) = handler
&& let Some(callable) = handler.to_callable()
&& let Err(exc) = callable.invoke((signum.as_i32(), vm.ctx.none()), vm)
{
// On error, re-schedule a call to trigger_signals() for the
// remaining signals and the pending wakeup errors.
set_triggered();
return Err(exc);
}
}

// SAFETY: TRIGGERS has the same length as the signal_handlers
let signum = unsafe { SignalNum::new_unchecked(signum as i32) };

// Read the handler out and drop the borrow before running it. A
// handler is free to call signal.signal(), which takes the same cell
// mutably, and a live read borrow turns that into a panic.
let handler = signal_handlers.borrow()[signum].clone();

if let Some(handler) = handler
&& let Some(callable) = handler.to_callable()
{
callable.invoke((signum.as_i32(), vm.ctx.none()), vm)?;
} else {
let os_signals_pending = TRIGGERS
.iter()
.skip(1)
.any(|trigger| trigger.load(Ordering::Relaxed));
#[cfg(all(feature = "host_env", any(unix, windows)))]
let os_signals_pending =
os_signals_pending || crate::stdlib::_signal::_signal::wakeup_errors_pending();
if os_signals_pending {
// Leave OS signals for the main interpreter's main thread. Re-arm
// before running VM-local callbacks, which may raise.
set_triggered();
}
}

if let Some(signal_rx) = &vm.signal_rx {
for f in signal_rx.rx.try_iter() {
f(vm)?;
if let Err(exc) = f(vm) {
set_triggered();
return Err(exc);
}
}
}

// Wakeup fd errors are pending calls that only the main interpreter's
// main thread runs.
#[cfg(all(feature = "host_env", any(unix, windows)))]
if can_handle_signals {
crate::stdlib::_signal::_signal::report_wakeup_errors(vm);
}

Ok(())
}

Expand Down
Loading
Loading