Merge pull request #6949 from youknowone/faulthandler

Update faulthandler to 3.14.2
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-02-03 13:43:25 +09:00
commit ec34befc3c
12 files changed
+608 -381

No files matched your search

+87 -77
View File
@@ -22,6 +22,16 @@ if not support.has_subprocess_support:
TIMEOUT = 0.5
STACK_HEADER_STR = r'Stack (most recent call first):'
# Regular expressions
STACK_HEADER = re.escape(STACK_HEADER_STR)
THREAD_NAME = r'( \[.*\])?'
THREAD_ID = fr'Thread 0x[0-9a-f]+{THREAD_NAME}'
THREAD_HEADER = fr'{THREAD_ID} \(most recent call first\):'
CURRENT_THREAD_ID = fr'Current thread 0x[0-9a-f]+{THREAD_NAME}'
CURRENT_THREAD_HEADER = fr'{CURRENT_THREAD_ID} \(most recent call first\):'
def expected_traceback(lineno1, lineno2, header, min_count=1):
regex = header
@@ -45,6 +55,13 @@ def temporary_filename():
finally:
os_helper.unlink(filename)
ADDRESS_EXPR = "0x[0-9a-f]+"
C_STACK_REGEX = [
r"Current thread's C stack trace \(most recent call first\):",
fr'( Binary file ".+"(, at .*(\+|-){ADDRESS_EXPR})? \[{ADDRESS_EXPR}\])|(<.+>)'
]
class FaultHandlerTests(unittest.TestCase):
def get_output(self, code, filename=None, fd=None):
@@ -93,6 +110,7 @@ class FaultHandlerTests(unittest.TestCase):
fd=None, know_current_thread=True,
py_fatal_error=False,
garbage_collecting=False,
c_stack=True,
function='<module>'):
"""
Check that the fault handler for fatal errors is enabled and check the
@@ -100,21 +118,32 @@ class FaultHandlerTests(unittest.TestCase):
Raise an error if the output doesn't match the expected format.
"""
if all_threads:
all_threads_disabled = (
all_threads
and (not sys._is_gil_enabled())
)
if all_threads and not all_threads_disabled:
if know_current_thread:
header = 'Current thread 0x[0-9a-f]+'
header = CURRENT_THREAD_HEADER
else:
header = 'Thread 0x[0-9a-f]+'
header = THREAD_HEADER
else:
header = 'Stack'
header = STACK_HEADER
regex = [f'^{fatal_error}']
if py_fatal_error:
regex.append("Python runtime state: initialized")
regex.append('')
regex.append(fr'{header} \(most recent call first\):')
if garbage_collecting:
regex.append(' Garbage-collecting')
regex.append(fr' File "<string>", line {lineno} in {function}')
if all_threads_disabled and not py_fatal_error:
regex.append("<Cannot show all threads while the GIL is disabled>")
regex.append(fr'{header}')
if support.Py_GIL_DISABLED and py_fatal_error and not know_current_thread:
regex.append(" <tstate is freed>")
else:
if garbage_collecting and not all_threads_disabled:
regex.append(' Garbage-collecting')
regex.append(fr' File "<string>", line {lineno} in {function}')
if c_stack:
regex.extend(C_STACK_REGEX)
regex = '\n'.join(regex)
if other_regex:
@@ -137,8 +166,6 @@ class FaultHandlerTests(unittest.TestCase):
fatal_error = 'Windows fatal exception: %s' % name_regex
self.check_error(code, line_number, fatal_error, **kw)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(sys.platform.startswith('aix'),
"the first page of memory is a mapped read-only on AIX")
def test_read_null(self):
@@ -162,8 +189,6 @@ class FaultHandlerTests(unittest.TestCase):
3,
'access violation')
# TODO: RUSTPYTHON, AssertionError: Regex didn't match
@unittest.expectedFailure
@skip_segfault_on_android
def test_sigsegv(self):
self.check_fatal_error("""
@@ -174,8 +199,7 @@ class FaultHandlerTests(unittest.TestCase):
3,
'Segmentation fault')
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '(?m)^Fatal Python error: Segmentation fault\n\n<Cannot show all threads while the GIL is disabled>\nStack\\ \\(most\\ recent\\ call\\ first\\):\n File "<string>", line 9 in __del__\nCurrent thread\'s C stack trace \\(most recent call first\\):\n( Binary file ".+"(, at .*(\\+|-)0x[0-9a-f]+)? \\[0x[0-9a-f]+\\])|(<.+>)' not found in 'exit'
@skip_segfault_on_android
def test_gc(self):
# bpo-44466: Detect if the GC is running
@@ -212,8 +236,7 @@ class FaultHandlerTests(unittest.TestCase):
function='__del__',
garbage_collecting=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 == 0
def test_fatal_error_c_thread(self):
self.check_fatal_error("""
import faulthandler
@@ -226,8 +249,7 @@ class FaultHandlerTests(unittest.TestCase):
func='faulthandler_fatal_error_thread',
py_fatal_error=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@support.skip_if_sanitizer("TSAN itercepts SIGABRT", thread=True)
def test_sigabrt(self):
self.check_fatal_error("""
import faulthandler
@@ -237,10 +259,9 @@ class FaultHandlerTests(unittest.TestCase):
3,
'Aborted')
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(sys.platform == 'win32',
"SIGFPE cannot be caught on Windows")
@support.skip_if_sanitizer("TSAN itercepts SIGFPE", thread=True)
def test_sigfpe(self):
self.check_fatal_error("""
import faulthandler
@@ -252,6 +273,7 @@ class FaultHandlerTests(unittest.TestCase):
@unittest.skipIf(_testcapi is None, 'need _testcapi')
@unittest.skipUnless(hasattr(signal, 'SIGBUS'), 'need signal.SIGBUS')
@support.skip_if_sanitizer("TSAN itercepts SIGBUS", thread=True)
@skip_segfault_on_android
def test_sigbus(self):
self.check_fatal_error("""
@@ -266,6 +288,7 @@ class FaultHandlerTests(unittest.TestCase):
@unittest.skipIf(_testcapi is None, 'need _testcapi')
@unittest.skipUnless(hasattr(signal, 'SIGILL'), 'need signal.SIGILL')
@support.skip_if_sanitizer("TSAN itercepts SIGILL", thread=True)
@skip_segfault_on_android
def test_sigill(self):
self.check_fatal_error("""
@@ -291,13 +314,9 @@ class FaultHandlerTests(unittest.TestCase):
func='_testcapi_fatal_error_impl',
py_fatal_error=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_fatal_error(self):
self.check_fatal_error_func(False)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_fatal_error_without_gil(self):
self.check_fatal_error_func(True)
@@ -316,8 +335,6 @@ class FaultHandlerTests(unittest.TestCase):
'(?:Segmentation fault|Bus error)',
other_regex='unable to raise a stack overflow')
# TODO: RUSTPYTHON
@unittest.expectedFailure
@skip_segfault_on_android
def test_gil_released(self):
self.check_fatal_error("""
@@ -328,8 +345,6 @@ class FaultHandlerTests(unittest.TestCase):
3,
'Segmentation fault')
# TODO: RUSTPYTHON
@unittest.expectedFailure
@skip_segfault_on_android
def test_enable_file(self):
with temporary_filename() as filename:
@@ -343,8 +358,6 @@ class FaultHandlerTests(unittest.TestCase):
'Segmentation fault',
filename=filename)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(sys.platform == "win32",
"subprocess doesn't support pass_fds on Windows")
@skip_segfault_on_android
@@ -361,8 +374,6 @@ class FaultHandlerTests(unittest.TestCase):
'Segmentation fault',
fd=fd)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@skip_segfault_on_android
def test_enable_single_thread(self):
self.check_fatal_error("""
@@ -389,8 +400,7 @@ class FaultHandlerTests(unittest.TestCase):
"%r is present in %r" % (not_expected, stderr))
self.assertNotEqual(exitcode, 0)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Cannot find 'Extension modules:' in 'Fatal Python error: Segmentation fault\n\nCurrent thread 0x0000000000004284 (most recent call first):\n File "<string>", line 6 in <module>'
@skip_segfault_on_android
def test_dump_ext_modules(self):
code = """
@@ -511,7 +521,7 @@ class FaultHandlerTests(unittest.TestCase):
else:
lineno = 14
expected = [
'Stack (most recent call first):',
f'{STACK_HEADER_STR}',
' File "<string>", line %s in funcB' % lineno,
' File "<string>", line 17 in funcA',
' File "<string>", line 19 in <module>'
@@ -523,14 +533,10 @@ class FaultHandlerTests(unittest.TestCase):
def test_dump_traceback(self):
self.check_dump_traceback()
# TODO: RUSTPYTHON - binary file write needs different handling
@unittest.expectedFailure
def test_dump_traceback_file(self):
with temporary_filename() as filename:
self.check_dump_traceback(filename=filename)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(sys.platform == "win32",
"subprocess doesn't support pass_fds on Windows")
def test_dump_traceback_fd(self):
@@ -553,7 +559,7 @@ class FaultHandlerTests(unittest.TestCase):
func_name=func_name,
)
expected = [
'Stack (most recent call first):',
f'{STACK_HEADER_STR}',
' File "<string>", line 4 in %s' % truncated,
' File "<string>", line 6 in <module>'
]
@@ -607,28 +613,24 @@ class FaultHandlerTests(unittest.TestCase):
lineno = 10
# When the traceback is dumped, the waiter thread may be in the
# `self.running.set()` call or in `self.stop.wait()`.
regex = r"""
^Thread 0x[0-9a-f]+ \(most recent call first\):
regex = fr"""
^{THREAD_HEADER}
(?: File ".*threading.py", line [0-9]+ in [_a-z]+
){{1,3}} File "<string>", line (?:22|23) in run
File ".*threading.py", line [0-9]+ in _bootstrap_inner
File ".*threading.py", line [0-9]+ in _bootstrap
Current thread 0x[0-9a-f]+ \(most recent call first\):
{CURRENT_THREAD_HEADER}
File "<string>", line {lineno} in dump
File "<string>", line 28 in <module>$
"""
regex = dedent(regex.format(lineno=lineno)).strip()
regex = dedent(regex).strip()
self.assertRegex(output, regex)
self.assertEqual(exitcode, 0)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_dump_traceback_threads(self):
self.check_dump_traceback_threads(None)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_dump_traceback_threads_file(self):
with temporary_filename() as filename:
self.check_dump_traceback_threads(filename)
@@ -688,44 +690,33 @@ class FaultHandlerTests(unittest.TestCase):
count = loops
if repeat:
count *= 2
header = r'Timeout \(%s\)!\nThread 0x[0-9a-f]+ \(most recent call first\):\n' % timeout_str
header = (fr'Timeout \({timeout_str}\)!\n'
fr'{THREAD_HEADER}\n')
regex = expected_traceback(17, 26, header, min_count=count)
self.assertRegex(trace, regex)
else:
self.assertEqual(trace, '')
self.assertEqual(exitcode, 0)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_dump_traceback_later(self):
self.check_dump_traceback_later()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_dump_traceback_later_repeat(self):
self.check_dump_traceback_later(repeat=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_dump_traceback_later_cancel(self):
self.check_dump_traceback_later(cancel=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_dump_traceback_later_file(self):
with temporary_filename() as filename:
self.check_dump_traceback_later(filename=filename)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(sys.platform == "win32",
"subprocess doesn't support pass_fds on Windows")
def test_dump_traceback_later_fd(self):
with tempfile.TemporaryFile('wb+') as fp:
self.check_dump_traceback_later(fd=fp.fileno())
# TODO: RUSTPYTHON
@unittest.expectedFailure
@support.requires_resource('walltime')
def test_dump_traceback_later_twice(self):
self.check_dump_traceback_later(loops=2)
@@ -801,9 +792,9 @@ class FaultHandlerTests(unittest.TestCase):
trace = '\n'.join(trace)
if not unregister:
if all_threads:
regex = r'Current thread 0x[0-9a-f]+ \(most recent call first\):\n'
regex = fr'{CURRENT_THREAD_HEADER}\n'
else:
regex = r'Stack \(most recent call first\):\n'
regex = fr'{STACK_HEADER}\n'
regex = expected_traceback(14, 32, regex)
self.assertRegex(trace, regex)
else:
@@ -813,37 +804,26 @@ class FaultHandlerTests(unittest.TestCase):
else:
self.assertEqual(exitcode, 0)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_register(self):
self.check_register()
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_unregister(self):
self.check_register(unregister=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_register_file(self):
with temporary_filename() as filename:
self.check_register(filename=filename)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(sys.platform == "win32",
"subprocess doesn't support pass_fds on Windows")
def test_register_fd(self):
with tempfile.TemporaryFile('wb+') as fp:
self.check_register(fd=fp.fileno())
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_register_threads(self):
self.check_register(all_threads=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@support.skip_if_sanitizer("gh-129825: hangs under TSAN", thread=True)
def test_register_chain(self):
self.check_register(chain=True)
@@ -871,8 +851,6 @@ class FaultHandlerTests(unittest.TestCase):
with self.check_stderr_none():
faulthandler.register(signal.SIGUSR1)
# TODO: RUSTPYTHON, AttributeError: module 'msvcrt' has no attribute 'GetErrorMode'
@unittest.expectedFailure
@unittest.skipUnless(MS_WINDOWS, 'specific to Windows')
def test_raise_exception(self):
for exc, name in (
@@ -985,5 +963,37 @@ class FaultHandlerTests(unittest.TestCase):
_, exitcode = self.get_output(code)
self.assertEqual(exitcode, 0)
def check_c_stack(self, output):
starting_line = output.pop(0)
self.assertRegex(starting_line, C_STACK_REGEX[0])
self.assertGreater(len(output), 0)
for line in output:
with self.subTest(line=line):
if line != '': # Ignore trailing or leading newlines
self.assertRegex(line, C_STACK_REGEX[1])
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 0
def test_dump_c_stack(self):
code = dedent("""
import faulthandler
faulthandler.dump_c_stack()
""")
output, exitcode = self.get_output(code)
self.assertEqual(exitcode, 0)
self.check_c_stack(output)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'faulthandler' has no attribute 'dump_c_stack'
def test_dump_c_stack_file(self):
import tempfile
with tempfile.TemporaryFile("w+") as tmp:
faulthandler.dump_c_stack(file=tmp)
tmp.flush() # Just in case
tmp.seek(0)
self.check_c_stack(tmp.read().split("\n"))
if __name__ == "__main__":
unittest.main()
-1
View File
@@ -540,7 +540,6 @@ class TestInterpreterStack(IsTestBase):
self.istest(inspect.istraceback, 'git.ex.__traceback__')
self.istest(inspect.isframe, 'mod.fr')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_stack(self):
self.assertTrue(len(mod.st) >= 5)
frame1, frame2, frame3, frame4, *_ = mod.st
-2
View File
@@ -716,8 +716,6 @@ class ListComprehensionTest(unittest.TestCase):
self._check_in_scopes(code, {"x": 2, "y": [3]}, ns={"x": 3}, scopes=["class"])
self._check_in_scopes(code, {"x": 2, "y": [2]}, ns={"x": 3}, scopes=["function", "module"])
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_exception_locations(self):
# The location of an exception raised from __init__ or
# __next__ should should be the iterator expression
-1
View File
@@ -152,7 +152,6 @@ We also repeat each of the above scoping tests inside a function
"""
class SetComprehensionTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FrameSummary' object has no attribute 'end_lineno'
def test_exception_locations(self):
# The location of an exception raised from __init__ or
# __next__ should should be the iterator expression
-4
View File
@@ -3423,8 +3423,6 @@ class TestStack(unittest.TestCase):
s = traceback.StackSummary.extract(iter([(f, 6)]))
self.assertEqual(s[0].locals, None)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_format_locals(self):
def some_inner(k, v):
a = 1
@@ -3441,8 +3439,6 @@ class TestStack(unittest.TestCase):
' v = 4\n' % (__file__, some_inner.__code__.co_firstlineno + 3)
], s.format())
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_custom_format_frame(self):
class CustomStackSummary(traceback.StackSummary):
def format_frame_summary(self, frame_summary, colorize=False):
+15 -4
View File
@@ -4638,7 +4638,7 @@ impl Compiler {
self.emit_load_const(ConstantData::Str { value: name.into() });
if let Some(arguments) = arguments {
self.codegen_call_helper(2, arguments)?;
self.codegen_call_helper(2, arguments, self.current_source_range)?;
} else {
emit!(self, Instruction::Call { nargs: 2 });
}
@@ -7079,6 +7079,10 @@ impl Compiler {
}
fn compile_call(&mut self, func: &ast::Expr, args: &ast::Arguments) -> CompileResult<()> {
// Save the call expression's source range so CALL instructions use the
// call start line, not the last argument's line.
let call_range = self.current_source_range;
// Method call: obj → LOAD_ATTR_METHOD → [method, self_or_null] → args → CALL
// Regular call: func → PUSH_NULL → args → CALL
if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = &func {
@@ -7096,21 +7100,21 @@ impl Compiler {
self.emit_load_zero_super_method(idx);
}
}
self.codegen_call_helper(0, args)?;
self.codegen_call_helper(0, args, call_range)?;
} else {
// Normal method call: compile object, then LOAD_ATTR with method flag
// LOAD_ATTR(method=1) pushes [method, self_or_null] on stack
self.compile_expression(value)?;
let idx = self.name(attr.as_str());
self.emit_load_attr_method(idx);
self.codegen_call_helper(0, args)?;
self.codegen_call_helper(0, args, call_range)?;
}
} else {
// Regular call: push func, then NULL for self_or_null slot
// Stack layout: [func, NULL, args...] - same as method call [func, self, args...]
self.compile_expression(func)?;
emit!(self, Instruction::PushNull);
self.codegen_call_helper(0, args)?;
self.codegen_call_helper(0, args, call_range)?;
}
Ok(())
}
@@ -7152,10 +7156,13 @@ impl Compiler {
}
/// Compile call arguments and emit the appropriate CALL instruction.
/// `call_range` is the source range of the call expression, used to set
/// the correct line number on the CALL instruction.
fn codegen_call_helper(
&mut self,
additional_positional: u32,
arguments: &ast::Arguments,
call_range: TextRange,
) -> CompileResult<()> {
let nelts = arguments.args.len();
let nkwelts = arguments.keywords.len();
@@ -7186,6 +7193,8 @@ impl Compiler {
self.compile_expression(&keyword.value)?;
}
// Restore call expression range for kwnames and CALL_KW
self.set_source_range(call_range);
self.emit_load_const(ConstantData::Tuple {
elements: kwarg_names,
});
@@ -7193,6 +7202,7 @@ impl Compiler {
let nargs = additional_positional + nelts.to_u32() + nkwelts.to_u32();
emit!(self, Instruction::CallKw { nargs });
} else {
self.set_source_range(call_range);
let nargs = additional_positional + nelts.to_u32();
emit!(self, Instruction::Call { nargs });
}
@@ -7284,6 +7294,7 @@ impl Compiler {
emit!(self, Instruction::PushNull);
}
self.set_source_range(call_range);
emit!(self, Instruction::CallFunctionEx);
}
+396 -269
View File
@@ -7,7 +7,6 @@ mod decl {
PyObjectRef, PyResult, VirtualMachine,
frame::Frame,
function::{ArgIntoFloat, OptionalArg},
py_io::Write,
};
use alloc::sync::Arc;
use core::sync::atomic::{AtomicBool, AtomicI32, Ordering};
@@ -66,11 +65,7 @@ mod decl {
#[cfg(windows)]
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
// Signal handlers use mutable statics matching faulthandler.c implementation.
#[cfg(unix)]
static mut FAULTHANDLER_HANDLERS: [FaultHandler; FAULTHANDLER_NSIGNALS] = [
FaultHandler::new(libc::SIGBUS, "Bus error"),
@@ -101,6 +96,10 @@ mod decl {
all_threads: AtomicBool::new(true),
};
/// Arc<Mutex<Vec<FrameRef>>> - shared frame slot for a thread
#[cfg(feature = "threading")]
type ThreadFrameSlot = Arc<parking_lot::Mutex<Vec<crate::vm::frame::FrameRef>>>;
// Watchdog thread state for dump_traceback_later
struct WatchdogState {
cancel: bool,
@@ -109,46 +108,13 @@ mod decl {
repeat: bool,
exit: bool,
header: String,
#[cfg(feature = "threading")]
thread_frame_slots: Vec<(u64, ThreadFrameSlot)>,
}
type WatchdogHandle = Arc<(Mutex<WatchdogState>, Condvar)>;
static WATCHDOG: Mutex<Option<WatchdogHandle>> = Mutex::new(None);
// 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],
filename_len: usize,
lineno: u32,
funcname: [u8; 128],
funcname_len: usize,
}
#[cfg(any(unix, windows))]
impl FrameSnapshot {
const EMPTY: Self = Self {
filename: [0; 256],
filename_len: 0,
lineno: 0,
funcname: [0; 128],
funcname_len: 0,
};
}
#[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];
#[cfg(any(unix, windows))]
static SNAPSHOT_COUNT: core::sync::atomic::AtomicUsize =
core::sync::atomic::AtomicUsize::new(0);
// Signal-safe output functions
// PUTS macro
@@ -166,6 +132,20 @@ mod decl {
};
}
#[cfg(any(unix, windows))]
fn puts_bytes(fd: i32, s: &[u8]) {
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())
}
};
}
// _Py_DumpHexadecimal (traceback.c)
#[cfg(any(unix, windows))]
fn dump_hexadecimal(fd: i32, value: u64, width: usize) {
@@ -235,59 +215,69 @@ mod decl {
// write_thread_id (traceback.c:1240-1256)
#[cfg(any(unix, windows))]
fn write_thread_id(fd: i32, is_current: bool) {
fn write_thread_id(fd: i32, thread_id: u64, is_current: bool) {
if is_current {
puts(fd, "Current thread 0x");
puts(fd, "Current thread ");
} else {
puts(fd, "Thread 0x");
puts(fd, "Thread ");
}
let thread_id = current_thread_id();
// Use appropriate width based on platform pointer size
dump_hexadecimal(fd, thread_id, core::mem::size_of::<usize>() * 2);
puts(fd, " (most recent call first):\n");
}
// dump_frame (traceback.c:1037-1087)
/// Dump the current thread's live frame chain to fd (signal-safe).
/// Walks the `Frame.previous` pointer chain starting from the
/// thread-local current frame pointer.
#[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())
fn dump_live_frames(fd: i32) {
const MAX_FRAME_DEPTH: usize = 100;
let mut frame_ptr = crate::vm::vm::thread::get_current_frame();
if frame_ptr.is_null() {
puts(fd, " <no Python frame>\n");
return;
}
let mut depth = 0;
while !frame_ptr.is_null() && depth < MAX_FRAME_DEPTH {
let frame = unsafe { &*frame_ptr };
dump_frame_from_raw(fd, frame);
frame_ptr = frame.previous_frame();
depth += 1;
}
if depth >= MAX_FRAME_DEPTH && !frame_ptr.is_null() {
puts(fd, " ...\n");
}
}
/// Dump a single frame's info to fd (signal-safe), reading live data.
#[cfg(any(unix, windows))]
fn dump_frame_from_raw(fd: i32, frame: &Frame) {
let filename = frame.code.source_path.as_str();
let funcname = frame.code.obj_name.as_str();
let lasti = frame.lasti();
let lineno = if lasti == 0 {
frame.code.first_line_number.map(|n| n.get()).unwrap_or(1) as u32
} else {
let idx = (lasti as usize).saturating_sub(1);
if idx < frame.code.locations.len() {
frame.code.locations[idx].0.line.get() as u32
} else {
frame.code.first_line_number.map(|n| n.get()).unwrap_or(0) as u32
}
};
puts(fd, " File \"");
dump_ascii(fd, filename);
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())
}
};
dump_ascii(fd, funcname);
puts(fd, "\n");
}
// faulthandler_dump_traceback
// faulthandler_dump_traceback (signal-safe, for fatal errors)
#[cfg(any(unix, windows))]
fn faulthandler_dump_traceback(fd: i32, _all_threads: bool) {
fn faulthandler_dump_traceback(fd: i32, all_threads: bool) {
static REENTRANT: AtomicBool = AtomicBool::new(false);
if REENTRANT.swap(true, Ordering::SeqCst) {
@@ -295,76 +285,82 @@ mod decl {
}
// 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],
);
}
}
}
if all_threads {
write_thread_id(fd, current_thread_id(), true);
} else {
puts(fd, " <no Python frame>\n");
puts(fd, "Stack (most recent call first):\n");
}
dump_live_frames(fd);
REENTRANT.store(false, Ordering::SeqCst);
}
const MAX_FUNCTION_NAME_LEN: usize = 500;
/// MAX_STRING_LENGTH in traceback.c
const MAX_STRING_LENGTH: usize = 500;
fn truncate_name(name: &str) -> String {
if name.len() > MAX_FUNCTION_NAME_LEN {
format!("{}...", &name[..MAX_FUNCTION_NAME_LEN])
} else {
name.to_string()
/// Truncate a UTF-8 string to at most `max_bytes` without splitting a
/// multi-byte codepoint. Signal-safe (no allocation, no panic).
#[cfg(any(unix, windows))]
fn safe_truncate(s: &str, max_bytes: usize) -> (&str, bool) {
if s.len() <= max_bytes {
return (s, false);
}
let mut end = max_bytes;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
(&s[..end], true)
}
/// Write a string to fd, truncating with "..." if it exceeds MAX_STRING_LENGTH.
/// Mirrors `_Py_DumpASCII` truncation behavior.
#[cfg(any(unix, windows))]
fn dump_ascii(fd: i32, s: &str) {
let (truncated_s, was_truncated) = safe_truncate(s, MAX_STRING_LENGTH);
puts(fd, truncated_s);
if was_truncated {
puts(fd, "...");
}
}
fn get_file_for_output(
file: OptionalArg<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<PyObjectRef> {
match file {
OptionalArg::Present(f) => {
// If it's an integer, we can't use it directly as a file object
// For now, just return it and let the caller handle it
Ok(f)
}
OptionalArg::Missing => {
// Get sys.stderr
let stderr = vm.sys_module.get_attr("stderr", vm)?;
if vm.is_none(&stderr) {
return Err(vm.new_runtime_error("sys.stderr is None".to_owned()));
}
Ok(stderr)
}
}
}
fn collect_frame_info(frame: &crate::vm::PyRef<Frame>) -> String {
let func_name = truncate_name(frame.code.obj_name.as_str());
// If lasti is 0, execution hasn't started yet - use first line number or 1
let line = if frame.lasti() == 0 {
frame.code.first_line_number.map(|n| n.get()).unwrap_or(1)
/// Write a frame's info to an fd using signal-safe I/O.
#[cfg(any(unix, windows))]
fn dump_frame_from_ref(fd: i32, frame: &crate::vm::PyRef<Frame>) {
let funcname = frame.code.obj_name.as_str();
let filename = frame.code.source_path.as_str();
let lineno = if frame.lasti() == 0 {
frame.code.first_line_number.map(|n| n.get()).unwrap_or(1) as u32
} else {
frame.current_location().line.get()
frame.current_location().line.get() as u32
};
format!(
" File \"{}\", line {} in {}",
frame.code.source_path, line, func_name
)
puts(fd, " File \"");
dump_ascii(fd, filename);
puts(fd, "\", line ");
dump_decimal(fd, lineno as usize);
puts(fd, " in ");
dump_ascii(fd, funcname);
puts(fd, "\n");
}
/// Dump traceback for a thread given its frame stack (for cross-thread dumping).
#[cfg(all(any(unix, windows), feature = "threading"))]
fn dump_traceback_thread_frames(
fd: i32,
thread_id: u64,
is_current: bool,
frames: &[crate::vm::frame::FrameRef],
) {
write_thread_id(fd, thread_id, is_current);
if frames.is_empty() {
puts(fd, " <no Python frame>\n");
} else {
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
}
}
}
#[derive(FromArgs)]
@@ -377,22 +373,70 @@ mod decl {
#[pyfunction]
fn dump_traceback(args: DumpTracebackArgs, vm: &VirtualMachine) -> PyResult<()> {
let _ = args.all_threads; // TODO: implement all_threads support
let fd = get_fd_from_file_opt(args.file, vm)?;
let file = get_file_for_output(args.file, vm)?;
// Collect frame info first to avoid RefCell borrow conflict
let frame_lines: Vec<String> = vm.frames.borrow().iter().map(collect_frame_info).collect();
// Now write to file (in reverse order - most recent call first)
let mut writer = crate::vm::py_io::PyWriter(file, vm);
writeln!(writer, "Stack (most recent call first):")?;
for line in frame_lines.iter().rev() {
writeln!(writer, "{}", line)?;
#[cfg(any(unix, windows))]
{
if args.all_threads {
dump_all_threads(fd, vm);
} else {
puts(fd, "Stack (most recent call first):\n");
let frames = vm.frames.borrow();
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
}
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = (fd, args.all_threads);
}
Ok(())
}
/// Dump tracebacks of all threads.
#[cfg(any(unix, windows))]
fn dump_all_threads(fd: i32, vm: &VirtualMachine) {
// Get all threads' frame stacks from the shared registry
#[cfg(feature = "threading")]
{
let current_tid = rustpython_vm::stdlib::thread::get_ident();
let registry = vm.state.thread_frames.lock();
// First dump non-current threads, then current thread last
for (&tid, slot) in registry.iter() {
if tid == current_tid {
continue;
}
let frames_guard = slot.lock();
dump_traceback_thread_frames(fd, tid, false, &frames_guard);
puts(fd, "\n");
}
// Now dump current thread (use vm.frames for most up-to-date data)
write_thread_id(fd, current_tid, true);
let frames = vm.frames.borrow();
if frames.is_empty() {
puts(fd, " <no Python frame>\n");
} else {
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
}
}
}
#[cfg(not(feature = "threading"))]
{
write_thread_id(fd, current_thread_id(), true);
let frames = vm.frames.borrow();
for frame in frames.iter().rev() {
dump_frame_from_ref(fd, frame);
}
}
}
#[derive(FromArgs)]
#[allow(unused)]
struct EnableArgs {
@@ -464,9 +508,8 @@ mod decl {
.find(|h| h.signum == signum)
};
// faulthandler_fatal_error
if let Some(h) = handler {
// Disable handler first (restores previous)
// Disable handler (restores previous)
unsafe {
faulthandler_disable_fatal_handler(h);
}
@@ -480,18 +523,24 @@ mod decl {
puts(fd, "\n\n");
}
// 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);
// raise
// Called immediately thanks to SA_NODEFER flag
// Reset to default handler and re-raise to ensure process terminates.
// We cannot just restore the previous handler because Rust's runtime
// may have installed its own SIGSEGV handler (for stack overflow detection)
// that doesn't terminate the process on software-raised signals.
unsafe {
libc::signal(signum, libc::SIG_DFL);
libc::raise(signum);
}
// Fallback if raise() somehow didn't terminate the process
unsafe {
libc::_exit(1);
}
}
// faulthandler_fatal_error for Windows
@@ -529,14 +578,84 @@ mod decl {
set_errno(save_errno);
// On Windows, don't explicitly call the previous handler for SIGSEGV
if signum == libc::SIGSEGV {
return;
}
unsafe {
libc::signal(signum, libc::SIG_DFL);
libc::raise(signum);
}
// Fallback
std::process::exit(1);
}
// Windows vectored exception handler (faulthandler.c:417-480)
#[cfg(windows)]
static EXC_HANDLER: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
#[cfg(windows)]
fn faulthandler_ignore_exception(code: u32) -> bool {
// bpo-30557: ignore exceptions which are not errors
if (code & 0x80000000) == 0 {
return true;
}
// bpo-31701: ignore MSC and COM exceptions
if code == 0xE06D7363 || code == 0xE0434352 {
return true;
}
false
}
#[cfg(windows)]
unsafe extern "system" fn faulthandler_exc_handler(
exc_info: *mut windows_sys::Win32::System::Diagnostics::Debug::EXCEPTION_POINTERS,
) -> i32 {
const EXCEPTION_CONTINUE_SEARCH: i32 = 0;
if !FATAL_ERROR.enabled.load(Ordering::Relaxed) {
return EXCEPTION_CONTINUE_SEARCH;
}
let record = unsafe { &*(*exc_info).ExceptionRecord };
let code = record.ExceptionCode as u32;
if faulthandler_ignore_exception(code) {
return EXCEPTION_CONTINUE_SEARCH;
}
let fd = FATAL_ERROR.fd.load(Ordering::Relaxed);
puts(fd, "Windows fatal exception: ");
match code {
0xC0000005 => puts(fd, "access violation"),
0xC000008C => puts(fd, "float divide by zero"),
0xC0000091 => puts(fd, "float overflow"),
0xC0000094 => puts(fd, "int divide by zero"),
0xC0000095 => puts(fd, "integer overflow"),
0xC0000006 => puts(fd, "page error"),
0xC00000FD => puts(fd, "stack overflow"),
0xC000001D => puts(fd, "illegal instruction"),
_ => {
puts(fd, "code ");
dump_hexadecimal(fd, code as u64, 8);
}
}
puts(fd, "\n\n");
// Disable SIGSEGV handler for access violations to avoid double output
if code == 0xC0000005 {
unsafe {
for handler in FAULTHANDLER_HANDLERS.iter_mut() {
if handler.signum == libc::SIGSEGV {
faulthandler_disable_fatal_handler(handler);
break;
}
}
}
}
let all_threads = FATAL_ERROR.all_threads.load(Ordering::Relaxed);
faulthandler_dump_traceback(fd, all_threads);
EXCEPTION_CONTINUE_SEARCH
}
// faulthandler_enable
@@ -595,6 +714,14 @@ mod decl {
}
}
// Register Windows vectored exception handler
#[cfg(windows)]
{
use windows_sys::Win32::System::Diagnostics::Debug::AddVectoredExceptionHandler;
let h = unsafe { AddVectoredExceptionHandler(1, Some(faulthandler_exc_handler)) };
EXC_HANDLER.store(h as usize, Ordering::Relaxed);
}
FATAL_ERROR.enabled.store(true, Ordering::Relaxed);
true
}
@@ -611,6 +738,18 @@ mod decl {
faulthandler_disable_fatal_handler(handler);
}
}
// Remove Windows vectored exception handler
#[cfg(windows)]
{
use windows_sys::Win32::System::Diagnostics::Debug::RemoveVectoredExceptionHandler;
let h = EXC_HANDLER.swap(0, Ordering::Relaxed);
if h != 0 {
unsafe {
RemoveVectoredExceptionHandler(h as *mut core::ffi::c_void);
}
}
}
}
#[cfg(not(any(unix, windows)))]
@@ -646,16 +785,17 @@ mod decl {
let hour = min / 60;
let min = min % 60;
// Match Python's timedelta str format: H:MM:SS.ffffff (no leading zero for hours)
if us != 0 {
format!("Timeout ({:02}:{:02}:{:02}.{:06})!\n", hour, min, sec, us)
format!("Timeout ({}:{:02}:{:02}.{:06})!\n", hour, min, sec, us)
} else {
format!("Timeout ({:02}:{:02}:{:02})!\n", hour, min, sec)
format!("Timeout ({}:{:02}:{:02})!\n", hour, min, sec)
}
}
fn get_fd_from_file_opt(file: OptionalArg<PyObjectRef>, vm: &VirtualMachine) -> PyResult<i32> {
match file {
OptionalArg::Present(f) => {
OptionalArg::Present(f) if !vm.is_none(&f) => {
// Check if it's an integer (file descriptor)
if let Ok(fd) = f.try_to_value::<i32>(vm) {
if fd < 0 {
@@ -677,8 +817,8 @@ mod decl {
let _ = vm.call_method(&f, "flush", ());
Ok(fd)
}
OptionalArg::Missing => {
// Get sys.stderr
_ => {
// file=None or file not passed: fall back to sys.stderr
let stderr = vm.sys_module.get_attr("stderr", vm)?;
if vm.is_none(&stderr) {
return Err(vm.new_runtime_error("sys.stderr is None".to_owned()));
@@ -709,8 +849,12 @@ mod decl {
}
// Extract values before releasing lock for I/O
let (repeat, exit, fd, header) =
(guard.repeat, guard.exit, guard.fd, guard.header.clone());
let repeat = guard.repeat;
let exit = guard.exit;
let fd = guard.fd;
let header = guard.header.clone();
#[cfg(feature = "threading")]
let thread_frame_slots = guard.thread_frame_slots.clone();
drop(guard); // Release lock before I/O
// Timeout occurred, dump traceback
@@ -719,35 +863,21 @@ 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,
header_bytes.as_ptr() as *const libc::c_void,
header_bytes.len(),
);
}
puts_bytes(fd, header.as_bytes());
// Note: We cannot dump actual Python traceback from a separate thread
// 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);
// Use thread frame slots when threading is enabled (includes all threads).
// Fall back to live frame walking for non-threaded builds.
#[cfg(feature = "threading")]
{
for (tid, slot) in &thread_frame_slots {
let frames = slot.lock();
dump_traceback_thread_frames(fd, *tid, false, &frames);
}
}
#[cfg(not(windows))]
unsafe {
libc::write(fd, msg.as_ptr() as *const libc::c_void, msg.len());
#[cfg(not(feature = "threading"))]
{
write_thread_id(fd, current_thread_id(), false);
dump_live_frames(fd);
}
if exit {
@@ -792,6 +922,16 @@ mod decl {
let header = format_timeout(timeout_us);
// Snapshot thread frame slots so watchdog can dump tracebacks
#[cfg(feature = "threading")]
let thread_frame_slots: Vec<(u64, ThreadFrameSlot)> = {
let registry = vm.state.thread_frames.lock();
registry
.iter()
.map(|(&id, slot)| (id, Arc::clone(slot)))
.collect()
};
// Cancel any previous watchdog
cancel_dump_traceback_later();
@@ -804,6 +944,8 @@ mod decl {
repeat: args.repeat,
exit: args.exit,
header,
#[cfg(feature = "threading")]
thread_frame_slots,
}),
Condvar::new(),
));
@@ -845,14 +987,13 @@ mod decl {
const NSIG: usize = 64;
#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct UserSignal {
pub enabled: bool,
pub fd: i32,
#[allow(dead_code)]
pub all_threads: bool,
pub chain: bool,
pub previous: libc::sighandler_t,
pub previous: libc::sigaction,
}
impl Default for UserSignal {
@@ -862,7 +1003,8 @@ mod decl {
fd: 2, // stderr
all_threads: true,
chain: false,
previous: libc::SIG_DFL,
// SAFETY: sigaction is a C struct that can be zero-initialized
previous: unsafe { core::mem::zeroed() },
}
}
}
@@ -892,7 +1034,7 @@ mod decl {
&& signum < v.len()
&& v[signum].enabled
{
let old = v[signum].clone();
let old = v[signum];
v[signum] = UserSignal::default();
return Some(old);
}
@@ -910,38 +1052,33 @@ mod decl {
#[cfg(unix)]
extern "C" fn faulthandler_user_signal(signum: libc::c_int) {
let save_errno = get_errno();
let user = match user_signals::get_user_signal(signum as usize) {
Some(u) if u.enabled => u,
_ => return,
};
// Write traceback header
let header = b"Current thread 0x0000 (most recent call first):\n";
let _ = unsafe {
libc::write(
user.fd,
header.as_ptr() as *const libc::c_void,
header.len(),
)
};
faulthandler_dump_traceback(user.fd, user.all_threads);
// Note: We cannot easily access RustPython's frame stack from a signal handler
// because signal handlers run asynchronously. We just output a placeholder.
let msg = b" <signal handler invoked, traceback unavailable in signal context>\n";
let _ = unsafe { libc::write(user.fd, msg.as_ptr() as *const libc::c_void, msg.len()) };
// If chain is enabled, call the previous handler
if user.chain && user.previous != libc::SIG_DFL && user.previous != libc::SIG_IGN {
// Re-register the old handler and raise the signal
if user.chain {
// Restore the previous handler and re-raise
unsafe {
libc::signal(signum, user.previous);
libc::raise(signum);
// Re-register our handler
libc::signal(
signum,
faulthandler_user_signal as *const () as libc::sighandler_t,
);
libc::sigaction(signum, &user.previous, core::ptr::null_mut());
}
set_errno(save_errno);
unsafe {
libc::raise(signum);
}
// Re-install our handler with the same flags as register()
let save_errno2 = get_errno();
unsafe {
let mut action: libc::sigaction = core::mem::zeroed();
action.sa_sigaction = faulthandler_user_signal as *const () as libc::sighandler_t;
action.sa_flags = libc::SA_NODEFER;
libc::sigaction(signum, &action, core::ptr::null_mut());
}
set_errno(save_errno2);
}
}
@@ -989,25 +1126,31 @@ mod decl {
// Get current handler to save as previous
let previous = if !user_signals::is_enabled(signum) {
// Install signal handler
let prev = unsafe {
libc::signal(
args.signum,
faulthandler_user_signal as *const () as libc::sighandler_t,
)
};
if prev == libc::SIG_ERR {
return Err(vm.new_os_error(format!(
"Failed to register signal handler for signal {}",
args.signum
)));
unsafe {
let mut action: libc::sigaction = core::mem::zeroed();
action.sa_sigaction = faulthandler_user_signal as *const () as libc::sighandler_t;
// SA_RESTART by default; SA_NODEFER only when chaining
// (faulthandler.c:860-864)
action.sa_flags = if args.chain {
libc::SA_NODEFER
} else {
libc::SA_RESTART
};
let mut prev: libc::sigaction = core::mem::zeroed();
if libc::sigaction(args.signum, &action, &mut prev) != 0 {
return Err(vm.new_os_error(format!(
"Failed to register signal handler for signal {}",
args.signum
)));
}
prev
}
prev
} else {
// Already registered, keep previous handler
user_signals::get_user_signal(signum)
.map(|u| u.previous)
.unwrap_or(libc::SIG_DFL)
.unwrap_or(unsafe { core::mem::zeroed() })
};
user_signals::set_user_signal(
@@ -1032,7 +1175,7 @@ mod decl {
if let Some(old) = user_signals::clear_user_signal(signum as usize) {
// Restore previous handler
unsafe {
libc::signal(signum, old.previous);
libc::sigaction(signum, &old.previous, core::ptr::null_mut());
}
Ok(true)
} else {
@@ -1043,14 +1186,15 @@ mod decl {
// Test functions for faulthandler testing
#[pyfunction]
fn _read_null() {
// This function intentionally causes a segmentation fault by reading from NULL
// Used for testing faulthandler
fn _read_null(_vm: &VirtualMachine) {
#[cfg(not(target_arch = "wasm32"))]
unsafe {
{
suppress_crash_report();
let ptr: *const i32 = core::ptr::null();
core::ptr::read_volatile(ptr);
unsafe {
let ptr: *const i32 = core::ptr::null();
core::ptr::read_volatile(ptr);
}
}
}
@@ -1062,39 +1206,28 @@ mod decl {
}
#[pyfunction]
fn _sigsegv(_args: SigsegvArgs) {
// Raise SIGSEGV signal
fn _sigsegv(_args: SigsegvArgs, _vm: &VirtualMachine) {
#[cfg(not(target_arch = "wasm32"))]
{
suppress_crash_report();
// Reset SIGSEGV to default behavior before raising
// This ensures the process will actually crash
// Write to NULL pointer to trigger a real hardware SIGSEGV,
// matching CPython's *((volatile int *)NULL) = 0;
// Using raise(SIGSEGV) doesn't work reliably because Rust's runtime
// installs its own signal handler that may swallow software signals.
unsafe {
libc::signal(libc::SIGSEGV, libc::SIG_DFL);
}
#[cfg(windows)]
{
// On Windows, we need to raise SIGSEGV multiple times
loop {
unsafe {
libc::raise(libc::SIGSEGV);
}
}
}
#[cfg(not(windows))]
unsafe {
libc::raise(libc::SIGSEGV);
let ptr: *mut i32 = core::ptr::null_mut();
core::ptr::write_volatile(ptr, 0);
}
}
}
#[pyfunction]
fn _sigabrt() {
fn _sigabrt(_vm: &VirtualMachine) {
#[cfg(not(target_arch = "wasm32"))]
{
suppress_crash_report();
unsafe {
libc::abort();
}
@@ -1102,17 +1235,11 @@ mod decl {
}
#[pyfunction]
fn _sigfpe() {
fn _sigfpe(_vm: &VirtualMachine) {
#[cfg(not(target_arch = "wasm32"))]
{
suppress_crash_report();
// Reset SIGFPE to default behavior before raising
unsafe {
libc::signal(libc::SIGFPE, libc::SIG_DFL);
}
// Raise SIGFPE
unsafe {
libc::raise(libc::SIGFPE);
}
@@ -1196,7 +1323,7 @@ mod decl {
#[cfg(windows)]
#[pyfunction]
fn _raise_exception(args: RaiseExceptionArgs) {
fn _raise_exception(args: RaiseExceptionArgs, _vm: &VirtualMachine) {
use windows_sys::Win32::System::Diagnostics::Debug::RaiseException;
suppress_crash_report();
+10 -1
View File
@@ -25,8 +25,8 @@ use crate::{
};
use alloc::fmt;
use core::iter::zip;
#[cfg(feature = "threading")]
use core::sync::atomic;
use core::sync::atomic::AtomicPtr;
use indexmap::IndexMap;
use itertools::Itertools;
@@ -90,6 +90,9 @@ pub struct Frame {
/// Borrowed reference (not ref-counted) to avoid Generator↔Frame cycle.
/// Cleared by the generator's Drop impl.
pub generator: PyAtomicBorrow,
/// Previous frame in the call chain for signal-safe traceback walking.
/// Mirrors `_PyInterpreterFrame.previous`.
pub(crate) previous: AtomicPtr<Frame>,
}
impl PyPayload for Frame {
@@ -179,6 +182,7 @@ impl Frame {
trace_opcodes: PyMutex::new(false),
temporary_refs: PyMutex::new(vec![]),
generator: PyAtomicBorrow::new(),
previous: AtomicPtr::new(core::ptr::null_mut()),
}
}
@@ -197,6 +201,11 @@ impl Frame {
self.code.locations[self.lasti() as usize - 1].0
}
/// Get the previous frame pointer for signal-safe traceback walking.
pub fn previous_frame(&self) -> *const Frame {
self.previous.load(atomic::Ordering::Relaxed)
}
pub fn lasti(&self) -> u32 {
#[cfg(feature = "threading")]
{
+22
View File
@@ -2,6 +2,7 @@
use crate::{PyResult, VirtualMachine};
use alloc::fmt;
use core::sync::atomic::{AtomicBool, Ordering};
use std::cell::Cell;
use std::sync::mpsc;
pub(crate) const NSIG: usize = 64;
@@ -11,6 +12,20 @@ static ANY_TRIGGERED: AtomicBool = AtomicBool::new(false);
const ATOMIC_FALSE: AtomicBool = AtomicBool::new(false);
pub(crate) static TRIGGERS: [AtomicBool; NSIG] = [ATOMIC_FALSE; NSIG];
thread_local! {
/// Prevent recursive signal handler invocation. When a Python signal
/// handler is running, new signals are deferred until it completes.
static IN_SIGNAL_HANDLER: Cell<bool> = const { Cell::new(false) };
}
struct SignalHandlerGuard;
impl Drop for SignalHandlerGuard {
fn drop(&mut self) {
IN_SIGNAL_HANDLER.with(|h| h.set(false));
}
}
#[cfg_attr(feature = "flame-it", flame)]
#[inline(always)]
pub fn check_signals(vm: &VirtualMachine) -> PyResult<()> {
@@ -27,6 +42,13 @@ pub fn check_signals(vm: &VirtualMachine) -> PyResult<()> {
#[inline(never)]
#[cold]
fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> {
if IN_SIGNAL_HANDLER.with(|h| h.replace(true)) {
// Already inside a signal handler — defer pending signals
set_triggered();
return Ok(());
}
let _guard = SignalHandlerGuard;
// unwrap should never fail since we check above
let signal_handlers = vm.signal_handlers.as_ref().unwrap().borrow();
for (signum, trigger) in TRIGGERS.iter().enumerate().skip(1) {
+4 -3
View File
@@ -1,9 +1,10 @@
//! Implementation of the _thread module
#[cfg(unix)]
pub(crate) use _thread::after_fork_child;
pub use _thread::get_ident;
#[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
pub(crate) use _thread::{
CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, get_all_current_frames, get_ident,
CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, get_all_current_frames,
init_main_thread_ident, module_def,
};
@@ -873,12 +874,12 @@ pub(crate) mod _thread {
// Re-export type from vm::thread for PyGlobalState
pub use crate::vm::thread::CurrentFrameSlot;
/// Get all threads' current frames. Used by sys._current_frames().
/// Get all threads' current (top) frames. Used by sys._current_frames().
pub fn get_all_current_frames(vm: &VirtualMachine) -> Vec<(u64, FrameRef)> {
let registry = vm.state.thread_frames.lock();
registry
.iter()
.filter_map(|(id, slot)| slot.lock().clone().map(|f| (*id, f)))
.filter_map(|(id, slot)| slot.lock().last().cloned().map(|f| (*id, f)))
.collect()
}
+13 -4
View File
@@ -939,9 +939,16 @@ impl VirtualMachine {
) -> PyResult<R> {
self.with_recursion("", || {
self.frames.borrow_mut().push(frame.clone());
// Update the current frame slot for sys._current_frames()
// Update the shared frame stack for sys._current_frames() and faulthandler
#[cfg(feature = "threading")]
crate::vm::thread::update_current_frame(Some(frame.clone()));
crate::vm::thread::push_thread_frame(frame.clone());
// Link frame into the signal-safe frame chain (previous pointer)
let frame_ptr: *const Frame = &**frame;
let old_frame = crate::vm::thread::set_current_frame(frame_ptr);
frame.previous.store(
old_frame as *mut Frame,
core::sync::atomic::Ordering::Relaxed,
);
// Push a new exception context for frame isolation
// Each frame starts with no active exception (None)
// This prevents exceptions from leaking between function calls
@@ -949,11 +956,13 @@ impl VirtualMachine {
let result = f(frame);
// Pop the exception context - restores caller's exception state
self.pop_exception();
// Restore previous frame as current (unlink from chain)
crate::vm::thread::set_current_frame(old_frame);
// defer dec frame
let _popped = self.frames.borrow_mut().pop();
// Update the frame slot to the new top frame (or None if empty)
// Pop from shared frame stack
#[cfg(feature = "threading")]
crate::vm::thread::update_current_frame(self.frames.borrow().last().cloned());
crate::vm::thread::pop_thread_frame();
result
})
}
+61 -15
View File
@@ -1,9 +1,11 @@
use crate::frame::Frame;
#[cfg(feature = "threading")]
use crate::frame::FrameRef;
use crate::{AsObject, PyObject, VirtualMachine};
use core::{
cell::{Cell, RefCell},
ptr::NonNull,
sync::atomic::{AtomicPtr, Ordering},
};
use itertools::Itertools;
#[cfg(feature = "threading")]
@@ -11,8 +13,10 @@ use std::sync::Arc;
use std::thread_local;
/// Type for current frame slot - shared between threads for sys._current_frames()
/// Stores the full frame stack so faulthandler can dump complete tracebacks
/// for all threads.
#[cfg(feature = "threading")]
pub type CurrentFrameSlot = Arc<parking_lot::Mutex<Option<FrameRef>>>;
pub type CurrentFrameSlot = Arc<parking_lot::Mutex<Vec<FrameRef>>>;
thread_local! {
pub(super) static VM_STACK: RefCell<Vec<NonNull<VirtualMachine>>> = Vec::with_capacity(1).into();
@@ -22,6 +26,14 @@ thread_local! {
/// Current thread's frame slot for sys._current_frames()
#[cfg(feature = "threading")]
static CURRENT_FRAME_SLOT: RefCell<Option<CurrentFrameSlot>> = const { RefCell::new(None) };
/// Current top frame for signal-safe traceback walking.
/// Mirrors `PyThreadState.current_frame`. Read by faulthandler's signal
/// handler to dump tracebacks without accessing RefCell or locks.
/// Uses AtomicPtr for async-signal-safety (signal handlers may read this
/// while the owning thread is writing).
pub(crate) static CURRENT_FRAME: AtomicPtr<Frame> =
const { AtomicPtr::new(core::ptr::null_mut()) };
}
scoped_tls::scoped_thread_local!(static VM_CURRENT: VirtualMachine);
@@ -53,7 +65,7 @@ fn init_frame_slot_if_needed(vm: &VirtualMachine) {
CURRENT_FRAME_SLOT.with(|slot| {
if slot.borrow().is_none() {
let thread_id = crate::stdlib::thread::get_ident();
let new_slot = Arc::new(parking_lot::Mutex::new(None));
let new_slot = Arc::new(parking_lot::Mutex::new(Vec::new()));
vm.state
.thread_frames
.lock()
@@ -63,17 +75,40 @@ fn init_frame_slot_if_needed(vm: &VirtualMachine) {
});
}
/// Update the current thread's frame. Called when frames are pushed/popped.
/// This is a hot path - uses only thread-local storage, no locks.
/// Push a frame onto the current thread's shared frame stack.
/// Called when a new frame is entered.
#[cfg(feature = "threading")]
pub fn update_current_frame(frame: Option<FrameRef>) {
pub fn push_thread_frame(frame: FrameRef) {
CURRENT_FRAME_SLOT.with(|slot| {
if let Some(s) = slot.borrow().as_ref() {
*s.lock() = frame;
s.lock().push(frame);
}
});
}
/// Pop a frame from the current thread's shared frame stack.
/// Called when a frame is exited.
#[cfg(feature = "threading")]
pub fn pop_thread_frame() {
CURRENT_FRAME_SLOT.with(|slot| {
if let Some(s) = slot.borrow().as_ref() {
s.lock().pop();
}
});
}
/// Set the current thread's top frame pointer for signal-safe traceback walking.
/// Returns the previous frame pointer so it can be restored on pop.
pub fn set_current_frame(frame: *const Frame) -> *const Frame {
CURRENT_FRAME.with(|c| c.swap(frame as *mut Frame, Ordering::Relaxed) as *const Frame)
}
/// Get the current thread's top frame pointer.
/// Used by faulthandler's signal handler to start traceback walking.
pub fn get_current_frame() -> *const Frame {
CURRENT_FRAME.with(|c| c.load(Ordering::Relaxed) as *const Frame)
}
/// Cleanup frame tracking for the current thread. Called at thread exit.
#[cfg(feature = "threading")]
pub fn cleanup_current_thread_frames(vm: &VirtualMachine) {
@@ -85,20 +120,31 @@ pub fn cleanup_current_thread_frames(vm: &VirtualMachine) {
}
/// Reinitialize frame slot after fork. Called in child process.
/// Creates a fresh slot and registers it for the current thread.
/// Creates a fresh slot and registers it for the current thread,
/// preserving the current thread's frames from `vm.frames`.
#[cfg(feature = "threading")]
pub fn reinit_frame_slot_after_fork(vm: &VirtualMachine) {
let current_ident = crate::stdlib::thread::get_ident();
let new_slot = Arc::new(parking_lot::Mutex::new(None));
// Preserve the current thread's frames across fork
let current_frames: Vec<FrameRef> = vm.frames.borrow().clone();
let new_slot = Arc::new(parking_lot::Mutex::new(current_frames));
// Try to update the global registry. If we can't get the lock
// (parent thread might have been holding it during fork), skip.
if let Some(mut registry) = vm.state.thread_frames.try_lock() {
registry.clear();
registry.insert(current_ident, new_slot.clone());
}
// After fork, only the current thread exists. If the lock was held by
// another thread during fork, force unlock it.
let mut registry = match vm.state.thread_frames.try_lock() {
Some(guard) => guard,
None => {
// SAFETY: After fork in child process, only the current thread
// exists. The lock holder no longer exists.
unsafe { vm.state.thread_frames.force_unlock() };
vm.state.thread_frames.lock()
}
};
registry.clear();
registry.insert(current_ident, new_slot.clone());
drop(registry);
// Always update thread-local to point to the new slot
// Update thread-local to point to the new slot
CURRENT_FRAME_SLOT.with(|s| {
*s.borrow_mut() = Some(new_slot);
});