En bref : PHP libère la mémoire avant tout par le comptage de références : chaque valeur sait combien de variables pointent vers elle et est supprimée dès que ce compteur atteint zéro. Pour les références circulaires, qui ne seraient jamais libérées ainsi, il existe un garbage collector de cycles. À la fin de chaque requête, PHP nettoie en outre tout ce que la requête a occupé. Les deux failles PHP2Shell s'attaquent à des états de la mémoire : un état de dépassement de mémoire pendant le traitement de la requête, et un état interne qui subsiste trop longtemps après la libération.
Comment PHP gère la mémoire : vue d'ensemble
Dans des langages comme C, le programmeur doit demander et libérer lui-même la mémoire. PHP s'en charge à sa place. Chaque variable, chaque tableau et chaque objet occupe de la mémoire que PHP gère via son propre gestionnaire de mémoire. La limite est fixée par memory_limit, qui vaut par défaut 128M selon le manuel.
Pour les décideurs, un point est central : en exploitation classique avec PHP-FPM ou mod_php, des processus workers de longue durée traitent de nombreuses requêtes les unes après les autres. Ce qu'une requête laisse derrière elle doit être nettoyé de façon fiable avant la suivante. C'est précisément là que se situe l'une des failles PHP2Shell.
Comptage de références : compter qui utilise une valeur
PHP stocke chaque valeur dans un conteneur appelé zval. Outre le type et le contenu, il porte selon le manuel un compteur de références (refcount) et un marqueur indiquant si la valeur fait partie d'une référence PHP avec & (is_ref).
<?php
$a = str_repeat('x', 1000); // une valeur, refcount 1
$b = $a; // pas de copie, refcount 2
$b .= 'y'; // ce n'est qu'ici qu'on copie (copy-on-write)
unset($a); // le refcount de l'original diminue
// Quand un compteur atteint 0, PHP libère immédiatement la mémoire.
Le principe s'appelle copy-on-write : PHP ne copie une valeur que lorsqu'une des variables la modifie. Cela économise beaucoup de mémoire. Le compteur d'une variable peut être rendu visible avec l'extension Xdebug et la fonction xdebug_debug_zval(). Le manuel précise que les chiffres exacts peuvent différer selon la version de PHP.
Cycles : quand le comptage de références ne suffit pas
Le comptage de références a une faiblesse connue. Si une structure pointe vers elle-même, son compteur ne tombe jamais à zéro.
<?php
$a = ['eins'];
$a[] = &$a; // le tableau contient une référence vers lui-même
unset($a); // plus personne ne peut y accéder, mais le refcount reste 1
Selon le manuel, de telles références circulaires apparaissent surtout avec des objets, par exemple dans des structures parent-enfant ou des analyseurs syntaxiques. Quelques fuites isolées sont inoffensives, car PHP nettoie à la fin de la requête. Dans des processus de longue durée comme les workers de files d'attente ou de grandes exécutions de tests, des millions d'entre elles peuvent toutefois s'additionner en un véritable problème de mémoire.
Le garbage collector de cycles
Depuis PHP 5.3.0, PHP utilise un algorithme de Bacon et Rajan pour trouver de tels cycles. L'idée de base : un cycle ne peut naître que lorsqu'un compteur tombe à une valeur supérieure à zéro. PHP ne retient comme racines possibles que ces valeurs.
- Les racines possibles atterrissent dans un tampon. Le manuel indique comme taille par défaut 10 000 entrées ; le seuil actuellement en vigueur est affiché par
gc_status()dans le champthreshold. - Lorsque le tampon est plein, PHP parcourt toutes les valeurs atteignables et diminue à titre d'essai leurs compteurs.
- Dans un second passage : les valeurs dont le compteur est à zéro sont des déchets, pour toutes les autres la diminution est annulée.
- Les racines sont retirées du tampon et toutes les valeurs reconnues comme déchets sont libérées.
Le contrôle se fait via zend.enable_gc (activé par défaut) ou, à l'exécution, avec gc_enable() et gc_disable(). gc_collect_cycles() force une passe et renvoie le nombre de cycles collectés.
Mesurer et observer la mémoire
| Fonction | Rôle |
|---|---|
memory_get_usage() |
mémoire actuellement occupée par le script |
memory_get_usage(true) |
mémoire réservée auprès du système par le gestionnaire de mémoire ; c'est par rapport à cette valeur que memory_limit est vérifiée |
memory_get_peak_usage() |
pic de la mémoire occupée |
gc_status() |
passes, cycles collectés, seuil, racines ; depuis PHP 8.3.0 aussi les durées |
gc_collect_cycles() |
déclencher immédiatement la collecte des cycles |
WeakMap |
association à des objets sans empêcher leur libération, depuis PHP 8.0 |
Selon le manuel, memory_get_usage() ne saisit que la mémoire que PHP demande via son propre gestionnaire de mémoire. Ce que les extensions obtiennent directement auprès du système d'exploitation ne figure pas dans ces chiffres. Pour la limite de mémoire de l'ensemble du processus, ce sont donc le système d'exploitation ou le conteneur qui sont compétents.
Gestion de la mémoire et PHP2Shell
Les descriptions des deux failles mènent directement à ce sujet. Avec CVE-2026-109618, un état de dépassement de mémoire peut interrompre PHP alors que les données de la requête ne sont que partiellement initialisées ; lors du nettoyage, des données invalides peuvent alors être traitées comme valides. Ce qui se passe en interne lors d'un dépassement de mémoire est expliqué sur la page memory_limit et Out-of-Memory. Avec CVE-2026-109619, un état interne d'une requête peut subsister après la libération de la mémoire et être utilisé par une requête ultérieure dans le même worker avant d'être réinitialisé. Le déroulement, du démarrage au nettoyage, est décrit sur la page Le cycle de vie d'une requête.
Comme mesure transitoire, php.net recommande de définir memory_limit=-1 de manière que les applications ne puissent pas le redéfinir, par exemple avec php_admin_value dans le pool FPM, et de limiter plutôt la mémoire via des conteneurs ou des cgroups, non via RLIMIT_AS ou RLIMIT_DATA. La manière de procéder avec systemd est montrée dans l'article Définir des limites de mémoire avec systemd et cgroups.
; Pool FPM : mesure transitoire selon php.net, puis redémarrer tous les workers
php_admin_value[memory_limit] = -1
Sans limite via conteneur ou cgroup, un seul processus peut saturer le serveur. Et aucun réglage ne remplace la mise à jour vers PHP 8.5.12, 8.4.27, 8.3.36 ou 8.2.35 avec redémarrage de tous les workers, car seul le redémarrage retire les anciens processus de la mémoire.
Comment PHP SOS aide-t-il concrètement ?
- Nous vérifions les réglages de mémoire effectifs de chaque pool et si les applications peuvent les redéfinir.
- Nous appliquons les mesures transitoires avec une limite de mémoire au niveau du conteneur ou du cgroup et testons la charge.
- Nous installons la mise à jour, redémarrons tous les workers et prouvons qu'aucun ancien processus ne tourne plus, dans le cadre de notre service de correctifs PHP.
- Nous analysons les erreurs de mémoire et les workers interrompus dans les logs à partir du 20 octobre 2026.
Questions fréquentes
Qu'est-ce que le comptage de références en PHP ?
PHP compte, pour chaque valeur, combien de variables ou d'éléments y font référence. Lorsque le compteur tombe à zéro, la mémoire est libérée immédiatement. Ainsi, l'essentiel de la mémoire est restitué sans passe de nettoyage dédiée.
À quoi sert alors encore un garbage collector à PHP ?
Aux cycles. Si un tableau se référence lui-même ou si deux objets se connaissent mutuellement, le compteur ne tombe jamais à zéro, même si plus personne n'y accède. Le garbage collector de cycles trouve et supprime de telles structures.
PHP libère-t-il la mémoire à la fin d'une requête ?
Oui, la mémoire qu'un script occupe pendant une requête est nettoyée par PHP à la fin de celle-ci. Avec PHP2Shell, CVE-2026-109619 concerne toutefois précisément le cas où un état interne peut subsister après la libération et être utilisé par une requête ultérieure dans le même worker.
Pourquoi php.net recommande-t-il memory_limit=-1 comme mesure transitoire ?
La faille CVE-2026-109618 est liée à un état de dépassement de mémoire (out-of-memory). php.net recommande de définir memory_limit=-1 de manière que les applications ne puissent pas le redéfinir, et de limiter plutôt la mémoire via des conteneurs ou des cgroups. Ce n'est pas une protection complète et cela ne remplace pas la mise à jour.