Skip to content

Commit 943cc14

Browse files
pablogsalivonastojanovicgodlygeek
authored
gh-131591: Implement PEP 768 (#131937)
Co-authored-by: Ivona Stojanovic <[email protected]> Co-authored-by: Matt Wozniski <[email protected]>
1 parent 275056a commit 943cc14

31 files changed

+1796
-2
lines changed

Doc/library/sys.rst

+22
Original file line numberDiff line numberDiff line change
@@ -1835,6 +1835,28 @@ always available. Unless explicitly noted otherwise, all variables are read-only
18351835

18361836
.. versionadded:: 3.12
18371837

1838+
1839+
.. function:: remote_exec(pid, script)
1840+
1841+
Executes *script*, a file containing Python code in the remote
1842+
process with the given *pid*.
1843+
1844+
This function returns immediately, and the code will be executed by the
1845+
target process's main thread at the next available opportunity, similarly
1846+
to how signals are handled. There is no interface to determine when the
1847+
code has been executed. The caller is responsible for making sure that
1848+
the file still exists whenever the remote process tries to read it and that
1849+
it hasn't been overwritten.
1850+
1851+
The remote process must be running a CPython interpreter of the same major
1852+
and minor version as the local process. If either the local or remote
1853+
interpreter is pre-release (alpha, beta, or release candidate) then the
1854+
local and remote interpreters must be the same exact version.
1855+
1856+
.. availability:: Unix, Windows.
1857+
.. versionadded:: next
1858+
1859+
18381860
.. function:: _enablelegacywindowsfsencoding()
18391861

18401862
Changes the :term:`filesystem encoding and error handler` to 'mbcs' and

Doc/using/cmdline.rst

+20
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,17 @@ Miscellaneous options
603603

604604
.. versionadded:: 3.13
605605

606+
* ``-X disable_remote_debug`` disables the remote debugging support as described
607+
in :pep:`768`. This includes both the functionality to schedule code for
608+
execution in another process and the functionality to receive code for
609+
execution in the current process.
610+
611+
This option is only available on some platforms and will do nothing
612+
if is not supported on the current system. See also
613+
:envvar:`PYTHON_DISABLE_REMOTE_DEBUG` and :pep:`768`.
614+
615+
.. versionadded:: next
616+
606617
* :samp:`-X cpu_count={n}` overrides :func:`os.cpu_count`,
607618
:func:`os.process_cpu_count`, and :func:`multiprocessing.cpu_count`.
608619
*n* must be greater than or equal to 1.
@@ -1160,7 +1171,16 @@ conflict.
11601171

11611172
.. versionadded:: 3.13
11621173

1174+
.. envvar:: PYTHON_DISABLE_REMOTE_DEBUG
1175+
1176+
If this variable is set to a non-empty string, it disables the remote
1177+
debugging feature described in :pep:`768`. This includes both the functionality
1178+
to schedule code for execution in another process and the functionality to
1179+
receive code for execution in the current process.
1180+
1181+
See also the :option:`-X disable_remote_debug` command-line option.
11631182

1183+
.. versionadded:: next
11641184

11651185
.. envvar:: PYTHON_CPU_COUNT
11661186

Doc/using/configure.rst

+11
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,17 @@ also be used to improve performance.
660660
Add ``-fstrict-overflow`` to the C compiler flags (by default we add
661661
``-fno-strict-overflow`` instead).
662662

663+
.. option:: --without-remote-debug
664+
665+
Deactivate remote debugging support described in :pep:`768` (enabled by default).
666+
When this flag is provided the code that allows the interpreter to schedule the
667+
execution of a Python file in a separate process as described in :pep:`768` is
668+
not compiled. This includes both the functionality to schedule code to be executed
669+
and the functionality to receive code to be executed.
670+
671+
672+
.. versionadded:: next
673+
663674

664675
.. _debug-build:
665676

Doc/whatsnew/3.14.rst

+57
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,63 @@ If you encounter :exc:`NameError`\s or pickling errors coming out of
9090
New features
9191
============
9292

93+
.. _whatsnew314-pep678:
94+
95+
PEP 768: Safe external debugger interface for CPython
96+
-----------------------------------------------------
97+
98+
:pep:`768` introduces a zero-overhead debugging interface that allows debuggers and profilers
99+
to safely attach to running Python processes. This is a significant enhancement to Python's
100+
debugging capabilities allowing debuggers to forego unsafe alternatives.
101+
102+
The new interface provides safe execution points for attaching debugger code without modifying
103+
the interpreter's normal execution path or adding runtime overhead. This enables tools to
104+
inspect and interact with Python applications in real-time without stopping or restarting
105+
them — a crucial capability for high-availability systems and production environments.
106+
107+
For convenience, CPython implements this interface through the :mod:`sys` module with a
108+
:func:`sys.remote_exec` function::
109+
110+
sys.remote_exec(pid, script_path)
111+
112+
This function allows sending Python code to be executed in a target process at the next safe
113+
execution point. However, tool authors can also implement the protocol directly as described
114+
in the PEP, which details the underlying mechanisms used to safely attach to running processes.
115+
116+
Here's a simple example that inspects object types in a running Python process:
117+
118+
.. code-block:: python
119+
120+
import os
121+
import sys
122+
import tempfile
123+
124+
# Create a temporary script
125+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
126+
script_path = f.name
127+
f.write(f"import my_debugger; my_debugger.connect({os.getpid()})")
128+
try:
129+
# Execute in process with PID 1234
130+
print("Behold! An offering:")
131+
sys.remote_exec(1234, script_path)
132+
finally:
133+
os.unlink(script_path)
134+
135+
The debugging interface has been carefully designed with security in mind and includes several
136+
mechanisms to control access:
137+
138+
* A :envvar:`PYTHON_DISABLE_REMOTE_DEBUG` environment variable.
139+
* A :option:`-X disable-remote-debug` command-line option.
140+
* A :option:`--without-remote-debug` configure flag to completely disable the feature at build time.
141+
142+
A key implementation detail is that the interface piggybacks on the interpreter's existing evaluation
143+
loop and safe points, ensuring zero overhead during normal execution while providing a reliable way
144+
for external processes to coordinate debugging operations.
145+
146+
See :pep:`768` for more details.
147+
148+
(Contributed by Pablo Galindo Salgado, Matt Wozniski, and Ivona Stojanovic in :gh:`131591`.)
149+
93150
.. _whatsnew314-pep758:
94151

95152
PEP 758 – Allow except and except* expressions without parentheses

Include/cpython/initconfig.h

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ typedef struct PyConfig {
143143
int faulthandler;
144144
int tracemalloc;
145145
int perf_profiling;
146+
int remote_debug;
146147
int import_time;
147148
int code_debug_ranges;
148149
int show_ref_count;

Include/cpython/pystate.h

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ typedef int (*Py_tracefunc)(PyObject *, PyFrameObject *, int, PyObject *);
2929
#define PyTrace_C_RETURN 6
3030
#define PyTrace_OPCODE 7
3131

32+
/* Remote debugger support */
33+
#define MAX_SCRIPT_PATH_SIZE 512
34+
typedef struct _remote_debugger_support {
35+
int32_t debugger_pending_call;
36+
char debugger_script_path[MAX_SCRIPT_PATH_SIZE];
37+
} _PyRemoteDebuggerSupport;
38+
3239
typedef struct _err_stackitem {
3340
/* This struct represents a single execution context where we might
3441
* be currently handling an exception. It is a per-coroutine state
@@ -202,6 +209,7 @@ struct _ts {
202209
The PyThreadObject must hold the only reference to this value.
203210
*/
204211
PyObject *threading_local_sentinel;
212+
_PyRemoteDebuggerSupport remote_debugger_support;
205213
};
206214

207215
# define Py_C_RECURSION_LIMIT 5000

Include/internal/pycore_ceval.h

+12
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,18 @@ void _Py_unset_eval_breaker_bit_all(PyInterpreterState *interp, uintptr_t bit);
347347

348348
PyAPI_FUNC(_PyStackRef) _PyFloat_FromDouble_ConsumeInputs(_PyStackRef left, _PyStackRef right, double value);
349349

350+
#ifndef Py_SUPPORTS_REMOTE_DEBUG
351+
#if defined(__APPLE__)
352+
# if !defined(TARGET_OS_OSX)
353+
// Older macOS SDKs do not define TARGET_OS_OSX
354+
# define TARGET_OS_OSX 1
355+
# endif
356+
#endif
357+
#if ((defined(__APPLE__) && TARGET_OS_OSX) || defined(MS_WINDOWS) || (defined(__linux__) && HAVE_PROCESS_VM_READV))
358+
# define Py_SUPPORTS_REMOTE_DEBUG 1
359+
#endif
360+
#endif
361+
350362
#ifdef __cplusplus
351363
}
352364
#endif

Include/internal/pycore_debug_offsets.h

+19
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ typedef struct _Py_DebugOffsets {
7373
uint64_t id;
7474
uint64_t next;
7575
uint64_t threads_head;
76+
uint64_t threads_main;
7677
uint64_t gc;
7778
uint64_t imports_modules;
7879
uint64_t sysdict;
@@ -206,6 +207,15 @@ typedef struct _Py_DebugOffsets {
206207
uint64_t gi_iframe;
207208
uint64_t gi_frame_state;
208209
} gen_object;
210+
211+
struct _debugger_support {
212+
uint64_t eval_breaker;
213+
uint64_t remote_debugger_support;
214+
uint64_t remote_debugging_enabled;
215+
uint64_t debugger_pending_call;
216+
uint64_t debugger_script_path;
217+
uint64_t debugger_script_path_size;
218+
} debugger_support;
209219
} _Py_DebugOffsets;
210220

211221

@@ -223,6 +233,7 @@ typedef struct _Py_DebugOffsets {
223233
.id = offsetof(PyInterpreterState, id), \
224234
.next = offsetof(PyInterpreterState, next), \
225235
.threads_head = offsetof(PyInterpreterState, threads.head), \
236+
.threads_main = offsetof(PyInterpreterState, threads.main), \
226237
.gc = offsetof(PyInterpreterState, gc), \
227238
.imports_modules = offsetof(PyInterpreterState, imports.modules), \
228239
.sysdict = offsetof(PyInterpreterState, sysdict), \
@@ -326,6 +337,14 @@ typedef struct _Py_DebugOffsets {
326337
.gi_iframe = offsetof(PyGenObject, gi_iframe), \
327338
.gi_frame_state = offsetof(PyGenObject, gi_frame_state), \
328339
}, \
340+
.debugger_support = { \
341+
.eval_breaker = offsetof(PyThreadState, eval_breaker), \
342+
.remote_debugger_support = offsetof(PyThreadState, remote_debugger_support), \
343+
.remote_debugging_enabled = offsetof(PyInterpreterState, config.remote_debug), \
344+
.debugger_pending_call = offsetof(_PyRemoteDebuggerSupport, debugger_pending_call), \
345+
.debugger_script_path = offsetof(_PyRemoteDebuggerSupport, debugger_script_path), \
346+
.debugger_script_path_size = MAX_SCRIPT_PATH_SIZE, \
347+
}, \
329348
}
330349

331350

Include/internal/pycore_global_objects_fini_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ struct _Py_global_strings {
686686
STRUCT_FOR_ID(salt)
687687
STRUCT_FOR_ID(sched_priority)
688688
STRUCT_FOR_ID(scheduler)
689+
STRUCT_FOR_ID(script)
689690
STRUCT_FOR_ID(second)
690691
STRUCT_FOR_ID(security_attributes)
691692
STRUCT_FOR_ID(seek)

Include/internal/pycore_runtime_init_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_sysmodule.h

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ extern int _PySys_ClearAttrString(PyInterpreterState *interp,
2424
extern int _PySys_SetFlagObj(Py_ssize_t pos, PyObject *new_value);
2525
extern int _PySys_SetIntMaxStrDigits(int maxdigits);
2626

27+
extern int _PySysRemoteDebug_SendExec(int pid, int tid, const char *debugger_script_path);
28+
2729
#ifdef __cplusplus
2830
}
2931
#endif

Include/internal/pycore_unicodeobject_generated.h

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_capi/test_config.py

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def test_config_get(self):
7373
("program_name", str, None),
7474
("pycache_prefix", str | None, "pycache_prefix"),
7575
("quiet", bool, None),
76+
("remote_debug", int, None),
7677
("run_command", str | None, None),
7778
("run_filename", str | None, None),
7879
("run_module", str | None, None),

Lib/test/test_embed.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
626626
'write_bytecode': True,
627627
'verbose': 0,
628628
'quiet': False,
629+
'remote_debug': True,
629630
'user_site_directory': True,
630631
'configure_c_stdio': False,
631632
'buffered_stdio': True,
@@ -975,7 +976,7 @@ def test_init_global_config(self):
975976
'verbose': True,
976977
'quiet': True,
977978
'buffered_stdio': False,
978-
979+
'remote_debug': True,
979980
'user_site_directory': False,
980981
'pathconfig_warnings': False,
981982
}
@@ -1031,6 +1032,7 @@ def test_init_from_config(self):
10311032
'write_bytecode': False,
10321033
'verbose': 1,
10331034
'quiet': True,
1035+
'remote_debug': True,
10341036
'configure_c_stdio': True,
10351037
'buffered_stdio': False,
10361038
'user_site_directory': False,

0 commit comments

Comments
 (0)