vivo 6.x内核无法使用除magisk以外root方案的研究与解决办法

XingChenRS 发布于 2026-02-17 597 次阅读


awa

一、现象回顾与总体结论

1.1 现象回顾(从「卡死到 panic」开始)

  • 设备环境:MT6989,Android 14,GKI 内核 6.1.145,集成 KernelSU(KSU)+ SUSFS + vivo 自研安全模块。
  • 实测现象:
    • 仅刷入带 KSU 的 GKI 内核:开启 KSU 后,大约 十分之一概率 出现卡死并触发 kernel panic;
    • 在此基础上刷入模块(带 SUSFS 等):开机/刷入过程中,大约 二分之一概率 卡死到 panic。
  • 多次抓取 console-ramoops-0dmesg 后的观察:
    • 早期 panic 栈多落在 vr.ko 的任务遍历与 tracepoint 路径;
    • 后期 panic 栈中 vr 相关符号消失,转而落在 vivo_bsp_engine 模块的 hung_task 检测逻辑上(vkhungtaskd → check_hung_uninterruptible_tasks → panic)。

基于这些现象,可以基本排除「5 系那种遇到 uid==0 即击杀」的简单逻辑

  • 5 系更偏向于「通过安全钩子强扫进程,一旦发现 uid==0 或异常 cred 立即拒绝/击杀」;
  • 本机型的行为更接近「定时扫描 + 条件触发」:
    • KSU 开启后,会通过修改内核中的 task/cred 链表,隐藏 Root 进程与相关 path;
    • 某厂安全模块(包括 vr.kovivo_bsp_engine 等)后台线程从 init_task 出发,定期遍历这些链表或监控 cred/任务状态;
    • 当 KSU 正在摘除或修改某个节点时,安全模块若正好访问到“断开”或“半修改”的指针,有机会导致非法访问或长时间等待,被 hung_task 判定为卡死 → panic。

1.2 当前状态与高层结论

经过对 vr.kovivo_bsp_engine 的有针对性的 patch 后,当前状态如下:

  • KernelSU 已基本稳定,无需再走「先 Alpha 再 KSU」的绕路:
    • 只需要刷入带 KSU 的 GKI,或修补 init_boot 以集成 KSU,即可获得稳定的 KSU 支持;
    • 原先依赖 Alpha/Magisk 提供的「标签掩护」已不是必要条件。
  • 关于历史上的「先 Alpha 再 KSU 有效」现象,现在可以更合理地理解为:
    • 通过 Alpha/Magisk 路径安装时,系统属性或进程标签上残留了对厂方策略友好的标记,使得安全模块对这些进程更宽松;
    • 当前方案从内核模块(vr 与 hung_task)层面直接剥离/收缩对 KSU 不利的执行面,使得 KSU 可以直接作为主入口工作,无需再依赖这些标记。

下文分别从 vr.kovivo_bsp_engine 两个模块出发,给出:

  • 阻断逻辑的核心 patch(解除对 KSU 的权限阻断);
  • panic 逻辑的优化 patch(降低因 vr/hung_task 与 KSU/SUSFS 并发导致的 panic 风险);
  • 并附上关键代码特征与核心结论。

二、GKI vr.ko:阻断逻辑与 panic 逻辑的拆解与控制

2.1 模块职责抽象(代码级)

结合 IDA + MCP,GKI 版本 vr.ko 的主要职责可以抽象为三块:

  1. 进程/任务维度的「全局观察者」

    • 导入 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 栈中最常见的入口之一。
  2. 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;
    }
  3. 配置与状态机中心

    • 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.textinit_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」直接相关的核心逻辑拆成两块:

  1. 关闭周期回调总开关 byte_1C781

    • 在未修改版本中:
    // init_module 中的关键调用
    sub_AFD4();     // VA: 0xADB0(.init.text)
    ...
    • sub_AFD4 伪代码要点:
      • 解析 bootargs 中与 "=sendFastboot" 相关的配置项;
      • 若条件满足,则将 byte_1C781 置 1;
      • vr_run 周期线程中只有在 byte_1C781 == 1 时才执行特定回调链。
    • 当前 patch:
      • 0xADB0 处把 BL sub_AFD4 替换为 MOV W0,#0(小端字节 00 00 80 52);
      • 效果:byte_1C781 在整个生命周期内保持为 0,vr_run 中基于该开关的周期回调永远不被触发。
  2. 不注册检查链 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 路径:

    • 0xADACBL sub_AEA8MOV W0,#0(收缩内部列表/环境初始化);
    • 0xADB8BL sub_B08CMOV W0,#0(不再进行复杂表解析);
    • 0xADD0 / 0xADE4 / 0xADF8:三处 BL sub_C9D0 → 均改为 MOV W0,#0(不再执行多轮表驱动初始化);
    • 0xAE08BL kfreeMOV W0,#0(配合上方不再分配,避免释放风险)。
  • 关闭 vr_run 周期线程:

    • 0xAE14BL sub_B270MOV 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_B32Csub_16178 的任务链表遍历链路从控制流上整体失活。

  • 收缩 ops / tracepoint / 状态注册:

    • 0xAE30BL sub_BE8CMOV W0,#0(不注册 VR 内部 ops 表);
    • 0xAE34BL sub_BFB4MOV W0,#0(不在 initcall_finish tracepoint 上挂回调);
    • 0xAE3CBL sub_C0C4MOV 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_8E28sub_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 线程:UploadCodeProxyHeapTaskDaemonAsyncTask #1FileObserver 等;
  • 栈中普遍卡在: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,我们将 0x57200x5758 两条分支改写为无条件跳转到 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 侧):

  1. vivo_bsp_engine 的 hung_task 机制通过 watchdog → check_hung_uninterruptible_tasks → check_hung_task 三段逻辑,对所有 D 状态线程定期扫描,并按规则触发 panic。
  2. 当前 patch 将「扫描 + 日志 + 栈打印」能力完整保留,只是在 panic 前的条件跳转处改为跳回 epilogue,使 panic() 从控制流上不可达。
  3. 这样既避免了因 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 后续可选优化方向

  1. 针对 SUSFS + fsnotify 的交互进行进一步分析与收敛
    • 在当前不 panic 的前提下,结合阻塞栈信息评估 SUSFS 在 fsnotify 路径上的设计是否存在长时间持有 SRCU、不释放引用等问题,并尝试在 KSU/SUSFS 层面做「软解」而非仅依靠 hung_task 绕过。
  2. 在保证功能与可观测性的前提下,进一步精简 vr.ko
    • 当前 vr.ko 已基本退化为「有状态记录、无强制执行」的模块;
    • 若后续发现仍有残余影响,可在不破坏 debug / 状态输出的前提下,继续精简部分非必要 init 子路径。

整体而言,当前方案在「解除 KSU 权限阻断」「降低 vr/ hung_task 相关 panic 概率」「尽量保持其它功能与可观测性」三者之间达成了较为均衡的结果,可作为后续演进与回溯的稳定基线。

此作者没有提供个人介绍。
最后更新于 2026-02-18