diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index bb88cf73ec1e89..81ff9bc809f10c 100644
--- a/Doc/library/sys.rst
+++ b/Doc/library/sys.rst
@@ -1835,6 +1835,28 @@ always available. Unless explicitly noted otherwise, all variables are read-only
.. versionadded:: 3.12
+
+.. function:: remote_exec(pid, script)
+
+ Executes a file containing Python code given by *script* in the remote
+ process with the given *pid*.
+
+ This function returns immediately, and the code will be executed by the
+ target process's main thread at the next available opportunity, similarly
+ to how signals are handled. There is no interface to determine when the
+ code has been executed. The caller is responsible for making sure that
+ the file still exists whenever the remote process tries to read it and that
+ it hasn't been overwritten.
+
+ The remote process must be running a CPython interpreter of the same major
+ and minor version as the local process. If either the local or remote
+ interpreter is pre-release (alpha, beta, or release candidate) then the
+ local and remote interpreters must be the same exact version.
+
+ .. availability:: Unix, Windows.
+ .. versionadded:: 3.14
+
+
.. function:: _enablelegacywindowsfsencoding()
Changes the :term:`filesystem encoding and error handler` to 'mbcs' and
diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst
index 2a59cf3f62d4c5..38782da8109337 100644
--- a/Doc/using/cmdline.rst
+++ b/Doc/using/cmdline.rst
@@ -603,6 +603,13 @@ Miscellaneous options
.. versionadded:: 3.13
+ * ``-X disable_remote_debug`` disables the remote debugging support as described
+ in :pep:`768`. This option is only available on some platforms and will do nothing
+ if is not supported on the current system. See also
+ :envvar:`PYTHON_DISABLE_REMOTE_DEBUG` and :pep:`768`.
+
+ .. versionadded:: 3.14
+
* :samp:`-X cpu_count={n}` overrides :func:`os.cpu_count`,
:func:`os.process_cpu_count`, and :func:`multiprocessing.cpu_count`.
*n* must be greater than or equal to 1.
@@ -1160,7 +1167,14 @@ conflict.
.. versionadded:: 3.13
+.. envvar:: PYTHON_DISABLE_REMOTE_DEBUG
+
+ If this variable is set to a non-empty string, it disables the remote
+ debugging feature described in :pep:`768`.
+
+ See also the :option:`-X disable_remote_debug` command-line option.
+ .. versionadded:: 3.14
.. envvar:: PYTHON_CPU_COUNT
diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst
index 4d47cf945219dd..e2e90394eb8e6c 100644
--- a/Doc/using/configure.rst
+++ b/Doc/using/configure.rst
@@ -660,6 +660,15 @@ also be used to improve performance.
Add ``-fstrict-overflow`` to the C compiler flags (by default we add
``-fno-strict-overflow`` instead).
+.. option:: --without-remote-debug
+
+ Deactivate remote debugging support described in :pep:`768` (enabled by default).
+ When this flag is provided the code that allows the interpreter to schedule the
+ execution of a Python file in a separate process as described in :pep:`768` is
+ not compiled.
+
+ ..versionadded:: 3.14
+
.. _debug-build:
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index ac5b53ef94bfb1..48c0680d20a6cd 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -90,6 +90,79 @@ If you encounter :exc:`NameError`\s or pickling errors coming out of
New features
============
+.. _whatsnew314-pep678:
+
+PEP 768: Safe external debugger interface for CPython
+----------------------------------------------------
+
+:pep:`768` introduces a zero-overhead debugging interface that allows debuggers and profilers
+to safely attach to running Python processes. This is a significant enhancement to Python's
+debugging capabilities, bringing them in line with other major programming languages.
+
+The new interface provides safe execution points for attaching debugger code without modifying
+the interpreter's normal execution path or adding runtime overhead. This enables tools to
+inspect and interact with Python applications in real-time without stopping or restarting
+them — a crucial capability for high-availability systems and production environments.
+
+For convenience, CPython implements this interface through the :mod:`sys` module with a
+:func:`sys.remote_exec` function::
+
+ sys.remote_exec(pid, script_path)
+
+This function allows sending Python code to be executed in a target process at the next safe
+execution point. However, tool authors can also implement the protocol directly as described
+in the PEP, which details the underlying mechanisms used to safely attach to running processes.
+
+Here's a simple example that inspects object types in a running Python process:
+
+ .. code-block:: python
+
+ import sys
+ import tempfile
+ import os
+
+ # Create a temporary script
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
+ script_path = f.name
+ f.write("""
+ import gc
+ import collections
+
+ # Collect all objects managed by the garbage collector
+ gc.collect()
+
+ # Count objects by type
+ type_counts = collections.Counter(type(obj).__name__
+ for obj in gc.get_objects())
+
+ # Print the most common types
+ print("Most common object types in process:")
+ for type_name, count in type_counts.most_common(10):
+ print(f" {type_name}: {count}")
+ """)
+
+ try:
+ # Execute in process with PID 1234
+ print("Behold! An offering:")
+ sys.remote_exec(1234, script_path)
+ finally:
+ os.unlink(script_path)
+
+The debugging interface has been carefully designed with security in mind and includes several
+mechanisms to control access:
+
+* A :envvar:`PYTHON_DISABLE_REMOTE_DEBUG` environment variable.
+* A :option:`-X disable-remote-debug` command-line option.
+* A ``--without-remote-debug`` configure flag to completely disable the feature at build time.
+
+A key implementation detail is that the interface piggybacks on the interpreter's existing evaluation
+loop and safe points, ensuring zero overhead during normal execution while providing a reliable way
+for external processes to coordinate debugging operations.
+
+See :pep:`768` for more details.
+
+(Contributed by Pablo Galindo Salgado, Matt Wozniski, and Ivona Stojanovic in :gh:`131591`.)
+
.. _whatsnew314-pep649:
PEP 649: deferred evaluation of annotations
diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h
index 8ef19f677066c2..2932fa6c9809e9 100644
--- a/Include/cpython/initconfig.h
+++ b/Include/cpython/initconfig.h
@@ -143,6 +143,7 @@ typedef struct PyConfig {
int faulthandler;
int tracemalloc;
int perf_profiling;
+ int remote_debug;
int import_time;
int code_debug_ranges;
int show_ref_count;
diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h
index 65bc11ca0f5ba9..05b618797a63cc 100644
--- a/Include/cpython/pystate.h
+++ b/Include/cpython/pystate.h
@@ -29,6 +29,13 @@ typedef int (*Py_tracefunc)(PyObject *, PyFrameObject *, int, PyObject *);
#define PyTrace_C_RETURN 6
#define PyTrace_OPCODE 7
+/* Remote debugger support */
+# define MAX_SCRIPT_PATH_SIZE 512
+typedef struct _remote_debugger_support {
+ int32_t debugger_pending_call;
+ char debugger_script_path[MAX_SCRIPT_PATH_SIZE];
+} _PyRemoteDebuggerSupport;
+
typedef struct _err_stackitem {
/* This struct represents a single execution context where we might
* be currently handling an exception. It is a per-coroutine state
@@ -202,6 +209,7 @@ struct _ts {
The PyThreadObject must hold the only reference to this value.
*/
PyObject *threading_local_sentinel;
+ _PyRemoteDebuggerSupport remote_debugger_support;
};
# define Py_C_RECURSION_LIMIT 5000
diff --git a/Include/internal/pycore_debug_offsets.h b/Include/internal/pycore_debug_offsets.h
index a61096c17f143b..124b104e4ba8ae 100644
--- a/Include/internal/pycore_debug_offsets.h
+++ b/Include/internal/pycore_debug_offsets.h
@@ -73,6 +73,7 @@ typedef struct _Py_DebugOffsets {
uint64_t id;
uint64_t next;
uint64_t threads_head;
+ uint64_t threads_main;
uint64_t gc;
uint64_t imports_modules;
uint64_t sysdict;
@@ -206,6 +207,15 @@ typedef struct _Py_DebugOffsets {
uint64_t gi_iframe;
uint64_t gi_frame_state;
} gen_object;
+
+ struct _debugger_support {
+ uint64_t eval_breaker;
+ uint64_t remote_debugger_support;
+ uint64_t remote_debugging_enabled;
+ uint64_t debugger_pending_call;
+ uint64_t debugger_script_path;
+ uint64_t debugger_script_path_size;
+ } debugger_support;
} _Py_DebugOffsets;
@@ -223,6 +233,7 @@ typedef struct _Py_DebugOffsets {
.id = offsetof(PyInterpreterState, id), \
.next = offsetof(PyInterpreterState, next), \
.threads_head = offsetof(PyInterpreterState, threads.head), \
+ .threads_main = offsetof(PyInterpreterState, threads.main), \
.gc = offsetof(PyInterpreterState, gc), \
.imports_modules = offsetof(PyInterpreterState, imports.modules), \
.sysdict = offsetof(PyInterpreterState, sysdict), \
@@ -326,6 +337,14 @@ typedef struct _Py_DebugOffsets {
.gi_iframe = offsetof(PyGenObject, gi_iframe), \
.gi_frame_state = offsetof(PyGenObject, gi_frame_state), \
}, \
+ .debugger_support = { \
+ .eval_breaker = offsetof(PyThreadState, eval_breaker), \
+ .remote_debugger_support = offsetof(PyThreadState, remote_debugger_support), \
+ .remote_debugging_enabled = offsetof(PyInterpreterState, config.remote_debug), \
+ .debugger_pending_call = offsetof(_PyRemoteDebuggerSupport, debugger_pending_call), \
+ .debugger_script_path = offsetof(_PyRemoteDebuggerSupport, debugger_script_path), \
+ .debugger_script_path_size = MAX_SCRIPT_PATH_SIZE, \
+ }, \
}
diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h
index 605c9e1c480c6e..dc072cc77d5d53 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -1195,6 +1195,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(salt));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sched_priority));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(scheduler));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(script));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(second));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(security_attributes));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(seek));
diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h
index 76bc00cfaecf9b..b579ee1a5586b9 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -686,6 +686,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(salt)
STRUCT_FOR_ID(sched_priority)
STRUCT_FOR_ID(scheduler)
+ STRUCT_FOR_ID(script)
STRUCT_FOR_ID(second)
STRUCT_FOR_ID(security_attributes)
STRUCT_FOR_ID(seek)
diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h
index 3927cb8adf14c5..eb1408f67d706d 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -1193,6 +1193,7 @@ extern "C" {
INIT_ID(salt), \
INIT_ID(sched_priority), \
INIT_ID(scheduler), \
+ INIT_ID(script), \
INIT_ID(second), \
INIT_ID(security_attributes), \
INIT_ID(seek), \
diff --git a/Include/internal/pycore_sysmodule.h b/Include/internal/pycore_sysmodule.h
index 9536579e965f7b..008a2da0d04fa7 100644
--- a/Include/internal/pycore_sysmodule.h
+++ b/Include/internal/pycore_sysmodule.h
@@ -24,6 +24,8 @@ extern int _PySys_ClearAttrString(PyInterpreterState *interp,
extern int _PySys_SetFlagObj(Py_ssize_t pos, PyObject *new_value);
extern int _PySys_SetIntMaxStrDigits(int maxdigits);
+extern int _PySysRemoteDebug_SendExec(int pid, int tid, const char *debugger_script_path);
+
#ifdef __cplusplus
}
#endif
diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h
index 67c71986410eef..0de9e9213401cf 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -2532,6 +2532,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(script);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(second);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index c64a80d83f154e..c7bba978127651 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -11,13 +11,18 @@
import subprocess
import sys
import sysconfig
+import socket
import test.support
+from io import StringIO
+from unittest import mock
from test import support
from test.support import os_helper
from test.support.script_helper import assert_python_ok, assert_python_failure
+from test.support.socket_helper import find_unused_port
from test.support import threading_helper
from test.support import import_helper
from test.support import force_not_colorized
+from test.support import SHORT_TIMEOUT
try:
from test.support import interpreters
except ImportError:
@@ -1944,5 +1949,197 @@ def write(self, s):
self.assertEqual(out, b"")
self.assertEqual(err, b"")
+
+def _supports_remote_attaching():
+ PROCESS_VM_READV_SUPPORTED = False
+
+ try:
+ from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
+ except ImportError:
+ pass
+
+ return PROCESS_VM_READV_SUPPORTED
+
+@unittest.skipIf(not sys.is_remote_debug_enabled(), "Remote debugging is not enabled")
+@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux" and sys.platform != "win32",
+ "Test only runs on Linux and MacOS")
+@unittest.skipIf(sys.platform == "linux" and not _supports_remote_attaching(),
+ "Test only runs on Linux with process_vm_readv support")
+class TestRemoteExec(unittest.TestCase):
+ def tearDown(self):
+ test.support.reap_children()
+
+ def _run_remote_exec_test(self, script_code, python_args=None, env=None, prologue=''):
+ # Create the script that will be remotely executed
+ script = os_helper.TESTFN + '_remote.py'
+ self.addCleanup(os_helper.unlink, script)
+
+ with open(script, 'w') as f:
+ f.write(script_code)
+
+ # Create and run the target process
+ target = os_helper.TESTFN + '_target.py'
+ self.addCleanup(os_helper.unlink, target)
+
+ parent_sock, child_sock = socket.socketpair()
+
+ with open(target, 'w') as f:
+ f.write(f'''
+import sys
+import time
+import socket
+import os
+
+# Get the socket from the passed file descriptor
+sock = socket.socket(fileno={child_sock.fileno()})
+
+# Signal that the process is ready
+sock.sendall(b"ready")
+
+{prologue}
+
+print("Target process running...")
+
+# Wait for remote script to be executed
+# (the execution will happen as the following
+# code is processed as soon as the recv call
+# unblocks)
+sock.recv(1024)
+
+# Write confirmation back
+sock.sendall(b"executed")
+sock.close()
+''')
+
+ # Start the target process and capture its output
+ cmd = [sys.executable]
+ if python_args:
+ cmd.extend(python_args)
+ cmd.append(target)
+
+ with subprocess.Popen(cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env,
+ pass_fds=[child_sock.fileno()],
+ ) as proc:
+ try:
+ # Close the child socket in the parent process as it's now owned by the child
+ child_sock.close()
+
+ # Wait for process to be ready
+ response = parent_sock.recv(1024)
+ self.assertEqual(response, b"ready")
+
+ # Try remote exec on the target process
+ sys.remote_exec(proc.pid, script)
+
+ # Signal script to continue
+ parent_sock.sendall(b"continue")
+
+ # Wait for execution confirmation
+ response = parent_sock.recv(1024)
+ self.assertEqual(response, b"executed")
+
+ # Return output for test verification
+ stdout, stderr = proc.communicate(timeout=10.0)
+ return proc.returncode, stdout, stderr
+ except PermissionError:
+ self.skipTest("Insufficient permissions to execute code in remote process")
+ finally:
+ # Wait for execution confirmation
+ parent_sock.close()
+ proc.kill()
+ proc.terminate()
+ proc.wait(timeout=SHORT_TIMEOUT)
+
+ def test_remote_exec(self):
+ """Test basic remote exec functionality"""
+ script = '''
+print("Remote script executed successfully!")
+'''
+ returncode, stdout, stderr = self._run_remote_exec_test(script)
+ # self.assertEqual(returncode, 0)
+ self.assertIn(b"Remote script executed successfully!", stdout)
+ self.assertEqual(stderr, b"")
+
+ def test_remote_exec_with_self_process(self):
+ """Test remote exec with the target process being the same as the test process"""
+
+ code = 'import sys;print("Remote script executed successfully!", file=sys.stderr)'
+ file = os_helper.TESTFN + '_remote_self.py'
+ with open(file, 'w') as f:
+ f.write(code)
+ self.addCleanup(os_helper.unlink, file)
+ with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
+ with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout:
+ sys.remote_exec(os.getpid(), os.path.abspath(file))
+ print("Done")
+ self.assertEqual(mock_stderr.getvalue(), "Remote script executed successfully!\n")
+ self.assertEqual(mock_stdout.getvalue(), "Done\n")
+
+ def test_remote_exec_raises_audit_event(self):
+ """Test remote exec raises an audit event"""
+ prologue = '''\
+import sys
+def audit_hook(event, arg):
+ print(f"Audit event: {event}, arg: {arg}")
+sys.addaudithook(audit_hook)
+'''
+ script = '''
+print("Remote script executed successfully!")
+'''
+ returncode, stdout, stderr = self._run_remote_exec_test(script, prologue=prologue)
+ self.assertEqual(returncode, 0)
+ self.assertIn(b"Remote script executed successfully!", stdout)
+ self.assertIn(b"Audit event: remote_debugger_script, arg: ", stdout)
+ self.assertEqual(stderr, b"")
+
+ def test_remote_exec_with_exception(self):
+ """Test remote exec with an exception raised in the target process
+
+ The exception should be raised in the main thread of the target process
+ but not crash the target process.
+ """
+ script = '''
+raise Exception("Remote script exception")
+'''
+ returncode, stdout, stderr = self._run_remote_exec_test(script)
+ self.assertEqual(returncode, 0)
+ self.assertIn(b"Remote script exception", stderr)
+ self.assertEqual(stdout.strip(), b"Target process running...")
+
+ def test_remote_exec_disabled_by_env(self):
+ """Test remote exec is disabled when PYTHON_DISABLE_REMOTE_DEBUG is set"""
+ env = os.environ.copy()
+ env['PYTHON_DISABLE_REMOTE_DEBUG'] = '1'
+ with self.assertRaisesRegex(RuntimeError, "Remote debugging is not enabled in the remote process"):
+ self._run_remote_exec_test("print('should not run')", env=env)
+
+ def test_remote_exec_disabled_by_xoption(self):
+ """Test remote exec is disabled with -Xdisable-remote-debug"""
+ with self.assertRaisesRegex(RuntimeError, "Remote debugging is not enabled in the remote process"):
+ self._run_remote_exec_test("print('should not run')", python_args=['-Xdisable-remote-debug'])
+
+ def test_remote_exec_invalid_pid(self):
+ """Test remote exec with invalid process ID"""
+ with self.assertRaises(OSError):
+ sys.remote_exec(99999, "print('should not run')")
+
+ def test_remote_exec_syntax_error(self):
+ """Test remote exec with syntax error in script"""
+ script = '''
+this is invalid python code
+'''
+ returncode, stdout, stderr = self._run_remote_exec_test(script)
+ self.assertEqual(returncode, 0)
+ self.assertIn(b"SyntaxError", stderr)
+ self.assertEqual(stdout.strip(), b"Target process running...")
+
+ def test_remote_exec_invalid_script_path(self):
+ """Test remote exec with invalid script path"""
+ with self.assertRaises(OSError):
+ sys.remote_exec(os.getpid(), "invalid_script_path")
+
if __name__ == "__main__":
unittest.main()
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 9658bfa44b98e4..949d7555dd5547 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -506,6 +506,7 @@ PYTHON_OBJS= \
Python/suggestions.o \
Python/perf_trampoline.o \
Python/perf_jit_trampoline.o \
+ Python/remote_debugging.o \
Python/$(DYNLOADFILE) \
$(LIBOBJS) \
$(MACHDEP_OBJS) \
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-03-31-13-25-14.gh-issue-131591.DsPKZt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-03-31-13-25-14.gh-issue-131591.DsPKZt.rst
new file mode 100644
index 00000000000000..5c75c9175f0038
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-03-31-13-25-14.gh-issue-131591.DsPKZt.rst
@@ -0,0 +1,4 @@
+Implement :pep:`768` (Safe external debugger interface for CPython). Add a
+new :func:`sys.remote_exec` function to the :mod:`sys` module. This function
+shcedules the execution of a Python file in a separate process. Patch by
+Pablo Galindo, Matt Matt Wozniski and Ivona Stojanovic.
diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj
index 44292ee32b19fa..59d664dc1745c1 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -260,6 +260,7 @@
+
diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters
index 3842f52e514bb4..0a64de1d4f0e88 100644
--- a/PCbuild/_freeze_module.vcxproj.filters
+++ b/PCbuild/_freeze_module.vcxproj.filters
@@ -406,6 +406,9 @@
Source Files
+
+ Source Files
+
Source Files
diff --git a/PCbuild/build.bat b/PCbuild/build.bat
index 9fbc3e62ce75cc..db67ae72981345 100644
--- a/PCbuild/build.bat
+++ b/PCbuild/build.bat
@@ -95,6 +95,7 @@ if "%~1"=="--experimental-jit" (set UseJIT=true) & (set UseTIER2=1) & shift & go
if "%~1"=="--experimental-jit-off" (set UseJIT=true) & (set UseTIER2=3) & shift & goto CheckOpts
if "%~1"=="--experimental-jit-interpreter" (set UseTIER2=4) & shift & goto CheckOpts
if "%~1"=="--experimental-jit-interpreter-off" (set UseTIER2=6) & shift & goto CheckOpts
+if "%~1"=="--without-remote-debug" (set DisableRemoteDebug=true) & shift & goto CheckOpts
if "%~1"=="--pystats" (set PyStats=1) & shift & goto CheckOpts
if "%~1"=="--tail-call-interp" (set UseTailCallInterp=true) & shift & goto CheckOpts
rem These use the actual property names used by MSBuild. We could just let
@@ -192,6 +193,7 @@ echo on
/p:UseTIER2=%UseTIER2%^
/p:PyStats=%PyStats%^
/p:UseTailCallInterp=%UseTailCallInterp%^
+ /p:DisableRemoteDebug=%DisableRemoteDebug%^
%1 %2 %3 %4 %5 %6 %7 %8 %9
@echo off
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index 2e639ddfc320f5..009e4f2302e311 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -108,6 +108,7 @@
_Py_TIER2=$(UseTIER2);%(PreprocessorDefinitions)
Py_TAIL_CALL_INTERP=1;%(PreprocessorDefinitions)
HAVE_COMPUTED_GOTOS;%(PreprocessorDefinitions)
+ Py_REMOTE_DEBUG;%(PreprocessorDefinitions)
version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;%(AdditionalDependencies)
@@ -640,6 +641,7 @@
+
diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters
index 31064f50f5c8d7..134212662ab379 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -1490,6 +1490,9 @@
Python
+
+ Python
+
Python
diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c
index 77632b283b256c..8fd8f48ce5c576 100644
--- a/Python/ceval_gil.c
+++ b/Python/ceval_gil.c
@@ -1192,6 +1192,54 @@ _PyEval_DisableGIL(PyThreadState *tstate)
}
#endif
+#ifdef Py_REMOTE_DEBUG
+// Note that this function is inline to avoid creating a PLT entry
+// that would be an easy target for a ROP gadget.
+static inline void run_remote_debugger_script(const char *path)
+{
+ if (0 != PySys_Audit("remote_debugger_script", "s", path)) {
+ PyErr_FormatUnraisable(
+ "Audit hook failed for remote debugger script %s", path);
+ return;
+ }
+
+ // Open the debugger script with the open code hook, and reopen the
+ // resulting file object to get a C FILE* object.
+ PyObject* fileobj = PyFile_OpenCode(path);
+ if (!fileobj) {
+ PyErr_FormatUnraisable("Can't open debugger script %s", path);
+ return;
+ }
+
+ int fd = PyObject_AsFileDescriptor(fileobj);
+ if (fd == -1) {
+ PyErr_FormatUnraisable("Can't find fd for debugger script %s", path);
+ } else {
+#ifdef MS_WINDOWS
+ FILE* f = _fdopen(fd, "r");
+#else
+ FILE* f = fdopen(fd, "r");
+#endif
+ if (!f) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ } else {
+ PyRun_AnyFile(f, path);
+ }
+
+ if (PyErr_Occurred()) {
+ PyErr_FormatUnraisable("Error executing debugger script %s", path);
+ }
+ }
+
+ PyObject* res = PyObject_CallMethod(fileobj, "close", "");
+ if (!res) {
+ PyErr_FormatUnraisable("Error closing debugger script %s", path);
+ } else {
+ Py_DECREF(res);
+ }
+ Py_DECREF(fileobj);
+}
+#endif
/* Do periodic things, like check for signals and async I/0.
* We need to do reasonably frequently, but not too frequently.
@@ -1319,5 +1367,35 @@ _Py_HandlePending(PyThreadState *tstate)
return -1;
}
}
+
+#ifdef Py_REMOTE_DEBUG
+ const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
+ if (config->remote_debug == 1
+ && tstate->remote_debugger_support.debugger_pending_call == 1)
+ {
+ tstate->remote_debugger_support.debugger_pending_call = 0;
+
+ // Immediately make a copy in case of a race with another debugger
+ // process that's trying to write to the buffer. At least this way
+ // we'll be internally consistent: what we audit is what we run.
+ const size_t pathsz
+ = sizeof(tstate->remote_debugger_support.debugger_script_path);
+
+ char *path = PyMem_Malloc(pathsz);
+ if (path) {
+ // And don't assume the debugger correctly null terminated it.
+ memcpy(
+ path,
+ tstate->remote_debugger_support.debugger_script_path,
+ pathsz);
+ path[pathsz - 1] = '\0';
+ if (*path) {
+ run_remote_debugger_script(path);
+ }
+ PyMem_Free(path);
+ }
+ }
+#endif
+
return 0;
}
diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h
index d274b0c924cfda..8b73ccefc30ee5 100644
--- a/Python/clinic/sysmodule.c.h
+++ b/Python/clinic/sysmodule.c.h
@@ -1519,6 +1519,104 @@ sys_is_stack_trampoline_active(PyObject *module, PyObject *Py_UNUSED(ignored))
return sys_is_stack_trampoline_active_impl(module);
}
+PyDoc_STRVAR(sys_is_remote_debug_enabled__doc__,
+"is_remote_debug_enabled($module, /)\n"
+"--\n"
+"\n"
+"Return True if remote debugging is enabled, False otherwise.");
+
+#define SYS_IS_REMOTE_DEBUG_ENABLED_METHODDEF \
+ {"is_remote_debug_enabled", (PyCFunction)sys_is_remote_debug_enabled, METH_NOARGS, sys_is_remote_debug_enabled__doc__},
+
+static PyObject *
+sys_is_remote_debug_enabled_impl(PyObject *module);
+
+static PyObject *
+sys_is_remote_debug_enabled(PyObject *module, PyObject *Py_UNUSED(ignored))
+{
+ return sys_is_remote_debug_enabled_impl(module);
+}
+
+PyDoc_STRVAR(sys_remote_exec__doc__,
+"remote_exec($module, /, pid, script)\n"
+"--\n"
+"\n"
+"Executes a file containing Python code in a given remote Python process.\n"
+"\n"
+"This function returns immediately, and the code will be executed by the\n"
+"target process\'s main thread at the next available opportunity, similarly\n"
+"to how signals are handled. There is no interface to determine when the\n"
+"code has been executed. The caller is responsible for making sure that\n"
+"the file still exists whenever the remote process tries to read it and that\n"
+"it hasn\'t been overwritten.\n"
+"\n"
+"The remote process must be running a CPython interpreter of the same major\n"
+"and minor version as the local process. If either the local or remote\n"
+"interpreter is pre-release (alpha, beta, or release candidate) then the\n"
+"local and remote interpreters must be the same exact version.\n"
+"\n"
+"Args:\n"
+" pid (int): The process ID of the target Python process.\n"
+" script (str|bytes): The path to a file containing\n"
+" the Python code to be executed.");
+
+#define SYS_REMOTE_EXEC_METHODDEF \
+ {"remote_exec", _PyCFunction_CAST(sys_remote_exec), METH_FASTCALL|METH_KEYWORDS, sys_remote_exec__doc__},
+
+static PyObject *
+sys_remote_exec_impl(PyObject *module, int pid, PyObject *script);
+
+static PyObject *
+sys_remote_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 2
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ Py_hash_t ob_hash;
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_hash = -1,
+ .ob_item = { &_Py_ID(pid), &_Py_ID(script), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"pid", "script", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "remote_exec",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[2];
+ int pid;
+ PyObject *script;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+ /*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ pid = PyLong_AsInt(args[0]);
+ if (pid == -1 && PyErr_Occurred()) {
+ goto exit;
+ }
+ script = args[1];
+ return_value = sys_remote_exec_impl(module, pid, script);
+
+exit:
+ return return_value;
+}
+
PyDoc_STRVAR(sys__dump_tracelets__doc__,
"_dump_tracelets($module, /, outpath)\n"
"--\n"
@@ -1766,4 +1864,4 @@ sys__is_gil_enabled(PyObject *module, PyObject *Py_UNUSED(ignored))
#ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
#define SYS_GETANDROIDAPILEVEL_METHODDEF
#endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
-/*[clinic end generated code: output=75e202eec4450f50 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=1aca52cefbeb800f input=a9049054013a1b77]*/
diff --git a/Python/initconfig.c b/Python/initconfig.c
index bc48ebd61f2837..e398b1cbeaa25d 100644
--- a/Python/initconfig.c
+++ b/Python/initconfig.c
@@ -162,6 +162,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
SPEC(parse_argv, BOOL, READ_ONLY, NO_SYS),
SPEC(pathconfig_warnings, BOOL, READ_ONLY, NO_SYS),
SPEC(perf_profiling, UINT, READ_ONLY, NO_SYS),
+ SPEC(remote_debug, BOOL, READ_ONLY, NO_SYS),
SPEC(program_name, WSTR, READ_ONLY, NO_SYS),
SPEC(run_command, WSTR_OPT, READ_ONLY, NO_SYS),
SPEC(run_filename, WSTR_OPT, READ_ONLY, NO_SYS),
@@ -317,6 +318,7 @@ The following implementation-specific options are available:\n\
-X perf: support the Linux \"perf\" profiler; also PYTHONPERFSUPPORT=1\n\
-X perf_jit: support the Linux \"perf\" profiler with DWARF support;\n\
also PYTHON_PERF_JIT_SUPPORT=1\n\
+-X disable-remote-debug: disable remote debugging; also PYTHON_DISABLE_REMOTE_DEBUG\n\
"
#ifdef Py_DEBUG
"-X presite=MOD: import this module before site; also PYTHON_PRESITE\n"
@@ -994,6 +996,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
config->faulthandler = -1;
config->tracemalloc = -1;
config->perf_profiling = -1;
+ config->remote_debug = -1;
config->module_search_paths_set = 0;
config->parse_argv = 0;
config->site_import = -1;
@@ -1986,6 +1989,28 @@ config_init_perf_profiling(PyConfig *config)
}
+static PyStatus
+config_init_remote_debug(PyConfig *config)
+{
+#ifndef Py_REMOTE_DEBUG
+ config->remote_debug = 0;
+#else
+ int active = 1;
+ const char *env = Py_GETENV("PYTHON_DISABLE_REMOTE_DEBUG");
+ if (env) {
+ active = 0;
+ }
+ const wchar_t *xoption = config_get_xoption(config, L"disable-remote-debug");
+ if (xoption) {
+ active = 0;
+ }
+
+ config->remote_debug = active;
+#endif
+ return _PyStatus_OK();
+
+}
+
static PyStatus
config_init_tracemalloc(PyConfig *config)
{
@@ -2170,6 +2195,13 @@ config_read_complex_options(PyConfig *config)
}
}
+ if (config->remote_debug < 0) {
+ status = config_init_remote_debug(config);
+ if (_PyStatus_EXCEPTION(status)) {
+ return status;
+ }
+ }
+
if (config->int_max_str_digits < 0) {
status = config_init_int_max_str_digits(config);
if (_PyStatus_EXCEPTION(status)) {
@@ -2531,6 +2563,9 @@ config_read(PyConfig *config, int compute_path_config)
if (config->perf_profiling < 0) {
config->perf_profiling = 0;
}
+ if (config->remote_debug < 0) {
+ config->remote_debug = -1;
+ }
if (config->use_hash_seed < 0) {
config->use_hash_seed = 0;
config->hash_seed = 0;
diff --git a/Python/remote_debugging.c b/Python/remote_debugging.c
new file mode 100644
index 00000000000000..13e23f83fb2eb5
--- /dev/null
+++ b/Python/remote_debugging.c
@@ -0,0 +1,940 @@
+#define _GNU_SOURCE
+#include "pyconfig.h"
+
+#ifdef __linux__
+# include
+# include
+# if INTPTR_MAX == INT64_MAX
+# define Elf_Ehdr Elf64_Ehdr
+# define Elf_Shdr Elf64_Shdr
+# define Elf_Phdr Elf64_Phdr
+# else
+# define Elf_Ehdr Elf32_Ehdr
+# define Elf_Shdr Elf32_Shdr
+# define Elf_Phdr Elf32_Phdr
+# endif
+# include
+#endif
+
+#if defined(__APPLE__)
+# include
+// Older macOS SDKs do not define TARGET_OS_OSX
+# if !defined(TARGET_OS_OSX)
+# define TARGET_OS_OSX 1
+# endif
+# if TARGET_OS_OSX
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# endif
+#endif
+
+#ifdef MS_WINDOWS
+ // Windows includes and definitions
+#include
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#ifndef MS_WINDOWS
+#include
+#include
+#include
+#include
+#endif
+
+#ifndef Py_BUILD_CORE_BUILTIN
+# define Py_BUILD_CORE_MODULE 1
+#endif
+#include "Python.h"
+#include
+#include
+
+#ifndef HAVE_PROCESS_VM_READV
+# define HAVE_PROCESS_VM_READV 0
+#endif
+
+// Define a platform-independent process handle structure
+typedef struct {
+ pid_t pid;
+#ifdef MS_WINDOWS
+ HANDLE hProcess;
+#endif
+} proc_handle_t;
+
+// Initialize the process handle
+static int
+init_proc_handle(proc_handle_t *handle, pid_t pid) {
+ handle->pid = pid;
+#ifdef MS_WINDOWS
+ handle->hProcess = OpenProcess(
+ PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION,
+ FALSE, pid);
+ if (handle->hProcess == NULL) {
+ PyErr_SetFromWindowsErr(0);
+ return -1;
+ }
+#endif
+ return 0;
+}
+
+// Clean up the process handle
+static void
+cleanup_proc_handle(proc_handle_t *handle) {
+#ifdef MS_WINDOWS
+ if (handle->hProcess != NULL) {
+ CloseHandle(handle->hProcess);
+ handle->hProcess = NULL;
+ }
+#endif
+ handle->pid = 0;
+}
+
+/*[clinic input]
+module _pdb
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=7fb1cf2618bcf972]*/
+
+#if defined(__APPLE__) && TARGET_OS_OSX
+static uintptr_t
+return_section_address(
+ const char* section,
+ mach_port_t proc_ref,
+ uintptr_t base,
+ void* map
+) {
+ struct mach_header_64* hdr = (struct mach_header_64*)map;
+ int ncmds = hdr->ncmds;
+
+ int cmd_cnt = 0;
+ struct segment_command_64* cmd = map + sizeof(struct mach_header_64);
+
+ mach_vm_size_t size = 0;
+ mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
+ mach_vm_address_t address = (mach_vm_address_t)base;
+ vm_region_basic_info_data_64_t r_info;
+ mach_port_t object_name;
+ uintptr_t vmaddr = 0;
+
+ for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
+ if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__TEXT") == 0) {
+ vmaddr = cmd->vmaddr;
+ }
+ if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
+ while (cmd->filesize != size) {
+ address += size;
+ kern_return_t ret = mach_vm_region(
+ proc_ref,
+ &address,
+ &size,
+ VM_REGION_BASIC_INFO_64,
+ (vm_region_info_t)&r_info, // cppcheck-suppress [uninitvar]
+ &count,
+ &object_name
+ );
+ if (ret != KERN_SUCCESS) {
+ PyErr_SetString(
+ PyExc_RuntimeError, "Cannot get any more VM maps.\n");
+ return 0;
+ }
+ }
+
+ int nsects = cmd->nsects;
+ struct section_64* sec = (struct section_64*)(
+ (void*)cmd + sizeof(struct segment_command_64)
+ );
+ for (int j = 0; j < nsects; j++) {
+ if (strcmp(sec[j].sectname, section) == 0) {
+ return base + sec[j].addr - vmaddr;
+ }
+ }
+ cmd_cnt++;
+ }
+
+ cmd = (struct segment_command_64*)((void*)cmd + cmd->cmdsize);
+ }
+
+ // We should not be here, but if we are there, we should say about this
+ PyErr_SetString(
+ PyExc_RuntimeError, "Cannot find section address.\n");
+ return 0;
+}
+
+static uintptr_t
+search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_size_t size, mach_port_t proc_ref)
+{
+ int fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ PyErr_Format(PyExc_RuntimeError, "Cannot open binary %s\n", path);
+ return 0;
+ }
+
+ struct stat fs;
+ if (fstat(fd, &fs) == -1) {
+ PyErr_Format(PyExc_RuntimeError, "Cannot get size of binary %s\n", path);
+ close(fd);
+ return 0;
+ }
+
+ void* map = mmap(0, fs.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (map == MAP_FAILED) {
+ PyErr_Format(PyExc_RuntimeError, "Cannot map binary %s\n", path);
+ close(fd);
+ return 0;
+ }
+
+ uintptr_t result = 0;
+
+ struct mach_header_64* hdr = (struct mach_header_64*)map;
+ switch (hdr->magic) {
+ case MH_MAGIC:
+ case MH_CIGAM:
+ case FAT_MAGIC:
+ case FAT_CIGAM:
+ PyErr_SetString(PyExc_RuntimeError, "32-bit Mach-O binaries are not supported");
+ break;
+ case MH_MAGIC_64:
+ case MH_CIGAM_64:
+ result = return_section_address(secname, proc_ref, base, map);
+ break;
+ default:
+ PyErr_SetString(PyExc_RuntimeError, "Unknown Mach-O magic");
+ break;
+ }
+
+ munmap(map, fs.st_size);
+ if (close(fd) != 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ }
+ return result;
+}
+
+static mach_port_t
+pid_to_task(pid_t pid)
+{
+ mach_port_t task;
+ kern_return_t result;
+
+ result = task_for_pid(mach_task_self(), pid, &task);
+ if (result != KERN_SUCCESS) {
+ PyErr_Format(PyExc_PermissionError, "Cannot get task for PID %d", pid);
+ return 0;
+ }
+ return task;
+}
+
+static uintptr_t
+search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr) {
+ mach_vm_address_t address = 0;
+ mach_vm_size_t size = 0;
+ mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
+ vm_region_basic_info_data_64_t region_info;
+ mach_port_t object_name;
+
+ mach_port_t proc_ref = pid_to_task(handle->pid);
+ if (proc_ref == 0) {
+ PyErr_SetString(PyExc_PermissionError, "Cannot get task for PID");
+ return 0;
+ }
+
+ int match_found = 0;
+ char map_filename[MAXPATHLEN + 1];
+ while (mach_vm_region(
+ proc_ref,
+ &address,
+ &size,
+ VM_REGION_BASIC_INFO_64,
+ (vm_region_info_t)®ion_info,
+ &count,
+ &object_name) == KERN_SUCCESS)
+ {
+ if ((region_info.protection & VM_PROT_READ) == 0
+ || (region_info.protection & VM_PROT_EXECUTE) == 0) {
+ address += size;
+ continue;
+ }
+
+ int path_len = proc_regionfilename(
+ handle->pid, address, map_filename, MAXPATHLEN);
+ if (path_len == 0) {
+ address += size;
+ continue;
+ }
+
+ char* filename = strrchr(map_filename, '/');
+ if (filename != NULL) {
+ filename++; // Move past the '/'
+ } else {
+ filename = map_filename; // No path, use the whole string
+ }
+
+ if (!match_found && strncmp(filename, substr, strlen(substr)) == 0) {
+ match_found = 1;
+ return search_section_in_file(
+ secname, map_filename, address, size, proc_ref);
+ }
+
+ address += size;
+ }
+
+ PyErr_SetString(PyExc_RuntimeError,
+ "mach_vm_region failed to find the section");
+ return 0;
+}
+
+#endif // (__APPLE__ && TARGET_OS_OSX)
+
+#ifdef __linux__
+static uintptr_t
+find_map_start_address(proc_handle_t *handle, char* result_filename, const char* map)
+{
+ char maps_file_path[64];
+ sprintf(maps_file_path, "/proc/%d/maps", handle->pid);
+
+ FILE* maps_file = fopen(maps_file_path, "r");
+ if (maps_file == NULL) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return 0;
+ }
+
+ int match_found = 0;
+
+ char line[256];
+ char map_filename[PATH_MAX];
+ uintptr_t result_address = 0;
+ while (fgets(line, sizeof(line), maps_file) != NULL) {
+ unsigned long start_address = 0;
+ sscanf(line, "%lx-%*x %*s %*s %*s %*s %s", &start_address, map_filename);
+ char* filename = strrchr(map_filename, '/');
+ if (filename != NULL) {
+ filename++; // Move past the '/'
+ } else {
+ filename = map_filename; // No path, use the whole string
+ }
+
+ if (!match_found && strncmp(filename, map, strlen(map)) == 0) {
+ match_found = 1;
+ result_address = start_address;
+ strcpy(result_filename, map_filename);
+ break;
+ }
+ }
+
+ fclose(maps_file);
+
+ if (!match_found) {
+ map_filename[0] = '\0';
+ }
+
+ return result_address;
+}
+
+static uintptr_t
+search_map_for_section(proc_handle_t *handle, const char* secname, const char* map)
+{
+ char elf_file[256];
+ uintptr_t start_address = find_map_start_address(handle, elf_file, map);
+
+ if (start_address == 0) {
+ return 0;
+ }
+
+ uintptr_t result = 0;
+ void* file_memory = NULL;
+
+ int fd = open(elf_file, O_RDONLY);
+ if (fd < 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ goto exit;
+ }
+
+ struct stat file_stats;
+ if (fstat(fd, &file_stats) != 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ goto exit;
+ }
+
+ file_memory = mmap(NULL, file_stats.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (file_memory == MAP_FAILED) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ goto exit;
+ }
+
+ Elf_Ehdr* elf_header = (Elf_Ehdr*)file_memory;
+
+ Elf_Shdr* section_header_table = (Elf_Shdr*)(file_memory + elf_header->e_shoff);
+
+ Elf_Shdr* shstrtab_section = §ion_header_table[elf_header->e_shstrndx];
+ char* shstrtab = (char*)(file_memory + shstrtab_section->sh_offset);
+
+ Elf_Shdr* section = NULL;
+ for (int i = 0; i < elf_header->e_shnum; i++) {
+ char* this_sec_name = shstrtab + section_header_table[i].sh_name;
+ // Move 1 character to account for the leading "."
+ this_sec_name += 1;
+ if (strcmp(secname, this_sec_name) == 0) {
+ section = §ion_header_table[i];
+ break;
+ }
+ }
+
+ Elf_Phdr* program_header_table = (Elf_Phdr*)(file_memory + elf_header->e_phoff);
+ // Find the first PT_LOAD segment
+ Elf_Phdr* first_load_segment = NULL;
+ for (int i = 0; i < elf_header->e_phnum; i++) {
+ if (program_header_table[i].p_type == PT_LOAD) {
+ first_load_segment = &program_header_table[i];
+ break;
+ }
+ }
+
+ if (section != NULL && first_load_segment != NULL) {
+ uintptr_t elf_load_addr = first_load_segment->p_vaddr
+ - (first_load_segment->p_vaddr % first_load_segment->p_align);
+ result = start_address + (uintptr_t)section->sh_addr - elf_load_addr;
+ }
+
+exit:
+ if (fd >= 0 && close(fd) != 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ }
+ if (file_memory != NULL) {
+ munmap(file_memory, file_stats.st_size);
+ }
+ return result;
+}
+
+#endif // __linux__
+
+#ifdef MS_WINDOWS
+
+static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char* secname) {
+ HANDLE hFile = CreateFileW(mod_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (hFile == INVALID_HANDLE_VALUE) {
+ PyErr_SetFromWindowsErr(0);
+ return NULL;
+ }
+ HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
+ if (!hMap) {
+ PyErr_SetFromWindowsErr(0);
+ CloseHandle(hFile);
+ return NULL;
+ }
+
+ BYTE* mapView = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
+ if (!mapView) {
+ PyErr_SetFromWindowsErr(0);
+ CloseHandle(hMap);
+ CloseHandle(hFile);
+ return NULL;
+ }
+
+ IMAGE_DOS_HEADER* pDOSHeader = (IMAGE_DOS_HEADER*)mapView;
+ if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid DOS signature.");
+ UnmapViewOfFile(mapView);
+ CloseHandle(hMap);
+ CloseHandle(hFile);
+ return NULL;
+ }
+
+ IMAGE_NT_HEADERS* pNTHeaders = (IMAGE_NT_HEADERS*)(mapView + pDOSHeader->e_lfanew);
+ if (pNTHeaders->Signature != IMAGE_NT_SIGNATURE) {
+ PyErr_SetString(PyExc_RuntimeError, "Invalid NT signature.");
+ UnmapViewOfFile(mapView);
+ CloseHandle(hMap);
+ CloseHandle(hFile);
+ return NULL;
+ }
+
+ IMAGE_SECTION_HEADER* pSection_header = (IMAGE_SECTION_HEADER*)(mapView + pDOSHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
+ void* runtime_addr = NULL;
+
+ for (int i = 0; i < pNTHeaders->FileHeader.NumberOfSections; i++) {
+ const char* name = (const char*)pSection_header[i].Name;
+ if (strncmp(name, secname, IMAGE_SIZEOF_SHORT_NAME) == 0) {
+ runtime_addr = remote_base + pSection_header[i].VirtualAddress;
+ break;
+ }
+ }
+
+ UnmapViewOfFile(mapView);
+ CloseHandle(hMap);
+ CloseHandle(hFile);
+
+ return runtime_addr;
+}
+
+
+static uintptr_t
+search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr) {
+ HANDLE hProcSnap;
+ do {
+ hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid);
+ } while (hProcSnap == INVALID_HANDLE_VALUE && GetLastError() == ERROR_BAD_LENGTH);
+
+ if (hProcSnap == INVALID_HANDLE_VALUE) {
+ PyErr_SetString(PyExc_PermissionError, "Unable to create module snapshot. Check permissions or PID.");
+ return 0;
+ }
+
+ MODULEENTRY32W moduleEntry;
+ moduleEntry.dwSize = sizeof(moduleEntry);
+ void* runtime_addr = NULL;
+
+ for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
+ // Look for either python executable or DLL
+ if (wcsstr(moduleEntry.szModule, substr)) {
+ runtime_addr = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
+ if (runtime_addr != NULL) {
+ break;
+ }
+ }
+ }
+
+ CloseHandle(hProcSnap);
+ return (uintptr_t)runtime_addr;
+}
+
+#endif // MS_WINDOWS
+
+// Get the PyRuntime section address for any platform
+static uintptr_t
+get_py_runtime(proc_handle_t* handle)
+{
+ uintptr_t address = 0;
+
+#ifndef MS_WINDOWS
+ // On non-Windows platforms, try libpython first, then fall back to python
+ address = search_map_for_section(handle, "PyRuntime", "libpython");
+ if (address == 0) {
+ // TODO: Differentiate between not found and error
+ PyErr_Clear();
+ address = search_map_for_section(handle, "PyRuntime", "python");
+ }
+#else
+ // On Windows, search for 'python' in executable or DLL
+ address = search_windows_map_for_section(handle, "PyRuntime", L"python");
+ if (address == 0) {
+ // Error out: 'python' substring covers both executable and DLL
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find the PyRuntime section in the process.");
+ }
+#endif
+
+ return address;
+}
+
+// Platform-independent memory read function
+static int
+read_memory(proc_handle_t *handle, uint64_t remote_address, size_t len, void* dst)
+{
+#ifdef MS_WINDOWS
+ SIZE_T read_bytes = 0;
+ SIZE_T result = 0;
+ do {
+ if (!ReadProcessMemory(handle->hProcess, (LPCVOID)(remote_address + result), (char*)dst + result, len - result, &read_bytes)) {
+ PyErr_SetFromWindowsErr(0);
+ return -1;
+ }
+ result += read_bytes;
+ } while (result < len);
+ return 0;
+#elif defined(__linux__) && HAVE_PROCESS_VM_READV
+ struct iovec local[1];
+ struct iovec remote[1];
+ Py_ssize_t result = 0;
+ Py_ssize_t read_bytes = 0;
+
+ do {
+ local[0].iov_base = (char*)dst + result;
+ local[0].iov_len = len - result;
+ remote[0].iov_base = (void*)(remote_address + result);
+ remote[0].iov_len = len - result;
+
+ read_bytes = process_vm_readv(handle->pid, local, 1, remote, 1, 0);
+ if (read_bytes < 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return -1;
+ }
+
+ result += read_bytes;
+ } while ((size_t)read_bytes != local[0].iov_len);
+ return 0;
+#elif defined(__APPLE__) && TARGET_OS_OSX
+ Py_ssize_t result = -1;
+ kern_return_t kr = mach_vm_read_overwrite(
+ pid_to_task(handle->pid),
+ (mach_vm_address_t)remote_address,
+ len,
+ (mach_vm_address_t)dst,
+ (mach_vm_size_t*)&result);
+
+ if (kr != KERN_SUCCESS) {
+ switch (kr) {
+ case KERN_PROTECTION_FAILURE:
+ PyErr_SetString(PyExc_PermissionError, "Not enough permissions to read memory");
+ break;
+ case KERN_INVALID_ARGUMENT:
+ PyErr_SetString(PyExc_PermissionError, "Invalid argument to mach_vm_read_overwrite");
+ break;
+ default:
+ PyErr_SetString(PyExc_RuntimeError, "Unknown error reading memory");
+ }
+ return -1;
+ }
+ return 0;
+#else
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Memory reading is not supported on this platform");
+ return -1;
+#endif
+}
+
+// Platform-independent memory write function
+static int
+write_memory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src)
+{
+#ifdef MS_WINDOWS
+ SIZE_T written = 0;
+ SIZE_T result = 0;
+ do {
+ if (!WriteProcessMemory(handle->hProcess, (LPVOID)(remote_address + result), (const char*)src + result, len - result, &written)) {
+ PyErr_SetFromWindowsErr(0);
+ return -1;
+ }
+ result += written;
+ } while (result < len);
+ return 0;
+#elif defined(__linux__) && HAVE_PROCESS_VM_READV
+ struct iovec local[1];
+ struct iovec remote[1];
+ Py_ssize_t result = 0;
+ Py_ssize_t written = 0;
+
+ do {
+ local[0].iov_base = (void*)((char*)src + result);
+ local[0].iov_len = len - result;
+ remote[0].iov_base = (void*)((char*)remote_address + result);
+ remote[0].iov_len = len - result;
+
+ written = process_vm_writev(handle->pid, local, 1, remote, 1, 0);
+ if (written < 0) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return -1;
+ }
+
+ result += written;
+ } while ((size_t)written != local[0].iov_len);
+ return 0;
+#elif defined(__APPLE__) && TARGET_OS_OSX
+ kern_return_t kr = mach_vm_write(
+ pid_to_task(handle->pid),
+ (mach_vm_address_t)remote_address,
+ (vm_offset_t)src,
+ (mach_msg_type_number_t)len);
+
+ if (kr != KERN_SUCCESS) {
+ switch (kr) {
+ case KERN_PROTECTION_FAILURE:
+ PyErr_SetString(PyExc_PermissionError, "Not enough permissions to write memory");
+ break;
+ case KERN_INVALID_ARGUMENT:
+ PyErr_SetString(PyExc_PermissionError, "Invalid argument to mach_vm_write");
+ break;
+ default:
+ PyErr_SetString(PyExc_RuntimeError, "Unknown error writing memory");
+ }
+ return -1;
+ }
+ return 0;
+#else
+ PyErr_Format(PyExc_RuntimeError, "Writing memory is not supported on this platform");
+ return -1;
+#endif
+}
+
+static int
+is_prerelease_version(uint64_t version)
+{
+ return (version & 0xF0) != 0xF0;
+}
+
+static int
+ensure_debug_offset_compatibility(const _Py_DebugOffsets* debug_offsets)
+{
+ if (memcmp(debug_offsets->cookie, _Py_Debug_Cookie, sizeof(debug_offsets->cookie)) != 0) {
+ // The remote is probably running a Python version predating debug offsets.
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Can't determine the Python version of the remote process");
+ return -1;
+ }
+
+ // Assume debug offsets could change from one pre-release version to another,
+ // or one minor version to another, but are stable across patch versions.
+ if (is_prerelease_version(Py_Version) && Py_Version != debug_offsets->version) {
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Can't send commands from a pre-release Python interpreter"
+ " to a process running a different Python version");
+ return -1;
+ }
+
+ if (is_prerelease_version(debug_offsets->version) && Py_Version != debug_offsets->version) {
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Can't send commands to a pre-release Python interpreter"
+ " from a process running a different Python version");
+ return -1;
+ }
+
+ unsigned int remote_major = (debug_offsets->version >> 24) & 0xFF;
+ unsigned int remote_minor = (debug_offsets->version >> 16) & 0xFF;
+
+ if (PY_MAJOR_VERSION != remote_major || PY_MINOR_VERSION != remote_minor) {
+ PyErr_Format(
+ PyExc_RuntimeError,
+ "Can't send commands from a Python %d.%d process to a Python %d.%d process",
+ PY_MAJOR_VERSION, PY_MINOR_VERSION, remote_major, remote_minor);
+ return -1;
+ }
+
+ // The debug offsets differ between free threaded and non-free threaded builds.
+ if (_Py_Debug_Free_Threaded && !debug_offsets->free_threaded) {
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Cannot send commands from a free-threaded Python process"
+ " to a process running a non-free-threaded version");
+ return -1;
+ }
+
+ if (!_Py_Debug_Free_Threaded && debug_offsets->free_threaded) {
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Cannot send commands to a free-threaded Python process"
+ " from a process running a non-free-threaded version");
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+read_offsets(
+ proc_handle_t *handle,
+ uintptr_t *runtime_start_address,
+ _Py_DebugOffsets* debug_offsets
+) {
+ *runtime_start_address = get_py_runtime(handle);
+ if (!*runtime_start_address) {
+ if (!PyErr_Occurred()) {
+ PyErr_SetString(
+ PyExc_RuntimeError, "Failed to get PyRuntime address");
+ }
+ return -1;
+ }
+ size_t size = sizeof(struct _Py_DebugOffsets);
+ if (0 != read_memory(handle, *runtime_start_address, size, debug_offsets)) {
+ return -1;
+ }
+ if (ensure_debug_offset_compatibility(debug_offsets)) {
+ return -1;
+ }
+ return 0;
+}
+
+static int
+send_exec_to_proc_handle(proc_handle_t *handle, int tid, const char *debugger_script_path)
+{
+ uintptr_t runtime_start_address;
+ struct _Py_DebugOffsets debug_offsets;
+
+ if (read_offsets(handle, &runtime_start_address, &debug_offsets)) {
+ return -1;
+ }
+
+ uintptr_t interpreter_state_list_head = debug_offsets.runtime_state.interpreters_head;
+
+ uintptr_t interpreter_state_addr;
+ if (0 != read_memory(
+ handle,
+ runtime_start_address + interpreter_state_list_head,
+ sizeof(void*),
+ &interpreter_state_addr))
+ {
+ return -1;
+ }
+
+ if (interpreter_state_addr == 0) {
+ PyErr_SetString(PyExc_RuntimeError, "Can't find a running interpreter in the remote process");
+ return -1;
+ }
+
+ int is_remote_debugging_enabled = 0;
+ if (0 != read_memory(
+ handle,
+ interpreter_state_addr + debug_offsets.debugger_support.remote_debugging_enabled,
+ sizeof(int),
+ &is_remote_debugging_enabled))
+ {
+ return -1;
+ }
+
+ if (is_remote_debugging_enabled != 1) {
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Remote debugging is not enabled in the remote process");
+ return -1;
+ }
+
+ uintptr_t thread_state_addr;
+ unsigned long this_tid = 0;
+
+ if (tid != 0) {
+ if (0 != read_memory(
+ handle,
+ interpreter_state_addr + debug_offsets.interpreter_state.threads_head,
+ sizeof(void*),
+ &thread_state_addr))
+ {
+ return -1;
+ }
+ while (thread_state_addr != 0) {
+ if (0 != read_memory(
+ handle,
+ thread_state_addr + debug_offsets.thread_state.native_thread_id,
+ sizeof(pid_t),
+ &this_tid))
+ {
+ return -1;
+ }
+
+ if (this_tid == (unsigned long)tid) {
+ break;
+ }
+
+ if (0 != read_memory(
+ handle,
+ thread_state_addr + debug_offsets.thread_state.next,
+ sizeof(void*),
+ &thread_state_addr))
+ {
+ return -1;
+ }
+ }
+
+ if (thread_state_addr == 0) {
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Can't find the specified thread in the remote process");
+ return -1;
+ }
+ } else {
+ if (0 != read_memory(
+ handle,
+ interpreter_state_addr + debug_offsets.interpreter_state.threads_main,
+ sizeof(void*),
+ &thread_state_addr))
+ {
+ return -1;
+ }
+
+ if (thread_state_addr == 0) {
+ PyErr_SetString(
+ PyExc_RuntimeError,
+ "Can't find the main thread in the remote process");
+ return -1;
+ }
+ }
+
+ uintptr_t eval_breaker;
+ if (0 != read_memory(
+ handle,
+ thread_state_addr + debug_offsets.debugger_support.eval_breaker,
+ sizeof(uintptr_t),
+ &eval_breaker))
+ {
+ return -1;
+ }
+
+ eval_breaker |= _PY_EVAL_PLEASE_STOP_BIT;
+
+ // Ensure our path is not too long
+ if (debug_offsets.debugger_support.debugger_script_path_size <= strlen(debugger_script_path)) {
+ PyErr_SetString(PyExc_ValueError, "Debugger script path is too long");
+ return -1;
+ }
+
+ uintptr_t debugger_script_path_addr = (
+ thread_state_addr +
+ debug_offsets.debugger_support.remote_debugger_support +
+ debug_offsets.debugger_support.debugger_script_path);
+ if (0 != write_memory(
+ handle,
+ debugger_script_path_addr,
+ strlen(debugger_script_path) + 1,
+ debugger_script_path))
+ {
+ return -1;
+ }
+
+ int pending_call = 1;
+ uintptr_t debugger_pending_call_addr = (
+ thread_state_addr +
+ debug_offsets.debugger_support.remote_debugger_support +
+ debug_offsets.debugger_support.debugger_pending_call);
+ if (0 != write_memory(
+ handle,
+ debugger_pending_call_addr,
+ sizeof(int),
+ &pending_call))
+
+ {
+ return -1;
+ }
+
+ if (0 != write_memory(
+ handle,
+ thread_state_addr + debug_offsets.debugger_support.eval_breaker,
+ sizeof(uintptr_t),
+ &eval_breaker))
+
+ {
+ return -1;
+ }
+
+ return 0;
+}
+
+int
+_PySysRemoteDebug_SendExec(int pid, int tid, const char *debugger_script_path)
+{
+#if (!defined(__linux__) && !defined(__APPLE__) && !defined(MS_WINDOWS)) || (defined(__linux__) && !HAVE_PROCESS_VM_READV)
+ PyErr_SetString(PyExc_RuntimeError, "Remote debugging is not supported on this platform");
+ return -1;
+#endif
+
+ proc_handle_t handle;
+ if (init_proc_handle(&handle, pid) < 0) {
+ return -1;
+ }
+
+ int rc = send_exec_to_proc_handle(&handle, tid, debugger_script_path);
+ cleanup_proc_handle(&handle);
+ return rc;
+}
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index 1b2019a9f74d42..4be7b088514494 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -2421,6 +2421,124 @@ sys_is_stack_trampoline_active_impl(PyObject *module)
Py_RETURN_FALSE;
}
+
+/*[clinic input]
+sys.is_remote_debug_enabled
+
+Return True if remote debugging is enabled, False otherwise.
+[clinic start generated code]*/
+
+static PyObject *
+sys_is_remote_debug_enabled_impl(PyObject *module)
+/*[clinic end generated code: output=7ca3d38bdd5935eb input=7335c4a2fe8cf4f3]*/
+{
+#ifndef Py_REMOTE_DEBUG
+ Py_RETURN_FALSE;
+#else
+ const PyConfig *config = _Py_GetConfig();
+ return PyBool_FromLong(config->remote_debug);
+#endif
+}
+
+static PyObject *
+sys_remote_exec_unicode_path(PyObject *module, int pid, PyObject *script)
+{
+ const char *debugger_script_path = PyUnicode_AsUTF8(script);
+ if (debugger_script_path == NULL) {
+ return NULL;
+ }
+
+#ifdef MS_WINDOWS
+ // Use UTF-16 (wide char) version of the path for permission checks
+ wchar_t *debugger_script_path_w = PyUnicode_AsWideCharString(script, NULL);
+ if (debugger_script_path_w == NULL) {
+ return NULL;
+ }
+
+ // Check file attributes using wide character version (W) instead of ANSI (A)
+ DWORD attr = GetFileAttributesW(debugger_script_path_w);
+ PyMem_Free(debugger_script_path_w);
+ if (attr == INVALID_FILE_ATTRIBUTES) {
+ DWORD err = GetLastError();
+ if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
+ PyErr_SetString(PyExc_FileNotFoundError, "Script file does not exist");
+ }
+ else if (err == ERROR_ACCESS_DENIED) {
+ PyErr_SetString(PyExc_PermissionError, "Script file cannot be read");
+ }
+ else {
+ PyErr_SetFromWindowsErr(0);
+ }
+ return NULL;
+ }
+#else
+ if (access(debugger_script_path, F_OK | R_OK) != 0) {
+ switch (errno) {
+ case ENOENT:
+ PyErr_SetString(PyExc_FileNotFoundError, "Script file does not exist");
+ break;
+ case EACCES:
+ PyErr_SetString(PyExc_PermissionError, "Script file cannot be read");
+ break;
+ default:
+ PyErr_SetFromErrno(PyExc_OSError);
+ }
+ return NULL;
+ }
+#endif
+
+ if (_PySysRemoteDebug_SendExec(pid, 0, debugger_script_path) < 0) {
+ return NULL;
+ }
+
+ Py_RETURN_NONE;
+}
+
+/*[clinic input]
+sys.remote_exec
+
+ pid: int
+ script: object
+
+Executes a file containing Python code in a given remote Python process.
+
+This function returns immediately, and the code will be executed by the
+target process's main thread at the next available opportunity, similarly
+to how signals are handled. There is no interface to determine when the
+code has been executed. The caller is responsible for making sure that
+the file still exists whenever the remote process tries to read it and that
+it hasn't been overwritten.
+
+The remote process must be running a CPython interpreter of the same major
+and minor version as the local process. If either the local or remote
+interpreter is pre-release (alpha, beta, or release candidate) then the
+local and remote interpreters must be the same exact version.
+
+Args:
+ pid (int): The process ID of the target Python process.
+ script (str|bytes): The path to a file containing
+ the Python code to be executed.
+[clinic start generated code]*/
+
+static PyObject *
+sys_remote_exec_impl(PyObject *module, int pid, PyObject *script)
+/*[clinic end generated code: output=7d94c56afe4a52c0 input=39908ca2c5fe1eb0]*/
+{
+ PyObject *ret = NULL;
+ PyObject *os = PyImport_ImportModule("os");
+ if (os) {
+ PyObject *path = PyObject_CallMethod(os, "fsdecode", "O", script);
+ if (path) {
+ ret = sys_remote_exec_unicode_path(module, pid, path);
+ Py_DECREF(path);
+ }
+ Py_DECREF(os);
+ }
+ return ret;
+}
+
+
+
/*[clinic input]
sys._dump_tracelets
@@ -2695,6 +2813,8 @@ static PyMethodDef sys_methods[] = {
SYS_ACTIVATE_STACK_TRAMPOLINE_METHODDEF
SYS_DEACTIVATE_STACK_TRAMPOLINE_METHODDEF
SYS_IS_STACK_TRAMPOLINE_ACTIVE_METHODDEF
+ SYS_IS_REMOTE_DEBUG_ENABLED_METHODDEF
+ SYS_REMOTE_EXEC_METHODDEF
SYS_UNRAISABLEHOOK_METHODDEF
SYS_GET_INT_MAX_STR_DIGITS_METHODDEF
SYS_SET_INT_MAX_STR_DIGITS_METHODDEF
diff --git a/configure b/configure
index a058553480ca5a..d7153914fe7b5e 100755
--- a/configure
+++ b/configure
@@ -1123,6 +1123,7 @@ with_wheel_pkg_dir
with_readline
with_computed_gotos
with_tail_call_interp
+with_remote_debug
with_ensurepip
with_openssl
with_openssl_rpath
@@ -1932,6 +1933,7 @@ Optional Packages:
default on supported compilers)
--with-tail-call-interp enable tail-calling interpreter in evaluation loop
and rest of CPython
+ --with-remote-debug enable remote debugging support (default is yes)
--with-ensurepip[=install|upgrade|no]
"install" or "upgrade" using bundled pip (default is
upgrade)
@@ -29302,6 +29304,34 @@ esac
fi
+# Check for --with-remote-debug
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for --with-remote-debug" >&5
+printf %s "checking for --with-remote-debug... " >&6; }
+
+# Check whether --with-remote-debug was given.
+if test ${with_remote_debug+y}
+then :
+ withval=$with_remote_debug;
+else case e in #(
+ e) with_remote_debug=yes ;;
+esac
+fi
+
+
+if test "$with_remote_debug" = yes; then
+
+printf "%s\n" "#define Py_REMOTE_DEBUG 1" >>confdefs.h
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+else
+
+printf "%s\n" "#define Py_REMOTE_DEBUG 0" >>confdefs.h
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
case $ac_sys_system in
AIX*)
diff --git a/configure.ac b/configure.ac
index 23bd81ed4431b9..4e24930662c1f8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -7034,6 +7034,26 @@ fi
],
[AC_MSG_RESULT([no value specified])])
+# Check for --with-remote-debug
+AC_MSG_CHECKING([for --with-remote-debug])
+AC_ARG_WITH(
+ [remote-debug],
+ [AS_HELP_STRING(
+ [--with-remote-debug],
+ [enable remote debugging support (default is yes)])],
+ [],
+ [with_remote_debug=yes])
+
+if test "$with_remote_debug" = yes; then
+ AC_DEFINE([Py_REMOTE_DEBUG], [1],
+ [Define if you want to enable remote debugging support.])
+ AC_MSG_RESULT([yes])
+else
+ AC_DEFINE([Py_REMOTE_DEBUG], [0],
+ [Define if you want to enable remote debugging support.])
+ AC_MSG_RESULT([no])
+fi
+
case $ac_sys_system in
AIX*)
diff --git a/pyconfig.h.in b/pyconfig.h.in
index dbf7865447bc2e..aa086d49e90a5b 100644
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -1718,6 +1718,9 @@
/* Define if year with century should be normalized for strftime. */
#undef Py_NORMALIZE_CENTURY
+/* Define if you want to enable remote debugging support. */
+#undef Py_REMOTE_DEBUG
+
/* Define if rl_startup_hook takes arguments */
#undef Py_RL_STARTUP_HOOK_TAKES_ARGS