In breve: PHP libera la memoria soprattutto tramite il conteggio dei riferimenti: ogni valore sa quante variabili puntano a esso e viene eliminato non appena il contatore raggiunge zero. Per i riferimenti circolari, che in questo modo non verrebbero mai liberati, esiste un garbage collector dei cicli. Al termine di ogni richiesta PHP ripulisce inoltre tutto ciò che la richiesta ha occupato. Entrambe le vulnerabilità PHP2Shell agiscono su stati della memoria: su uno stato di Out-of-Memory durante l'elaborazione della richiesta e su uno stato interno che dopo la liberazione resta presente troppo a lungo.
Come PHP gestisce la memoria: la panoramica
In linguaggi come C il programmatore deve richiedere e restituire la memoria da sé. PHP lo toglie di mezzo. Ogni variabile, ogni array e ogni oggetto occupa memoria che PHP gestisce tramite il proprio gestore della memoria. Il limite è fissato da memory_limit, secondo il manuale 128M per impostazione predefinita.
Per chi decide, un punto è centrale: nell'esercizio classico con PHP-FPM o mod_php girano processi worker di lunga durata, che elaborano molte richieste una dopo l'altra. Ciò che una richiesta lascia deve essere ripulito in modo affidabile prima della successiva. Proprio qui si trova una delle vulnerabilità PHP2Shell.
Conteggio dei riferimenti: contare chi usa un valore
PHP memorizza ogni valore in un contenitore, il cosiddetto zval. Oltre a tipo e contenuto, secondo il manuale porta un contatore di riferimenti (refcount) e un indicatore che segnala se il valore fa parte di un riferimento PHP con & (is_ref).
<?php
$a = str_repeat('x', 1000); // un valore, refcount 1
$b = $a; // nessuna copia, refcount 2
$b .= 'y'; // solo ora si copia (copy-on-write)
unset($a); // il refcount dell'originale scende
// Quando un contatore raggiunge 0, PHP libera subito la memoria.
Il principio si chiama copy-on-write: PHP copia un valore solo quando una delle variabili lo modifica. Ciò fa risparmiare molta memoria. Il contatore di una variabile si può rendere visibile con l'estensione Xdebug e la funzione xdebug_debug_zval(). Il manuale avverte che i numeri esatti possono variare a seconda della versione di PHP.
Cicli: quando il conteggio dei riferimenti non basta
Il conteggio dei riferimenti ha un punto debole noto. Se una struttura punta a sé stessa, il suo contatore non scende mai a zero.
<?php
$a = ['eins'];
$a[] = &$a; // l'array contiene un riferimento a sé stesso
unset($a); // nessuno può più accedervi, ma il refcount resta 1
Secondo il manuale, questi riferimenti circolari compaiono soprattutto negli oggetti, ad esempio nelle strutture genitore-figlio o nei parser. Singole perdite sono innocue, perché PHP ripulisce al termine della richiesta. Nei processi di lunga durata, come i worker delle code o le grandi esecuzioni di test, milioni di esse possono però sommarsi in un vero problema di memoria.
Il garbage collector dei cicli
Da PHP 5.3.0 PHP usa un algoritmo di Bacon e Rajan per trovare questi cicli. L'idea di fondo: un ciclo può nascere solo quando un contatore scende a un valore maggiore di zero. Solo questi valori PHP li memorizza come possibili radici.
- Le possibili radici finiscono in un buffer. Il manuale indica come dimensione predefinita 10.000 voci; la soglia attualmente valida la mostra
gc_status()nel campothreshold. - Quando il buffer è pieno, PHP percorre tutti i valori raggiungibili e ne diminuisce in via sperimentale i contatori.
- In un secondo passaggio vale: i valori con contatore zero sono spazzatura, per tutti gli altri la diminuzione viene annullata.
- Le radici vengono rimosse dal buffer e tutti i valori riconosciuti come spazzatura vengono liberati.
Il controllo avviene tramite zend.enable_gc (attivo per impostazione predefinita) oppure in fase di esecuzione con gc_enable() e gc_disable(). gc_collect_cycles() forza un'esecuzione e restituisce il numero di cicli raccolti.
Misurare e osservare la memoria
| Funzione | Scopo |
|---|---|
memory_get_usage() |
memoria attualmente occupata dallo script |
memory_get_usage(true) |
memoria riservata dal gestore della memoria presso il sistema; memory_limit viene verificato rispetto a questo valore |
memory_get_peak_usage() |
picco della memoria occupata |
gc_status() |
esecuzioni, cicli raccolti, soglia, radici; da PHP 8.3.0 anche i tempi di esecuzione |
gc_collect_cycles() |
avviare subito la raccolta dei cicli |
WeakMap |
associazione a oggetti senza impedirne la liberazione, da PHP 8.0 |
Secondo il manuale memory_get_usage() rileva solo la memoria che PHP richiede tramite il proprio gestore della memoria. Ciò che le estensioni prelevano direttamente dal sistema operativo manca da questi numeri. Per il limite di memoria dell'intero processo sono quindi competenti il sistema operativo o il container.
Gestione della memoria e PHP2Shell
Le descrizioni di entrambe le vulnerabilità portano direttamente a questo tema. In CVE-2026-109618 uno stato di Out-of-Memory può interrompere PHP mentre i dati della richiesta sono inizializzati solo in parte; durante la pulizia dati non validi possono allora essere trattati come validi. Che cosa accada internamente in caso di superamento della memoria lo spiega la pagina memory_limit e Out-of-Memory. In CVE-2026-109619 lo stato interno di una richiesta può rimanere presente dopo la liberazione della memoria ed essere usato da una richiesta successiva nello stesso worker, prima di essere reimpostato. Lo svolgimento dall'avvio alla pulizia lo descrive la pagina Il ciclo di vita della richiesta.
Come misura transitoria php.net raccomanda di impostare memory_limit=-1 in modo che le applicazioni non possano sovrascriverlo, ad esempio con php_admin_value nel pool FPM, e di limitare invece la memoria tramite container o cgroup, non tramite RLIMIT_AS o RLIMIT_DATA. Come fare con systemd lo mostra l'articolo Impostare limiti di memoria con systemd e cgroup.
; Pool FPM: misura transitoria secondo php.net, poi riavviare tutti i worker
php_admin_value[memory_limit] = -1
Senza un limite tramite container o cgroup, un singolo processo può saturare il server. E nessuna impostazione sostituisce l'aggiornamento a PHP 8.5.12, 8.4.27, 8.3.36 o 8.2.35 con il riavvio di tutti i worker, perché solo il riavvio rimuove dalla memoria i vecchi processi.
Come aiuta concretamente PHP SOS?
- Verifichiamo le impostazioni di memoria effettive per ogni pool e se le applicazioni possono sovrascriverle.
- Attuiamo le misure transitorie con un limite di memoria a livello di container o cgroup e testiamo il carico.
- Installiamo l'aggiornamento, riavviamo tutti i worker e dimostriamo che non girano più vecchi processi, nell'ambito del nostro servizio di patch PHP.
- Analizziamo gli errori di memoria e i worker interrotti nei log dal 20/10/2026.
Domande frequenti
Che cos'è il conteggio dei riferimenti in PHP?
PHP conta, per ogni valore, quante variabili o elementi vi fanno riferimento. Quando il contatore scende a zero, la memoria viene liberata subito. Così la maggior parte della memoria viene restituita senza un ciclo di pulizia a parte.
Perché PHP ha ancora bisogno di un garbage collector?
Per i cicli. Se un array fa riferimento a sé stesso o due oggetti si conoscono a vicenda, il contatore non scende mai a zero, anche quando nessuno vi accede più. Il garbage collector dei cicli trova e rimuove queste strutture.
PHP libera la memoria al termine di una richiesta?
Sì, la memoria che uno script occupa durante una richiesta viene ripulita da PHP al termine della richiesta. Con PHP2Shell, però, CVE-2026-109619 riguarda proprio il caso in cui uno stato interno resti presente dopo la liberazione e possa essere usato da una richiesta successiva nello stesso worker.
Perché php.net raccomanda memory_limit=-1 come misura transitoria?
La vulnerabilità CVE-2026-109618 è legata a uno stato di Out-of-Memory. php.net raccomanda di impostare memory_limit=-1 in modo che le applicazioni non possano sovrascriverlo e di limitare invece la memoria tramite container o cgroup. Non è una protezione completa e non sostituisce l'aggiornamento.