Skip to content

JIT segfault or aborts from shape confusion code #141648

@devdanzin

Description

@devdanzin

Crash report

What happened?

It's possible to segfault or cause different aborts on a patched debug JIT build by running the code below. Please let me know whether you can reproduce this issue.

The patch is (it might be possible to reduce this diff and keep the repro):

diff --git a/Include/internal/pycore_backoff.h b/Include/internal/pycore_backoff.h
index 71066f1bd9f..e4a31f76ff2 100644
--- a/Include/internal/pycore_backoff.h
+++ b/Include/internal/pycore_backoff.h
@@ -112,8 +112,8 @@ trigger_backoff_counter(void)
 // For example, 4095 does not work for the nqueens benchmark on pyperformance
 // as we always end up tracing the loop iteration's
 // exhaustion iteration. Which aborts our current tracer.
-#define JUMP_BACKWARD_INITIAL_VALUE 4000
-#define JUMP_BACKWARD_INITIAL_BACKOFF 12
+#define JUMP_BACKWARD_INITIAL_VALUE 63
+#define JUMP_BACKWARD_INITIAL_BACKOFF 6
 static inline _Py_BackoffCounter
 initial_jump_backoff_counter(void)
 {
@@ -125,8 +125,8 @@ initial_jump_backoff_counter(void)
  * Must be larger than ADAPTIVE_COOLDOWN_VALUE,
  * otherwise when a side exit warms up we may construct
  * a new trace before the Tier 1 code has properly re-specialized. */
-#define SIDE_EXIT_INITIAL_VALUE 4000
-#define SIDE_EXIT_INITIAL_BACKOFF 12
+#define SIDE_EXIT_INITIAL_VALUE 63
+#define SIDE_EXIT_INITIAL_BACKOFF 6

 static inline _Py_BackoffCounter
 initial_temperature_backoff_counter(void)
diff --git a/Include/internal/pycore_optimizer.h b/Include/internal/pycore_optimizer.h
index 0307a174e77..13f163584c1 100644
--- a/Include/internal/pycore_optimizer.h
+++ b/Include/internal/pycore_optimizer.h
@@ -86,7 +86,7 @@ PyAPI_FUNC(void) _Py_Executors_InvalidateCold(PyInterpreterState *interp);
 // Used as the threshold to trigger executor invalidation when
 // executor_creation_counter is greater than this value.
 // This value is arbitrary and was not optimized.
-#define JIT_CLEANUP_THRESHOLD 1000
+#define JIT_CLEANUP_THRESHOLD 10000

 int _Py_uop_analyze_and_optimize(
     PyFunctionObject *func,
@@ -118,7 +118,7 @@ static inline uint16_t uop_get_error_target(const _PyUOpInstruction *inst)
 }

 // Holds locals, stack, locals, stack ... co_consts (in that order)
-#define MAX_ABSTRACT_INTERP_SIZE 4096
+#define MAX_ABSTRACT_INTERP_SIZE 8192

 #define TY_ARENA_SIZE (UOP_MAX_TRACE_LENGTH * 5)

@@ -129,7 +129,7 @@ static inline uint16_t uop_get_error_target(const _PyUOpInstruction *inst)
 // progress (and inserting a new ENTER_EXECUTOR instruction). In practice, this
 // is the "maximum amount of polymorphism" that an isolated trace tree can
 // handle before rejoining the rest of the program.
-#define MAX_CHAIN_DEPTH 4
+#define MAX_CHAIN_DEPTH 16

 /* Symbols */
 /* See explanation in optimizer_symbols.c */
diff --git a/Python/optimizer.c b/Python/optimizer.c
index 9db894f0bf0..14cbf670dec 100644
--- a/Python/optimizer.c
+++ b/Python/optimizer.c
@@ -509,7 +509,7 @@ guard_ip_uop[MAX_UOP_ID + 1] = {


 #define CONFIDENCE_RANGE 1000
-#define CONFIDENCE_CUTOFF 333
+#define CONFIDENCE_CUTOFF 100

 #ifdef Py_DEBUG
 #define DPRINTF(level, ...) \

This is the code that causes the segfault, please comment out the indicated blocks to cause assertion failures instead.

def uop_harness_f1():

    def f1():
        return 0.0

    def f2():
        return 0.1111
    funcs = [f1, f2]

    # 1- Comment this block to get an abort instead
    for i in range(200):
        func_to_call = funcs[i % 2]
        try:
            func_to_call()
        except Exception:
            pass
    
    # 2- Also comment this block to get a different abort
    for i in range(200):
        func_to_call = funcs[i % 2]
        if callable(func_to_call):
            for x in range(10):
                try:        
                    func_to_call()
                except Exception:
                    pass


    class ShapeA:
        payload = -1.11

    class ShapeB:
        @property
        def payload(self):
            return 'property_payload'

    class ShapeC:
        def payload(self):
            return id(self)

    class ShapeD:
        __slots__ = ['payload']
        def __init__(self):
            self.payload = 'slot_payload'

    shapes = [ShapeA(), ShapeB(), ShapeC(), ShapeD()]
    for i in range(500):
        obj = shapes[i % len(shapes)]
        try:
            payload_val = obj.payload
            if callable(payload_val):
                for _ in range(15):
                    payload_val()
        except Exception:
            pass

for i_f1 in range(300):
    uop_harness_f1()

Backtrace for segfault:

Program received signal SIGSEGV, Segmentation fault.
0x0000555555c6126d in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7e8ff65e5290, throwflag=<optimized out>) at Python/generated_cases.c.h:5480
5480                assert(executor->vm_data.index == INSTR_OFFSET() - 1);

#0  0x0000555555c6126d in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7e8ff65e5290, throwflag=<optimized out>) at Python/generated_cases.c.h:5480
#1  0x0000555555c8ef88 in _PyEval_EvalFrame (tstate=tstate@entry=0x55555669e760 <_PyRuntime+358304>, frame=frame@entry=0x7e8ff65e5220, throwflag=throwflag@entry=0)
    at ./Include/internal/pycore_ceval.h:121
#2  0x0000555555c8f27c in _PyEval_Vector (tstate=tstate@entry=0x55555669e760 <_PyRuntime+358304>, func=func@entry=0x7cfff661ec60, locals=locals@entry=0x7c7ff6687240, args=args@entry=0x0,
    argcount=argcount@entry=0, kwnames=kwnames@entry=0x0) at Python/ceval.c:2104
