https://github.com/python/cpython/commit/943cc1431ebf4a265b79f69fa286787e77410fe9
commit: 943cc1431ebf4a265b79f69fa286787e77410fe9
branch: main
author: Pablo Galindo Salgado <pablog...@gmail.com>
committer: pablogsal <pablog...@gmail.com>
date: 2025-04-03T16:20:01+01:00
summary:

gh-131591: Implement PEP 768 (#131937)

Co-authored-by: Ivona Stojanovic <stojanovi...@hotmail.com>
Co-authored-by: Matt Wozniski <godlyg...@gmail.com>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-03-31-13-25-14.gh-issue-131591.DsPKZt.rst
A Python/remote_debugging.c
M Doc/library/sys.rst
M Doc/using/cmdline.rst
M Doc/using/configure.rst
M Doc/whatsnew/3.14.rst
M Include/cpython/initconfig.h
M Include/cpython/pystate.h
M Include/internal/pycore_ceval.h
M Include/internal/pycore_debug_offsets.h
M Include/internal/pycore_global_objects_fini_generated.h
M Include/internal/pycore_global_strings.h
M Include/internal/pycore_runtime_init_generated.h
M Include/internal/pycore_sysmodule.h
M Include/internal/pycore_unicodeobject_generated.h
M Lib/test/test_capi/test_config.py
M Lib/test/test_embed.py
M Lib/test/test_sys.py
M Makefile.pre.in
M PCbuild/_freeze_module.vcxproj
M PCbuild/_freeze_module.vcxproj.filters
M PCbuild/build.bat
M PCbuild/pythoncore.vcxproj
M PCbuild/pythoncore.vcxproj.filters
M Python/ceval_gil.c
M Python/clinic/sysmodule.c.h
M Python/initconfig.c
M Python/sysmodule.c
M configure
M configure.ac
M pyconfig.h.in

diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index bb88cf73ec1e89..477a4e71d18f9b 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 *script*, a file containing Python code 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:: next
+
+
 .. 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..d487d800960c3b 100644
--- a/Doc/using/cmdline.rst
+++ b/Doc/using/cmdline.rst
@@ -603,6 +603,17 @@ Miscellaneous options
 
      .. versionadded:: 3.13
 
+   * ``-X disable_remote_debug`` disables the remote debugging support as 
described
+     in :pep:`768`.  This includes both the functionality to schedule code for
+     execution in another process and the functionality to receive code for
+     execution in the current process.
+
+     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:: next
+
    * :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 +1171,16 @@ 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`. This includes both the 
functionality
+   to schedule code for execution in another process and the functionality to
+   receive code for execution in the current process.
+
+   See also the :option:`-X disable_remote_debug` command-line option.
 
+   .. versionadded:: next
 
 .. envvar:: PYTHON_CPU_COUNT
 
diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst
index 4d47cf945219dd..3bf53bc9f04102 100644
--- a/Doc/using/configure.rst
+++ b/Doc/using/configure.rst
@@ -660,6 +660,17 @@ 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. This includes both the functionality to schedule code to be 
executed
+   and the functionality to receive code to be executed.
+
+
+   .. versionadded:: next
+
 
 .. _debug-build:
 
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 108768de086bb2..a561d3b3383fc5 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -90,6 +90,63 @@ 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 allowing debuggers to forego unsafe alternatives.
+
+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 os
+     import sys
+     import tempfile
+
+     # Create a temporary script
+     with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as 
f:
+         script_path = f.name
+         f.write(f"import my_debugger; my_debugger.connect({os.getpid()})")
+     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 :option:`--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-pep758:
 
 PEP 758 – Allow except and except* expressions without parentheses
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..c562426767c2cb 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_ceval.h b/Include/internal/pycore_ceval.h
index 7d28d26d887e20..044403a6548b50 100644
--- a/Include/internal/pycore_ceval.h
+++ b/Include/internal/pycore_ceval.h
@@ -347,6 +347,18 @@ void _Py_unset_eval_breaker_bit_all(PyInterpreterState 
*interp, uintptr_t bit);
 
 PyAPI_FUNC(_PyStackRef) _PyFloat_FromDouble_ConsumeInputs(_PyStackRef left, 
_PyStackRef right, double value);
 
+#ifndef Py_SUPPORTS_REMOTE_DEBUG
+    #if defined(__APPLE__)
+    #  if !defined(TARGET_OS_OSX)
+// Older macOS SDKs do not define TARGET_OS_OSX
+    #     define TARGET_OS_OSX 1
+    #  endif
+    #endif
+    #if ((defined(__APPLE__) && TARGET_OS_OSX) || defined(MS_WINDOWS) || 
(defined(__linux__) && HAVE_PROCESS_VM_READV))
+    #    define Py_SUPPORTS_REMOTE_DEBUG 1
+    #endif
+#endif
+
 #ifdef __cplusplus
 }
 #endif
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_capi/test_config.py 
b/Lib/test/test_capi/test_config.py
index 8dde2ac1e00d89..060a764a4d37d8 100644
--- a/Lib/test/test_capi/test_config.py
+++ b/Lib/test/test_capi/test_config.py
@@ -73,6 +73,7 @@ def test_config_get(self):
             ("program_name", str, None),
             ("pycache_prefix", str | None, "pycache_prefix"),
             ("quiet", bool, None),
+            ("remote_debug", int, None),
             ("run_command", str | None, None),
             ("run_filename", str | None, None),
             ("run_module", str | None, None),
diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py
index fdece755a740de..f3e182d0b17a9c 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -626,6 +626,7 @@ class InitConfigTests(EmbeddingTestsMixin, 
unittest.TestCase):
         'write_bytecode': True,
         'verbose': 0,
         'quiet': False,
+        'remote_debug': True,
         'user_site_directory': True,
         'configure_c_stdio': False,
         'buffered_stdio': True,
@@ -975,7 +976,7 @@ def test_init_global_config(self):
             'verbose': True,
             'quiet': True,
             'buffered_stdio': False,
-
+            'remote_debug': True,
             'user_site_directory': False,
             'pathconfig_warnings': False,
         }
@@ -1031,6 +1032,7 @@ def test_init_from_config(self):
             'write_bytecode': False,
             'verbose': 1,
             'quiet': True,
+            'remote_debug': True,
             'configure_c_stdio': True,
             'buffered_stdio': False,
             'user_site_directory': False,
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 368a5ba413b6ce..4843a9e11931b1 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -7,17 +7,22 @@
 import operator
 import os
 import random
+import socket
 import struct
 import subprocess
 import sys
 import sysconfig
 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,235 @@ 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, Windows and MacOS")
