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

Python Enhancement Proposals

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

Inhaltsverzeichnis

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.


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

Zuletzt geändert: 2025-02-01 08:55:40 GMT