awa
一、现象回顾与总体结论
1.1 现象回顾(从「卡死到 panic」开始)
- 设备环境:MT6989,Android 14,GKI 内核 6.1.145,集成 KernelSU(KSU)+ SUSFS + vivo 自研安全模块。
- 实测现象:
- 仅刷入带 KSU 的 GKI 内核:开启 KSU 后,大约 十分之一概率 出现卡死并触发 kernel panic;
- 在此基础上刷入模块(带 SUSFS 等):开机/刷入过程中,大约 二分之一概率 卡死到 panic。
- 多次抓取
console-ramoops-0与dmesg后的观察:- 早期 panic 栈多落在
vr.ko的任务遍历与 tracepoint 路径; - 后期 panic 栈中 vr 相关符号消失,转而落在
vivo_bsp_engine模块的 hung_task 检测逻辑上(vkhungtaskd → check_hung_uninterruptible_tasks → panic)。
- 早期 panic 栈多落在
基于这些现象,可以基本排除「5 系那种遇到 uid==0 即击杀」的简单逻辑:
- 5 系更偏向于「通过安全钩子强扫进程,一旦发现
uid==0或异常 cred 立即拒绝/击杀」; - 本机型的行为更接近「定时扫描 + 条件触发」:
- KSU 开启后,会通过修改内核中的 task/cred 链表,隐藏 Root 进程与相关 path;
- 某厂安全模块(包括
vr.ko与vivo_bsp_engine等)后台线程从init_task出发,定期遍历这些链表或监控 cred/任务状态; - 当 KSU 正在摘除或修改某个节点时,安全模块若正好访问到“断开”或“半修改”的指针,有机会导致非法访问或长时间等待,被 hung_task 判定为卡死 → panic。
1.2 当前状态与高层结论
经过对 vr.ko 与 vivo_bsp_engine 的有针对性的 patch 后,当前状态如下:
- KernelSU 已基本稳定,无需再走「先 Alpha 再 KSU」的绕路:
- 只需要刷入带 KSU 的 GKI,或修补
init_boot以集成 KSU,即可获得稳定的 KSU 支持; - 原先依赖 Alpha/Magisk 提供的「标签掩护」已不是必要条件。
- 只需要刷入带 KSU 的 GKI,或修补
- 关于历史上的「先 Alpha 再 KSU 有效」现象,现在可以更合理地理解为:
- 通过 Alpha/Magisk 路径安装时,系统属性或进程标签上残留了对厂方策略友好的标记,使得安全模块对这些进程更宽松;
- 当前方案从内核模块(vr 与 hung_task)层面直接剥离/收缩对 KSU 不利的执行面,使得 KSU 可以直接作为主入口工作,无需再依赖这些标记。
下文分别从 vr.ko 与 vivo_bsp_engine 两个模块出发,给出:
- 阻断逻辑的核心 patch(解除对 KSU 的权限阻断);
- panic 逻辑的优化 patch(降低因 vr/hung_task 与 KSU/SUSFS 并发导致的 panic 风险);
- 并附上关键代码特征与核心结论。
二、GKI vr.ko:阻断逻辑与 panic 逻辑的拆解与控制
2.1 模块职责抽象(代码级)
结合 IDA + MCP,GKI 版本 vr.ko 的主要职责可以抽象为三块:
-
进程/任务维度的「全局观察者」
- 导入
init_task/tasklist_lock; - 通过
sub_16178对任务链表做遍历(典型伪代码):
// sub_16178 伪代码(节选) __int64 sub_16178() { void (*cb)(void *ctx, void *task); void *ctx; cb = off_1A8C8[0]; // 回调函数指针 ctx = qword_1A8D0; // 回调上下文 raw_read_lock(&tasklist_lock); for (p = init_task->tasks.next; p != &init_task->tasks; p = p->next) { task = container_of(p, task_struct, tasks); list = task->some_list; // 任务内部链表 list_for_each(t, list) { if (!(t->flags & 4) && t->field_41) cb(ctx, task, ...); } } raw_read_unlock(&tasklist_lock); return 0; }- 上述遍历通过
vr_run周期线程间接触发,是早期 panic 栈中最常见的入口之一。
- 导入
-
cred/权限变化维度的「策略执行者」
- 通过 tracepoint / rvh(例如
android_rvh_commit_creds)挂接到 cred/path 相关的内核事件; sub_B32C作为多阶段流水线,最终调用sub_DC10完成 commit_creds 相关钩子注册:
// B32C 阶段流水线末尾 __int64 sub_B32C() { ... v0 = sub_D58C(); v0 = sub_D67C(); v0 = sub_D708(); v0 = sub_D7DC(); v0 = sub_D9A8(); v0 = sub_DB24(...); v0 = sub_DBCC(); ... v0 = sub_DC10(); // commit_creds & 其它 rvh 注册入口 ... } // DC10 内部(节选) __int64 sub_DC10() { v0 = sub_134B0(); if (!v0) sub_135B8(); // 注册 android_rvh_commit_creds 钩子 v1 = android_rvh_probe_register(&_tracepoint_android_rvh_commit_creds, sub_72F0, 0); if (v1) { v2 = v1; } else { v2 = sub_137DC(); if (!v2) return v2; sub_15C1C(); // commit_creds 系列 tracepoint 安装 } if (!sub_13854(0)) sub_13964(); // 补充 tracepoint 注册 return v2; } - 通过 tracepoint / rvh(例如
-
配置与状态机中心
init_module内的sub_AEA8 / sub_B08C / 多个 sub_C9D0 / sub_BE8C / sub_BFB4 / sub_C0C4负责装载各类表、注册 ops 与 tracepoint、设置状态字节;sub_CF2C与一组全局变量(dword_1AC40/1AC44/qword_1AC48/1AC50等)共同维护 vr 的「版本 / 运行状态 / fatal 信息」,并通过 proc/seq_file 对外暴露:
// sub_16A70(状态输出)伪代码 __int64 sub_16A70(struct seq_file *m) { seq_printf(m, "ver:%unsz:%unvr_ver:%llxn" "ld_init:%dnld_retry:%dnld_done:%dnfatal:%dn" "fss_init:%dnfss_retry:%dnfss_done:%dn" "ld_stage:%dnld_res:%llxn" "skip:%llxnskip_state:%dn" "callsite:%snfatal_rel:%snfatal_ver:%sn" "fatal_prod:%snfatal_time:%sn" "ld_stages_rerunable:%llxn", dword_1AC44, qword_1AC50, qword_1AC48, ...); ... }
在上述结构下,阻断 KSU/root 与造成 panic 的逻辑是可以拆开的:
- 阻断主要依赖:
byte_1C781的置位 + 检查链注册; - panic 风险主要来自:
vr_run周期遍历任务链表 + 多阶段流水线与 tracepoint 的并发执行。
2.2 阻断核心 patch:解除对 KSU 的权限阻断
目标:在 GKI vr.ko 上解除对 KernelSU/root 的阻断逻辑,且不破坏其余 init/状态机。
分析对象:originvr/6.1-gki/vr.ko,.init.text 内 init_module@0xAD80。关键伪代码(IDA 反编节选)如下:
__int64 init_module()
{
_QWORD v8[3];
...
sub_AEA8(); // 列表/环境 init
sub_AFD4(); // 解析 bootargs,可能置位 byte_1C781
if (!sub_B08C(v8)) {
sub_C9D0(v8, &off_1B1C0, 11);
sub_C9D0(v8, &off_1C310, 23);
sub_C9D0(v8, &off_1C538, 1);
v8[0] = 0;
if (v8[1])
kfree();
}
LOBYTE(v8[0]) = 0;
v0 = sub_B270(v8); // 创建 vr_run 线程(见 2.3)
if (!(v0 | v8[0] & 1)) {
v1 = sub_B4E4(); // 注册检查链 off_1C7F0 / sub_ACD8 / sub_17508
v2 = sub_BE8C();
v3 = sub_BFB4(v2);
sub_C0C4();
complete(&unk_1C898);
...
}
return 0;
}
围绕这一流程,可以把与「阻断 KSU/root」直接相关的核心逻辑拆成两块:
-
关闭周期回调总开关
byte_1C781- 在未修改版本中:
// init_module 中的关键调用 sub_AFD4(); // VA: 0xADB0(.init.text) ...sub_AFD4伪代码要点:- 解析 bootargs 中与
"=sendFastboot"相关的配置项; - 若条件满足,则将
byte_1C781置 1; vr_run周期线程中只有在byte_1C781 == 1时才执行特定回调链。
- 解析 bootargs 中与
- 当前 patch:
- 在
0xADB0处把BL sub_AFD4替换为MOV W0,#0(小端字节00 00 80 52); - 效果:
byte_1C781在整个生命周期内保持为 0,vr_run中基于该开关的周期回调永远不被触发。
- 在
-
不注册检查链
off_1C7F0 / sub_ACD8 / sub_17508- 在未修改版本中:
// init_module 末尾 v1 = sub_B4E4(); // VA: 0xAE28 ...sub_B4E4伪代码要点:- 设置
off_1C7F0 = sub_17508; - 把
sub_ACD8写入[qword_1A5B0 + 0x18]; - 后续多个路径通过
sub_ACD8间接调用sub_17508,完成一整套「表驱动检查与分发」。
- 设置
- 当前 patch:
- 在
0xAE28处把BL sub_B4E4替换为MOV W0,#0; - 效果:检查链不被注册,依赖该分发器的策略逻辑缺少执行入口。
- 在
2.3 panic 优化 patch:收缩 init 与 vr_run 线程
目标:在解除权限阻断之后,进一步减少 vr.ko 与任务链表 / tracepoint / ops 的并发行为,降低因并发访问导致的 panic 风险。
从前述 init_module 伪代码出发,在 .init.text 内针对以下调用做收缩(全部改为 MOV W0,#0):
-
收缩复杂 init 路径:
0xADAC:BL sub_AEA8→MOV W0,#0(收缩内部列表/环境初始化);0xADB8:BL sub_B08C→MOV W0,#0(不再进行复杂表解析);0xADD0 / 0xADE4 / 0xADF8:三处BL sub_C9D0→ 均改为MOV W0,#0(不再执行多轮表驱动初始化);0xAE08:BL kfree→MOV W0,#0(配合上方不再分配,避免释放风险)。
-
关闭
vr_run周期线程:0xAE14:BL sub_B270→MOV W0,#0
sub_B270的关键伪代码为:__int64 sub_B270(_BYTE *a1) { if (a1) *a1 = 0; if (sub_CC0C() & 1) { qword_1A608 = (long)sub_B32C; // 周期回调 qword_1A610 = 0; dword_1A618 = 60000; // 周期 60s strcpy(name, "vr_run"); task = kthread_create_on_node(sub_8E28, &qword_1A608, ..., name); if (!IS_ERR(task)) { wake_up_process(task); qword_1C760 = task; return 0; } ... } else { byte_1A689 = 1; return -517; } }patch 后:不再创建名为
vr_run的线程,sub_8E28不再被运行,sub_B32C与sub_16178的任务链表遍历链路从控制流上整体失活。 -
收缩 ops / tracepoint / 状态注册:
0xAE30:BL sub_BE8C→MOV W0,#0(不注册 VR 内部 ops 表);0xAE34:BL sub_BFB4→MOV W0,#0(不在initcall_finishtracepoint 上挂回调);0xAE3C:BL sub_C0C4→MOV W0,#0(不更新dword_1C77C/dword_1CB14等全局状态)。
panic 侧的效果总结:
- 周期性
vr_run线程不再存在,避免与 KSU 改链表时在任务遍历上出现并发问题; - 与任务链表/tracepoint/ops 相关的大部分 init/注册逻辑被收缩,减少了与其他模块(如 SUSFS、KernelSU)之间的复杂交互;
- 权限阻断依赖的两块(
byte_1C781+ 检查链)由上一小节的「阻断核心 patch」精确切断。
三、vr.ko 与 KernelSU / SUSFS 的关系
3.1 vr.ko 自身的 commit_creds / rvh 路径
如上所示,GKI 版 vr.ko 在未修改时,通过 sub_B32C → sub_DC10 → sub_15C1C/1607C/162E8/16638 完成自身对 android_rvh_commit_creds 及其它 tracepoint 的挂钩;在当前 patch 下:
- 由于
sub_B270不再被调用,vr_run线程不创建,sub_8E28与sub_B32C不再执行; - MCP 交叉引用也显示
sub_DC10仅被sub_B32C调用,无其他入口; - 因此,vr.ko 自身对 commit_creds / rvh 的挂钩逻辑在当前组合下从控制流上已不可达。
3.2 与 KernelSU / SUSFS 的分工边界
- KSU(GKI 模式)自身在 cred/路径级别会安装一套独立钩子,用来实现 root 管理与隐藏;
- SUSFS4KSU 在 VFS / fsnotify / sdcard 方向插入
susfs_handle_sdcard_inode_event一类回调,对外隐藏 su/KSU 的痕迹; - 当前通过收缩 vr.ko,我们将「三方交汇点」减少到:
- KSU 与 SUSFS 之间:主要在 fsnotify / VFS / cred 侧交互;
vivo_bsp_engine与上述两者:主要通过 hung_task 检测看到 D 状态线程。
这为后续进一步在 SUSFS 或 KSU 挂钩策略上做精细化调整,提供了更干净的内核侧环境。
四、vivo_bsp_engine hung_task 机制与绕过方案
4.1 模块职责与 panic 触发路径
多次 console-ramoops-0 显示,当前大部分残余 panic 来自 vivo_bsp_engine 模块内的 hung_task 检测,而非 vr.ko。核心函数调用关系为:
// 周期线程:watchdog
void __noreturn watchdog()
{
unsigned long last = jiffies;
set_user_nice(current, -10);
for (;;) {
if (hung_timeout_jiffies(last, vhung_task_timeout_secs) > 0) {
schedule_timeout_interruptible(...);
} else {
unsigned int old = xchg(&reset_hung_task, 0);
if (!old)
check_hung_uninterruptible_tasks();
last = jiffies;
}
}
}
// 扫描所有 D 状态任务的主逻辑
__int64 check_hung_uninterruptible_tasks()
{
int budget = sysctl_hung_task_check_count;
unsigned long now = jiffies;
printk("hung_task: check_hung_uninterruptible_tasks\n");
if (did_panic & 1)
return;
g_hung_task_culprit = 0;
hung_task_call_panic = 0;
hung_task_call_panic_critical = 0;
rcu_read_lock();
for (task = init_task; // 通过 init_task 遍历系统所有任务
task != &init_task;
task = next_task) {
// 访问 per-task 内部链表
for (t = task->some_list; t != head; t = t->next) {
if (!budget)
goto out;
// 自身的超时预算,避免扫描过久
if (now - jiffies + 25 < 0) {
if (!rcu_lock_break(task, t))
goto out;
now = jiffies;
}
// 只关注不可中断睡眠(TASK_UNINTERRUPTIBLE)线程
if (t->state == 2)
check_hung_task(t_task_struct);
budget--;
}
}
out:
rcu_read_unlock();
// 后续根据 hung_task_call_panic / hung_task_call_panic_critical 决定是否 panic
}
check_hung_task() 针对单个 D 状态线程做进一步判断与日志输出:
unsigned __int64 __fastcall check_hung_task(task_struct *t)
{
u64 last_switch = *(u64 *)(t + 1344); // 上次调度时间
u64 now = sched_clock();
// 忽略打了特殊标志的线程,或未达超时时间的线程
if ((t->flags & 0x8000) != 0)
return now;
if (now/1e9 < vhung_task_timeout_secs + last_switch/1e9)
return now;
// 读取优先级/属性后过滤特殊场景
rcu_read_lock();
u32 prio = *(u32 *)(*(u64 *)(t + 2096) + 4);
rcu_read_unlock();
if (prio >> 4 >= 0x271)
return printk("hung_task: skip special prio...\n");
if (task_in_vivo_white_list(t))
return printk("hung_task: task in white list, ignore\n");
// 走到这里认为是「可疑阻塞任务」
hung_task_call_panic = 1;
printk("hung_task: blocked task ...\n");
sched_show_task(t);
if (task_in_vivo_critical_list(t)) {
printk("hung_task: critical task blocked ...\n");
hung_task_call_panic_critical = 1;
}
if (!g_hung_task_culprit ||
g_hung_task_culprit->last_switch_time != last_switch)
g_hung_task_culprit = t;
return now;
}
多次 panic 日志显示,被判为「blocked」的任务包括:
kworker/u16:*:fsnotify_mark_destroy_workfn/fsnotify_connector_destroy_workfn;- 多个 Java 线程:
UploadCodeProxy、HeapTaskDaemon、AsyncTask #1、FileObserver等; - 栈中普遍卡在:
fsnotify_destroy_group → __synchronize_srcu → wait_for_completion等路径,其中一条包含susfs_handle_sdcard_inode_event。
上述说明:
- SUSFS 在 fsnotify/sdcard 事件上安装的钩子,使得部分 inotify/fsnotify 销毁路径长时间处于 D 状态;
vivo_bsp_engine的 hung_task 机制把这类「长时间 D 状态」一视同仁地认为是系统卡死,并直接调用 panic。
4.2 panic 触发分支与 patch 点
在 check_hung_uninterruptible_tasks 的尾部,实际的 panic 决策伪代码为:
if (hung_task_call_panic == 1) {
if (enable_state == 2 || download_mode) {
if (!download_mode)
show_dsleep_task_stack(...);
if (hung_task_during_test != 1)
panic("hung_task: blocked tasks");
hung_task_test_panic = 1;
_warn_printk("hung_task: blocked tasks");
BRK #0x800;
} else if (enable_state == 1 && hung_task_call_panic_critical == 1) {
show_dsleep_task_stack(...);
if (hung_task_during_test != 1)
panic("hung_task: blocked critical tasks");
hung_task_test_panic = 1;
_warn_printk("hung_task: blocked critical tasks");
BRK #0x800;
}
}
原始实现中,只要有任何 D 状态线程触发了 check_hung_task() 中的条件,就会将 hung_task_call_panic 置 1,在合适的 enable_state / download_mode 配置下直接 panic。
4.3 hung_task 绕过 patch:保留检测与日志,切断 panic
目标:不再因为 fsnotify/SUSFS 上的 D 任务触发 panic 重启,但保留 hung_task 对阻塞线程的检测与日志能力。
我们的策略是:
- 不修改
watchdog线程与check_hung_uninterruptible_tasks的遍历与日志输出; - 不修改
check_hung_task对白名单/关键任务的识别与sched_show_task打印; - 仅在 panic 分支前的条件跳转处插入无条件跳转到函数 epilogue,从控制流上让
panic()不可达。
反汇编中,关键分支大致形如(地址为示例):
5714 ADRP X8, hung_task_during_test
5718 LDRB W8, [X8,#...]
571C CMP W8, #1
5720 B.NE loc_57A4 ; W8 != 1 → 跳往 panic("blocked critical tasks") 分支
...
5754 LDRB W8, [hung_task_during_test]
5758 B.NE loc_5798 ; W8 != 1 → 跳往 panic("blocked tasks") 分支
...
5798 ADRL X0, "hung_task: blocked tasks"
57A0 BL panic
57A4 ADRL X0, "hung_task: blocked critical tasks"
57AC BL panic
通过 IDA Python,我们将 0x5720 与 0x5758 两条分支改写为无条件跳转到 epilogue(0x5778):
0x5720:由B.NE loc_57A4改为B 0x5778;0x5758:由B.NE loc_5798改为B 0x5778。
修改后:
- 上层逻辑仍会设置
hung_task_call_panic/hung_task_call_panic_critical,并调用show_dsleep_task_stack、_warn_printk等函数输出详细信息; - 但执行流不会再落入
0x5798/0x57A4所在的BL panic分支,从而不会因为这些 D 状态线程重启整机。
核心结论(hung_task 侧):
vivo_bsp_engine的 hung_task 机制通过watchdog → check_hung_uninterruptible_tasks → check_hung_task三段逻辑,对所有 D 状态线程定期扫描,并按规则触发 panic。- 当前 patch 将「扫描 + 日志 + 栈打印」能力完整保留,只是在 panic 前的条件跳转处改为跳回 epilogue,使
panic()从控制流上不可达。 - 这样既避免了因 SUSFS/fsnotify 组合导致的大量 D 线程而整机重启,又保留了定位问题所需的栈与任务信息。
五、综合效果与后续方向
5.1 综合效果(简要)
在当前 GKI vr.ko + vivo_bsp_engine patch 组合下:
- KSU 可以直接稳定运行,无须再通过“先 Alpha/Magisk 再转 KSU”路径;
vr.ko仍保留配置与状态机功能,但不再:- 置位
byte_1C781; - 注册基于
off_1C7F0/sub_17508/sub_ACD8的检查链; - 创建
vr_run周期线程及其任务遍历/tracepoint 流水线;
- 置位
vivo_bsp_engine的 hung_task 机制仍会检测并打印阻塞线程,但不再因为 SUSFS/fsnotify 等导致的大量 D 状态线程直接 panic。
5.2 后续可选优化方向
- 针对 SUSFS + fsnotify 的交互进行进一步分析与收敛
- 在当前不 panic 的前提下,结合阻塞栈信息评估 SUSFS 在 fsnotify 路径上的设计是否存在长时间持有 SRCU、不释放引用等问题,并尝试在 KSU/SUSFS 层面做「软解」而非仅依靠 hung_task 绕过。
- 在保证功能与可观测性的前提下,进一步精简 vr.ko
- 当前 vr.ko 已基本退化为「有状态记录、无强制执行」的模块;
- 若后续发现仍有残余影响,可在不破坏 debug / 状态输出的前提下,继续精简部分非必要 init 子路径。
整体而言,当前方案在「解除 KSU 权限阻断」「降低 vr/ hung_task 相关 panic 概率」「尽量保持其它功能与可观测性」三者之间达成了较为均衡的结果,可作为后续演进与回溯的稳定基线。
Comments 1 条评论
这么强?!