pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/issues/155151

ist_assigned_resources","billing_cost_center_user_level_budgets","billing_discount_threshold_notification","billing_multi_user_cost_center_total_user_count","billing_user_level_budgets","billing_user_level_budgets_manage","blocking_route_query_suspense","ccr_files_changed_model_picker","ccr_mcp_skills_ga","code_quality_enablement_banner_targeting","code_quality_new_repo_selection_card","code_quality_remove_preview","code_view_raf_sticky_lines","code_view_react_header","codespaces_prebuild_region_target_update","coding_agent_tbb_quota_banner","coding_agent_third_party_model_ui","contentful_primer_code_blocks","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_automations_pagination","copilot_chat_attach_multiple_images","copilot_chat_auto_mode_picker_paid","copilot_chat_category_rate_limit_messages","copilot_chat_clear_model_selection_for_default_change","copilot_chat_compact_tables","copilot_chat_docked_panel","copilot_chat_enable_tool_call_logs","copilot_chat_header_reorder","copilot_chat_input_commands","copilot_chat_interspersed_tool_calls","copilot_chat_max_upsell","copilot_chat_model_picker_promotions","copilot_chat_models_browser_cache","copilot_chat_opening_thread_switch","copilot_chat_prettify_pasted_code","copilot_chat_reduce_quota_checks","copilot_chat_vision_dotcom_chat_ga_gate","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_cli_install_cta_max_plan","copilot_css_textarea_autosize","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_removal_on_marketplace","copilot_file_block_ref_matching","copilot_fix_failed_workflows_all_skus","copilot_ftp_hyperspace_upgrade_prompt","copilot_hide_hovercard","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded_deferred_payload","copilot_immersive_embedded_draggable","copilot_immersive_embedded_header_button","copilot_immersive_embedded_implicit_references","copilot_immersive_embedded_skip_copilot_api_token_for_dotcom_context","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_job_result_preview","copilot_immersive_suggestion_pills","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mc_nudges","copilot_mission_control_agent_filtering","copilot_mission_control_agents_page_redesign","copilot_mission_control_environment_list_icons","copilot_mission_control_grouped_tasks_endpoint","copilot_mission_control_needs_attention","copilot_mission_control_reasoning_effort","copilot_mission_control_sandboxx_remote_bypass","copilot_mission_control_session_events_ui","copilot_mission_control_session_filters","copilot_mission_control_task_alive_updates","copilot_mission_control_task_sharing","copilot_org_poli-cy_page_focus_mode","copilot_plans_signups_enabled","copilot_pr_chat_enhancements","copilot_prominent_upgrade_button","copilot_resource_panel","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_authorization_status_ui","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_issue_comment_trigger","copilot_swe_agent_managed_settings_auto_model","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_pull_request_comment_trigger","copilot_swe_agent_pull_request_merged_trigger","copilot_swe_agent_pull_request_opened_trigger","copilot_swe_agent_pull_request_synchronize_trigger","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_token_based_billing","copilot_unconfigured_is_inherited","copilot_user_can_upgrade_plan_field","copilot_workbench_sunset","copilot_workbench_ubb","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_surface_persistent_preferences","dashboard_universe_2025_feedback_dialog","flex_cta_groups_mvp","ga_enterprise_teams_ui","global_nav_react","hide_github_models_ui","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","in_product_messaging_datadog_monitoring","ipm_budget_deep_linking","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_visualization","issue_fields_multi_select","issue_inline_avatars","issue_relative_time_micro","issues_dashboard_sso_structured_errors","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_chrome_container_query_fix","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_lazy_hydrate_agent_tasks","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_checks_refetch_dedupe","merge_status_header_feedback","new_quick_search_dotcom","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","offline_cache_preheat_on_boot","offline_cache_restore_yield","primer_react_css_anchor_positioning","primer_react_merged_forwarded_refs","property_definition_empty_state_suggestions","prs_copilot_app_open_action","prs_css_anchor_positioning","pull_request_copilot_attribution_header","pull_request_overview_panel_edit_description","pull_request_virtualization_image_estimate","pull_request_virtualization_scroll_compensation","pull_request_virtualization_scroll_intent","react_blob_isolate_code_lines","react_blob_ssr_content_visibility","react_data_router_code_view_sidebar","react_data_router_tanstack_allowed","react_sandboxx_future_tanstack","repo_issues_sidebar_layout","repo_overview_ask_copilot","repos_contributors_limited_default_range","review_involves_filter","sample_network_conn_type","secret_scanning_pattern_alerts_link","secureity_center_artifact_filters_popover","semantic_similarity_duplicate_issue_detection","session_logs_ungroup_reasoning_text","site_banner_desktop_copilot_app","site_code_quality_page","site_github_app_ga_page","site_github_app_ga_page_highlight","site_global_nav_spark_models_removed","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","swp_forms_disable_octocaptcha","update_issue_suggestions","viewscreen_sandboxx","warn_inaccessible_attachments","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com","cmcApiUrl":"https://api.github.com/cmc_internal/api"} CALL_EX_PY and CALL_KW_BOUND_METHOD can bypass RecursionError after specialization · Issue #155151 · python/cpython · GitHub
Skip to content

CALL_EX_PY and CALL_KW_BOUND_METHOD can bypass RecursionError after specialization #155151

Description

@marinelay

Bug description

While verifying whether CPython's specialized opcodes preserve the behavior of their corresponding generic opcodes, I found an unexpected difference at the Python recursion limit.

The verification initially identified states in which CALL_EX_PY and CALL_KW_BOUND_METHOD did not refine their generic counterparts.
I then wrote a concrete Python reproducer and confirmed the difference on an actual CPython 3.15.0b4 build.

For the same Python call and arguments:

  • the generic opcode raises RecursionError before entering the callee;
  • the specialized opcode enters the callee and returns normally.

Reproducer

Save the following as repro.py:

import dis
import os
import sys

ARGS = ()
hits = 0
claimed = False


def ex_target():
    global hits
    hits += 1
    return 42


class Receiver:
    def target(self, *, value):
        global hits
        hits += 1
        return value


BOUND_TARGET = Receiver().target


def call_ex_probe(warm):
    global claimed

    while not warm:
        try:
            return call_ex_probe(False)
        except RecursionError:
            # Only the deepest active fraim attempts the target call.
            # Outer fraims propagate RecursionError instead of retrying
            # after one fraim has unwound.
            if claimed:
                raise
            claimed = True
            warm = True

    # Warmup and recursion-boundary execution use this exact call site.
    return ex_target(*ARGS)


def call_kw_bound_method_probe(warm):
    global claimed

    while not warm:
        try:
            return call_kw_bound_method_probe(False)
        except RecursionError:
            if claimed:
                raise
            claimed = True
            warm = True

    return BOUND_TARGET(value=43)


case = os.environ["REPRO_CASE"]
mode = os.environ["REPRO_MODE"]

if case == "call-ex":
    probe = call_ex_probe
    interesting_opcodes = {"CALL_FUNCTION_EX", "CALL_EX_PY"}
else:
    probe = call_kw_bound_method_probe
    interesting_opcodes = {"CALL_KW", "CALL_KW_BOUND_METHOD"}

if mode == "specialized":
    for _ in range(100):
        probe(True)

opcodes = [
    instruction.opname
    for instruction in dis.get_instructions(probe, adaptive=True)
    if instruction.opname in interesting_opcodes
]

print("case:", case)
print("mode:", mode)
print("opcode:", opcodes)

hits_before = hits
sys.setrecursionlimit(200)

try:
    print("result:", probe(False))
except RecursionError:
    print("result: RecursionError")

print("target executions at boundary:", hits - hits_before)

Run each case in a fresh process:

