Накратко: Жизненият цикъл на заявката (PHP Request Lifecycle) има две нива. Веднъж за процес PHP зарежда своите разширения (фаза MINIT) и накрая ги демонтира (MSHUTDOWN). За всяка отделна заявка има собствена фаза на старт и на почистване (RINIT и RSHUTDOWN). В уеб режим един worker обработва много заявки последователно, затова между две заявки PHP трябва надеждно да нулира цялото състояние на заявката. Именно на тази граница се намираше CVE-2026-109619, втората уязвимост PHP2Shell.
Две нива: процесът и заявката
За нетехническите читатели помага една картина: PHP worker е като гише в учреждение. Гишето се подрежда веднъж сутринта, с формуляри, печати и компютър. След това идват последователно много посетители. След всеки посетител масата трябва да е празна, за да не види следващият чужди документи.
PHP работи по същия начин. При старта на процеса зарежда разширения и настройки, които са еднакви за всички заявки. Всяка заявка получава след това собствена памет, собствени променливи и собствени данни на заявката като $_GET, $_POST или $_FILES. В края на заявката PHP освобождава тази памет. PHP Internals Book го обобщава накратко: PHP забравя паметта на заявката в края на всяка заявка.
Фазите на PHP Request Lifecycle накратко
Имената идват от вътрешността на PHP; всяко разширение може да се закачи към тези фази.
| Фаза | Кога се изпълнява | Колко често | Типична задача |
|---|---|---|---|
| MINIT (Module Init) | при старта на процеса | веднъж за процес | регистриране на разширения, създаване на ini записи, трайни данни |
| GINIT | при създаването на глобалните данни | за процес, при нишки за нишка | подготовка на глобалните променливи на разширение |
| RINIT (Request Init) | в началото на всяка заявка | за заявка | подготовка на паметта и състоянието на заявката |
| Изпълнение | между тях | за заявка | работи Вашият PHP код |
| RSHUTDOWN | в края на всяка заявка | за заявка | освобождаване на ресурсите на заявката |
| PRSHUTDOWN | след RSHUTDOWN | за заявка, рядко използвана | последни почистващи дейности |
| MSHUTDOWN | при приключване на процеса | веднъж за процес | освобождаване на трайните данни |
Едно важно изречение от PHP Internals Book за разработчиците на разширения: глобалните данни не се изчистват автоматично след всяка заявка. Онова, което трябва да е ново за всяка заявка, принадлежи в RINIT за нулиране. Това правило показва защо границата между две заявки е критично за сигурността място.
В края на една заявка редът също е определен: първо се изпълняват shutdown функциите, регистрирани с register_shutdown_function(), след това деструкторите, след това изходният буфер се изпразва. Едва тогава следва RSHUTDOWN.
Защо worker процесите се използват повторно
В командния ред един процес обработва точно една заявка и приключва. В уеб режим това би било твърде бавно. Затова PHP-FPM, Apache с mod_php, LiteSpeed и FrankenPHP държат наготово worker процеси, които обслужват много заявки поред. MINIT тогава се изпълнява веднъж, а RINIT и RSHUTDOWN – хиляди пъти. Как FPM управлява своите worker процеси, обяснява PHP-FPM, обяснен.
В PHP-FPM pm.max_requests определя след колко заявки един worker се рестартира. По подразбиране е 0, т.е. без ограничение. Ръководството посочва настройката като средство срещу течове на памет в библиотеки на трети страни:
; FPM пул: рестартиране на worker след 500 заявки (срещу течове на памет)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Още по-далеч отива worker режимът на FrankenPHP: там цялото приложение се стартира веднъж и остава в паметта. Състоянието на приложението тогава оцелява и на ниво PHP. Примерът показва разликата:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // в класически FPM режим за всяка заявка отново е 0
return ++$zaehler; // в worker режим стойността продължава да расте през заявките
}
echo anfrageZaehler();
В класически режим извикването винаги извежда 1, защото PHP отхвърля паметта на заявката. В worker режим стойността расте, докато worker-ът живее. Документацията на FrankenPHP затова съветва състоянието, свързано със заявката, съзнателно да се нулира между заявките.
Къде се намира CVE-2026-109619
CVE-2026-109619 не засяга кода на Вашето приложение, а самия PHP. Според php.net вътрешно състояние, принадлежащо на заявката, е могло да оцелее след освобождаването на паметта си. По-късна заявка в същия worker е могла да го използва, преди инициализацията да го нулира. В картината: върху масата на гишето е останал лист, който всъщност вече е бил прибран. Подробности за експлоатацията съзнателно не публикуваме; оценката е на страницата CVE-2026-109619.
Временните мерки на php.net за тази уязвимост се опират на потвърдени пътища, те не са пълна защита:
session.upload_progress.enabled=0в php.ini, FPM пула или конфигурацията на сървъра, след това рестартиране на всички worker процеси; не чрезini_set()или.user.ini.- При PHP 8.5 допълнително
fatal_error_backtraces=0и рестартиране. - Премахване на невалидни или остарели настройки за всяка заявка, които пораждат съобщения преди инициализацията. Само скриването на съобщенията според php.net не помага.
- Отстраняване в приложението на фатални грешки и надхвърляния на времето, които могат да бъдат предизвикани отвън, включително в callbacks, при обработка на качвания и импорт.
Същинското средство е актуализацията до PHP 8.5.12, 8.4.27, 8.3.36 или 8.2.35. Тъй като грешката се намира в работещия worker, тя действа едва след рестартиране на всички процеси; стъпките са описани в Рестартиране на PHP-FPM и на всички worker процеси.
След рестартирането проверете с ps -eo pid,lstart,cmd | grep php дали всички PHP процеси са стартирани след момента на актуализацията.
Какво означава жизненият цикъл за експлоатацията и разработката
От модела могат да се изведат ясни правила. За администраторите: актуализацията действа едва когато не работи нито един процес със стар код. Всеки режим на работа с дълготрайни worker процеси изисква документирано рестартиране. За разработчиците: код, който работи в worker режими, не бива да оставя състояние, свързано със заявка, в статични променливи или singletons, например данни за потребители или права. И за двете групи важи: пазете логовете от 20.10.2026 г., за да може по-късно да се прецени дали преди пача е станало нещо.
Как помага PHP SOS конкретно?
С услугата за PHP пачове прилагаме модела на практика:
- Определяне на режима на работа: Установяваме дали работи PHP-FPM, mod_php, LiteSpeed, FrankenPHP или php-cgi и как се управляват worker процесите.
- Актуализация и рестартиране: Внедряваме коригираната версия и рестартираме контролирано всеки worker.
- Проверка на процесите: Доказваме по времената на стартиране и по версиите, че не е останал стар процес.
- Проверка на worker режимите: При дълготрайни приложения разглеждаме дали състоянието, свързано със заявката, се нулира чисто.
Често задавани въпроси
PHP стартира ли нов процес за всяка заявка?
В командния ред да, в уеб режим най-често не. PHP-FPM, Apache с mod_php, LiteSpeed или FrankenPHP оставят един процес да обработва много заявки една след друга. Между заявките PHP почиства състоянието на заявката.
Каква е разликата между MINIT и RINIT?
MINIT се изпълнява веднъж, когато PHP процесът стартира, и създава трайни структури. RINIT се изпълнява в началото на всяка отделна заявка и подготвя онова, което е нужно само на тази заявка.
pm.max_requests помага ли срещу CVE-2026-109619?
php.net не посочва по-честите рестартирания на worker процесите като временна мярка. Настройката ограничава само колко заявки обработва един worker, преди да се рестартира. Срещу уязвимостта помага актуализацията с последващо рестартиране на всички worker процеси.
Какво е worker режимът на FrankenPHP?
В worker режим приложението се стартира веднъж и остава в паметта. Това спестява време, но статичните променливи, глобалните променливи и кешовете се запазват между заявките. Според документацията на FrankenPHP състоянието, свързано със заявката, затова трябва съзнателно да се нулира.