Skip to content

Compiler crash: inlined comprehensions overflow _PyCfgExceptStack, bypassing the u_nfblocks block-nesting limit #156091

Description

@kid-lxy

Crash report

What happened?

Compiling source with 23 or more nested list/set/dict comprehensions crashes
the compiler: an assertion failure in debug builds, a segfault in release
(NDEBUG) builds. Nothing is executed — compile() alone is enough. The input
is ~300 bytes.

Reproduction

def src(d):
    e = "i for i in r"
    for _ in range(d - 1):
        e = "[" + e + "] for i in r"
    return "x = [" + e + "]"

compile(src(23), "<s>", "exec")   # crashes; depth 22 compiles fine

Observed behavior

Cliff is exactly CO_MAXBLOCKS + 2 nested comprehensions:

Build Crash at Behavior
main (20e6c2fc), assertions on depth 23 python: Python/flowgraph.c: push_except_block: Assertion 'stack->depth <= CO_MAXBLOCKS' failed
main (20e6c2fc), -DNDEBUG (-O0 and -O2) depth 23 SIGSEGV
3.12.3 (CO_MAXBLOCKS = 20), distro release build depth 22 SIGSEGV
any build depth ≤ cliff−1 compiles fine

Release-build backtrace (NDEBUG, -O0 -g, gdb):

Thread 1 "fuzz_pycompile_" received signal SIGSEGV, Segmentation fault.
0x00005b2b9480d547 in except_stack_top (stack=0x7ba300866070) at Python/flowgraph.c:724
warning: Source file is more recent than executable.
724	    return stack->handlers[stack->depth];

=== full backtrace (NDEBUG, -O0 -g, HEAD 20e6c2fc) ===
#0  0x00005b2b9480d547 in except_stack_top (stack=0x7ba300866070) at Python/flowgraph.c:724
#1  0x00005b2b94809f6d in label_exception_targets (entryblock=0x7ba300529d40) at Python/flowgraph.c:921
#2  0x00005b2b94809a4d in _PyCfg_OptimizeCodeUnit (g=0x7ba300589350, consts=0x7ba3005a6a80, const_cache=0x7ba30053b140, nlocals=1, nparams=0, firstlineno=1) at Python/flowgraph.c:3798
#3  0x00005b2b947ff534 in optimize_and_assemble_code_unit (u=0x5b2ba5b4cd30, const_cache=0x7ba30053b140, code_flags=0, filename=0x7ba30053b0b0) at Python/compile.c:1481
#4  0x00005b2b947ff29b in _PyCompile_OptimizeAndAssemble (c=0x7ba300542f10, addNone=1) at Python/compile.c:1523
#5  0x00005b2b947ff771 in compiler_mod (c=0x7ba300542f10, mod=0x5b2ba5b6a600) at Python/compile.c:901
#6  0x00005b2b947ff644 in _PyAST_Compile (mod=0x5b2ba5b6a600, filename=0x7ba30053b0b0, pflags=0x7ffeec136588, optimize=0, arena=0x7ba300831b90, module=0x0) at Python/compile.c:1536
#7  0x00005b2b9487244b in _Py_CompileString (str=0x7ffeec132580 "x = ", '[' <repeats 23 times>, "i for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for"..., filename=0x7ba30053b0b0, start=257, flags=0x7ffeec136588, optimize=0, module=0x0) at Python/pythonrun.c:1595
#8  0x00005b2b948722e8 in Py_CompileStringObject (str=0x7ffeec132580 "x = ", '[' <repeats 23 times>, "i for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for"..., filename=0x7ba30053b0b0, start=257, flags=0x7ffeec136588, optimize=0) at Python/pythonrun.c:1561
#9  0x00005b2b94872548 in Py_CompileStringExFlags (str=0x7ffeec132580 "x = ", '[' <repeats 23 times>, "i for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for i in r] for"..., filename_str=0x5b2b94b12d58 "<fuzz input>", start=257, flags=0x7ffeec136588, optimize=0) at Python/pythonrun.c:1608
#10 0x00005b2b9467b515 in fuzz_pycompile (data=0x5b2ba5ac08f0 "\002\001", size=307) at Modules/_xxtestfuzz/fuzzer.c:564
#11 0x00005b2b9467b342 in _run_fuzz (data=0x5b2ba5ac08f0 "\002\001", size=307, fuzzer=0x5b2b9467b370 <fuzz_pycompile>) at Modules/_xxtestfuzz/fuzzer.c:583
#12 0x00005b2b9467b30b in LLVMFuzzerTestOneInput (data=0x5b2ba5ac08f0 "\002\001", size=307) at Modules/_xxtestfuzz/fuzzer.c:724
#13 0x00005b2b94638813 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) ()
#14 0x00005b2b94622691 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) ()
#15 0x00005b2b94628358 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) ()
#16 0x00005b2b94651973 in main ()
#17 0x00007ba30062a1ca in __libc_start_call_main (main=main@entry=0x5b2b94651950 <main>, argc=argc@entry=2, argv=argv@entry=0x7ffeec136df8) at ../sysdeps/nptl/libc_start_call_main.h:58
#18 0x00007ba30062a28b in __libc_start_main_impl (main=0x5b2b94651950 <main>, argc=2, argv=0x7ffeec136df8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffeec136de8) at ../csu/libc-start.c:360
#19 0x00005b2b9461d035 in _start ()

The faulting address equals &handlers[0] + depth * 8, where depth is a
corrupted value (see below), ~1 GB away from the struct.

Root cause

Since PEP 709 (3.12), non-generator comprehensions are inlined into the
enclosing scope. Each inlined comprehension emits one "virtual try/finally"
(SETUP_FINALLY cleanup to restore shadowed outer locals) — but it is
deliberately not counted as an fblock, in codegen_push_inlined_comprehension_locals
(Python/codegen.c):

// no need to push an fblock for this "virtual" try/finally; there can't
// be return/continue/break inside a comprehension

So the only static nesting guard, u_nfblocks >= CO_MAXBLOCKS → "too many statically nested blocks" (Python/compile.c), never sees these blocks.

The backend then trusts the frontend limit and uses a fixed-size stack in
Python/flowgraph.c:

struct _PyCfgExceptStack {
    basicblock *handlers[CO_MAXBLOCKS+2];
    int depth;
};

The N-th inlined comprehension pushes the N-th block; at nesting depth
CO_MAXBLOCKS + 2 the push writes one slot past the array.

  • Assertions on: assert(stack->depth <= CO_MAXBLOCKS) in
    push_except_block fires → abort.
  • NDEBUG: the out-of-bounds store writes handlers[CO_MAXBLOCKS+2],
    which by struct layout is exactly the depth field
    (&handlers[CO_MAXBLOCKS+2] == &depth, confirmed with gdb). depth is
    clobbered with the low 32 bits of a basicblock *; the next
    except_stack_top uses it as an index into handlers[] → wild read →
    deterministic SIGSEGV.

Suggested fix

Count the inlined-comprehension cleanup blocks toward u_nfblocks so the
existing "too many statically nested blocks" SyntaxError fires (consistent
with gh-118348), or make _PyCfgExceptStack dynamically sized / bounds-checked.

Environment

  • CPython main 20e6c2fc7c174342214d561845419c4030f8638f (2026-08-19)
  • Ubuntu 24.04

CPython versions tested on:

3.15, CPython main branch, 3.16, 3.14, 3.13, 3.12

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions