PEP 651 – Robuste Behandlung von Stack-Überläufen
- Autor:
- Mark Shannon <mark at hotpy.org>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 18-Jan-2021
- Post-History:
- 19-Jan-2021
Ablehnungsbescheid
Diese PEP wurde vom Python Steering Council abgelehnt.
Zusammenfassung
Diese PEP schlägt vor, dass Python Maschinen-Stack-Überläufe anders behandelt als unerwünschte Rekursion.
Dies würde es Programmen ermöglichen, die maximale Rekursionstiefe nach Bedarf einzustellen und zusätzliche Sicherheitsgarantien zu bieten.
Wenn diese PEP akzeptiert wird, läuft das folgende Programm sicher bis zum Ende
sys.setrecursionlimit(1_000_000)
def f(n):
if n:
f(n-1)
f(500_000)
und das folgende Programm löst eine StackOverflow aus, ohne einen VM-Absturz zu verursachen.
sys.setrecursionlimit(1_000_000)
class X:
def __add__(self, other):
return self + other
X() + 1
Motivation
CPython verwendet einen einzigen Rekursionstiefenzähler, um sowohl unerwünschte Rekursion als auch C-Stack-Überläufe zu verhindern. Unerwünschte Rekursion und Maschinen-Stack-Überläufe sind jedoch zwei verschiedene Dinge. Der Maschinen-Stack-Überlauf ist eine potenzielle Sicherheitslücke, aber die Begrenzung der Rekursionstiefe kann die Verwendung einiger Algorithmen in Python verhindern.
Derzeit muss ein Programm, das tief rekursiv sein muss, die maximal erlaubte Rekursionstiefe verwalten und hoffentlich eine Einstellung zwischen dem Minimum für korrekte Ausführung und dem Maximum für die Vermeidung von Speicherfehlern finden.
Durch die Trennung der Überprüfungen auf C-Stack-Überlauf von den Überprüfungen der Rekursionstiefe können reine Python-Programme sicher ausgeführt werden und dabei jedes benötigte Rekursionsniveau verwenden.
Begründung
CPython verlässt sich derzeit auf eine einzige Grenze, um sowohl vor potenziell gefährlichen Stack-Überläufen in der virtuellen Maschine als auch vor unkontrollierter Rekursion im Python-Programm zu schützen.
Dies ist eine Folge der Implementierung, die den Python- und den C-Call-Stack koppelt. Durch das Brechen dieser Kopplung können wir sowohl die Benutzerfreundlichkeit von CPython als auch seine Sicherheit verbessern.
Das Rekursionslimit dient dem Schutz vor unerwünschter Rekursion; die Integrität der virtuellen Maschine sollte nicht davon abhängen. Ebenso sollte die Rekursion nicht durch Implementierungsdetails begrenzt werden.
Spezifikation
Zwei neue Ausnahmeklassen werden hinzugefügt: StackOverflow und RecursionOverflow, die beide Unterklassen von RecursionError sein werden.
StackOverflow exception
Eine StackOverflow-Ausnahme wird ausgelöst, wenn der Interpreter oder integrierte Modulcode feststellt, dass der C-Stack nahe an einer sicheren Grenze ist. StackOverflow ist eine Unterklasse von RecursionError, sodass jeder Code, der RecursionError behandelt, auch StackOverflow behandeln wird.
RecursionOverflow exception
Eine RecursionOverflow-Ausnahme wird ausgelöst, wenn ein Aufruf einer Python-Funktion das Rekursionslimit überschreitet. Dies ist eine leichte Änderung gegenüber dem aktuellen Verhalten, das eine RecursionError auslöst. RecursionOverflow ist eine Unterklasse von RecursionError, sodass jeder Code, der RecursionError behandelt, weiterhin wie bisher funktioniert.
Entkopplung des Python-Stacks vom C-Stack
Um die oben genannten Garantien zu bieten und sicherzustellen, dass jedes bisher funktionierende Programm weiterhin funktioniert, müssen der Python- und der C-Stack getrennt werden. Das heißt, Aufrufe von Python-Funktionen aus Python-Funktionen sollten keinen Speicherplatz auf dem C-Stack verbrauchen. Aufrufe von und zu integrierten Funktionen werden weiterhin Speicherplatz auf dem C-Stack verbrauchen.
Die Größe des C-Stacks wird implementierungsabhängig sein und kann von Rechner zu Rechner variieren. Sie kann sogar zwischen Threads unterschiedlich sein. Es wird jedoch erwartet, dass jeder Code, der mit der auf den vorherigen Standardwert gesetzten Rekursionstiefe ausgeführt werden könnte, weiterhin ausgeführt wird.
Viele Operationen in Python führen eine Art von Aufruf auf C-Ebene durch. Die meisten davon werden weiterhin C-Stack verbrauchen und zu einer StackOverflow-Ausnahme führen, wenn unkontrollierte Rekursion auftritt.
Andere Implementierungen
Andere Implementierungen müssen unabhängig vom eingestellten Rekursionslimit sicher fehlschlagen.
Wenn die Implementierung den Python-Stack mit dem zugrunde liegenden VM- oder Hardware-Stack koppelt, sollte sie eine RecursionOverflow-Ausnahme auslösen, wenn das Rekursionslimit überschritten wird, der zugrunde liegende Stack jedoch nicht überläuft. Wenn der zugrunde liegende Stack überläuft oder nahe am Überlauf ist, sollte eine StackOverflow-Ausnahme ausgelöst werden.
C-API
Eine neue Funktion, Py_CheckStackDepth(), wird hinzugefügt und das Verhalten von Py_EnterRecursiveCall() wird leicht modifiziert.
Py_CheckStackDepth()
int Py_CheckStackDepth(const char *where) gibt 0 zurück, wenn keine unmittelbare Gefahr eines C-Stack-Überlaufs besteht. Sie gibt -1 zurück und setzt eine Ausnahme, wenn der C-Stack kurz vor dem Überlaufen steht. Der Parameter where wird in der Fehlermeldung verwendet, in gleicher Weise wie der Parameter where von Py_EnterRecursiveCall().
Py_EnterRecursiveCall()
Py_EnterRecursiveCall() wird modifiziert, um Py_CheckStackDepth() aufzurufen, bevor sie ihre aktuelle Funktion ausführt.
PyLeaveRecursiveCall()
Py_LeaveRecursiveCall() bleibt unverändert.
Abwärtskompatibilität
Diese Funktion ist auf Python-Ebene vollständig abwärtskompatibel. Einige Low-Level-Tools wie Machine-Code-Debugger müssen modifiziert werden. Zum Beispiel müssen die gdb-Skripte für Python berücksichtigen, dass es mehr als einen Python-Frame pro C-Frame geben kann.
C-Code, der das Paar Py_EnterRecursiveCall(), PyLeaveRecursiveCall() verwendet, funktioniert weiterhin korrekt. Zusätzlich kann Py_EnterRecursiveCall() eine StackOverflow-Ausnahme auslösen.
Neuer Code sollte die Funktion Py_CheckStackDepth() verwenden, es sei denn, der Code soll als Python-Funktionsaufruf im Hinblick auf das Rekursionslimit gezählt werden.
Wir empfehlen, dass „python-ähnlicher“ Code, wie z. B. von Cython generierte Funktionen, Py_EnterRecursiveCall() verwendet, anderer Code jedoch Py_CheckStackDepth().
Sicherheitsimplikationen
Es wird nicht mehr möglich sein, die CPython-VM durch Rekursion zum Absturz zu bringen.
Leistungsauswirkungen
Es ist unwahrscheinlich, dass die Leistungseinbußen signifikant sind.
Die zusätzliche Logik wird wahrscheinlich eine sehr geringe negative Auswirkung auf die Leistung haben. Die verbesserte Lokalität der Referenz durch reduzierten C-Stack-Verbrauch sollte eine geringe positive Auswirkung haben.
Es ist schwer vorherzusagen, ob der Gesamteffekt positiv oder negativ sein wird, aber es ist ziemlich wahrscheinlich, dass der Nettoeffekt zu klein sein wird, um messbar zu sein.
Implementierung
Überwachung des C-Stack-Verbrauchs
Die Einschätzung, ob ein C-Stack-Überlauf droht, ist schwierig. Wir müssen also konservativ sein. Wir müssen sichere Grenzen für den Stack bestimmen, was in portablen C-Code nicht möglich ist.
Für Hauptplattformen wird die plattformspezifische API verwendet, um eine genaue Stack-Grenze zu ermitteln. Für kleinere Plattformen kann jedoch ein gewisses Raten erforderlich sein. Obwohl dies schlecht klingen mag, ist es nicht schlimmer als die aktuelle Situation, in der wir schätzen, dass die Größe des C-Stacks mindestens 1000-mal so groß ist wie der Stack-Platz, der für die Aufrufkette von _PyEval_EvalFrameDefault bis _PyEval_EvalFrameDefault benötigt wird.
Das bedeutet, dass in einigen Fällen die mögliche Rekursionsmenge reduziert werden kann. Im Allgemeinen sollte sich die mögliche Rekursionsmenge jedoch erhöhen, da viele Aufrufe keinen C-Stack verwenden.
Unser allgemeiner Ansatz zur Ermittlung einer Grenze für den C-Stack besteht darin, so früh wie möglich in der Aufrufkette eine Adresse innerhalb des aktuellen C-Frames zu erhalten. Die Grenze kann dann durch Hinzufügen einer Konstante dazu geschätzt werden.
Python-zu-Python-Aufrufe ohne Verbrauch des C-Stacks
Aufrufe im Interpreter werden durch die Anweisungen CALL_FUNCTION, CALL_FUNCTION_KW, CALL_FUNCTION_EX und CALL_METHOD behandelt. Der Code für diese Anweisungen wird so modifiziert, dass bei Aufruf einer Python-Funktion oder -Methode der Interpreter anstelle eines C-Aufrufs den Frame des Aufgerufenen einrichtet und die Interpretation wie gewohnt fortsetzt.
Die Anweisung RETURN_VALUE führt die umgekehrte Operation aus, außer wenn der aktuelle Frame der Einstiegsframe des Interpreters ist, dann wird normal zurückgekehrt.
Abgelehnte Ideen
Bisher keine.
Offene Fragen
Bisher keine.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0651.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT