0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全ディストロに15年潜んだLinuxカーネルの権限昇格バグ ― CVE-2026-43499「GhostLock」に学ぶ、futex優先度継承の落とし穴とコンテナ時代のパッチ運用

0
Last updated at Posted at 2026-07-13

なぜ今このバグを追う必要があるのか

2026年7月7日、セキュリティ研究チームNebuSecが「GhostLock」と名付けたLinuxカーネルの脆弱性CVE-2026-43499の詳細な解析を公開した。ローカル権限昇格(LPE)の一種で、CVSS 3.1のベーススコアは7.8(HIGH)、攻撃ベクトルはローカルだが必要権限は一般ユーザー相当という組み合わせになっている。

このバグの怖いところは規模と潜伏期間にある。導入されたのはLinux 2.6.39、つまり2011年のリリースだ。修正が入ったのは2026年のLinux 7.1系で、その間およそ15年、ほぼすべてのディストリビューションの本番カーネルに黙って居座っていたことになる。研究チームはGoogleのkernelCTF環境で97パーセントという高い安定性の権限昇格を実証し、9万2千ドル超の報奨金を得たと報告している。

日本のインフラ現場では、ローカル権限昇格と聞くと「外部からいきなり侵入されるわけではないから優先度は低い」と後回しにしがちだ。しかしこの見立てはコンテナやマルチテナントの時代には通用しない。一般ユーザー権限さえあれば刺さるバグは、共有カーネルの上で動くコンテナや、開発者が相乗りする共用サーバ、CIランナーといった環境では「侵入の最後の一歩」を一気に埋めてしまう。本記事ではGhostLockの技術的な核心を追いつつ、日本の運用現場が具体的に何を確認し、どう直すべきかまで落とし込む。

何が起きているのか、事実を整理する

GhostLockはカーネルのkernel/locking/rtmutex.cに存在するUse-after-Free(解放済みメモリの再利用、CWE-416)だ。核心にあるのはremove_waiter()という関数の後始末処理が、想定外の経路で「別のタスクの状態」を消してしまう点にある。

まず影響範囲を押さえておきたい。NebuSecの解析とNVDの登録情報を突き合わせると、修正済みバージョンは次のように分かれている。裏を返せば、これより古い各系列はすべて影響を受ける。

系列       修正が入ったバージョン   これ未満は影響あり
2.6.39〜   (起点。ここから混入)
6.1.x      6.1.175                6.1.174 以下
6.6.x      6.6.140                6.6.139 以下
6.12.x     6.12.86                6.12.85 以下
6.18.x     6.18.27                6.18.26 以下
7.0.x      7.0.4                  7.0.3 以下
7.1        (オリジナルの修正)

発動条件はビルド設定CONFIG_FUTEX_PI=yが有効なことで、これは主要ディストリビューションではデフォルトで入っている。特別な権限やユーザー名前空間の有効化すら不要で、一般ユーザーのプロセスから素のfutexシステムコールを組み合わせるだけで到達できる。この「前提条件がほぼ無い」点が、後述するコンテナ環境での深刻さに直結する。

修正はkernel.org側で2026年4月に本流へ入り、各安定版ブランチへバックポートされている。パッチの本質は、remove_waiter()が消すべき対象を「今実行中のタスク」ではなく「本来の待機者のタスク」に正し、なおかつ正しいタスクのpi_lockを保持した状態で処理するように直したことだ。

技術的な核心、なぜ待機者の状態が壊れるのか

このバグを理解するには、優先度継承futex(PI-futex)とrequeueという二つの仕組みを押さえる必要がある。

PI-futexとrt_mutexの関係

通常のfutexは、ユーザー空間のロック変数が競合したときだけカーネルに降りてきて待機する軽量な同期プリミティブだ。ここに優先度継承を足したものがPI-futexで、低優先度のスレッドがロックを握ったまま高優先度スレッドを待たせる優先度逆転を防ぐ。カーネル内部ではこの調停をリアルタイムミューテックスrt_mutexが担い、誰が誰を待っているかという依存関係をタスクごとのpi_blocked_onポインタと、優先度順に並べたrb木(赤黒木)で管理している。

