
CVE-2026-98260(posix-cpu-timers exec UAF)エクスプロイト研究:S9280 Android 14 GKI 6.1 — UAFトリガー + ゼロ書き込みプリミティブ + SELinux Permissive。引き継ぎドキュメントはHANDOFF.mdにあります。
exec: Cleanup POSIX timers right after de_thread()(作者 Hyunwoo Kim、2026-09-11 アップストリーム、
Fixes: 55e8c8eb2c7b = 5.7 以降)。
非リーダースレッド E に CLOCK_THREAD_CPUTIME_ID タイマーが登録されている。execve() 内で
de_thread() が tid を交換した後、タイマーは PID による task の逆引きに失敗し、ノードは E の
posix_cputimers に残ったままになる。exec_mmap() の mmap_read_lock_killable() が -EINTR
を返す場合(exec が失敗し、クリーンアップコードはさらに後方にある)、do_exit() → exit_itimers() が
k_itimer を解放するがノードは取り除かれない。親プロセスの wait4() による回収時に
cleanup_timerqueue() → timerqueue_del() が解放済みオブジェクトに対して操作を行い → UAF となる。
| ブランチ | posixtimer_exec | 状態 |
|---|---|---|
| linux-6.1.y | なし | 未修正 |
| linux-5.15.y | なし | 未修正 |
| linux-6.6.y | あり | 修正済み(6.6.158+) |
| linux-6.12.y | あり | 修正済み(6.12.112+) |
→ 本プロジェクトの対象(5.15 / 6.1 系、および 6.6.158 未満の 6.6)はすべて影響範囲内。
fragment_vmas() は memfd + MAP_FIXED でアドレス空間を
--vmas(デフォルト 50000)個のマージ不可 VMA に分割する(各 VMA の vm_pgoff はすべて 0 で、
隣接アドレスの pgoff が連続しない → vma_merge が失敗)。locker スレッドが全区間を繰り返し
mprotect し、数万の VMA を走査することで mmap 書き込みロックがミリ秒単位で保持される。tgkill(tgid, 旧 tid, 0) が ESRCH を返すことで
「de_thread() の tid 交換が完了した」と判断し、その時点で初めて SIGKILL を送る。exec_mmap() の箇所で mmap_read_lock_killable() に必ずブロックし
(読み取りロックが書き込みロックに阻まれる)、SIGKILL はその直後にマイクロ秒単位で到達する → この呼び出しは -EINTR
を返す → exec が失敗する。ウィンドウ(ms)≫ 反応時間(µs)であるため、運に依存しない。⚠️ 注意すべき区別:トリガーは確定的である。UAF から任意書き込み/cred 改変までは
posix_timers_cache の slab 再利用の編成(order、objects/slab、CPU partial、
memcg の所属)に依存し、これは測定可能かつ反復可能なヒープエンジニアリングであり、競合状態ではない。
export ANDROID_NDK_HOME=/home/nt/项目/samsung_root_research/06_工具链/android-ndk-r29
make # 产物 trigger(aarch64 / Android 21+ / 仅依赖 libc、libdl)
adb push trigger /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/trigger"
adb shell "/data/local/tmp/trigger --check" # 先看环境
adb shell "/data/local/tmp/trigger; echo exit=\$?"
成功の目印(3つのうち1つで可):
[*] 開始トリガー を出力した後にデバイスが再起動/画面が黒くなる(panic_on_oops=1 のとき最も直接的)exit=137/139)、かつその後もシステムが生きているcleanup_timerqueue / timerqueue_del /
rb_erase / rb_next のコールスタックが現れる失敗(未トリガー)の場合、プログラムは自ら --attempts 回リトライし、各ラウンドの wait4 結果を出力し、
最後にパラメータ調整のヒントを表示する:--vmas 120000 --lockers 4 --cpu 0。
panic_on_oops は 0 の可能性がある:この場合タスクは kill され、カーネルは破損した rbtree を抱えたまま動き続ける。
2〜3 回連続で実行し、後続の panic が現れるか観察することを推奨。テスト環境:S9280ZHU6EZIF、カーネル 6.1.162-android14-11-34343818-abS9280ZHU6EZIF
(ビルド 2026-09-20)、未 root、shell ドメイン(uid 2000)、SELinux Enforcing。
| ラウンド | 観測 | 意味 |
|---|---|---|
| 各ラウンド | 親プロセスの tgkill(child_tgid, etid, 0) が ESRCH を返す | de_thread() が tid 交換を完了した |
| 各ラウンド | 続いて kill(child_tgid, SIGKILL)、子プロセスの rc=-9 | exec が中止された(成功した exec の rc=0 ではない) |
--no-killer 対照 | exec が armed から終了まで 277 ms(killer ありでは 287 ms) | mmap 書き込みロックが確かに exec_mmap() の mmap_read_lock_killable() を妨げた |
5/5 ラウンドすべてで「ESRCH → SIGKILL → exec 失敗」に命中し、exec が成功した例は一度もない。
これは de_thread 後に exec を中止させるウィンドウが制御可能かつ安定していることを証明する;ウィンドウは locker の
mprotect(6 万 VMA、1 回 200〜700 ms)に由来し、親プロセスの反応は µs 級である。
de_thread() は zap_other_threads() で同一プロセスの
他のスレッドをすべて殺すため、子プロセス内に書いた killer は de_thread の瞬間に自ら先に死に、
永遠に ESRCH を見られない(exec が毎回成功するという挙動になる)。SIGKILL 直後の spray は無効——その時点では子プロセスはまだ死んでおらず、
k_itimer はまだ RCU で解放されていない。正しい順序は
waitid(WNOWAIT) でゾンビを待つ → RCU コールバックの解放を待つ → ページを奪う → 最後に wait4 で回収パスをトリガーする。現在「解放後もノードがキューに残る」段階は安定して達成しているが、wait4 時点の rb_erase
で oops は起きていない——被害 slab ページがまだ我々の spray に奪われていないことを示す(posix_timers_cache
の partial slab はまだキャッシュされており、ページは buddy に戻っていない)。これは exploit の回収オーケストレーションの問題であり、
競合状態の問題ではない。vivo ノートのやり方に従う必要がある:まず k_itimer 自身で slab を満杯/空にして
ページ全体の返却を促し、次に order-0 のキャリア(AIO ring など)で split された半ページを正確に受け止める。
次のステップの提案:このマシンを基準とし、docs/08-reclaim-choreography.md(vivo ノート)の
考え方に従って定向回収を行う;または KASAN 付きの自前ビルドカーネルで UAF が実際に発生するか検証する。
公開チェーン(Honor uid0.c)は tracefs の kmem イベントに依存して2つの重要なことを行う:
| 用途 | Honor 611/6.12 用の | 本機(Samsung 6.1.162) |
|---|---|---|
被害者の k_itimer アドレス V を特定(payload のページ内オフセットを計算) | kmem:kmem_cache_alloc | イベントは存在するが、events/kmem/*/enable は 0644 root:root であり、shell からはenable できない(events/sched/*/enable は書き込み可能、kmem は不可) |
| order-1 slab ページが buddy に返却されたことを判定(正確なページ奪取タイミング) | kmem:mm_page_free | 同上、存在するが enable できない |
| 生の trace バッファを読み取る | per_cpu/cpuN/trace_pipe_raw | 読み取り可能(-r--r----- root readtracefs)✓ |
| KASLR slide | 各 trace イベントの static site | 可能:sched イベントは enable 可能、プロジェクトに同種の手法あり ✓ |
結論:公開されているチェーンはそのまま移植できない。不足しているのはカーネル機能(イベントはすべて存在する)ではなく、shell に kmem イベントの enable 権限がないことである。
V は不要になる:payload をページ内位置非依存にする——8KB ページ内のすべての可能な
object スロット(k*0x108 + 0x78 + 0x10)に対して target を一度ずつ書き込む。これにより、被害オブジェクトが
slab のどのスロットに位置しても、rb_erase は target に到達する。これは「全スロットカバー」で
kmem_cache_alloc によるアドレス発見を置き換えることに等しい。mm_page_free による正確な同期を外した後は、RCU グレースピリオド後に
大量の sk_buff データページ(AF_UNIX SO_SNDBUF)を spray して、返却されたばかりの order-1 ページに
ぶつけるしかなく、成功率は低下するため、precheck + リトライループが必要になる。SLIDE_TRACEFS_* 機構がある)。cred アドレスをどこから得るか。公開チェーンは同じ kmem リークで cred を取得するが、
本機では不可能。選択肢:
selinux_state.enforcing、slide は既知)を攻撃する。
ただしそれでは SELinux を無効化できるだけで、uid 0 は取得できない。結論:完全なチェーン = 数日規模の移植(まず位置非依存のページ奪取で UAF を可視化し、次に cred リークを解決する)であり、 一巡で通せるものではない。
デバイス:SM-S9280 /
6.1.162-android14-11-34343818-abS9280ZHU6EZIF(One UI 9.0 / Android 17) ルート:UAF → ゼロ書きプリミティブ → cred ゼロ書き → uid 0。三つの段階で推進中、現在は第二段階で停滞。
| 段階 | 目標 | 状態 |
|---|---|---|
| 一、トリガー | posix-cpu-timers UAF を確実に発生させる | ✅ 実機で確認 |
| 二、UAF の可視化/制御 | 解放済みオブジェクトを我々が制御する(キャリアによるページ奪取 or 同一 slab の再利用) | ✅ ページ奪取は実機で成立(P10:victim ページが buddy に返却され、こちらが受け止めた);⚠ タイミング要修正:キャリアの着地は reap より前でなければならない |
| 三、権限昇格 | ゼロ書きで cred を改変 → uid 0 | 🔶 チェーンは構築済み、実機では未達成 |
de_thread() 後の exec 失敗 → exit_itimers() が k_itimer を解放するが rbtree ノードは摘除されない →
親プロセスの reap 時に timerqueue_del() が解放済みオブジェクトを操作する。実機の panic スタックがその証拠である:
pc : rb_next+0x24/0x54 lr : timerqueue_del+0x38/0x74
Call trace: rb_next / posix_cpu_timers_exit+0x78 / release_task+0xec /
wait_consider_task+0x65c / do_wait / kernel_wait4 / __arm64_sys_wait4
2 つの重要な実装ポイント(いずれもコード内に反映済み):
clone(CLONE_VM|SIGCHLD) の独立プロセスでなければならず、pthread は使えない ——
zap_other_threads() は同じ thread group しか殺さないため、pthread locker は de_thread() の冒頭で死に、
書き込みロックが解放される → exec が成功する → UAF は発生しない。pthread_create と locker スタックの mmap は、lockers が書き込みロックを獲得する前に完了していなければならない。