@@ -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 ) ]
113113pub 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.
125125unsafe impl Send for FramePtr { }
126126
127- #[ derive( Debug , Default ) ]
127+ #[ derive( Debug ) ]
128128struct 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