Lyhyesti: PHP:n Request Lifecycle koostuu kahdesta tasosta. Kerran prosessia kohti PHP lataa laajennuksensa (vaihe MINIT) ja purkaa ne lopuksi (MSHUTDOWN). Jokaisella yksittäisellä pyynnöllä on oma käynnistys- ja siivousvaiheensa (RINIT ja RSHUTDOWN). Verkkokäytössä worker käsittelee monta pyyntöä peräkkäin, joten PHP:n on nollattava luotettavasti kaikki pyyntökohtainen tila kahden pyynnön välillä. Juuri tällä rajalla oli CVE-2026-109619, toinen PHP2Shell-aukoista.
Kaksi tasoa: prosessi ja pyyntö
Ei-teknisille lukijoille auttaa vertaus: PHP-worker on kuin virastotiski. Tiski järjestetään aamulla kerran, lomakkeineen, leimasimineen ja tietokoneineen. Sen jälkeen tulee peräkkäin monta asiakasta. Jokaisen asiakkaan jälkeen pöydän on oltava tyhjä, jotta seuraava ei näe vieraita papereita.
PHP toimii samoin. Prosessin käynnistyessä se lataa laajennukset ja asetukset, jotka ovat kaikille pyynnöille samat. Jokainen pyyntö saa sen jälkeen oman muistinsa, omat muuttujansa ja omat pyyntötietonsa, kuten $_GET, $_POST tai $_FILES. Pyynnön päättyessä PHP vapauttaa tämän muistin. PHP Internals Book tiivistää sen lyhyesti: PHP unohtaa pyyntömuistin jokaisen pyynnön lopussa.
PHP Request Lifecycle -vaiheet pähkinänkuoressa
Nimet tulevat PHP:n sisältä; jokainen laajennus voi kytkeytyä näihin vaiheisiin.
| Vaihe | Milloin suoritetaan | Kuinka usein | Tyypillinen tehtävä |
|---|---|---|---|
| MINIT (Module Init) | prosessin käynnistyessä | kerran prosessia kohti | rekisteröidä laajennukset, luoda ini-merkinnät, pysyvä data |
| GINIT | globaalien luomisen yhteydessä | prosessia kohti, säikeillä säiettä kohti | valmistella laajennuksen globaalit muuttujat |
| RINIT (Request Init) | jokaisen pyynnön alussa | pyyntöä kohti | valmistella pyyntömuisti ja pyyntökohtainen tila |
| Suoritus | välissä | pyyntöä kohti | PHP-koodinne suoritetaan |
| RSHUTDOWN | jokaisen pyynnön lopussa | pyyntöä kohti | vapauttaa pyyntöresurssit |
| PRSHUTDOWN | RSHUTDOWNin jälkeen | pyyntöä kohti, harvoin käytetty | viimeiset siivoustyöt |
| MSHUTDOWN | prosessin päättyessä | kerran prosessia kohti | vapauttaa pysyvä data |
Tärkeä lause PHP Internals Bookista laajennusten kehittäjille: globaaleja ei tyhjennetä automaattisesti jokaisen pyynnön jälkeen. Se, minkä pitää olla pyyntökohtaisesti tuore, kuuluu nollata vaiheessa RINIT. Tämä sääntö osoittaa, miksi kahden pyynnön välinen raja on tietoturvan kannalta kriittinen kohta.
Myös pyynnön lopussa järjestys on kiinteä: ensin suoritetaan shutdown-funktiot, jotka olette rekisteröineet funktiolla register_shutdown_function(), sitten destruktorit, sitten tulostepuskuri tyhjennetään. Vasta sen jälkeen seuraa RSHUTDOWN.
Miksi workereita käytetään uudelleen
Komentorivillä prosessi käsittelee täsmälleen yhden pyynnön ja päättyy. Verkkokäytössä se olisi liian hidasta. PHP-FPM, Apache mod_php:n kanssa, LiteSpeed ja FrankenPHP pitävät siksi workereita valmiina palvelemaan monta pyyntöä peräkkäin. MINIT suoritetaan silloin kerran, RINIT ja RSHUTDOWN tuhansia kertoja. Miten FPM ohjaa workereitaan, selittää PHP-FPM selitettynä.
PHP-FPM:ssä pm.max_requests määrää, monenko pyynnön jälkeen worker käynnistyy uudelleen. Oletus on 0 eli rajaton. Käsikirja mainitsee asetuksen keinona kolmansien osapuolten kirjastojen muistivuotoja vastaan:
; FPM-pooli: käynnistä worker uudelleen 500 pyynnön jälkeen (muistivuotoja vastaan)
pm = dynamic
pm.max_children = 20
pm.max_requests = 500
Vielä pidemmälle menee FrankenPHP:n worker-tila: siinä koko sovellus käynnistyy kerran ja pysyy muistissa. Sovelluksen tila säilyy silloin myös PHP-tasolla. Esimerkki näyttää eron:
<?php
function anfrageZaehler(): int
{
static $zaehler = 0; // perinteisessä FPM-käytössä pyyntöä kohti taas 0
return ++$zaehler; // worker-tilassa arvo jatkaa laskemista pyyntöjen yli
}
echo anfrageZaehler();
Perinteisessä käytössä kutsu tulostaa aina 1, koska PHP hylkää pyyntömuistin. Worker-tilassa arvo kasvaa niin kauan kuin worker elää. FrankenPHP:n dokumentaatio neuvoo siksi nollaamaan pyyntökohtaisen tilan pyyntöjen välillä tietoisesti.
Mihin CVE-2026-109619 kohdistuu
CVE-2026-109619 ei koske sovelluskoodiannne vaan PHP:tä itseään. php.netin mukaan pyyntöön kuuluva sisäinen tila saattoi säilyä muistinsa vapauttamisen jälkeen. Myöhempi pyyntö samassa workerissa saattoi käyttää sitä, ennen kuin alustus nollasi sen. Vertauksessa: tiskin pöydälle jäi lappu, joka olisi jo pitänyt siivota pois. Hyväksikäytön yksityiskohtia emme tietoisesti julkaise; luokitus löytyy sivulta CVE-2026-109619.
php.netin väliaikaiset toimenpiteet tähän aukkoon kohdistuvat vahvistettuihin reitteihin, eivätkä ne ole täydellinen suoja:
session.upload_progress.enabled=0tiedostossa php.ini, FPM-poolissa tai palvelinkonfiguraatiossa, sen jälkeen kaikkien workerien uudelleenkäynnistys; ei funktiollaini_set()eikä tiedostolla.user.ini.- PHP 8.5:ssä lisäksi
fatal_error_backtraces=0ja uudelleenkäynnistys. - Poistakaa virheelliset tai vanhentuneet pyyntökohtaiset asetukset, jotka tuottavat ilmoituksia ennen alustusta. Pelkkä ilmoitusten piilottaminen ei php.netin mukaan auta.
- Korjatkaa sovelluksessa ulkoa laukaistavissa olevat vakavat virheet ja aikakatkaisut, myös takaisinkutsuissa sekä latausten ja tuontien käsittelyssä.
Varsinainen korjaus on päivitys versioon PHP 8.5.12, 8.4.27, 8.3.36 tai 8.2.35. Koska virhe on käynnissä olevassa workerissa, päivitys tehoaa vasta kaikkien prosessien uudelleenkäynnistyksen jälkeen; vaiheet ovat sivulla PHP-FPM:n ja kaikkien workerien uudelleenkäynnistys.
Tarkistakaa uudelleenkäynnistyksen jälkeen komennolla ps -eo pid,lstart,cmd | grep php, että kaikki PHP-prosessit on käynnistetty päivityksen ajankohdan jälkeen.
Mitä elinkaari merkitsee käytölle ja kehitykselle
Mallista voi johtaa selkeitä sääntöjä. Ylläpitäjille: päivitys tehoaa vasta, kun yksikään prosessi ei enää käytä vanhaa koodia. Jokainen pitkäikäisiä workereita käyttävä ajotapa tarvitsee dokumentoidun uudelleenkäynnistyksen. Kehittäjille: worker-tiloissa ajettava koodi ei saa jättää pyyntökohtaista tilaa staattisiin muuttujiin tai singletoneihin, esimerkiksi käyttäjätietoja tai oikeuksia. Molemmille pätee: säilyttäkää lokit 20.10.2026 alkaen, jotta myöhemmin voi arvioida, tapahtuiko ennen korjausta jotain.
Miten PHP SOS auttaa konkreettisesti?
PHP-korjauspalvelun avulla toteutamme mallin käytännössä:
- Ajotavan selvitys: Toteamme, onko käytössä PHP-FPM, mod_php, LiteSpeed, FrankenPHP vai php-cgi ja miten workereita hallitaan.
- Päivitys ja uudelleenkäynnistys: Asennamme korjatun version ja käynnistämme jokaisen workerin hallitusti uudelleen.
- Prosessien tarkistus: Osoitamme käynnistysajoilla ja versioilla, ettei vanhoja prosesseja ole jäljellä.
- Worker-tilojen tarkistus: Pitkäikäisten sovellusten kohdalla katsomme, nollataanko pyyntökohtainen tila siististi.
Usein kysytyt kysymykset
Käynnistääkö PHP uuden prosessin jokaiselle pyynnölle?
Komentorivillä käynnistää, verkkokäytössä yleensä ei. PHP-FPM, Apache mod_php:n kanssa, LiteSpeed tai FrankenPHP antavat yhden prosessin käsitellä monta pyyntöä peräkkäin. Pyyntöjen välillä PHP siivoaa pyyntökohtaisen tilan.
Mikä on ero MINITin ja RINITin välillä?
MINIT suoritetaan kerran, kun PHP-prosessi käynnistyy, ja se luo pysyvät rakenteet. RINIT suoritetaan jokaisen yksittäisen pyynnön alussa ja valmistelee sen, mitä vain kyseinen pyyntö tarvitsee.
Auttaako pm.max_requests haavoittuvuutta CVE-2026-109619 vastaan?
php.net ei mainitse tiheämpiä worker-uudelleenkäynnistyksiä väliaikaisena toimenpiteenä. Asetus rajoittaa vain sitä, montako pyyntöä worker käsittelee ennen uudelleenkäynnistystä. Aukkoa vastaan auttaa päivitys ja sitä seuraava kaikkien workerien uudelleenkäynnistys.
Mikä on FrankenPHP:n worker-tila?
Worker-tilassa sovellus käynnistyy kerran ja pysyy muistissa. Se säästää aikaa, mutta staattiset muuttujat, globaalit ja välimuistit säilyvät pyyntöjen välillä. FrankenPHP:n dokumentaation mukaan pyyntökohtainen tila on siksi nollattava tietoisesti.