mod_phpのOOM経路と修正の概念図
Technology

PHPのメモリ不足がなぜRCEに?2件の脆弱性とリクエスト処理の落とし穴

結論:PHPのOOM脆弱性は「止まり方」と「状態の寿命」が問題

PHPのメモリ不足(OOM)が起きても、ただちにリモートコード実行(RCE)になるわけではありません。危険なのは、内部処理が中途半端に止まり、後続処理がその状態を完成済みとして信じることです。

今回の2件は、次のように原因を分けて読むと整理できます。

脆弱性 ずれが生じる場所 古い/未完成な状態を使う側
CVE-2026-109618 内部データの準備途中でOOMが起きる 同じリクエストの終了処理
CVE-2026-109619 リクエスト終了後も参照が残る 同じworkerが扱う次のリクエスト

2件を必ず組み合わせる攻撃という意味ではありません。それぞれ独立した問題ですが、状態を作る処理と読む処理の境界が崩れる点は共通します。

以下は2026年10月21日時点の公開情報に基づきます。同日のPHP公式告知は、修正を含む8.5.12、8.4.27、8.3.36、8.2.35への更新を求めています。

PHPのリクエスト処理はアプリの1行目より前に始まる

まずworkerを、HTTPリクエストを処理するPHPのプロセスと考えてください。実行方式によっては、1つのworkerが複数のリクエストを順番に扱います。

ここで重要なのは、リクエストごとのメモリと、workerそのものの寿命が同じとは限らないことです。リクエストが終わってメモリを解放しても、workerは次の仕事へ進めます。

PHPはアプリのPHPコードを実行する前にも、入力の解析や設定の適用、拡張機能の準備を行います。つまり「アプリの先頭行に到達していない」は「PHPが何もしていない」ではありません。

session upload progressの公式説明にも、アップロード情報がスクリプト実行前にセッションへ記録されるとあります。

そのため、アプリでまだsession_start()を呼んでいなくても、PHP内部のセッション処理まで未実行だとは判断できません。

また、PHPの設定資料でいうmemory_limitは、スクリプトが確保できるメモリ量の制限です。サーバーの物理RAM枯渇とは別です。

受信データの大きさを制限するpost_max_sizeとも役割が異なります。入力の内部表現には管理情報が加わるため、受信サイズと処理中のメモリ量は一致しません。

CVE-2026-109618:完成前のデータを終了処理が触る

CVE-2026-109618の鍵は、OOM時のlongjmpです。これは途中の呼び出しをまとめて抜け、別の地点へ処理を移すC言語側の仕組みです。

PHPアプリの通常の例外と同じではありません。深い処理の途中から一気に離れるため、各処理が想定した片付けや初期化を通らない場合があります。

Califの原著にある配列の例を単純化すると、先に「要素が1件増えた」と記録し、続いて中身を準備します。その途中でOOMになると、件数だけ更新された状態が残ります。

その後の終了処理が件数を信用すれば、未初期化の値や参照を完成済みとして扱いかねません。失敗したメモリ確保と、実際に壊れるデータが同じ場所である必要もありません。

mod_phpのOOM経路と修正の概念図

図1:公開情報から独自作成。青い欄は後述の公式修正差分、下段のFPMの違いはCalifの解析に基づきます。

CVE-2026-109618の公式advisoryでは、mod_phpでこの流れからRCEまで実証されています。

今回実証されたRCEは、Web workerの権限で攻撃者がコードを実行できることです。管理者であるrootの権限取得と同義ではなく、どのOOMもRCEになるという意味でもありません。

プロセス停止、メモリ安全性の破壊、制御可能なコード実行は別の段階です。ログにOOMが1件あるだけで、侵害を証明したことにもなりません。

CVE-2026-109619:前のリクエストの状態が次へ残る

公式advisoryでは、状態を残す側(producer)と使う側(consumer)の両方が必要と整理されています。図2はその境界を示します。

use-after-freeとは、解放済みメモリをまだ有効なデータとして使うことです。返却済みの部屋番号だけが台帳に残り、次の利用者の部屋を以前の部屋と誤認する状況に似ています。

古い参照を次のリクエストが使う仕組み

図2:公開情報から独自作成。青い欄の修正については、後述の公式修正差分でも確認できます。

ここでいうglobal stateは、PHPエンジンや拡張のworker寿命の内部状態です。PHPコードのglobal変数や、正常に保存された$_SESSIONとは別物です。

mod_phpとFPMでは「実証済み」の範囲が違う

SAPIは、PHPとWebサーバーなどの実行環境をつなぐ方式です。mod_phpとPHP-FPMではworkerの終了条件が異なり、同じ脆弱性でも確認された経路が違います。

実行方式 CVE-2026-109618 CVE-2026-109619
mod_php 初期化中のOOM経路でRCE実証 致命的エラーの記録と、セッション状態の持ち越しでRCE実証
PHP-FPM ソース上の懸念はあるが完全RCEは未実証 適切なアプリ操作を伴うセッション状態の持ち越しでRCE実証

実証範囲は、OOMの公式advisoryと古い状態の公式advisoryを分けて確認しています。

Califの解析が示す違いは、FPMがリクエストの起動に失敗すると子workerを終了させる点です。起動中のOOMから終了処理を使う109618の経路は、mod_phpと同じ形では続きません。

一方、起動に成功した後でアプリ処理が内部状態を残すと、workerが存続する場合があります。次のリクエストで早いsession処理へ到達し、109619の条件がそろい得ます。

したがって「FPMなら安全」とは言えません。逆に、未実証の経路を実証済みと書くのも不正確です。FPMの109619も、すべての環境で無条件に成立するわけではありません。

Apacheなら必ずmod_php、Nginxなら必ずFPMとも限りません。実行方式の名称だけで、個別環境の安全性を断定しないことが大切です。

WAFやアプリの疎通確認だけでは修正を証明できない

ここからは、公開された仕組みを踏まえた運用上の考察です。WAF(Webアプリケーションファイアウォール)による通常のHTTP検査だけでは、PHP内部の参照の寿命や初期化順序までは確認できません。

通常の画面表示やテストが成功しても、例外的な中断後の終了処理や、同一worker内で次のリクエストへ渡る参照を検証したことにはなりません。

しかも、mod_phpで確認された経路にはアプリコードより前の入力処理が含まれます。アプリ側の入力検証が正常に動くことは、その前段にあるPHP内部の安全性の証明になりません。

WAFやアプリの検証は防御や監視の一部にはなっても、修正版が入っていることの代替証明にはできません。OOMログがないことも、古い状態が残らない証拠にはなりません。

パッチは「安全に止める」と「古い状態を先に消す」

PHP 8.5の公式修正差分は、単純なNULLチェックを1つ足したものではありません。

OOM側では、soft OOMを処理待ちの状態として記録し、PHPの実行エンジン(VM)が安全に扱える区切りで処理します。さらに高いhard limitを超える場合は、通常の後片付けへ進まずプロセスを終了します。

これはメモリ制限をなくす修正ではありません。未完成の内部データを公開した直後に、危険な地点へ制御が飛ぶことを避ける設計です。

古い状態の側では、終了時に参照を消去するだけでなく、入力処理の早いconsumerより前にも初期化します。壊れた前提を次へ持ち越さず、読む前に消す二重の対処です。

仕組みから持ち帰りたい要点は3つです。

  • メモリ上限は、超えた後に「どこで、どう止まるか」まで含めて安全性を考える
  • リクエスト終了とworker終了を区別し、worker寿命の状態を次へ残さない
  • 「ある経路が未実証」と「影響がない」を同じ意味にしない

完全な対策の結論は、修正を含む版への更新です。対象版と更新時の確認項目は、PHP重大脆弱性の更新チェックリストにまとめています。

まとめ

2件を理解する手がかりは、データが「いつ完成し、いつ無効になるか」と、それを「誰がいつ読むか」の対応です。

「OOMなら停止するだけ」「アプリが正常なら問題ない」「FPMだから影響しない」という短絡はできません。状態を作る処理、寿命、読む処理の順序で捉える必要があります。

WAFや疎通確認だけでPHP内部の修正を証明しようとせず、実行方式と導入版を確認し、完全な修正を含むPHPへ更新してください。

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です