#3  0x0000555555c8f52c in PyEval_EvalCode (co=co@entry=0x7d1ff661bed0, globals=globals@entry=0x7c7ff6687240, locals=locals@entry=0x7c7ff6687240) at Python/ceval.c:944
#4  0x0000555555e575b0 in run_eval_code_obj (tstate=tstate@entry=0x55555669e760 <_PyRuntime+358304>, co=co@entry=0x7d1ff661bed0, globals=globals@entry=0x7c7ff6687240,
    locals=locals@entry=0x7c7ff6687240) at Python/pythonrun.c:1372
#5  0x0000555555e578f6 in run_mod (mod=mod@entry=0x7e4ff6615738, filename=filename@entry=0x7cbff65fc010, globals=globals@entry=0x7c7ff6687240, locals=locals@entry=0x7c7ff6687240,
    flags=flags@entry=0x7bfff541ba30, arena=arena@entry=0x7c7ff66adeb0, interactive_src=<optimized out>, generate_new_source=<optimized out>) at Python/pythonrun.c:1475
#6  0x0000555555e58830 in pyrun_file (fp=fp@entry=0x7d4ff65efd00, filename=filename@entry=0x7cbff65fc010, start=start@entry=257, globals=globals@entry=0x7c7ff6687240,
    locals=locals@entry=0x7c7ff6687240, closeit=closeit@entry=1, flags=0x7bfff541ba30) at Python/pythonrun.c:1300

Backtrace for abort from commenting block 1 out:

