Pe scurt: PHP eliberează memoria în primul rând prin numărarea referințelor: fiecare valoare știe câte variabile trimit la ea și este ștearsă de îndată ce acest contor ajunge la zero. Pentru referințele circulare, care altfel n-ar fi eliberate niciodată, există un garbage collector de cicluri. La sfârșitul fiecărei cereri, PHP curăță în plus tot ce a ocupat cererea. Ambele vulnerabilități PHP2Shell pornesc de la stări ale memoriei: de la o stare Out-of-Memory în timpul procesării cererii și de la o stare internă care rămâne prea mult după eliberare.
Cum gestionează PHP memoria: privire de ansamblu
În limbaje precum C, programatorul trebuie să solicite și să elibereze singur memoria. PHP îl scutește de aceasta. Fiecare variabilă, fiecare array și fiecare obiect ocupă memorie, pe care PHP o gestionează prin propriul manager de memorie. Limita pentru aceasta este stabilită de memory_limit, potrivit manualului implicit 128M.
Pentru factorii de decizie, un punct este esențial: în exploatarea clasică cu PHP-FPM sau mod_php rulează procese worker de lungă durată, care procesează una după alta multe cereri. Ce lasă în urmă o cerere trebuie să fie curățat în mod sigur înainte de următoarea. Exact aici se află una dintre vulnerabilitățile PHP2Shell.
Numărarea referințelor: se numără cine folosește o valoare
PHP stochează fiecare valoare într-un container numit zval. Pe lângă tip și conținut, acesta ține, potrivit manualului, un contor de referințe (refcount) și un marcaj care arată dacă valoarea face parte dintr-o referință PHP cu & (is_ref).
<?php
$a = str_repeat('x', 1000); // o valoare, refcount 1
$b = $a; // fără copiere, refcount 2
$b .= 'y'; // abia acum se copiază (copy-on-write)
unset($a); // refcount-ul originalului scade
// Când un contor ajunge la 0, PHP eliberează imediat memoria.
Principiul se numește copy-on-write: PHP copiază o valoare abia când una dintre variabile o modifică. Aceasta economisește multă memorie. Contorul unei variabile poate fi făcut vizibil cu extensia Xdebug și funcția xdebug_debug_zval(). Manualul atrage atenția că numerele exacte pot diferi de la o versiune PHP la alta.
Cicluri: când numărarea referințelor nu ajunge
Numărarea referințelor are o slăbiciune cunoscută. Dacă o structură trimite la ea însăși, contorul ei nu scade niciodată la zero.
<?php
$a = ['eins'];
$a[] = &$a; // array-ul conține o referință la el însuși
unset($a); // nimeni nu mai poate accesa array-ul, dar refcount rămâne 1
Potrivit manualului, asemenea referințe circulare apar mai ales la obiecte, de exemplu în structuri părinte-copil sau în parsere. Scurgerile izolate sunt inofensive, deoarece PHP curăță la sfârșitul cererii. În procese care rulează mult timp, precum workerii de cozi de așteptare sau rulările mari de teste, milioane de astfel de cazuri se pot însuma însă într-o problemă reală de memorie.
Garbage collectorul de cicluri
Din PHP 5.3.0, PHP folosește un algoritm după Bacon și Rajan pentru a găsi asemenea cicluri. Ideea de bază: un ciclu poate apărea doar dacă un contor scade la o valoare mai mare decât zero. Numai aceste valori le reține PHP ca posibile rădăcini.
- Posibilele rădăcini ajung într-un buffer. Manualul indică drept dimensiune implicită 10.000 de intrări; pragul valabil în prezent îl arată
gc_status()în câmpulthreshold. - Când bufferul este plin, PHP parcurge toate valorile accesibile și le scade de probă contoarele.
- Într-o a doua trecere se aplică: valorile cu contor zero sunt gunoi, iar la toate celelalte scăderea este anulată.
- Rădăcinile sunt scoase din buffer, iar toate valorile recunoscute ca gunoi sunt eliberate.
Controlul se face prin zend.enable_gc (implicit activ) sau, la execuție, cu gc_enable() și gc_disable(). gc_collect_cycles() forțează o rulare și returnează numărul de cicluri colectate.
Măsurarea și observarea memoriei
| Funcție | Scop |
|---|---|
memory_get_usage() |
memoria ocupată în prezent de script |
memory_get_usage(true) |
memoria rezervată de managerul de memorie de la sistem; față de această valoare este verificat memory_limit |
memory_get_peak_usage() |
valoarea maximă a memoriei ocupate |
gc_status() |
rulări, cicluri colectate, prag, rădăcini; din PHP 8.3.0 și durate de execuție |
gc_collect_cycles() |
declanșează imediat colectarea ciclurilor |
WeakMap |
asociere cu obiecte, fără a împiedica eliberarea lor, din PHP 8.0 |
Potrivit manualului, memory_get_usage() înregistrează doar memoria pe care PHP o solicită prin propriul manager de memorie. Ce obțin extensiile direct de la sistemul de operare lipsește din aceste cifre. Pentru limita de memorie a întregului proces sunt de aceea responsabile sistemul de operare sau containerul.
Gestionarea memoriei și PHP2Shell
Descrierile ambelor vulnerabilități duc direct la acest subiect. La CVE-2026-109618, o stare Out-of-Memory poate întrerupe PHP în timp ce datele cererii sunt inițializate doar parțial; la curățare, date invalide pot fi tratate apoi ca valide. Ce se întâmplă intern la o depășire de memorie explică pagina memory_limit și Out-of-Memory. La CVE-2026-109619, starea internă a unei cereri poate rămâne după eliberarea memoriei și poate fi folosită de o cerere ulterioară din același worker, înainte de a fi resetată. Desfășurarea de la pornire până la curățare o descrie pagina Ciclul de viață al cererii.
Ca măsură de tranziție, php.net recomandă setarea memory_limit=-1 astfel încât aplicațiile să nu o poată suprascrie, de exemplu cu php_admin_value în pool-ul FPM, și limitarea memoriei prin containere sau cgroups, nu prin RLIMIT_AS sau RLIMIT_DATA. Cum se face aceasta cu systemd arată articolul Setarea limitelor de memorie cu systemd și cgroups.
; pool FPM: măsură de tranziție conform php.net, apoi repornește toți workerii
php_admin_value[memory_limit] = -1
Fără o limită prin container sau cgroup, un singur proces poate suprasolicita serverul. Și nicio setare nu înlocuiește actualizarea la PHP 8.5.12, 8.4.27, 8.3.36 sau 8.2.35 împreună cu repornirea tuturor workerilor, deoarece doar repornirea scoate procesele vechi din memorie.
Cum ajută PHP SOS concret?
- Verificăm setările de memorie efective pentru fiecare pool și dacă aplicațiile le pot suprascrie.
- Aplicăm măsurile de tranziție cu o limită de memorie la nivel de container sau cgroup și testăm sarcina.
- Instalăm actualizarea, repornim toți workerii și dovedim că nu mai rulează procese vechi, în cadrul serviciului nostru de patch PHP.
- Evaluăm erorile de memorie și workerii întrerupți din loguri, începând cu 20.10.2026.
Întrebări frecvente
Ce este numărarea referințelor în PHP?
PHP numără, pentru fiecare valoare, câte variabile sau elemente trimit la ea. Când contorul scade la zero, memoria este eliberată imediat. Astfel, cea mai mare parte a memoriei este returnată fără o curățare separată.
La ce mai are nevoie PHP atunci de un garbage collector?
Pentru cicluri. Dacă un array trimite la el însuși sau două obiecte se cunosc reciproc, contorul nu scade niciodată la zero, chiar dacă nimeni nu mai accesează structura. Garbage collectorul de cicluri găsește și elimină asemenea structuri.
Eliberează PHP memoria la sfârșitul unei cereri?
Da, memoria pe care un script o ocupă în timpul unei cereri este curățată de PHP la sfârșitul cererii. La PHP2Shell, CVE-2026-109619 privește însă tocmai cazul în care starea internă poate rămâne după eliberare și poate fi folosită de o cerere ulterioară din același worker.
De ce recomandă php.net memory_limit=-1 ca măsură de tranziție?
Vulnerabilitatea CVE-2026-109618 este legată de o stare Out-of-Memory. php.net recomandă setarea memory_limit=-1 astfel încât aplicațiile să nu o poată suprascrie și limitarea memoriei prin containere sau cgroups. Aceasta nu este o protecție completă și nu înlocuiește actualizarea.