Stručne: Životný cyklus požiadavky PHP má dve úrovne. Raz na proces PHP načíta svoje rozšírenia (fáza MINIT) a na konci ich opäť odbúra (MSHUTDOWN). Pre každú jednotlivú požiadavku existuje vlastná štartovacia a upratovacia fáza (RINIT a RSHUTDOWN). Vo webovej prevádzke worker vybavuje mnoho požiadaviek po sebe, preto musí PHP medzi dvoma požiadavkami spoľahlivo vynulovať všetok stav požiadavky. Práve na tejto hranici ležala CVE-2026-109619, druhá zraniteľnosť PHP2Shell.
Dve úrovne: proces a požiadavka
Pre netechnikov pomáha prirovnanie: worker PHP je ako okienko na úrade. Okienko sa ráno raz zariadi, s formulármi, pečiatkami a počítačom. Potom prichádza po sebe mnoho návštevníkov. Po každom návštevníkovi musí byť stôl prázdny, aby ďalší nevidel cudzie podklady.
PHP funguje rovnako. Pri štarte procesu načíta rozšírenia a nastavenia, ktoré sú pre všetky požiadavky rovnaké. Každá požiadavka potom dostane vlastnú pamäť, vlastné premenné a vlastné údaje požiadavky, ako $_GET, $_POST alebo $_FILES. Na konci požiadavky PHP túto pamäť uvoľní. PHP Internals Book to zhŕňa stručne: PHP na konci každej požiadavky zabudne pamäť požiadavky.
Fázy životného cyklu požiadavky PHP v prehľade
Názvy pochádzajú z vnútra PHP; každé rozšírenie sa môže do týchto fáz zapojiť.
| Fáza | Kedy beží | Ako často | Typická úloha |
|---|---|---|---|
| MINIT (Module Init) | pri štarte procesu | raz na proces | registrovať rozšírenia, vytvoriť záznamy ini, trvalé údaje |
| GINIT | pri vytváraní globálnych premenných | na proces, pri vláknach na vlákno | pripraviť globálne premenné rozšírenia |
| RINIT (Request Init) | na začiatku každej požiadavky | na požiadavku | pripraviť pamäť a stav požiadavky |
| Vykonanie | medzitým | na požiadavku | beží váš kód PHP |
| RSHUTDOWN | na konci každej požiadavky | na požiadavku | uvoľniť zdroje požiadavky |
| PRSHUTDOWN | po RSHUTDOWN | na požiadavku, zriedka používané | posledné upratovacie práce |
| MSHUTDOWN | pri ukončení procesu | raz na proces | uvoľniť trvalé údaje |
Dôležitá veta z PHP Internals Book pre vývojárov rozšírení: globálne premenné sa po každej požiadavke automaticky nevyprázdňujú. Čo musí byť pri každej požiadavke čerstvé, patrí vynulovať v RINIT. Toto pravidlo ukazuje, prečo je hranica medzi dvoma požiadavkami bezpečnostne kritickým miestom.
Na konci požiadavky je poradie tiež pevne dané: najprv bežia funkcie shutdown, ktoré ste zaregistrovali pomocou register_shutdown_function(), potom destruktory, potom sa vyprázdni výstupný buffer. Až potom nasleduje RSHUTDOWN.
Prečo sa workery používajú opakovane
V príkazovom riadku vybaví proces presne jednu požiadavku a skončí. Vo webovej prevádzke by to bolo príliš pomalé. PHP-FPM, Apache s mod_php, LiteSpeed a FrankenPHP preto držia workery, ktoré obsluhujú mnoho požiadaviek za sebou. MINIT potom beží raz, RINIT a RSHUTDOWN tisíckrát. Ako FPM riadi svojich workerov, vysvetľuje PHP-FPM vysvetlené.
V PHP-FPM určuje pm.max_requests, po koľkých požiadavkách sa worker reštartuje. Štandard je 0, teda neobmedzene. Príručka toto nastavenie uvádza ako prostriedok proti únikom pamäte v knižniciach tretích strán:
; Pool FPM: reštartovať worker po 500 požiadavkách (proti únikom pamäte)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Ešte ďalej ide režim workera vo FrankenPHP: tam sa celá aplikácia spustí raz a zostáva v pamäti. Stav aplikácie potom prežíva aj na úrovni PHP. Príklad ukazuje rozdiel:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // v klasickej prevádzke FPM je pri každej požiadavke opäť 0
return ++$zaehler; // v režime workera hodnota naďalej rastie naprieč požiadavkami
}
echo anfrageZaehler();
V klasickej prevádzke vypíše volanie vždy 1, pretože PHP zahodí pamäť požiadavky. V režime workera hodnota rastie, kým worker žije. Dokumentácia FrankenPHP preto radí stav viazaný na požiadavku medzi požiadavkami vedome vynulovať.
Kde útočí CVE-2026-109619
CVE-2026-109619 sa netýka vášho aplikačného kódu, ale samotného PHP. Podľa php.net mohol interný stav patriaci k požiadavke prežiť uvoľnenie svojej pamäte. Neskoršia požiadavka v tom istom workeri ho mohla použiť skôr, než ho inicializácia vynulovala. V obraze: na stole okienka zostal ležať lístok, ktorý mal byť už odložený. Podrobnosti o zneužití zámerne nezverejňujeme; zaradenie je uvedené v CVE-2026-109619.
Prechodné opatrenia php.net k tejto zraniteľnosti sa opierajú o potvrdené cesty, nie sú úplnou ochranou:
session.upload_progress.enabled=0v php.ini, poole FPM alebo konfigurácii servera, potom reštartovať všetky workery; nie cezini_set()ani.user.ini.- Na PHP 8.5 navyše
fatal_error_backtraces=0a reštart. - Odstrániť neplatné alebo zastarané nastavenia pre každú požiadavku, ktoré pred inicializáciou vytvárajú hlásenia. Hlásenia iba skryť podľa php.net nepomáha.
- Opraviť v aplikácii fatálne chyby a prekročenia času, ktoré sa dajú vyvolať zvonku, aj v callbackoch, pri spracovaní nahrávaní a importov.
Skutočným riešením je aktualizácia na PHP 8.5.12, 8.4.27, 8.3.36 alebo 8.2.35. Pretože chyba sídli v bežiacom workeri, účinkuje až po reštarte všetkých procesov; kroky sú uvedené v Reštart PHP-FPM a všetkých workerov.
Po reštarte skontrolujte pomocou ps -eo pid,lstart,cmd | grep php, či sa všetky procesy PHP spustili po čase aktualizácie.
Čo životný cyklus znamená pre prevádzku a vývoj
Z modelu sa dajú odvodiť jasné pravidlá. Pre administrátorov: aktualizácia je účinná, až keď už nebeží žiadny proces so starým kódom. Každý spôsob prevádzky s dlhožijúcimi workermi potrebuje zdokumentovaný reštart. Pre vývojárov: kód, ktorý beží v režimoch workera, nesmie nechávať stav viazaný na požiadavku v statických premenných alebo singletonoch, napríklad údaje používateľa alebo oprávnenia. Pre oboch platí: uchovajte logy od 20. 10. 2026, aby sa neskôr dalo posúdiť, či sa pred záplatou niečo stalo.
Ako PHP SOS konkrétne pomáha?
Pomocou služby záplatovania PHP model uplatňujeme v praxi:
- Určenie spôsobu prevádzky: Zisťujeme, či beží PHP-FPM, mod_php, LiteSpeed, FrankenPHP alebo php-cgi a ako sa workery spravujú.
- Aktualizácia a reštart: Nasadzujeme opravenú verziu a kontrolovane reštartujeme každý worker.
- Kontrola procesov: Pomocou časov štartu a verzií dokladujeme, že nezostal žiadny starý proces.
- Kontrola režimov workera: Pri dlhožijúcich aplikáciách sa pozeráme, či sa stav viazaný na požiadavku čisto nuluje.
Časté otázky
Spúšťa PHP pre každú požiadavku nový proces?
V príkazovom riadku áno, vo webovej prevádzke väčšinou nie. PHP-FPM, Apache s mod_php, LiteSpeed alebo FrankenPHP nechávajú jeden proces vybavovať mnoho požiadaviek po sebe. Medzi požiadavkami PHP upratuje stav požiadavky.
Aký je rozdiel medzi MINIT a RINIT?
MINIT beží raz, keď sa proces PHP spustí, a vytvára trvalé štruktúry. RINIT beží na začiatku každej jednotlivej požiadavky a pripravuje to, čo potrebuje len táto požiadavka.
Pomáha pm.max_requests proti CVE-2026-109619?
php.net častejšie reštarty workerov ako prechodné opatrenie neuvádza. Nastavenie obmedzuje iba to, koľko požiadaviek worker vybaví pred reštartom. Proti zraniteľnosti pomáha aktualizácia s následným reštartom všetkých workerov.
Čo je režim workera vo FrankenPHP?
V režime workera sa aplikácia spustí raz a zostáva v pamäti. Ušetrí to čas, ale statické premenné, globálne premenné a cache sa medzi požiadavkami zachovávajú. Podľa dokumentácie FrankenPHP sa preto stav viazaný na požiadavku musí vedome vynulovať.