Röviden: A PHP a memóriát elsősorban referenciaszámlálással szabadítja fel: minden érték tudja, hány változó mutat rá, és törlődik, amint ez a számláló eléri a nullát. Azokra a körkörös hivatkozásokra, amelyeket így sosem szabadítana fel, ciklusokat kezelő szemétgyűjtő (garbage collector) van. Minden kérés végén a PHP ezen felül rendbe teszi mindazt, amit a kérés lefoglalt. A PHP2Shell mindkét rése memóriaállapotokon támad: egy Out-of-Memory-állapoton a kérés feldolgozása közben, és olyan belső állapoton, amely a felszabadítás után túl sokáig megmarad.
Hogyan kezeli a PHP a memóriát: áttekintés
Az olyan nyelvekben, mint a C, a programozónak magának kell memóriát kérnie és ismét felszabadítania. A PHP ezt átveszi. Minden változó, minden tömb és minden objektum memóriát foglal, amelyet a PHP a saját memóriakezelőjén keresztül kezel. Ennek határát a memory_limit szabja meg, a kézikönyv szerint alapértelmezés szerint 128M.
A döntéshozók számára egy pont központi: a klasszikus, PHP-FPM-mel vagy mod_php-val végzett üzemben hosszú életű worker-folyamatok futnak, amelyek egymás után sok kérést dolgoznak fel. Amit egy kérés maga után hagy, azt a következő előtt megbízhatóan rendbe kell tenni. A PHP2Shell egyik rése éppen ezen a ponton van.
Referenciaszámlálás: számoljuk, ki használ egy értéket
A PHP minden értéket egy zval nevű tárolóban tárol. A típus és a tartalom mellett a kézikönyv szerint referenciaszámlálót (refcount) vezet, valamint egy jelölést arról, hogy az érték része-e egy & jellel képzett PHP-referenciának (is_ref).
<?php
$a = str_repeat('x', 1000); // egy érték, refcount 1
$b = $a; // nincs másolás, refcount 2
$b .= 'y'; // csak most másolódik (copy-on-write)
unset($a); // az eredeti refcountja csökken
// Ha egy számláló eléri a 0-t, a PHP azonnal felszabadítja a memóriát.
Az elv neve copy-on-write: a PHP csak akkor másol egy értéket, ha valamelyik változó módosítja. Ez sok memóriát spórol. Egy változó számlálóját az Xdebug bővítménnyel és az xdebug_debug_zval() függvénnyel lehet láthatóvá tenni. A kézikönyv felhívja a figyelmet, hogy a pontos számok PHP-verziónként eltérhetnek.
Ciklusok: amikor a referenciaszámlálás nem elég
A referenciaszámlálásnak ismert gyengéje van. Ha egy szerkezet önmagára mutat, a számlálója soha nem csökken nullára.
<?php
$a = ['eins'];
$a[] = &$a; // a tömb önmagára mutató referenciát tartalmaz
unset($a); // már senki sem férhet hozzá, de a refcount 1 marad
A kézikönyv szerint az ilyen körkörös hivatkozások különösen objektumoknál fordulnak elő, például szülő-gyermek szerkezetekben vagy értelmezőkben. Az egyes szivárgások ártalmatlanok, mert a PHP a kérés végén rendet tesz. A hosszan futó folyamatokban, például várakozási sor workerekben vagy nagy tesztfuttatásokban azonban több millió belőlük valódi memóriaproblémává összegződhet.
A ciklusokat kezelő szemétgyűjtő
A PHP 5.3.0 óta a PHP Bacon és Rajan algoritmusát használja az ilyen ciklusok megtalálására. Az alapgondolat: ciklus csak akkor keletkezhet, ha egy számláló nullánál nagyobb értékre csökken. A PHP csak ezeket az értékeket jegyzi meg lehetséges gyökérként.
- A lehetséges gyökerek egy pufferbe kerülnek. A kézikönyv alapértelmezett méretként 10 000 bejegyzést nevez meg; az éppen érvényes küszöbértéket a
gc_status()mutatja athresholdmezőben. - Ha a puffer megtelt, a PHP végigjárja az összes elérhető értéket, és próbaképpen csökkenti a számlálóikat.
- A második menetben az érvényes: a nulla számlálójú értékek szemétnek számítanak, az összes többinél a csökkentést visszavonják.
- A gyökereket eltávolítják a pufferből, és a szemétként felismert összes értéket felszabadítják.
Ezt a zend.enable_gc (alapértelmezés szerint bekapcsolva) vagy futásidőben a gc_enable() és a gc_disable() vezérli. A gc_collect_cycles() futást kényszerít ki, és visszaadja az összegyűjtött ciklusok számát.
A memória mérése és megfigyelése
| Függvény | Cél |
|---|---|
memory_get_usage() |
a szkript által jelenleg lefoglalt memória |
memory_get_usage(true) |
a memóriakezelő által a rendszertől lefoglalt memória; a memory_limit ezzel az értékkel szemben kerül ellenőrzésre |
memory_get_peak_usage() |
a lefoglalt memória csúcsértéke |
gc_status() |
futások, összegyűjtött ciklusok, küszöbérték, gyökerek; a PHP 8.3.0 óta futásidők is |
gc_collect_cycles() |
a ciklusgyűjtés azonnali kiváltása |
WeakMap |
objektumokhoz rendelés azok felszabadulásának megakadályozása nélkül, a PHP 8.0 óta |
A kézikönyv szerint a memory_get_usage() csak azt a memóriát számolja, amelyet a PHP a saját memóriakezelőjén át kér. Amit a bővítmények közvetlenül az operációs rendszertől szereznek, az hiányzik ezekből a számokból. A teljes folyamat memóriahatáráért ezért az operációs rendszer vagy a konténer felel.
Memóriakezelés és PHP2Shell
Mindkét rés leírása közvetlenül ehhez a témához vezet. A CVE-2026-109618-nál egy Out-of-Memory-állapot megszakíthatja a PHP-t, miközben a kéradatok csak részben vannak inicializálva; a takarításkor érvénytelen adatok ilyenkor érvényesként kezelődhetnek. Hogy memóriatúlcsordulásnál mi történik belül, azt a memory_limit és Out-of-Memory oldal magyarázza. A CVE-2026-109619-nél egy kérés belső állapota a memória felszabadítása után megmaradhat, és egy későbbi kérés ugyanabban a workerben felhasználhatja, mielőtt visszaállnának. A folyamatot az indulástól a takarításig A kérés életciklusa oldal írja le.
Átmeneti intézkedésként a php.net azt ajánlja, hogy a memory_limit=-1 úgy legyen beállítva, hogy az alkalmazások ne írhassák felül, például az FPM-poolban php_admin_value-val, a memóriát pedig konténerekkel vagy cgroupokkal korlátozzák, nem az RLIMIT_AS vagy az RLIMIT_DATA segítségével. Hogyan működik ez a systemddel, azt a Memóriahatárok beállítása systemd-vel és cgroupokkal cikk mutatja.
; FPM-pool: a php.net szerinti átmeneti intézkedés, utána minden workert újra kell indítani
php_admin_value[memory_limit] = -1
Konténeren vagy cgroupon keresztüli korlát nélkül egyetlen folyamat is lefoglalhatja a szervert. Egyetlen beállítás sem helyettesíti a frissítést a PHP 8.5.12, 8.4.27, 8.3.36 vagy 8.2.35 verzióra az összes worker újraindításával, mert csak az újraindítás távolítja el a régi folyamatokat a memóriából.
Hogyan segít konkrétan a PHP SOS?
- Megvizsgáljuk a poolonkénti érvényes memóriabeállításokat, és azt, hogy az alkalmazások felülírhatják-e őket.
- Az átmeneti intézkedéseket konténer- vagy cgroup-szintű memóriahatárral valósítjuk meg, és teszteljük a terhelést.
- Telepítjük a frissítést, újraindítjuk az összes workert, és igazoljuk, hogy már nem fut régi folyamat, a PHP-javítási szolgáltatásunk keretében.
-
- október 20-tól kiértékeljük a memóriahibákat és a megszakadt workereket a naplókban.
Gyakori kérdések
Mi az a referenciaszámlálás a PHP-ban?
A PHP minden értéknél számolja, hány változó vagy elem hivatkozik rá. Ha a számláló nullára csökken, a memória azonnal felszabadul. Így a memória nagy részét külön takarítási menet nélkül adja vissza.
Mire kell akkor még a PHP-nak szemétgyűjtő?
A ciklusokhoz. Ha egy tömb önmagára hivatkozik, vagy két objektum ismeri egymást, a számláló soha nem csökken nullára, még ha senki nem is fér hozzá. A ciklusokat kezelő szemétgyűjtő megtalálja és eltávolítja az ilyen szerkezeteket.
Felszabadítja a PHP a memóriát a kérés végén?
Igen, azt a memóriát, amelyet egy szkript a kérés alatt lefoglalt, a PHP a kérés végén rendbe teszi. A PHP2Shellnél a CVE-2026-109619 viszont éppen arra az esetre vonatkozik, hogy a belső állapot a felszabadítás után megmaradhat, és ugyanabban a workerben egy későbbi kérés felhasználhatja.
Miért ajánlja a php.net a memory_limit=-1 értéket átmeneti intézkedésként?
A CVE-2026-109618 rés egy Out-of-Memory-állapottal függ össze. A php.net azt ajánlja, hogy a memory_limit=-1 úgy legyen beállítva, hogy az alkalmazások ne írhassák felül, a memóriát pedig konténerekkel vagy cgroupokkal korlátozzák. Ez nem teljes védelem, és nem helyettesíti a frissítést.