Накратко: PHP освобождава паметта най-вече чрез броене на препратки: всяка стойност знае колко променливи сочат към нея и се изтрива, щом този брояч достигне нула. За циклични препратки, които така никога не биха се освободили, има garbage collector за цикли. В края на всяка заявка PHP допълнително почиства всичко, което заявката е заела. И двете уязвимости PHP2Shell засягат състояния на паметта: състояние на недостиг на памет по време на обработката на заявката и вътрешно състояние, което остава твърде дълго след освобождаването.
Как PHP управлява паметта: преглед
В езици като C програмистът сам трябва да заявява и отново да освобождава паметта. PHP му го спестява. Всяка променлива, всеки масив и всеки обект заема памет, която PHP управлява чрез собствен мениджър на паметта. Границата за това задава memory_limit, според ръководството по подразбиране 128M.
За вземащите решения една точка е централна: при класическата работа с PHP-FPM или mod_php работят дълготрайни worker процеси, които обработват последователно много заявки. Онова, което една заявка оставя, трябва да бъде надеждно почистено преди следващата. Точно тук се намира една от уязвимостите PHP2Shell.
Броене на препратки: отчитане кой използва дадена стойност
PHP съхранява всяка стойност в контейнер, т.нар. zval. Освен тип и съдържание, според ръководството той води брояч на препратките (refcount) и маркер дали стойността е част от PHP препратка с & (is_ref).
<?php
$a = str_repeat('x', 1000); // една стойност, refcount 1
$b = $a; // без копиране, refcount 2
$b .= 'y'; // едва сега се копира (Copy-on-Write)
unset($a); // refcount на оригинала намалява
// Щом един брояч достигне 0, PHP освобождава паметта веднага.
Принципът се нарича Copy-on-Write: PHP копира една стойност едва когато някоя от променливите я промени. Това спестява много памет. Броячът на една променлива може да се направи видим с разширението Xdebug и функцията xdebug_debug_zval(). Ръководството посочва, че точните числа могат да се различават според версията на PHP.
Цикли: когато броенето на препратки не стига
Броенето на препратки има известна слабост. Ако една структура сочи към себе си, нейният брояч никога не пада до нула.
<?php
$a = ['eins'];
$a[] = &$a; // масивът съдържа препратка към себе си
unset($a); // никой вече не може да го достъпи, но refcount остава 1
Според ръководството такива циклични препратки се срещат особено при обекти, например в структури родител-дете или в парсери. Отделни течове са безобидни, защото PHP почиства в края на заявката. В дълго работещи процеси като worker-и на опашки или големи тестови изпълнения обаче милиони от тях могат да се сумират в истински проблем с паметта.
Garbage collector-ът за цикли
От PHP 5.3.0 PHP използва алгоритъм на Bacon и Rajan, за да намира такива цикли. Основната идея: цикъл може да възникне само ако брояч падне до стойност, по-голяма от нула. Само тези стойности PHP запомня като възможни корени.
- Възможните корени попадат в буфер. Ръководството посочва като размер по подразбиране 10 000 записа; текущо действащия праг показва
gc_status()в полетоthreshold. - Когато буферът се напълни, PHP обхожда всички достижими стойности и пробно намалява техните броячи.
- При втори обход важи: стойностите с брояч нула са боклук, а при всички останали намаляването се връща.
- Корените се премахват от буфера и всички разпознати като боклук стойности се освобождават.
Управлява се чрез zend.enable_gc (по подразбиране включено) или по време на изпълнение с gc_enable() и gc_disable(). gc_collect_cycles() принуждава изпълнение и връща броя на събраните цикли.
Измерване и наблюдение на паметта
| Функция | Предназначение |
|---|---|
memory_get_usage() |
заетата в момента от скрипта памет |
memory_get_usage(true) |
памет, резервирана от мениджъра на паметта в системата; спрямо тази стойност се проверява memory_limit |
memory_get_peak_usage() |
най-висока стойност на заетата памет |
gc_status() |
изпълнения, събрани цикли, праг, корени; от PHP 8.3.0 и времена на изпълнение |
gc_collect_cycles() |
незабавно стартиране на събирането на цикли |
WeakMap |
съпоставяне към обекти, без да се пречи на освобождаването им, от PHP 8.0 |
Според ръководството memory_get_usage() отчита само паметта, която PHP заявява чрез собствения си мениджър на паметта. Онова, което разширенията вземат директно от операционната система, липсва в тези числа. За паметовата граница на целия процес затова са отговорни операционната система или контейнерът.
Управление на паметта и PHP2Shell
Описанията на двете уязвимости водят директно към тази тема. При CVE-2026-109618 състояние на недостиг на памет може да прекъсне PHP, докато данните от заявката са само частично инициализирани; при почистването невалидни данни могат да бъдат третирани като валидни. Какво се случва вътрешно при препълване на паметта, обяснява страницата memory_limit и Out-of-Memory. При CVE-2026-109619 вътрешно състояние на една заявка може да остане след освобождаването на паметта и да бъде използвано от по-късна заявка в същия worker, преди да бъде нулирано. Последователността от старта до почистването описва страницата Жизненият цикъл на заявката.
Като временна мярка php.net препоръчва memory_limit=-1 да се задава така, че приложенията да не могат да го презапишат, например с php_admin_value в FPM пула, а паметта да се ограничава чрез контейнер или cgroups, не чрез RLIMIT_AS или RLIMIT_DATA. Как става това със systemd, показва статията Задаване на граници на паметта със systemd и cgroups.
; FPM пул: временна мярка според php.net, след това рестартирайте всички worker процеси
php_admin_value[memory_limit] = -1
Без граница чрез контейнер или cgroup един-единствен процес може да натовари сървъра докрай. И никоя настройка не замества актуализацията до PHP 8.5.12, 8.4.27, 8.3.36 или 8.2.35 заедно с рестартиране на всички worker процеси, защото само рестартирането премахва старите процеси от паметта.
Как помага PHP SOS конкретно?
- Проверяваме действащите настройки на паметта за всеки пул и дали приложенията могат да ги презапишат.
- Прилагаме временните мерки с граница на паметта на ниво контейнер или cgroup и тестваме натоварването.
- Внедряваме актуализацията, рестартираме всички worker процеси и доказваме, че вече не работят стари процеси, в рамките на нашата услуга за PHP пачове.
- Анализираме грешките поради памет и прекратените worker процеси в логовете от 20.10.2026 г.
Често задавани въпроси
Какво е броене на препратки в PHP?
PHP отчита за всяка стойност колко променливи или елементи сочат към нея. Когато броячът падне до нула, паметта се освобождава веднага. Така по-голямата част от паметта се връща без отделно почистване.
За какво тогава PHP има нужда от garbage collector?
За цикли. Ако масив сочи към себе си или два обекта се познават взаимно, броячът никога не пада до нула, макар никой вече да не достъпва структурата. Garbage collector-ът за цикли намира и премахва такива структури.
PHP освобождава ли паметта в края на заявката?
Да, паметта, която скриптът е заел по време на заявка, PHP почиства в края ѝ. При PHP2Shell обаче CVE-2026-109619 засяга точно случая, при който вътрешно състояние може да остане след освобождаването и да бъде използвано от по-късна заявка в същия worker.
Защо php.net препоръчва memory_limit=-1 като временна мярка?
Уязвимостта CVE-2026-109618 е свързана със състояние на недостиг на памет. php.net препоръчва memory_limit=-1 да се задава така, че приложенията да не могат да го презапишат, а паметта да се ограничава чрез контейнер или cgroups. Това не е пълна защита и не замества актуализацията.