
HUAWEI MatePad Pro 11 GOT-W29 における CVE-2026-43499 (GhostLock) の研究
GOT-W29(HarmonyOS 4.0、カーネル 4.19.157-perf+)上の
CVE-2026-43499(rtmutex/futex-PI UAF、"GhostLock")に関する権限昇格の研究。
核心的な結論:
sysctl_bootid を書き換え、KASLR slide を導出)。| 項目 | 値 |
|---|---|
| 型番 | HUAWEI MatePad Pro 11 GOT-W29 |
| SoC | Qualcomm kona (SM8250, Snapdragon 870) |
| OS | HarmonyOS 4.0 (104.0.0.136) |
| カーネル | 4.19.157-perf+ |
| VA | 39-bit, 4K pages, KASLR on |
kernel/locking/rtmutex.c の remove_waiter() が
rt_mutex_start_proxy_lock() のロールバックパスで current ではなく waiter->task を使ってクリーンアップするため、ダングリング pi_blocked_on(スタック UAF)が発生する。影響範囲は 2.6.39 ~ 7.1(本カーネルは範囲内)。上流の修正 commit は 3bfdc63936dd。
本デバイスで確認済み:ソース rtmutex.c:1110-1112、boot.elf の逆コンパイル、実機トリガーのすべてを検証。
PI ループを作り FUTEX_CMP_REQUEUE_PI を -EDEADLK で返させ、ロールバックで remove_waiter のバグをトリガーし、ダングリング pi_blocked_on(waiter スレッドのカーネルスタック上の rt_waiter を指す)を残す。
旧トリガーは waiter が requeue 対象 futex を自己保持(self-own)させていたが、本カーネルの task_blocks_on_rt_mutex にある早期の owner==task チェック(boot.elf 0x3808-0x3868)にちょうど該当し、pi_blocked_on の書き込み前に戻る → ダングリングポインタは決して生成されない → overlay 配置は誤診だった(クラッシュなし + boot_id 不変)。
PI ループ:owner が FUTEX_LOCK_PI(target) で requeue 対象を保持し、waiter が chain futex を保持し、owner が chain にブロックする(ループ:waiter→target→owner→chain→waiter)。requeue 時にチェーンが rt_mutex_owner(chain)==top_task を検出 → -EDEADLK → ロールバックで誤ったスレッドの pi_blocked_on をクリア → waiter の pi_blocked_on がダングリングになる。owner は優先度を下げる(nice=10)必要があり、boost 後の prio が owner_waiter->prio と異なるようにする。そうしないと rt_mutex_waiter_equal が早期リターンする。
シェル(uid 2000)で perf_event_paranoid=-1 の場合、perf_event_open(PERF_SAMPLE_IP, exclude_user=1) でカーネルテキストアドレス群をサンプリングし、既知のシンボルオフセットに合わせて slide を導出する。
samples=27651 kernel_ips=1685 lo=0xffffff948728176c hi=0xffffff9488ebfc7c
KASLR slide=0x147f200000 (40/40 IP がカーネルテキスト領域にマップされることを検証)
runtime _stext=0xffffff9487280800
ツール: tools/perf_kaslr.c。実行前提:シェル(Shizuku rish)、seccomp による遮断なし。
[M] CMP_REQUEUE_PI ret=-1 errno=35 (EDEADLK!)
[W] WAIT_REQUEUE_PI ret=-1 errno=110 (ETIMEDOUT) ← waiter が戻る
[M] waiter_returned=1 ← ダングリング pi_blocked_on を残す
ツール: tools/edeadlk_probe.c(variant 8+2+1 = 11、または 27)。
rt_mutex_adjust_prio_chain の step[7] が fake waiter に対して rb_erase(単一左子パス)を実行する:*(tree_left) = tree_pc + __rb_change_child のインクリメンタル書き込み。target.h の全オフセットは boot.elf の逆アセンブルによる実測値。
exploit/ghostlock-source/src/target.h を参照。要点:
自作 KPM(tools/kpm-debug/rtmutex-dbg.c、KernelPatch 0.13.5 inline-hook)が fake walk 時に overlay(tree/task/lock)を再構築し、next_lock パラメータを empty_zero_page に書き換える(KernelPatch _transit8 は変更後の fargs で元の関数を呼ぶ)。これにより rt_mutex_adjust_prio_chain の [3] next_lock==waiter->lock が通過し、[5] trylock がゼロロックで成功し、[6] ownerless になり、[7] rt_mutex_dequeue(rb_erase 単一左子)が実行される——sysctl_bootid が &loggers[0][1] に書き換えられ、slide-kaslr-ok。
REPAIR3 waiter=0xffffff801ecdbc00 lock=0xffffffa7c4950000
slide boot_id_leaked_nfulnl_logger value=ffffffa7c4612320
slide-kaslr-ok base=ffffffa7c1280000 slide=00000027b9200000
スタックジオメトリは boot.elf のフレームサイズで実証済み:__arm64_sys_futex(0x70) + do_futex(0x60+0x1a0) 内の rt_waiter は sp+0xc0 → 深さ 0x1b0。pselect パスの stack_fds[0] は深さ 0x210 で、差は 0x60 = 12 words。したがって word_i は stack_fds[12+i] に位置する:words 0-2 は ex[2..4] 入力領域(直接制御可能)、words 6-7(task/lock)は res_in[3..4](in[3..4] + POLLIN-ready fd でエンコード)、words 3-5/8-10 は 0 のまま。pselect は ready fd により即座に戻る → waiter はユーザー空間でビジーウェイト(シグナル禁止、ゼロ syscall)し、consumer がトリガーを完了するまで待つ。
overlay の task/lock words は res_in[3]/[4](fd ready でエンコード)に位置する。実測では res_in[4](lock)が pselect の戻りパスで決定的に上書きされ(rt_sigreturn フレームの残骸)、payload の fake_lock と一致することは決してない。res_in[3](task)は時々無傷のまま。ユーザー空間での回避(FP 操作 + sched_yield)は do_notify_resume のトリガー率を ~21% に下げるだけだが、lock の上書きはほぼ確実。
関連するもう 2 つの事実:
rt_mutex_adjust_pi() に if (!owner) return 0; があり、fake_lock に owner がない場合 adjust_prio_chain は呼ばれない。popsicle(6.12)にはこのチェックがない。owner-ful payload は移植済み(fake_lock owner=fake_task|1)だが、lock ワードが必ず上書きされるため確実にはトリガーできない。boot.elf の逆アセンブル __arm64_sys_ppoll / do_sys_poll で確認:pollfd は 16 バイト構造体(fd 4B + events 4B + revents 4B + pad 4B)で、fake waiter の 64 ビット task/lock words を運べない——fd 値は制限され(実 fd でなければならない)、events は 4 バイトだけで連続せず、revents はカーネルが書き込む(制御不可)。
smt878u / popsicle は完全な権限昇格が可能:それらのスタックジオメトリでは words がユーザー制御可能な in/out/ex(pselect の 3 つの fd_set)に配置できる。GOT-W29 の waiter 位置(bits+0x60 → task/lock が res_in[3]/[4])にはそのウィンドウがない。これはカーネルスタックジオメトリの差異であり、実装の欠陥ではない。
アプリドメイン(untrusted_app)には既存の KASLR チャネルがない(perf/kallsyms/pagemap/dmesg すべて拒否)。CMP_REQUEUE_PI は 1(requeue 成功)を返し、EDEADLK ロールバックを経由しない。非 root cpuset の major_only はチェーンの step[6] をハードに短絡する。QOS を変更する唯一の入口 /dev/iaware_qos_ctrl は SELinux で拒否される。したがって通常のアプリ権限ではこの CVE をトリガーできない。
自作 KernelPatch モジュール(rtmutex-dbg)で実機の fake waiter と書き込みプリミティブを観測。LyraVoid/KernelPatch 0.13.5(FolkPatch と同源)のヘッダーに基づいてコンパイル。
cd <KernelPatch>/kpms/rtmutex-dbg
make TARGET_COMPILE=aarch64-linux-android- \
CC=$PREFIX/bin/aarch64-linux-android-clang \
LD=$PREFIX/bin/aarch64-linux-android-ld
出力は rtmutex_dbg.kpm。コンパイルフラグには必ず
-fno-pic -fno-pie -fno-asynchronous-unwind-tables -fno-unwind-tables
を含めること(Makefile に組み込み済み):clang のデフォルト PIC は GOT 再配置を生成し、デフォルトで .eh_frame(R_AARCH64_PREL32)を生成するが、KPM ローダーはどちらもサポートしない → ロード失敗 -1。
FolkPatch の superkey は su(APatch のデフォルト KernelPatch ではない)。sc_kpm_load ツール(ソース sc_kpm_load.c)を使用:
adb shell /data/local/tmp/sc_kpm_load su /sdcard/Download/rtmutex_dbg.kpm # ロード
adb shell /data/local/tmp/sc_kpm_load unload rtmutex-dbg su # アンロード
adb shell /data/local/tmp/sc_kpm_load ctl rtmutex-dbg counts su # カウンター
run_rtmdbg_test.sh(デバイス側 /data/local/tmp/ghostlock-test/):シェル身份(GOT_SLIDE_NO_RT=1 実パス)で GhostLock を実行し、0.5s の sync ループでログ損失を防ぎ、dmesg -w をディスクに保存し、90 秒で自動収束(SIGSTOP で soft-lock 再起動を防止)。テスト前:
adb shell 'su -c "sh /sdcard/ghostlock-test/set_debug_no_reboot.sh"' # 再起動防止
[RTMDBG])REPAIR3:書き込みプリミティブ検証時に overlay を再構築し next_lock パラメータを書き換えFAKEWALK skip:上書きされた fake walk がスキップされる(書き込みなし・クラッシュなし)prio_chain[N] / prio_chain_ret:walk 呼び出しと戻り値(0=完了;4294967261=-EDEADLK)do_select n=320:カーネル側の res_in[3]/[4]futex op=13/14:CMP_REQUEUE_PI / WAIT_REQUEUE_PIHuawei の oops/panic 完全スタックは /data/log/bbox/history.log に記録される。
tools/ 検証ツール(perf KASLR、EDEADLK プローブ、KPM ツールチェーン)
target/ 全実測オフセット
exploit/ 移植した slide.c(EDEADLK トリガー変更を含む)
| hook | 役割 |
|---|
rt_mutex_adjust_pi | PI 調整を記録。overlay を上書きするときは skip(pi_blocked_on をクリア) |
rt_mutex_adjust_prio_chain | fake walk を無条件 skip。waiter を完全ダンプ |
__arm64_sys_pselect6 / __arm64_sys_ppoll | 戻りパスで _TIF_WORK_MASK をクリア。fd_set を観測 |
do_select | カーネル側で res_in[3]/[4] を読み取り |
__arm64_sys_futex | WAIT_REQUEUE_PI / CMP_REQUEUE_PI を追跡 |
rt_mutex_dequeue | step[7] の書き込みプリミティブ実行と tree 形状を確認 |