Ajankohtainen tilanne: PHP2Shell. Kaksi kriittistä PHP-haavoittuvuutta (CVSS 9.2) sallii haittakoodin suorittamisen ilman kirjautumista. Korjaukset 21.10.2026 alkaen.Mitä nyt on tehtävä →

Pyynnön elinkaari: käynnistys, suoritus, sammutus

Miten PHP käynnistää prosesseja, käsittelee pyyntöjä ja siivoaa niiden välillä. Vaiheet MINITistä MSHUTDOWNiin ja yhteys haavoittuvuuteen CVE-2026-109619.

Tilanne Lukuaika 4 minPHP-tietoa

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:

  1. session.upload_progress.enabled=0 tiedostossa php.ini, FPM-poolissa tai palvelinkonfiguraatiossa, sen jälkeen kaikkien workerien uudelleenkäynnistys; ei funktiolla ini_set() eikä tiedostolla .user.ini.
  2. PHP 8.5:ssä lisäksi fatal_error_backtraces=0 ja uudelleenkäynnistys.
  3. Poistakaa virheelliset tai vanhentuneet pyyntökohtaiset asetukset, jotka tuottavat ilmoituksia ennen alustusta. Pelkkä ilmoitusten piilottaminen ei php.netin mukaan auta.
  4. 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.

Vinkki

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.

Lähteet

Pikayhteys

Jokainen tunti on tärkeä. Kirjoittakaa meille nyt.

Kuvatkaa lyhyesti järjestelmä, PHP-versio ja havainnot. Otamme yhteyttä, selvitämme tilanteen ja työskentelemme etänä, kunnes tapaus on käsitelty loppuun ja dokumentoitu.

Pikaapu