Lyhyesti: PHP vapauttaa muistia ennen kaikkea viitelaskennalla: jokainen arvo tietää, kuinka moni muuttuja osoittaa siihen, ja se poistetaan heti, kun tämä laskuri saavuttaa nollan. Kehäviittauksille, joita ei näin koskaan vapautettaisi, on olemassa kehiä keräävä roskienkerääjä. Jokaisen pyynnön päättyessä PHP lisäksi siivoaa kaiken, minkä pyyntö on varannut. Kumpikin PHP2Shell-aukko kohdistuu muistitiloihin: Out-of-Memory-tilaan pyynnön käsittelyn aikana ja sisäiseen tilaan, joka säilyy vapautuksen jälkeen liian pitkään.
Miten PHP hallitsee muistia: yleiskatsaus
Kielissä kuten C ohjelmoijan on itse pyydettävä muistia ja vapautettava se. PHP ottaa tämän huolehdittavakseen. Jokainen muuttuja, taulukko ja olio varaa muistia, jota PHP hallitsee omalla muistinhallinnallaan. Rajan tälle asettaa memory_limit, joka on käsikirjan mukaan oletuksena 128M.
Päättäjälle yksi seikka on keskeinen: perinteisessä PHP-FPM- tai mod_php-käytössä pitkäikäiset worker-prosessit käsittelevät peräkkäin monta pyyntöä. Mitä pyyntö jättää jälkeensä, sen on oltava luotettavasti siivottu ennen seuraavaa. Juuri tässä kohdassa on toinen PHP2Shell-aukoista.
Viitelaskenta: lasketaan, kuka käyttää arvoa
PHP tallentaa jokaisen arvon säiliöön, jota kutsutaan nimellä zval. Tyypin ja sisällön lisäksi siinä on käsikirjan mukaan viitelaskuri (refcount) ja merkintä siitä, onko arvo osa PHP-viitettä merkillä & (is_ref).
<?php
$a = str_repeat('x', 1000); // yksi arvo, refcount 1
$b = $a; // ei kopioida, refcount 2
$b .= 'y'; // vasta nyt kopioidaan (copy-on-write)
unset($a); // alkuperäisen refcount laskee
// Kun laskuri saavuttaa arvon 0, PHP vapauttaa muistin heti.
Periaatetta kutsutaan nimellä copy-on-write: PHP kopioi arvon vasta, kun jokin muuttujista muuttaa sitä. Se säästää paljon muistia. Muuttujan laskurin voi tehdä näkyväksi Xdebug-laajennuksella ja funktiolla xdebug_debug_zval(). Käsikirja huomauttaa, että tarkat luvut voivat vaihdella PHP-versioittain.
Kehät: kun viitelaskenta ei riitä
Viitelaskennalla on tunnettu heikkous. Jos rakenne osoittaa itseensä, sen laskuri ei koskaan putoa nollaan.
<?php
$a = ['eins'];
$a[] = &$a; // taulukko sisältää viitteen itseensä
unset($a); // kukaan ei enää pääse siihen käsiksi, mutta refcount pysyy arvossa 1
Käsikirjan mukaan tällaisia kehäviittauksia esiintyy erityisesti olioissa, esimerkiksi vanhempi–lapsi-rakenteissa tai jäsentimissä. Yksittäiset vuodot ovat vaarattomia, koska PHP siivoaa pyynnön päättyessä. Pitkään käynnissä olevissa prosesseissa, kuten jonotyöntekijöissä tai suurissa testiajoissa, miljoonat tällaiset voivat kuitenkin kasvaa todelliseksi muisti-ongelmaksi.
Kehiä keräävä roskienkerääjä
Versiosta PHP 5.3.0 lähtien PHP käyttää Baconin ja Rajanin algoritmia tällaisten kehien löytämiseen. Perusajatus: kehä voi syntyä vain, jos laskuri laskee arvoon, joka on suurempi kuin nolla. Vain nämä arvot PHP muistaa mahdollisina juurina.
- Mahdolliset juuret päätyvät puskuriin. Käsikirja mainitsee oletuskooksi 10 000 alkiota; nykyisen kynnysarvon näyttää
gc_status()kentässäthreshold. - Kun puskuri on täynnä, PHP käy läpi kaikki tavoitettavissa olevat arvot ja pienentää kokeeksi niiden laskureita.
- Toisella läpikäynnillä pätee: arvot, joiden laskuri on nolla, ovat roskaa; kaikilla muilla pienennys perutaan.
- Juuret poistetaan puskurista ja kaikki roskaksi tunnistetut arvot vapautetaan.
Toimintaa ohjataan asetuksella zend.enable_gc (oletuksena päällä) tai ajon aikana funktioilla gc_enable() ja gc_disable(). gc_collect_cycles() pakottaa ajon ja palauttaa kerättyjen kehien määrän.
Muistin mittaus ja seuranta
| Funktio | Tarkoitus |
|---|---|
memory_get_usage() |
komentosarjan tällä hetkellä varaama muisti |
memory_get_usage(true) |
muistinhallinnan järjestelmältä varaama muisti; tätä arvoa vasten memory_limit tarkistetaan |
memory_get_peak_usage() |
varatun muistin huippuarvo |
gc_status() |
ajot, kerätyt kehät, kynnysarvo, juuret; versiosta PHP 8.3.0 lähtien myös ajoajat |
gc_collect_cycles() |
käynnistää kehien keräyksen heti |
WeakMap |
liitos olioihin estämättä niiden vapautusta, versiosta PHP 8.0 lähtien |
Käsikirjan mukaan memory_get_usage() laskee vain muistin, jonka PHP pyytää omalla muistinhallinnallaan. Se, minkä laajennukset hakevat suoraan käyttöjärjestelmältä, puuttuu näistä luvuista. Koko prosessin muistirajasta vastaavat siksi käyttöjärjestelmä tai kontti.
Muistinhallinta ja PHP2Shell
Molempien aukkojen kuvaukset johtavat suoraan tähän aiheeseen. Haavoittuvuudessa CVE-2026-109618 Out-of-Memory-tila voi keskeyttää PHP:n, kun pyyntödata on vasta osittain alustettu; siivouksessa virheellistä dataa voidaan sitten käsitellä kelvollisena. Mitä muistin ylityksessä tapahtuu sisäisesti, selittää sivu memory_limit ja Out-of-Memory. Haavoittuvuudessa CVE-2026-109619 pyynnön sisäinen tila voi säilyä muistin vapautuksen jälkeen, ja myöhempi pyyntö samassa workerissa voi käyttää sitä ennen kuin se nollataan. Kulun alusta siivoukseen kuvaa sivu Pyynnön elinkaari.
Väliaikaisena toimenpiteenä php.net suosittelee asettamaan memory_limit=-1 niin, etteivät sovellukset voi ylikirjoittaa sitä, esimerkiksi php_admin_value-direktiivillä FPM-poolissa, ja rajaamaan muistia sen sijaan kontilla tai cgroupeilla, ei RLIMIT_AS- tai RLIMIT_DATA-rajoilla. Miten se tehdään systemd:llä, näyttää artikkeli Muistirajojen asettaminen systemd:llä ja cgroupeilla.
; FPM-pooli: php.netin mukainen väliaikainen toimenpide, sen jälkeen kaikkien workerien uudelleenkäynnistys
php_admin_value[memory_limit] = -1
Ilman kontin tai cgroupin kautta asetettua rajaa yksittäinen prosessi voi kuormittaa palvelimen täyteen. Eikä mikään asetus korvaa päivitystä versioon PHP 8.5.12, 8.4.27, 8.3.36 tai 8.2.35 sekä kaikkien workerien uudelleenkäynnistystä, sillä vasta uudelleenkäynnistys poistaa vanhat prosessit muistista.
Miten PHP SOS auttaa konkreettisesti?
- Tarkistamme voimassa olevat muistiasetukset poolikohtaisesti ja sen, voivatko sovellukset ylikirjoittaa ne.
- Toteutamme väliaikaiset toimenpiteet muistirajalla kontin tai cgroupin tasolla ja testaamme kuorman.
- Asennamme päivityksen, käynnistämme kaikki workerit uudelleen ja osoitamme, ettei vanhoja prosesseja enää ole käynnissä, PHP-korjauspalvelumme puitteissa.
- Analysoimme lokeista muistivirheet ja keskeytyneet workerit 20.10.2026 alkaen.
Usein kysytyt kysymykset
Mitä viitelaskenta on PHP:ssä?
PHP laskee jokaiselle arvolle, kuinka moni muuttuja tai alkio viittaa siihen. Kun laskuri putoaa nollaan, muisti vapautetaan heti. Näin suurin osa muistista palautetaan ilman erillistä siivousajoa.
Miksi PHP tarvitsee silti roskienkerääjän?
Kehiä varten. Jos taulukko viittaa itseensä tai kaksi oliota tuntee toisensa, laskuri ei koskaan putoa nollaan, vaikka kukaan ei enää käytä niitä. Kehiä keräävä roskienkerääjä löytää ja poistaa tällaiset rakenteet.
Vapauttaako PHP muistin pyynnön päättyessä?
Kyllä, muistin, jonka komentosarja varaa pyynnön aikana, PHP siivoaa pyynnön päättyessä. PHP2Shellissä CVE-2026-109619 koskee kuitenkin juuri tapausta, jossa sisäinen tila voi säilyä vapautuksen jälkeen ja myöhempi pyyntö samassa workerissa voi käyttää sitä.
Miksi php.net suosittelee memory_limit=-1 väliaikaisena toimenpiteenä?
Haavoittuvuus CVE-2026-109618 liittyy Out-of-Memory-tilaan. php.net suosittelee asettamaan memory_limit=-1 niin, etteivät sovellukset voi ylikirjoittaa sitä, ja rajaamaan muistia sen sijaan kontilla tai cgroupeilla. Se ei ole täydellinen suoja eikä korvaa päivitystä.