Skip to content

Fix Python 3.13 mixed-mode debugger crash by computing line numbers in-process - #8582

Merged
Rich Chiodo (rchiodo) merged 2 commits into
microsoft:mainfrom
rchiodo:rchiodo-python-313-mixed-mode-debug
Jul 21, 2026
Merged

Fix Python 3.13 mixed-mode debugger crash by computing line numbers in-process#8582
Rich Chiodo (rchiodo) merged 2 commits into
microsoft:mainfrom
rchiodo:rchiodo-python-313-mixed-mode-debug

Conversation

@rchiodo

Copy link
Copy Markdown
Contributor

Problem

Mixed-mode (Python/Native) debugging crashes Visual Studio when debugging a C++ app that embeds CPython 3.11+ (reported against 3.13). When a breakpoint is hit in a GUI app that pumps messages, VS becomes unresponsive and then crashes. A console build of the same app doesn't crash but stepping from Python into native code misbehaves. Python 3.9 worked previously.

Root cause

When building the call stack, PyFrameObject.ComputeLineNumber needs the current line for each Python frame. On 3.11+ the frame's f_lineno is 0 during execution (the line is computed lazily), so the code fell back to a per-frame func-eval in the debuggee on every stop:

((int(*)(void*))PyFrame_GetLineNumber)(frameAddr)

Executing debuggee code (a func-eval) while constructing the call stack is unsafe in a GUI process: the func-eval runs the interpreter, which pumps messages / re-enters, causing reentrancy and deadlock that hangs and crashes VS. It also perturbs the step engine, which explains the broken Python→native stepping in the console case. On 3.9 the line number was read directly from the frame, so no func-eval happened and it worked.

Fix

Compute the line number in-process using memory reads only, mirroring CPython's PyCode_Addr2Line / _PyInterpreterFrame_GetLine:

  • Add the interpreter frame's instruction pointer to the PyInterpreterFrame proxy (instr_ptr on 3.13+, prev_instr on 3.11/3.12).
  • Derive the bytecode offset as instr_ptr - co_code_adaptive and walk co_linetable (a managed port of CPython's location-table decoder) to get the line.
  • PyFrameObject.ComputeLineNumber now returns f_lineno when it's already set, otherwise decodes the line table in-process, and only falls back to the old func-eval as a last resort.

This path only feeds CallStackFilter (display), never step decisions, so stepping behavior is unaffected — but it removes the debuggee func-eval that caused the crash.

Files

  • Python/Product/Debugger.Concord/Proxies/Structs/PyInterpreterFrame.cs — add prev_instr/instr_ptr fields and ComputeLineNumber() (Addr2Line decoder).
  • Python/Product/Debugger.Concord/Proxies/Structs/PyCodeObject311.cs — add co_linetable.
  • Python/Product/Debugger.Concord/Proxies/Structs/PyFrameObject.cs — prefer f_lineno, then in-process decode, then legacy func-eval fallback.
  • Python/Product/Debugger.Concord/Proxies/Structs/PyFrameObject311.cs — override to decode from the interpreter frame.

Testing

Manually verified against a C++ GUI app embedding CPython 3.13 (the Examples/PythonNative sample) in VS 18 (2026 preview): breakpoints in both Python and native code are hit, stepping between Python and native works, and Visual Studio no longer hangs or crashes.

Notes

  • This addresses the Issue-1 GUI crash and the console stepping side-effect. The separate report about combining native debugging + debugpy attach in the same VS instance is architectural (the native debugger freezes the process debugpy needs running) and is not changed here.

…n-process

On Python 3.11+, the heavy PyFrameObject's f_lineno is 0 during execution, so
the call-stack line-number computation fell back to a per-frame func-eval in the
debuggee (PyFrame_GetLineNumber via CppExpressionEvaluator) on every stop. In
GUI apps that pump messages this func-eval caused reentrancy/deadlock and
crashed Visual Studio; it also perturbed the step engine.

Replace the func-eval with an in-process decode of the code object's line table
(a managed port of CPython's PyCode_Addr2Line / _PyInterpreterFrame_GetLine),
computing the bytecode offset from the interpreter frame's instruction pointer
(instr_ptr on 3.13+, prev_instr on 3.11/3.12) minus co_code_adaptive, then
walking co_linetable. The old func-eval remains only as a last-resort fallback.

This only feeds CallStackFilter (display), never step decisions, so stepping
behavior is unaffected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@rchiodo
Rich Chiodo (rchiodo) requested a review from a team as a code owner July 21, 2026 22:20
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@heejaechang

Copy link
Copy Markdown

🔒 Automated review in progress — Heejae Chang (@heejaechang) is auto-reviewing this PR.

@StellaHuang95

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

- Handle the 3.11/3.12 entry-frame case where prev_instr points one code unit
  before the first instruction (negative offset). CPython's PyCode_Addr2Line
  returns co_firstlineno there; previously we returned 0 and fell back to the
  func-eval this change exists to avoid.
- Extract the location-table (PEP 626) decoder into a testable internal static
  class PyLineTable and add table-driven DebuggerTests coverage using
  co_linetable bytes recorded from real 3.11.9, 3.12.7 and 3.13.14 runtimes,
  including NONE (no-line) markers, negative offsets, and past-end offsets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo

Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@rchiodo
Rich Chiodo (rchiodo) enabled auto-merge (squash) July 21, 2026 23:14
@rchiodo
Rich Chiodo (rchiodo) merged commit ee28739 into microsoft:main Jul 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants