From 58305c16263eeb541cc58cf0980c8716f8f0f5bb Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Wed, 26 Feb 2025 22:12:57 +0000 Subject: [PATCH] PEP 768: Add some clarifications and minor edits --- peps/pep-0768.rst | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/peps/pep-0768.rst b/peps/pep-0768.rst index 32cbe17ba5d..5c60d7b69f3 100644 --- a/peps/pep-0768.rst +++ b/peps/pep-0768.rst @@ -141,8 +141,10 @@ A new structure is added to PyThreadState to support remote debugging: This structure is appended to ``PyThreadState``, adding only a few fields that are **never accessed during normal execution**. The ``debugger_pending_call`` field -indicates when a debugger has requested execution, while ``debugger_script`` -provides Python code to be executed when the interpreter reaches a safe point. +indicates when a debugger has requested execution, while ``debugger_script_path`` +provides a filesystem path to a Python source file (.py) that will be executed when +the interpreter reaches a safe point. The path must point to a Python source file, +not compiled Python code (.pyc) or any other format. The value for ``MAX_SCRIPT_PATH_SIZE`` will be a trade-off between binary size and how big debugging scripts' paths can be. To limit the memory overhead per @@ -177,7 +179,7 @@ debugger support: These offsets allow debuggers to locate critical debugging control structures in the target process's memory space. The ``eval_breaker`` and ``remote_debugger_support`` offsets are relative to each ``PyThreadState``, while the ``debugger_pending_call`` -and ``debugger_script`` offsets are relative to each ``_PyRemoteDebuggerSupport`` +and ``debugger_script_path`` offsets are relative to each ``_PyRemoteDebuggerSupport`` structure, allowing the new structure and its fields to be found regardless of where they are in memory. ``debugger_script_path_size`` informs the attaching tool of the size of the buffer. @@ -200,13 +202,20 @@ When a debugger wants to attach to a Python process, it follows these steps: 5. Write control information: - - Write a filename containing Python code to be executed into the - ``debugger_script`` field in ``_PyRemoteDebuggerSupport``. + - Most debuggers will pause the process before writing to its memory. This is + standard practice for tools like GDB, which use SIGSTOP or ptrace to pause the process. + This approach prevents races when writing to process memory. Profilers and other tools + that don't wish to stop the process can still use this interface, but they need to + handle possible races, which is a normal consideration for profilers in general. + + - Write a file path to a Python source file (.py) into the + ``debugger_script_path`` field in ``_PyRemoteDebuggerSupport``. - Set ``debugger_pending_call`` flag in ``_PyRemoteDebuggerSupport`` to 1 + - Set ``debugger_pending_call`` flag in ``_PyRemoteDebuggerSupport`` - Set ``_PY_EVAL_PLEASE_STOP_BIT`` in the ``eval_breaker`` field -Once the interpreter reaches the next safe point, it will execute the script -provided by the debugger. +Once the interpreter reaches the next safe point, it will execute the Python code +contained in the file specified by the debugger. Interpreter Integration ----------------------- @@ -237,7 +246,7 @@ to be audited or disabled if desired by a system's administrator. if (tstate->eval_breaker) { if (tstate->remote_debugger_support.debugger_pending_call) { tstate->remote_debugger_support.debugger_pending_call = 0; - const char *path = tstate->remote_debugger_support.debugger_script; + const char *path = tstate->remote_debugger_support.debugger_script_path; if (*path) { if (0 != PySys_Audit("debugger_script", "%s", path)) { PyErr_Clear(); @@ -273,16 +282,17 @@ arbitrary Python code within the context of a specified Python process: .. code-block:: python - def remote_exec(pid: int, code: str, timeout: int = 0) -> None: + def remote_exec(pid: int, code: str) -> None: """ Executes a block of Python code in a given remote Python process. + This function returns immediately, and the code will be executed at the next + available opportunity in the target process, similar to how signals are handled. + There is no way to determine when or if the code has been executed. + Args: pid (int): The process ID of the target Python process. code (str): A string containing the Python code to be executed. - timeout (int): An optional timeout for waiting for the remote - process to execute the code. If the timeout is exceeded a - ``TimeoutError`` will be raised. """ An example usage of the API would look like: @@ -292,9 +302,7 @@ An example usage of the API would look like: import sys # Execute a print statement in a remote Python process with PID 12345 try: - sys.remote_exec(12345, "print('Hello from remote execution!')", timeout=3) - except TimeoutError: - print(f"The remote process took too long to execute the code") + sys.remote_exec(12345, "print('Hello from remote execution!')") except Exception as e: print(f"Failed to execute code: {e}") @@ -454,8 +462,8 @@ Rejected Ideas Writing Python code into the buffer ----------------------------------- -We have chosen to have debuggers write the code to be executed into a file -whose path is written into a buffer in the remote process. This has been deemed +We have chosen to have debuggers write the path to a file containing Python code +into a buffer in the remote process. This has been deemed more secure than writing the Python code to be executed itself into a buffer in the remote process, because it means that an attacker who has gained arbitrary writes in a process but not arbitrary code execution or file system