Na kratko: PHP pomnilnik sprošča predvsem prek štetja sklicev: vsaka vrednost ve, koliko spremenljivk kaže nanjo, in se izbriše, brž ko ta števec doseže nič. Za krožna sklicevanja, ki se tako nikoli ne bi sprostila, obstaja garbage collector za cikle. Ob koncu vsake zahteve PHP poleg tega pospravi vse, kar je zahteva zasedla. Obe ranljivosti PHP2Shell napadata stanja pomnilnika: stanje Out-of-Memory med obdelavo zahteve in notranje stanje, ki po sprostitvi ostane predolgo.
Kako PHP upravlja pomnilnik: pregled
V jezikih, kot je C, mora programer pomnilnik zahtevati in ga znova sprostiti sam. PHP to prevzame. Vsaka spremenljivka, vsako polje in vsak objekt zaseda pomnilnik, ki ga PHP upravlja prek lastnega upravljalnika pomnilnika. Mejo za to določa memory_limit, po priročniku privzeto 128M.
Za odločevalce je ena točka osrednja: pri klasičnem delovanju s PHP-FPM ali mod_php tečejo dolgoživi procesi workerjev, ki zaporedoma obdelujejo veliko zahtev. Kar zahteva pusti za seboj, mora biti pred naslednjo zanesljivo pospravljeno. Prav tu leži ena od ranljivosti PHP2Shell.
Štetje sklicev: štetje, kdo uporablja vrednost
PHP vsako vrednost shrani v vsebnik, imenovan zval. Poleg tipa in vsebine po priročniku vodi števec sklicev (refcount) in oznako, ali je vrednost del reference PHP z & (is_ref).
<?php
$a = str_repeat('x', 1000); // ena vrednost, refcount 1
$b = $a; // brez kopiranja, refcount 2
$b .= 'y'; // šele zdaj se kopira (Copy-on-Write)
unset($a); // refcount izvirnika pade
// Ko števec doseže 0, PHP pomnilnik takoj sprosti.
Načelo se imenuje copy-on-write: PHP vrednost kopira šele, ko jo ena od spremenljivk spremeni. To prihrani veliko pomnilnika. Števec spremenljivke je mogoče narediti vidnega z razširitvijo Xdebug in funkcijo xdebug_debug_zval(). Priročnik opozarja, da se natančne številke med različicami PHP lahko razlikujejo.
Cikli: ko štetje sklicev ne zadošča
Štetje sklicev ima znano šibkost. Če struktura kaže sama nase, njen števec nikoli ne pade na nič.
<?php
$a = ['eins'];
$a[] = &$a; // polje vsebuje referenco nase
unset($a); // nihče več ne more dostopati do njega, a refcount ostane 1
Po priročniku se takšna krožna sklicevanja pojavljajo zlasti pri objektih, na primer v strukturah starš-otrok ali parserjih. Posamezne uhajanja so neškodljive, ker PHP ob koncu zahteve pospravi. V dolgo tekočih procesih, kot so delavci čakalnih vrst ali veliki testni zagoni, pa se lahko milijoni takih seštejejo v pravo težavo s pomnilnikom.
Garbage collector za cikle
Od PHP 5.3.0 PHP za iskanje takih ciklov uporablja algoritem po Baconu in Rajanu. Osnovna zamisel: cikel lahko nastane samo, če števec pade na vrednost, večjo od nič. Samo te vrednosti si PHP zapomni kot možne korenine.
- Možne korenine pristanejo v medpomnilniku. Priročnik kot privzeto velikost navaja 10.000 vnosov; trenutno veljavni prag kaže
gc_status()v poljuthreshold. - Ko je medpomnilnik poln, PHP prehodi vse dosegljive vrednosti in poskusno zmanjša njihove števce.
- V drugem prehodu velja: vrednosti s števcem nič so smeti, pri vseh drugih se zmanjšanje razveljavi.
- Korenine se odstranijo iz medpomnilnika, vse vrednosti, prepoznane kot smeti, pa se sprostijo.
Nadzoruje se z zend.enable_gc (privzeto vklopljeno) ali med izvajanjem z gc_enable() in gc_disable(). gc_collect_cycles() zažene izvedbo na silo in vrne število zbranih ciklov.
Merjenje in opazovanje pomnilnika
| Funkcija | Namen |
|---|---|
memory_get_usage() |
trenutno pomnilnik, ki ga zaseda skripta |
memory_get_usage(true) |
pomnilnik, ki ga upravljalnik pomnilnika rezervira pri sistemu; glede na to vrednost se preverja memory_limit |
memory_get_peak_usage() |
najvišja raven zasedenega pomnilnika |
gc_status() |
izvedbe, zbrani cikli, prag, korenine; od PHP 8.3.0 tudi časi izvajanja |
gc_collect_cycles() |
takojšnje sprožitev zbiranja ciklov |
WeakMap |
preslikava na objekte, ne da bi preprečevala njihovo sprostitev, od PHP 8.0 |
Po priročniku memory_get_usage() zajame samo pomnilnik, ki ga PHP zahteva prek lastnega upravljalnika pomnilnika. Kar razširitve vzamejo neposredno pri operacijskem sistemu, v teh številkah manjka. Za mejo pomnilnika celotnega procesa sta zato pristojna operacijski sistem ali vsebnik.
Upravljanje pomnilnika in PHP2Shell
Opisa obeh ranljivosti vodita neposredno k tej temi. Pri CVE-2026-109618 lahko stanje Out-of-Memory prekine PHP, medtem ko so podatki zahteve le delno inicializirani; pri čiščenju se lahko neveljavni podatki nato obravnavajo kot veljavni. Kaj se ob prekoračitvi pomnilnika dogaja notranje, razlaga stran memory_limit in Out-of-Memory. Pri CVE-2026-109619 lahko notranje stanje zahteve po sprostitvi pomnilnika ostane in ga lahko poznejša zahteva v istem workerju uporabi, preden se ponastavi. Potek od zagona do čiščenja opisuje stran Življenjski cikel zahteve.
Kot prehodni ukrep php.net priporoča, da memory_limit=-1 nastavite tako, da ga aplikacije ne morejo prepisati, na primer s php_admin_value v poolu FPM, pomnilnik pa namesto tega omejite prek vsebnikov ali cgroups, ne prek RLIMIT_AS ali RLIMIT_DATA. Kako to storiti s systemd, kaže prispevek Nastavitev omejitev pomnilnika s systemd in cgroups.
; pool FPM: prehodni ukrep po php.net, nato znova zaženite vse workerje
php_admin_value[memory_limit] = -1
Brez omejitve prek vsebnika ali cgroup lahko posamezen proces obremeni strežnik. In nobena nastavitev ne nadomesti posodobitve na PHP 8.5.12, 8.4.27, 8.3.36 ali 8.2.35 skupaj s ponovnim zagonom vseh workerjev, saj stare procese iz pomnilnika odstrani šele ponovni zagon.
Kako PHP SOS pomaga konkretno?
- Preverimo veljavne nastavitve pomnilnika po poolih in ali jih lahko aplikacije prepišejo.
- Prehodne ukrepe uvedemo z omejitvijo pomnilnika na ravni vsebnika ali cgroup in preizkusimo obremenitev.
- Namestimo posodobitev, znova zaženemo vse workerje in dokažemo, da ne teče noben star proces, v okviru naše storitve popravljanja PHP.
- Napake pomnilnika in prekinjene workerje v dnevnikih ovrednotimo od 20. 10. 2026.
Pogosta vprašanja
Kaj je štetje sklicev v PHP?
PHP za vsako vrednost šteje, koliko spremenljivk ali elementov se nanjo sklicuje. Ko števec pade na nič, se pomnilnik takoj sprosti. Tako se večina pomnilnika vrne brez lastnega čiščenja.
Čemu PHP potem sploh potrebuje garbage collector?
Za cikle. Če se polje sklicuje samo nase ali se dva objekta medsebojno poznata, števec nikoli ne pade na nič, tudi če nihče več ne dostopa do njiju. Garbage collector za cikle takšne strukture najde in odstrani.
Ali PHP pomnilnik sprosti ob koncu zahteve?
Da, pomnilnik, ki ga skripta zasede med zahtevo, PHP pospravi ob koncu zahteve. Pri PHP2Shell pa CVE-2026-109619 zadeva ravno primer, da notranje stanje po sprostitvi ostane in ga lahko uporabi poznejša zahteva v istem workerju.
Zakaj php.net kot prehodni ukrep priporoča memory_limit=-1?
Ranljivost CVE-2026-109618 je povezana s stanjem Out-of-Memory. php.net priporoča, da memory_limit=-1 nastavite tako, da ga aplikacije ne morejo prepisati, pomnilnik pa namesto tega omejite prek vsebnikov ali cgroups. To ni popolna zaščita in posodobitve ne nadomesti.