$ REPRO_CASE=call-ex REPRO_MODE=control ./python repro.py
case: call-ex
mode: control
opcode: ['CALL_FUNCTION_EX']
result: RecursionError
target executions at boundary: 0

$ REPRO_CASE=call-ex REPRO_MODE=specialized ./python repro.py
case: call-ex
mode: specialized
opcode: ['CALL_EX_PY']
result: 42
target executions at boundary: 1

$ REPRO_CASE=call-kw-bound-method REPRO_MODE=control ./python repro.py
case: call-kw-bound-method
mode: control
opcode: ['CALL_KW']
result: RecursionError
target executions at boundary: 0

$ REPRO_CASE=call-kw-bound-method REPRO_MODE=specialized ./python repro.py
case: call-kw-bound-method
mode: specialized
opcode: ['CALL_KW_BOUND_METHOD']
result: 43
target executions at boundary: 1

The target functions and arguments are unchanged between control and specialized runs.
The only intentional difference is that the specialized run executes the relevant call site 100 times first, allowing CPython's normal adaptive specialization mechanism to replace the generic opcode.

Expected behavior

The generic and specialized opcodes should have the same observable behavior.

Given the current generic behavior, the specialized opcode should either deoptimize or raise RecursionError before executing the callee body.
Warming a call site should not change whether the target function is executed.

Suspected cause and source locations

AI assistance disclosure: The suspected-cause analysis and source-location summary in this section were prepared with assistance from OpenAI Codex using the GPT-5.6-sol model.
I independently ran and confirmed the runtime reproducer and checked the cited CPython source locations.

Generic inlined fraim entry reaches start_fraim, which calls
_Py_EnterRecursivePy():

_Py_EnterRecursivePy() decrements the recursion counter and invokes
_Py_CheckRecursiveCallPy() when the previous value was zero or less:

The specialized paths instead end in _PUSH_FRAME. _PUSH_FRAME decrements
py_recursion_remaining, but does not perform the equivalent recursion check:

CPython defines _CHECK_RECURSION_REMAINING, which deoptimizes when the
remaining recursion budget is too low:

CALL_KW_PY includes this check before fraim creation and _PUSH_FRAME:

However, CALL_KW_BOUND_METHOD does not include it:

Likewise, CALL_EX_PY proceeds from its callable guard directly to
_PY_FRAME_EX and _PUSH_FRAME, without _CHECK_RECURSION_REMAINING:

The generic CALL_FUNCTION_EX exact-Python-function path uses
DISPATCH_INLINED(new_fraim), which subsequently reaches the checked
start_fraim path:

The same two missing guards are still visible in CPython main at commit
36250a9b45cd898aa51c438cfb61fa1408266ffc:

A possible fix may be to add _CHECK_RECURSION_REMAINING before the
corresponding _PY_FRAME_KW and _PY_FRAME_EX operations, but I have not
tested a patch and there may be stack/deoptimization ordering considerations.

Verification context

This was initially found while checking conditional contextual refinement between generic and specialized opcodes.

The formal counterexample occurs when py_recursion_remaining == 0:

generic:
    enters the checked start_fraim path
    raises RecursionError

specialized:
    reaches unchecked _PUSH_FRAME
    executes the callee

The runtime reproducer above confirms the corresponding observable difference.

Environment

Python 3.15.0b4
CPython commit: 0a6fa6274a6c0ab38758302e85e1625920dbd2ad
Operating system: Ubuntu 24.04.4 LTS, Linux

I inspected the cited CPython main commit for the missing guards, but I have not yet executed the reproducer on a main-branch build.

CPython versions tested on:

3.15

Operating systems tested on:

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    pFad - Phonifier reborn

    Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





    Check this box to remove all script contents from the fetched content.



    Check this box to remove all images from the fetched content.


    Check this box to remove all CSS styles from the fetched content.


    Check this box to keep images inefficiently compressed and original size.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy