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 →

Fijar límites de memoria con systemd y cgroups

php.net recomienda como medida transitoria de PHP2Shell memory_limit=-1 y un límite de memoria fuera de PHP. Cómo fijar ese límite con systemd y cgroups sin que un solo worker arrastre a todo el servicio.

Actualizado 6 min de lecturaRefuerzo y configuración

En resumen: para la medida transitoria de PHP2Shell memory_limit=-1, PHP-FPM necesita un límite de memoria fuera de PHP. Bajo systemd se fija con MemoryHigh= como freno y MemoryMax= como tope duro, en un archivo complementario para el servicio FPM. Según php.net, ulimit, LimitAS= o LimitDATA= no son adecuados para esto. Compruebe además OOMPolicy=, para que un solo worker no termine todo el servicio.

Una recomendación que al principio sorprende

Entre las medidas transitorias que php.net publicó el 21 de octubre de 2026 sobre PHP2Shell hay una que hizo dudar a muchos administradores: memory_limit=-1, es decir, ningún límite de memoria en el propio PHP. Durante años se consideró buena práctica justo lo contrario.

Una relación con la descripción de la vulnerabilidad es evidente. En CVE-2026-109618, un estado de falta de memoria (out-of-memory) puede interrumpir PHP mientras los datos de la petición solo están inicializados parcialmente. La medida transitoria traslada el límite de memoria desde PHP al sistema operativo. php.net la vincula expresamente a dos condiciones: el valor debe estar bloqueado frente a la sobrescritura y el límite debe proceder de un contenedor o una cgroup, no de RLIMIT_AS ni de RLIMIT_DATA.

Quien aplica solo la primera mitad tiene un servidor sin ningún límite de memoria. Este artículo muestra la segunda mitad. Incluso en conjunto, la medida sigue siendo una transición: la vulnerabilidad solo queda corregida con PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35 o un backport confirmado.

Las cgroups explicadas en dos frases

Los grupos de control, o cgroups, son una función del kernel de Linux que agrupa procesos y limita sus recursos. systemd crea automáticamente una cgroup propia para cada servicio, y todos los procesos del servicio, en el servicio FPM el maestro y todos los workers, pertenecen a ella.

Las opciones de memoria de systemd fijan valores del controlador de memoria de cgroup v2. Si su sistema usa cgroup v2 lo muestra un breve comando:

# "cgroup2fs" significa: cgroup v2 está activo
stat -fc %T /sys/fs/cgroup/

Las versiones actuales de las distribuciones habituales usan cgroup v2 por defecto. Si el comando muestra otra cosa, acláre lo antes del cambio, porque la documentación de systemd describe las antiguas opciones de v1 como obsoletas.

Fijar systemd MemoryMax para PHP-FPM

El límite va en un archivo complementario, el llamado drop-in. Así el archivo de unidad del paquete permanece sin cambios y una actualización no sobrescribe su ajuste. Los nombres de servicio varían según la distribución; el ejemplo usa Debian y Ubuntu con PHP 8.3.

# Abre un editor para el drop-in: sudo systemctl edit php8.3-fpm
# Resultado: /etc/systemd/system/php8.3-fpm.service.d/override.conf
[Service]
MemoryAccounting=yes
MemoryHigh=1536M
MemoryMax=2G
MemorySwapMax=0
OOMPolicy=continue

Las opciones de un vistazo, según la documentación de systemd:

Opción Efecto Indicación para PHP-FPM
MemoryHigh= límite de estrangulamiento: los procesos se frenan con fuerza y se recupera memoria la magnitud de control real, algo por debajo de MemoryMax
MemoryMax= límite absoluto: si no puede mantenerse, el OOM killer se activa en el servicio última línea de defensa
MemorySwapMax= límite para el intercambio 0 impide que el servicio recurra al swap
OOMPolicy= comportamiento tras un evento OOM: continue, stop o kill continue deja que los demás workers sigan funcionando
LimitAS=, LimitDATA= rlimits por proceso según php.net, inadecuados para esta medida

OOMPolicy= merece especial atención. Con stop o kill, tras un evento OOM systemd termina todo el servicio y dejan de funcionar todos los sitios web de ese servicio FPM. Qué valor rige sin indicación propia lo determina el valor por defecto del sistema DefaultOOMPolicy=. Por eso compruebe el valor efectivo en lugar de suponerlo.

Después, reinicie el servicio y bloquee memory_limit en el pool:

sudo systemctl restart php8.3-fpm
systemctl show php8.3-fpm -p MemoryHigh -p MemoryMax -p MemorySwapMax -p OOMPolicy -p MemoryCurrent
; Archivo del pool, p. ej. /etc/php/8.3/fpm/pool.d/seite1.conf
php_admin_value[memory_limit] = -1

Cómo funciona el bloqueo lo describe php_admin_value: bloquear ajustes frente a la sobrescritura.

Encontrar el valor correcto

Un límite demasiado ajustado provoca cortes en el funcionamiento normal; uno demasiado holgado apenas protege. El punto de partida es el consumo real.

  1. Observar durante varios días la memoria necesaria de todos los procesos FPM, también en los picos de carga.
  2. Averiguar la necesidad típica de un worker y multiplicarla por pm.max_children de todos los pools del servicio.
  3. Prever un margen para el proceso maestro, OPcache y los picos.
  4. Fijar MemoryHigh= justo por encima del valor máximo medido y MemoryMax= con algo de margen por encima.
  5. Reajustar los valores al cabo de una semana con base en las mediciones y los eventos OOM.
# Memoria de todos los procesos PHP-FPM en MB (Resident Set Size)
ps -C php-fpm8.3 -o rss= | awk '{s+=$1} END {printf "%.0f MB\n", s/1024}'
# Resumen continuo de todas las cgroups ordenado por memoria
systemd-cgtop -m

Debería anotar el valor de memory_limit antes del cambio. Tras la actualización puede volver a un límite fijo en PHP; cómo elegirlo con criterio figura en Fijar bien memory_limit.

Vigilar: cuándo actúa el límite

Un límite de memoria cuya activación nadie nota es una fuente silenciosa de errores. El kernel cuenta los eventos OOM por cgroup, y systemd y el registro del kernel los anotan.

# Contador OOM de la cgroup del servicio (ruta con cgroup v2)
cat /sys/fs/cgroup/system.slice/php8.3-fpm.service/memory.events
# Mensajes del OOM killer en el registro del kernel
journalctl -k --since "-1 day" | grep -i "out of memory"

Si sube el contador oom_kill, el kernel termina workers. Eso puede indicar un límite demasiado ajustado, una aplicación defectuosa o peticiones inusuales. En la situación actual merece la pena cotejar esos eventos con los registros de acceso de la misma hora.

Importante

El límite de memoria es una medida transitoria y no sustituye a la actualización de PHP. Planifique la actualización en paralelo y reinicie después todos los procesos de PHP. El resumen de todas las medidas transitorias figura en Medidas inmediatas si todavía no puede parchear.

Lo que ocurre dentro de PHP ante la falta de memoria lo explica memory_limit y Out-of-Memory. En Apache con mod_php rige la misma técnica para el servicio Apache, aunque entonces PHP comparte el límite con el propio servidor web.

Conclusión: no olvide la segunda mitad

memory_limit=-1 solo es una medida transitoria defendible junto con un límite externo. systemd hace posible ese límite con pocas líneas: MemoryHigh como freno, MemoryMax como tope duro, MemorySwapMax contra la huida al swap y una OOMPolicy elegida con criterio. Mida antes, observe después y tenga presente que todos los pools de un servicio comparten el límite.

Pero sobre todo: planifique el final de la medida. Tras la actualización a una versión corregida de PHP, vuelva a un memory_limit fijo. El límite de cgroup puede conservarse como protección adicional contra procesos desbocados.

¿Cómo ayuda PHP SOS en concreto?

  • Medir: registramos la necesidad de memoria de sus servicios PHP y deducimos de ahí los límites adecuados.
  • Aplicar: fijamos drop-ins con MemoryHigh, MemoryMax y una OOMPolicy adecuada, bloqueamos memory_limit en el pool y reiniciamos los servicios de forma controlada.
  • Vigilar: incorporamos los eventos OOM a la supervisión y reajustamos los valores.
  • Llegar al parche: mediante el servicio de parcheo de PHP instalamos la actualización y retiramos después de forma ordenada los valores transitorios.

Preguntas frecuentes

¿Por qué debe estar memory_limit en -1? ¿No es peligroso?

Por sí solo lo sería. php.net recomienda memory_limit=-1 como medida transitoria expresamente junto con un límite de memoria mediante contenedor o cgroup. La limitación sale así de PHP y pasa al sistema operativo. Sin este límite externo, no debería fijar el valor.

¿Puedo usar en su lugar ulimit o LimitAS=?

No. php.net desaconseja expresamente RLIMIT_AS y RLIMIT_DATA, y son precisamente estos los límites que fijan ulimit -v, LimitAS= y LimitDATA=. La documentación de systemd advierte además de que LimitAS= limita el espacio de direcciones, no el consumo de memoria.

¿Qué ocurre cuando PHP-FPM alcanza el límite?

Con MemoryHigh, el kernel frena los procesos y recupera memoria. Con MemoryMax, el OOM killer se activa dentro del servicio y termina un proceso, normalmente un worker grande. Si después se detiene todo el servicio lo regula OOMPolicy=.

¿Rige el límite para cada pool por separado?

No. Todos los pools de un servicio FPM comparten una cgroup y, por tanto, un límite común. Los límites separados por sitio web requieren servicios FPM o contenedores separados.

¿Cómo fijo el límite en Docker o Kubernetes?

Mediante la limitación de memoria del contenedor, por ejemplo la opción --memory en docker run o resources.limits.memory en Kubernetes. Ambos usan también cgroups. El principio sigue siendo el mismo: límite fuera de PHP, memory_limit bloqueado en PHP.

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