Skip to content

gh-155151: Check the recursion limit in CALL_EX_PY and CALL_KW_BOUND_METHOD - #155272

Closed
tekinertekin wants to merge 1 commit into
python:mainfrom
tekinertekin:fix-specialized-call-recursion
Closed

gh-155151: Check the recursion limit in CALL_EX_PY and CALL_KW_BOUND_METHOD#155272
tekinertekin wants to merge 1 commit into
python:mainfrom
tekinertekin:fix-specialized-call-recursion

Conversation

@tekinertekin

@tekinertekin tekinertekin commented Aug 6, 2026

Copy link
Copy Markdown

Fixes #155151.

The generic CALL_FUNCTION_EX and CALL_KW reach start_frame, which calls
_Py_EnterRecursivePy() and raises RecursionError before the callee runs.
Their specialized forms end in _PUSH_FRAME, which decrements
py_recursion_remaining without checking it. So once a call site had been
warmed up, the callee was entered and returned normally where the
unspecialized instruction raised — warming a call site changed whether the
target function executed.

CALL_KW_PY and CALL_BOUND_METHOD_EXACT_ARGS already guard their frame push
with _CHECK_RECURSION_REMAINING. This adds it to the two that were missing
it.

Where the check goes

_CHECK_RECURSION_REMAINING deopts, so it cannot follow a uop that has
already mutated the stack. Both placements sit where a deopt is already safe:

  • CALL_KW_BOUND_METHOD: after the existing flush that follows
    _EXPAND_METHOD_KW. This is the pattern CALL_BOUND_METHOD_EXACT_ARGS
    uses, where the same flush carries the comment "In case the following
    deopt"
    ahead of _CHECK_STACK_SPACE and _CHECK_RECURSION_REMAINING.
  • CALL_EX_PY: after _CHECK_IS_PY_CALLABLE_EX, which already EXIT_IFs at
    that point.

No new uop is introduced — _CHECK_RECURSION_REMAINING is already present in
executor_cases.c.h and optimizer_cases.c.h, and make regen-cases left
both files unchanged.

Verification

Built from main (free-threaded, --with-pydebug, TSAN) on macOS/arm64.

The reporter's four-case reproducer, before and after:

case opcode before after
call-ex CALL_FUNCTION_EX (control) RecursionError, 0 executions unchanged
call-ex CALL_EX_PY (specialized) returned 42, 1 execution RecursionError, 0 executions
call-kw-bound-method CALL_KW (control) RecursionError, 0 executions unchanged
call-kw-bound-method CALL_KW_BOUND_METHOD (specialized) returned 43, 1 execution RecursionError, 0 executions

The call sites still specialize afterwards — dis(..., adaptive=True) still
reports CALL_EX_PY and CALL_KW_BOUND_METHOD, so this adds the check rather
than suppressing the specialization.

Two regression tests were added next to test_recursion_check_for_general_calls
in test_opcache.py. They warm the call site, assert it specialized, then make
the call from the deepest live frame at the recursion limit and assert the
target never ran. Both fail on unpatched main with AssertionError: RecursionError not raised. The targets bump the counter with plain bytecode
rather than through another call, so reaching them cannot be masked by a second
recursion check.

test_opcache passes (84 tests), including under -R 3:3. test_call,
test_dis, test_generated_cases, test_monitoring, test_optimizer,
test_sys_settrace and test_capi pass. Tools/patchcheck is clean.

I did not build with --enable-experimental-jit locally, so the JIT job here
will be the first check of the tier 2 side.

AI tools were used on this PR: Claude Code (Opus) reproduced the issue, wrote
the patch and the tests, and ran the verification above. I reviewed the change,
understand it and can explain it, and I take responsibility for it.

…BOUND_METHOD

The generic CALL_FUNCTION_EX and CALL_KW opcodes reach start_frame, which
calls _Py_EnterRecursivePy() and raises RecursionError before the callee
runs. Their specialized forms end in _PUSH_FRAME, which decrements
py_recursion_remaining without checking it, so once a call site had been
warmed up the callee was entered and returned normally where the
unspecialized instruction raised. Warming a call site should not change
whether the target function executes.

CALL_KW_PY and CALL_BOUND_METHOD_EXACT_ARGS already guard their frame push
with _CHECK_RECURSION_REMAINING; add it to these two as well. It deopts, so
it goes where a deopt is already safe: after the existing flush in
CALL_KW_BOUND_METHOD, following the pattern of
CALL_BOUND_METHOD_EXACT_ARGS, and after _CHECK_IS_PY_CALLABLE_EX in
CALL_EX_PY, which already exits at that point.
@picnixz

picnixz commented Aug 6, 2026

Copy link
Copy Markdown
Member

The description of this PR and #155269 is almost identical, and thus even the description was probably generated entirely by an agent. In particular, I consider this as an excessive use of agents and will reject this PR especially since it touches the core interpreter.

@picnixz picnixz closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CALL_EX_PY and CALL_KW_BOUND_METHOD can bypass RecursionError after specialization

2 participants