+@unittest.skipIf(sys.platform == "linux" and not _supports_remote_attaching(),
+                    "Test only runs on Linux with process_vm_readv support")
+@test.support.cpython_only
+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)
+
+        port = find_unused_port()
+
+        with open(target, 'w') as f:
+            f.write(f'''
+import sys
+import time
+import socket
+
+# Connect to the test process
+sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+sock.connect(('localhost', {port}))
+
+{prologue}
+
+# Signal that the process is ready
+sock.sendall(b"ready")
+
+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)
+
+# Do a bunch of work to give the remote script time to run
+x = 0
+for i in range(100):
+    x += i
+
+# 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)
+
+        # Create a socket server to communicate with the target process
+        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        server_socket.bind(('localhost', port))
+        server_socket.settimeout(SHORT_TIMEOUT)
+        server_socket.listen(1)
+
+        with subprocess.Popen(cmd,
+                              stdout=subprocess.PIPE,
+                              stderr=subprocess.PIPE,
+                              env=env,
+                              ) as proc:
+            client_socket = None
+            try:
+                # Accept connection from target process
+                client_socket, _ = server_socket.accept()
+                server_socket.close()
+
+                response = client_socket.recv(1024)
+                self.assertEqual(response, b"ready")
+
+                # Try remote exec on the target process
+                sys.remote_exec(proc.pid, script)
+
+                # Signal script to continue
+                client_socket.sendall(b"continue")
+
+                # Wait for execution confirmation
+                response = client_socket.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:
+                if client_socket is not None:
+                    client_socket.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")
+
+    def test_remote_exec_in_process_without_debug_fails_envvar(self):
+        """Test remote exec in a process without remote debugging enabled"""
+        script = os_helper.TESTFN + '_remote.py'
+        self.addCleanup(os_helper.unlink, script)
+        with open(script, 'w') as f:
+            f.write('print("Remote script executed successfully!")')
+        env = os.environ.copy()
+        env['PYTHON_DISABLE_REMOTE_DEBUG'] = '1'
+
+        _, out, err = assert_python_failure('-c', f'import os, sys; 
sys.remote_exec(os.getpid(), "{script}")', **env)
+        self.assertIn(b"Remote debugging is not enabled", err)
+        self.assertEqual(out, b"")
+
+    def test_remote_exec_in_process_without_debug_fails_xoption(self):
+        """Test remote exec in a process without remote debugging enabled"""
+        script = os_helper.TESTFN + '_remote.py'
+        self.addCleanup(os_helper.unlink, script)
+        with open(script, 'w') as f:
+            f.write('print("Remote script executed successfully!")')
+
+        _, out, err = assert_python_failure('-Xdisable-remote-debug', '-c', 
f'import os, sys; sys.remote_exec(os.getpid(), "{script}")')
+        self.assertIn(b"Remote debugging is not enabled", err)
+        self.assertEqual(out, b"")
+
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 2102f9cfe882bd..3ef0c6320c85db 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..6530f167d583a3
--- /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
+schedules the execution of a Python file in a separate process. Patch by
+Pablo Galindo, 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 @@
     <ClCompile Include="..\Python\Python-tokenize.c" />
     <ClCompile Include="..\Python\pytime.c" />
     <ClCompile Include="..\Python\qsbr.c" />
+    <ClCompile Include="..\Python\remote_debugging.c" />
     <ClCompile Include="..\Python\specialize.c" />
     <ClCompile Include="..\Python\structmember.c" />
     <ClCompile Include="..\Python\suggestions.c" />
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 @@
     <ClCompile Include="..\Objects\sliceobject.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\remote_debugging.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Python\specialize.c">
       <Filter>Source Files</Filter>
     </ClCompile>
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 @@
       <PreprocessorDefinitions Condition="'$(UseTIER2)' != '' and 
'$(UseTIER2)' != 
'0'">_Py_TIER2=$(UseTIER2);%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <PreprocessorDefinitions Condition="'$(UseTailCallInterp)' == 
'true'">Py_TAIL_CALL_INTERP=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <PreprocessorDefinitions Condition="'$(WITH_COMPUTED_GOTOS)' != 
''">HAVE_COMPUTED_GOTOS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions Condition="'$(DisableRemoteDebug)' != 
'true'">Py_REMOTE_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
     <Link>
       
<AdditionalDependencies>version.lib;ws2_32.lib;pathcch.lib;bcrypt.lib;%(AdditionalDependencies)</AdditionalDependencies>
@@ -640,6 +641,7 @@
     <ClCompile Include="..\Python\pystrcmp.c" />
     <ClCompile Include="..\Python\pystrhex.c" />
     <ClCompile Include="..\Python\pystrtod.c" />
+    <ClCompile Include="..\Python\remote_debugging.c" />
     <ClCompile Include="..\Python\qsbr.c" />
     <ClCompile Include="..\Python\dtoa.c" />
     <ClCompile Include="..\Python\Python-ast.c" />
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 @@
     <ClCompile Include="..\Python\pystrtod.c">
       <Filter>Python</Filter>
     </ClCompile>
+    <ClCompile Include="..\Python\remote_debugging.c">
+      <Filter>Python</Filter>
+    </ClCompile>
     <ClCompile Include="..\Python\qsbr.c">
       <Filter>Python</Filter>
     </ClCompile>
diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c
index 77632b283b256c..a455779a6598e9 100644
--- a/Python/ceval_gil.c
+++ b/Python/ceval_gil.c
@@ -1192,6 +1192,71 @@ _PyEval_DisableGIL(PyThreadState *tstate)
 }
 #endif
 
+#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_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 {
+        int dup_fd = -1;
+        FILE *f = NULL;
+
+#ifdef MS_WINDOWS
+        dup_fd = _dup(fd);
+        if (dup_fd != -1) {
+            f = _fdopen(dup_fd, "r");
+        }
+        if (!f) {
+            _close(dup_fd);
+        }
+#else
+        dup_fd = dup(fd);
+        if (dup_fd != -1) {
+            f = fdopen(dup_fd, "r");
+        }
+        if (!f) {
+            close(dup_fd);
+        }
+#endif
+        if (!f) {
+            PyErr_SetFromErrno(PyExc_OSError);
+        }
+        else {
+            PyRun_AnyFileEx(f, path, 1);
+        }
+
+        if (PyErr_Occurred()) {
+            PyErr_FormatUnraisable("Error executing debugger script %s", path);
+        }
+    }
+
+    PyObject* res = PyObject_CallMethodNoArgs(fileobj, &_Py_ID(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 +1384,35 @@ _Py_HandlePending(PyThreadState *tstate)
             return -1;
         }
     }
+
+#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_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..9b2297b5627aa3
--- /dev/null
+++ b/Python/remote_debugging.c
@@ -0,0 +1,984 @@
+#define _GNU_SOURCE
+#include "pyconfig.h"
+
+#include "Python.h"
+#include "internal/pycore_runtime.h"
+#include "internal/pycore_ceval.h"
+
+#ifdef __linux__
+#    include <elf.h>
+#    include <sys/uio.h>
+#    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 <sys/mman.h>
+#endif
+
+#if defined(__APPLE__)
+#  include <TargetConditionals.h>
+// 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 <libproc.h>
+#    include <mach-o/fat.h>
+#    include <mach-o/loader.h>
+#    include <mach-o/nlist.h>
+#    include <mach/mach.h>
+#    include <mach/mach_vm.h>
+#    include <mach/machine.h>
+#    include <sys/mman.h>
+#    include <sys/proc.h>
+#    include <sys/sysctl.h>
+#  endif
+#endif
+
+#ifdef MS_WINDOWS
+    // Windows includes and definitions
+#include <windows.h>
+#include <psapi.h>
+#include <tlhelp32.h>
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifndef MS_WINDOWS
+#include <sys/param.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
+#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;
+}
+
+#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
+
+#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)&region_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)
+
+#if defined(__linux__) && HAVE_PROCESS_VM_READV
+static uintptr_t
+search_elf_file_for_section(
+        proc_handle_t *handle,
+        const char* secname,
+        uintptr_t start_address,
+        const char *elf_file)
+{
+    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 = &section_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 = &section_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 (file_memory != NULL) {
+        munmap(file_memory, file_stats.st_size);
+    }
+    if (fd >= 0 && close(fd) != 0) {
+        PyErr_SetFromErrno(PyExc_OSError);
+    }
+    return result;
+}
+
+static uintptr_t
+search_linux_map_for_section(proc_handle_t *handle, const char* secname, const 
char* substr)
+{
+    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;
+    }
+
+    size_t linelen = 0;
+    size_t linesz = PATH_MAX;
+    char *line = PyMem_Malloc(linesz);
+    if (!line) {
+        fclose(maps_file);
+        PyErr_NoMemory();
+        return 0;
+    }
+
+    uintptr_t retval = 0;
+    while (fgets(line + linelen, linesz - linelen, maps_file) != NULL) {
+        linelen = strlen(line);
+        if (line[linelen - 1] != '\n') {
+            // Read a partial line: realloc and keep reading where we left off.
+            // Note that even the last line will be terminated by a newline.
+            linesz *= 2;
+            char *biggerline = PyMem_Realloc(line, linesz);
+            if (!biggerline) {
+                PyMem_Free(line);
+                fclose(maps_file);
+                PyErr_NoMemory();
+                return 0;
+            }
+            line = biggerline;
+            continue;
+        }
+
+        // Read a full line: strip the newline
+        line[linelen - 1] = '\0';
+        // and prepare to read the next line into the start of the buffer.
+        linelen = 0;
+
+        unsigned long start = 0;
+        unsigned long path_pos = 0;
+        sscanf(line, "%lx-%*x %*s %*s %*s %*s %ln", &start, &path_pos);
+
+        if (!path_pos) {
+            // Line didn't match our format string.  This shouldn't be
+            // possible, but let's be defensive and skip the line.
+            continue;
+        }
+
+        const char *path = line + path_pos;
+        const char *filename = strrchr(path, '/');
+        if (filename) {
+            filename++;  // Move past the '/'
+        } else {
+            filename = path;  // No directories, or an empty string
+        }
+
+        if (strstr(filename, substr)) {
+            retval = search_elf_file_for_section(handle, secname, start, path);
+            if (retval) {
+                break;
+            }
+        }
+    }
+
+    PyMem_Free(line);
+    fclose(maps_file);
+
+    return retval;
+}
+
+
+#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;
+
+#ifdef MS_WINDOWS
+    // 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.");
+    }
+#elif defined(__linux__)
+    // On Linux, search for 'python' in executable or DLL
+    address = search_linux_map_for_section(handle, "PyRuntime", "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.");
+    }
+#else
+    // On macOS, 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");
+    }
+#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
+    Py_UNREACHABLE();
+#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_Format(PyExc_RuntimeError, "Unknown error writing memory: 
%d", (int)kr);
+        }
+        return -1;
+    }
+    return 0;
+#else
+    Py_UNREACHABLE();
+#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 = 
(uintptr_t)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(this_tid),
+                    &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;
+        }
+    }
+
+    // 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 = (uintptr_t)(
+        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 = (uintptr_t)(
+        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;
+    }
+
+    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;
+
+    if (0 != write_memory(
+            handle,
+            thread_state_addr + 
(uintptr_t)debug_offsets.debugger_support.eval_breaker,
+            sizeof(uintptr_t),
+            &eval_breaker))
+
+    {
+        return -1;
+    }
+
+    return 0;
+}
+
+#endif // defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
+
+int
+_PySysRemoteDebug_SendExec(int pid, int tid, const char *debugger_script_path)
+{
+#if !defined(Py_SUPPORTS_REMOTE_DEBUG)
+    PyErr_SetString(PyExc_RuntimeError, "Remote debugging is not supported on 
this platform");
+    return -1;
+#elif !defined(Py_REMOTE_DEBUG)
+    PyErr_SetString(PyExc_RuntimeError, "Remote debugging support has not been 
compiled in");
+    return -1;
+#else
+
+    PyThreadState *tstate = _PyThreadState_GET();
+    const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
+    if (config->remote_debug != 1) {
+        PyErr_SetString(PyExc_RuntimeError, "Remote debugging is not enabled");
+        return -1;
+    }
+
+    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;
+#endif
+}
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index 1b2019a9f74d42..2a28fab2f51ea3 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -2421,6 +2421,120 @@ 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 *path;
+    if (PyUnicode_FSDecoder(script, &path)) {
+        ret = sys_remote_exec_unicode_path(module, pid, path);
+        Py_DECREF(path);
+    }
+    return ret;
+}
+
+
+
 /*[clinic input]
 sys._dump_tracelets
 
@@ -2695,6 +2809,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
 

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to