From 093a5638977c223a2f23c562778e8de8529f40f4 Mon Sep 17 00:00:00 2001 From: Han <180024497+LH-and-FPGA@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:10:15 +0100 Subject: [PATCH] [3.14] gh-130279: Fix missing exception handlers for jumps in loops (GH-159460) (cherry picked from commit 036d309cf542986a39eeba5ca805b2ec7619731d) --- Lib/test/test_compile.py | 22 +++++++++++++ Lib/test/test_dis.py | 31 +++++++++---------- Lib/test/test_monitoring.py | 31 +++++++++++++++++++ Misc/ACKS | 1 + ...-10-15-12-36-09.gh-issue-130279.N9N-sw.rst | 2 ++ Python/flowgraph.c | 24 ++++++++------ 6 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 91d1365a0a21ba4..2333015dce25aaf 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1374,6 +1374,28 @@ def f(): self.check_line_numbers(f.__code__, 'JUMP_BACKWARD') + def test_backward_jump_conditional_in_loop_in_try(self): + # gh-130279: The jumps in a loop body, including the backward jump + # for the untaken branch of a conditional at the end of the body, + # must be covered by the exception handler of the enclosing try. + def f(items): + try: + for x in items: + if x < 0: + raise ValueError(x) + except KeyboardInterrupt: + return 'handled' + + bytecode = dis.Bytecode(f) + jumps = [instr for instr in bytecode + if instr.opcode in dis.hasjump] + self.assertTrue(jumps) + for instr in jumps: + with self.subTest(instr=instr): + self.assertTrue( + any(entry.start <= instr.offset < entry.end + for entry in bytecode.exception_entries)) + def test_big_dict_literal(self): # The compiler has a flushing point in "compiler_dict" that calls compiles # a portion of the dictionary literal when the loop that iterates over the items diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 48df7e530de2c1e..3f6931425a31c8e 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -659,11 +659,11 @@ async def _asyncwith(c): L20: CLEANUP_THROW L21: END_SEND TO_BOOL - POP_JUMP_IF_TRUE 2 (to L24) - L22: NOT_TAKEN - L23: RERAISE 2 - L24: POP_TOP - L25: POP_EXCEPT + POP_JUMP_IF_TRUE 2 (to L22) + NOT_TAKEN + RERAISE 2 + L22: POP_TOP + L23: POP_EXCEPT POP_TOP POP_TOP POP_TOP @@ -673,25 +673,24 @@ async def _asyncwith(c): LOAD_CONST 0 (None) RETURN_VALUE - -- L26: COPY 3 + -- L24: COPY 3 POP_EXCEPT RERAISE 1 - L27: CALL_INTRINSIC_1 3 (INTRINSIC_STOPITERATION_ERROR) + L25: CALL_INTRINSIC_1 3 (INTRINSIC_STOPITERATION_ERROR) RERAISE 1 ExceptionTable: - L1 to L3 -> L27 [0] lasti + L1 to L3 -> L25 [0] lasti L3 to L4 -> L12 [4] - L4 to L6 -> L27 [0] lasti + L4 to L6 -> L25 [0] lasti L6 to L7 -> L16 [2] lasti - L7 to L9 -> L27 [0] lasti + L7 to L9 -> L25 [0] lasti L9 to L10 -> L14 [2] - L10 to L13 -> L27 [0] lasti - L14 to L15 -> L27 [0] lasti - L16 to L18 -> L26 [4] lasti + L10 to L13 -> L25 [0] lasti + L14 to L15 -> L25 [0] lasti + L16 to L18 -> L24 [4] lasti L18 to L19 -> L20 [7] - L19 to L22 -> L26 [4] lasti - L23 to L25 -> L26 [4] lasti - L25 to L27 -> L27 [0] lasti + L19 to L23 -> L24 [4] lasti + L23 to L25 -> L25 [0] lasti """ % (_asyncwith.__code__.co_firstlineno, _asyncwith.__code__.co_firstlineno + 1, _asyncwith.__code__.co_firstlineno + 2, diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index ffa1fd957eb4561..bf4597d18f99081 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -2200,6 +2200,37 @@ def callback(code, instruction_offset): sys.monitoring.restart_events() sys.monitoring.set_events(0, 0) + def test_130279(self): + # gh-130279 + # An exception raised by the BRANCH_LEFT callback of a forward + # branch must be caught by the enclosing try + class Boom(Exception): + pass + + raised = False + + def callback(code, src, dst): + nonlocal raised + if not raised: + raised = True + raise Boom + + def foo(x): + try: + if x: + pass + except Boom: + return "caught" + return "not raised" + + sys.monitoring.register_callback(TEST_TOOL, E.BRANCH_LEFT, callback) + sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.BRANCH_LEFT) + try: + self.assertEqual(foo(True), "caught") + finally: + sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0) + sys.monitoring.register_callback(TEST_TOOL, E.BRANCH_LEFT, None) + class TestOptimizer(MonitoringTestBase, unittest.TestCase): diff --git a/Misc/ACKS b/Misc/ACKS index 2a8246ce71f6ebe..bc052db5867f25c 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1127,6 +1127,7 @@ Ivan Levkivskyi Ben Lewis William Lewis Akira Li +Han Li Jiahao Li Robert Li Xuanji Li diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst new file mode 100644 index 000000000000000..996eb7020f53449 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst @@ -0,0 +1,2 @@ +Fix bug where compiler did not set the right exception handler for some jump +instructions. diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 4aa72cbf3b99b71..39de8473b809878 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -186,7 +186,8 @@ cfg_builder_new_block(cfg_builder *g) } static int -basicblock_addop(basicblock *b, int opcode, int oparg, location loc) +basicblock_addop(basicblock *b, int opcode, int oparg, location loc, + basicblock *except) { assert(IS_WITHIN_OPCODE_RANGE(opcode)); assert(!IS_ASSEMBLER_OPCODE(opcode)); @@ -202,12 +203,14 @@ basicblock_addop(basicblock *b, int opcode, int oparg, location loc) i->i_oparg = oparg; i->i_target = NULL; i->i_loc = loc; + i->i_except = except; return SUCCESS; } static int -basicblock_add_jump(basicblock *b, int opcode, basicblock *target, location loc) +basicblock_add_jump(basicblock *b, int opcode, basicblock *target, location loc, + basicblock *except) { cfg_instr *last = basicblock_last_instr(b); if (last && is_jump(last)) { @@ -215,7 +218,7 @@ basicblock_add_jump(basicblock *b, int opcode, basicblock *target, location loc) } RETURN_IF_ERROR( - basicblock_addop(b, opcode, target->b_label.id, loc)); + basicblock_addop(b, opcode, target->b_label.id, loc, except)); last = basicblock_last_instr(b); assert(last && last->i_opcode == opcode); last->i_target = target; @@ -481,7 +484,7 @@ int _PyCfgBuilder_Addop(cfg_builder *g, int opcode, int oparg, location loc) { RETURN_IF_ERROR(cfg_builder_maybe_start_new_block(g)); - return basicblock_addop(g->g_curblock, opcode, oparg, loc); + return basicblock_addop(g->g_curblock, opcode, oparg, loc, NULL); } @@ -542,7 +545,7 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) { bool is_forward = last->i_target->b_visited == 0; if (is_forward) { RETURN_IF_ERROR( - basicblock_addop(b, NOT_TAKEN, 0, last->i_loc)); + basicblock_addop(b, NOT_TAKEN, 0, last->i_loc, last->i_except)); return SUCCESS; } @@ -571,9 +574,11 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) { return ERROR; } RETURN_IF_ERROR( - basicblock_addop(backwards_jump, NOT_TAKEN, 0, last->i_loc)); + basicblock_addop(backwards_jump, NOT_TAKEN, 0, last->i_loc, + last->i_except)); RETURN_IF_ERROR( - basicblock_add_jump(backwards_jump, JUMP, target, last->i_loc)); + basicblock_add_jump(backwards_jump, JUMP, target, last->i_loc, + last->i_except)); backwards_jump->b_startdepth = target->b_startdepth; last->i_opcode = reversed_opcode; last->i_target = b->b_next; @@ -1275,7 +1280,8 @@ jump_thread(basicblock *bb, cfg_instr *inst, cfg_instr *target, int opcode) RETURN_IF_ERROR( basicblock_add_jump( - bb, opcode, target->i_target, target->i_loc)); + bb, opcode, target->i_target, target->i_loc, + target->i_except)); return true; } @@ -3453,7 +3459,7 @@ push_cold_blocks_to_end(cfg_builder *g) { b->b_next->b_label.id = next_lbl++; } basicblock_addop(explicit_jump, JUMP_NO_INTERRUPT, b->b_next->b_label.id, - NO_LOCATION); + NO_LOCATION, NULL); explicit_jump->b_cold = 1; explicit_jump->b_next = b->b_next; explicit_jump->b_predecessors = 1;