# Pyynnön elinkaari: käynnistys, suoritus, sammutus

> PHP Request Lifecycle selitettynä: MINIT, RINIT, RSHUTDOWN, MSHUTDOWN, workerien uudelleenkäyttö ja se, miksi CVE-2026-109619 kohdistuu juuri tähän.

Lähde: https://phpsos.de/fi/php/request-lebenszyklus/  
Tilanne: 25.10.2026  
Palveluntarjoaja: PHP SOS, PHP-hätäapu ja palvelimen tietoturva. Pikaapu WhatsAppilla: https://wa.me/491736494914?text=Hei%20PHP%20SOS%2C%20tarvitsemme%20apua%20PHP-palvelimemme%20kanssa.%20Yritys%3A%20%E2%80%A6%20Verkkosivusto%3A%20%E2%80%A6

**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ä](https://phpsos.de/fi/php/php-fpm-erklaert/).

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:

```ini
; 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
<?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](https://phpsos.de/fi/php2shell/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](https://phpsos.de/fi/php2shell/php-fpm-neustart/).

> **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](https://phpsos.de/fi/leistungen/php-patch-service/) 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

- [php.net: tietoturvailmoitus 21.10.2026](https://www.php.net/)
- [PHP-käsikirja: FPM-konfiguraatio](https://www.php.net/manual/en/install.fpm.configuration.php)
- [PHP Internals Book: PHP:n elinkaari](https://www.phpinternalsbook.com/)
