Skip to content
Published on

GhostLock — Linuxのrtmutexに15年潜んでいたスタックuse-after-free(CVE-2026-43499)

シェア
Authors

はじめに — 原因は驚くほど小さかった

2026年7月7日、nebusec.aiに「IonStack part II」と題した分析記事が公開され、Hacker News で話題になりました。この記事が扱うのが GhostLock、すなわち CVE-2026-43499 です。Linuxカーネルのリアルタイムミューテックス(rtmutex)コード(kernel/locking/rtmutex.c)にあったスタックuse-after-freeで、研究チームの説明では、2011年の Linux 2.6.39-rc1 からメインラインにあり、2026年4月の 7.1-rc1 でようやく修正されました。およそ15年です。

面白いのは、その15年という数字ではありません。原因が恥ずかしくなるほど小さいことです。自分を呼ぶスレッドが誰なのかを取り違えた一つのヘルパー関数が、15年分のレビューとファジングと lockdep を生き延びました。本稿は三つを扱います。スタックuse-after-freeとは何でなぜ厄介なのか、これほど小さいバグがどうしてそんなに長く潜めるのか、そして意図的に誇張を削ぎ落として、GhostLock を実際に悪用するには何が必要で、私たちは何をすべきかです。

先に但し書きを一つ。これはシリーズの第2部で、第1部を要約はしてくれません。私は第2部が扱った内容だけを述べ、どの数値が独立に確認されたものではなくこの一本の記事に基づくのかも合わせて記します。

スタックuse-after-freeとは何か

use-after-free は文字どおりです。メモリが解放されたのに、そのポインタを使い続けることです。ヒープの例はおなじみです。スタック版はもっと奇妙です。ローカル変数は関数のスタックフレームに住みます。関数が戻ると、そのフレームは「解放」されます — その領域が次の呼び出しにすぐ再利用できる、という意味においてです。ローカル変数を指すポインタが、その変数を所有する関数より長く生き残ると、後続のコードは今や別の何かのものになったメモリを読み書きできてしまいます。

カーネルではこれがユーザー空間より厄介です。汚すべきアロケータのメタデータもなく、目に見えるクラッシュもありません。スタックはただの RAM で、その領域の再利用は正常で速い経路だからです。解放されたスタックフレームを指す宙ぶらりんのポインタは、誰かのデータがたまたまそこに落ちてくるまで、何もしません。

その沈黙こそが問題のすべてです。このバグが通常のテストで見えないのは、ほとんどの場合、解放されたフレームをまだ誰も上書きしていないからです。攻撃者が自分の制御するバイトでその領域を意図的に取り戻して埋めたとき、初めて武器になります — GhostLock の実証コードがしているのは、まさにこれです。

GhostLockの実体

問題のコードはリアルタイムミューテックス(rtmutex)サブシステムで、PI-futex システムコールから到達します。ロックを握った低優先度スレッドが高優先度スレッドを飢えさせないようにする、優先度継承(priority inheritance)の仕組みです。問題のヘルパーは remove_waiter() です。もともと一つの状況のために書かれました — スレッドがロックでブロックし、自分で後始末をする状況です。そのため、実行中のスレッド(current)が常に取り除く対象の waiter だと仮定し、current->pi_blocked_on を消していました。

requeue-PI 機能がその仮定を静かに破りました。FUTEX_WAIT_REQUEUE_PIFUTEX_CMP_REQUEUE_PI を使うと、あるスレッドの rt_mutex_waiter(そのスレッドのスタックに住みます)を、別のスレッドが rt_mutex_start_proxy_lock() で別の futex に代理登録します。この経路がデッドロックの循環を検知して -EDEADLK でロールバックすると、間違ったスレッドから remove_waiter() を呼び、本当の waiter ではなく代理登録者の状態を消します。結果として本物の waiter は、再利用された自分のスタックフレームを指すポインタで依然として PI チェーンに繋がれたままユーザー空間へ戻り、次のチェーン走査が解放済みメモリを参照します。

/* rtmutex.c: ヘルパーは実行中のスレッドが waiter だと仮定していた */
static void remove_waiter(struct rt_mutex *lock, struct rt_mutex_waiter *w)
{
        /* ... ロックの waiter ツリーから w を外す ... */
        current->pi_blocked_on = NULL;   /* バグ: w ではなく current を消す */
}

