
Android kernel LPE research for CVE-2026-98260 (posix-cpu-timers UAF): trigger PoC, fake-cred and real-cred uid-0 shell chains, plus a Shizuku/KernelSU bootstrap app.
English | 简体中文
Android 本地提权研究。漏洞位于 posix-cpu-timers 的退出路径:非 leader 线程
execve() 失败后,已排队的 timer 节点没有被摘除,父进程回收 zombie 时
cleanup_timerqueue() 会对已释放的 k_itimer 执行 rb_erase()。
仓库包含:仅触发的 PoC、fake-cred 验证型 EXP、真实 cred 的 uid-0 shell、 基于 Shizuku 的 KernelSU 提权 App,以及漏洞 pre-check。
分支
magicos-11.0.0.109-6.12.58: 当前分支是双 profile。APK 和 native 二进制在运行时按精确uname -r选择 profile:MagicOS 10.0.0 / 6.12.38 是已验证 profile,MagicOS 11.0.0.109 / 6.12.58 是静态适配、 设备端运行验证仍待完成。APK 同时打包两套 226 条ko-syms表,并按 内核版本选择。
| 项目 | 值 |
|---|---|
| CVE | CVE-2026-98260 |
| 类型 | LPE(use-after-free,timerqueue rb-tree erase) |
| 目标 | 荣耀 MagicPad3 Pro 13.3 YLP-W00,MagicOS 10.0.0 / 11.0.0.109(Android 16),分支 magicos-11.0.0.109-6.12.58 |
| 内核 | 6.12.38-android16-5-gfde7767f6ef6-abogki481467632-4k(已验证)6.12.58-android16-6-g3a7ac1674240-abogki541502914-4k(已适配) |
| profile 状态 | 双 profile;6.12.38 已验证,6.12.58 运行时验证待做 |
| SELinux | Enforcing |
| 补丁 | 测试构建上不存在 |
| 缓解措施 | KASLR 开,SMEP/SMAP/PAN 开,KASAN 编译但未启用 |
| Bootloader | 锁定且不可解锁;不写任何分区 |
| 组件 | 状态 | 结果 |
|---|---|---|
src/poc.c | 已在 6.12.38 验证 | 破坏性触发;reap 在 cleanup_timerqueue 中 panic |
src/exp.c | 已在 6.12.38 验证 | fake cred;getuid()==0 && geteuid()==0;不能 exec |
src/uid0.c | 已在 6.12.38 验证 | 真实 cred;uid-0 shell,u:r:shell:s0 |
app/ | 已在 6.12.38 验证 | Shizuku + KernelSU 引导;uid 0,u:r:ksu:s0 |
src/vuln_precheck.h | 已加入 | profile/环境门禁;--check / --force |
| 6.12.58 profile | 已适配 | 分配点、selinux_state 和对应 226 条 ko-syms 表已重提取;设备运行待做 |
| 双 profile APK | 已在 6.12.38 测试 | 按 uname -r 选择 profile 和 ko-syms;6.12.38 live-KSU 路径通过 |
当前 release: v1.4.3-app。
v1.4.2-app pre-release 已于 2026-10-30 撤下,其 tag 仅作历史保留。发布记录
与已知缺口见 docs/release-notes.md。
src/poc.c 仅触发;在测试内核上 reap 会 panic
src/exp.c fake-cred 链;用 getuid/geteuid 验证 uid 0
src/uid0.c 真实 cred 链;清零 cred->uid/gid 后 exec sh
src/kernel_profile.h 各 release 的分配点、符号和结构偏移
src/trace_ids.h 运行时 kmem trace event ID;无 root 探测
src/vuln_precheck.h 共享的 profile/环境 pre-check
app/ Android 提权 App(Shizuku + KernelSU 引导)
docs/analysis.md 根因、利用链与设备数据
scripts/dump_device.sh 设备端全量导出(全部 .ko/modinfo、kernel/config/policy、perf/tracefs、vendor mount、harness、活动分区 boot.img)
Makefile 唯一构建入口(NDK 交叉编译、App APK)
README.zh-CN.md 本文
非 leader 线程创建了 CLOCK_THREAD_CPUTIME_ID posix timer,随后在
de_thread() 之后 execve() 失败。打断 exec 的 SIGKILL 让线程走
exit_itimers(),但此时 task 已经在退出,timer 无法被 disarm。
k_itimer 被释放时,它的 timerqueue_node 仍留在该 task 的
posix_cputimers 队列里。父进程回收 zombie 时:
release_task()
-> posix_cpu_timers_exit()
-> cleanup_timerqueue()
-> timerqueue_del() / rb_erase(stale node)
rb_erase() 就是写原语:它把可控的 __rb_parent_color 写到可控的
rb_left 目标。利用链通过 slab/页回收编排,让已释放的 stale node 落在
被 payload 填满的页上,再选择目标地址和写入值。
更详细的 panic 栈、YLP-W00 偏移、tracefs 泄漏、SLUB/RCU 编排、两次 reap 的
fake-cred 链和一次 reap 的 real-cred 链见
docs/analysis.md。
依赖 Android NDK r29(/opt/android-ndk-r29,可用 NDK= 覆盖),目标默认
arm64/API 24。App 额外需要 Android SDK(build-tools 37.0.0、platform 37)、
JDK,以及 ~/.local/share/android-cve-env/cve98260-app 下的 Shizuku jars
(可用 APP_DEPS= 覆盖)。编译产物只写 build/,已被 git 忽略。Shizuku
jars 由用户自备;scripts/fetch-deps.sh 按 deps.lock 校验 SHA-256,不做
任何下载。
| 目标 | 输出 / 行为 |
|---|---|
make / make poc | build/poc |
make exp | build/exp |
make uid0 | build/uid0(静态;shell 阶段会 re-exec 自己) |
make build-all | 构建 poc + exp + uid0 + apk |
make host | build/poc.static,用于 host 侧 qemu 验证 |
make emu | 在 qemu 下运行 build/poc.static(带 --force) |
make apk | app/CVE98260Root.apk |
make clean | 删除 build/ |
make help | 列出目标 |
poc、exp、uid0 在接触内核之前都会先过 profile 门禁:
kernel_profile.h 中的精确 release(6.12.38 已验证,6.12.58
已静态适配);exp/uid0 还检查 tracefs/kmem 是否可用;exp 的 fake-cred 链还要求 panic_on_warn=0。其他 release 一律视为“未确认”,程序退出码为 2:
build/uid0 --check # 只打印结论,不触发漏洞
build/uid0 --check --force # 对未确认环境放行,只做检查
只有独立验证过内核后,才使用 --force 或 CVE98260_FORCE=1。--force
只放宽环境检查;exp、uid0 和 App 仍必须匹配 kernel_profile.h 中的
精确 profile。这个门禁故意不做“运行时探测”:任何真正走到 bug 路径的探测
都会留下 stale timerqueue node,而回收它就是破坏性步骤。
在 YLP-W00 上,普通 shell 读不到 /proc/sys/kernel/panic_on_warn 和
/proc/cmdline;对已知 profile,pre-check 在不可读时沿用 6.12.38 的已验证
值 0;6.12.58 在完成实测前也沿用该假设,未知 profile 仍拒绝。
利用程序以普通 shell 进程启动(uid 2000,SELinux u:r:shell:s0),不使用
su。不要通过 su 启动:测试前提就是非特权启动。
make uid0
adb push build/uid0 /data/local/tmp/uid0
adb shell chmod 755 /data/local/tmp/uid0
adb shell /data/local/tmp/uid0 # 打印 id 和 context
adb shell /data/local/tmp/uid0 shell # 交互式 sh -i
完整利用前先用非破坏性模式:
adb shell /data/local/tmp/uid0 --probe-ids # 探测 kmem trace event ID
adb shell /data/local/tmp/uid0 leak # 只做 timer/cred 泄漏,不触发 UAF
kmem trace event ID 在启动时分配,跨内核构建不稳定(6.12.38 是
318/319/322/324)。--probe-ids 通过可写的 rss_stat/trigger 一次只启用
一个事件,从 raw trace 反查 ID;正常运行也会自动探测。leak 会打印
[base]、完整的 timer_site/slide 和 cred 分配点,不会进入
run_plant()。
root 是静态链接的,子进程会通过 /proc/self/exe 重新执行自己来获得私有的
真实 cred,因此它必须留在 push 的路径上。
成功时输出以这些行结束:
[child] woke uid=0 euid=2000
[child] setuid(0)=0 errno=0 resuid=0/0/2000
uid=0(root) gid=0(root) groups=0(root),...
context=u:r:shell:s0
ROOT_SHELL_OK
如果 adb 会话可能中断,请用 detached 方式启动。parked 进程绝不能被 kill(见“限制”):
adb shell 'setsid /data/local/tmp/uid0 >/data/local/tmp/uid0.log 2>&1 &'
任何未验证状态(victim slab 未释放、目标页被其他分配抢走、reap 状态不符)
都会选择 park 而不回收:对 stale node 的不可控回收会 panic 内核。launcher
现在由 lifecycle.h 放进独立 session、忽略 SIGPIPE,并有一个
PR_SET_CHILD_SUBREAPER keeper;关闭管道/adbd 组杀不得杀死 zombie owner。
若出现 PARKED=1,不要把重启当作干净清理:关机路径仍可能回收僵尸并 panic。
app/ 是一个 Android UI,通过普通 Shizuku shell 在 YLP-W00 上引导
KernelSU。它复用同一套利用核心作为 native 库,src/poc.c 和 src/exp.c
保持不动。界面只保留必要操作:授权 Shizuku、部署资产、开始提权、清理临时
文件、导出/复制当前日志。
make apk # -> app/CVE98260Root.apk
adb install -r app/CVE98260Root.apk
典型流程:
libmagicroot.so 部署到
/data/local/tmp/cveapp;kernelsu
已经加载,则只检查 su -c id。开始提权会清空上一次“授权/部署”在界面留下的日志,远程 run.sh 的 stdout
直接流式显示;native worker 另外追加
/data/local/tmp/cve98260-worker-<pid>.log,管道关闭也不会丢失记录。
导出日志会覆盖 /sdcard/MagicRoot.log。任务运行期间前四个按钮禁用,
任务结束后恢复。
这个 App 是 YLP-W00 专用的:它依赖管理器里的 KMI 模块、随包的
ko-syms-6.12.38.txt / ko-syms-6.12.58.txt 和所选 profile 的
selinux_state 地址,不是通用一键 root。App 的 native 库已在进入链路前
调用 src 里的共享 pre-check;live 分支先执行 magicroot --check。
src/uid0.c 保留 exp.c 的触发、泄漏和页回收,但不再伪造 cred。子进程
exec 本二进制,prepare_exec_creds() 会通过 cred_jar 分配一个私有的真实
cred R(trace 特征:type 318、call-site low21 在 6.12.38 为 0x18a248、
在 6.12.58 为 0x19c474,bytes_req 0xb8、bytes_alloc 0xc0)。子进程阻塞在 pipe 上,父进程用只含零写的
payload reap stale node:
[R + 8] = 0 # cred->uid / cred->gid
cred->uid 变成 0 后,子进程调用 setuid(0):kuid == old->uid,不需要
CAP_SETUID,内核把 euid/fsuid 置 0。此时 cred == real_cred 仍成立,
SELinux blob 是 exec 时分配的真实 blob,因此
execve("/system/bin/sh") 可以通过 commit_creds(),最终运行在
u:r:shell:s0。
exec 的 capability fixup 只按 bounding set 重算(本机是 CAP_SETGID、
CAP_SETUID、CAP_SYS_NICE),所以 shell 没有 CAP_DAC_OVERRIDE。
这是真正的 uid-0 shell,但不是不受限制的系统 root,SELinux 仍把它限制在
shell 域。
make run 执行的是破坏性 PoC。在 YLP-W00 上它会在 6 秒后 reap,预期 panic;
QCOM watchdog 大约 40–50 秒后重启,临时 root(如果有)随之丢失。
干净启动(没有 kernelsu 模块)时,点 开始提权 会依次执行: