Protokoll zum Anhängen eines Debuggers aus der Ferne
****************************************************

Dieses Protokoll erlaubt es externen Werkzeugen, sich an einen
laufenden CPython-Prozess anzuhängen und Python-Code aus der Ferne
auszuführen.

Sich an einen anderen Python-Prozess anzuhängen kann je nach Plattform
zusätzliche Berechtigungen oder Konfiguration erfordern.


Debuggen aus der Ferne abschalten
=================================

Um die Unterstützung für das Debuggen aus der Ferne abzuschalten,
verwende eine der folgenden Möglichkeiten:

* Setze die Umgebungsvariable "PYTHON_DISABLE_REMOTE_DEBUG" auf "1",
  bevor du den Interpreter startest.

* Use the "-X disable-remote-debug" command-line option.

* Kompiliere Python mit dem Build-Flag "--without-remote-debug".


Anforderungen an die Berechtigungen
***********************************

Sich zum Debuggen aus der Ferne an einen laufenden Python-Prozess
anzuhängen erfordert auf den meisten Plattformen eine besondere
Konfiguration. Die genauen Anforderungen und die Schritte zur
Fehlersuche hängen von deinem Betriebssystem ab:

-[ Linux ]-

Im Allgemeinen kannst du deine eigenen Prozesse debuggen, aber es gibt
mehrere verbreitete Konfigurationen, die das abschalten können. Manche
Linux-Distributionen schalten als Form der Systemhärtung **ptrace-
Beschränkungen** ein, auch „Yama“ genannt. Neuere Fassungen des
Befehls "setpriv" (util-linux 2.41, veröffentlicht im Juni 2025)
erlauben es dir, die ptrace-Beschränkungen je Prozess zu lockern:

   "setpriv --ptracer any python3"

(Das wird an dem Prozess eingestellt, der *gedebuggt werden soll*.) Du
kannst die ptrace-Beschränkungen bis zum nächsten Neustart auch für
alle Prozesse abschalten:

   "echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope"

Das lässt sich auch dauerhaft einstellen, üblicherweise in
"/etc/sysctl.d".

Bemerkung:

  "ptrace_scope" abzuschalten verringert die Systemhärtung und sollte
  nur in Umgebungen mit geringem Sicherheitsbedarf geschehen.

Es ist auch möglich, dass der Systemaufruf "ptrace" wegen eines
Sicherheitsfilters abgeschaltet ist. Das war insbesondere bei älteren
Fassungen mancher Container-Software verbreitet. Docker 19.03 oder
neuer (veröffentlicht 2019) und containerd 1.6.7 oder neuer
(veröffentlicht 2022) erlauben die Verwendung des Systemaufrufs
"ptrace" innerhalb von Containern automatisch, wenn sie auf Linux-
Kernel 4.8 oder höher laufen. Wenn du nicht auf diese Fassungen
aktualisieren kannst, kannst du deinen Container mit einer Option wie
"--security-opt seccomp=unconfined" erzeugen, um den Sicherheitsfilter
für Systemaufrufe für diesen Container abzuschalten. Das schwächt die
Isolation des Containers und sollte nur in Umgebungen mit geringem
Sicherheitsbedarf geschehen.

Wenn du einen Prozess verfolgen musst, der dir *nicht* gehört,
brauchst du Superuser-Zugriff oder etwas Gleichwertiges. Das gilt auch
für Prozesse, die ihre Sicherheitsmerkmale geändert haben, etwa
Prozesse mit set-user-ID oder set-group-ID (was bei Python allerdings
unüblich ist). Versuche, den Debug-Befehl mit "sudo -E" auszuführen.

Bemerkung:

  Die Fähigkeit "CAP_SYS_PTRACE" entspricht Superuser-Zugriff,
  insofern sie das Debuggen *jedes* Prozesses erlaubt und nicht nur
  der eigenen. Im Internet findest du vielleicht den Rat, sie zu
  verwenden, um ptrace-Beschränkungen oder Filter für Systemaufrufe zu
  umgehen. In der Praxis kann das funktionieren, genau wie "sudo",
  aber es gibt dem Debug-Prozess viel mehr Zugriff, als er braucht,
  und sollte nur in Umgebungen mit geringem Sicherheitsbedarf
  geschehen.

Beachte schließlich, dass ein Prozess immer nur einen Verfolger haben
kann. Wenn du dich an einen Python-Prozess schon mit "strace", "gdb"
oder Ähnlichem angehängt hast, kannst du das Debuggen aus der Ferne
nicht gleichzeitig verwenden. (Superuser-Zugriff hilft über diese
Beschränkung nicht hinweg.)

-[ macOS ]-

Standardmäßig schaltet macOS die Möglichkeit ab, andere Prozesse zu
debuggen.

Du kannst deine Python-Programmdatei so verändern, dass sie dem
Debuggen zustimmt – indem du ihr eine **Ad-hoc-Codesignatur** mit
einer **Berechtigung** gibst, die das Debuggen erlaubt. (Eine Ad-
hoc-„Signatur“ ist nur eine Konfiguration ohne echte kryptografische
Signatur und ohne die Notwendigkeit eines Zertifikats oder einer
Mitgliedschaft im Apple-Entwicklerprogramm.)

Die folgenden Befehle erzeugen eine Datei "get-task-allow.plist" mit
der nötigen Berechtigung und fügen sie der Python-Programmdatei hinzu:

   echo '{"com.apple.security.get-task-allow": true}' | plutil -convert xml1 -o get-task-allow.plist -
   codesign --sign - --entitlements get-task-allow.plist path/to/bin/python3

wobei "path/to/bin/python3" der Pfad zu deiner Python-Programmdatei
ist, den du zum Beispiel über "which python3" oder durch Auswerten von
"sys.base_executable" in der Python-REPL findest. (Diese Anweisungen
gelten für einen Python-Build ohne Framework. Framework-Builds müssen
möglicherweise anders eingerichtet werden.)

Danach solltest du deine eigenen Python-Prozesse debuggen können, die
mit dieser Programmdatei gestartet wurden.

Wie unter Linux unterliegen Prozesse mit Superuser-Rechten, etwa über
"sudo", dieser Prüfung nicht und können jeden Prozess jedes Benutzers
auf dem System debuggen (wobei es wegen des System Integrity
Protection zusätzliche Prüfungen für bestimmte Programmdateien gibt,
etwa für vom Betriebssystem bereitgestellte Befehle).

-[ Windows ]-

Um dich an einen anderen Prozess anzuhängen, musst du dein Debug-
Werkzeug üblicherweise mit Administratorrechten ausführen. Starte die
Eingabeaufforderung oder das Terminal als Administrator.

Manche Prozesse können selbst mit Administratorrechten unzugänglich
bleiben, sofern du nicht das Recht "SeDebugPrivilege" eingeschaltet
hast.

Um Zugriffsprobleme bei Dateien oder Ordnern zu lösen, passe die
Sicherheitsberechtigungen an:

   1. Klicke die Datei oder den Ordner mit der rechten Maustaste an
      und wähle **Eigenschaften**.

   2. Wechsle auf die Registerkarte **Sicherheit**, um Benutzer und
      Gruppen mit Zugriff zu sehen.

   3. Klicke auf **Bearbeiten**, um die Berechtigungen zu ändern.

   4. Wähle dein Benutzerkonto aus.

   5. Setze unter **Berechtigungen** nach Bedarf ein Häkchen bei
      **Lesen** oder **Vollzugriff**.

   6. Klicke auf **Übernehmen** und dann auf **OK**, um zu bestätigen.

Bemerkung:

  Stelle sicher, dass du alle Anforderungen an die Berechtigungen
  erfüllt hast, bevor du weitermachst.

Dieser Abschnitt beschreibt das tiefliegende Protokoll, mit dem
externe Werkzeuge ein Python-Skript in einen laufenden CPython-Prozess
einspeisen und dort ausführen können.

Dieser Mechanismus ist die Grundlage der Funktion "sys.remote_exec()",
die einen fernen Python-Prozess anweist, eine ".py"-Datei auszuführen.
Dieser Abschnitt dokumentiert allerdings nicht die Verwendung dieser
Funktion. Er erklärt vielmehr ausführlich das zugrunde liegende
Protokoll, das als Eingabe die "pid" eines Ziel-Python-Prozesses und
den Pfad zu einer auszuführenden Python-Quelldatei nimmt. Diese
Informationen erlauben es, das Protokoll unabhängig und in jeder
Programmiersprache neu umzusetzen.

Warnung:

  Die Ausführung des eingespeisten Skripts hängt davon ab, dass der
  Interpreter einen sicheren Auswertungspunkt erreicht. Die Ausführung
  kann sich daher je nach Laufzeitzustand des Zielprozesses verzögern.

Nach dem Einspeisen wird das Skript vom Interpreter im Zielprozess
ausgeführt, sobald das nächste Mal ein sicherer Auswertungspunkt
erreicht wird. Dieser Ansatz erlaubt die Ausführung aus der Ferne,
ohne das Verhalten oder den Aufbau der laufenden Python-Anwendung zu
verändern.

Die folgenden Abschnitte beschreiben das Protokoll Schritt für
Schritt, einschließlich der Techniken, um Interpreter-Strukturen im
Speicher zu finden, sicher auf interne Felder zuzugreifen und die
Codeausführung anzustoßen. Plattformspezifische Abweichungen sind, wo
nötig, angemerkt, und Beispielumsetzungen verdeutlichen jeden Vorgang.


Die Struktur PyRuntime finden
*****************************

CPython legt die Struktur "PyRuntime" in einen eigenen Abschnitt der
Programmdatei, damit externe Werkzeuge sie zur Laufzeit finden können.
Name und Format dieses Abschnitts unterscheiden sich je nach
Plattform. So wird auf ELF-Systemen ".PyRuntime" verwendet und unter
macOS "__DATA,__PyRuntime". Werkzeuge können den Versatz dieser
Struktur finden, indem sie die Programmdatei auf der Festplatte
untersuchen.

Die Struktur "PyRuntime" enthält den globalen Interpreter-Zustand von
CPython und bietet Zugriff auf weitere interne Daten, darunter die
Liste der Interpreter, die Thread-Zustände und die Felder für die
Debugger-Unterstützung.

Um mit einem fernen Python-Prozess zu arbeiten, muss ein Debugger
zuerst die Speicheradresse der Struktur "PyRuntime" im Zielprozess
finden. Diese Adresse lässt sich nicht fest eintragen oder aus einem
Symbolnamen berechnen, weil sie davon abhängt, wohin das
Betriebssystem die Programmdatei geladen hat.

Wie man "PyRuntime" findet, hängt von der Plattform ab, aber die
Schritte sind im Allgemeinen dieselben:

1. Finde die Basisadresse, an der die Python-Programmdatei oder die
   dynamische Bibliothek im Zielprozess geladen wurde.

2. Bestimme über die Programmdatei auf der Festplatte den Versatz des
   Abschnitts ".PyRuntime".

3. Addiere den Versatz des Abschnitts zur Basisadresse, um die Adresse
   im Speicher zu berechnen.

Die folgenden Abschnitte erklären, wie das auf jeder unterstützten
Plattform geht, und enthalten Beispielcode.

-[ Linux (ELF) ]-

Um die Struktur "PyRuntime" unter Linux zu finden:

1. Lies die Speicherkarte des Prozesses (zum Beispiel
   "/proc/<pid>/maps"), um die Adresse zu finden, an der die Python-
   Programmdatei oder "libpython" geladen wurde.

2. Zerlege die ELF-Abschnittsköpfe in der Programmdatei, um den
   Versatz des Abschnitts ".PyRuntime" zu bekommen.

3. Addiere diesen Versatz zur Basisadresse aus Schritt 1, um die
   Speicheradresse von "PyRuntime" zu bekommen.

Hier eine Beispielumsetzung:

   def find_py_runtime_linux(pid: int) -> int:
       # Step 1: Try to find the Python executable in memory
       binary_path, base_address = find_mapped_binary(
           pid, name_contains="python"
       )

       # Step 2: Fallback to shared library if executable is not found
       if binary_path is None:
           binary_path, base_address = find_mapped_binary(
               pid, name_contains="libpython"
           )

       # Step 3: Parse ELF headers to get .PyRuntime section offset
       section_offset = parse_elf_section_offset(
           binary_path, ".PyRuntime"
       )

       # Step 4: Compute PyRuntime address in memory
       return base_address + section_offset

Auf Linux-Systemen gibt es zwei wesentliche Ansätze, um Speicher aus
einem anderen Prozess zu lesen. Der erste führt über das
"/proc"-Dateisystem, genauer über das Lesen von "/proc/[pid]/mem", das
unmittelbaren Zugriff auf den Speicher des Prozesses gibt. Das
erfordert passende Berechtigungen – entweder derselbe Benutzer wie
beim Zielprozess zu sein oder Root-Zugriff zu haben. Der zweite Ansatz
ist der Systemaufruf "process_vm_readv()", der einen effizienteren Weg
bietet, Speicher zwischen Prozessen zu kopieren. Die ptrace-Operation
"PTRACE_PEEKTEXT" lässt sich zum Lesen von Speicher ebenfalls
verwenden, ist aber deutlich langsamer, weil sie immer nur ein Wort
liest und mehrere Kontextwechsel zwischen dem verfolgenden und dem
verfolgten Prozess erfordert.

Zum Zerlegen von ELF-Abschnitten gehört es, die Strukturen des ELF-
Dateiformats aus der Programmdatei auf der Festplatte zu lesen und
auszuwerten. Der ELF-Kopf enthält einen Zeiger auf die Tabelle der
Abschnittsköpfe. Jeder Abschnittskopf enthält Metadaten über einen
Abschnitt, darunter seinen Namen (in einer separaten
Zeichenkettentabelle gespeichert), seinen Versatz und seine Größe. Um
einen bestimmten Abschnitt wie .PyRuntime zu finden, musst du diese
Köpfe durchgehen und den Abschnittsnamen vergleichen. Der
Abschnittskopf liefert dann den Versatz, an dem dieser Abschnitt in
der Datei liegt, mit dem sich seine Laufzeitadresse berechnen lässt,
sobald die Programmdatei in den Speicher geladen ist.

Mehr über das ELF-Dateiformat kannst du in der ELF-Spezifikation
nachlesen.

-[ macOS (Mach-O) ]-

Um die Struktur "PyRuntime" unter macOS zu finden:

1. Rufe "task_for_pid()" auf, um den "mach_port_t"-Task-Port des
   Zielprozesses zu bekommen. Dieses Handle ist nötig, um Speicher
   über APIs wie "mach_vm_read_overwrite" und "mach_vm_region" zu
   lesen.

2. Durchsuche die Speicherbereiche, um den zu finden, der die Python-
   Programmdatei oder "libpython" enthält.

3. Lade die Programmdatei von der Festplatte und zerlege die
   Mach-O-Köpfe, um den Abschnitt namens "PyRuntime" im Segment
   "__DATA" zu finden. Unter macOS werden Symbolnamen automatisch mit
   einem Unterstrich versehen. Das Symbol "PyRuntime" erscheint in der
   Symboltabelle daher als "_PyRuntime", der Abschnittsname bleibt
   davon aber unberührt.

Hier eine Beispielumsetzung:

   def find_py_runtime_macos(pid: int) -> int:
       # Step 1: Get access to the process's memory
       handle = get_memory_access_handle(pid)

       # Step 2: Try to find the Python executable in memory
       binary_path, base_address = find_mapped_binary(
           handle, name_contains="python"
       )

       # Step 3: Fallback to libpython if the executable is not found
       if binary_path is None:
           binary_path, base_address = find_mapped_binary(
               handle, name_contains="libpython"
           )

       # Step 4: Parse Mach-O headers to get __DATA,__PyRuntime section offset
       section_offset = parse_macho_section_offset(
           binary_path, "__DATA", "__PyRuntime"
       )

       # Step 5: Compute the PyRuntime address in memory
       return base_address + section_offset

Unter macOS erfordert der Zugriff auf den Speicher eines anderen
Prozesses Mach-O-spezifische APIs und Dateiformate. Der erste Schritt
ist, über "task_for_pid()" ein "task_port"-Handle zu bekommen, das
Zugriff auf den Speicherbereich des Zielprozesses gibt. Dieses Handle
ermöglicht Speicheroperationen über APIs wie
"mach_vm_read_overwrite()".

Der Speicher des Prozesses lässt sich mit "mach_vm_region()"
untersuchen, um den virtuellen Speicherbereich zu durchsuchen, während
"proc_regionfilename()" dabei hilft, zu erkennen, welche
Programmdateien an welchem Speicherbereich geladen sind. Ist die
Python-Programmdatei oder -Bibliothek gefunden, müssen ihre
Mach-O-Köpfe zerlegt werden, um die Struktur "PyRuntime" zu finden.

Das Mach-O-Format gliedert Code und Daten in Segmente und Abschnitte.
Die Struktur "PyRuntime" liegt in einem Abschnitt namens "__PyRuntime"
innerhalb des Segments "__DATA". Für die eigentliche Berechnung der
Laufzeitadresse muss das Segment "__TEXT" gefunden werden, das als
Basisadresse der Programmdatei dient, und dann das Segment "__DATA",
das unseren Zielabschnitt enthält. Die endgültige Adresse ergibt sich
aus der Basisadresse und den passenden Abschnittsversätzen aus den
Mach-O-Köpfen.

Beachte, dass der Zugriff auf den Speicher eines anderen Prozesses
unter macOS üblicherweise erhöhte Rechte erfordert – entweder Root-
Zugriff oder besondere Sicherheitsberechtigungen für den Debug-
Prozess.

-[ Windows (PE) ]-

Um die Struktur "PyRuntime" unter Windows zu finden:

1. Verwende die ToolHelp-API, um alle im Zielprozess geladenen Module
   aufzuzählen. Das geschieht über Funktionen wie
   CreateToolhelp32Snapshot, Module32First und Module32Next.

2. Bestimme das Modul, das zu "python.exe" oder "python*XY*.dll"
   gehört, wobei "X" und "Y" die Haupt- und Nebenversionsnummer der
   Python-Version sind, und notiere seine Basisadresse.

3. Finde den Abschnitt "PyRuntim". Wegen der Begrenzung von
   Abschnittsnamen auf 8 Zeichen im PE-Format (festgelegt als
   "IMAGE_SIZEOF_SHORT_NAME") wird der ursprüngliche Name "PyRuntime"
   abgeschnitten. Dieser Abschnitt enthält die Struktur "PyRuntime".

4. Hole die relative virtuelle Adresse (RVA) des Abschnitts und
   addiere sie zur Basisadresse des Moduls.

Hier eine Beispielumsetzung:

   def find_py_runtime_windows(pid: int) -> int:
       # Step 1: Try to find the Python executable in memory
       binary_path, base_address = find_loaded_module(
           pid, name_contains="python"
       )

       # Step 2: Fallback to shared pythonXY.dll if the executable is not
       # found
       if binary_path is None:
           binary_path, base_address = find_loaded_module(
               pid, name_contains="python3"
           )

       # Step 3: Parse PE section headers to get the RVA of the PyRuntime
       # section. The section name appears as "PyRuntim" due to the
       # 8-character limit defined by the PE format (IMAGE_SIZEOF_SHORT_NAME).
       section_rva = parse_pe_section_offset(binary_path, "PyRuntim")

       # Step 4: Compute PyRuntime address in memory
       return base_address + section_rva

Unter Windows erfordert der Zugriff auf den Speicher eines anderen
Prozesses Windows-API-Funktionen wie "CreateToolhelp32Snapshot()" und
"Module32First()/Module32Next()", um die geladenen Module aufzuzählen.
Die Funktion "OpenProcess()" liefert ein Handle für den Zugriff auf
den Speicherbereich des Zielprozesses und ermöglicht damit
Speicheroperationen über "ReadProcessMemory()".

Der Speicher des Prozesses lässt sich untersuchen, indem man die
geladenen Module aufzählt, um die Python-Programmdatei oder -DLL zu
finden. Ist sie gefunden, müssen ihre PE-Köpfe zerlegt werden, um die
Struktur "PyRuntime" zu finden.

Das PE-Format gliedert Code und Daten in Abschnitte. Die Struktur
"PyRuntime" liegt in einem Abschnitt namens „PyRuntim“ (von
„PyRuntime“ abgeschnitten wegen der Begrenzung der PE-Namen auf 8
Zeichen). Für die eigentliche Berechnung der Laufzeitadresse muss die
Basisadresse des Moduls aus dem Moduleintrag ermittelt und dann unser
Zielabschnitt in den PE-Köpfen gefunden werden. Die endgültige Adresse
ergibt sich aus der Basisadresse und der virtuellen Adresse des
Abschnitts aus den PE-Abschnittsköpfen.

Beachte, dass der Zugriff auf den Speicher eines anderen Prozesses
unter Windows üblicherweise passende Rechte erfordert – entweder
Administratorzugriff oder das Recht "SeDebugPrivilege" für den Debug-
Prozess.


_Py_DebugOffsets lesen
**********************

Sobald die Adresse der Struktur "PyRuntime" bestimmt ist, besteht der
nächste Schritt darin, die Struktur "_Py_DebugOffsets" zu lesen, die
am Anfang des "PyRuntime"-Blocks liegt.

Diese Struktur liefert versionsspezifische Feldversätze, die nötig
sind, um den Speicher des Interpreter- und Thread-Zustands sicher zu
lesen. Diese Versätze unterscheiden sich zwischen CPython-Versionen
und müssen vor der Verwendung geprüft werden, damit sie auch passen.

Um die Debug-Versätze zu lesen und zu prüfen, folge diesen Schritten:

1. Lies ab der "PyRuntime"-Adresse Speicher aus dem Zielprozess, und
   zwar so viele Bytes, wie die Struktur "_Py_DebugOffsets" groß ist.
   Diese Struktur liegt am ganz Anfang des "PyRuntime"-Speicherblocks.
   Ihre Anordnung ist in den internen Headern von CPython festgelegt
   und bleibt innerhalb einer Nebenversion gleich, kann sich in
   Hauptversionen aber ändern.

2. Prüfe, ob die Struktur gültige Daten enthält:

   * Das Feld "cookie" muss zur erwarteten Debug-Markierung passen.

   * Das Feld "version" muss zur Version des Python-Interpreters
     passen, die der Debugger verwendet.

   * Wenn der Debugger oder der Zielprozess eine Vorabversion
     verwendet (zum Beispiel eine Alpha-, Beta- oder Release-
     Candidate-Version), müssen die Versionen genau übereinstimmen.

   * Das Feld "free_threaded" muss im Debugger und im Zielprozess
     denselben Wert haben.

3. Ist die Struktur gültig, lassen sich die darin enthaltenen Versätze
   verwenden, um Felder im Speicher zu finden. Scheitert eine der
   Prüfungen, sollte der Debugger den Vorgang abbrechen, um nicht
   Speicher im falschen Format zu lesen.

Hier eine Beispielumsetzung, die "_Py_DebugOffsets" liest und prüft:

   def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets:
       # Step 1: Read memory from the target process at the PyRuntime address
       data = read_process_memory(
           pid, address=py_runtime_addr, size=DEBUG_OFFSETS_SIZE
       )

       # Step 2: Deserialize the raw bytes into a _Py_DebugOffsets structure
       debug_offsets = parse_debug_offsets(data)

       # Step 3: Validate the contents of the structure
       if debug_offsets.cookie != EXPECTED_COOKIE:
           raise RuntimeError("Invalid or missing debug cookie")
       if debug_offsets.version != LOCAL_PYTHON_VERSION:
           raise RuntimeError(
               "Mismatch between caller and target Python versions"
           )
       if debug_offsets.free_threaded != LOCAL_FREE_THREADED:
           raise RuntimeError("Mismatch in free-threaded configuration")

       return debug_offsets

Warnung:

  **Anhalten des Prozesses empfohlen**Um Wettläufe zu vermeiden und
  die Konsistenz des Speichers zu sichern, ist es sehr empfehlenswert,
  den Zielprozess anzuhalten, bevor Vorgänge ausgeführt werden, die
  internen Interpreter-Zustand lesen oder schreiben. Die Python-
  Laufzeit kann die Datenstrukturen des Interpreters im normalen
  Betrieb gleichzeitig verändern – etwa Threads erzeugen oder
  zerstören. Das kann zu ungültigen Lese- oder Schreibzugriffen
  führen.Ein Debugger kann die Ausführung anhalten, indem er sich mit
  "ptrace" an den Prozess anhängt oder ein "SIGSTOP"-Signal sendet.
  Die Ausführung sollte erst fortgesetzt werden, wenn die
  Speicheroperationen auf Debugger-Seite abgeschlossen sind.

  Bemerkung:

    Manche Werkzeuge, etwa Profiler oder abtastende Debugger, arbeiten
    möglicherweise an einem laufenden Prozess, ohne ihn anzuhalten. In
    solchen Fällen müssen die Werkzeuge ausdrücklich darauf ausgelegt
    sein, teilweise aktualisierten oder widersprüchlichen Speicher zu
    verkraften. Für die meisten Debugger-Umsetzungen bleibt es der
    sicherste und robusteste Weg, den Prozess anzuhalten.


Den Interpreter- und Thread-Zustand finden
******************************************

Bevor Code in einen fernen Python-Prozess eingespeist und ausgeführt
werden kann, muss der Debugger einen Thread wählen, in dem die
Ausführung eingeplant wird. Das ist nötig, weil die Steuerfelder für
das Einspeisen von Code aus der Ferne in der Struktur
"_PyRemoteDebuggerSupport" liegen, die in ein "PyThreadState"-Objekt
eingebettet ist. Diese Felder werden vom Debugger verändert, um die
Ausführung eingespeister Skripte anzufordern.

Die Struktur "PyThreadState" stellt einen Thread dar, der innerhalb
eines Python-Interpreters läuft. Sie führt den Auswertungskontext des
Threads und enthält die Felder, die für die Zusammenarbeit mit dem
Debugger nötig sind. Ein gültiges "PyThreadState" zu finden ist daher
eine wesentliche Voraussetzung dafür, die Ausführung aus der Ferne
anzustoßen.

Ein Thread wird üblicherweise anhand seiner Rolle oder seiner Kennung
gewählt. In den meisten Fällen wird der Haupt-Thread verwendet, aber
manche Werkzeuge zielen über die native Thread-Kennung auf einen
bestimmten Thread. Sobald der Ziel-Thread gewählt ist, muss der
Debugger sowohl die Interpreter- als auch die zugehörigen Thread-
Zustandsstrukturen im Speicher finden.

Die betroffenen internen Strukturen sind wie folgt festgelegt:

* "PyInterpreterState" stellt eine isolierte Python-Interpreter-
  Instanz dar. Jeder Interpreter führt seine eigene Menge importierter
  Module, seinen eigenen integrierten Zustand und seine eigene Liste
  von Thread-Zuständen. Die meisten Python-Anwendungen verwenden zwar
  einen einzigen Interpreter, aber CPython unterstützt mehrere
  Interpreter im selben Prozess.

* "PyThreadState" stellt einen Thread dar, der innerhalb eines
  Interpreters läuft. Es enthält den Ausführungszustand und die
  Steuerfelder, die der Debugger verwendet.

Um einen Thread zu finden:

1. Verwende den Versatz "runtime_state.interpreters_head", um die
   Adresse des ersten Interpreters in der Struktur "PyRuntime" zu
   bekommen. Das ist der Einstiegspunkt in die verkettete Liste der
   aktiven Interpreter.

2. Verwende den Versatz "interpreter_state.threads_main", um auf den
   Zustand des Haupt-Threads zuzugreifen, der zum gewählten
   Interpreter gehört. Das ist üblicherweise der verlässlichste Thread
   als Ziel.

3. Verwende wahlweise den Versatz "interpreter_state.threads_head", um
   die verkettete Liste aller Thread-Zustände durchzugehen. Jede
   "PyThreadState"-Struktur enthält ein Feld "native_thread_id", das
   sich mit einer Ziel-Thread-Kennung vergleichen lässt, um einen
   bestimmten Thread zu finden.

4. Sobald ein gültiges "PyThreadState" gefunden ist, lässt sich seine
   Adresse in den späteren Schritten des Protokolls verwenden, etwa
   zum Schreiben der Steuerfelder des Debuggers und zum Einplanen der
   Ausführung.

Hier eine Beispielumsetzung, die den Zustand des Haupt-Threads findet:

   def find_main_thread_state(
       pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets,
   ) -> int:
       # Step 1: Read interpreters_head from PyRuntime
       interp_head_ptr = (
           py_runtime_addr + debug_offsets.runtime_state.interpreters_head
       )
       interp_addr = read_pointer(pid, interp_head_ptr)
       if interp_addr == 0:
           raise RuntimeError("No interpreter found in the target process")

       # Step 2: Read the threads_main pointer from the interpreter
       threads_main_ptr = (
           interp_addr + debug_offsets.interpreter_state.threads_main
       )
       thread_state_addr = read_pointer(pid, threads_main_ptr)
       if thread_state_addr == 0:
           raise RuntimeError("Main thread state is not available")

       return thread_state_addr

Das folgende Beispiel zeigt, wie man einen Thread über seine native
Thread-Kennung findet:

   def find_thread_by_id(
       pid: int,
       interp_addr: int,
       debug_offsets: DebugOffsets,
       target_tid: int,
   ) -> int:
       # Start at threads_head and walk the linked list
       thread_ptr = read_pointer(
           pid,
           interp_addr + debug_offsets.interpreter_state.threads_head
       )

       while thread_ptr:
           native_tid_ptr = (
               thread_ptr + debug_offsets.thread_state.native_thread_id
           )
           native_tid = read_int(pid, native_tid_ptr)
           if native_tid == target_tid:
               return thread_ptr
           thread_ptr = read_pointer(
               pid,
               thread_ptr + debug_offsets.thread_state.next
           )

       raise RuntimeError("Thread with the given ID was not found")

Sobald ein gültiger Thread-Zustand gefunden ist, kann der Debugger
damit fortfahren, dessen Steuerfelder zu verändern und die Ausführung
einzuplanen, wie im nächsten Abschnitt beschrieben.


Steuerinformationen schreiben
*****************************

Sobald eine gültige "PyThreadState"-Struktur bestimmt ist, kann der
Debugger Steuerfelder darin verändern, um die Ausführung eines
bestimmten Python-Skripts einzuplanen. Diese Steuerfelder werden vom
Interpreter regelmäßig geprüft, und wenn sie richtig gesetzt sind,
lösen sie die Ausführung des Codes aus der Ferne an einem sicheren
Punkt in der Auswertungsschleife aus.

Jedes "PyThreadState" enthält eine Struktur
"_PyRemoteDebuggerSupport", die der Verständigung zwischen Debugger
und Interpreter dient. Die Lage ihrer Felder ist durch die Struktur
"_Py_DebugOffsets" festgelegt und umfasst Folgendes:

* "debugger_script_path": Ein Puffer fester Größe, der den
  vollständigen Pfad zu einer Python-Quelldatei (".py") aufnimmt.
  Diese Datei muss für den Zielprozess zugänglich und lesbar sein,
  wenn die Ausführung angestoßen wird.

* "debugger_pending_call": Ein ganzzahliges Flag. Es auf "1" zu setzen
  sagt dem Interpreter, dass ein Skript zur Ausführung bereitsteht.

* "eval_breaker": Ein Feld, das der Interpreter während der Ausführung
  prüft. Bit 5 ("_PY_EVAL_PLEASE_STOP_BIT", Wert "1U << 5") in diesem
  Feld zu setzen bringt den Interpreter dazu, anzuhalten und auf
  Debugger-Aktivität zu prüfen.

Um das Einspeisen abzuschließen, muss der Debugger die folgenden
Schritte ausführen:

1. Schreibe den vollständigen Skriptpfad in den Puffer
   "debugger_script_path".

2. Setze "debugger_pending_call" auf "1".

3. Lies den aktuellen Wert von "eval_breaker", setze Bit 5
   ("_PY_EVAL_PLEASE_STOP_BIT") und schreibe den aktualisierten Wert
   zurück. Damit wird dem Interpreter signalisiert, auf Debugger-
   Aktivität zu prüfen.

Hier eine Beispielumsetzung:

   def inject_script(
       pid: int,
       thread_state_addr: int,
       debug_offsets: DebugOffsets,
       script_path: str
   ) -> None:
       # Compute the base offset of _PyRemoteDebuggerSupport
       support_base = (
           thread_state_addr +
           debug_offsets.debugger_support.remote_debugger_support
       )

       # Step 1: Write the script path into debugger_script_path
       script_path_ptr = (
           support_base +
           debug_offsets.debugger_support.debugger_script_path
       )
       write_string(pid, script_path_ptr, script_path)

       # Step 2: Set debugger_pending_call to 1
       pending_ptr = (
           support_base +
           debug_offsets.debugger_support.debugger_pending_call
       )
       write_int(pid, pending_ptr, 1)

       # Step 3: Set _PY_EVAL_PLEASE_STOP_BIT (bit 5, value 1 << 5) in
       # eval_breaker
       eval_breaker_ptr = (
           thread_state_addr +
           debug_offsets.debugger_support.eval_breaker
       )
       breaker = read_int(pid, eval_breaker_ptr)
       breaker |= (1 << 5)
       write_int(pid, eval_breaker_ptr, breaker)

Sobald diese Felder gesetzt sind, kann der Debugger den Prozess
fortsetzen (falls er angehalten war). Der Interpreter verarbeitet die
Anfrage am nächsten sicheren Auswertungspunkt, lädt das Skript von der
Festplatte und führt es aus.

Es liegt in der Verantwortung des Debuggers, dafür zu sorgen, dass die
Skriptdatei während der Ausführung vorhanden und für den Zielprozess
zugänglich bleibt.

Bemerkung:

  Die Ausführung des Skripts ist asynchron. Die Skriptdatei darf nicht
  unmittelbar nach dem Einspeisen gelöscht werden. Der Debugger sollte
  warten, bis das eingespeiste Skript eine beobachtbare Wirkung
  erzeugt hat, bevor er die Datei entfernt. Welche Wirkung das ist,
  hängt davon ab, was das Skript tun soll. Ein Debugger könnte zum
  Beispiel warten, bis sich der ferne Prozess an einen Socket
  zurückmeldet, bevor er das Skript entfernt. Sobald eine solche
  Wirkung beobachtet ist, darf man davon ausgehen, dass die Datei
  nicht mehr gebraucht wird.


Zusammenfassung
***************

Um ein Python-Skript in einen fernen Prozess einzuspeisen und
auszuführen:

1. Finde die Struktur "PyRuntime" im Speicher des Zielprozesses.

2. Lies die Struktur "_Py_DebugOffsets" am Anfang von "PyRuntime" und
   prüfe sie.

3. Verwende die Versätze, um ein gültiges "PyThreadState" zu finden.

4. Schreibe den Pfad zu einem Python-Skript in "debugger_script_path".

5. Setze das Flag "debugger_pending_call" auf "1".

6. Setze "_PY_EVAL_PLEASE_STOP_BIT" im Feld "eval_breaker".

7. Setze den Prozess fort (falls angehalten). Das Skript wird am
   nächsten sicheren Auswertungspunkt ausgeführt.


Sicherheit und Bedrohungsmodell
*******************************

Das Protokoll zum Debuggen aus der Ferne stützt sich auf dieselben
Betriebssystemmittel, die native Debugger wie GDB und LLDB verwenden.
Sich an einen Prozess anzuhängen erfordert **dieselben Rechte**, die
diese Debugger brauchen, zum Beispiel "ptrace" / Yama LSM unter Linux,
"task_for_pid" unter macOS und "SeDebugPrivilege" unter Windows.
Python führt keinen neuen Weg zur Rechteausweitung ein, wenn ein
Angreifer die nötigen Rechte zum Anhängen an einen Prozess schon
besitzt, könnte er genauso GDB verwenden, um Speicher zu lesen oder
Code einzuspeisen.

Die folgenden Grundsätze legen fest, was bei dieser Funktionalität als
Sicherheitslücke gilt und was nicht:

Das Anhängen erfordert Rechte auf Betriebssystemebene
   Auf jeder unterstützten Plattform sichert das Betriebssystem den
   prozessübergreifenden Speicherzugriff hinter Rechteprüfungen ab
   ("CAP_SYS_PTRACE", Root oder Administratorrechte). Eine Meldung,
   die ein Problem erst zeigt, nachdem diese Rechte schon erlangt
   wurden, ist **keine** Sicherheitslücke in CPython, weil die
   Sicherheitsgrenze des Betriebssystems bereits überschritten war.

Abstürze oder Speicherfehler beim Lesen eines kompromittierten
Prozesses sind keine Sicherheitslücken
   Ein Werkzeug, das internen Interpreter-Zustand aus einem
   Zielprozess liest, muss darauf vertrauen, dass dieser Speicher
   wohlgeformt ist. Ist der Zielprozess beschädigt oder von einem
   Angreifer gesteuert, kann der Debugger oder Profiler abstürzen,
   Unsinn ausgeben oder sich unvorhersehbar verhalten. Das ist
   dasselbe Risiko, das jeder "ptrace"-basierte Debugger in Kauf
   nimmt. Fehler dieser Art (Pufferüberläufe, Speicherzugriffsfehler
   oder undefiniertes Verhalten, ausgelöst durch das Lesen
   beschädigten Zustands) werden **nicht** als Sicherheitsprobleme
   behandelt, Korrekturen zur Verbesserung der Robustheit sind aber
   willkommen.

Sicherheitslücken im Zielprozess sind nicht Gegenstand
   Ist der gedebuggte Python-Prozess schon kompromittiert, steuert der
   Angreifer die Ausführung in diesem Prozess bereits. Von diesem
   Ausgangspunkt weitere Auswirkungen vorzuführen stellt keine
   Sicherheitslücke im Protokoll zum Debuggen aus der Ferne dar.


Wann man "PYTHON_DISABLE_REMOTE_DEBUG" verwenden sollte
=======================================================

The environment variable "PYTHON_DISABLE_REMOTE_DEBUG" (and the
equivalent "-X disable-remote-debug" flag) allows operators to disable
the in-process side of the protocol as a **defence-in-depth** measure.
This may be useful in hardened or sandboxed deployment environments
where no debugging or profiling of the process is expected and
reducing attack surface is a priority, even though the OS-level
privilege checks already prevent unprivileged access.

Diese Variable zu setzen beeinflusst andere Debug-Schnittstellen auf
Betriebssystemebene **nicht** ("ptrace", "/proc", "task_for_pid"
usw.). Die bleiben nach ihren eigenen Berechtigungsmodellen verfügbar.