pthread_cond_waitのような条件変数を効率よく実装するために、LinuxにはFUTEX_WAIT_REQUEUE_PIとFUTEX_CMP_REQUEUE_PIという操作がある。あるfutexで待っているスレッドを、起こす代わりに別のPI-futexの待ち行列へ「付け替える(requeue)」仕組みだ。この付け替えの内部で呼ばれるのがrt_mutex_start_proxy_lock()、すなわち「待機者本人ではない第三者(requeueする側)が、待機者の代理でロック取得を仕掛ける」処理になる。

想定が一つだけ違った

問題のremove_waiter()は、もともと「あるスレッドが自分でブロックした後、自分の後始末をする」という単一のシナリオのために書かれていた。だから内部では暗黙のうちに「後始末する相手=いま実行中の自分」と決め打ちしている。

ところがrequeue-piの代理ロック経路はこの前提を崩す。__rt_mutex_start_proxy_lock()がデッドロックを検出して-EDEADLKで巻き戻す際にremove_waiter()を呼ぶと、消されるのは代理でロックを仕掛けた「requeueする側」のpi_blocked_onであって、本来クリアすべき「待機者」のそれではない。しかもrb木からの取り外し(dequeue)が、待機者タスクのpi_lockを保持しないまま行われる。

結果として、待機者のpi_blocked_onは、システムコールが戻って解放されたカーネルスタック上のrt_mutex_waiter構造体を指したまま宙に浮く。これがdangling pointerであり、UAFの入口になる。誤クリアのイメージを疑似コードで示すとこうなる。

/* 概念図。実際のコードそのものではない */
static void remove_waiter(struct rt_mutex *lock,
                          struct rt_mutex_waiter *waiter)
{
    /* 旧: 「後始末する相手=current」と決め打ち */
    current->pi_blocked_on = NULL;        /* ← requeue経路では“requeue側”を消す誤り */

    /* rb木からの取り外しが waiter->task->pi_lock 非保持のまま走る */
    rt_mutex_dequeue(lock, waiter);

    /* 修正後は waiter->task の pi_lock を取り、
       waiter->task->pi_blocked_on を正しくクリアする */
}

宙に浮いたポインタを武器に変える

UAFそのものは「解放済み領域を指すポインタが残る」という状態にすぎない。ここからroot奪取まで持っていく道のりが、GhostLockの解析で最も読み応えのある部分だ。要点だけ追うと次のような多段構成になっている。

まずprctl(PR_SET_MM_MAP)などを使って、解放されたスタックフレームの上に攻撃者が制御するバイトを敷き直し、rb木ノードを細工した偽のrt_mutex_waiterを並べる。次にsched_setattr()でPIチェーンを辿らせ、rb木の取り外し処理を「任意アドレスへの書き込み」プリミティブとして悪用する。この書き込みでCPUエントリエリア(CEA)内を指すようinet6_protos[IPPROTO_UDP]を書き換え、そこへ偽のinet6_protocolハンドラを再配置したうえで、ループバック宛てのIPv6 UDPパケットを一発送るだけで制御フローを乗っ取る。

仕上げは研究チームが「DirtyMode」と呼ぶ手口で、たった一回のROP書き込みでcore_patternのモードビットを書き換え、コアダンプ発生時に任意バイナリをrootで起動させる。これによりKASLRやスタック保護をかいくぐりつつ、安定して管理者権限を取る。

なぜ「15年」「全ディストロ」なのかも、ここまで来ると腑に落ちる。requeue-piが追加されたのが2.6.39であり、CONFIG_FUTEX_PIが既定で有効なため、その世代以降のカーネルはビルドした瞬間からこの経路を抱え込んでいた。誰も踏まなかったのではなく、デッドロック検出という滅多に通らない巻き戻し経路に潜んでいたため見過ごされ続けた、という性質のバグだ。

日本の実務への示唆、どこを警戒しどう直すか

「ローカル権限昇格だから低リスク」という思い込みを捨てる

まず認識を改めたいのは、共有カーネルの上ではローカル権限昇格が実質的な「境界突破」になるという点だ。コンテナはホストとカーネルを共有する。したがってコンテナ内の一般権限プロセスがこのバグを突けば、到達するのはコンテナの権限ではなくホストのカーネルであり、ホストroot、さらにはノード上の他テナントまで被害が及びうる。

しかもDockerのデフォルトseccompプロファイルはfutexシステムコールを許可しているため、FUTEX_WAIT_REQUEUE_PIやFUTEX_CMP_REQUEUE_PIを含む発動経路が標準状態で通ってしまう。マネージドKubernetesのマルチテナントノード、外部PRを実行するCIランナー、研究室や受託開発でありがちな共用ログインサーバ。こうした「信頼しきれない一般ユーザーがコード実行できる」環境こそ最優先の対処対象になる。

