From 2f34ee105c9f08bca1a3d19e58089346f298eecd Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 12 Dec 2025 00:02:34 +0900 Subject: [PATCH 1/4] faulthandler errno --- crates/stdlib/src/faulthandler.rs | 125 +++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 26 deletions(-) diff --git a/crates/stdlib/src/faulthandler.rs b/crates/stdlib/src/faulthandler.rs index e9006b982a3..54234e616aa 100644 --- a/crates/stdlib/src/faulthandler.rs +++ b/crates/stdlib/src/faulthandler.rs @@ -6,6 +6,8 @@ mod decl { PyObjectRef, PyResult, VirtualMachine, builtins::PyFloat, frame::Frame, function::OptionalArg, py_io::Write, }; + #[cfg(any(unix, windows))] + use rustpython_common::os::{get_errno, set_errno}; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread; @@ -78,6 +80,55 @@ mod decl { } } + /// Signal-safe integer to string conversion and write (no heap allocation) + #[cfg(any(unix, windows))] + fn write_int_noraise(fd: i32, mut n: i32) { + // Buffer for i32: max 11 chars (-2147483648) + null + let mut buf = [0u8; 12]; + let mut i = buf.len(); + let negative = n < 0; + + if n == 0 { + write_str_noraise(fd, "0"); + return; + } + + // Handle negative numbers + if negative { + n = -n; + } + + // Convert digits in reverse order + while n > 0 { + i -= 1; + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + } + + if negative { + i -= 1; + buf[i] = b'-'; + } + + // Write the number + #[cfg(not(windows))] + unsafe { + libc::write( + fd as libc::c_int, + buf[i..].as_ptr() as *const libc::c_void, + buf.len() - i, + ); + } + #[cfg(windows)] + unsafe { + libc::write( + fd as libc::c_int, + buf[i..].as_ptr() as *const libc::c_void, + (buf.len() - i) as u32, + ); + } + } + const MAX_FUNCTION_NAME_LEN: usize = 500; fn truncate_name(name: &str) -> String { @@ -178,6 +229,9 @@ mod decl { // faulthandler_fatal_error() in Modules/faulthandler.c #[cfg(unix)] extern "C" fn faulthandler_fatal_error(signum: libc::c_int) { + // Save errno (will be restored before returning) + let save_errno = get_errno(); + if !ENABLED.load(Ordering::Relaxed) { return; } @@ -185,21 +239,29 @@ mod decl { let fd = FATAL_ERROR_FD.load(Ordering::Relaxed); // Find handler and restore previous handler BEFORE printing - let signal_name = - if let Some(idx) = FATAL_SIGNALS.iter().position(|(sig, _)| *sig == signum) { - // Restore previous handler first - unsafe { - libc::sigaction(signum, &PREVIOUS_HANDLERS[idx], std::ptr::null_mut()); - } - FATAL_SIGNALS[idx].1 - } else { - "Unknown signal" - }; + let found = if let Some(idx) = FATAL_SIGNALS.iter().position(|(sig, _)| *sig == signum) { + // Restore previous handler first + unsafe { + libc::sigaction(signum, &PREVIOUS_HANDLERS[idx], std::ptr::null_mut()); + } + Some(FATAL_SIGNALS[idx].1) + } else { + None + }; // Print error message - write_str_noraise(fd, "Fatal Python error: "); - write_str_noraise(fd, signal_name); - write_str_noraise(fd, "\n\n"); + if let Some(name) = found { + write_str_noraise(fd, "Fatal Python error: "); + write_str_noraise(fd, name); + write_str_noraise(fd, "\n\n"); + } else { + write_str_noraise(fd, "Fatal Python error from unexpected signum: "); + write_int_noraise(fd, signum); + write_str_noraise(fd, "\n\n"); + } + + // Restore errno + set_errno(save_errno); // Re-raise signal to trigger default behavior (core dump, etc.) // Called immediately thanks to SA_NODEFER flag @@ -225,6 +287,9 @@ mod decl { // faulthandler_fatal_error() in Modules/faulthandler.c #[cfg(windows)] extern "C" fn faulthandler_fatal_error_windows(signum: libc::c_int) { + // Save errno at the start of signal handler + let save_errno = get_errno(); + if !ENABLED.load(Ordering::Relaxed) { return; } @@ -232,21 +297,29 @@ mod decl { let fd = FATAL_ERROR_FD.load(Ordering::Relaxed); // Find handler and restore previous handler BEFORE printing - let signal_name = - if let Some(idx) = FATAL_SIGNALS.iter().position(|(sig, _)| *sig == signum) { - // Restore previous handler first - unsafe { - libc::signal(signum, PREVIOUS_HANDLERS_WIN[idx]); - } - FATAL_SIGNALS[idx].1 - } else { - "Unknown signal" - }; + let found = if let Some(idx) = FATAL_SIGNALS.iter().position(|(sig, _)| *sig == signum) { + // Restore previous handler first + unsafe { + libc::signal(signum, PREVIOUS_HANDLERS_WIN[idx]); + } + Some(FATAL_SIGNALS[idx].1) + } else { + None + }; // Print error message - write_str_noraise(fd, "Fatal Python error: "); - write_str_noraise(fd, signal_name); - write_str_noraise(fd, "\n\n"); + if let Some(name) = found { + write_str_noraise(fd, "Fatal Python error: "); + write_str_noraise(fd, name); + write_str_noraise(fd, "\n\n"); + } else { + write_str_noraise(fd, "Fatal Python error from unexpected signum: "); + write_int_noraise(fd, signum); + write_str_noraise(fd, "\n\n"); + } + + // Restore errno + set_errno(save_errno); // On Windows, don't explicitly call the previous handler for SIGSEGV // The execution continues and the same instruction raises the same fault, From 56b7451e42feaa6468e14e417e9702bb773c6160 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 12 Dec 2025 00:57:29 +0900 Subject: [PATCH 2/4] faulthandler signals --- .cspell.dict/python-more.txt | 1 + Lib/test/test_cmd_line.py | 2 - Lib/test/test_faulthandler.py | 4 - crates/stdlib/src/faulthandler.rs | 626 +++++++++++++++++++++--------- crates/vm/src/vm/setting.rs | 5 +- src/lib.rs | 10 + src/settings.rs | 5 +- 7 files changed, 462 insertions(+), 191 deletions(-) diff --git a/.cspell.dict/python-more.txt b/.cspell.dict/python-more.txt index 8e1c0128383..620843cdd6a 100644 --- a/.cspell.dict/python-more.txt +++ b/.cspell.dict/python-more.txt @@ -170,6 +170,7 @@ pyexpat PYTHONBREAKPOINT PYTHONDEBUG PYTHONDONTWRITEBYTECODE +PYTHONFAULTHANDLER PYTHONHASHSEED PYTHONHOME PYTHONINSPECT diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index a7e4f6dd27f..eb455ebaed7 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -713,8 +713,6 @@ def run_xdev(self, *args, check_exitcode=True, xdev=True): self.assertEqual(proc.returncode, 0, proc) return proc.stdout.rstrip() - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_xdev(self): # sys.flags.dev_mode code = "import sys; print(sys.flags.dev_mode)" diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py index ffdc82bf053..5596e5b1669 100644 --- a/Lib/test/test_faulthandler.py +++ b/Lib/test/test_faulthandler.py @@ -442,8 +442,6 @@ def test_disabled_by_default(self): output = subprocess.check_output(args) self.assertEqual(output.rstrip(), b"False") - # TODO: RUSTPYTHON, subprocess.CalledProcessError: Command '' returned non-zero exit status 1. - @unittest.expectedFailure @support.requires_subprocess() def test_sys_xoptions(self): # Test python -X faulthandler @@ -457,8 +455,6 @@ def test_sys_xoptions(self): output = subprocess.check_output(args, env=env) self.assertEqual(output.rstrip(), b"True") - # TODO: RUSTPYTHON - @unittest.expectedFailure @support.requires_subprocess() def test_env_var(self): # empty env var diff --git a/crates/stdlib/src/faulthandler.rs b/crates/stdlib/src/faulthandler.rs index 54234e616aa..c2e100e5f88 100644 --- a/crates/stdlib/src/faulthandler.rs +++ b/crates/stdlib/src/faulthandler.rs @@ -1,5 +1,6 @@ pub(crate) use decl::make_module; +#[allow(static_mut_refs)] // TODO: group code only with static mut refs #[pymodule(name = "faulthandler")] mod decl { use crate::vm::{ @@ -8,13 +9,110 @@ mod decl { }; #[cfg(any(unix, windows))] use rustpython_common::os::{get_errno, set_errno}; - use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread; use std::time::Duration; - static ENABLED: AtomicBool = AtomicBool::new(false); - static FATAL_ERROR_FD: AtomicI32 = AtomicI32::new(2); // stderr by default + /// fault_handler_t + #[cfg(unix)] + struct FaultHandler { + signum: libc::c_int, + enabled: bool, + name: &'static str, + previous: libc::sigaction, + } + + #[cfg(windows)] + struct FaultHandler { + signum: libc::c_int, + enabled: bool, + name: &'static str, + previous: libc::sighandler_t, + } + + /// faulthandler_handlers[] + /// Number of fatal signals + #[cfg(unix)] + const FAULTHANDLER_NSIGNALS: usize = 5; + #[cfg(windows)] + const FAULTHANDLER_NSIGNALS: usize = 3; + + // CPython uses static arrays for signal handlers which requires mutable static access. + // This is safe because: + // 1. Signal handlers run in a single-threaded context (from the OS perspective) + // 2. FAULTHANDLER_HANDLERS is only modified during enable/disable operations + // 3. This matches CPython's faulthandler.c implementation + #[cfg(unix)] + static mut FAULTHANDLER_HANDLERS: [FaultHandler; FAULTHANDLER_NSIGNALS] = unsafe { + [ + FaultHandler { + signum: libc::SIGBUS, + enabled: false, + name: "Bus error", + previous: std::mem::zeroed(), + }, + FaultHandler { + signum: libc::SIGILL, + enabled: false, + name: "Illegal instruction", + previous: std::mem::zeroed(), + }, + FaultHandler { + signum: libc::SIGFPE, + enabled: false, + name: "Floating-point exception", + previous: std::mem::zeroed(), + }, + FaultHandler { + signum: libc::SIGABRT, + enabled: false, + name: "Aborted", + previous: std::mem::zeroed(), + }, + FaultHandler { + signum: libc::SIGSEGV, + enabled: false, + name: "Segmentation fault", + previous: std::mem::zeroed(), + }, + ] + }; + + #[cfg(windows)] + static mut FAULTHANDLER_HANDLERS: [FaultHandler; FAULTHANDLER_NSIGNALS] = [ + FaultHandler { + signum: libc::SIGFPE, + enabled: false, + name: "Floating-point exception", + previous: 0, + }, + FaultHandler { + signum: libc::SIGABRT, + enabled: false, + name: "Aborted", + previous: 0, + }, + FaultHandler { + signum: libc::SIGSEGV, + enabled: false, + name: "Segmentation fault", + previous: 0, + }, + ]; + + /// fatal_error state + struct FatalErrorState { + enabled: AtomicBool, + fd: AtomicI32, + all_threads: AtomicBool, + } + + static FATAL_ERROR: FatalErrorState = FatalErrorState { + enabled: AtomicBool::new(false), + fd: AtomicI32::new(2), // stderr by default + all_threads: AtomicBool::new(true), + }; // Watchdog thread state for dump_traceback_later struct WatchdogState { @@ -29,104 +127,206 @@ mod decl { type WatchdogHandle = Arc<(Mutex, Condvar)>; static WATCHDOG: Mutex> = Mutex::new(None); - // Number of fatal signals we handle - #[cfg(unix)] - const NUM_FATAL_SIGNALS: usize = 5; - #[cfg(windows)] - const NUM_FATAL_SIGNALS: usize = 3; + // Frame snapshot for signal-safe traceback (RustPython-specific) - // Fatal signals to handle (with names for error messages) - #[cfg(unix)] - const FATAL_SIGNALS: [(libc::c_int, &str); NUM_FATAL_SIGNALS] = [ - (libc::SIGBUS, "Bus error"), - (libc::SIGILL, "Illegal instruction"), - (libc::SIGFPE, "Floating-point exception"), - (libc::SIGABRT, "Aborted"), - (libc::SIGSEGV, "Segmentation fault"), - ]; + /// Frame information snapshot for signal-safe access + #[derive(Clone, Copy)] + struct FrameSnapshot { + filename: [u8; 256], + filename_len: usize, + lineno: u32, + funcname: [u8; 128], + funcname_len: usize, + } - #[cfg(windows)] - const FATAL_SIGNALS: [(libc::c_int, &str); NUM_FATAL_SIGNALS] = [ - (libc::SIGFPE, "Floating-point exception"), - (libc::SIGABRT, "Aborted"), - (libc::SIGSEGV, "Segmentation fault"), - ]; + impl FrameSnapshot { + const EMPTY: Self = Self { + filename: [0; 256], + filename_len: 0, + lineno: 0, + funcname: [0; 128], + funcname_len: 0, + }; + } - // Storage for previous signal handlers (Unix) - #[cfg(unix)] - static mut PREVIOUS_HANDLERS: [libc::sigaction; NUM_FATAL_SIGNALS] = - unsafe { std::mem::zeroed() }; + const MAX_SNAPSHOT_FRAMES: usize = 100; - /// Signal-safe write function - no memory allocation - #[cfg(all(not(target_arch = "wasm32"), not(windows)))] - fn write_str_noraise(fd: i32, s: &str) { - unsafe { - libc::write( - fd as libc::c_int, - s.as_ptr() as *const libc::c_void, - s.len(), - ); - } + /// Signal-safe global storage for frame snapshots + static mut FRAME_SNAPSHOTS: [FrameSnapshot; MAX_SNAPSHOT_FRAMES] = + [FrameSnapshot::EMPTY; MAX_SNAPSHOT_FRAMES]; + static SNAPSHOT_COUNT: AtomicUsize = AtomicUsize::new(0); + + // Signal-safe output functions + + // PUTS macro + #[cfg(any(unix, windows))] + fn puts(fd: i32, s: &str) { + let _ = unsafe { + #[cfg(windows)] + { + libc::write(fd, s.as_ptr() as *const libc::c_void, s.len() as u32) + } + #[cfg(not(windows))] + { + libc::write(fd, s.as_ptr() as *const libc::c_void, s.len()) + } + }; } - #[cfg(windows)] - fn write_str_noraise(fd: i32, s: &str) { - unsafe { - libc::write( - fd as libc::c_int, - s.as_ptr() as *const libc::c_void, - s.len() as u32, - ); + // _Py_DumpHexadecimal (traceback.c) + #[cfg(any(unix, windows))] + fn dump_hexadecimal(fd: i32, value: u64, width: usize) { + const HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; + let mut buf = [0u8; 18]; // "0x" + 16 hex digits + buf[0] = b'0'; + buf[1] = b'x'; + + for i in 0..width { + let digit = ((value >> (4 * (width - 1 - i))) & 0xf) as usize; + buf[2 + i] = HEX_CHARS[digit]; } + + let _ = unsafe { + #[cfg(windows)] + { + libc::write(fd, buf.as_ptr() as *const libc::c_void, (2 + width) as u32) + } + #[cfg(not(windows))] + { + libc::write(fd, buf.as_ptr() as *const libc::c_void, 2 + width) + } + }; } - /// Signal-safe integer to string conversion and write (no heap allocation) + // _Py_DumpDecimal (traceback.c) #[cfg(any(unix, windows))] - fn write_int_noraise(fd: i32, mut n: i32) { - // Buffer for i32: max 11 chars (-2147483648) + null - let mut buf = [0u8; 12]; + fn dump_decimal(fd: i32, value: usize) { + let mut buf = [0u8; 20]; + let mut v = value; let mut i = buf.len(); - let negative = n < 0; - if n == 0 { - write_str_noraise(fd, "0"); + if v == 0 { + puts(fd, "0"); return; } - // Handle negative numbers - if negative { - n = -n; - } - - // Convert digits in reverse order - while n > 0 { + while v > 0 { i -= 1; - buf[i] = b'0' + (n % 10) as u8; - n /= 10; + buf[i] = b'0' + (v % 10) as u8; + v /= 10; } - if negative { - i -= 1; - buf[i] = b'-'; + let len = buf.len() - i; + let _ = unsafe { + #[cfg(windows)] + { + libc::write(fd, buf[i..].as_ptr() as *const libc::c_void, len as u32) + } + #[cfg(not(windows))] + { + libc::write(fd, buf[i..].as_ptr() as *const libc::c_void, len) + } + }; + } + + /// Get current thread ID + #[cfg(unix)] + fn current_thread_id() -> u64 { + unsafe { libc::pthread_self() as u64 } + } + + #[cfg(windows)] + fn current_thread_id() -> u64 { + unsafe { windows_sys::Win32::System::Threading::GetCurrentThreadId() as u64 } + } + + // write_thread_id (traceback.c:1240-1256) + #[cfg(any(unix, windows))] + fn write_thread_id(fd: i32, is_current: bool) { + if is_current { + puts(fd, "Current thread 0x"); + } else { + puts(fd, "Thread 0x"); } + let thread_id = current_thread_id(); + // Use appropriate width based on platform pointer size + dump_hexadecimal(fd, thread_id, std::mem::size_of::() * 2); + puts(fd, " (most recent call first):\n"); + } - // Write the number - #[cfg(not(windows))] - unsafe { - libc::write( - fd as libc::c_int, - buf[i..].as_ptr() as *const libc::c_void, - buf.len() - i, - ); + // dump_frame (traceback.c:1037-1087) + #[cfg(any(unix, windows))] + fn dump_frame(fd: i32, filename: &[u8], lineno: u32, funcname: &[u8]) { + puts(fd, " File \""); + let _ = unsafe { + #[cfg(windows)] + { + libc::write( + fd, + filename.as_ptr() as *const libc::c_void, + filename.len() as u32, + ) + } + #[cfg(not(windows))] + { + libc::write(fd, filename.as_ptr() as *const libc::c_void, filename.len()) + } + }; + puts(fd, "\", line "); + dump_decimal(fd, lineno as usize); + puts(fd, " in "); + let _ = unsafe { + #[cfg(windows)] + { + libc::write( + fd, + funcname.as_ptr() as *const libc::c_void, + funcname.len() as u32, + ) + } + #[cfg(not(windows))] + { + libc::write(fd, funcname.as_ptr() as *const libc::c_void, funcname.len()) + } + }; + puts(fd, "\n"); + } + + // faulthandler_dump_traceback + #[cfg(any(unix, windows))] + fn faulthandler_dump_traceback(fd: i32, _all_threads: bool) { + static REENTRANT: AtomicBool = AtomicBool::new(false); + + if REENTRANT.swap(true, Ordering::SeqCst) { + return; } - #[cfg(windows)] - unsafe { - libc::write( - fd as libc::c_int, - buf[i..].as_ptr() as *const libc::c_void, - (buf.len() - i) as u32, - ); + + // Write thread header + write_thread_id(fd, true); + + // Try to dump traceback from snapshot + let count = SNAPSHOT_COUNT.load(Ordering::Acquire); + if count > 0 { + // Using index access instead of iterator because FRAME_SNAPSHOTS is static mut + #[allow(clippy::needless_range_loop)] + for i in 0..count { + unsafe { + let snap = &FRAME_SNAPSHOTS[i]; + if snap.filename_len > 0 { + dump_frame( + fd, + &snap.filename[..snap.filename_len], + snap.lineno, + &snap.funcname[..snap.funcname_len], + ); + } + } + } + } else { + puts(fd, " \n"); } + + REENTRANT.store(false, Ordering::SeqCst); } const MAX_FUNCTION_NAME_LEN: usize = 500; @@ -209,181 +409,240 @@ mod decl { all_threads: bool, } + // faulthandler_py_enable #[pyfunction] fn enable(args: EnableArgs, vm: &VirtualMachine) -> PyResult<()> { - // Check that file is valid (if provided) or sys.stderr is not None - let _file = get_file_for_output(args.file, vm)?; + // Get file descriptor + let fd = get_fd_from_file_opt(args.file, vm)?; - ENABLED.store(true, Ordering::Relaxed); + // Store fd and all_threads in global state + FATAL_ERROR.fd.store(fd, Ordering::Relaxed); + FATAL_ERROR + .all_threads + .store(args.all_threads, Ordering::Relaxed); - // Install signal handlers for fatal errors - #[cfg(any(unix, windows))] - { - install_fatal_handlers(vm); + // Install signal handlers + if !faulthandler_enable_internal() { + return Err(vm.new_runtime_error("Failed to enable faulthandler".to_owned())); } Ok(()) } - /// Unix signal handler for fatal errors - // faulthandler_fatal_error() in Modules/faulthandler.c + // Signal handlers + + /// faulthandler_disable_fatal_handler (faulthandler.c:310-321) + #[cfg(unix)] + unsafe fn faulthandler_disable_fatal_handler(handler: &mut FaultHandler) { + if !handler.enabled { + return; + } + handler.enabled = false; + unsafe { + libc::sigaction(handler.signum, &handler.previous, std::ptr::null_mut()); + } + } + + #[cfg(windows)] + unsafe fn faulthandler_disable_fatal_handler(handler: &mut FaultHandler) { + if !handler.enabled { + return; + } + handler.enabled = false; + unsafe { + libc::signal(handler.signum, handler.previous); + } + } + + // faulthandler_fatal_error #[cfg(unix)] extern "C" fn faulthandler_fatal_error(signum: libc::c_int) { - // Save errno (will be restored before returning) let save_errno = get_errno(); - if !ENABLED.load(Ordering::Relaxed) { + if !FATAL_ERROR.enabled.load(Ordering::Relaxed) { return; } - let fd = FATAL_ERROR_FD.load(Ordering::Relaxed); + let fd = FATAL_ERROR.fd.load(Ordering::Relaxed); + + let handler = unsafe { + FAULTHANDLER_HANDLERS + .iter_mut() + .find(|h| h.signum == signum) + }; - // Find handler and restore previous handler BEFORE printing - let found = if let Some(idx) = FATAL_SIGNALS.iter().position(|(sig, _)| *sig == signum) { - // Restore previous handler first + // faulthandler_fatal_error + if let Some(h) = handler { + // Disable handler first (restores previous) unsafe { - libc::sigaction(signum, &PREVIOUS_HANDLERS[idx], std::ptr::null_mut()); + faulthandler_disable_fatal_handler(h); } - Some(FATAL_SIGNALS[idx].1) - } else { - None - }; - // Print error message - if let Some(name) = found { - write_str_noraise(fd, "Fatal Python error: "); - write_str_noraise(fd, name); - write_str_noraise(fd, "\n\n"); + puts(fd, "Fatal Python error: "); + puts(fd, h.name); + puts(fd, "\n\n"); } else { - write_str_noraise(fd, "Fatal Python error from unexpected signum: "); - write_int_noraise(fd, signum); - write_str_noraise(fd, "\n\n"); + puts(fd, "Fatal Python error from unexpected signum: "); + dump_decimal(fd, signum as usize); + puts(fd, "\n\n"); } - // Restore errno + // faulthandler_dump_traceback + let all_threads = FATAL_ERROR.all_threads.load(Ordering::Relaxed); + faulthandler_dump_traceback(fd, all_threads); + + // restore errno set_errno(save_errno); - // Re-raise signal to trigger default behavior (core dump, etc.) + // raise // Called immediately thanks to SA_NODEFER flag unsafe { libc::raise(signum); } } - #[cfg(unix)] - fn install_fatal_handlers(_vm: &VirtualMachine) { - for (idx, (signum, _)) in FATAL_SIGNALS.iter().enumerate() { - let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; - action.sa_sigaction = faulthandler_fatal_error as libc::sighandler_t; - action.sa_flags = libc::SA_NODEFER; - - unsafe { - libc::sigaction(*signum, &action, &mut PREVIOUS_HANDLERS[idx]); - } - } - } - - /// Windows signal handler for fatal errors - // faulthandler_fatal_error() in Modules/faulthandler.c + // faulthandler_fatal_error for Windows #[cfg(windows)] - extern "C" fn faulthandler_fatal_error_windows(signum: libc::c_int) { - // Save errno at the start of signal handler + extern "C" fn faulthandler_fatal_error(signum: libc::c_int) { let save_errno = get_errno(); - if !ENABLED.load(Ordering::Relaxed) { + if !FATAL_ERROR.enabled.load(Ordering::Relaxed) { return; } - let fd = FATAL_ERROR_FD.load(Ordering::Relaxed); + let fd = FATAL_ERROR.fd.load(Ordering::Relaxed); - // Find handler and restore previous handler BEFORE printing - let found = if let Some(idx) = FATAL_SIGNALS.iter().position(|(sig, _)| *sig == signum) { - // Restore previous handler first - unsafe { - libc::signal(signum, PREVIOUS_HANDLERS_WIN[idx]); - } - Some(FATAL_SIGNALS[idx].1) - } else { - None + let handler = unsafe { + FAULTHANDLER_HANDLERS + .iter_mut() + .find(|h| h.signum == signum) }; - // Print error message - if let Some(name) = found { - write_str_noraise(fd, "Fatal Python error: "); - write_str_noraise(fd, name); - write_str_noraise(fd, "\n\n"); + if let Some(h) = handler { + unsafe { + faulthandler_disable_fatal_handler(h); + } + puts(fd, "Fatal Python error: "); + puts(fd, h.name); + puts(fd, "\n\n"); } else { - write_str_noraise(fd, "Fatal Python error from unexpected signum: "); - write_int_noraise(fd, signum); - write_str_noraise(fd, "\n\n"); + puts(fd, "Fatal Python error from unexpected signum: "); + dump_decimal(fd, signum as usize); + puts(fd, "\n\n"); } - // Restore errno + let all_threads = FATAL_ERROR.all_threads.load(Ordering::Relaxed); + faulthandler_dump_traceback(fd, all_threads); + set_errno(save_errno); // On Windows, don't explicitly call the previous handler for SIGSEGV - // The execution continues and the same instruction raises the same fault, - // calling the now-restored previous handler if signum == libc::SIGSEGV { return; } - // For other signals, re-raise to call the previous handler unsafe { libc::raise(signum); } } - #[cfg(windows)] - static mut PREVIOUS_HANDLERS_WIN: [libc::sighandler_t; NUM_FATAL_SIGNALS] = - [0; NUM_FATAL_SIGNALS]; + // faulthandler_enable + #[cfg(unix)] + fn faulthandler_enable_internal() -> bool { + if FATAL_ERROR.enabled.load(Ordering::Relaxed) { + return true; + } - #[cfg(windows)] - fn install_fatal_handlers(_vm: &VirtualMachine) { - for (idx, (signum, _)) in FATAL_SIGNALS.iter().enumerate() { - unsafe { - PREVIOUS_HANDLERS_WIN[idx] = libc::signal( - *signum, - faulthandler_fatal_error_windows as libc::sighandler_t, - ); + unsafe { + for handler in FAULTHANDLER_HANDLERS.iter_mut() { + if handler.enabled { + continue; + } + + let mut action: libc::sigaction = std::mem::zeroed(); + action.sa_sigaction = faulthandler_fatal_error as libc::sighandler_t; + // SA_NODEFER flag + action.sa_flags = libc::SA_NODEFER; + + if libc::sigaction(handler.signum, &action, &mut handler.previous) != 0 { + return false; + } + + handler.enabled = true; } } - } - #[pyfunction] - fn disable() -> bool { - let was_enabled = ENABLED.swap(false, Ordering::Relaxed); + FATAL_ERROR.enabled.store(true, Ordering::Relaxed); + true + } - // Restore default signal handlers - #[cfg(any(unix, windows))] - { - uninstall_fatal_handlers(); + #[cfg(windows)] + fn faulthandler_enable_internal() -> bool { + if FATAL_ERROR.enabled.load(Ordering::Relaxed) { + return true; } - was_enabled - } + unsafe { + for handler in FAULTHANDLER_HANDLERS.iter_mut() { + if handler.enabled { + continue; + } - #[cfg(unix)] - fn uninstall_fatal_handlers() { - for (idx, (signum, _)) in FATAL_SIGNALS.iter().enumerate() { - unsafe { - libc::sigaction(*signum, &PREVIOUS_HANDLERS[idx], std::ptr::null_mut()); + handler.previous = libc::signal( + handler.signum, + faulthandler_fatal_error as libc::sighandler_t, + ); + + // SIG_ERR is -1 as sighandler_t (which is usize on Windows) + if handler.previous == libc::SIG_ERR as libc::sighandler_t { + return false; + } + + handler.enabled = true; } } + + FATAL_ERROR.enabled.store(true, Ordering::Relaxed); + true } - #[cfg(windows)] - fn uninstall_fatal_handlers() { - for (idx, (signum, _)) in FATAL_SIGNALS.iter().enumerate() { - unsafe { - libc::signal(*signum, PREVIOUS_HANDLERS_WIN[idx]); + // faulthandler_disable + #[cfg(any(unix, windows))] + fn faulthandler_disable_internal() { + if !FATAL_ERROR.enabled.swap(false, Ordering::Relaxed) { + return; + } + + unsafe { + for handler in FAULTHANDLER_HANDLERS.iter_mut() { + faulthandler_disable_fatal_handler(handler); } } } + #[cfg(not(any(unix, windows)))] + fn faulthandler_enable_internal() -> bool { + FATAL_ERROR.enabled.store(true, Ordering::Relaxed); + true + } + + #[cfg(not(any(unix, windows)))] + fn faulthandler_disable_internal() { + FATAL_ERROR.enabled.store(false, Ordering::Relaxed); + } + + // faulthandler_disable_py + #[pyfunction] + fn disable() -> bool { + let was_enabled = FATAL_ERROR.enabled.load(Ordering::Relaxed); + faulthandler_disable_internal(); + was_enabled + } + + // faulthandler_is_enabled #[pyfunction] fn is_enabled() -> bool { - ENABLED.load(Ordering::Relaxed) + FATAL_ERROR.enabled.load(Ordering::Relaxed) } fn format_timeout(timeout_us: u64) -> String { @@ -701,8 +960,9 @@ mod decl { #[cfg(unix)] fn check_signum(signum: i32, vm: &VirtualMachine) -> PyResult<()> { - // Check if it's a fatal signal - if FATAL_SIGNALS.iter().any(|(sig, _)| *sig == signum) { + // Check if it's a fatal signal (faulthandler.c uses faulthandler_handlers array) + let is_fatal = unsafe { FAULTHANDLER_HANDLERS.iter().any(|h| h.signum == signum) }; + if is_fatal { return Err(vm.new_runtime_error(format!( "signal {} cannot be registered, use enable() instead", signum diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index caf8c9139d6..deaca705c47 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -19,7 +19,9 @@ pub struct Settings { /// None means use_hash_seed = 0 in CPython pub hash_seed: Option, - // int faulthandler; + /// -X faulthandler, PYTHONFAULTHANDLER + pub faulthandler: bool, + // int tracemalloc; // int perf_profiling; // int import_time; @@ -157,6 +159,7 @@ impl Default for Settings { path_list: vec![], argv: vec![], hash_seed: None, + faulthandler: false, buffered_stdio: true, check_hash_pycs_mode: CheckHashPycsMode::Default, allow_external_library: cfg!(feature = "importlib"), diff --git a/src/lib.rs b/src/lib.rs index 0737ebdddc4..40e50ab84c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -187,6 +187,16 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { ); } + // Enable faulthandler if -X faulthandler, PYTHONFAULTHANDLER or -X dev is set + // _PyFaulthandler_Init() + if vm.state.settings.faulthandler { + let _ = vm.run_code_string( + vm.new_scope_with_builtins(), + "import faulthandler; faulthandler.enable()", + "".to_owned(), + ); + } + let is_repl = matches!(run_mode, RunMode::Repl); if !vm.state.settings.quiet && (vm.state.settings.verbose > 0 || (is_repl && std::io::stdin().is_terminal())) diff --git a/src/settings.rs b/src/settings.rs index 23553aa5c06..f77db4d159d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -267,6 +267,7 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { }; match &*name { "dev" => settings.dev_mode = true, + "faulthandler" => settings.faulthandler = true, "warn_default_encoding" => settings.warn_default_encoding = true, "no_sig_int" => settings.install_signal_handlers = false, "int_max_str_digits" => { @@ -291,9 +292,11 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { settings.warn_default_encoding = settings.warn_default_encoding || env_bool("PYTHONWARNDEFAULTENCODING"); + settings.faulthandler = settings.faulthandler || env_bool("PYTHONFAULTHANDLER"); if settings.dev_mode { - settings.warnoptions.push("default".to_owned()) + settings.warnoptions.push("default".to_owned()); + settings.faulthandler = true; } if settings.bytes_warning > 0 { let warn = if settings.bytes_warning > 1 { From cf3ad0adafa07421d3909913eaca750a52cf8f69 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 12 Dec 2025 08:19:05 +0900 Subject: [PATCH 3/4] apply review --- crates/stdlib/src/faulthandler.rs | 88 +++++++++++-------------------- 1 file changed, 32 insertions(+), 56 deletions(-) diff --git a/crates/stdlib/src/faulthandler.rs b/crates/stdlib/src/faulthandler.rs index c2e100e5f88..dfb1809a1e5 100644 --- a/crates/stdlib/src/faulthandler.rs +++ b/crates/stdlib/src/faulthandler.rs @@ -31,74 +31,50 @@ mod decl { previous: libc::sighandler_t, } + #[cfg(unix)] + impl FaultHandler { + const fn new(signum: libc::c_int, name: &'static str) -> Self { + Self { + signum, + enabled: false, + name, + // SAFETY: sigaction is a C struct that can be zero-initialized + previous: unsafe { std::mem::zeroed() }, + } + } + } + + #[cfg(windows)] + impl FaultHandler { + const fn new(signum: libc::c_int, name: &'static str) -> Self { + Self { + signum, + enabled: false, + name, + previous: 0, + } + } + } + /// faulthandler_handlers[] /// Number of fatal signals #[cfg(unix)] const FAULTHANDLER_NSIGNALS: usize = 5; #[cfg(windows)] - const FAULTHANDLER_NSIGNALS: usize = 3; + const FAULTHANDLER_NSIGNALS: usize = 4; // CPython uses static arrays for signal handlers which requires mutable static access. // This is safe because: // 1. Signal handlers run in a single-threaded context (from the OS perspective) // 2. FAULTHANDLER_HANDLERS is only modified during enable/disable operations // 3. This matches CPython's faulthandler.c implementation - #[cfg(unix)] - static mut FAULTHANDLER_HANDLERS: [FaultHandler; FAULTHANDLER_NSIGNALS] = unsafe { - [ - FaultHandler { - signum: libc::SIGBUS, - enabled: false, - name: "Bus error", - previous: std::mem::zeroed(), - }, - FaultHandler { - signum: libc::SIGILL, - enabled: false, - name: "Illegal instruction", - previous: std::mem::zeroed(), - }, - FaultHandler { - signum: libc::SIGFPE, - enabled: false, - name: "Floating-point exception", - previous: std::mem::zeroed(), - }, - FaultHandler { - signum: libc::SIGABRT, - enabled: false, - name: "Aborted", - previous: std::mem::zeroed(), - }, - FaultHandler { - signum: libc::SIGSEGV, - enabled: false, - name: "Segmentation fault", - previous: std::mem::zeroed(), - }, - ] - }; - - #[cfg(windows)] static mut FAULTHANDLER_HANDLERS: [FaultHandler; FAULTHANDLER_NSIGNALS] = [ - FaultHandler { - signum: libc::SIGFPE, - enabled: false, - name: "Floating-point exception", - previous: 0, - }, - FaultHandler { - signum: libc::SIGABRT, - enabled: false, - name: "Aborted", - previous: 0, - }, - FaultHandler { - signum: libc::SIGSEGV, - enabled: false, - name: "Segmentation fault", - previous: 0, - }, + #[cfg(unix)] + FaultHandler::new(libc::SIGBUS, "Bus error"), + FaultHandler::new(libc::SIGILL, "Illegal instruction"), + FaultHandler::new(libc::SIGFPE, "Floating-point exception"), + FaultHandler::new(libc::SIGABRT, "Aborted"), + FaultHandler::new(libc::SIGSEGV, "Segmentation fault"), ]; /// fatal_error state From ac33c4abebbbd0c152c56400dc1bd36c68f4664b Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 12 Dec 2025 08:49:06 +0900 Subject: [PATCH 4/4] fix wasm32 build --- crates/stdlib/src/faulthandler.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/stdlib/src/faulthandler.rs b/crates/stdlib/src/faulthandler.rs index dfb1809a1e5..a06b063d305 100644 --- a/crates/stdlib/src/faulthandler.rs +++ b/crates/stdlib/src/faulthandler.rs @@ -9,7 +9,7 @@ mod decl { }; #[cfg(any(unix, windows))] use rustpython_common::os::{get_errno, set_errno}; - use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread; use std::time::Duration; @@ -68,8 +68,8 @@ mod decl { // 1. Signal handlers run in a single-threaded context (from the OS perspective) // 2. FAULTHANDLER_HANDLERS is only modified during enable/disable operations // 3. This matches CPython's faulthandler.c implementation + #[cfg(unix)] static mut FAULTHANDLER_HANDLERS: [FaultHandler; FAULTHANDLER_NSIGNALS] = [ - #[cfg(unix)] FaultHandler::new(libc::SIGBUS, "Bus error"), FaultHandler::new(libc::SIGILL, "Illegal instruction"), FaultHandler::new(libc::SIGFPE, "Floating-point exception"), @@ -77,6 +77,14 @@ mod decl { FaultHandler::new(libc::SIGSEGV, "Segmentation fault"), ]; + #[cfg(windows)] + static mut FAULTHANDLER_HANDLERS: [FaultHandler; FAULTHANDLER_NSIGNALS] = [ + FaultHandler::new(libc::SIGILL, "Illegal instruction"), + FaultHandler::new(libc::SIGFPE, "Floating-point exception"), + FaultHandler::new(libc::SIGABRT, "Aborted"), + FaultHandler::new(libc::SIGSEGV, "Segmentation fault"), + ]; + /// fatal_error state struct FatalErrorState { enabled: AtomicBool, @@ -106,6 +114,7 @@ mod decl { // Frame snapshot for signal-safe traceback (RustPython-specific) /// Frame information snapshot for signal-safe access + #[cfg(any(unix, windows))] #[derive(Clone, Copy)] struct FrameSnapshot { filename: [u8; 256], @@ -115,6 +124,7 @@ mod decl { funcname_len: usize, } + #[cfg(any(unix, windows))] impl FrameSnapshot { const EMPTY: Self = Self { filename: [0; 256], @@ -125,12 +135,15 @@ mod decl { }; } + #[cfg(any(unix, windows))] const MAX_SNAPSHOT_FRAMES: usize = 100; /// Signal-safe global storage for frame snapshots + #[cfg(any(unix, windows))] static mut FRAME_SNAPSHOTS: [FrameSnapshot; MAX_SNAPSHOT_FRAMES] = [FrameSnapshot::EMPTY; MAX_SNAPSHOT_FRAMES]; - static SNAPSHOT_COUNT: AtomicUsize = AtomicUsize::new(0); + #[cfg(any(unix, windows))] + static SNAPSHOT_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); // Signal-safe output functions @@ -698,6 +711,9 @@ mod decl { drop(guard); // Release lock before I/O // Timeout occurred, dump traceback + #[cfg(target_arch = "wasm32")] + let _ = (exit, fd, &header); + #[cfg(not(target_arch = "wasm32"))] { let header_bytes = header.as_bytes();