Situación actual: PHP2Shell. Dos vulnerabilidades críticas de PHP (CVSS 9.2) permiten ejecutar código malicioso sin autenticación. Parches desde el 21/10/2026.Qué hacer ahora →

Gestión de memoria en PHP: conteo de referencias y recolección de basura

PHP gestiona la memoria con conteo de referencias y un recolector de basura para los ciclos. Esta página explica la mecánica, las funciones de medición clave y por qué ambas vulnerabilidades de PHP2Shell tienen que ver con estados de memoria.

Actualizado 5 min de lecturaConocimiento de PHP

En resumen: PHP libera la memoria sobre todo mediante el conteo de referencias: cada valor sabe cuántas variables apuntan a él y se elimina en cuanto ese contador llega a cero. Para las referencias circulares, que así nunca se liberarían, existe un recolector de basura de ciclos. Además, al final de cada petición PHP limpia todo lo que esta ha ocupado. Ambas vulnerabilidades de PHP2Shell inciden en estados de memoria: en un estado de falta de memoria durante el procesamiento de la petición y en un estado interno que permanece demasiado tiempo tras la liberación.

Cómo gestiona PHP la memoria: visión general

En lenguajes como C, el programador debe solicitar y liberar la memoria por sí mismo. PHP se lo ahorra. Cada variable, cada array y cada objeto ocupa memoria que PHP gestiona mediante su propio administrador de memoria. El límite lo fija memory_limit, que según el manual es por defecto 128M.

Para quienes toman decisiones hay un punto central: en el funcionamiento clásico con PHP-FPM o mod_php, hay procesos worker de larga vida que atienden muchas peticiones una tras otra. Lo que deja una petición debe estar limpio de forma fiable antes de la siguiente. Justo ahí se sitúa una de las vulnerabilidades de PHP2Shell.

Conteo de referencias: contar quién usa un valor

PHP guarda cada valor en un contenedor llamado zval. Además del tipo y el contenido, según el manual lleva un contador de referencias (refcount) y una marca que indica si el valor forma parte de una referencia de PHP con & (is_ref).

<?php
$a = str_repeat('x', 1000);  // un valor, refcount 1
$b = $a;                     // no se copia, refcount 2
$b .= 'y';                   // solo ahora se copia (copy-on-write)
unset($a);                   // el refcount del original baja
// Cuando un contador llega a 0, PHP libera la memoria de inmediato.

El principio se llama copy-on-write: PHP copia un valor solo cuando una de las variables lo modifica. Esto ahorra mucha memoria. El contador de una variable puede hacerse visible con la extensión Xdebug y la función xdebug_debug_zval(). El manual advierte de que las cifras exactas pueden variar según la versión de PHP.

Ciclos: cuando el conteo de referencias no basta

El conteo de referencias tiene una debilidad conocida. Si una estructura apunta a sí misma, su contador nunca baja a cero.

<?php
$a = ['eins'];
$a[] = &$a;   // el array contiene una referencia a sí mismo
unset($a);    // nadie puede acceder ya a él, pero el refcount sigue en 1

Según el manual, estas referencias circulares aparecen sobre todo en objetos, por ejemplo en estructuras padre-hijo o en analizadores sintácticos. Las fugas aisladas son inofensivas, porque PHP limpia al final de la petición. En procesos de larga duración, como workers de colas o grandes ejecuciones de pruebas, sin embargo, millones de ellas pueden sumar un problema de memoria real.

El recolector de basura de ciclos

Desde PHP 5.3.0, PHP utiliza un algoritmo de Bacon y Rajan para encontrar estos ciclos. La idea básica: un ciclo solo puede surgir si un contador baja a un valor mayor que cero. PHP solo anota esos valores como posibles raíces.

  1. Las posibles raíces van a un búfer. El manual indica como tamaño por defecto 10 000 entradas; el umbral válido en cada momento lo muestra gc_status() en el campo threshold.
  2. Cuando el búfer está lleno, PHP recorre todos los valores alcanzables y reduce de forma provisional sus contadores.
  3. En una segunda pasada: los valores con contador cero son basura; en todos los demás se deshace la reducción.
  4. Las raíces se retiran del búfer y se liberan todos los valores reconocidos como basura.

Se controla mediante zend.enable_gc (activado por defecto) o, en tiempo de ejecución, con gc_enable() y gc_disable(). gc_collect_cycles() fuerza una ejecución y devuelve el número de ciclos recogidos.

Medir y observar la memoria

Función Finalidad
memory_get_usage() memoria ocupada actualmente por el script
memory_get_usage(true) memoria reservada al sistema por el administrador de memoria; contra este valor se comprueba memory_limit
memory_get_peak_usage() máximo de memoria ocupada
gc_status() ejecuciones, ciclos recogidos, umbral, raíces; desde PHP 8.3.0 también tiempos
gc_collect_cycles() lanzar de inmediato la recolección de ciclos
WeakMap asociación a objetos sin impedir su liberación, desde PHP 8.0

Según el manual, memory_get_usage() solo contabiliza la memoria que PHP solicita a través de su propio administrador de memoria. Lo que las extensiones obtienen directamente del sistema operativo no aparece en esas cifras. Del límite de memoria de todo el proceso se encargan, por tanto, el sistema operativo o el contenedor.

Gestión de memoria y PHP2Shell

Las descripciones de ambas vulnerabilidades conducen directamente a este tema. En CVE-2026-109618, un estado de falta de memoria puede interrumpir PHP mientras los datos de la petición están inicializados solo en parte; al limpiar, datos no válidos pueden tratarse entonces como válidos. Qué ocurre internamente en un desbordamiento de memoria lo explica la página memory_limit y Out-of-Memory. En CVE-2026-109619, el estado interno de una petición puede permanecer tras liberar la memoria y ser utilizado por una petición posterior en el mismo worker antes de restablecerse. El proceso desde el inicio hasta la limpieza lo describe la página El ciclo de vida de la petición.

Como medida provisional, php.net recomienda fijar memory_limit=-1 de modo que las aplicaciones no puedan sobrescribirlo, por ejemplo con php_admin_value en el pool de FPM, y limitar la memoria mediante contenedores o cgroups, no mediante RLIMIT_AS ni RLIMIT_DATA. Cómo hacerlo con systemd lo muestra el artículo Fijar límites de memoria con systemd y cgroups.

; Pool de FPM: medida provisional según php.net; después, reiniciar todos los workers
php_admin_value[memory_limit] = -1
Importante

Sin un límite mediante contenedor o cgroup, un solo proceso puede saturar el servidor. Y ningún ajuste sustituye a la actualización a PHP 8.5.12, 8.4.27, 8.3.36 o 8.2.35 con el reinicio de todos los workers, porque solo el reinicio retira de la memoria los procesos antiguos.

¿Cómo ayuda PHP SOS en concreto?

  • Revisamos los ajustes de memoria efectivos de cada pool y si las aplicaciones pueden sobrescribirlos.
  • Aplicamos las medidas provisionales con un límite de memoria a nivel de contenedor o cgroup y probamos la carga.
  • Aplicamos la actualización, reiniciamos todos los workers y demostramos que ya no hay procesos antiguos en ejecución, en el marco de nuestro servicio de parches de PHP.
  • Analizamos los errores de memoria y los workers interrumpidos en los logs desde el 20/10/2026.

Preguntas frecuentes

¿Qué es el conteo de referencias en PHP?

PHP cuenta, para cada valor, cuántas variables o elementos apuntan a él. Cuando el contador baja a cero, la memoria se libera de inmediato. Así se devuelve la mayor parte de la memoria sin una pasada de limpieza propia.

¿Para qué necesita PHP entonces un recolector de basura?

Para los ciclos. Si un array se refiere a sí mismo o dos objetos se conocen mutuamente, el contador nunca baja a cero, aunque ya nadie acceda a ellos. El recolector de basura de ciclos encuentra y elimina esas estructuras.

¿Libera PHP la memoria al final de una petición?

Sí, la memoria que un script ocupa durante una petición la limpia PHP al final de ella. En PHP2Shell, sin embargo, CVE-2026-109619 afecta justo al caso en que un estado interno permanece tras la liberación y puede ser utilizado por una petición posterior en el mismo worker.

¿Por qué recomienda php.net memory_limit=-1 como medida provisional?

La vulnerabilidad CVE-2026-109618 está relacionada con un estado de falta de memoria. php.net recomienda fijar memory_limit=-1 de modo que las aplicaciones no puedan sobrescribirlo, y limitar en su lugar la memoria mediante contenedores o cgroups. No es una protección completa y no sustituye a la actualización.

Fuentes

Contacto inmediato

Cada hora cuenta. Escríbanos ahora.

Describa brevemente el sistema, la versión de PHP y lo que ha observado. Nos pondremos en contacto, aclararemos la situación y trabajaremos en remoto hasta que el incidente quede cerrado y documentado.

Ayuda inmediata