Stručně: Životní cyklus požadavku PHP má dvě úrovně. Jednou za proces PHP načte svá rozšíření (fáze MINIT) a nakonec je zase odbourá (MSHUTDOWN). Pro každý jednotlivý požadavek existuje vlastní fáze startu a úklidu (RINIT a RSHUTDOWN). Ve webovém provozu vyřizuje worker mnoho požadavků po sobě, proto musí PHP mezi dvěma požadavky spolehlivě vynulovat veškerý stav požadavku. Právě na této hranici ležela CVE-2026-109619, druhá zranitelnost PHP2Shell.
Dvě úrovně: proces a požadavek
Pro netechniky pomůže přirovnání: worker PHP je jako přepážka na úřadě. Přepážka se ráno jednou zařídí, s formuláři, razítky a počítačem. Poté přicházejí postupně mnozí návštěvníci. Po každém návštěvníkovi musí být stůl prázdný, aby další neviděl cizí podklady.
PHP funguje stejně. Při startu procesu načte rozšíření a nastavení, která jsou pro všechny požadavky stejná. Každý požadavek pak dostane vlastní paměť, vlastní proměnné a vlastní data požadavku, jako jsou $_GET, $_POST nebo $_FILES. Na konci požadavku PHP tuto paměť uvolní. PHP Internals Book to shrnuje stručně: PHP na konci každého požadavku zapomene paměť požadavku.
Fáze životního cyklu požadavku PHP v přehledu
Názvy pocházejí z nitra PHP; každé rozšíření se může do těchto fází zapojit.
| Fáze | Kdy probíhá | Jak často | Typický úkol |
|---|---|---|---|
| MINIT (Module Init) | při startu procesu | jednou na proces | registrovat rozšíření, zřídit položky ini, trvalá data |
| GINIT | při vytváření globálních proměnných | na proces, u vláken na vlákno | připravit globální proměnné rozšíření |
| RINIT (Request Init) | na začátku každého požadavku | na požadavek | připravit paměť a stav požadavku |
| Provedení | mezitím | na požadavek | běží váš kód PHP |
| RSHUTDOWN | na konci každého požadavku | na požadavek | uvolnit zdroje požadavku |
| PRSHUTDOWN | po RSHUTDOWN | na požadavek, používá se zřídka | poslední úklidové práce |
| MSHUTDOWN | při ukončení procesu | jednou na proces | uvolnit trvalá data |
Důležitá věta z PHP Internals Book pro vývojáře rozšíření: globální proměnné se po každém požadavku automaticky nevyprazdňují. Co musí být pro každý požadavek čerstvé, patří do RINIT k resetování. Toto pravidlo ukazuje, proč je hranice mezi dvěma požadavky bezpečnostně kritickým místem.
Na konci požadavku je pořadí rovněž pevně dané: nejdřív proběhnou shutdown funkce, které jste zaregistrovali pomocí register_shutdown_function(), poté destruktory, poté se vyprázdní výstupní buffer. Teprve potom následuje RSHUTDOWN.
Proč se workery používají opakovaně
Na příkazové řádce vyřídí proces právě jeden požadavek a skončí. Ve webovém provozu by to bylo příliš pomalé. PHP-FPM, Apache s mod_php, LiteSpeed a FrankenPHP proto drží workery, které obsluhují mnoho požadavků za sebou. MINIT pak proběhne jednou, RINIT a RSHUTDOWN tisíckrát. Jak FPM své workery řídí, vysvětluje stránka PHP-FPM vysvětleno.
V PHP-FPM určuje pm.max_requests, po kolika požadavcích se worker restartuje. Výchozí hodnota je 0, tedy neomezeně. Příručka toto nastavení uvádí jako prostředek proti únikům paměti v knihovnách třetích stran:
; pool FPM: restartovat worker po 500 požadavcích (proti únikům paměti)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Ještě dál jde režim worker u FrankenPHP: tam se celá aplikace spustí jednou a zůstává v paměti. Stav aplikace pak přežívá i na úrovni PHP. Příklad ukazuje rozdíl:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // v klasickém provozu FPM je na každý požadavek opět 0
return ++$zaehler; // v režimu worker hodnota počítá dál napříč požadavky
}
echo anfrageZaehler();
V klasickém provozu vypíše volání vždy 1, protože PHP paměť požadavku zahodí. V režimu worker hodnota roste, dokud worker žije. Dokumentace FrankenPHP proto radí stav vázaný na požadavek mezi požadavky vědomě resetovat.
Kde CVE-2026-109619 míří
CVE-2026-109619 se netýká vašeho aplikačního kódu, ale samotného PHP. Podle php.net mohl interní stav patřící k požadavku přežít uvolnění své paměti. Pozdější požadavek ve stejném workeru jej mohl využít dříve, než jej inicializace vynulovala. V obrazu: na stole přepážky zůstal lístek, který měl být už dávno uklizen. Podrobnosti zneužití záměrně nezveřejňujeme; zařazení najdete v CVE-2026-109619.
Přechodná opatření php.net k této zranitelnosti míří na potvrzené cesty, nejsou úplnou ochranou:
session.upload_progress.enabled=0v php.ini, poolu FPM nebo konfiguraci serveru, poté restartovat všechny workery; ne přesini_set()nebo.user.ini.- U PHP 8.5 navíc
fatal_error_backtraces=0a restart. - Odstranit neplatná nebo zastaralá nastavení pro jednotlivé požadavky, která před inicializací vyvolávají hlášení. Pouhé skrytí hlášení podle php.net nepomáhá.
- V aplikaci odstranit zvenčí vyvolatelné fatální chyby a překročení časových limitů, i v callbacích, při zpracování nahrávání a importů.
Skutečnou nápravou je aktualizace na PHP 8.5.12, 8.4.27, 8.3.36 nebo 8.2.35. Protože chyba sídlí v běžícím workeru, projeví se až po restartu všech procesů; kroky najdete v článku Restart PHP-FPM a všech workerů.
Po restartu zkontrolujte pomocí ps -eo pid,lstart,cmd | grep php, zda byly všechny procesy PHP spuštěny po okamžiku aktualizace.
Co životní cyklus znamená pro provoz a vývoj
Z modelu lze odvodit jasná pravidla. Pro administrátory: aktualizace je účinná teprve tehdy, když neběží žádný proces se starým kódem. Každý provozní režim s dlouhověkými workery potřebuje zdokumentovaný restart. Pro vývojáře: kód, který běží v režimech worker, nesmí nechat stav vázaný na požadavek ve statických proměnných nebo singletonech, například uživatelská data nebo oprávnění. Pro oba platí: uchovejte logy od 20. 10. 2026, abyste mohli později posoudit, zda se před záplatou něco nestalo.
Jak konkrétně pomáhá PHP SOS?
Pomocí služby záplatování PHP model převádíme do praxe:
- Určit provozní režim: Zjistíme, zda běží PHP-FPM, mod_php, LiteSpeed, FrankenPHP nebo php-cgi a jak se workery spravují.
- Aktualizace a restart: Nasadíme opravenou verzi a řízeně restartujeme každého workera.
- Kontrola procesů: Podle časů startu a verzí doložíme, že nezůstal žádný starý proces.
- Prověřit režimy worker: U dlouho běžících aplikací se podíváme, zda se stav vázaný na požadavek čistě resetuje.
Časté dotazy
Spouští PHP pro každý požadavek nový proces?
Na příkazové řádce ano, ve webovém provozu většinou ne. PHP-FPM, Apache s mod_php, LiteSpeed nebo FrankenPHP nechají jeden proces vyřizovat mnoho požadavků po sobě. Mezi požadavky PHP stav požadavku uklidí.
Jaký je rozdíl mezi MINIT a RINIT?
MINIT proběhne jednou při startu procesu PHP a vytvoří trvalé struktury. RINIT proběhne na začátku každého jednotlivého požadavku a připraví to, co potřebuje jen tento požadavek.
Pomáhá pm.max_requests proti CVE-2026-109619?
php.net častější restarty workerů jako přechodné opatření neuvádí. Nastavení omezuje pouze to, kolik požadavků worker vyřídí, než se restartuje. Proti zranitelnosti pomáhá aktualizace s následným restartem všech workerů.
Co je režim worker u FrankenPHP?
V režimu worker se aplikace spustí jednou a zůstává v paměti. To šetří čas, ale statické proměnné, globální proměnné a cache zůstávají mezi požadavky zachovány. Podle dokumentace FrankenPHP je proto nutné stav vázaný na požadavek vědomě resetovat.