Skip to content

Commit e1fb03f

Browse files
committed
py: Fix VM crash with unwinding jump out of a finally block.
This patch fixes a bug in the VM when breaking within a try-finally. The bug has to do with executing a break within the finally block of a try-finally statement. For example: def f(): for x in (1,): print('a', x) try: raise Exception finally: print(1) break print('b', x) f() Currently in uPy the above code will print: a 1 1 1 segmentation fault (core dumped) micropython Not only is there a seg fault, but the "1" in the finally block is printed twice. This is because when the VM executes a finally block it doesn't really know if that block was executed due to a fall-through of the try (no exception raised), or because an exception is active. In particular, for nested finallys the VM has no idea which of the nested ones have active exceptions and which are just fall-throughs. So when a break (or continue) is executed it tries to unwind all of the finallys, when in fact only some may be active. It's questionable whether break (or return or continue) should be allowed within a finally block, because they implicitly swallow any active exception, but nevertheless it's allowed by CPython (although almost never used in the standard library). And uPy should at least not crash in such a case. The solution here relies on the fact that exception and finally handlers always appear in the bytecode after the try body. Note: there was a similar bug with a return in a finally block, but that was previously fixed in b735208
1 parent b5f33ac commit e1fb03f

File tree

6 files changed

+114
-12
lines changed

6 files changed

+114
-12
lines changed

py/compile.c

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,7 +1599,6 @@ STATIC void compile_try_except(compiler_t *comp, mp_parse_node_t pn_body, int n_
15991599
}
16001600
compile_node(comp, pns_except->nodes[1]); // the <body>
16011601
if (qstr_exception_local != 0) {
1602-
EMIT(pop_block);
16031602
EMIT_ARG(load_const_tok, MP_TOKEN_KW_NONE);
16041603
EMIT_ARG(label_assign, l3);
16051604
EMIT_ARG(load_const_tok, MP_TOKEN_KW_NONE);
@@ -1635,7 +1634,6 @@ STATIC void compile_try_finally(compiler_t *comp, mp_parse_node_t pn_body, int n
16351634
} else {
16361635
compile_try_except(comp, pn_body, n_except, pn_except, pn_else);
16371636
}
1638-
EMIT(pop_block);
16391637
EMIT_ARG(load_const_tok, MP_TOKEN_KW_NONE);
16401638
EMIT_ARG(label_assign, l_finally_block);
16411639
compile_node(comp, pn_finally);
@@ -1811,8 +1809,7 @@ STATIC void compile_async_with_stmt_helper(compiler_t *comp, int n, mp_parse_nod
18111809
compile_async_with_stmt_helper(comp, n - 1, nodes + 1, body);
18121810
EMIT_ARG(adjust_stack_size, -3);
18131811

1814-
// Finish the "try" block
1815-
EMIT(pop_block);
1812+
// We have now finished the "try" block and fall through to the "finally"
18161813

18171814
// At this point, after the with body has executed, we have 3 cases:
18181815
// 1. no exception, we just fall through to this point; stack: (..., ctx_mgr)

py/emitbc.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,6 @@ void mp_emit_bc_setup_block(emit_t *emit, mp_uint_t label, int kind) {
738738
}
739739

740740
void mp_emit_bc_with_cleanup(emit_t *emit, mp_uint_t label) {
741-
mp_emit_bc_pop_block(emit);
742741
mp_emit_bc_load_const_tok(emit, MP_TOKEN_KW_NONE);
743742
mp_emit_bc_label_assign(emit, label);
744743
emit_bc_pre(emit, 2); // ensure we have enough stack space to call the __exit__ method

py/vm.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,6 @@ run_code_state: ;
633633
// replacing it with None, which signals END_FINALLY to just
634634
// execute the finally handler normally.
635635
SET_TOP(mp_const_none);
636-
assert(exc_sp >= exc_stack);
637-
POP_EXC_BLOCK();
638636
} else {
639637
// We need to re-raise the exception. We pop __exit__ handler
640638
// by copying the exception instance down to the new top-of-stack.
@@ -654,7 +652,7 @@ unwind_jump:;
654652
while ((unum & 0x7f) > 0) {
655653
unum -= 1;
656654
assert(exc_sp >= exc_stack);
657-
if (MP_TAGPTR_TAG1(exc_sp->val_sp)) {
655+
if (MP_TAGPTR_TAG1(exc_sp->val_sp) && exc_sp->handler > ip) {
658656
// Getting here the stack looks like:
659657
// (..., X, dest_ip)
660658
// where X is pointed to by exc_sp->val_sp and in the case
@@ -698,6 +696,8 @@ unwind_jump:;
698696
// if TOS is an integer, finishes coroutine and returns control to caller
699697
// if TOS is an exception, reraises the exception
700698
if (TOP() == mp_const_none) {
699+
assert(exc_sp >= exc_stack);
700+
POP_EXC_BLOCK();
701701
sp--;
702702
} else if (mp_obj_is_small_int(TOP())) {
703703
// We finished "finally" coroutine and now dispatch back
@@ -1063,7 +1063,7 @@ unwind_jump:;
10631063
unwind_return:
10641064
// Search for and execute finally handlers that aren't already active
10651065
while (exc_sp >= exc_stack) {
1066-
if (!currently_in_except_block && MP_TAGPTR_TAG1(exc_sp->val_sp)) {
1066+
if (!currently_in_except_block && MP_TAGPTR_TAG1(exc_sp->val_sp) && exc_sp->handler > ip) {
10671067
// Found a finally handler that isn't active.
10681068
// Getting here the stack looks like:
10691069
// (..., X, [iter0, iter1, ...,] ret_val)
@@ -1419,7 +1419,7 @@ unwind_jump:;
14191419
mp_obj_exception_add_traceback(MP_OBJ_FROM_PTR(nlr.ret_val), source_file, source_line, block_name);
14201420
}
14211421

1422-
while (currently_in_except_block) {
1422+
while (currently_in_except_block || (exc_sp >= exc_stack && exc_sp->handler <= code_state->ip)) {
14231423
// nested exception
14241424

14251425
assert(exc_sp >= exc_stack);

tests/basics/try_finally_break.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# test break within (nested) finally
2+
3+
# basic case with break in finally
4+
def f():
5+
for _ in range(2):
6+
print(1)
7+
try:
8+
pass
9+
finally:
10+
print(2)
11+
break
12+
print(3)
13+
print(4)
14+
print(5)
15+
f()
16+
17+
# where the finally swallows an exception
18+
def f():
19+
lst = [1, 2, 3]
20+
for x in lst:
21+
print('a', x)
22+
try:
23+
raise Exception
24+
finally:
25+
print(1)
26+
break
27+
print('b', x)
28+
f()
29+
30+
# basic nested finally with break in inner finally
31+
def f():
32+
for i in range(2):
33+
print('iter', i)
34+
try:
35+
raise TypeError
36+
finally:
37+
print(1)
38+
try:
39+
raise ValueError
40+
finally:
41+
break
42+
print(f())
43+
44+
# similar to above but more nesting
45+
def f():
46+
for i in range(2):
47+
try:
48+
raise ValueError
49+
finally:
50+
print(1)
51+
try:
52+
raise TypeError
53+
finally:
54+
print(2)
55+
try:
56+
pass
57+
finally:
58+
break
59+
print(f())
60+
61+
# lots of nesting
62+
def f():
63+
for i in range(2):
64+
try:
65+
raise ValueError
66+
finally:
67+
print(1)
68+
try:
69+
raise TypeError
70+
finally:
71+
print(2)
72+
try:
73+
raise Exception
74+
finally:
75+
break
76+
print(f())
77+
78+
# basic case combined with try-else
79+
def f(arg):
80+
for _ in range(2):
81+
print(1)
82+
try:
83+
if arg == 1:
84+
raise ValueError
85+
elif arg == 2:
86+
raise TypeError
87+
except ValueError:
88+
print(2)
89+
else:
90+
print(3)
91+
finally:
92+
print(4)
93+
break
94+
print(5)
95+
print(6)
96+
print(7)
97+
f(0) # no exception, else should execute
98+
f(1) # exception caught, else should be skipped
99+
f(2) # exception not caught, finally swallows exception, else should be skipped

tests/basics/try_return.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# test use of return with try-except
22

3+
def f():
4+
try:
5+
print(1)
6+
return
7+
except:
8+
print(2)
9+
print(3)
10+
f()
11+
312
def f(l, i):
413
try:
514
return l[i]

tests/cmdline/cmd_showbc.py.exp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ Raw bytecode (code_info_size=\\d\+, bytecode_size=\\d\+):
265265
\\d\+ POP_EXCEPT
266266
\\d\+ JUMP \\d\+
267267
\\d\+ END_FINALLY
268-
\\d\+ POP_BLOCK
269268
\\d\+ LOAD_CONST_NONE
270269
\\d\+ LOAD_FAST 1
271270
\\d\+ POP_TOP
@@ -286,7 +285,6 @@ Raw bytecode (code_info_size=\\d\+, bytecode_size=\\d\+):
286285
\\d\+ POP_TOP
287286
\\d\+ LOAD_DEREF 14
288287
\\d\+ POP_TOP
289-
\\d\+ POP_BLOCK
290288
\\d\+ LOAD_CONST_NONE
291289
\\d\+ WITH_CLEANUP
292290
\\d\+ END_FINALLY

0 commit comments

Comments
 (0)