
CVE-2026-52910 のレース再現ツール兼ストレステストツールキットです。これは Linux カーネルの classic BPF (cBPF) reuseport セレクタプログラム の処理における use-after-free (UAF) であり、アップストリームではコミット "bpf: Free reuseport cBPF prog after RCU grace period" で修正されています。
| CVE | CVE-2026-52910 |
| 種別 | Use-after-free / 範囲外読み取り (CWE-125)、CVSS 3.1 7.8 HIGH AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| 導入 | v4.5 (reuseport cBPF サポートとともに) |
| 修正済み | 5.10.259, 5.15.210, 6.1.176, 6.6.143, 6.12.94, 6.18.36, 7.0.13 (stable); mainline v7.1 |
| アップストリームの splat | BUG: KASAN: vmalloc-out-of-bounds in reuseport_select_sock (net/core/sock_reuseport.c:596) |
| 報告者 | Eulgyu Kim |
[!WARNING] これはカーネルストレステストツールです。 脆弱なカーネル上では use-after-free のレースウィンドウを意図的に広げます。ヒットすると マシンがクラッシュしたり破損したりする可能性があります。 自分が所有するマシン、または明示的にテストを許可されたマシン (テスト用 VM、使い捨ての CI マシン) でのみ実行し、本番システムでは 決して実行しないでください。
SO_REUSEPORT は多数のソケットが同じ UDP ポートにバインドすることを可能にします。受信パケットごとに、カーネルは reuseport_select_sock() (net/core/sock_reuseport.c) でグループ内のソケットを1つ選択します。グループにはセレクタプログラム — setsockopt(SO_ATTACH_REUSEPORT_CBPF) でアタッチされる classic BPF プログラム — をインストールでき、これがパケットごとにグループ内のどのソケットがそれを受け取るかを決定します。このプログラムは RCU 読み取り側クリティカルセクション内の RX softirq (ネットワーク受信処理) で実行されます。
バグ: プログラムが setsockopt() (reuseport_attach_prog() / reuseport_detach_prog()) で置き換えられたりデタッチされたりすると、古い cBPF プログラムは sk_reuseport_prog_free() によって即座に解放され、実行中の RCU リーダーを待ちません。解放されたプログラムの命令をまだ辿っている CPU は、解放済みの vmalloc メモリを読み取ります:
sequenceDiagram
autonumber
participant C as CPU0 — churner thread
participant K as setsockopt() path
participant R as CPU1 — RX softirq
R->>R: rcu_read_lock()
R->>R: prog = rcu_dereference(reuse->prog)
C->>K: setsockopt(SO_ATTACH_REUSEPORT_CBPF, progB)
K->>K: swap progA → progB
K->>K: sk_reuseport_prog_free(progA)
Note right of K: unfixed kernels: bpf_prog_free()<br/>runs NOW — no RCU grace period
R->>R: execute progA->insns (run_bpf_filter)
Note right of R: progA was already freed<br/>KASAN: vmalloc-out-of-bounds
Note over K: fix: call_rcu(sk_reuseport_prog_free_rcu) —<br/>free deferred by one RCU grace period
eBPF セレクタパス (SO_ATTACH_REUSEPORT_EBPF) は影響を受けません。これは既に遅延された bpf_prog_put() ステージを通じてプログラムを解放しています。修正は cBPF パスにも同じ扱いを与えます — 古いプログラムが解放される前に1回の RCU グレースピリオドを置きます。
アップストリームの KASAN レポート (7.0 デバッグカーネル上):
BUG: KASAN: vmalloc-out-of-bounds in reuseport_select_sock+0xedc/0x1220
Read of size 4 at addr ffffc9000051e004 by task slowme/10208
net/core/sock_reuseport.c:596
| ファイル | 目的 |
|---|---|
reuseport_race_hammer.c | 再現ツール: 完全な UDP 負荷の下で cBPF セレクタを撹拌するマルチスレッドハンマー。その後、配信の完全性を検証します。 |
run_hammer.sh | 1回実行用ラッパー: net.core.optmem_max を引き上げ、ハンマーを実行し、dmesg の splat、/proc/vmallocinfo のリークした bpf_prog アロケーション、オプションで kmemleak をチェックします。 |
livepatch_cycle.sh | ハンマーの実行中に修正を搭載した livepatch を適用/復元します — call_rcu() ベースの修正の livepatch ライフサイクルハザードを追跡します。 |
Makefile | ハンマーをビルドします。 |
.github/workflows/ci.yml | CI: ビルド + shellcheck (カーネルランタイムテストはなし。CI を参照)。 |
Linux テストマシン上で:
$ make
$ sudo ./run_hammer.sh 600 # 10-minute run
...
== result: RC=0 (0 clean / 1 setup / 2 splat / 3 leak / 4 integrity) ==
要件:
gcc と bash。dmesg、/proc/vmallocinfo、sysctl) には root 権限。ハンマー自体は非特権で実行されます (アップストリームの再現は UID 1000 で実行されました)。期待される結果:
RC=2。時折、ハンマーの完全性チェックが先に発火 → RC=4。RC=0、安定した bpf_prog vmalloc カウント。レースウィンドウは非常に小さい (解放 vs. 実行中の RX 実行) ため、1回のクリーンな実行は結論を出せないものとして扱ってください。実際のテストでは数時間実行します。例:
$ sudo ./run_hammer.sh 86400 512 8 16 4 127.0.0.1 0
reuseport_race_hammer$ ./reuseport_race_hammer [dur_sec] [insns] [nports] [nsocks] [nsenders] [ip] [ebpf]
| 引数 | デフォルト | 意味 |
|---|---|---|
dur_sec | 300 (最小 45) | 総実行秒数 |
insns | 256 | 撹拌される cBPF プログラム内のフィラー命令。プログラムが大きいほど、ヒットする解放領域が大きくなります。アタッチが ENOMEM で失敗する場合は、net.core.optmem_max を引き上げてください (ラッパーがこれを代行します)。 |
nports | 4 (最大 64) | reuseport グループ (それぞれ 21000 から1つの UDP ポート) |
nsocks | 8 (最大 512) | グループあたりのソケット数 |
nsenders | 4 (最大 32) | グループあたりの UDP 送信スレッド数 |
ip | 127.0.0.1 | ターゲットアドレス。物理 NIC の IP を使用すると RX softirq を CPU 間に分散できます (RSS) |
ebpf | 0 | 1 = SO_ATTACH_REUSEPORT_EBPF のアタッチ/デタッチも撹拌します (CAP_BPF/CAP_NET_ADMIN が必要)。このパスは脆弱ではありません。これは比較/カバレッジ用です |
各グループは、タイトループで setsockopt() を介して cBPF セレクタを交換・デタッチする churner スレッドを1つ実行し、sender スレッドがグループに 64 バイトの UDP データグラムをフラッディングし、レシーバーがソケットごとの配信をカウントします。
実行フェーズ (T = dur_sec):
time ──────────────────────────────────────────────────────────────►
[0 ──────────── T-15s) [T-15s ── T-10s) [T-10s ─────────── T]
CHURN + FLOOD SETTLE MEASURE
churner swaps/detaches churn frozen, deterministic program
the selector prog at final program (selects the LAST
max rate under full attached socket): EVERY packet
UDP flood — THE (selects LAST must land on the LAST
race window open socket) socket; snapshot A →
run → snapshot B
完全性チェック: 測定フェーズ中、最終プログラムはグループの最後のソケットを決定論的に選択します。そのウィンドウ中にグループが受信したパケットがすべてそのソケットに着地しなかった場合、選択が誤って行われたことになります (KASAN なしでも起こりうる UAF の影響) → 終了コード 2。
ハンマーの終了コード: 0 PASS · 1 セットアップ/ランタイムエラー · 2 完全性 WARN。
run_hammer.sh — 1回実行用ラッパーハンマーを実行し、1回の実行を意味あるものにするチェックを追加します:
/proc/vmallocinfo の bpf_prog アロケーションカウントをスナップショットします。net.core.optmem_max を引き上げます。DRAIN (デフォルト 30 秒) 待ちます。BUG:、Oops:、WARNING:、RIP:、leaked、stuck) がないかスキャンします。bpf_prog vmalloc カウントを前後で比較し (リークチェック)、/sys/kernel/debug/kmemleak が存在する場合はオプションで kmemleak をスキャンします。| 環境変数 | デフォルト | 意味 |
|---|---|---|
HAMMER | ./reuseport_race_hammer | ハンマーバイナリ |
DRAIN | 30 | 実行後、チェック前に待機する秒数 |
OPTMEM_MAX | 131072 | net.core.optmem_max の値。0 = 変更しない |
終了コード: 0 クリーン · 1 セットアップエラー (ハンマーのセットアップ失敗を含む) · 2 カーネル splat 検出 · 3 bpf_prog リークの可能性 · 4 ハンマー完全性 WARN。
livepatch_cycle.sh — livepatch ライフサイクルテストこの修正は call_rcu() コールバックから古い cBPF プログラムを解放します。修正を livepatch (カーネルライブパッチ — 実行中のカーネルにコードをパッチする) として配布する場合、コールバック関数自体がパッチモジュール内に存在します。コールバックがまだ保留中の間に復元/アンロードすると、コールバックの足元でモジュールテキストが解放されます。このスクリプトは、ハンマーでレースウィンドウをホットに保ちながら適用/復元サイクルを実行し、/sys/kernel/livepatch/*/transition と dmesg を監視します。
$ MODE=rcu ./livepatch_cycle.sh 20 120 # 20 cycles × 120s hammer each
| モード | 動作 |
|---|---|
cycle (デフォルト) | 適用 → 復元、どちらも持続的なハンマー負荷の下で |
rcu | 最大撹拌時に即座に復元 — 上記のハザードケース |
safe | ハンマーを停止 → GRACE スリープ (デフォルト 30 秒、1グレースピリオド) → 復元 |
| 環境変数 | デフォルト | 意味 |
|---|---|---|
APPLY_CMD / REVERT_CMD | kpatch load $PATCH / kpatch unload $PATCH | livepatch コマンド |
PATCH | ./livepatch-reuseport.ko | パッチモジュール |
HAMMER | ./reuseport_race_hammer | ハンマーバイナリ |
HAMMER_ARGS | 256 4 8 4 127.0.0.1 0 | ハンマーの引数 |
TRANSITION_TIMEOUT | 60 | livepatch トランジションを待つ最大秒数 |
GRACE | 30 | MODE=safe のグレースピリオドスリープ |
FORCE | 0 | 1 = livepatch トランジションが検出されなくても実行 |
終了コード: 0 クリーン · 1 コマンド失敗 · 2 splat またはスタックしたトランジション · 4 ハンマーからの完全性 WARN。