/* 修正、コミット 3bfdc63936dd: waiter 自身の task を対象にする */
raw_spin_lock(&w->task->pi_lock);
w->task->pi_blocked_on = NULL;

修正、コミット 3bfdc63936dd(「rtmutex: Use waiter::task instead of current in remove_waiter()」)は、本質的に一行です。current ではなく waiter 自身の task を取り、waiter->task->pi_lock をロックして waiter->task->pi_blocked_on を消します。15年の露出が、正しい構造を指すだけで閉じました — 安心でもあり、少し薄ら寒くもある事実です。

15年も見えていた理由 — そして悪用の実際の難しさ

長く生き延びた理由についての記事の説明は、平凡ですが説得力があります。仮定の境界を越えた関数の再利用です。remove_waiter() は元の呼び出し元には正しく、そこでは正しいままでした。requeue-PI が current が誰かについて別の前提を持つ二番目の呼び出し元を追加したのに、誰もその仮定を再確認しませんでした。しかもバグに到達するには、無作為ではない特定の配置が要ります — -EDEADLK のロールバックを強制するために、三つの futex と三つのスレッドにまたがる優先度継承の依存循環を組む必要があります。ファザーや日常のワークロードが滅多に作らない状態です。

さて、影響についての正直な部分です。GhostLock は ローカル権限昇格であって、リモートコード実行ではありません 。これらが成り立つには、すでにそのマシンでコードを実行できる必要があります — シェルでも、コンテナ内のプロセスでも。実証コードも一行では済みません。公開されたチェーンは prctl(PR_SET_MM_MAP) で解放済みフレームを整えて偽の waiter を捏造し、制約された rb-tree 書き込みで CPU entry area 上の inet6_protos[IPPROTO_UDP] を上書きし、最後に /proc/sys/kernel/core_pattern の権限ビットを反転させて root を得ます。

これは研究レベルの作業です。著者らはハードニング機能を切った kernelCTF の条件で97%の安定性を報告し、Google の kernelCTF はこれに 92,337ドル を支払いました。公平に読むなら、信頼でき実在するが、限界も明確です — ローカルアクセスと精緻なチェーンが必要です。ローカル権限昇格が、侵害された一つのワークロードをノード全体の掌握に変えるコンテナ脱出こそ、最も警戒すべきシナリオです。

公開の流れも速く、その点は修正そのものと同じくらい重要です。記事によれば、バグは2026年4月18日に security@kernel.org へ報告され、4月20日にメインラインで修正され、5月4日に stable へバックポートされました。Google は6月30日に kernelCTF の提出を認め、公開分析は7月7日に出ました。静かに直された時点と、公に詳細が明かされた時点の間隔は約2か月で、stable 利用者がその仕組みが一面に載る前に更新できる時間があった、ということです。

おわりに — パッチ、そして影響有無の確認方法

実務的な対応は退屈ですが正しいものです。パッチです。修正はメインラインにコミット 3bfdc63936dd(「rtmutex: Use waiter::task instead of current in remove_waiter()」)として入り、2026年5月4日に stable ツリーへバックポートされたので、最新に更新されたディストリビューションのカーネルなら既に含んでいます。影響範囲は原則として広く — CONFIG_FUTEX_PI=y が有効な 2.6.39-rc1 以降のすべてのカーネル、事実上すべてのディストリビューションのビルドに到達します。ですから本当の問いは「影響を受けるか」ではなく「パッチ済みか」です。

uname -r                      # 実行中のカーネル
# 次にディストリビューションの CVE-2026-43499 勧告を確認する。
# 2026-05-04 以降に更新された stable カーネルならパッチ済み。

記事が挙げる多層防御を二つ — どちらもパッチの代わりにはなりません。RANDOMIZE_KSTACK_OFFSET はシステムコールごとにスタックオフセットを無作為化し、実証コードが頼る決定論的なフレームの重なりを崩します。約5ビット、おおよそ32分の1の確率にすぎませんが、無いよりはましです。STATIC_USERMODE_HELPER はここで使われた特定の /proc/sys 経路を塞ぎますが、根本のバグ自体は塞ぎません。そして数値についての正直な但し書きです。15年という寿命とバージョンの詳細は、シリーズ第2部にあたる一本のベンダー記事に由来します。メカニズムは具体的でカーネルツリーと照合できるので私は信頼しますが、周辺の数字は独立確認ではなくその出典の記述として受け取るのが妥当です。

参考資料