python: Python/ceval_macros.h:226: GETITEM: Assertion `i < PyTuple_GET_SIZE(v)' failed.

Program received signal SIGABRT, Aborted.

#0  __pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (threadid=<optimized out>, signo=6) at ./nptl/pthread_kill.c:89
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:100
#3  0x00007ffff7445e2e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7428888 in __GI_abort () at ./stdlib/abort.c:77
#5  0x00007ffff74287f0 in __assert_fail_base (fmt=<optimized out>, assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=<optimized out>) at ./assert/assert.c:118
#6  0x00007ffff743c19f in __assert_fail (assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=<optimized out>) at ./assert/assert.c:127
#7  0x0000555555c3172c in GETITEM (v=0x7caff6626470, i=i@entry=5) at Python/ceval_macros.h:226
#8  0x0000555555c700d2 in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7e8ff65e5290, throwflag=<optimized out>) at Python/generated_cases.c.h:7829
#9  0x0000555555c8ef88 in _PyEval_EvalFrame (tstate=tstate@entry=0x55555669e760 <_PyRuntime+358304>, frame=frame@entry=0x7e8ff65e5220, throwflag=throwflag@entry=0)
    at ./Include/internal/pycore_ceval.h:121
#10 0x0000555555c8f27c in _PyEval_Vector (tstate=tstate@entry=0x55555669e760 <_PyRuntime+358304>, func=func@entry=0x7cfff661ec60, locals=locals@entry=0x7c7ff6687240, args=args@entry=0x0,
    argcount=argcount@entry=0, kwnames=kwnames@entry=0x0) at Python/ceval.c:2104
#11 0x0000555555c8f52c in PyEval_EvalCode (co=co@entry=0x7d1ff661b750, globals=globals@entry=0x7c7ff6687240, locals=locals@entry=0x7c7ff6687240) at Python/ceval.c:944

Backtrace for abort from also commenting block 2 out:

python: Python/optimizer.c:1646: _Py_ExecutorDetach: Assertion `instruction->op.code == ENTER_EXECUTOR' failed.

Program received signal SIGABRT, Aborted.

#0  __pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (threadid=<optimized out>, signo=6) at ./nptl/pthread_kill.c:89
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:100
#3  0x00007ffff7445e2e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7428888 in __GI_abort () at ./stdlib/abort.c:77
#5  0x00007ffff74287f0 in __assert_fail_base (fmt=<optimized out>, assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=<optimized out>) at ./assert/assert.c:118
#6  0x00007ffff743c19f in __assert_fail (assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=<optimized out>) at ./assert/assert.c:127
#7  0x0000555555e14dfc in _Py_ExecutorDetach (executor=<optimized out>) at Python/optimizer.c:1646
#8  0x00005555559c74e4 in clear_executors (co=0x7d5ff6603790) at Objects/codeobject.c:2191
#9  0x00005555559cec89 in _PyCode_Clear_Executors (code=<optimized out>) at Objects/codeobject.c:2202
#10 0x0000555555e15c19 in _Py_Executors_InvalidateAll (interp=interp@entry=0x555556667900 <_PyRuntime+133440>, is_invalidation=is_invalidation@entry=0) at Python/optimizer.c:1755
#11 0x0000555555e4070e in finalize_modules (tstate=tstate@entry=0x55555669e760 <_PyRuntime+358304>) at Python/pylifecycle.c:1724
#12 0x0000555555e4c100 in _Py_Finalize (runtime=runtime@entry=0x555556646fc0 <_PyRuntime>) at Python/pylifecycle.c:2265
#13 0x0000555555e4c1da in Py_FinalizeEx () at Python/pylifecycle.c:2388
#14 0x0000555555eb0c6a in Py_RunMain () at Modules/main.c:774

Output from running with PYTHON_LLTRACE=4 (segfault):
236_segfault_lltrace.txt

Output from running with PYTHON_DEBUG=4 (segfault):
236_segfault_opt_debug.txt

I'm not sure this is all a single issue, but it was found based on a single fuzzer attack (shape confusion). We can open more issues if warranted.

Found using lafleur.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

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

Python 3.15.0a1+ (heads/main-dirty:ed73c909f27, Nov 17 2025, 00:19:17) [GCC 15.2.0]

Linked PRs

Metadata

Metadata

Labels

interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-JITtype-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