Kurz gesagt: Der PHP Request Lifecycle hat zwei Ebenen. Einmal pro Prozess lädt PHP seine Erweiterungen (Phase MINIT) und baut sie am Ende wieder ab (MSHUTDOWN). Für jede einzelne Anfrage gibt es eine eigene Start- und Aufräumphase (RINIT und RSHUTDOWN). Im Webbetrieb bearbeitet ein Worker viele Anfragen nacheinander, deshalb muss PHP zwischen zwei Anfragen allen Anfragezustand zuverlässig zurücksetzen. Genau an dieser Grenze lag CVE-2026-109619, die zweite PHP2Shell-Lücke.
Zwei Ebenen: der Prozess und die Anfrage
Für Nicht-Techniker hilft ein Bild: Ein PHP-Worker ist wie ein Schalter in einer Behörde. Der Schalter wird morgens einmal eingerichtet, mit Formularen, Stempeln und Computer. Danach kommen nacheinander viele Besucher. Nach jedem Besucher muss der Tisch leer sein, damit der nächste keine fremden Unterlagen sieht.
PHP arbeitet genauso. Beim Start eines Prozesses lädt es Erweiterungen und Einstellungen, die für alle Anfragen gleich sind. Jede Anfrage bekommt dann eigenen Speicher, eigene Variablen und eigene Anfragedaten wie $_GET, $_POST oder $_FILES. Am Ende der Anfrage gibt PHP diesen Speicher frei. Das PHP Internals Book fasst es knapp zusammen: PHP vergisst den Anfragespeicher am Ende jeder Anfrage.
Die Phasen des PHP Request Lifecycle im Überblick
Die Namen stammen aus dem Inneren von PHP; jede Erweiterung kann sich in diese Phasen einhängen.
| Phase | Wann sie läuft | Wie oft | Typische Aufgabe |
|---|---|---|---|
| MINIT (Module Init) | beim Start des Prozesses | einmal je Prozess | Erweiterungen registrieren, ini-Einträge anlegen, dauerhafte Daten |
| GINIT | beim Anlegen der Globals | je Prozess, bei Threads je Thread | globale Variablen einer Erweiterung vorbereiten |
| RINIT (Request Init) | zu Beginn jeder Anfrage | je Anfrage | Anfragespeicher und Anfragezustand vorbereiten |
| Ausführung | dazwischen | je Anfrage | Ihr PHP-Code läuft |
| RSHUTDOWN | am Ende jeder Anfrage | je Anfrage | Anfrageressourcen freigeben |
| PRSHUTDOWN | nach RSHUTDOWN | je Anfrage, selten genutzt | letzte Aufräumarbeiten |
| MSHUTDOWN | beim Beenden des Prozesses | einmal je Prozess | dauerhafte Daten freigeben |
Ein wichtiger Satz aus dem PHP Internals Book für Erweiterungsentwickler: Globals werden nicht nach jeder Anfrage automatisch geleert. Was pro Anfrage frisch sein muss, gehört in RINIT zurückgesetzt. Diese Regel zeigt, warum die Grenze zwischen zwei Anfragen eine sicherheitskritische Stelle ist.
Am Ende einer Anfrage ist die Reihenfolge ebenfalls festgelegt: Erst laufen Shutdown-Funktionen, die Sie mit register_shutdown_function() registriert haben, dann Destruktoren, dann wird der Ausgabepuffer geleert. Erst danach folgt RSHUTDOWN.
Warum Worker wiederverwendet werden
Auf der Kommandozeile bearbeitet ein Prozess genau eine Anfrage und endet. Im Webbetrieb wäre das zu langsam. PHP-FPM, Apache mit mod_php, LiteSpeed und FrankenPHP halten deshalb Worker vor, die viele Anfragen hintereinander bedienen. MINIT läuft dann einmal, RINIT und RSHUTDOWN tausende Male. Wie FPM seine Worker steuert, erklärt PHP-FPM erklärt.
In PHP-FPM legt pm.max_requests fest, nach wie vielen Anfragen ein Worker neu startet. Der Standard ist 0, also unbegrenzt. Das Handbuch nennt die Einstellung als Mittel gegen Speicherlecks in Bibliotheken von Drittanbietern:
; FPM-Pool: Worker nach 500 Anfragen neu starten (gegen Speicherlecks)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Noch weiter geht der Worker-Modus von FrankenPHP: Dort bootet die ganze Anwendung einmal und bleibt im Speicher. Anwendungszustand überlebt dann auch auf PHP-Ebene. Das Beispiel zeigt den Unterschied:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // im klassischen FPM-Betrieb je Anfrage wieder 0
return ++$zaehler; // im Worker-Modus zählt der Wert über Anfragen hinweg weiter
}
echo anfrageZaehler();
Im klassischen Betrieb gibt der Aufruf immer 1 aus, weil PHP den Anfragespeicher verwirft. Im Worker-Modus wächst der Wert, solange der Worker lebt. Die FrankenPHP-Doku rät deshalb, anfragebezogenen Zustand zwischen Anfragen bewusst zurückzusetzen.
Wo CVE-2026-109619 ansetzt
CVE-2026-109619 betrifft nicht Ihren Anwendungscode, sondern PHP selbst. Laut php.net konnte zur Anfrage gehörender interner Zustand das Freigeben seines Speichers überleben. Eine spätere Anfrage im selben Worker konnte ihn nutzen, bevor die Initialisierung ihn zurücksetzte. Im Bild: Auf dem Schaltertisch blieb ein Zettel liegen, der eigentlich schon weggeräumt war. Ausnutzungsdetails veröffentlichen wir bewusst nicht; die Einordnung steht unter CVE-2026-109619.
Die Übergangsmassnahmen von php.net zu dieser Lücke setzen an bestätigten Wegen an, sie sind kein vollständiger Schutz:
session.upload_progress.enabled=0in php.ini, FPM-Pool oder Serverkonfiguration, danach alle Worker neu starten; nicht überini_set()oder.user.ini.- Auf PHP 8.5 zusätzlich
fatal_error_backtraces=0und Neustart. - Ungültige oder veraltete Einstellungen je Anfrage entfernen, die vor der Initialisierung Meldungen erzeugen. Meldungen nur auszublenden hilft laut php.net nicht.
- Von aussen auslösbare fatale Fehler und Zeitüberschreitungen in der Anwendung beheben, auch in Callbacks, Upload- und Importverarbeitung.
Die eigentliche Abhilfe ist das Update auf PHP 8.5.12, 8.4.27, 8.3.36 oder 8.2.35. Weil der Fehler im laufenden Worker steckt, wirkt es erst nach einem Neustart aller Prozesse; die Schritte stehen unter PHP-FPM und alle Worker neu starten.
Prüfen Sie nach dem Neustart mit ps -eo pid,lstart,cmd | grep php, ob alle PHP-Prozesse nach dem Zeitpunkt des Updates gestartet wurden.
Was der Lebenszyklus für Betrieb und Entwicklung bedeutet
Aus dem Modell lassen sich klare Regeln ableiten. Für Admins: Ein Update ist erst wirksam, wenn kein Prozess mit altem Code mehr läuft. Jede Betriebsart mit langlebigen Workern braucht einen dokumentierten Neustart. Für Entwickler: Code, der in Worker-Modi läuft, darf keinen anfragebezogenen Zustand in statischen Variablen oder Singletons liegen lassen, etwa Benutzerdaten oder Berechtigungen. Für beide gilt: Logs ab dem 20.10.2026 aufbewahren, um später beurteilen zu können, ob vor dem Patch etwas passiert ist.
Wie hilft PHP SOS konkret?
Mit dem PHP-Patch-Service setzen wir das Modell in der Praxis um:
- Betriebsart bestimmen: Wir stellen fest, ob PHP-FPM, mod_php, LiteSpeed, FrankenPHP oder php-cgi läuft und wie Worker verwaltet werden.
- Update und Neustart: Wir spielen die korrigierte Version ein und starten jeden Worker kontrolliert neu.
- Prozessprüfung: Wir belegen anhand von Startzeiten und Versionen, dass kein alter Prozess übrig ist.
- Worker-Modi prüfen: Bei langlebigen Anwendungen sehen wir uns an, ob anfragebezogener Zustand sauber zurückgesetzt wird.
Häufige Fragen
Startet PHP für jede Anfrage einen neuen Prozess?
Auf der Kommandozeile ja, im Webbetrieb meist nicht. PHP-FPM, Apache mit mod_php, LiteSpeed oder FrankenPHP lassen einen Prozess viele Anfragen nacheinander bearbeiten. Zwischen den Anfragen räumt PHP den Anfragezustand auf.
Was ist der Unterschied zwischen MINIT und RINIT?
MINIT läuft einmal, wenn der PHP-Prozess startet, und legt dauerhafte Strukturen an. RINIT läuft zu Beginn jeder einzelnen Anfrage und bereitet das vor, was nur diese Anfrage braucht.
Hilft pm.max_requests gegen CVE-2026-109619?
php.net nennt häufigere Worker-Neustarts nicht als Übergangsmassnahme. Die Einstellung begrenzt nur, wie viele Anfragen ein Worker bearbeitet, bevor er neu startet. Gegen die Lücke hilft das Update mit anschliessendem Neustart aller Worker.
Was ist der Worker-Modus von FrankenPHP?
Im Worker-Modus bootet die Anwendung einmal und bleibt im Speicher. Das spart Zeit, aber statische Variablen, Globals und Caches bleiben zwischen Anfragen erhalten. Laut FrankenPHP-Doku muss anfragebezogener Zustand deshalb bewusst zurückgesetzt werden.