CINXE.COM

PEP 768 – Safe external debugger interface for CPython | peps.python.org

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="color-scheme" content="light dark"> <title>PEP 768 – Safe external debugger interface for CPython | peps.python.org</title> <link rel="shortcut icon" href="../_static/py.png"> <link rel="canonical" href="https://peps.python.org/pep-0768/"> <link rel="stylesheet" href="../_static/style.css" type="text/css"> <link rel="stylesheet" href="../_static/mq.css" type="text/css"> <link rel="stylesheet" href="../_static/pygments.css" type="text/css" media="(prefers-color-scheme: light)" id="pyg-light"> <link rel="stylesheet" href="../_static/pygments_dark.css" type="text/css" media="(prefers-color-scheme: dark)" id="pyg-dark"> <link rel="alternate" type="application/rss+xml" title="Latest PEPs" href="https://peps.python.org/peps.rss"> <meta property="og:title" content='PEP 768 – Safe external debugger interface for CPython | peps.python.org'> <meta property="og:description" content="This PEP proposes adding a zero-overhead debugging interface to CPython that allows debuggers and profilers to safely attach to running Python processes. The interface provides safe execution points for attaching debugger code without modifying the inte..."> <meta property="og:type" content="website"> <meta property="og:url" content="https://peps.python.org/pep-0768/"> <meta property="og:site_name" content="Python Enhancement Proposals (PEPs)"> <meta property="og:image" content="https://peps.python.org/_static/og-image.png"> <meta property="og:image:alt" content="Python PEPs"> <meta property="og:image:width" content="200"> <meta property="og:image:height" content="200"> <meta name="description" content="This PEP proposes adding a zero-overhead debugging interface to CPython that allows debuggers and profilers to safely attach to running Python processes. The interface provides safe execution points for attaching debugger code without modifying the inte..."> <meta name="theme-color" content="#3776ab"> </head> <body> <svg xmlns="http://www.w3.org/2000/svg" style="display: none;"> <symbol id="svg-sun-half" viewBox="0 0 24 24" pointer-events="all"> <title>Following system colour scheme</title> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="9"></circle> <path d="M12 3v18m0-12l4.65-4.65M12 14.3l7.37-7.37M12 19.6l8.85-8.85"></path> </svg> </symbol> <symbol id="svg-moon" viewBox="0 0 24 24" pointer-events="all"> <title>Selected dark colour scheme</title> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path> </svg> </symbol> <symbol id="svg-sun" viewBox="0 0 24 24" pointer-events="all"> <title>Selected light colour scheme</title> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="5"></circle> <line x1="12" y1="1" x2="12" y2="3"></line> <line x1="12" y1="21" x2="12" y2="23"></line> <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> <line x1="1" y1="12" x2="3" y2="12"></line> <line x1="21" y1="12" x2="23" y2="12"></line> <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> </svg> </symbol> </svg> <script> document.documentElement.dataset.colour_scheme = localStorage.getItem("colour_scheme") || "auto" </script> <section id="pep-page-section"> <header> <h1>Python Enhancement Proposals</h1> <ul class="breadcrumbs"> <li><a href="https://www.python.org/" title="The Python Programming Language">Python</a> &raquo; </li> <li><a href="../pep-0000/">PEP Index</a> &raquo; </li> <li>PEP 768</li> </ul> <button id="colour-scheme-cycler" onClick="setColourScheme(nextColourScheme())"> <svg aria-hidden="true" class="colour-scheme-icon-when-auto"><use href="#svg-sun-half"></use></svg> <svg aria-hidden="true" class="colour-scheme-icon-when-dark"><use href="#svg-moon"></use></svg> <svg aria-hidden="true" class="colour-scheme-icon-when-light"><use href="#svg-sun"></use></svg> <span class="visually-hidden">Toggle light / dark / auto colour theme</span> </button> </header> <article> <section id="pep-content"> <h1 class="page-title">PEP 768 – Safe external debugger interface for CPython</h1> <dl class="rfc2822 field-list simple"> <dt class="field-odd">Author<span class="colon">:</span></dt> <dd class="field-odd">Pablo Galindo Salgado &lt;pablogsal&#32;&#97;t&#32;python.org&gt;, Matt Wozniski &lt;godlygeek&#32;&#97;t&#32;gmail.com&gt;, Ivona Stojanovic &lt;stojanovic.i&#32;&#97;t&#32;hotmail.com&gt;</dd> <dt class="field-even">Discussions-To<span class="colon">:</span></dt> <dd class="field-even"><a class="reference external" href="https://discuss.python.org/t/pep-768-safe-external-debugger-interface-for-cpython/73969">Discourse thread</a></dd> <dt class="field-odd">Status<span class="colon">:</span></dt> <dd class="field-odd"><abbr title="Proposal under active discussion and revision">Draft</abbr></dd> <dt class="field-even">Type<span class="colon">:</span></dt> <dd class="field-even"><abbr title="Normative PEP with a new feature for Python, implementation change for CPython or interoperability standard for the ecosystem">Standards Track</abbr></dd> <dt class="field-odd">Created<span class="colon">:</span></dt> <dd class="field-odd">25-Nov-2024</dd> <dt class="field-even">Python-Version<span class="colon">:</span></dt> <dd class="field-even">3.14</dd> <dt class="field-odd">Post-History<span class="colon">:</span></dt> <dd class="field-odd"><a class="reference external" href="https://discuss.python.org/t/pep-768-safe-external-debugger-interface-for-cpython/73969" title="Discourse thread">11-Dec-2024</a></dd> </dl> <hr class="docutils" /> <section id="contents"> <details><summary>Table of Contents</summary><ul class="simple"> <li><a class="reference internal" href="#abstract">Abstract</a></li> <li><a class="reference internal" href="#motivation">Motivation</a></li> <li><a class="reference internal" href="#rationale">Rationale</a></li> <li><a class="reference internal" href="#specification">Specification</a><ul> <li><a class="reference internal" href="#runtime-state-extensions">Runtime State Extensions</a></li> <li><a class="reference internal" href="#debug-offsets-table">Debug Offsets Table</a></li> <li><a class="reference internal" href="#attachment-protocol">Attachment Protocol</a></li> <li><a class="reference internal" href="#interpreter-integration">Interpreter Integration</a></li> <li><a class="reference internal" href="#python-api">Python API</a></li> <li><a class="reference internal" href="#configuration-api">Configuration API</a></li> <li><a class="reference internal" href="#multi-threading-considerations">Multi-threading Considerations</a></li> </ul> </li> <li><a class="reference internal" href="#backwards-compatibility">Backwards Compatibility</a></li> <li><a class="reference internal" href="#security-implications">Security Implications</a><ul> <li><a class="reference internal" href="#security-scenarios">Security scenarios</a></li> </ul> </li> <li><a class="reference internal" href="#how-to-teach-this">How to Teach This</a></li> <li><a class="reference internal" href="#reference-implementation">Reference Implementation</a></li> <li><a class="reference internal" href="#rejected-ideas">Rejected Ideas</a><ul> <li><a class="reference internal" href="#writing-python-code-into-the-buffer">Writing Python code into the buffer</a></li> <li><a class="reference internal" href="#using-a-single-runtime-buffer">Using a Single Runtime Buffer</a></li> </ul> </li> <li><a class="reference internal" href="#thanks">Thanks</a></li> <li><a class="reference internal" href="#copyright">Copyright</a></li> </ul> </details></section> <section id="abstract"> <h2><a class="toc-backref" href="#abstract" role="doc-backlink">Abstract</a></h2> <p>This PEP proposes adding a zero-overhead debugging interface to CPython that allows debuggers and profilers to safely attach to running Python processes. The interface provides safe execution points for attaching debugger code without modifying the interpreter’s normal execution path or adding runtime overhead.</p> <p>A key application of this interface will be enabling pdb to attach to live processes by process ID, similar to <code class="docutils literal notranslate"><span class="pre">gdb</span> <span class="pre">-p</span></code>, allowing developers to inspect and debug Python applications interactively in real-time without stopping or restarting them.</p> </section> <section id="motivation"> <h2><a class="toc-backref" href="#motivation" role="doc-backlink">Motivation</a></h2> <p>Debugging Python processes in production and live environments presents unique challenges. Developers often need to analyze application behavior without stopping or restarting services, which is especially crucial for high-availability systems. Common scenarios include diagnosing deadlocks, inspecting memory usage, or investigating unexpected behavior in real-time.</p> <p>Very few Python tools can attach to running processes, primarily because doing so requires deep expertise in both operating system debugging interfaces and CPython internals. While C/C++ debuggers like GDB and LLDB can attach to processes using well-understood techniques, Python tools must implement all of these low-level mechanisms plus handle additional complexity. For example, when GDB needs to execute code in a target process, it:</p> <ol class="arabic simple"> <li>Uses ptrace to allocate a small chunk of executable memory (easier said than done)</li> <li>Writes a small sequence of machine code - typically a function prologue, the desired instructions, and code to restore registers</li> <li>Saves all the target thread’s registers</li> <li>Changes the instruction pointer to the injected code</li> <li>Lets the process run until it hits a breakpoint at the end of the injected code</li> <li>Restores the original registers and continues execution</li> </ol> <p>Python tools face this same challenge of code injection, but with an additional layer of complexity. Not only do they need to implement the above mechanism, they must also understand and safely interact with CPython’s runtime state, including the interpreter loop, garbage collector, thread state, and reference counting system. This combination of low-level system manipulation and deep domain specific interpreter knowledge makes implementing Python debugging tools exceptionally difficult.</p> <p>The few tools (see for example <a class="reference external" href="https://github.com/microsoft/debugpy/blob/43f41029eabce338becbd1fa1a09727b3cfb1140/src/debugpy/_vendored/pydevd/pydevd_attach_to_process/linux_and_mac/attach.cpp#L4">DebugPy</a> and <a class="reference external" href="https://github.com/bloomberg/memray/blob/main/src/memray/_memray/inject.cpp">Memray</a>) that do attempt this resort to suboptimal and unsafe methods, using system debuggers like GDB and LLDB to forcefully inject code. This approach is fundamentally unsafe because the injected code can execute at any point during the interpreter’s execution cycle - even during critical operations like memory allocation, garbage collection, or thread state management. When this happens, the results are catastrophic: attempting to allocate memory while already inside <code class="docutils literal notranslate"><span class="pre">malloc()</span></code> causes crashes, modifying objects during garbage collection corrupts the interpreter’s state, and touching thread state at the wrong time leads to deadlocks.</p> <p>Various tools attempt to minimize these risks through complex workarounds, such as spawning separate threads for injected code or carefully timing their operations or trying to select some good points to stop the process. However, these mitigations cannot fully solve the underlying problem: without cooperation from the interpreter, there’s no way to know if it’s safe to execute code at any given moment. Even carefully implemented tools can crash the interpreter because they’re fundamentally working against it rather than with it.</p> </section> <section id="rationale"> <h2><a class="toc-backref" href="#rationale" role="doc-backlink">Rationale</a></h2> <p>Rather than forcing tools to work around interpreter limitations with unsafe code injection, we can extend CPython with a proper debugging interface that guarantees safe execution. By adding a few thread state fields and integrating with the interpreter’s existing evaluation loop, we can ensure debugging operations only occur at well-defined safe points. This eliminates the possibility of crashes and corruption while maintaining zero overhead during normal execution.</p> <p>The key insight is that we don’t need to inject code at arbitrary points - we just need to signal to the interpreter that we want code executed at the next safe opportunity. This approach works with the interpreter’s natural execution flow rather than fighting against it.</p> <p>After describing this idea to the PyPy development team, this proposal has already <a class="reference external" href="https://github.com/pypy/pypy/pull/5135">been implemented in PyPy</a>, proving both its feasibility and effectiveness. Their implementation demonstrates that we can provide safe debugging capabilities with zero runtime overhead during normal execution. The proposed mechanism not only reduces risks associated with current debugging approaches but also lays the foundation for future enhancements. For instance, this framework could enable integration with popular observability tools, providing real-time insights into interpreter performance or memory usage. One compelling use case for this interface is enabling pdb to attach to running Python processes, similar to how gdb allows users to attach to a program by process ID (<code class="docutils literal notranslate"><span class="pre">gdb</span> <span class="pre">-p</span> <span class="pre">&lt;pid&gt;</span></code>). With this feature, developers could inspect the state of a running application, evaluate expressions, and step through code dynamically. This approach would align Python’s debugging capabilities with those of other major programming languages and debugging tools that support this mode.</p> </section> <section id="specification"> <h2><a class="toc-backref" href="#specification" role="doc-backlink">Specification</a></h2> <p>This proposal introduces a safe debugging mechanism that allows external processes to trigger code execution in a Python interpreter at well-defined safe points. The key insight is that rather than injecting code directly via system debuggers, we can leverage the interpreter’s existing evaluation loop and thread state to coordinate debugging operations.</p> <p>The mechanism works by having debuggers write to specific memory locations in the target process that the interpreter then checks during its normal execution cycle. When the interpreter detects that a debugger wants to attach, it executes the requested operations only when it’s safe to do so - that is, when no internal locks are held and all data structures are in a consistent state.</p> <section id="runtime-state-extensions"> <h3><a class="toc-backref" href="#runtime-state-extensions" role="doc-backlink">Runtime State Extensions</a></h3> <p>A new structure is added to PyThreadState to support remote debugging:</p> <div class="highlight-C notranslate"><div class="highlight"><pre><span></span><span class="k">typedef</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">_remote_debugger_support</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">debugger_pending_call</span><span class="p">;</span> <span class="w"> </span><span class="kt">char</span><span class="w"> </span><span class="n">debugger_script_path</span><span class="p">[</span><span class="n">MAX_SCRIPT_PATH_SIZE</span><span class="p">];</span> <span class="p">}</span><span class="w"> </span><span class="n">_PyRemoteDebuggerSupport</span><span class="p">;</span> </pre></div> </div> <p>This structure is appended to <code class="docutils literal notranslate"><span class="pre">PyThreadState</span></code>, adding only a few fields that are <strong>never accessed during normal execution</strong>. The <code class="docutils literal notranslate"><span class="pre">debugger_pending_call</span></code> field indicates when a debugger has requested execution, while <code class="docutils literal notranslate"><span class="pre">debugger_script_path</span></code> provides a filesystem path to a Python source file (.py) that will be executed when the interpreter reaches a safe point. The path must point to a Python source file, not compiled Python code (.pyc) or any other format.</p> <p>The value for <code class="docutils literal notranslate"><span class="pre">MAX_SCRIPT_PATH_SIZE</span></code> will be a trade-off between binary size and how big debugging scripts’ paths can be. To limit the memory overhead per thread we will be limiting this to 512 bytes. This size will also be provided as part of the debugger support structure so debuggers know how much they can write. This value can be extended in the future if we ever need to.</p> </section> <section id="debug-offsets-table"> <h3><a class="toc-backref" href="#debug-offsets-table" role="doc-backlink">Debug Offsets Table</a></h3> <p>Python 3.12 introduced a debug offsets table placed at the start of the PyRuntime structure. This section contains the <code class="docutils literal notranslate"><span class="pre">_Py_DebugOffsets</span></code> structure that allows external tools to reliably find critical runtime structures regardless of <a class="reference external" href="https://en.wikipedia.org/wiki/Address_space_layout_randomization">ASLR</a> or how Python was compiled.</p> <p>This proposal extends the existing debug offsets table with new fields for debugger support:</p> <div class="highlight-C notranslate"><div class="highlight"><pre><span></span><span class="k">struct</span><span class="w"> </span><span class="nc">_debugger_support</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kt">uint64_t</span><span class="w"> </span><span class="n">eval_breaker</span><span class="p">;</span><span class="w"> </span><span class="c1">// Location of the eval breaker flag</span> <span class="w"> </span><span class="kt">uint64_t</span><span class="w"> </span><span class="n">remote_debugger_support</span><span class="p">;</span><span class="w"> </span><span class="c1">// Offset to our support structure</span> <span class="w"> </span><span class="kt">uint64_t</span><span class="w"> </span><span class="n">debugger_pending_call</span><span class="p">;</span><span class="w"> </span><span class="c1">// Where to write the pending flag</span> <span class="w"> </span><span class="kt">uint64_t</span><span class="w"> </span><span class="n">debugger_script_path</span><span class="p">;</span><span class="w"> </span><span class="c1">// Where to write the script path</span> <span class="w"> </span><span class="kt">uint64_t</span><span class="w"> </span><span class="n">debugger_script_path_size</span><span class="p">;</span><span class="w"> </span><span class="c1">// Size of the script path buffer</span> <span class="p">}</span><span class="w"> </span><span class="n">debugger_support</span><span class="p">;</span> </pre></div> </div> <p>These offsets allow debuggers to locate critical debugging control structures in the target process’s memory space. The <code class="docutils literal notranslate"><span class="pre">eval_breaker</span></code> and <code class="docutils literal notranslate"><span class="pre">remote_debugger_support</span></code> offsets are relative to each <code class="docutils literal notranslate"><span class="pre">PyThreadState</span></code>, while the <code class="docutils literal notranslate"><span class="pre">debugger_pending_call</span></code> and <code class="docutils literal notranslate"><span class="pre">debugger_script_path</span></code> offsets are relative to each <code class="docutils literal notranslate"><span class="pre">_PyRemoteDebuggerSupport</span></code> structure, allowing the new structure and its fields to be found regardless of where they are in memory. <code class="docutils literal notranslate"><span class="pre">debugger_script_path_size</span></code> informs the attaching tool of the size of the buffer.</p> </section> <section id="attachment-protocol"> <h3><a class="toc-backref" href="#attachment-protocol" role="doc-backlink">Attachment Protocol</a></h3> <p>When a debugger wants to attach to a Python process, it follows these steps:</p> <ol class="arabic simple"> <li>Locate <code class="docutils literal notranslate"><span class="pre">PyRuntime</span></code> structure in the process:<ul class="simple"> <li>Find Python binary (executable or libpython) in process memory (OS dependent process)</li> <li>Extract <code class="docutils literal notranslate"><span class="pre">.PyRuntime</span></code> section offset from binary’s format (ELF/Mach-O/PE)</li> <li>Calculate the actual <code class="docutils literal notranslate"><span class="pre">PyRuntime</span></code> address in the running process by relocating the offset to the binary’s load address</li> </ul> </li> <li>Access debug offset information by reading the <code class="docutils literal notranslate"><span class="pre">_Py_DebugOffsets</span></code> at the start of the <code class="docutils literal notranslate"><span class="pre">PyRuntime</span></code> structure.</li> <li>Use the offsets to locate the desired thread state</li> <li>Use the offsets to locate the debugger interface fields within that thread state</li> <li>Write control information:<ul class="simple"> <li>Most debuggers will pause the process before writing to its memory. This is standard practice for tools like GDB, which use SIGSTOP or ptrace to pause the process. This approach prevents races when writing to process memory. Profilers and other tools that don’t wish to stop the process can still use this interface, but they need to handle possible races. This is a normal consideration for profilers.</li> <li>Write a file path to a Python source file (.py) into the <code class="docutils literal notranslate"><span class="pre">debugger_script_path</span></code> field in <code class="docutils literal notranslate"><span class="pre">_PyRemoteDebuggerSupport</span></code>.</li> <li>Set <code class="docutils literal notranslate"><span class="pre">debugger_pending_call</span></code> flag in <code class="docutils literal notranslate"><span class="pre">_PyRemoteDebuggerSupport</span></code> to 1</li> <li>Set <code class="docutils literal notranslate"><span class="pre">_PY_EVAL_PLEASE_STOP_BIT</span></code> in the <code class="docutils literal notranslate"><span class="pre">eval_breaker</span></code> field</li> </ul> </li> </ol> <p>Once the interpreter reaches the next safe point, it will execute the Python code contained in the file specified by the debugger.</p> </section> <section id="interpreter-integration"> <h3><a class="toc-backref" href="#interpreter-integration" role="doc-backlink">Interpreter Integration</a></h3> <p>The interpreter’s regular evaluation loop already includes a check of the <code class="docutils literal notranslate"><span class="pre">eval_breaker</span></code> flag for handling signals, periodic tasks, and other interrupts. We leverage this existing mechanism by checking for debugger pending calls only when the <code class="docutils literal notranslate"><span class="pre">eval_breaker</span></code> is set, ensuring zero overhead during normal execution. This check has no overhead. Indeed, profiling with Linux <code class="docutils literal notranslate"><span class="pre">perf</span></code> shows this branch is highly predictable - the <code class="docutils literal notranslate"><span class="pre">debugger_pending_call</span></code> check is never taken during normal execution, allowing modern CPUs to effectively speculate past it.</p> <p>When a debugger has set both the <code class="docutils literal notranslate"><span class="pre">eval_breaker</span></code> flag and <code class="docutils literal notranslate"><span class="pre">debugger_pending_call</span></code>, the interpreter will execute the provided debugging code at the next safe point. This all happens in a completely safe context, since the interpreter is guaranteed to be in a consistent state whenever the eval breaker is checked.</p> <p>The only valid values for <code class="docutils literal notranslate"><span class="pre">debugger_pending_call</span></code> will initially be 0 and 1 and other values are reserved for future use.</p> <p>An audit event will be raised before the code is executed, allowing this mechanism to be audited or disabled if desired by a system’s administrator.</p> <div class="highlight-c notranslate"><div class="highlight"><pre><span></span><span class="c1">// In ceval.c</span> <span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">tstate</span><span class="o">-&gt;</span><span class="n">eval_breaker</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">tstate</span><span class="o">-&gt;</span><span class="n">remote_debugger_support</span><span class="p">.</span><span class="n">debugger_pending_call</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">tstate</span><span class="o">-&gt;</span><span class="n">remote_debugger_support</span><span class="p">.</span><span class="n">debugger_pending_call</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span> <span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">tstate</span><span class="o">-&gt;</span><span class="n">remote_debugger_support</span><span class="p">.</span><span class="n">debugger_script_path</span><span class="p">;</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">*</span><span class="n">path</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="mi">0</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="n">PySys_Audit</span><span class="p">(</span><span class="s">&quot;debugger_script&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;%s&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">path</span><span class="p">))</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">PyErr_Clear</span><span class="p">();</span> <span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="kt">FILE</span><span class="o">*</span><span class="w"> </span><span class="n">f</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">fopen</span><span class="p">(</span><span class="n">path</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;r&quot;</span><span class="p">);</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">f</span><span class="p">)</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">PyErr_SetFromErrno</span><span class="p">(</span><span class="n">OSError</span><span class="p">);</span> <span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">PyRun_AnyFile</span><span class="p">(</span><span class="n">f</span><span class="p">,</span><span class="w"> </span><span class="n">path</span><span class="p">);</span> <span class="w"> </span><span class="n">fclose</span><span class="p">(</span><span class="n">f</span><span class="p">);</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">PyErr_Occurred</span><span class="p">())</span><span class="w"> </span><span class="p">{</span> <span class="w"> </span><span class="n">PyErr_WriteUnraisable</span><span class="p">(...);</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">}</span> <span class="w"> </span><span class="p">}</span> <span class="p">}</span> </pre></div> </div> <p>If the code being executed raises any Python exception it will be processed as an <a class="reference external" href="https://docs.python.org/3/c-api/exceptions.html#c.PyErr_WriteUnraisable">unraisable exception</a> in the thread where the code was executed.</p> </section> <section id="python-api"> <h3><a class="toc-backref" href="#python-api" role="doc-backlink">Python API</a></h3> <p>To support safe execution of Python code in a remote process without having to re-implement all these steps in every tool, this proposal extends the <code class="docutils literal notranslate"><span class="pre">sys</span></code> module with a new function. This function allows debuggers or external tools to execute arbitrary Python code within the context of a specified Python process:</p> <div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">def</span><span class="w"> </span><span class="nf">remote_exec</span><span class="p">(</span><span class="n">pid</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">script</span><span class="p">:</span> <span class="nb">str</span><span class="o">|</span><span class="nb">bytes</span><span class="o">|</span><span class="n">PathLike</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span> <span class="w"> </span><span class="sd">&quot;&quot;&quot;</span> <span class="sd"> Executes a file containing Python code in a given remote Python process.</span> <span class="sd"> This function returns immediately, and the code will be executed by the</span> <span class="sd"> target process&#39;s main thread at the next available opportunity, similarly</span> <span class="sd"> to how signals are handled. There is no interface to determine when the</span> <span class="sd"> code has been executed. The caller is responsible for making sure that</span> <span class="sd"> the file still exists whenever the remote process tries to read it and that</span> <span class="sd"> it hasn&#39;t been overwritten.</span> <span class="sd"> Args:</span> <span class="sd"> pid (int): The process ID of the target Python process.</span> <span class="sd"> script (str|bytes|PathLike): The path to a file containing</span> <span class="sd"> the Python code to be executed.</span> <span class="sd"> &quot;&quot;&quot;</span> </pre></div> </div> <p>An example usage of the API would look like:</p> <div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">sys</span> <span class="kn">import</span><span class="w"> </span><span class="nn">uuid</span> <span class="c1"># Execute a print statement in a remote Python process with PID 12345</span> <span class="n">script</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;/tmp/</span><span class="si">{</span><span class="n">uuid</span><span class="o">.</span><span class="n">uuid4</span><span class="p">()</span><span class="si">}</span><span class="s2">.py&quot;</span> <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">script</span><span class="p">,</span> <span class="s2">&quot;w&quot;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span> <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">&quot;print(&#39;Hello from remote execution!&#39;)&quot;</span><span class="p">)</span> <span class="k">try</span><span class="p">:</span> <span class="n">sys</span><span class="o">.</span><span class="n">remote_exec</span><span class="p">(</span><span class="mi">12345</span><span class="p">,</span> <span class="n">script</span><span class="p">)</span> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Failed to execute code: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span> </pre></div> </div> </section> <section id="configuration-api"> <h3><a class="toc-backref" href="#configuration-api" role="doc-backlink">Configuration API</a></h3> <p>To allow redistributors, system administrators, or users to disable this mechanism, several methods will be provided to control the behavior of the interpreter:</p> <p>A new <code class="docutils literal notranslate"><span class="pre">PYTHON_DISABLE_REMOTE_DEBUG</span></code> environment variable will be provided to control the behaviour at runtime. If set to any value (including an empty string), the interpreter will ignore any attempts to attach a debugger using this mechanism.</p> <p>This environment variable will be added together with a new <code class="docutils literal notranslate"><span class="pre">-X</span> <span class="pre">disable-remote-debug</span></code> flag to the Python interpreter to allow users to disable this feature at runtime.</p> <p>Additionally a new <code class="docutils literal notranslate"><span class="pre">--without-remote-debug</span></code> flag will be added to the <code class="docutils literal notranslate"><span class="pre">configure</span></code> script to allow redistributors to build Python without support for remote debugging if they so desire.</p> <p>A new flag indicating the status of remote debugging will be made available via the debug offsets so tools can query if a remote process has disabled the feature. This way, tools can offer a useful error message explaining why they won’t work, instead of believing that they have attached and then never having their script run.</p> </section> <section id="multi-threading-considerations"> <h3><a class="toc-backref" href="#multi-threading-considerations" role="doc-backlink">Multi-threading Considerations</a></h3> <p>The overall execution pattern resembles how Python handles signals internally. The interpreter guarantees that injected code only runs at safe points, never interrupting atomic operations within the interpreter itself. This approach ensures that debugging operations cannot corrupt the interpreter state while still providing timely execution in most real-world scenarios.</p> <p>However, debugging code injected through this interface can execute in any thread. This behavior is different than how Python handles signals, since signal handlers can only run in the main thread. If a debugger wants to inject code into every running thread, it must inject it into every <code class="docutils literal notranslate"><span class="pre">PyThreadState</span></code>. If a debugger wants to run code in the first available thread, it needs to inject it into every <code class="docutils literal notranslate"><span class="pre">PyThreadState</span></code>, and that injected code must check whether it has already been run by another thread (likely by setting some flag in the globals of some module).</p> <p>Note that the Global Interpreter Lock (GIL) continues to govern execution as normal when the injected code runs. This means if a target thread is currently executing a C extension that holds the GIL continuously, the injected code won’t be able to run until that operation completes and the GIL becomes available. However, the interface introduces no additional GIL contention beyond what the injected code itself requires. Importantly, the interface remains fully compatible with Python’s free-threaded mode.</p> <p>It may be useful for a debugger that injected some code to be run to follow that up by sending some pre-registered signal to the process, which can interrupt any blocking I/O or sleep states waiting for external resources, and allow a safe opportunity to run the injected code.</p> </section> </section> <section id="backwards-compatibility"> <h2><a class="toc-backref" href="#backwards-compatibility" role="doc-backlink">Backwards Compatibility</a></h2> <p>This change has no impact on existing Python code or interpreter performance. The added fields are only accessed during debugger attachment, and the checking mechanism piggybacks on existing interpreter safe points.</p> </section> <section id="security-implications"> <h2><a class="toc-backref" href="#security-implications" role="doc-backlink">Security Implications</a></h2> <p>This interface does not introduce new security concerns as it is only usable by processes that can already write to arbitrary memory within a given process and execute arbitrary code on the machine (in order to create the file containing the Python code to be executed).</p> <p>Furthermore, the execution of the code is gated by the interpreter’s audit hooks, which can be used to monitor or prevent the execution of the code in sensitive environments.</p> <p>Existing operating system security mechanisms are effective for guarding against attackers gaining arbitrary memory write access. Although the PEP doesn’t specify how memory should be written to the target process, in practice this will be done using standard system calls that are already being used by other debuggers and tools. Some examples are:</p> <ul class="simple"> <li>On Linux, the <a class="reference external" href="https://man7.org/linux/man-pages/man2/process_vm_readv.2.html">process_vm_readv()</a> and <a class="reference external" href="https://man7.org/linux/man-pages/man2/process_vm_writev.2.html">process_vm_writev()</a> system calls are used to read and write memory from another process. These operations are controlled by <a class="reference external" href="https://man7.org/linux/man-pages/man2/ptrace.2.html">ptrace</a> access mode checks - the same ones that govern debugger attachment. A process can only read from or write to another process’s memory if it has the appropriate permissions (typically requiring either root or the <a class="reference external" href="https://man7.org/linux/man-pages/man7/capabilities.7.html">CAP_SYS_PTRACE</a> capability, though less security minded distributions may allow any process running as the same uid to attach).</li> <li>On macOS, the interface would leverage <a class="reference external" href="https://developer.apple.com/documentation/kernel/1402127-mach_vm_read_overwrite">mach_vm_read_overwrite()</a> and <a class="reference external" href="https://developer.apple.com/documentation/kernel/1402070-mach_vm_write">mach_vm_write()</a> through the Mach task system. These operations require <code class="docutils literal notranslate"><span class="pre">task_for_pid()</span></code> access, which is strictly controlled by the operating system. By default, access is limited to processes running as root or those with specific entitlements granted by Apple’s security framework.</li> <li>On Windows, the <a class="reference external" href="https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory">ReadProcessMemory()</a> and <a class="reference external" href="https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory">WriteProcessMemory()</a> functions provide similar functionality. Access is controlled through the Windows security model - a process needs <a class="reference external" href="https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights">PROCESS_VM_READ</a> and <a class="reference external" href="https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights">PROCESS_VM_WRITE</a> permissions, which typically require the same user context or appropriate privileges. These are the same permissions required by debuggers, ensuring consistent security semantics across platforms.</li> </ul> <p>All mechanisms ensure that:</p> <ol class="arabic simple"> <li>Only authorized processes can read/write memory</li> <li>The same security model that governs traditional debugger attachment applies</li> <li>No additional attack surface is exposed beyond what the OS already provides for debugging</li> <li>Even if an attacker can write arbitrary memory, they cannot escalate this to arbitrary code execution unless they already have filesystem access</li> </ol> <p>The memory operations themselves are well-established and have been used safely for decades in tools like GDB, LLDB, and various system profilers.</p> <p>It’s important to note that any attempt to attach to a Python process via this mechanism would be detectable by system-level monitoring tools as well as by Python audit hooks. This transparency provides an additional layer of accountability, allowing administrators to audit debugging operations in sensitive environments.</p> <p>Further, the strict reliance on OS-level security controls ensures that existing system policies remain effective. For enterprise environments, this means administrators can continue to enforce debugging restrictions using standard tools and policies without requiring additional configuration. For instance, leveraging Linux’s <a class="reference external" href="https://www.kernel.org/doc/Documentation/security/Yama.txt">ptrace_scope</a> or macOS’s <code class="docutils literal notranslate"><span class="pre">taskgated</span></code> to restrict debugger access will equally govern the proposed interface.</p> <p>By maintaining compatibility with existing security frameworks, this design ensures that adopting the new interface requires no changes to established.</p> <section id="security-scenarios"> <h3><a class="toc-backref" href="#security-scenarios" role="doc-backlink">Security scenarios</a></h3> <ul class="simple"> <li>For an external attacker, the ability to write to arbitrary memory in a process is already a severe security issue. This interface does not introduce any new attack surface, as the attacker would already have the ability to execute arbitrary code in the process. This interface behaves in exactly the same way as existing debuggers, and does not introduce any new additional security risks.</li> <li>For an attacker who has gained arbitrary memory write access to a process but not arbitrary code execution, this interface does not allow them to escalate. The ability to calculate and write to specific memory locations is required, which is not available without compromising other machine resources that are external to the Python process.</li> </ul> <p>Additionally, the fact that the code to be executed is gated by the interpreter’s audit hooks means that the execution of the code can be monitored and controlled by system administrators. This means that even if the attacker has compromised the application <strong>and the filesystem</strong>, leveraging this interface for malicious purposes provides a very risky proposition for an attacker, as they risk exposing their actions to system administrators that could not only detect the attack but also take action to prevent it.</p> <p>Finally, is important to note that if an attacker has arbitrary memory write access to a process and has compromised the filesystem, they can already escalate to arbitrary code execution using other existing mechanisms, so this interface does not introduce any new risks in this scenario.</p> </section> </section> <section id="how-to-teach-this"> <h2><a class="toc-backref" href="#how-to-teach-this" role="doc-backlink">How to Teach This</a></h2> <p>For tool authors, this interface becomes the standard way to implement debugger attachment, replacing unsafe system debugger approaches. A section in the Python Developer Guide could describe the internal workings of the mechanism, including the <code class="docutils literal notranslate"><span class="pre">debugger_support</span></code> offsets and how to interact with them using system APIs.</p> <p>End users need not be aware of the interface, benefiting only from improved debugging tool stability and reliability.</p> </section> <section id="reference-implementation"> <h2><a class="toc-backref" href="#reference-implementation" role="doc-backlink">Reference Implementation</a></h2> <p>A reference implementation with a prototype adding remote support for <code class="docutils literal notranslate"><span class="pre">pdb</span></code> can be found <a class="reference external" href="https://github.com/pablogsal/cpython/compare/60ff67d010078eca15a74b1429caf779ac4f9c74...remote_pdb">here</a>.</p> </section> <section id="rejected-ideas"> <h2><a class="toc-backref" href="#rejected-ideas" role="doc-backlink">Rejected Ideas</a></h2> <section id="writing-python-code-into-the-buffer"> <h3><a class="toc-backref" href="#writing-python-code-into-the-buffer" role="doc-backlink">Writing Python code into the buffer</a></h3> <p>We have chosen to have debuggers write the path to a file containing Python code into a buffer in the remote process. This has been deemed more secure than writing the Python code to be executed itself into a buffer in the remote process, because it means that an attacker who has gained arbitrary writes in a process but not arbitrary code execution or file system manipulation can’t escalate to arbitrary code execution through this interface.</p> <p>This does require the attaching debugger to pay close attention to filesystem permissions when creating the file containing the code to be executed, however. If an attacker has the ability to overwrite the file, or to replace a symlink in the file path to point to somewhere attacker controlled, this would allow them to force their malicious code to be executed rather than the code the debugger intends to run.</p> </section> <section id="using-a-single-runtime-buffer"> <h3><a class="toc-backref" href="#using-a-single-runtime-buffer" role="doc-backlink">Using a Single Runtime Buffer</a></h3> <p>During the review of this PEP it has been suggested using a single shared buffer at the runtime level for all debugger communications. While this appeared simpler and required less memory, we discovered it would actually prevent scenarios where multiple debuggers need to coordinate operations across different threads, or where a single debugger needs to orchestrate complex debugging operations. A single shared buffer would force serialization of all debugging operations, making it impossible for debuggers to work independently on different threads.</p> <p>The per-thread buffer approach, despite its memory overhead in highly threaded applications, enables these important debugging scenarios by allowing each debugger to communicate independently with its target thread.</p> </section> </section> <section id="thanks"> <h2><a class="toc-backref" href="#thanks" role="doc-backlink">Thanks</a></h2> <p>We would like to thank CF Bolz-Tereick for their insightful comments and suggestions when discussing this proposal.</p> </section> <section id="copyright"> <h2><a class="toc-backref" href="#copyright" role="doc-backlink">Copyright</a></h2> <p>This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.</p> </section> </section> <hr class="docutils" /> <p>Source: <a class="reference external" href="https://github.com/python/peps/blob/main/peps/pep-0768.rst">https://github.com/python/peps/blob/main/peps/pep-0768.rst</a></p> <p>Last modified: <a class="reference external" href="https://github.com/python/peps/commits/main/peps/pep-0768.rst">2025-03-04 00:57:52 GMT</a></p> </article> <nav id="pep-sidebar"> <h2>Contents</h2> <ul> <li><a class="reference internal" href="#abstract">Abstract</a></li> <li><a class="reference internal" href="#motivation">Motivation</a></li> <li><a class="reference internal" href="#rationale">Rationale</a></li> <li><a class="reference internal" href="#specification">Specification</a><ul> <li><a class="reference internal" href="#runtime-state-extensions">Runtime State Extensions</a></li> <li><a class="reference internal" href="#debug-offsets-table">Debug Offsets Table</a></li> <li><a class="reference internal" href="#attachment-protocol">Attachment Protocol</a></li> <li><a class="reference internal" href="#interpreter-integration">Interpreter Integration</a></li> <li><a class="reference internal" href="#python-api">Python API</a></li> <li><a class="reference internal" href="#configuration-api">Configuration API</a></li> <li><a class="reference internal" href="#multi-threading-considerations">Multi-threading Considerations</a></li> </ul> </li> <li><a class="reference internal" href="#backwards-compatibility">Backwards Compatibility</a></li> <li><a class="reference internal" href="#security-implications">Security Implications</a><ul> <li><a class="reference internal" href="#security-scenarios">Security scenarios</a></li> </ul> </li> <li><a class="reference internal" href="#how-to-teach-this">How to Teach This</a></li> <li><a class="reference internal" href="#reference-implementation">Reference Implementation</a></li> <li><a class="reference internal" href="#rejected-ideas">Rejected Ideas</a><ul> <li><a class="reference internal" href="#writing-python-code-into-the-buffer">Writing Python code into the buffer</a></li> <li><a class="reference internal" href="#using-a-single-runtime-buffer">Using a Single Runtime Buffer</a></li> </ul> </li> <li><a class="reference internal" href="#thanks">Thanks</a></li> <li><a class="reference internal" href="#copyright">Copyright</a></li> </ul> <br> <a id="source" href="https://github.com/python/peps/blob/main/peps/pep-0768.rst">Page Source (GitHub)</a> </nav> </section> <script src="../_static/colour_scheme.js"></script> <script src="../_static/wrap_tables.js"></script> <script src="../_static/sticky_banner.js"></script> </body> </html>

Pages: 1 2 3 4 5 6 7 8 9 10