Skip to content

Commit a15185e

Browse files
committed
Fix exception state model and finally handler cleanup
- ExceptionStack: init with base slot instead of empty vec - with_frame_impl: save/restore exc_info value instead of push/pop slot, so callees see caller's handled exception - Remove unused with_frame_exc - resume_gen_frame: use push_exception/pop_exception methods - codegen: move RERAISE inside cleanup handler's exception table range in finally blocks (both try-finally and try-except-finally), so POP_EXCEPT runs before re-raising
1 parent 64cf654 commit a15185e

File tree

3 files changed

+62
-56
lines changed

3 files changed

+62
-56
lines changed

crates/codegen/src/compile.rs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3166,16 +3166,18 @@ impl Compiler {
31663166
}
31673167
self.compile_statements(finalbody)?;
31683168

3169-
// Pop FinallyEnd fblock BEFORE emitting RERAISE
3170-
// This ensures RERAISE routes to outer exception handler, not cleanup block
3171-
// Cleanup block is only for new exceptions raised during finally body execution
3169+
// RERAISE must be inside the cleanup handler's exception table
3170+
// range. When RERAISE re-raises the exception, the cleanup
3171+
// handler (COPY 3, POP_EXCEPT, RERAISE 1) runs POP_EXCEPT to
3172+
// restore exc_info before the exception reaches the outer handler.
3173+
emit!(self, Instruction::Reraise { depth: 0 });
3174+
3175+
// PopBlock after RERAISE (dead code, but marks the exception
3176+
// table range end so the cleanup covers RERAISE).
31723177
if finally_cleanup_block.is_some() {
31733178
emit!(self, PseudoInstruction::PopBlock);
31743179
self.pop_fblock(FBlockType::FinallyEnd);
31753180
}
3176-
3177-
// CPython re-raises first and lets the cleanup block restore prev_exc.
3178-
emit!(self, Instruction::Reraise { depth: 0 });
31793181
}
31803182

31813183
if let Some(cleanup) = finally_cleanup_block {
@@ -3432,16 +3434,18 @@ impl Compiler {
34323434
// Run finally body
34333435
self.compile_statements(finalbody)?;
34343436

3435-
// Pop FinallyEnd fblock BEFORE emitting RERAISE
3436-
// This ensures RERAISE routes to outer exception handler, not cleanup block
3437-
// Cleanup block is only for new exceptions raised during finally body execution
3437+
// RERAISE must be inside the cleanup handler's exception table
3438+
// range. The cleanup handler (COPY 3, POP_EXCEPT, RERAISE 1)
3439+
// runs POP_EXCEPT to restore exc_info before re-raising to
3440+
// the outer handler.
3441+
emit!(self, Instruction::Reraise { depth: 0 });
3442+
3443+
// PopBlock after RERAISE (dead code, but marks the exception
3444+
// table range end so the cleanup covers RERAISE).
34383445
if finally_cleanup_block.is_some() {
34393446
emit!(self, PseudoInstruction::PopBlock);
34403447
self.pop_fblock(FBlockType::FinallyEnd);
34413448
}
3442-
3443-
// CPython re-raises first and lets the cleanup block restore prev_exc.
3444-
emit!(self, Instruction::Reraise { depth: 0 });
34453449
}
34463450

34473451
// finally cleanup block
@@ -3473,11 +3477,7 @@ impl Compiler {
34733477
let handler_block = self.new_block();
34743478
let cleanup_block = self.new_block();
34753479
let end_block = self.new_block();
3476-
let orelse_block = if orelse.is_empty() {
3477-
end_block
3478-
} else {
3479-
self.new_block()
3480-
};
3480+
let orelse_block = self.new_block();
34813481

34823482
emit!(self, Instruction::Nop);
34833483
emit!(
@@ -3624,16 +3624,16 @@ impl Compiler {
36243624
emit!(self, Instruction::Reraise { depth: 1 });
36253625
self.set_no_location();
36263626

3627+
self.switch_to_block(orelse_block);
3628+
self.set_no_location();
36273629
if !orelse.is_empty() {
3628-
self.switch_to_block(orelse_block);
3629-
self.set_no_location();
36303630
self.compile_statements(orelse)?;
3631-
emit!(
3632-
self,
3633-
PseudoInstruction::JumpNoInterrupt { delta: end_block }
3634-
);
3635-
self.set_no_location();
36363631
}
3632+
emit!(
3633+
self,
3634+
PseudoInstruction::JumpNoInterrupt { delta: end_block }
3635+
);
3636+
self.set_no_location();
36373637

36383638
self.switch_to_block(end_block);
36393639
Ok(())

crates/vm/src/frame.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6769,7 +6769,6 @@ impl ExecutingFrame<'_> {
67696769
}
67706770
bytecode::RaiseKind::BareRaise => {
67716771
// RAISE_VARARGS 0: bare `raise` gets exception from VM state
6772-
// This is the current exception set by PUSH_EXC_INFO
67736772
vm.topmost_exception()
67746773
.ok_or_else(|| vm.new_runtime_error("No active exception to reraise"))?
67756774
}

crates/vm/src/vm/mod.rs

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub struct VirtualMachine {
108108
}
109109

110110
/// Non-owning frame pointer for the frames stack.
111-
/// The pointed-to frame is kept alive by the caller of with_frame_exc/resume_gen_frame.
111+
/// The pointed-to frame is kept alive by the caller of with_frame/resume_gen_frame.
112112
#[derive(Copy, Clone)]
113113
pub struct FramePtr(NonNull<Py<Frame>>);
114114

@@ -124,11 +124,23 @@ impl FramePtr {
124124
// FrameRef is alive on the call stack. The Vec is always empty when the VM moves between threads.
125125
unsafe impl Send for FramePtr {}
126126

127-
#[derive(Debug, Default)]
127+
#[derive(Debug)]
128128
struct ExceptionStack {
129+
/// Linked list of handled-exception slots (`_PyErr_StackItem` chain).
130+
/// Bottom element is the thread's base slot; generator/coroutine resume
131+
/// pushes an additional slot. Normal frame calls do **not** push/pop.
129132
stack: Vec<Option<PyBaseExceptionRef>>,
130133
}
131134

135+
impl Default for ExceptionStack {
136+
fn default() -> Self {
137+
// Thread's base `_PyErr_StackItem` – always present.
138+
Self {
139+
stack: vec![None],
140+
}
141+
}
142+
}
143+
132144
/// Stop-the-world state for fork safety. Before `fork()`, the requester
133145
/// stops all other Python threads so they are not holding internal locks.
134146
#[cfg(all(unix, feature = "threading"))]
@@ -1554,31 +1566,20 @@ impl VirtualMachine {
15541566
frame: FrameRef,
15551567
f: F,
15561568
) -> PyResult<R> {
1557-
self.with_frame_impl(frame, None, true, f)
1558-
}
1559-
1560-
/// Like `with_frame` but allows specifying the initial exception state.
1561-
pub fn with_frame_exc<R, F: FnOnce(FrameRef) -> PyResult<R>>(
1562-
&self,
1563-
frame: FrameRef,
1564-
exc: Option<PyBaseExceptionRef>,
1565-
f: F,
1566-
) -> PyResult<R> {
1567-
self.with_frame_impl(frame, exc, true, f)
1569+
self.with_frame_impl(frame, true, f)
15681570
}
15691571

15701572
pub(crate) fn with_frame_untraced<R, F: FnOnce(FrameRef) -> PyResult<R>>(
15711573
&self,
15721574
frame: FrameRef,
15731575
f: F,
15741576
) -> PyResult<R> {
1575-
self.with_frame_impl(frame, None, false, f)
1577+
self.with_frame_impl(frame, false, f)
15761578
}
15771579

15781580
fn with_frame_impl<R, F: FnOnce(FrameRef) -> PyResult<R>>(
15791581
&self,
15801582
frame: FrameRef,
1581-
exc: Option<PyBaseExceptionRef>,
15821583
traced: bool,
15831584
f: F,
15841585
) -> PyResult<R> {
@@ -1597,19 +1598,22 @@ impl VirtualMachine {
15971598
old_frame as *mut Frame,
15981599
core::sync::atomic::Ordering::Relaxed,
15991600
);
1600-
// Push exception context for frame isolation.
1601-
// For normal calls: None (clean slate).
1602-
// For generators: the saved exception from last yield.
1603-
self.push_exception(exc);
1601+
// Normal frame calls share the caller's exc_info slot so that
1602+
// callees can see the caller's handled exception via sys.exc_info().
1603+
// Save the current value to restore on exit — this prevents
1604+
// exc_info pollution from frames with unbalanced
1605+
// PUSH_EXC_INFO/POP_EXCEPT (e.g., exception escaping an except block
1606+
// whose cleanup entry is missing from the exception table).
1607+
let saved_exc = self.current_exception();
16041608
let old_owner = frame.owner.swap(
16051609
crate::frame::FrameOwner::Thread as i8,
16061610
core::sync::atomic::Ordering::AcqRel,
16071611
);
16081612

1609-
// Ensure cleanup on panic: restore owner, pop exception, frame chain, and frames Vec.
1613+
// Ensure cleanup on panic: restore owner, exc_info, frame chain, and frames Vec.
16101614
scopeguard::defer! {
16111615
frame.owner.store(old_owner, core::sync::atomic::Ordering::Release);
1612-
self.pop_exception();
1616+
self.set_exception(saved_exc);
16131617
crate::vm::thread::set_current_frame(old_frame);
16141618
self.frames.borrow_mut().pop();
16151619
#[cfg(feature = "threading")]
@@ -1624,9 +1628,9 @@ impl VirtualMachine {
16241628
})
16251629
}
16261630

1627-
/// Lightweight frame execution for generator/coroutine resume.
1628-
/// Pushes to the thread frame stack and fires trace/profile events,
1629-
/// but skips the thread exception update for performance.
1631+
/// Frame execution for generator/coroutine resume.
1632+
/// Pushes a new exc_info slot (gi_exc_state) onto the chain,
1633+
/// linking the generator's saved handled-exception.
16301634
pub fn resume_gen_frame<R, F: FnOnce(&Py<Frame>) -> PyResult<R>>(
16311635
&self,
16321636
frame: &FrameRef,
@@ -1649,20 +1653,20 @@ impl VirtualMachine {
16491653
old_frame as *mut Frame,
16501654
core::sync::atomic::Ordering::Relaxed,
16511655
);
1652-
// Inline exception push without thread exception update
1653-
self.exceptions.borrow_mut().stack.push(exc);
1656+
// Push generator's exc_info slot onto the chain
1657+
// (gi_exc_state.previous_item = tstate->exc_info;
1658+
// tstate->exc_info = &gi_exc_state;)
1659+
self.push_exception(exc);
16541660
let old_owner = frame.owner.swap(
16551661
crate::frame::FrameOwner::Thread as i8,
16561662
core::sync::atomic::Ordering::AcqRel,
16571663
);
16581664

1659-
// Ensure cleanup on panic: restore owner, pop exception, frame chain, frames Vec,
1660-
// and recursion depth.
1665+
// Ensure cleanup on panic: restore owner, pop exc_info slot, frame chain,
1666+
// frames Vec, and recursion depth.
16611667
scopeguard::defer! {
16621668
frame.owner.store(old_owner, core::sync::atomic::Ordering::Release);
1663-
self.exceptions.borrow_mut().stack
1664-
.pop()
1665-
.expect("pop_exception() without nested exc stack");
1669+
self.pop_exception();
16661670
crate::vm::thread::set_current_frame(old_frame);
16671671
self.frames.borrow_mut().pop();
16681672
#[cfg(feature = "threading")]
@@ -2037,12 +2041,14 @@ impl VirtualMachine {
20372041
}
20382042
}
20392043

2044+
/// Push a new exc_info slot (for generator/coroutine resume).
20402045
pub(crate) fn push_exception(&self, exc: Option<PyBaseExceptionRef>) {
20412046
self.exceptions.borrow_mut().stack.push(exc);
20422047
#[cfg(feature = "threading")]
20432048
thread::update_thread_exception(self.topmost_exception());
20442049
}
20452050

2051+
/// Pop the topmost exc_info slot (generator/coroutine yield/return).
20462052
pub(crate) fn pop_exception(&self) -> Option<PyBaseExceptionRef> {
20472053
let exc = self
20482054
.exceptions
@@ -2059,6 +2065,7 @@ impl VirtualMachine {
20592065
self.exceptions.borrow().stack.last().cloned().flatten()
20602066
}
20612067

2068+
/// Set the current exc_info slot value (PUSH_EXC_INFO / POP_EXCEPT).
20622069
pub(crate) fn set_exception(&self, exc: Option<PyBaseExceptionRef>) {
20632070
// don't be holding the RefCell guard while __del__ is called
20642071
let mut excs = self.exceptions.borrow_mut();

0 commit comments

Comments
 (0)