Em resumo: O PHP liberta memória sobretudo através da contagem de referências: cada valor sabe quantas variáveis apontam para ele e é eliminado assim que esse contador chega a zero. Para as referências circulares, que de outro modo nunca seriam libertadas, existe um garbage collector de ciclos. No fim de cada pedido, o PHP limpa ainda tudo o que o pedido ocupou. Ambas as vulnerabilidades do PHP2Shell atuam sobre estados de memória: sobre um estado de falta de memória durante o processamento do pedido e sobre um estado interno que persiste demasiado tempo após a libertação.
Como o PHP gere a memória: visão geral
Em linguagens como o C, o programador tem de pedir e libertar a memória ele próprio. O PHP dispensa-o disso. Cada variável, cada array e cada objeto ocupa memória, que o PHP gere através do seu próprio gestor de memória. O limite para isso é definido pelo memory_limit, segundo o manual 128M por predefinição.
Para quem decide, um ponto é central: na operação clássica com PHP-FPM ou mod_php correm processos worker de longa duração, que tratam muitos pedidos em sequência. O que um pedido deixa para trás tem de estar limpo de forma fiável antes do seguinte. É precisamente aqui que está uma das vulnerabilidades do PHP2Shell.
Contagem de referências: contar quem usa um valor
O PHP guarda cada valor num contentor, o chamado zval. Além do tipo e do conteúdo, segundo o manual, mantém um contador de referências (refcount) e uma marca que indica se o valor faz parte de uma referência PHP com & (is_ref).
<?php
$a = str_repeat('x', 1000); // um valor, refcount 1
$b = $a; // sem cópia, refcount 2
$b .= 'y'; // só agora é copiado (copy-on-write)
unset($a); // o refcount do original desce
// Quando um contador chega a 0, o PHP liberta a memória de imediato.
O princípio chama-se copy-on-write: o PHP só copia um valor quando uma das variáveis o altera. Isto poupa muita memória. O contador de uma variável pode tornar-se visível com a extensão Xdebug e a função xdebug_debug_zval(). O manual adverte que os números exatos podem diferir consoante a versão do PHP.
Ciclos: quando a contagem de referências não basta
A contagem de referências tem uma fraqueza conhecida. Se uma estrutura aponta para si própria, o seu contador nunca desce a zero.
<?php
$a = ['eins'];
$a[] = &$a; // o array contém uma referência a si próprio
unset($a); // ninguém lhe consegue aceder, mas o refcount continua em 1
Segundo o manual, estas referências circulares ocorrem sobretudo em objetos, por exemplo em estruturas pai-filho ou em analisadores. Fugas isoladas são inofensivas, porque o PHP limpa no fim do pedido. Em processos de longa duração, como workers de filas ou grandes execuções de testes, milhões delas podem, porém, somar-se num verdadeiro problema de memória.
O garbage collector de ciclos
Desde o PHP 5.3.0, o PHP usa um algoritmo de Bacon e Rajan para encontrar esses ciclos. A ideia de base: um ciclo só pode surgir quando um contador desce para um valor superior a zero. Só esses valores são retidos pelo PHP como possíveis raízes.
- As possíveis raízes vão para um buffer. O manual indica como tamanho predefinido 10 000 entradas; o limiar atualmente em vigor é mostrado pelo
gc_status()no campothreshold. - Quando o buffer está cheio, o PHP percorre todos os valores alcançáveis e reduz, a título de teste, os seus contadores.
- Numa segunda passagem aplica-se: os valores com contador a zero são lixo; em todos os outros, a redução é revertida.
- As raízes são retiradas do buffer e todos os valores reconhecidos como lixo são libertados.
Isto é controlado através de zend.enable_gc (ativo por predefinição) ou, em tempo de execução, com gc_enable() e gc_disable(). O gc_collect_cycles() força uma execução e devolve o número de ciclos recolhidos.
Medir e observar a memória
| Função | Finalidade |
|---|---|
memory_get_usage() |
memória atualmente ocupada pelo script |
memory_get_usage(true) |
memória reservada pelo gestor de memória junto do sistema; é contra este valor que se verifica o memory_limit |
memory_get_peak_usage() |
valor máximo da memória ocupada |
gc_status() |
execuções, ciclos recolhidos, limiar, raízes; desde o PHP 8.3.0 também tempos de execução |
gc_collect_cycles() |
desencadear de imediato a recolha de ciclos |
WeakMap |
associação a objetos sem impedir a sua libertação, desde o PHP 8.0 |
Segundo o manual, o memory_get_usage() regista apenas a memória que o PHP pede através do seu próprio gestor de memória. O que as extensões obtêm diretamente junto do sistema operativo não consta destes números. O limite de memória do processo inteiro cabe, por isso, ao sistema operativo ou ao contentor.
Gestão de memória e PHP2Shell
As descrições de ambas as vulnerabilidades levam diretamente a este tema. Na CVE-2026-109618, um estado de falta de memória pode interromper o PHP enquanto os dados do pedido estão apenas parcialmente inicializados; na limpeza, dados inválidos podem então ser tratados como válidos. O que acontece por dentro num excesso de memória explica-o a página memory_limit e Out-of-Memory. Na CVE-2026-109619, o estado interno de um pedido pode persistir após a libertação da memória e ser usado por um pedido posterior no mesmo worker antes de ser reposto. O percurso do arranque à limpeza é descrito na página O ciclo de vida do pedido.
Como medida provisória, o php.net recomenda definir memory_limit=-1 de modo que as aplicações não o possam sobrepor, por exemplo com php_admin_value no pool FPM, e limitar a memória através de contentor ou cgroups, e não através de RLIMIT_AS ou RLIMIT_DATA. Como fazê-lo com o systemd mostra-o o artigo Definir limites de memória com systemd e cgroups.
; Pool FPM: medida provisória segundo o php.net, depois reiniciar todos os workers
php_admin_value[memory_limit] = -1
Sem um limite através de contentor ou cgroup, um único processo pode esgotar o servidor. E nenhuma definição substitui a atualização para o PHP 8.5.12, 8.4.27, 8.3.36 ou 8.2.35, com o reinício de todos os workers, pois só o reinício remove os processos antigos da memória.
Como ajuda concretamente a PHP SOS?
- Verificamos as definições de memória efetivas em cada pool e se as aplicações as podem sobrepor.
- Aplicamos as medidas provisórias com um limite de memória ao nível do contentor ou do cgroup e testamos a carga.
- Aplicamos a atualização, reiniciamos todos os workers e comprovamos que já não correm processos antigos, no âmbito do nosso serviço de patches PHP.
- Avaliamos erros de memória e workers interrompidos nos registos a partir de 20/10/2026.
Perguntas frequentes
O que é a contagem de referências em PHP?
O PHP conta, para cada valor, quantas variáveis ou elementos apontam para ele. Quando o contador desce a zero, a memória é libertada de imediato. Assim, a maior parte da memória é devolvida sem uma execução de limpeza própria.
Para que precisa então o PHP de um garbage collector?
Para os ciclos. Se um array aponta para si próprio ou dois objetos se conhecem mutuamente, o contador nunca desce a zero, mesmo que ninguém lhes aceda. O garbage collector de ciclos encontra e remove essas estruturas.
O PHP liberta a memória no fim de um pedido?
Sim, a memória que um script ocupa durante um pedido, o PHP limpa-a no fim do pedido. No PHP2Shell, porém, a CVE-2026-109619 diz respeito precisamente ao caso em que um estado interno persiste após a libertação e pode ser usado por um pedido posterior no mesmo worker.
Porque é que o php.net recomenda memory_limit=-1 como medida provisória?
A vulnerabilidade CVE-2026-109618 está relacionada com um estado de falta de memória. O php.net recomenda definir memory_limit=-1 de modo que as aplicações não o possam sobrepor e limitar a memória através de contentor ou cgroups. Não é uma proteção completa e não substitui a atualização.