aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Python
diff options
context:
space:
mode:
authorPablo Galindo Salgado <Pablogsal@gmail.com>2025-04-03 16:20:01 +0100
committerGitHub <noreply@github.com>2025-04-03 16:20:01 +0100
commit943cc1431ebf4a265b79f69fa286787e77410fe9 (patch)
tree00f0c5bd86dc48bfd105c8a9aad56bee022b89c4 /Python
parent275056a7fdcbe36aaac494b4183ae59943a338eb (diff)
downloadcpython-943cc1431ebf4a265b79f69fa286787e77410fe9.tar.gz
cpython-943cc1431ebf4a265b79f69fa286787e77410fe9.zip
gh-131591: Implement PEP 768 (#131937)
Co-authored-by: Ivona Stojanovic <stojanovic.i@hotmail.com> Co-authored-by: Matt Wozniski <godlygeek@gmail.com>
Diffstat (limited to 'Python')
-rw-r--r--Python/ceval_gil.c95
-rw-r--r--Python/clinic/sysmodule.c.h100
-rw-r--r--Python/initconfig.c35
-rw-r--r--Python/remote_debugging.c984
-rw-r--r--Python/sysmodule.c116
5 files changed, 1329 insertions, 1 deletions
diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c
index 77632b283b2..a455779a659 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 d274b0c924c..8b73ccefc30 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 @@ exit:
#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 bc48ebd61f2..e398b1cbeaa 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;
@@ -1987,6 +1990,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)
{
int nframe;
@@ -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 00000000000..9b2297b5627
--- /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 1b2019a9f74..2a28fab2f51 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