GhostLock、15年間すべてのLinuxディストリビューションに存在したスタックUAF
(nebusec.ai)- GhostLock(CVE-2026-43499) は Linux 2.6.39 で導入され、7.1 で修正されたカーネル脆弱性で、非特権のローカル攻撃者が一般的なスレッド関連システムコールだけでスタック UAF を引き起こし、root 権限の取得とコンテナ脱出に悪用できる
- Requeue-PI プロキシ経路の
remove_waiter()が実際の待機タスクではなくcurrentのpi_blocked_onを消去するため、ユーザー空間に復帰したタスクに 解放済みスタックフレームを指すポインタ が残る - 3つの futex と 3つのスレッドで PI 依存関係の循環を作り、
-EDEADLKのロールバックを誘発し、PR_SET_MM_MAPの制御可能なスタックバッファに偽のrt_mutex_waiterを構築して 制約付きポインタ書き込み を確保する - エクスプロイトは
prefetchで KASLR・physmap の基準アドレスを見つけ、CPU entry area(CEA)に偽構造体と ROP スタックを配置したうえで、inet6_protos[IPPROTO_UDP]を上書きし、IPv6 UDP ループバックパケットで制御フローを乗っ取る - 研究チームは 97% の安定性を持つ権限昇格・コンテナ脱出 エクスプロイトにより Google kernelCTF で 92,337 ドルを受け取り、未パッチのすべての Linux ディストリビューションは最新 LTS へアップグレードすべきだとしている
影響範囲と脆弱性の概要
- GhostLock は VEGA が発見した Linux カーネル脆弱性で、追加権限やユーザーネームスペースなしにローカルの非特権ユーザーがトリガーできる
8161239a8bccの rtmutex 再実装によって導入され、影響範囲はv2.6.39-rc1からv7.1-rc1まで- 2026年4月に
3bfdc63936ddで修正され、必要なカーネル設定はCONFIG_FUTEX_PI=yのみ - 攻撃者は次の手順で権限を昇格できる
- 一般的なスレッド関連システムコールだけで、カーネルスタックメモリを指す ダングリングカーネルポインタ を取得する
- ほぼ任意のアドレスにポインタまたは 8 バイトの 0 を書き込める制約付きプリミティブを作る
- 関数テーブルを乗っ取って制御フローを掌握し、root 権限を取得する
- 未パッチのすべての Linux ディストリビューションが影響を受けるため、最新の LTS バージョンへアップグレードする必要がある
remove_waiter() が誤ったタスクをクリーンアップする原因
kernel/locking/rtmutex.cのremove_waiter()は、もともとブロックされたスレッドが自分の待機状態を直接クリーンアップする経路向けに書かれている- 通常のスローパスでは、実行中の
currentが waiter の所有タスクであるため、current->pi_blocked_onを消去する動作は正しい - Requeue-PI プロキシ経路 では、
rt_mutex_start_proxy_lock()が別のスリープ中タスクの代わりにrt_mutex_waiterをキューに入れ、エラーが発生するとそれをロールバックする- このとき
currentはFUTEX_CMP_REQUEUE_PIを呼び出した requeuer - 実際の waiter は
FUTEX_WAIT_REQUEUE_PIでスリープしている別タスク
- このとき
__rt_mutex_start_proxy_lock()が-EDEADLKを返すと、remove_waiter()は waiter を lock から外す一方でcurrent->pi_blocked_onだけをNULLにする- 実際の waiter の
pi_blocked_onは、自身のカーネルスタック上にあるrt_mutex_waiterを引き続き指し、waiter がユーザー空間に復帰すると、そのスタックフレームは解放済みと見なされる - 以後、PI チェーン探索がそのタスクを通過した瞬間に 解放済みスタックオブジェクトを逆参照 する
- lockdep はどの
pi_lockが保持されたかだけを確認し、それが誰の lock かは検査しないため、この不具合を検出できない
-EDEADLK ロールバックを作る 3 つの futex 循環
- エラーパスに到達するには、3つの futex と 3つのスレッドで PI 依存関係の循環を構成する
f_pi_chain: waiter が先にロックする PI futexf_pi_target: owner が先にロックし、requeue の対象になる PI futexf_wait: waiter がFUTEX_WAIT_REQUEUE_PIで待機する通常の futex
- トリガー順序は次の通り
- waiter が
f_pi_chainをロックした後、FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target)でブロックされ、rt_mutex_waiterが自身のカーネルスタック上に置かれる - owner が
f_pi_targetをロックした後、waiter が保持するf_pi_chainでブロックされる - main スレッドが
FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)を呼び出す
- waiter が
- プロキシ requeue が waiter を
f_pi_targetに接続しようとすると、waiter → f_pi_target → owner → f_pi_chain → waiterの循環が閉じる - PI チェーン探索が
-EDEADLKを返して誤ったロールバックを実行し、waiter はダングリングなpi_blocked_onを持ったまま起床する - 重要な条件は、waiter がまだスタックオブジェクトを保持している間に requeuer がロールバックすることであり、循環が完成した後は処理が自走する
- waiter がユーザー空間に戻れば、もはや時間的制約はなく、後からいつでも
sched_setattr()でチェーン探索を発生させられる - 構成には 3 スレッドを使うが、UAF の競合自体は 1つの CPU コアでもトリガー可能
スタック UAF が提供する初期プリミティブ
- ダングリングポインタは、以前の
FUTEX_WAIT_REQUEUE_PIフレームにあったrt_mutex_waiterを指している - 同じタスクの同じスタック深度に制御可能なバイト列を再配置すれば、カーネルにそれを偽の
rt_mutex_waiterとして逆参照させられる - 偽構造体をどう配置するかによって、1回のアクセスで 2 種類の主要なプリミティブを得られる
- 制約付きで、ほぼ任意のアドレスに ポインタを書き込める
- 制約付きで、ほぼ任意のアドレスに 8 バイトの 0 を書き込める
- 書き込み前に複数のポインタ逆参照と整合性チェックが行われるが、条件を満たせば書き込み後もカーネルはクラッシュせず正常に復帰する
- エクスプロイトを完成させるには、スタックフレームの再利用、偽 waiter の構造チェック通過、書き込み制約を満たす対象の選定がすべて必要になる
PR_SET_MM_MAP による解放済みスタックフレームの再利用
- waiter は futex システムコールから戻るとすぐに
prctl(PR_SET_MM, PR_SET_MM_MAP, ...)を呼び出す prctl_set_mm_map()はユーザー提供の auxv を固定サイズのスタックバッファunsigned long user_auxv[AT_VECTOR_SIZE]にコピーする- このバッファは解放済み waiter と近いスタック深度に配置されるため、大きく整列した制御可能な qword ブロックが以前の
rt_mutex_waiterの上に重なる - auxv の重ね合わせ領域は次のように構成する
tree: 削除時に選ばれた子ポインタW0_BASEをツリールートへ昇格させる rb ノードにするtask: チェーン探索の逆参照を安全に通過できるよう&init_taskに設定するlock: 書き込み先を合わせるため&inet6_protos[IPPROTO_UDP] - 8に指定するwake_state:0に設定する
- auxv を memfd に置き、コピーがページ境界をまたぐように配置したうえで、兄弟スレッドが
prctl実行中に後ろ側のページへfallocate(PUNCH_HOLE)の競合を仕掛け、copy_from_userの時間を引き延ばす - 別 CPU の consumer スレッドは、偽 waiter がスタック上に残っている間に waiter に対して
sched_setattr()を呼び出し、PI チェーンを探索する clone、setsockopt、pselect、keyctlのように、制御可能な大きいスタックローカル変数を使う他のシステムコールでも同じ役割を果たせるprctlが選ばれたのは、バッファが大きく整列しており、名前空間を必要としないためであり、追加候補は 公開 PoC コード に含まれている
rb-tree 削除で制約付きポインタ書き込みを作る
- 偽 waiter を制御できても直ちに完全な任意書き込みになるわけではなく、チェーン探索は次の経路を実行する
task->pi_blocked_onから偽 waiter を見つけるfake waiter->lockから偽rt_mutex_baseを見つけるrt_mutex_dequeue(lock, waiter)がlock->waitersで rb-tree の削除を行う
- 子が1つしかないルートノードを削除すると、その子がルートスロットに書き込まれる性質を利用する
lockをtarget - 8に指定すると、周辺データは次のrt_mutex_baseフィールドとして解釈されるtarget - 8: ロックされていない状態として読まれる必要があるwait_locktarget: 上書きされるwaiters.rb_root.rb_nodetarget + 8:waiters.rb_leftmosttarget + 16:owner
- 結果として実行される単一の書き込みは
*(uint64_t *)target = W0_BASEである - 対象アドレスはおおむね次の条件を満たす必要がある
target - 0x08の下位 32 ビットが0であることtarget + 0x08の 64 ビット値が0であることtarget + 0x10の owner ポインタから下位フラグを除いた値が0であること
- 手前の qword がロック済み spinlock のように見えると trylock が失敗し、何も書き込まずに終了する
- 後ろ側の値が制御されていない top waiter や owner を指す、あるいは未マップ値である場合、カーネルパニックが発生する可能性がある
W0_BASEは比較、再キューイング、優先度更新、owner なし wakeup が終わるまで有効である必要があるため、CEA の direct-map エイリアスを使う
prefetch リークと CPU entry area
-
KASLR・physmap ベースアドレスの特定
- 特定アドレスに対する
prefetchの実行時間は、現在のページテーブルでそのアドレスがマップされているかどうかによって変わる - 非特権プロセスがカーネルアドレス範囲の実行時間を測定すると、マップ位置を推定でき、詳細な原理は prefetch 論文 にまとめられている
- 基本 Linux カーネルイメージのベースアドレスのエントロピーは約 9 ビットであるため、反復測定によって KASLR ベースアドレスをほぼ 100% の信頼度で復元できる
- 理論上は
prefetchがあり適切な KPTI がない CPU が影響を受けるが、実際には KPTI が無効な x86 で主に使われる手法である - kernelCTF イメージでは KPTI が無効であり、KPTI が有効でも
prefetchと EntryBleed を組み合わせれば trampoline を通じてカーネルイメージのベースアドレスを復元できる
- 特定アドレスに対する
-
CEA アドレスランダム化の回避
- CPU entry area(CEA) は、エントリ処理・例外処理用のスタックとレジスタコンテキストを保持する x86 の CPU ごとの構造体である
- 非特権プログラムがソフトウェア例外を発生させると、自身のレジスタコンテキストを CEA 例外スタックの
pt_regsに記録でき、約 120 バイトの連続した制御可能メモリを作れる - Linux 6.2 より前では CEA 仮想アドレスは完全に固定されており、偽構造体、ポインタ逆参照の副作用吸収、ROP スタック構築にそのまま利用できた
- Project Zero の Bringing back the stack attack 公開後、Linux 6.2 から CEA 仮想アドレスは強くランダム化されるようになった
- 各 CPU の CEA 仮想アドレスはそれぞれ異なるようにランダム化されるが、物理アドレスは固定されているため、physmap ベースアドレスが分かれば direct-map エイリアスを計算できる
prefetch、候補境界の正規化、想定される CEA ページの検査を組み合わせることで、周辺エイリアスを除外し、cea_direct = physmap_base + CPU1_CEA_BASEを求める- kernelCTF LTS
6.12.80の 3.5GB ブート環境では、関連オフセットは0x11c517000(+0x1f58)である
CEA を偽 waiter と後続オブジェクトとして再利用
- 最初の書き込みの前に、CEA の
W0に自己整合性を持つ偽 waiter と lock を配置するtaskは&init_taskに設定するprioには有効な値を入れる- lock の
wait_lockはロックされていない状態に見えるようにする - owner は dequeue、再キューイング、優先度更新、wakeup を安全に通過できるよう構成する
- rb-tree 書き込みが終わると
W0はもう waiter である必要がないため、上書きした対象が要求する構造体として CEA を再度埋め直せる - CEA は約 120 バイトと小さいが、計算可能な固定カーネルアドレスにデータを配置できるため効率的である
- NPerm や kernelsnitch などは、より広い領域で同じ役割を果たせる
- エクスプロイトは 1 つの CEA 領域を、偽
rt_mutex_waiter、偽 lock、inet6_protocol、JOP・スタックピボット用スロット、最終的な ROP スタックとして順次または同時に活用する
inet6_protos[IPPROTO_UDP] による制御フロー奪取
- 一般的な x86_64 Linux では、KASLR のベースアドレスを確保した後、条件に合う関数テーブルやそれを含むオブジェクトを上書きする短い経路を選べる
- 書き込み可能なデータ領域の
inet6_protos[IPPROTO_UDP]周辺は、必要な制約を自然に満たしているinet6_protos[16] == NULLは偽のwait_lockの未ロック状態になるinet6_protos[17] == &udpv6_protocolは実際に上書きする対象であるinet6_protos[18] == NULLは偽のrb_leftmostになるinet6_protos[19] == NULLは偽の owner になる
- 書き込みが終わると、
inet6_protos[IPPROTO_UDP]は CEA ページ内の偽のinet6_protocolを指す - CEA を再度スプレーして構造体を次のように構成する
handler: 最初のピボットガジェットとして指定するerr_handler: 使用しないflags:INET6_PROTO_NOPOLICY | INET6_PROTO_FINALに設定する
::1にconnectした後、データを書き込む IPv6 UDP ループバックパケット を送ると、カーネルが偽のhandlerを呼び出し、プログラムカウンタを制御できる
短いピボットと DirtyMode による権限昇格
- Google kernelCTF の
lts-6.12.80対象では、適切な単一スタックピボットガジェットが見つからなかったため、追加の load/call で CEA アドレスをrbpに入れた後、mov rsp, rbp; pop rbp; retでピボットする ret2usrや/proc/%P/fd/xの全体上書きは、約 10 個のガジェット qword が必要で、限られた CEA 空間に比べて大きすぎる- 最終段階では、一度の書き込みで権限ビットを変え、残りの処理をユーザー空間で行う DirtyMode を使う
- 書き込み対象はカーネルデータの
coredump_sysctls[1].mode、つまりcore_patternsysctl のアクセスモードである - カーネルイメージと同じ KASLR スライドを共有するためアドレスを計算でき、書き込みビットである下から 2 番目のビットが設定された値であれば十分である
- 短い
pop reg; mov [reg], reg; retチェーンで mode 値を変更し、msleepで奪取したスレッドを安全に停止させる /proc/sys/kernel/core_patternを全ユーザーが書き込めるようになると、非特権プロセスが|/proc/%P/fd/666 %Pを書き込み、helper をクラッシュさせることで、カーネルに攻撃者のバイナリを root 権限で実行させられる- 初期の rb-tree 書き込みは配置制約のため
coredump_sysctls[1].modeに直接到達できず、mode 変更は短い ROP 段階で行う
全体のエクスプロイトの流れと結果
- 攻撃は次の順序で進む
prefetchでカーネルイメージのスライドと physmap のベースアドレスを漏えいさせる- GhostLock で waiter の
pi_blocked_onにダングリングrt_mutex_waiterを残す PR_SET_MM_MAPで同じカーネルスタックフレームを再利用し、偽の waiter を作る- rtmutex rb-tree の削除を利用して
inet6_protos[IPPROTO_UDP]に CEA ポインタを書き込む - CEA に偽の
inet6_protocol、ピボットスロット、ROP スタックを配置する - IPv6 UDP ループバックパケットで上書きした handler を呼び出す
- DirtyMode で
core_patternの mode ビットを変更し、ユーザー空間で権限昇格を完了する
- kernelCTF のリモート環境では、CEA と DirtyMode を組み合わせた経路で約 5 秒でフラグ を取得した
- 全体のエクスプロイトは CyberMeowfia プロジェクト で公開されている
- Android では、スタックフレーム再利用と ASLR・CFI 回避の方法が異なり、別の続編記事で扱う予定である
代替経路と緩和策
-
より大きな ROP 空間
- NPerm ベースのメモリは、制御フローを奪取した後に大きな偽スタックとして使える
- Lukas Maar の heap-KASLR 漏えい のように、より重い経路も可能だが、段階が増えるため実行時間が長くなる
- kernelCTF では最も短く信頼性の高いチェーンが有利なため、CEA と DirtyMode の組み合わせを使う
-
カーネルパッチ
- 最終パッチでは
currentの代わりにwaiter->taskを基準にpi_lockを取得し、pi_blocked_onを消去する remove_waiter()はwaiter_task = waiter->taskを保存した後、次の順序で処理するwaiter_task->pi_lockをロックする- waiter を rtmutex キューから削除する
waiter_task->pi_blocked_on = NULLに設定する- 後続の
rt_mutex_adjust_prio_chain()にもcurrentではなくwaiter_taskを渡す
- 研究者が v1 以前に送った別の修正案では、呼び出し側が所有タスクを明示的に渡す構成になっていた
- 自分自身がブロックされる経路では
currentを渡す - プロキシロールバックではプロキシ対象の
taskを渡す pi_blocked_onが依然としてその waiter を指しているときだけ消去し、タスクのpi_lockで保護する
- 自分自身がブロックされる経路では
- 最終パッチでは
-
RANDOMIZE_KSTACK_OFFSET- エクスプロイトは、解放された waiter フレームと後続の
user_auxvフレームが決定的に重なることに依存している RANDOMIZE_KSTACK_OFFSETを有効にすると、スタックオフセットが変わり、この段階は約 1/32 の確率の 5 ビット推測 に変わる- 提出された 2 つの一般対象ではこの設定はデフォルトで無効で、緩和対象では有効だったため、このエクスプロイト経路は使わない
- エクスプロイトは、解放された waiter フレームと後続の
-
STATIC_USERMODE_HELPERSTATIC_USERMODE_HELPERはこの特定の DirtyMode 経路を遮断する- ただし、アクセス権限を
ctl_table::modeで制御し、テーブルが予測可能な書き込み可能カーネルデータにある他の/proc/sys設定にも、同じ方式を一般化できる
公開日程
- 2026年4月18日: 脆弱性と草案パッチを
security@kernel.orgに送付 - 2026年4月20日: 別のパッチで脆弱性が修正される
- 2026年5月4日: 修正案 v1 がバックポートされる
- 2026年6月30日: Google が kernelCTF 提出を確認
- 2026年7月7日: 技術分析を公開
- VEGA が発見した脆弱性には標準の 90+30日公開ポリシー が適用される
1件のコメント
Hacker News の意見
Android 9・13・16 と、それぞれ異なる Firefox 150 未満のバージョンを使う端末 3 台でテストしたところ、2 台はブートループに陥ってリカバリモードに入る必要があり、残り 1 台は電源が落ちた。デモは対応する Pixel 端末の壁紙を変更するもので、テストページは IonStack で確認できる
個人端末でブログや任意のサイトを見るときは、メインブラウザとは別に Chromite のような Chromium 系ブラウザを入れ、フラグで JavaScript と、攻撃されがちなハードウェアアクセラレーション動画デコーダーを無効にし、壊れるサイトにはリーダーモードを使うほうが安全。あるいは専用タブレットを用意する方法もある
adbをインストールして確認したうえで全結果を明らかにする予定テストページにアクセスすると Firefox タブに出力が表示され、概念実証コードが実行されたように見えたが、その後スマートフォンが固まり、すべての入力を受け付けなくなった。再起動だけは動作したが、カーネルが停止しているように見える状況で、どうして再起動イベントには応答できるのか気になる。画面は実行結果の一部を表示したまま点灯し、その後スクリーンセーバーが作動した
エクスプロイトを発見しただけでなく、copyfail と違って誰でもすぐ使える ゼロデイのローカル権限昇格スクリプトを公開しなかったセキュリティ研究者たちに大きな賛辞を送りたい
Rocky Linux 9 で数時間にわたりローカル権限昇格(LPE)を試したが、幸い成功しなかった。時間に余裕があるか、非常に腕が立つのでなければ、企業向けディストリビューションで実際の攻撃に利用するのは難しそうだ
一般にブートローダーを解除できないスマートフォンでも、この脆弱性で ブートローダーのロック解除が可能なのか気になる。可能なら Android エコシステムで起きた最高の出来事の一つになり得る
タイトルにローカル権限昇格を意味する LPE を入れておけば、ほとんどの人が安心して週末に戻れたのではないかと思う
しかし今回の攻撃は、Firefox の隔離されたブラウザプロセスのような 強力にサンドボックス化されたプロセス内からでも発動できる。攻撃者は JavaScript の脆弱性で隔離サンドボックス内にローカルコードを実行し、その後この脆弱性でカーネルモードまで昇格する二段階攻撃をつなげればよいので、Firefox と Linux カーネルの両方を更新する必要がある
「Google が kernelCTF の報奨金として 92,337 ドルを支払った」というくだりから俄然興味を引かれた
Android アプリが NDK でネイティブコードを実行して root 権限を得られるという意味なのか、SELinux が防御に役立つのか気になる
古いカーネルにもパッチをバックポートすることはできるが、スマートフォンのアップデート変更履歴で CVE を明記することはまれなため、脆弱性検査ツールが事実上唯一の確認手段になる。Play Store や外部から入手したアプリが侵害されていれば即座に root 権限を得られるので、インストール時に信頼と監査を確認する原則は依然として重要だ
今後、すべての Google Play Integrity レベルにこの検査が追加され、パッチ未適用のスマートフォンには多くのアプリをインストールできなくなる可能性がある。任意のサイトや広告を避けにくいブラウザでは、サンドボックス脱出がアプリ分離まで迂回するため、より深刻であり、iOS の JailbreakMe に似ている
過去 15 年に出た Linux でネイティブコードを実行できるアプリなら、端末にカーネルアップデートが届くまで root 権限を得られる
GhostLock が Linux 2.6.39 に入り、Linux 7.1 でようやく修正されたという事実は衝撃的だ
コメントを前日にすでに読んだ気がするのに、投稿時刻はすべて 10 時間以内と表示されていて、HN の時刻表示がおかしいのか気になる
毎日確認している「underwater」リスト、つまり多くの賛成票を得たものの何らかの理由でトップページに載らなかった記事の一覧で、この投稿が最上位だったので再掲載した。奇妙に見えるが、まだこれより混乱の少ない代替案は出ていない