Kort sagt: PHP request lifecycle har to niveauer. Én gang pr. proces indlæser PHP sine udvidelser (fasen MINIT) og nedbryder dem igen til sidst (MSHUTDOWN). For hver enkelt forespørgsel er der en egen start- og oprydningsfase (RINIT og RSHUTDOWN). I webdrift behandler en worker mange forespørgsler efter hinanden, og derfor skal PHP mellem to forespørgsler pålideligt nulstille al forespørgselstilstand. Netop ved denne grænse lå CVE-2026-109619, den anden PHP2Shell-sårbarhed.
To niveauer: processen og forespørgslen
For ikke-teknikere hjælper et billede: En PHP-worker er som en skranke i en myndighed. Skranken indrettes én gang om morgenen, med formularer, stempler og computer. Derefter kommer mange besøgende efter hinanden. Efter hver besøgende skal bordet være tomt, så den næste ikke ser fremmede papirer.
PHP arbejder på samme måde. Ved start af en proces indlæser det udvidelser og indstillinger, der er ens for alle forespørgsler. Hver forespørgsel får så sin egen hukommelse, sine egne variabler og egne forespørgselsdata som $_GET, $_POST eller $_FILES. Ved forespørgslens afslutning frigiver PHP denne hukommelse. PHP Internals Book opsummerer det kort: PHP glemmer forespørgselshukommelsen ved afslutningen af hver forespørgsel.
Faserne i PHP request lifecycle i overblik
Navnene stammer fra PHP's indre; hver udvidelse kan hægte sig ind i disse faser.
| Fase | Hvornår den kører | Hvor ofte | Typisk opgave |
|---|---|---|---|
| MINIT (Module Init) | ved processens start | én gang pr. proces | registrere udvidelser, oprette ini-poster, varige data |
| GINIT | ved oprettelsen af globals | pr. proces, ved tråde pr. tråd | forberede en udvidelses globale variabler |
| RINIT (Request Init) | i begyndelsen af hver forespørgsel | pr. forespørgsel | forberede forespørgselshukommelse og forespørgselstilstand |
| Udførelse | imellem | pr. forespørgsel | jeres PHP-kode kører |
| RSHUTDOWN | ved afslutningen af hver forespørgsel | pr. forespørgsel | frigive forespørgselsressourcer |
| PRSHUTDOWN | efter RSHUTDOWN | pr. forespørgsel, sjældent brugt | sidste oprydningsarbejder |
| MSHUTDOWN | når processen afsluttes | én gang pr. proces | frigive varige data |
En vigtig sætning fra PHP Internals Book til udvidelsesudviklere: Globals tømmes ikke automatisk efter hver forespørgsel. Hvad der skal være frisk pr. forespørgsel, hører til at nulstille i RINIT. Denne regel viser, hvorfor grænsen mellem to forespørgsler er et sikkerhedskritisk sted.
Ved afslutningen af en forespørgsel er rækkefølgen også fastlagt: Først kører shutdown-funktioner, som I har registreret med register_shutdown_function(), derefter destruktorer, og så tømmes outputbufferen. Først derefter følger RSHUTDOWN.
Hvorfor workers genbruges
På kommandolinjen behandler en proces præcis én forespørgsel og slutter. I webdrift ville det være for langsomt. PHP-FPM, Apache med mod_php, LiteSpeed og FrankenPHP holder derfor workers klar, som betjener mange forespørgsler efter hinanden. MINIT kører så én gang, RINIT og RSHUTDOWN tusindvis af gange. Hvordan FPM styrer sine workers, forklarer PHP-FPM forklaret.
I PHP-FPM fastlægger pm.max_requests, efter hvor mange forespørgsler en worker genstarter. Standarden er 0, altså ubegrænset. Manualen nævner indstillingen som middel mod hukommelseslækager i tredjepartsbiblioteker:
; FPM-pool: genstart workers efter 500 forespørgsler (mod hukommelseslækager)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Endnu længere går FrankenPHP's worker-tilstand: Dér starter hele applikationen én gang og bliver i hukommelsen. Applikationstilstand overlever så også på PHP-niveau. Eksemplet viser forskellen:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // i klassisk FPM-drift igen 0 pr. forespørgsel
return ++$zaehler; // i worker-tilstand tæller værdien videre på tværs af forespørgsler
}
echo anfrageZaehler();
I klassisk drift udskriver kaldet altid 1, fordi PHP kasserer forespørgselshukommelsen. I worker-tilstand vokser værdien, så længe workeren lever. FrankenPHP-dokumentationen anbefaler derfor bevidst at nulstille forespørgselsrelateret tilstand mellem forespørgsler.
Hvor CVE-2026-109619 sætter ind
CVE-2026-109619 angår ikke jeres applikationskode, men PHP selv. Ifølge php.net kunne intern tilstand, der hørte til forespørgslen, overleve frigivelsen af sin hukommelse. En senere forespørgsel i den samme worker kunne bruge den, før initialiseringen nulstillede den. I billedet: På skrankebordet blev en seddel liggende, som egentlig allerede var ryddet væk. Udnyttelsesdetaljer offentliggør vi bevidst ikke; vurderingen står under CVE-2026-109619.
php.nets midlertidige foranstaltninger til denne sårbarhed sætter ind ved bekræftede veje, de er ingen fuldstændig beskyttelse:
session.upload_progress.enabled=0i php.ini, FPM-pool eller serverkonfiguration, derefter genstart alle workers; ikke viaini_set()eller.user.ini.- På PHP 8.5 desuden
fatal_error_backtraces=0og genstart. - Fjern ugyldige eller forældede indstillinger pr. forespørgsel, som frembringer meddelelser før initialiseringen. Blot at skjule meddelelserne hjælper ifølge php.net ikke.
- Ret fatale fejl og tidsoverskridelser i applikationen, som kan udløses udefra, også i callbacks, upload- og importbehandling.
Den egentlige afhjælpning er opdateringen til PHP 8.5.12, 8.4.27, 8.3.36 eller 8.2.35. Fordi fejlen sidder i den kørende worker, virker den først efter en genstart af alle processer; trinene står under Genstart PHP-FPM og alle workers.
Kontrollér efter genstarten med ps -eo pid,lstart,cmd | grep php, om alle PHP-processer er startet efter opdateringstidspunktet.
Hvad livscyklussen betyder for drift og udvikling
Af modellen kan der udledes klare regler. For administratorer: En opdatering er først virksom, når ingen proces med gammel kode kører mere. Enhver driftsform med langlivede workers har brug for en dokumenteret genstart. For udviklere: Kode, der kører i worker-tilstande, må ikke lade forespørgselsrelateret tilstand ligge i statiske variabler eller singletons, f.eks. brugerdata eller rettigheder. For begge gælder: Opbevar logs fra 20.10.2026 for senere at kunne vurdere, om der skete noget før patchen.
Hvordan hjælper PHP SOS konkret?
Med PHP-patchservicen gennemfører vi modellen i praksis:
- Bestemme driftsformen: Vi afgør, om PHP-FPM, mod_php, LiteSpeed, FrankenPHP eller php-cgi kører, og hvordan workers administreres.
- Opdatering og genstart: Vi indspiller den rettede version og genstarter hver worker kontrolleret.
- Proceskontrol: Vi dokumenterer ud fra starttidspunkter og versioner, at ingen gammel proces er tilbage.
- Kontrollere worker-tilstande: Ved langlivede applikationer ser vi på, om forespørgselsrelateret tilstand nulstilles korrekt.
Ofte stillede spørgsmål
Starter PHP en ny proces til hver forespørgsel?
På kommandolinjen ja, i webdrift som regel ikke. PHP-FPM, Apache med mod_php, LiteSpeed eller FrankenPHP lader en proces behandle mange forespørgsler efter hinanden. Mellem forespørgslerne rydder PHP forespørgselstilstanden op.
Hvad er forskellen på MINIT og RINIT?
MINIT kører én gang, når PHP-processen starter, og opretter varige strukturer. RINIT kører i begyndelsen af hver enkelt forespørgsel og forbereder det, som kun denne forespørgsel har brug for.
Hjælper pm.max_requests mod CVE-2026-109619?
php.net nævner hyppigere worker-genstarter som midlertidig foranstaltning ikke. Indstillingen begrænser kun, hvor mange forespørgsler en worker behandler, før den genstarter. Mod sårbarheden hjælper opdateringen med efterfølgende genstart af alle workers.
Hvad er FrankenPHP's worker-tilstand?
I worker-tilstand starter applikationen én gang og bliver i hukommelsen. Det sparer tid, men statiske variabler, globals og caches bevares mellem forespørgsler. Ifølge FrankenPHP-dokumentationen skal forespørgselsrelateret tilstand derfor nulstilles bevidst.