まず現状を確認する

自分の環境が影響下かどうかは、稼働カーネルのバージョンで判断できる。前掲の修正バージョン表と突き合わせればよい。

# 稼働中カーネルの確認
uname -r

# ディストロが配布するパッケージ版数の確認例
# Debian/Ubuntu
dpkg -l | grep -E 'linux-image'
# RHEL/AlmaLinux/Rocky
rpm -q kernel

注意点として、ディストリビューションのカーネル版数は上流のx.y.zと一致しない。RHEL 9系は5.14ベース、Ubuntu 22.04は5.15、Ubuntu 24.04は6.8、Debian 12は6.1というように、各社が独自の版数体系でセキュリティ修正だけをバックポートする。したがって「上流の6.1.175より古いから危険」と早合点せず、ディストリビューションが出す当該CVEのアドバイザリ(Ubuntu USN、Red Hat RHSA、Debian DSAなど)で「修正済みと明記されたパッケージ版数以上か」を確認するのが正しい。

直す、そして直せない間を耐える

本命は言うまでもなくカーネル更新だ。更新後は再起動が必要で、kexecやライブパッチを使わない限り、稼働中カーネルは古いままである点に気をつけたい。

# Debian/Ubuntu
sudo apt update && sudo apt upgrade
sudo reboot

# RHEL系
sudo dnf update kernel
sudo reboot

すぐに再起動できない、あるいは長寿命の組み込み機器などで即時更新が難しい場合は、悪用の難度を上げる緩和策を重ねる。GhostLockの解析でも次の二つが有効だと示されている。

CONFIG_RANDOMIZE_KSTACK_OFFSET=y
  システムコールごとにスタックオフセットを乱数化し、
  解放済みスタックフレームの再取得(spray)を難しくする。

CONFIG_STATIC_USERMODE_HELPER=y
  usermode-helperのパスを固定し、
  core_patternを書き換えるDirtyMode経路を塞ぐ。

コンテナ環境では、多層防御として発動経路そのものを絞る手も検討に値する。requeue系のfutex操作を業務で使っていないワークロードであれば、seccompプロファイルでrequeue-piに関する操作を制限することで攻撃面を減らせる。ただしfutex自体は一般的なランタイムが多用するため、闇雲に禁止すると正規のプログラムが壊れる。挙動を観測したうえで慎重に絞り込むべきだ。より根本的には、gVisorやKata Containersのようにカーネルを共有しない分離方式を、信頼できないコードを走らせる区画に採用するのが堅い。

検知の観点では、core_patternが実行ファイルを指す形へ書き換えられていないか、inet6_protosまわりの異常など、悪用の痕跡を監視するのが現実的だ。ただしこの種のカーネルLPEは痕跡が薄く、事後検知より事前のパッチ適用がはるかに費用対効果が高い。

まとめ、エンジニアが今日動くためのアクション

GhostLock(CVE-2026-43499)は、優先度継承futexという普段は意識しない機構の、めったに通らない巻き戻し経路に15年潜んでいたUAFだった。派手なリモートコード実行ではないぶん軽視されがちだが、コンテナとマルチテナントが当たり前になった今、ローカル権限昇格は「境界の内側での境界突破」として扱うべきリスクになっている。

現場で今日から動けることを整理する。第一に、uname -rで稼働カーネルを確認し、ディストリビューションのCVEアドバイザリで修正済み版数と突き合わせる。第二に、信頼できない一般ユーザーがコード実行できる環境(共用サーバ、CIランナー、マルチテナントK8sノード)から優先してカーネルを更新し、再起動まで完了させる。第三に、即時更新が難しい区画ではRANDOMIZE_KSTACK_OFFSETとSTATIC_USERMODE_HELPERで時間を稼ぎ、必要に応じてseccompやVM分離で発動経路を狭める。第四に、この一件を機に「自組織のカーネルはどこで、どれくらいの遅延でパッチが当たるのか」という運用フロー自体を点検しておきたい。15年潜むバグが今後も出てくる以上、勝負を分けるのは個別の脆弱性の知識ではなく、公開から適用までのリードタイムを短く保てる仕組みだ。


参考ソース

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?