Pe scurt: PHP Request Lifecycle are două niveluri. O dată pe proces, PHP își încarcă extensiile (faza MINIT) și le demontează la sfârșit (MSHUTDOWN). Pentru fiecare cerere în parte există o fază proprie de pornire și de curățare (RINIT și RSHUTDOWN). În exploatarea web, un worker procesează multe cereri una după alta, de aceea PHP trebuie să reseteze în mod sigur, între două cereri, toată starea cererii. Exact la această graniță se afla CVE-2026-109619, a doua vulnerabilitate PHP2Shell.
Două niveluri: procesul și cererea
Pentru netehnicieni ajută o imagine: un worker PHP este ca un ghișeu dintr-o autoritate publică. Ghișeul este pregătit dimineața o singură dată, cu formulare, ștampile și calculator. Apoi vin, unul după altul, mulți vizitatori. După fiecare vizitator, masa trebuie să fie goală, pentru ca următorul să nu vadă acte străine.
PHP lucrează la fel. La pornirea unui proces, încarcă extensiile și setările care sunt aceleași pentru toate cererile. Fiecare cerere primește apoi memorie proprie, variabile proprii și date proprii ale cererii, precum $_GET, $_POST sau $_FILES. La sfârșitul cererii, PHP eliberează această memorie. PHP Internals Book rezumă concis: PHP uită memoria cererii la sfârșitul fiecărei cereri.
Fazele PHP Request Lifecycle, pe scurt
Denumirile provin din interiorul PHP; fiecare extensie se poate agăța de aceste faze.
| Fază | Când rulează | Cât de des | Sarcină tipică |
|---|---|---|---|
| MINIT (Module Init) | la pornirea procesului | o dată pe proces | înregistrarea extensiilor, crearea intrărilor ini, date permanente |
| GINIT | la crearea globalelor | pe proces, la thread-uri pe thread | pregătirea variabilelor globale ale unei extensii |
| RINIT (Request Init) | la începutul fiecărei cereri | pe cerere | pregătirea memoriei și a stării cererii |
| Execuție | între cele două | pe cerere | rulează codul dumneavoastră PHP |
| RSHUTDOWN | la sfârșitul fiecărei cereri | pe cerere | eliberarea resurselor cererii |
| PRSHUTDOWN | după RSHUTDOWN | pe cerere, rar folosită | ultimele lucrări de curățare |
| MSHUTDOWN | la încheierea procesului | o dată pe proces | eliberarea datelor permanente |
O frază importantă din PHP Internals Book pentru dezvoltatorii de extensii: globalele nu sunt golite automat după fiecare cerere. Ce trebuie să fie proaspăt la fiecare cerere se resetează în RINIT. Această regulă arată de ce granița dintre două cereri este un punct critic pentru securitate.
La sfârșitul unei cereri, ordinea este de asemenea fixă: mai întâi rulează funcțiile shutdown înregistrate de dumneavoastră cu register_shutdown_function(), apoi destructorii, apoi este golit bufferul de ieșire. Abia după aceea urmează RSHUTDOWN.
De ce sunt reutilizați workerii
În linia de comandă, un proces procesează exact o cerere și se încheie. În exploatarea web, ar fi prea lent. PHP-FPM, Apache cu mod_php, LiteSpeed și FrankenPHP țin de aceea workeri pregătiți, care deservesc multe cereri la rând. MINIT rulează atunci o dată, RINIT și RSHUTDOWN de mii de ori. Cum își controlează FPM workerii explică PHP-FPM explicat.
În PHP-FPM, pm.max_requests stabilește după câte cereri repornește un worker. Valoarea implicită este 0, adică nelimitat. Manualul indică setarea ca mijloc împotriva scurgerilor de memorie din bibliotecile terților:
; pool FPM: repornește workerul după 500 de cereri (împotriva scurgerilor de memorie)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Și mai departe merge modul worker al FrankenPHP: acolo întreaga aplicație pornește o singură dată și rămâne în memorie. Starea aplicației supraviețuiește atunci și la nivel PHP. Exemplul arată diferența:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // în exploatarea clasică FPM, din nou 0 la fiecare cerere
return ++$zaehler; // în modul worker, valoarea continuă să crească de la o cerere la alta
}
echo anfrageZaehler();
În exploatarea clasică, apelul afișează întotdeauna 1, deoarece PHP aruncă memoria cererii. În modul worker, valoarea crește cât timp trăiește workerul. Documentația FrankenPHP recomandă de aceea resetarea deliberată a stării legate de cerere între cereri.
Unde pornește CVE-2026-109619
CVE-2026-109619 nu privește codul aplicației dumneavoastră, ci PHP însuși. Potrivit php.net, starea internă aparținând unei cereri putea supraviețui eliberării memoriei sale. O cerere ulterioară din același worker o putea folosi înainte ca inițializarea să o reseteze. În imagine: pe masa ghișeului a rămas un bilet care ar fi trebuit să fie deja strâns. Detaliile de exploatare nu le publicăm în mod deliberat; încadrarea se găsește la CVE-2026-109619.
Măsurile de tranziție de la php.net pentru această vulnerabilitate pornesc de la căi confirmate și nu sunt o protecție completă:
session.upload_progress.enabled=0în php.ini, pool-ul FPM sau configurația serverului, apoi repornirea tuturor workerilor; nu prinini_set()sau.user.ini.- Pe PHP 8.5, suplimentar
fatal_error_backtraces=0și repornire. - Eliminarea setărilor invalide sau depășite, pentru fiecare cerere, care produc mesaje înainte de inițializare. Simpla ascundere a mesajelor nu ajută, potrivit php.net.
- Remedierea în aplicație a erorilor fatale și a depășirilor de timp care pot fi declanșate din exterior, inclusiv în callback-uri și în procesarea upload-urilor și a importurilor.
Remedierea propriu-zisă este actualizarea la PHP 8.5.12, 8.4.27, 8.3.36 sau 8.2.35. Deoarece eroarea se află în workerul care rulează, actualizarea are efect abia după o repornire a tuturor proceselor; pașii se găsesc la Repornirea PHP-FPM și a tuturor workerilor.
După repornire, verificați cu ps -eo pid,lstart,cmd | grep php dacă toate procesele PHP au pornit după momentul actualizării.
Ce înseamnă ciclul de viață pentru exploatare și dezvoltare
Din model se pot deduce reguli clare. Pentru administratori: o actualizare are efect abia când nu mai rulează niciun proces cu cod vechi. Fiecare mod de operare cu workeri de lungă durată are nevoie de o repornire documentată. Pentru dezvoltatori: codul care rulează în moduri worker nu are voie să lase stare legată de cerere în variabile statice sau în singleton-uri, de exemplu date de utilizator sau permisiuni. Pentru amândoi se aplică: păstrați logurile începând cu 20.10.2026, pentru a putea aprecia ulterior dacă s-a întâmplat ceva înainte de patch.
Cum ajută PHP SOS concret?
Cu serviciul PHP-Patch aplicăm modelul în practică:
- Stabilirea modului de operare: Stabilim dacă rulează PHP-FPM, mod_php, LiteSpeed, FrankenPHP sau php-cgi și cum sunt administrați workerii.
- Actualizare și repornire: Instalăm versiunea corectată și repornim controlat fiecare worker.
- Verificarea proceselor: Dovedim pe baza orelor de pornire și a versiunilor că nu a mai rămas niciun proces vechi.
- Verificarea modurilor worker: La aplicațiile de lungă durată, analizăm dacă starea legată de cerere este resetată corect.
Întrebări frecvente
Pornește PHP un proces nou pentru fiecare cerere?
În linia de comandă da, în exploatarea web de obicei nu. PHP-FPM, Apache cu mod_php, LiteSpeed sau FrankenPHP lasă un proces să proceseze multe cereri una după alta. Între cereri, PHP curăță starea cererii.
Care este diferența dintre MINIT și RINIT?
MINIT rulează o singură dată, când pornește procesul PHP, și creează structuri permanente. RINIT rulează la începutul fiecărei cereri în parte și pregătește ceea ce are nevoie doar acea cerere.
Ajută pm.max_requests împotriva CVE-2026-109619?
php.net nu menționează repornirile mai dese ale workerilor ca măsură de tranziție. Setarea limitează doar câte cereri procesează un worker înainte de a reporni. Împotriva vulnerabilității ajută actualizarea, urmată de repornirea tuturor workerilor.
Ce este modul worker al FrankenPHP?
În modul worker, aplicația pornește o singură dată și rămâne în memorie. Aceasta economisește timp, dar variabilele statice, globalele și cache-urile se păstrează între cereri. Potrivit documentației FrankenPHP, starea legată de cerere trebuie de aceea resetată în mod deliberat.