Skip to content
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
install handler
  • Loading branch information
youknowone committed Dec 11, 2025
commit 8a9096ad957a75b037ae4b8a0190a68082424197
198 changes: 181 additions & 17 deletions crates/stdlib/src/faulthandler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ mod decl {
use std::time::Duration;

static ENABLED: AtomicBool = AtomicBool::new(false);
#[allow(dead_code)]
static FATAL_ERROR_FD: AtomicI32 = AtomicI32::new(2); // stderr by default

// Watchdog thread state for dump_traceback_later
Expand All @@ -28,16 +27,57 @@ mod decl {
type WatchdogHandle = Arc<(Mutex<WatchdogState>, Condvar)>;
static WATCHDOG: Mutex<Option<WatchdogHandle>> = Mutex::new(None);

Comment on lines +14 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l crates/stdlib/src/faulthandler.rs

Repository: RustPython/RustPython

Length of output: 102


🏁 Script executed:

cat -n crates/stdlib/src/faulthandler.rs | sed -n '1,50p'

Repository: RustPython/RustPython

Length of output: 2055


🏁 Script executed:

cat -n crates/stdlib/src/faulthandler.rs | sed -n '160,180p'

Repository: RustPython/RustPython

Length of output: 814


🏁 Script executed:

cat -n crates/stdlib/src/faulthandler.rs | sed -n '280,300p'

Repository: RustPython/RustPython

Length of output: 738


🏁 Script executed:

cat -n crates/stdlib/src/faulthandler.rs | sed -n '100,160p'

Repository: RustPython/RustPython

Length of output: 2528


🏁 Script executed:

grep -n "FATAL_ERROR_FD" crates/stdlib/src/faulthandler.rs

Repository: RustPython/RustPython

Length of output: 268


🏁 Script executed:

cat -n crates/stdlib/src/faulthandler.rs | sed -n '179,250p'

Repository: RustPython/RustPython

Length of output: 2971


🏁 Script executed:

rg "fn get_file_for_output" crates/stdlib/src/faulthandler.rs -A 15

Repository: RustPython/RustPython

Length of output: 731


🏁 Script executed:

rg "fn get_fd_from_file" crates/stdlib/src/faulthandler.rs

Repository: RustPython/RustPython

Length of output: 164


🏁 Script executed:

rg "PyObject_AsFileDescriptor\|get_file_descriptor\|fileno" crates/stdlib/src/faulthandler.rs

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

cat -n crates/stdlib/src/faulthandler.rs | sed -n '90,115p'

Repository: RustPython/RustPython

Length of output: 1239


🏁 Script executed:

rg "fn get_fd_from_file_opt" crates/stdlib/src/faulthandler.rs -A 20

Repository: RustPython/RustPython

Length of output: 1026


enable(file=...) does not affect FATAL_ERROR_FD; the file argument is effectively ignored for fatal signals.

FATAL_ERROR_FD is initialized to 2 (stderr) and is read by both the Unix and Windows fatal signal handlers, but it is never updated when enable() is called. The function only validates the file argument via get_file_for_output() and then discards it, meaning fatal-signal messages always write to stderr regardless of the file parameter.

To mirror CPython's faulthandler.enable(file=...) behavior, resolve the file argument to a file descriptor and store it in FATAL_ERROR_FD:

 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)?;
-
-    ENABLED.store(true, Ordering::Relaxed);
+    // Resolve and validate output; also update fatal-error fd.
+    let fd = get_fd_from_file_opt(args.file, vm)?;
+    FATAL_ERROR_FD.store(fd, Ordering::Relaxed);
+    ENABLED.store(true, Ordering::Relaxed);

Consider also resetting FATAL_ERROR_FD back to 2 in disable().

Committable suggestion skipped: line range outside the PR's diff.

// Fatal signal numbers that should use enable() instead
// Number of fatal signals we handle
#[cfg(unix)]
const NUM_FATAL_SIGNALS: usize = 5;
#[cfg(windows)]
const NUM_FATAL_SIGNALS: usize = 3;

// Fatal signals to handle (with names for error messages)
#[cfg(unix)]
const FATAL_SIGNALS: &[i32] = &[
libc::SIGBUS,
libc::SIGILL,
libc::SIGFPE,
libc::SIGABRT,
libc::SIGSEGV,
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"),
];

#[cfg(windows)]
const FATAL_SIGNALS: [(libc::c_int, &str); NUM_FATAL_SIGNALS] = [
(libc::SIGFPE, "Floating-point exception"),
(libc::SIGABRT, "Aborted"),
(libc::SIGSEGV, "Segmentation fault"),
];

// Storage for previous signal handlers (Unix)
#[cfg(unix)]
static mut PREVIOUS_HANDLERS: [libc::sigaction; NUM_FATAL_SIGNALS] =
unsafe { std::mem::zeroed() };

/// 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(),
);
}
}

#[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,
);
}
}

const MAX_FUNCTION_NAME_LEN: usize = 500;

fn truncate_name(name: &str) -> String {
Expand Down Expand Up @@ -122,37 +162,147 @@ mod decl {
ENABLED.store(true, Ordering::Relaxed);

// Install signal handlers for fatal errors
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(unix, windows))]
{
install_fatal_handlers(vm);
}

Ok(())
}

#[cfg(not(target_arch = "wasm32"))]
/// Unix signal handler for fatal errors
#[cfg(unix)]
extern "C" fn faulthandler_fatal_error(signum: libc::c_int) {
// Reentrant protection
static REENTRANT: AtomicBool = AtomicBool::new(false);
if REENTRANT.swap(true, Ordering::SeqCst) {
return;
}

let fd = FATAL_ERROR_FD.load(Ordering::Relaxed);

// Find and print signal name
let signal_name = FATAL_SIGNALS
.iter()
.find(|(sig, _)| *sig == signum)
.map(|(_, name)| *name)
.unwrap_or("Unknown signal");

write_str_noraise(fd, "\nFatal Python error: ");
write_str_noraise(fd, signal_name);
write_str_noraise(fd, "\n\n");

// Restore previous handler
if let Some(idx) = FATAL_SIGNALS.iter().position(|(sig, _)| *sig == signum) {
unsafe {
libc::sigaction(signum, &PREVIOUS_HANDLERS[idx], std::ptr::null_mut());
}
}

REENTRANT.store(false, Ordering::SeqCst);

// Re-raise signal to trigger default behavior (core dump, etc.)
unsafe {
libc::raise(signum);
}
}

#[cfg(unix)]
fn install_fatal_handlers(_vm: &VirtualMachine) {
// TODO: Install actual signal handlers for SIGSEGV, SIGFPE, etc.
// This requires careful handling because signal handlers have limited capabilities.
// For now, this is a placeholder that marks the module as enabled.
for (idx, (signum, _)) in FATAL_SIGNALS.iter().enumerate() {
let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
action.sa_sigaction = faulthandler_fatal_error as usize;
action.sa_flags = libc::SA_NODEFER;

unsafe {
libc::sigaction(*signum, &action, &mut PREVIOUS_HANDLERS[idx]);
}
}
}
Comment on lines +211 to +222

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect signal handler setup: mixing sa_sigaction with simple handler.

Line 214 assigns to sa_sigaction but the handler signature is extern "C" fn(c_int), which is the sa_handler signature. Without SA_SIGINFO flag, you should use sa_handler instead. This is undefined behavior.

     #[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 usize;
+            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]);
             }
         }
     }

Note: On some platforms, sa_sigaction and sa_handler are a union. Ensure the cast is correct for your target platforms. The handler function signature extern "C" fn(c_int) is correct for a simple handler.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/stdlib/src/faulthandler.rs around lines 210 to 221, the signal handler
is being assigned to sa_sigaction while the function has the simple handler
signature (extern "C" fn(c_int)); change the assignment to use sa_handler
(casting the function pointer to the correct type) and keep sa_flags as-is, or
if you truly need sa_sigaction then change the handler signature to the
SA_SIGINFO form and set SA_SIGINFO in sa_flags; ensure the cast respects
platforms where sa_handler/sa_sigaction are a union so the pointer type is
correct for the target.


/// Windows signal handler for fatal errors (simpler than VEH)
#[cfg(windows)]
extern "C" fn faulthandler_fatal_error_windows(signum: libc::c_int) {
// Reentrant protection
static REENTRANT: AtomicBool = AtomicBool::new(false);
if REENTRANT.swap(true, Ordering::SeqCst) {
return;
}

let fd = FATAL_ERROR_FD.load(Ordering::Relaxed);

// Find and print signal name
let signal_name = FATAL_SIGNALS
.iter()
.find(|(sig, _)| *sig == signum)
.map(|(_, name)| *name)
.unwrap_or("Unknown signal");

write_str_noraise(fd, "\nFatal Python error: ");
write_str_noraise(fd, signal_name);
write_str_noraise(fd, "\n\n");

REENTRANT.store(false, Ordering::SeqCst);

// For SIGSEGV on Windows, need to loop raise
if signum == libc::SIGSEGV {
loop {
unsafe {
libc::raise(signum);
}
}
} else {
unsafe {
libc::raise(signum);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

#[cfg(windows)]
static mut PREVIOUS_HANDLERS_WIN: [libc::sighandler_t; NUM_FATAL_SIGNALS] =
[0; NUM_FATAL_SIGNALS];

#[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,
);
}
}
}

#[pyfunction]
fn disable() -> bool {
let was_enabled = ENABLED.swap(false, Ordering::Relaxed);

// Restore default signal handlers
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(unix, windows))]
{
uninstall_fatal_handlers();
}

was_enabled
}

#[cfg(not(target_arch = "wasm32"))]
#[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());
}
}
}

#[cfg(windows)]
fn uninstall_fatal_handlers() {
// TODO: Restore original signal handlers
for (idx, (signum, _)) in FATAL_SIGNALS.iter().enumerate() {
unsafe {
libc::signal(*signum, PREVIOUS_HANDLERS_WIN[idx]);
}
}
}

#[pyfunction]
Expand Down Expand Up @@ -251,6 +401,15 @@ mod decl {
#[cfg(not(target_arch = "wasm32"))]
{
let header_bytes = header.as_bytes();
#[cfg(windows)]
unsafe {
libc::write(
fd,
header_bytes.as_ptr() as *const libc::c_void,
header_bytes.len() as u32,
);
}
#[cfg(not(windows))]
unsafe {
libc::write(
fd,
Expand All @@ -263,6 +422,11 @@ mod decl {
// because we don't have access to the VM's frame stack.
// Just output a message indicating timeout occurred.
let msg = b"<timeout: cannot dump traceback from watchdog thread>\n";
#[cfg(windows)]
unsafe {
libc::write(fd, msg.as_ptr() as *const libc::c_void, msg.len() as u32);
}
#[cfg(not(windows))]
unsafe {
libc::write(fd, msg.as_ptr() as *const libc::c_void, msg.len());
}
Expand Down Expand Up @@ -473,7 +637,7 @@ mod decl {
#[cfg(unix)]
fn check_signum(signum: i32, vm: &VirtualMachine) -> PyResult<()> {
// Check if it's a fatal signal
if FATAL_SIGNALS.contains(&signum) {
if FATAL_SIGNALS.iter().any(|(sig, _)| *sig == signum) {
return Err(vm.new_runtime_error(format!(
"signal {} cannot be registered, use enable() instead",
signum
Expand Down