Текуща ситуация: PHP2Shell. Две критични уязвимости в PHP (CVSS 9,2) позволяват злонамерен код без вход. Корекции от 21.10.2026 г.Какво да направите сега →

Жизненият цикъл на заявката: старт, изпълнение, shutdown

Как PHP стартира процеси, обработва заявки и почиства помежду им. С фазите от MINIT до MSHUTDOWN и връзката с CVE-2026-109619.

Актуалност 5 мин. четенеЗнания за PHP

Накратко: Жизненият цикъл на заявката (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 за тази уязвимост се опират на потвърдени пътища, те не са пълна защита:

  1. session.upload_progress.enabled=0 в php.ini, FPM пула или конфигурацията на сървъра, след това рестартиране на всички worker процеси; не чрез ini_set() или .user.ini.
  2. При PHP 8.5 допълнително fatal_error_backtraces=0 и рестартиране.
  3. Премахване на невалидни или остарели настройки за всяка заявка, които пораждат съобщения преди инициализацията. Само скриването на съобщенията според php.net не помага.
  4. Отстраняване в приложението на фатални грешки и надхвърляния на времето, които могат да бъдат предизвикани отвън, включително в 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 състоянието, свързано със заявката, затова трябва съзнателно да се нулира.

Източници

Незабавен контакт

Всеки час има значение. Пишете ни сега.

Опишете накратко системата, версията на PHP и какво сте забелязали. Ние се свързваме с Вас, уточняваме ситуацията и работим отдалечено, докато инцидентът бъде приключен и документиран.

Незабавна помощ