Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 768 – Sichere externe Debugger-Schnittstelle für CPython

Autor:
Pablo Galindo Salgado <pablogsal at python.org>, Matt Wozniski <godlygeek at gmail.com>, Ivona Stojanovic <stojanovic.i at hotmail.com>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Erstellt:
25-Nov-2024
Python-Version:
3.14
Post-History:
11-Dez-2024
Resolution:
17-Mär-2025

Inhaltsverzeichnis

Wichtig

Diese PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie nun unter Remote-Debugging-Anhangsprotokoll.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Diese PEP schlägt die Hinzufügung einer Zero-Overhead-Debugging-Schnittstelle zu CPython vor, die es Debuggern und Profilern ermöglicht, sich sicher an laufende Python-Prozesse anzuhängen. Die Schnittstelle bietet sichere Ausführungspunkte für das Anhängen von Debugger-Code, ohne den normalen Ausführungspfad des Interpreters zu verändern oder Laufzeit-Overhead hinzuzufügen.

Eine Schlüsselanwendung dieser Schnittstelle wird darin bestehen, pdb die Möglichkeit zu geben, sich an Live-Prozesse über die Prozess-ID anzuhängen, ähnlich wie gdb -p, was es Entwicklern ermöglicht, Python-Anwendungen interaktiv in Echtzeit zu inspizieren und zu debuggen, ohne sie zu stoppen oder neu zu starten.

Motivation

Das Debuggen von Python-Prozessen in Produktions- und Live-Umgebungen birgt einzigartige Herausforderungen. Entwickler müssen oft das Verhalten von Anwendungen analysieren, ohne Dienste zu stoppen oder neu zu starten, was für Hochverfügbarkeitssysteme besonders entscheidend ist. Typische Szenarien umfassen die Diagnose von Deadlocks, die Inspektion des Speicherverbrauchs oder die Untersuchung unerwarteten Verhaltens in Echtzeit.

Nur sehr wenige Python-Tools können sich an laufende Prozesse anhängen, hauptsächlich weil dies tiefgreifende Kenntnisse sowohl der Betriebssystem-Debugging-Schnittstellen als auch der CPython-Interna erfordert. Während C/C++-Debugger wie GDB und LLDB sich über gut verstandene Techniken an Prozesse anhängen können, müssen Python-Tools all diese Low-Level-Mechanismen implementieren und zusätzliche Komplexität handhaben. Zum Beispiel, wenn GDB Code in einem Zielprozess ausführen muss, dann

  1. Verwendet ptrace, um einen kleinen Block ausführbaren Speichers zu allokieren (leichter gesagt als getan)
  2. Schreibt eine kleine Sequenz von Maschinencode – typischerweise ein Funktions-Prolog, die gewünschten Instruktionen und Code zur Wiederherstellung von Registern
  3. Speichert alle Register des Zielthreads
  4. Ändert den Instruktionszeiger auf den injizierten Code
  5. Lässt den Prozess laufen, bis er auf einen Breakpoint am Ende des injizierten Codes trifft
  6. Stellt die ursprünglichen Register wieder her und fährt mit der Ausführung fort

Python-Tools stehen vor der gleichen Herausforderung der Code-Injektion, jedoch mit einer zusätzlichen Komplexitätsebene. Sie müssen nicht nur den obigen Mechanismus implementieren, sondern auch den Laufzeitstatus von CPython verstehen und sicher damit interagieren, einschließlich der Interpreter-Schleife, des Garbage Collectors, des Thread-Zustands und des Referenzzählsystems. Diese Kombination aus Low-Level-Systemmanipulation und tiefem domänenspezifischem Interpreterwissen macht die Implementierung von Python-Debugging-Tools außergewöhnlich schwierig.

Die wenigen Tools (siehe z.B. DebugPy und Memray), die dies versuchen, greifen zu suboptimalen und unsicheren Methoden, indem sie Systemdebugger wie GDB und LLDB verwenden, um Code zwangsweise zu injizieren. Dieser Ansatz ist grundsätzlich unsicher, da der injizierte Code zu jedem Zeitpunkt während des Ausführungszyklus des Interpreters ausgeführt werden kann – sogar während kritischer Operationen wie Speicherallokation, Garbage Collection oder Thread-Zustandsverwaltung. Wenn dies geschieht, sind die Ergebnisse katastrophal: Der Versuch, Speicher zu allokieren, während man sich bereits in malloc() befindet, verursacht Abstürze, die Modifizierung von Objekten während der Garbage Collection beschädigt den Zustand des Interpreters, und das falsche Ansprechen des Thread-Zustands führt zu Deadlocks.

Verschiedene Tools versuchen, diese Risiken durch komplexe Workarounds zu minimieren, wie z.B. das Starten separater Threads für injizierten Code oder das sorgfältige Timing ihrer Operationen oder der Versuch, einige gute Punkte zum Anhalten des Prozesses auszuwählen. Diese Minderungsmaßnahmen können das zugrunde liegende Problem jedoch nicht vollständig lösen: Ohne die Kooperation des Interpreters gibt es keine Möglichkeit zu wissen, ob es sicher ist, Code zu irgendeinem Zeitpunkt auszuführen. Selbst sorgfältig implementierte Tools können den Interpreter zum Absturz bringen, da sie grundlegend gegen ihn arbeiten und nicht mit ihm.

Begründung

Anstatt Tools zu zwingen, mit unsicherer Code-Injektion Einschränkungen des Interpreters zu umgehen, können wir CPython um eine ordnungsgemäße Debugging-Schnittstelle erweitern, die eine sichere Ausführung garantiert. Durch die Hinzufügung einiger Felder für den Thread-Zustand und die Integration in die bestehende Auswertungs-Schleife des Interpreters können wir sicherstellen, dass Debugging-Operationen nur an gut definierten sicheren Punkten stattfinden. Dies eliminiert die Möglichkeit von Abstürzen und Beschädigungen und erhält gleichzeitig den Zero-Overhead während der normalen Ausführung.

Die Kernidee ist, dass wir Code nicht an beliebigen Punkten injizieren müssen – wir müssen dem Interpreter lediglich signalisieren, dass wir Code zur nächsten sicheren Gelegenheit ausführen möchten. Dieser Ansatz funktioniert im Einklang mit dem natürlichen Ausführungsfluss des Interpreters anstatt dagegen zu kämpfen.

Nachdem diese Idee dem PyPy-Entwicklungsteam vorgestellt wurde, wurde dieser Vorschlag bereits in PyPy implementiert, was sowohl seine Machbarkeit als auch seine Effektivität beweist. Ihre Implementierung zeigt, dass wir sichere Debugging-Funktionen mit Null-Laufzeit-Overhead während der normalen Ausführung bereitstellen können. Der vorgeschlagene Mechanismus reduziert nicht nur die Risiken, die mit aktuellen Debugging-Ansätzen verbunden sind, sondern legt auch den Grundstein für zukünftige Verbesserungen. Beispielsweise könnte dieses Framework die Integration mit gängigen Observability-Tools ermöglichen und Echtzeit-Einblicke in die Interpreter-Leistung oder den Speicherverbrauch liefern. Ein überzeugendes Anwendungsbeispiel für diese Schnittstelle ist die Ermöglichung, dass pdb sich an laufende Python-Prozesse anhängt, ähnlich wie gdb Benutzern erlaubt, sich über die Prozess-ID an ein Programm anzuhängen (gdb -p <pid>). Mit dieser Funktion könnten Entwickler den Zustand einer laufenden Anwendung inspizieren, Ausdrücke auswerten und dynamisch durch den Code steppen. Dieser Ansatz würde die Debugging-Fähigkeiten von Python mit denen anderer großer Programmiersprachen und Debugging-Tools, die diesen Modus unterstützen, in Einklang bringen.

Spezifikation

Dieser Vorschlag führt einen sicheren Debugging-Mechanismus ein, der es externen Prozessen ermöglicht, die Codeausführung in einem Python-Interpreter an klar definierten sicheren Punkten auszulösen. Die Kernidee ist, dass wir anstatt Code direkt über System-Debugger zu injizieren, die bestehende Auswertungs-Schleife und den Thread-Zustand des Interpreters nutzen können, um Debugging-Operationen zu koordinieren.

Der Mechanismus funktioniert so, dass Debugger in bestimmte Speicherbereiche im Zielprozess schreiben, die der Interpreter während seiner normalen Ausführung überprüft. Wenn der Interpreter feststellt, dass ein Debugger angehängt werden möchte, führt er die angeforderten Operationen nur dann aus, wenn dies sicher ist – das heißt, wenn keine internen Sperren gehalten werden und alle Datenstrukturen in einem konsistenten Zustand sind.

Laufzeitstatus-Erweiterungen

Eine neue Struktur wird zu PyThreadState hinzugefügt, um Remote-Debugging zu unterstützen

typedef struct {
    int debugger_pending_call;
    char debugger_script_path[...];
} _PyRemoteDebuggerSupport;

Diese Struktur wird an PyThreadState angehängt und fügt nur wenige Felder hinzu, die **während der normalen Ausführung niemals abgerufen werden**. Das Feld debugger_pending_call zeigt an, wann ein Debugger eine Ausführung angefordert hat, während debugger_script_path einen Dateisystempfad zu einer Python-Quelldatei (.py) bereitstellt, die ausgeführt wird, wenn der Interpreter einen sicheren Punkt erreicht. Der Pfad muss auf eine Python-Quelldatei zeigen, nicht auf kompilierten Python-Code (.pyc) oder ein anderes Format.

Die Größe von debugger_script_path wird ein Kompromiss zwischen Binärgröße und der maximalen Länge von Pfaden für Debugging-Skripte sein. Um den Speicher-Overhead pro Thread zu begrenzen, werden wir dies auf 512 Bytes beschränken. Dieser Wert wird auch als Teil der Debugger-Unterstützungsstruktur bereitgestellt, damit Debugger wissen, wie viel sie schreiben können. Dieser Wert kann in Zukunft erweitert werden, falls wir ihn jemals benötigen.

Debug-Offsets-Tabelle

Python 3.12 führte eine Tabelle mit Debug-Offsets am Anfang der PyRuntime-Struktur ein. Dieser Abschnitt enthält die Struktur _Py_DebugOffsets, die es externen Werkzeugen ermöglicht, kritische Laufzeitstrukturen zuverlässig zu finden, unabhängig von ASLR oder der Art und Weise, wie Python kompiliert wurde.

Dieser Vorschlag erweitert die bestehende Tabelle mit Debug-Offsets um neue Felder für die Debugger-Unterstützung

struct _debugger_support {
    uint64_t eval_breaker;              // Location of the eval breaker flag
    uint64_t remote_debugger_support;   // Offset to our support structure
    uint64_t debugger_pending_call;     // Where to write the pending flag
    uint64_t debugger_script_path;      // Where to write the script path
    uint64_t debugger_script_path_size; // Size of the script path buffer
} debugger_support;

Diese Offsets ermöglichen es Debuggern, kritische Debugging-Kontrollstrukturen im Speicherbereich des Zielprozesses zu lokalisieren. Die Offsets eval_breaker und remote_debugger_support sind relativ zu jedem PyThreadState, während die Offsets debugger_pending_call und debugger_script_path relativ zu jeder _PyRemoteDebuggerSupport-Struktur sind. Dies ermöglicht es, die neue Struktur und ihre Felder unabhängig von ihrem Speicherort zu finden. debugger_script_path_size informiert das anhangende Tool über die Größe des Puffers.

Anhangsprotokoll

Wenn ein Debugger sich an einen Python-Prozess anhängen möchte, folgt er diesen Schritten

  1. Lokalisieren der PyRuntime-Struktur im Prozess
    • Finden der Python-Binärdatei (Executable oder libpython) im Prozessspeicher (OS-abhängiger Prozess)
    • Extrahieren des Offsets des Abschnitts .PyRuntime aus dem Format der Binärdatei (ELF/Mach-O/PE)
    • Berechnen der tatsächlichen Adresse von PyRuntime im laufenden Prozess durch Relokation des Offsets zur Ladeadresse der Binärdatei
  2. Zugriff auf Debug-Offset-Informationen durch Lesen von _Py_DebugOffsets am Anfang der PyRuntime-Struktur.
  3. Verwenden der Offsets, um den gewünschten Thread-Zustand zu lokalisieren
  4. Verwenden der Offsets, um die Debugger-Schnittstellenfelder innerhalb dieses Thread-Zustands zu lokalisieren
  5. Schreiben von Steuerinformationen
    • Die meisten Debugger pausieren den Prozess, bevor sie in seinen Speicher schreiben. Dies ist eine gängige Praxis für Tools wie GDB, die SIGSTOP oder ptrace verwenden, um den Prozess anzuhalten. Dieser Ansatz verhindert Race Conditions beim Schreiben in den Prozessspeicher. Profiler und andere Tools, die den Prozess nicht anhalten möchten, können diese Schnittstelle trotzdem verwenden, müssen aber mögliche Race Conditions handhaben. Dies ist eine normale Überlegung für Profiler.
    • Schreiben eines Dateipfads zu einer Python-Quelldatei (.py) in das Feld debugger_script_path in _PyRemoteDebuggerSupport.
    • Setzen des Flags debugger_pending_call in _PyRemoteDebuggerSupport auf 1
    • Setzen des Bits _PY_EVAL_PLEASE_STOP_BIT im Feld eval_breaker

Sobald der Interpreter den nächsten sicheren Punkt erreicht, wird der Python-Code aus der vom Debugger angegebenen Datei ausgeführt.

Interpreter-Integration

Die reguläre Auswertungs-Schleife des Interpreters enthält bereits eine Prüfung des Flags eval_breaker zur Behandlung von Signalen, periodischen Aufgaben und anderen Unterbrechungen. Wir nutzen diesen bestehenden Mechanismus, indem wir nur dann nach ausstehenden Debugger-Aufrufen suchen, wenn eval_breaker gesetzt ist, was einen Zero-Overhead während der normalen Ausführung gewährleistet. Diese Prüfung hat keinen Overhead. Tatsächlich zeigt das Profiling mit dem Linux-Tool perf, dass dieser Zweig hochgradig vorhersagbar ist – die Prüfung von debugger_pending_call wird während der normalen Ausführung nie genommen, was modernen CPUs ermöglicht, sie effektiv zu überspringen.

Wenn ein Debugger sowohl das Flag eval_breaker als auch debugger_pending_call gesetzt hat, führt der Interpreter den bereitgestellten Debugging-Code am nächsten sicheren Punkt aus. Dies geschieht alles in einem vollständig sicheren Kontext, da garantiert ist, dass der Interpreter in einem konsistenten Zustand ist, wann immer der Eval-Breaker überprüft wird.

Die einzigen gültigen Werte für debugger_pending_call werden anfänglich 0 und 1 sein; andere Werte sind für zukünftige Verwendungen reserviert.

Vor der Ausführung des Codes wird ein Audit-Ereignis ausgelöst, das es ermöglicht, diesen Mechanismus zu überprüfen oder ihn bei Bedarf durch den Administrator eines Systems zu deaktivieren.

// In ceval.c
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_path;
        if (*path) {
            if (0 != PySys_Audit("debugger_script", "%s", path)) {
                PyErr_Clear();
            } else {
                FILE* f = fopen(path, "r");
                if (!f) {
                    PyErr_SetFromErrno(OSError);
                } else {
                    PyRun_AnyFile(f, path);
                    fclose(f);
                }
                if (PyErr_Occurred()) {
                    PyErr_WriteUnraisable(...);
                }
            }
        }
    }
}

Wenn der ausgeführte Code eine Python-Ausnahme auslöst, wird diese als nicht abfangbare Ausnahme im Thread behandelt, in dem der Code ausgeführt wurde.

Python API

Um die sichere Ausführung von Python-Code in einem Remote-Prozess zu unterstützen, ohne all diese Schritte in jedem Tool neu implementieren zu müssen, erweitert dieser Vorschlag das Modul sys um eine neue Funktion. Diese Funktion ermöglicht es Debuggern oder externen Tools, beliebigen Python-Code im Kontext eines bestimmten Python-Prozesses auszuführen.

def remote_exec(pid: int, script: str|bytes|PathLike) -> None:
    """
    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.

    Args:
         pid (int): The process ID of the target Python process.
         script (str|bytes|PathLike): The path to a file containing
             the Python code to be executed.
    """

Ein Beispiel für die Verwendung der API würde wie folgt aussehen:

import sys
import uuid
# Execute a print statement in a remote Python process with PID 12345
script = f"/tmp/{uuid.uuid4()}.py"
with open(script, "w") as f:
    f.write("print('Hello from remote execution!')")
try:
    sys.remote_exec(12345, script)
except Exception as e:
    print(f"Failed to execute code: {e}")

Konfigurations-API

Um Verteilern, Systemadministratoren oder Benutzern die Deaktivierung dieses Mechanismus zu ermöglichen, werden mehrere Methoden bereitgestellt, um das Verhalten des Interpreters zu steuern.

Eine neue Umgebungsvariable PYTHON_DISABLE_REMOTE_DEBUG wird bereitgestellt, um das Verhalten zur Laufzeit zu steuern. Wenn sie auf einen beliebigen Wert gesetzt ist (auch einen leeren String), ignoriert der Interpreter alle Versuche, sich über diesen Mechanismus an einen Debugger anzuhängen.

Diese Umgebungsvariable wird zusammen mit einem neuen Flag -X disable-remote-debug zum Python-Interpreter hinzugefügt, damit Benutzer diese Funktion zur Laufzeit deaktivieren können.

Zusätzlich wird ein neues Flag --without-remote-debug zum configure-Skript hinzugefügt, damit Verteilungspartner Python ohne Unterstützung für Remote-Debugging erstellen können, wenn sie dies wünschen.

Ein neues Flag, das den Status des Remote-Debuggings anzeigt, wird über die Debug-Offsets verfügbar gemacht, damit Tools abfragen können, ob ein Remote-Prozess die Funktion deaktiviert hat. Auf diese Weise können Tools eine nützliche Fehlermeldung anbieten, die erklärt, warum sie nicht funktionieren, anstatt zu glauben, dass sie sich angehängt haben und ihr Skript nie ausgeführt wird.

Multithreading-Überlegungen

Das gesamte Ausführungsmuster ähnelt der internen Signalbehandlung von Python. Der Interpreter garantiert, dass injizierter Code nur an sicheren Punkten ausgeführt wird und niemals atomare Operationen innerhalb des Interpreters selbst unterbricht. Dieser Ansatz stellt sicher, dass Debugging-Operationen den Interpreter-Zustand nicht beschädigen können, während sie in den meisten realen Szenarien dennoch rechtzeitig ausgeführt werden.

Allerdings kann der durch diese Schnittstelle injizierte Debugging-Code in jedem Thread ausgeführt werden. Dieses Verhalten unterscheidet sich von der Signalbehandlung in Python, da Signal-Handler nur im Haupt-Thread ausgeführt werden können. Wenn ein Debugger Code in jeden laufenden Thread injizieren möchte, muss er ihn in jeden PyThreadState injizieren. Wenn ein Debugger Code im ersten verfügbaren Thread ausführen möchte, muss er ihn in jeden PyThreadState injizieren, und dieser injizierte Code muss prüfen, ob er bereits von einem anderen Thread ausgeführt wurde (wahrscheinlich durch Setzen eines Flags in den Globals eines Moduls).

Beachten Sie, dass der Global Interpreter Lock (GIL) die Ausführung wie gewohnt regelt, wenn der injizierte Code ausgeführt wird. Das bedeutet, wenn ein Ziel-Thread gerade eine C-Erweiterung ausführt, die den GIL kontinuierlich hält, kann der injizierte Code erst ausgeführt werden, wenn diese Operation abgeschlossen ist und der GIL verfügbar wird. Die Schnittstelle führt jedoch keine zusätzliche GIL-Konfliktgefahr ein, über das hinaus, was der injizierte Code selbst erfordert. Wichtig ist, dass die Schnittstelle vollständig kompatibel mit Pythons Free-Threading-Modus bleibt.

Es kann für einen Debugger, der Code zur Ausführung injiziert hat, nützlich sein, dies durch Senden eines vorregistrierten Signals an den Prozess zu verfolgen, das blockierende E/A- oder Schlafzustände, die auf externe Ressourcen warten, unterbrechen und eine sichere Gelegenheit zur Ausführung des injizierten Codes ermöglichen kann.

Abwärtskompatibilität

Diese Änderung hat keine Auswirkungen auf bestehenden Python-Code oder die Interpreter-Leistung. Die hinzugefügten Felder werden nur während des Debugger-Anhangs abgerufen, und der Prüfmechanismus nutzt bestehende Interpreter-Sicherheitspunkte.

Sicherheitsimplikationen

Diese Schnittstelle führt keine neuen Sicherheitsbedenken ein, da sie nur von Prozessen nutzbar ist, die bereits beliebigen Speicher innerhalb eines gegebenen Prozesses schreiben und beliebigen Code auf der Maschine ausführen können (um die Datei mit dem auszuführenden Python-Code zu erstellen).

Darüber hinaus wird die Ausführung des Codes durch die Audit-Hooks des Interpreters gesteuert, die verwendet werden können, um die Ausführung des Codes in sensiblen Umgebungen zu überwachen oder zu verhindern.

Bestehende Betriebssystem-Sicherheitsmechanismen sind wirksam zum Schutz vor Angreifern, die Zugriff auf beliebige Speicherbereiche erlangen. Obwohl die PEP nicht spezifiziert, wie der Speicher in den Zielprozess geschrieben werden soll, wird dies in der Praxis über Standard-Systemaufrufe erfolgen, die bereits von anderen Debuggern und Tools verwendet werden. Einige Beispiele sind:

  • Unter Linux werden die Systemaufrufe process_vm_readv() und process_vm_writev() zum Lesen und Schreiben von Speicher aus einem anderen Prozess verwendet. Diese Operationen werden durch ptrace Zugriffsmodusprüfungen gesteuert – die gleichen, die den Debugger-Anhang regeln. Ein Prozess kann nur dann Speicher eines anderen Prozesses lesen oder schreiben, wenn er die entsprechenden Berechtigungen hat (typischerweise erfordert dies entweder root oder die CAP_SYS_PTRACE-Berechtigung, obwohl weniger sicherheitsbewusste Distributionen jedem Prozess, der als dieselbe UID läuft, den Anhang erlauben können).
  • Unter macOS würde die Schnittstelle mach_vm_read_overwrite() und mach_vm_write() über das Mach-Task-System nutzen. Diese Operationen erfordern task_for_pid()-Zugriff, der vom Betriebssystem streng kontrolliert wird. Standardmäßig ist der Zugriff auf Prozesse beschränkt, die als root laufen, oder auf solche mit speziellen Berechtigungen, die vom Apple-Sicherheitsframework gewährt werden.
  • Unter Windows bieten die Funktionen ReadProcessMemory() und WriteProcessMemory() ähnliche Funktionalität. Der Zugriff wird über das Windows-Sicherheitsmodell gesteuert – ein Prozess benötigt die Berechtigungen PROCESS_VM_READ und PROCESS_VM_WRITE, die typischerweise denselben Benutzerkontext oder entsprechende Privilegien erfordern. Dies sind dieselben Berechtigungen, die von Debuggern benötigt werden, und gewährleisten plattformübergreifend konsistente Sicherheitssemantiken.

Alle Mechanismen stellen sicher, dass

  1. Nur autorisierte Prozesse Speicher lesen/schreiben können
  2. Das gleiche Sicherheitsmodell wie beim traditionellen Debugger-Anhang gilt
  3. Keine zusätzliche Angriffsfläche über das hinaus freigegeben wird, was das OS bereits für das Debugging bereitstellt
  4. Selbst wenn ein Angreifer beliebigen Speicher schreiben kann, kann er dies nicht zu beliebiger Codeausführung eskalieren, es sei denn, er hat bereits Dateisystemzugriff

Die Speicheroperationen selbst sind gut etabliert und werden seit Jahrzehnten sicher in Tools wie GDB, LLDB und verschiedenen Systemprofilern verwendet.

Es ist wichtig zu beachten, dass jeder Versuch, sich über diesen Mechanismus an einen Python-Prozess anzuhängen, sowohl von systemweiten Überwachungstools als auch von Python-Audit-Hooks erkannt werden kann. Diese Transparenz bietet eine zusätzliche Ebene der Verantwortlichkeit und ermöglicht es Administratoren, Debugging-Operationen in sensiblen Umgebungen zu überprüfen.

Darüber hinaus stellt die strikte Abhängigkeit von OS-Level-Sicherheitskontrollen sicher, dass bestehende Systemrichtlinien wirksam bleiben. Für Unternehmensumgebungen bedeutet dies, dass Administratoren weiterhin Debugging-Beschränkungen mit Standard-Tools und -Richtlinien erzwingen können, ohne zusätzliche Konfigurationen zu benötigen. Beispielsweise werden die Linux-Funktion ptrace_scope oder das macOS-Tool taskgated zur Einschränkung des Debugger-Zugriffs gleichermaßen die vorgeschlagene Schnittstelle regeln.

Durch die Aufrechterhaltung der Kompatibilität mit bestehenden Sicherheitsframeworks stellt dieses Design sicher, dass die Einführung der neuen Schnittstelle keine Änderungen an etablierten Praktiken erfordert.

Szenarien zur Sicherheit

  • Für einen externen Angreifer ist die Möglichkeit, beliebigen Speicher in einem Prozess zu schreiben, bereits ein schwerwiegendes Sicherheitsproblem. Diese Schnittstelle führt keine neue Angriffsfläche ein, da der Angreifer bereits die Möglichkeit hätte, beliebigen Code im Prozess auszuführen. Diese Schnittstelle verhält sich genau wie bestehende Debugger und führt keine zusätzlichen neuen Sicherheitsrisiken ein.
  • Für einen Angreifer, der Zugriff auf beliebige Speicherbeschreibungen in einem Prozess erlangt hat, aber nicht auf beliebige Codeausführung, erlaubt diese Schnittstelle keine Eskalation. Die Fähigkeit, spezifische Speicheradressen zu berechnen und dorthin zu schreiben, ist erforderlich, was ohne die Kompromittierung anderer externer Ressourcen des Python-Prozesses nicht verfügbar ist.

Darüber hinaus bedeutet die Tatsache, dass der auszuführende Code durch die Audit-Hooks des Interpreters gesteuert wird, dass die Ausführung des Codes von Systemadministratoren überwacht und kontrolliert werden kann. Das bedeutet, dass selbst wenn der Angreifer die Anwendung **und das Dateisystem** kompromittiert hat, die Nutzung dieser Schnittstelle für böswillige Zwecke ein sehr riskantes Unterfangen für einen Angreifer darstellt, da er riskiert, seine Aktionen an Systemadministratoren preiszugeben, die den Angriff nicht nur erkennen, sondern auch Maßnahmen zu seiner Verhinderung ergreifen könnten.

Schließlich ist wichtig zu beachten, dass wenn ein Angreifer beliebigen Speicher Schreibzugriff auf einen Prozess hat und das Dateisystem kompromittiert hat, er bereits durch andere bestehende Mechanismen zur beliebigen Codeausführung eskalieren kann, so dass diese Schnittstelle in diesem Szenario keine neuen Risiken einführt.

Wie man das lehrt

Für Tool-Autoren wird diese Schnittstelle zum Standardweg zur Implementierung von Debugger-Anhängen, was unsichere System-Debugger-Ansätze ersetzt. Ein Abschnitt im Python Developer Guide könnte die internen Funktionsweisen des Mechanismus beschreiben, einschließlich der debugger_support-Offsets und wie man mit ihnen über System-APIs interagiert.

Endbenutzer müssen von der Schnittstelle nichts wissen und profitieren lediglich von verbesserter Stabilität und Zuverlässigkeit der Debugging-Tools.

Referenzimplementierung

Eine Referenzimplementierung mit einem Prototyp zur Hinzufügung von Remote-Unterstützung für pdb finden Sie hier.

Abgelehnte Ideen

Schreiben von Python-Code in den Puffer

Wir haben uns entschieden, dass Debugger den Pfad zu einer Datei mit Python-Code in einen Puffer im Remote-Prozess schreiben. Dies wurde als sicherer erachtet als das Schreiben des auszuführenden Python-Codes selbst in einen Puffer im Remote-Prozess, da es bedeutet, dass ein Angreifer, der beliebige Schreibrechte in einem Prozess, aber keine beliebige Codeausführung oder Dateisystemmanipulation erlangt hat, über diese Schnittstelle nicht zu beliebiger Codeausführung eskalieren kann.

Dies erfordert jedoch, dass der anhangende Debugger bei der Erstellung der Datei mit dem auszuführenden Code auf Dateisystemberechtigungen achtet. Wenn ein Angreifer die Möglichkeit hat, die Datei zu überschreiben oder einen Symlink im Dateipfad zu ersetzen, damit er auf etwas Angreifer-kontrolliertes verweist, könnte dies ihm erlauben, seinen bösartigen Code anstelle des vom Debugger beabsichtigten Codes auszuführen.

Verwendung eines einzigen Laufzeitpuffers

Während der Überprüfung dieser PEP wurde vorgeschlagen, einen einzigen gemeinsamen Puffer auf Runtime-Ebene für die gesamte Debugger-Kommunikation zu verwenden. Obwohl dies einfacher erschien und weniger Speicher benötigte, stellten wir fest, dass es Szenarien verhindern würde, in denen mehrere Debugger Operationen über verschiedene Threads hinweg koordinieren müssen oder wo ein einzelner Debugger komplexe Debugging-Operationen orchestrieren muss. Ein einziger gemeinsamer Puffer würde die Serialisierung aller Debugging-Operationen erzwingen und es Debuggern unmöglich machen, unabhängig an verschiedenen Threads zu arbeiten.

Der Ansatz mit pro-Thread-Puffern ermöglicht trotz seines Speicher-Overheads in stark verschachtelten Anwendungen diese wichtigen Debugging-Szenarien, indem er es jedem Debugger ermöglicht, unabhängig mit seinem Ziel-Thread zu kommunizieren.

Danksagungen

Wir möchten CF Bolz-Tereick für ihre aufschlussreichen Kommentare und Vorschläge bei der Diskussion dieses Vorschlags danken.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0768.rst

Zuletzt geändert: 2025-10-04 13:46:29 GMT