Em resumo: O ciclo de vida do pedido PHP tem dois níveis. Uma vez por processo, o PHP carrega as suas extensões (fase MINIT) e desmonta-as no fim (MSHUTDOWN). Para cada pedido individual há uma fase própria de arranque e de limpeza (RINIT e RSHUTDOWN). Na operação web, um worker trata muitos pedidos em sequência, pelo que o PHP tem de repor de forma fiável todo o estado do pedido entre dois pedidos. Foi precisamente nesta fronteira que esteve a CVE-2026-109619, a segunda vulnerabilidade do PHP2Shell.
Dois níveis: o processo e o pedido
Para quem não é técnico, ajuda uma imagem: um worker PHP é como um balcão numa repartição. O balcão é montado uma vez de manhã, com formulários, carimbos e computador. Depois chegam, um após o outro, muitos visitantes. Após cada visitante, a mesa tem de ficar vazia, para que o seguinte não veja documentos alheios.
O PHP funciona da mesma maneira. No arranque de um processo, carrega extensões e definições que são iguais para todos os pedidos. Cada pedido recebe depois a sua própria memória, as suas próprias variáveis e os seus próprios dados de pedido, como $_GET, $_POST ou $_FILES. No fim do pedido, o PHP liberta essa memória. O PHP Internals Book resume-o de forma concisa: o PHP esquece a memória do pedido no fim de cada pedido.
As fases do ciclo de vida do pedido PHP em resumo
Os nomes vêm do interior do PHP; cada extensão pode ligar-se a estas fases.
| Fase | Quando corre | Com que frequência | Tarefa típica |
|---|---|---|---|
| MINIT (Module Init) | no arranque do processo | uma vez por processo | registar extensões, criar entradas ini, dados permanentes |
| GINIT | ao criar as globais | por processo, em threads por thread | preparar variáveis globais de uma extensão |
| RINIT (Request Init) | no início de cada pedido | por pedido | preparar a memória e o estado do pedido |
| Execução | entretanto | por pedido | o seu código PHP corre |
| RSHUTDOWN | no fim de cada pedido | por pedido | libertar recursos do pedido |
| PRSHUTDOWN | após o RSHUTDOWN | por pedido, raramente usada | últimos trabalhos de limpeza |
| MSHUTDOWN | ao terminar o processo | uma vez por processo | libertar dados permanentes |
Uma frase importante do PHP Internals Book para quem desenvolve extensões: as globais não são esvaziadas automaticamente após cada pedido. O que tem de estar fresco em cada pedido deve ser reposto no RINIT. Esta regra mostra por que razão a fronteira entre dois pedidos é um ponto crítico para a segurança.
No fim de um pedido, a ordem também está definida: primeiro correm as funções de shutdown que registou com register_shutdown_function(), depois os destrutores, depois o buffer de saída é esvaziado. Só depois se segue o RSHUTDOWN.
Porque é que os workers são reutilizados
Na linha de comandos, um processo trata exatamente um pedido e termina. Na operação web seria demasiado lento. O PHP-FPM, o Apache com mod_php, o LiteSpeed e o FrankenPHP mantêm por isso workers prontos, que atendem muitos pedidos seguidos. O MINIT corre então uma vez, o RINIT e o RSHUTDOWN milhares de vezes. Como o FPM controla os seus workers explica-o PHP-FPM explicado.
No PHP-FPM, o pm.max_requests define após quantos pedidos um worker reinicia. A predefinição é 0, ou seja, ilimitado. O manual indica a definição como meio contra fugas de memória em bibliotecas de terceiros:
; pool FPM: reiniciar o worker após 500 pedidos (contra fugas de memória)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
O modo worker do FrankenPHP vai ainda mais longe: aí, a aplicação inteira arranca uma vez e permanece em memória. O estado da aplicação sobrevive então também ao nível do PHP. O exemplo mostra a diferença:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // na operação clássica com FPM, volta a 0 em cada pedido
return ++$zaehler; // no modo worker, o valor continua a contar ao longo dos pedidos
}
echo anfrageZaehler();
Na operação clássica, a chamada imprime sempre 1, porque o PHP descarta a memória do pedido. No modo worker, o valor cresce enquanto o worker viver. A documentação do FrankenPHP aconselha por isso a repor deliberadamente, entre pedidos, o estado ligado ao pedido.
Onde atua a CVE-2026-109619
A CVE-2026-109619 não diz respeito ao código da sua aplicação, mas ao próprio PHP. Segundo o php.net, o estado interno pertencente ao pedido podia sobreviver à libertação da sua memória. Um pedido posterior no mesmo worker podia usá-lo antes de a inicialização o repor. Na imagem: na mesa do balcão ficou um papel que já devia ter sido arrumado. Não publicamos deliberadamente detalhes de exploração; o enquadramento está em CVE-2026-109619.
As medidas provisórias do php.net para esta vulnerabilidade atuam sobre caminhos confirmados e não são uma proteção completa:
session.upload_progress.enabled=0no php.ini, no pool FPM ou na configuração do servidor, depois reiniciar todos os workers; não através deini_set()nem de.user.ini.- No PHP 8.5, adicionalmente
fatal_error_backtraces=0e reinício. - Remover, em cada pedido, definições inválidas ou obsoletas que geram mensagens antes da inicialização. Segundo o php.net, apenas ocultar as mensagens não ajuda.
- Corrigir na aplicação os erros fatais e as expirações de tempo que podem ser desencadeados do exterior, também em callbacks, no processamento de uploads e de importações.
O remédio verdadeiro é a atualização para o PHP 8.5.12, 8.4.27, 8.3.36 ou 8.2.35. Como o erro está no worker em execução, só produz efeito após o reinício de todos os processos; os passos estão em Reiniciar o PHP-FPM e todos os workers.
Depois do reinício, verifique com ps -eo pid,lstart,cmd | grep php se todos os processos PHP foram iniciados depois do momento da atualização.
O que o ciclo de vida significa para a operação e o desenvolvimento
Do modelo podem deduzir-se regras claras. Para administradores: uma atualização só é eficaz quando já não corre nenhum processo com código antigo. Cada modo de operação com workers de longa duração precisa de um reinício documentado. Para programadores: o código que corre em modos worker não pode deixar estado ligado ao pedido em variáveis estáticas ou singletons, por exemplo dados de utilizadores ou permissões. Para ambos vale: conservar os registos a partir de 20/10/2026, para poder avaliar mais tarde se algo aconteceu antes do patch.
Como ajuda concretamente a PHP SOS?
Com o serviço de patches PHP, aplicamos o modelo na prática:
- Determinar o modo de operação: verificamos se corre PHP-FPM, mod_php, LiteSpeed, FrankenPHP ou php-cgi e como os workers são geridos.
- Atualização e reinício: aplicamos a versão corrigida e reiniciamos cada worker de forma controlada.
- Verificação dos processos: comprovamos, com base nas horas de arranque e nas versões, que não resta nenhum processo antigo.
- Verificar os modos worker: nas aplicações de longa duração, examinamos se o estado ligado ao pedido é reposto corretamente.
Perguntas frequentes
O PHP inicia um processo novo para cada pedido?
Na linha de comandos, sim; na operação web, geralmente não. O PHP-FPM, o Apache com mod_php, o LiteSpeed ou o FrankenPHP deixam um processo tratar muitos pedidos em sequência. Entre pedidos, o PHP limpa o estado do pedido.
Qual é a diferença entre MINIT e RINIT?
O MINIT corre uma só vez, quando o processo PHP arranca, e cria estruturas permanentes. O RINIT corre no início de cada pedido individual e prepara o que só esse pedido precisa.
O pm.max_requests ajuda contra a CVE-2026-109619?
O php.net não indica reinícios mais frequentes dos workers como medida provisória. A definição limita apenas quantos pedidos um worker trata antes de reiniciar. Contra a vulnerabilidade ajuda a atualização, seguida do reinício de todos os workers.
O que é o modo worker do FrankenPHP?
No modo worker, a aplicação arranca uma só vez e permanece em memória. Poupa tempo, mas as variáveis estáticas, as globais e as caches mantêm-se entre pedidos. Segundo a documentação do FrankenPHP, o estado ligado ao pedido tem por isso de ser reposto deliberadamente.