Trumpai: PHP Request Lifecycle turi dvi lygmenis. Kartą per procesą PHP įkelia savo plėtinius (fazė MINIT) ir pabaigoje vėl juos išardo (MSHUTDOWN). Kiekvienai atskirai užklausai yra sava paleidimo ir sutvarkymo fazė (RINIT ir RSHUTDOWN). Žiniatinklio veikloje vienas darbinis procesas (worker) paeiliui apdoroja daug užklausų, todėl tarp dviejų užklausų PHP turi patikimai atstatyti visą užklausos būseną. Būtent ties šia riba slypėjo CVE-2026-109619, antroji PHP2Shell spraga.
Du lygmenys: procesas ir užklausa
Ne technikams padeda vaizdinys: PHP darbinis procesas yra tarsi langelis įstaigoje. Langelis ryte vieną kartą įrengiamas, su blankais, antspaudais ir kompiuteriu. Po to paeiliui ateina daug lankytojų. Po kiekvieno lankytojo stalas turi būti tuščias, kad kitas nematytų svetimų dokumentų.
PHP veikia lygiai taip pat. Paleisdama procesą, ji įkelia plėtinius ir nustatymus, kurie visoms užklausoms vienodi. Kiekviena užklausa tuomet gauna savo atmintį, savus kintamuosius ir savus užklausos duomenis, tokius kaip $_GET, $_POST ar $_FILES. Užklausos pabaigoje PHP šią atmintį atlaisvina. PHP Internals Book tai apibendrina trumpai: PHP kiekvienos užklausos pabaigoje užmiršta užklausos atmintį.
PHP Request Lifecycle fazių apžvalga
Pavadinimai kilę iš PHP vidaus; kiekvienas plėtinys gali prisikabinti prie šių fazių.
| Fazė | Kada vykdoma | Kaip dažnai | Tipinė užduotis |
|---|---|---|---|
| MINIT (Module Init) | paleidžiant procesą | vieną kartą per procesą | registruoti plėtinius, sukurti ini įrašus, ilgalaikiai duomenys |
| GINIT | kuriant globalius kintamuosius | per procesą, su gijomis per giją | paruošti plėtinio globalius kintamuosius |
| RINIT (Request Init) | kiekvienos užklausos pradžioje | per užklausą | paruošti užklausos atmintį ir užklausos būseną |
| Vykdymas | tarp jų | per užklausą | vykdomas jūsų PHP kodas |
| RSHUTDOWN | kiekvienos užklausos pabaigoje | per užklausą | atlaisvinti užklausos išteklius |
| PRSHUTDOWN | po RSHUTDOWN | per užklausą, retai naudojama | paskutiniai sutvarkymo darbai |
| MSHUTDOWN | užbaigiant procesą | vieną kartą per procesą | atlaisvinti ilgalaikius duomenis |
Svarbus PHP Internals Book sakinys plėtinių kūrėjams: globalūs kintamieji po kiekvienos užklausos automatiškai neištuštinami. Kas kiekvienai užklausai turi būti švaru, turi būti atstatoma RINIT. Ši taisyklė parodo, kodėl riba tarp dviejų užklausų yra saugumui kritinė vieta.
Užklausos pabaigoje eilė taip pat nustatyta: pirmiausia vykdomos užbaigimo funkcijos (shutdown), kurias užregistravote su register_shutdown_function(), paskui destruktoriai, tada ištuštinamas išvesties buferis. Tik po to seka RSHUTDOWN.
Kodėl darbiniai procesai naudojami pakartotinai
Komandinėje eilutėje procesas apdoroja lygiai vieną užklausą ir baigiasi. Žiniatinklio veikloje tai būtų per lėta. PHP-FPM, Apache su mod_php, LiteSpeed ir FrankenPHP todėl palaiko darbinius procesus, kurie vienas po kito aptarnauja daug užklausų. MINIT tuomet vykdomas vieną kartą, RINIT ir RSHUTDOWN tūkstančius kartų. Kaip FPM valdo savo darbinius procesus, paaiškina puslapis PHP-FPM paaiškintas.
PHP-FPM pm.max_requests nustato, po kiek užklausų darbinis procesas pasileidžia iš naujo. Numatytoji reikšmė yra 0, t. y. neribota. Vadovas šį nustatymą nurodo kaip priemonę nuo atminties nutekėjimų trečiųjų šalių bibliotekose:
; FPM baseinas: darbinį procesą paleisti iš naujo po 500 užklausų (nuo atminties nutekėjimų)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Dar toliau eina FrankenPHP worker režimas: ten visa programa pasileidžia vieną kartą ir lieka atmintyje. Programos būsena tuomet išlieka ir PHP lygiu. Pavyzdys rodo skirtumą:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // klasikinėje FPM veikloje kiekvienai užklausai vėl 0
return ++$zaehler; // worker režimu reikšmė skaičiuojama toliau per užklausas
}
echo anfrageZaehler();
Klasikinėje veikloje iškvietimas visada išveda 1, nes PHP išmeta užklausos atmintį. Worker režimu reikšmė auga, kol darbinis procesas gyvas. Todėl FrankenPHP dokumentacija pataria sąmoningai atstatyti su užklausa susijusią būseną tarp užklausų.
Kur kimba CVE-2026-109619
CVE-2026-109619 liečia ne jūsų programos kodą, o patį PHP. Pasak php.net, su užklausa susijusi vidinė būsena galėjo išgyventi savo atminties atlaisvinimą. Vėlesnė užklausa tame pačiame darbiniame procese galėjo ją panaudoti, kol inicializacija jos neatstatė. Vaizdiniu požiūriu: ant langelio stalo liko lapelis, kuris iš tikrųjų jau turėjo būti nukeltas. Išnaudojimo detalių sąmoningai neskelbiame; įvertinimas pateiktas puslapyje CVE-2026-109619.
php.net pereinamosios priemonės šiai spragai kimba už patvirtintų kelių, jos nėra visiška apsauga:
session.upload_progress.enabled=0php.ini, FPM baseine arba serverio konfigūracijoje, po to visus darbinius procesus (worker) paleisti iš naujo; ne perini_set()ar.user.ini.- PHP 8.5 papildomai
fatal_error_backtraces=0ir paleidimas iš naujo. - Pašalinti nepagrįstus ar pasenusius nustatymus, kurie per kiekvieną užklausą prieš inicializaciją generuoja pranešimus. Vien pranešimų paslėpimas, pasak php.net, nepadeda.
- Programoje ištaisyti iš išorės sukeliamas mirtinas klaidas ir laiko limitų viršijimus, taip pat atgalinio iškvietimo (callback) funkcijose, įkėlimų ir importo apdorojime.
Tikroji pagalba yra atnaujinimas iki PHP 8.5.12, 8.4.27, 8.3.36 ar 8.2.35. Kadangi klaida slypi veikiančiame darbiniame procese, atnaujinimas pradeda veikti tik paleidus iš naujo visus procesus; žingsniai pateikti puslapyje PHP-FPM ir visų darbinių procesų paleidimas iš naujo.
Po paleidimo iš naujo su ps -eo pid,lstart,cmd | grep php patikrinkite, ar visi PHP procesai buvo paleisti po atnaujinimo laiko.
Ką gyvavimo ciklas reiškia eksploatacijai ir kūrimui
Iš modelio galima išvesti aiškias taisykles. Administratoriams: atnaujinimas veikia tik tada, kai nebeveikia nė vienas procesas su senu kodu. Kiekviena veikimo rūšis su ilgaamžiais darbiniais procesais reikalauja dokumentuoto paleidimo iš naujo. Kūrėjams: kodas, veikiantis worker režimais, negali palikti su užklausa susijusios būsenos statiniuose kintamuosiuose ar singleton objektuose, pavyzdžiui, naudotojo duomenų ar teisių. Abiem galioja: saugokite žurnalus nuo 2026-10-20, kad vėliau būtų galima įvertinti, ar prieš pataisą kas nors įvyko.
Kaip PHP SOS padeda konkrečiai?
Su PHP pataisų paslauga šį modelį įgyvendiname praktiškai:
- Nustatyti veikimo rūšį: išsiaiškiname, ar veikia PHP-FPM, mod_php, LiteSpeed, FrankenPHP ar php-cgi ir kaip valdomi darbiniai procesai.
- Atnaujinimas ir paleidimas iš naujo: įdiegiame ištaisytą versiją ir kontroliuojamai paleidžiame iš naujo kiekvieną darbinį procesą.
- Procesų patikra: pagal paleidimo laikus ir versijas įrodome, kad neliko seno proceso.
- Patikrinti worker režimus: ilgaamžėms programoms peržiūrime, ar su užklausa susijusi būsena tvarkingai atstatoma.
Dažni klausimai
Ar PHP kiekvienai užklausai paleidžia naują procesą?
Komandinėje eilutėje taip, žiniatinklio veikloje dažniausiai ne. PHP-FPM, Apache su mod_php, LiteSpeed ar FrankenPHP leidžia vienam procesui paeiliui apdoroti daug užklausų. Tarp užklausų PHP sutvarko užklausos būseną.
Kuo skiriasi MINIT ir RINIT?
MINIT vykdomas vieną kartą, kai paleidžiamas PHP procesas, ir sukuria ilgalaikes struktūras. RINIT vykdomas kiekvienos atskiros užklausos pradžioje ir paruošia tai, ko reikia tik šiai užklausai.
Ar pm.max_requests padeda nuo CVE-2026-109619?
php.net dažnesnių darbinių procesų (worker) paleidimų iš naujo kaip pereinamosios priemonės nenurodo. Nustatymas tik riboja, kiek užklausų darbinis procesas apdoroja prieš pasileisdamas iš naujo. Nuo spragos padeda atnaujinimas su vėlesniu visų darbinių procesų paleidimu iš naujo.
Kas yra FrankenPHP worker režimas?
Worker režimu programa pasileidžia vieną kartą ir lieka atmintyje. Tai taupo laiką, bet statiniai kintamieji, globalūs kintamieji ir talpyklos tarp užklausų išlieka. Pasak FrankenPHP dokumentacijos, su užklausa susijusią būseną todėl reikia sąmoningai atstatyti.