溺的文档
ReZygisk · 第 1 篇 / 共 4 篇

ReZygisk 守护进程 zygiskd 与 Root 适配

2026-06-30 · 阅读 6

本文是 ReZygisk 源码解读系列第 1 篇。全系列:

  • 00 总览与整体架构
  • 01 守护进程 zygiskd 与 Root 适配(本篇)
  • 02 注入机制:monitor 监控与 ptracer 注入
  • 03 注入体 injector 与反检测基石
  • 04 外围:IPC 协议、模块脚本、WebUI 与构建

代码引用格式为 文件:行号,基于撰写时的仓库状态(main 分支)。


一、本篇定位

zygiskd(ReZygiskd)是 ReZygisk 的用户态大脑。如果说 monitor/ptracer(见 02 篇)负责"把代码塞进 zygote",那么 zygiskd 负责回答塞进去之后的一切问题:

  • 系统当前装的是哪种 root(Magisk / KernelSU / APatch)?
  • 有哪些 Zygisk 模块、它们的 .so 在哪、哪些被禁用了?
  • 某个 APP 进程(按 UID / 进程名)是否被授予 root、是否在 DenyList、是不是管理器?
  • 模块的 companion(伴生守护进程)如何按需拉起、如何把请求转发给它?
  • 对需要"隐藏 root"的进程,如何给它一个干净的 mount namespace(卸载掉所有 root 挂载痕迹)?

它以 32 位 / 64 位两个独立二进制zygiskd32 / zygiskd64)常驻运行,各自监听一个 Unix socket,服务对应位宽的被注入进程。

本篇覆盖两大块代码:

目录 内容
zygiskd/src/main.c / zygiskd.c / companion.c / utils.c / constants.h 守护进程主体
zygiskd/src/root_impl/common.* / kernelsu.* / magisk.* / apatch.* Root 方案适配层

二、zygiskd 在系统中的位置

先建立全局观。ReZygisk 的运行时拓扑如下:

flowchart TB subgraph init["init (pid 1)"] direction TB I["被 monitor ptrace 追踪"] end subgraph monitor["zygisk-ptrace monitor"] M["epoll + signalfd 事件循环<br/>捕获 zygote exec"] end subgraph daemon["zygiskd 守护进程 (32/64)"] D1["模块管理 Context"] D2["daemon socket<br/>cp32.sock / cp64.sock"] D3["root_impl 适配层"] D4["companion 进程池"] D5["mount namespace 缓存"] end subgraph zygote["Zygote / app_process"] Z["libzygisk.so (注入体)"] end subgraph apps["各 APP / system_server 进程"] A["模块代码 + Zygisk API"] end M -->|"fork+exec<br/>zygisk-ptrace trace pid"| zygote Z -->|"DaemonSocketAction<br/>(Unix socket RPC)"| D2 Z -.->|"ZygoteInjected 回流"| monitor monitor -.->|"controller socket<br/>init_monitor"| daemon D4 -->|"companion socket"| A zygote -->|"fork+specialize"| apps D3 -.->|"prctl/ioctl/sqlite"| init

几条关键关系:

  • monitor → zygiskd:monitor 在捕获到 zygote 后,通过 ensure_daemon_created()fork+execl 拉起 zygiskd32/64(见 02 篇)。zygiskd 反过来通过 init_monitor 这个 datagram socket 把"root 实现 / 模块清单 / 错误信息"上报给 monitor,用于刷新模块描述与 state.json
  • 注入体 → zygiskd:被注入进 zygote 的 libzygisk.so 作为客户端,通过 cp32/64.sock 发起 DaemonSocketAction 请求(协议客户端在 loader/src/common/daemon.c,服务端在本篇的 zygiskd.c)。
  • zygiskd → 模块 companion:每个模块可有一个 companion 进程,由 zygiskd 用双重 fork 拉起,APP 侧通过 zygiskd 中转拿到与 companion 直连的 socket。

三、进程模型与启动入口(main.c)

zygiskd 是一个多功能二进制,靠子命令区分角色(main.c:10-63):

int main(int argc, char *argv[]) {
  LOGI("Welcome to ReZygiskd%s", LP_SELECT("32", "64"));

  if (argc > 1) {
    if (strcmp(argv[1], "companion") == 0) { /* companion <fd> */ ... companion_entry(fd); }
    else if (strcmp(argv[1], "version") == 0) { /* 打印版本 */ }
    else if (strcmp(argv[1], "root") == 0) { /* 探测并打印 root 实现 */ }
    else { /* usage */ }
    ...
  }

  /* 无参数 = 守护进程主模式 */
  if (switch_mount_namespace(1) == false) { ... return 1; }
  root_impls_setup();
  zygiskd_start(argv);

  return 0;
}

四种角色:

调用 作用
zygiskd(无参) 守护进程主模式:切到 init 的 mount namespace → 探测 root → 进入主循环
zygiskd companion <fd> 作为某模块的 companion 子进程运行(见第六节)
zygiskd version 打印 ZKSU_VERSION
zygiskd root 一次性探测并打印当前 root 实现(调试用)

为什么主模式第一步是 switch_mount_namespace(1)

main.c:54 在进入主循环前,先把自己切进 pid 1(init)的 mount namespace

// utils.c:26-48
bool switch_mount_namespace(pid_t pid) {
  char path[PATH_MAX];
  snprintf(path, sizeof(path), "/proc/%d/ns/mnt", pid);
  int nsfd = open(path, O_RDONLY | O_CLOEXEC);
  ...
  if (setns(nsfd, CLONE_NEWNS) == -1) { ... return false; }
  close(nsfd);
  return true;
}

root 方案(尤其 Magisk)会给自己和模块创建独立的 mount namespace。zygiskd 要做的事——枚举挂载、为目标进程卸载 root 挂载——必须站在 init 这个"全局视角"的 namespace 里,才能看到完整、未被局部 namespace 遮蔽的挂载树。这是后面 umount_root / save_mns_fd 能正确工作的前提。


四、模块的发现与加载

主循环开始前,zygiskd_start 会先 load_modules 扫描模块目录(zygiskd.c:50-123)。

模块数据结构

// zygiskd.c:18-27
struct Module {
  char *name;     // 模块目录名(如 "zygisk-lsposed")
  int lib_fd;     // 已打开的 zygisk/<arch>.so 文件描述符
  int companion;  // 与该模块 companion 的连接 fd(-1 表示未启动)
};

struct Context {
  struct Module *modules;
  size_t len;
};

扫描规则

// zygiskd.c:64-72(节选)
while ((entry = readdir(dir)) != NULL) {
  if (entry->d_type != DT_DIR) continue;
  if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0
      || strcmp(entry->d_name, "rezygisk") == 0) continue;

  char so_path[PATH_MAX];
  snprintf(so_path, PATH_MAX, "/data/adb/modules/%s/zygisk/" ARCH_STR ".so", name);
  if (access(so_path, R_OK) == -1) continue;       // 没有本架构的 .so → 跳过
  ...
  snprintf(disabled, PATH_MAX, "/data/adb/modules/%s/disable", name);
  if (access(disabled, F_OK) == 0) continue;        // 被禁用 → 跳过
  ...
}

要点:

  • 遍历 /data/adb/modules/ 下所有目录;
  • 必须存在 zygisk/<ARCH>.soARCH_STR 由编译期架构宏决定,zygiskd.c:36-47arm64-v8a / armeabi-v7a / x86_64 / x86);这天然实现了"32 位 daemon 只加载 32 位模块库";
  • 跳过自身 rezygisk 目录与带 disable 标记的模块;
  • 对每个有效模块 open.so 拿到 lib_fdO_RDONLY | O_CLOEXEC),companion 初始化为 -1

注意 zygiskd 此时并不 dlopen 模块——它只持有文件描述符。真正加载模块库的是被注入进 zygote 的 libzygisk.so(通过 ReadModules 拿到路径后用自定义 loader 加载,见 03 篇);companion 则由 zygiskd 在需要时通过 lib_fd 加载。


五、守护进程协议:DaemonSocketAction

这是 zygiskd 对外服务的核心。协议特征:Unix domain socket(SOCK_STREAM)、裸二进制、无包头、一次请求一次短连接;整型按主机字节序直接读写,字符串为"size_t 长度 + 原始字节(不含 \0)"。

socket 的建立与 SELinux 上下文

// zygiskd.c:135-139
static int create_daemon_socket(void) {
  set_socket_create_context("u:r:zygote:s0");
  return unix_listener_from_path(PATH_CP_NAME);
}
  • PATH_CP_NAME = /data/adb/rezygisk/cp32.sockcp64.sockzygiskd.c:32)。
  • set_socket_create_context("u:r:zygote:s0")utils.c:56-95):把后续创建的 socket 的 SELinux 上下文设为 zygote,这样 zygote 进程才有权连接它。
  • unix_listener_from_pathutils.c:174-213)创建监听 socket 后,还会 chcon(path, "u:object_r:zygisk_file:s0") 给 socket 文件打标签。

主循环骨架

// zygiskd.c:322-652(高度节选)
while (1) {
  int client_fd = accept(socket_fd, NULL, NULL);
  uint8_t action8 = 0;
  read_uint8_t(client_fd, &action8);
  enum DaemonSocketAction action = (enum DaemonSocketAction)action8;

  switch (action) {
    case ZygoteInjected:        ... break;
    case GetProcessFlags:       ... break;
    case GetInfo:               ... break;
    case ReadModules:           ... break;
    case RequestCompanionSocket:... break;
    case GetModuleDir:          ... break;
    case ZygoteRestart:         ... break;
    case UpdateMountNamespace:  ... break;
    case RemoveModule:          ... break;
  }
  close(client_fd);
}

协议全表

动作枚举定义在 constants.h:13-23,9 个动作(值 0..8)。下表把**服务端处理(zygiskd.c)客户端封装(daemon.c)**对照列出:

动作 (值) 客户端发送 服务端响应 语义
ZygoteInjected (0) u8 动作 注入体上报"已注入",zygiskd 转发 ZYGOTE_INJECTED 给 monitor(zygiskd.c:349-352
GetProcessFlags (1) u8+u32 uid+string 进程名 u32 flags 计算进程标志位(root/denylist/manager/首进程/root 类型),见第九节
GetInfo (2) u8 u32 flags + u32 pid + size_t 模块数 + N×string 返回 daemon pid、root 类型、模块名清单(供 info 命令/WebUI)
ReadModules (3) u8 size_t 数量 + N×string 库路径 返回各模块 .so 绝对路径,供注入体加载(zygiskd.c:467-484
RequestCompanionSocket (4) u8+size_t index u8(1/0)(成功则后续直接复用此连接) 按需拉起 companion,把客户端 fd 转交给它,见第六节
GetModuleDir (5) u8+size_t index SCM_RIGHTS 传一个目录 fd 返回模块目录的 fd(zygiskd.c:548-581
ZygoteRestart (6) u8 zygote 重启:关闭全部 companion fd(zygiskd.c:354-363
UpdateMountNamespace (7) u8+u32 pid+u8 state u32 our_pid + u32 ns_fd 返回一个 Clean/Mounted 的 mount namespace fd,见第七节
RemoveModule (8) u8+size_t index u8(1/0) 从 Context 移除某模块(加载失败时由注入体调用,保持两端一致)

客户端侧(daemon.c)每个动作都是 rezygiskd_connect(1) → 写动作字节 → 写参数 → 读响应 → closeRequestCompanionSocket 是唯一例外:成功后不关闭 fd,而是把它升级为与 companion 通信的长连接。

一次 GetProcessFlags 的完整时序

sequenceDiagram participant Z as libzygisk (zygote 内) participant D as zygiskd participant R as root_impl Z->>D: connect(cp64.sock) Z->>D: write u8 = GetProcessFlags(1) Z->>D: write u32 uid Z->>D: write string 进程名 D->>R: uid_is_manager(uid) D->>R: uid_granted_root(uid) D->>R: uid_should_umount(uid, 进程名) Note over D: 组装 flags(含 root 类型位、首进程位) D-->>Z: write u32 flags Z->>D: close

六、Companion 进程模型

Zygisk 的 companion 机制:模块可以提供一个运行在有 root 权限的独立进程里的伴生服务(zygote 内的模块代码受 SELinux/降权限制,很多事做不了,就委托 companion)。zygiskd 负责这些 companion 的生命周期。

6.1 拉起 companion:双重 fork + exec

spawn_companionzygiskd.c:141-258)的设计很讲究:

flowchart TB A["zygiskd 主进程"] -->|socketpair| B["fork() 子进程"] A -->|"父: waitpid 等子退出"| A2["父收到子已 exec 的确认"] B -->|"去 FD_CLOEXEC<br/>设置进程名"| C["non_blocking_execv<br/>zygiskd companion fd"] C -->|"内部再 fork"| D["孙进程 = companion_entry"] B -->|"exit(0)"| E["子进程退出<br/>→ 父 waitpid 返回"] A2 -->|"写模块名 + lib_fd"| D D -->|"dlopen+dlsym<br/>zygisk_companion_entry"| F["回 1=有/0=无 entry"]
  • 主进程先 socketpair 得到 daemon_fd / companion_fd,再 fork
  • 子进程去掉 companion_fdFD_CLOEXEC,把进程名设成 <nice_name>-<模块名>,然后 non_blocking_execv(ZYGISKD_PATH, {process_name, "companion", fd_str})——即重新 exec 自己、走 companion 子命令。non_blocking_execv 内部又 fork 一次(utils.c:421-453),所以真正的 companion 是"孙进程",被 init 收养,与 zygiskd 主进程脱钩。
  • 子进程随即 exit(0),主进程 waitpid 到它,确认 companion 已成功 exec。
  • 之后主进程通过 daemon_fd模块名模块 .so 的 fdwrite_fd via SCM_RIGHTS)发给 companion,并读取一个字节:1 = 模块有 companion entry,0 = 没有(此时返回 -2,主循环据此记录"该模块无 companion")。

6.2 companion 进程本体

companion_entrycompanion.c:84-182):

void companion_entry(int fd) {
  char name[256 + 1];
  read_string(fd, name, sizeof(name));        // 模块名
  int library_fd = read_fd(fd);               // 模块 .so 的 fd
  zygisk_companion_entry module_entry = load_module(library_fd);  // dlopen + dlsym
  close(library_fd);

  if (module_entry == NULL) { write_uint8_t(fd, 0); goto cleanup; } // 无 entry
  else write_uint8_t(fd, 1);

  signal(SIGPIPE, SIG_IGN);
  while (1) {
    if (!check_unix_socket(fd, true)) break;  // 阻塞等待 zygiskd 转发请求
    int client_fd = read_fd(fd);              // 收到一个 APP 客户端 fd
    ...
    pthread_create(&thread, NULL, entry_thread, args);  // 每个请求一个线程
    pthread_detach(thread);
  }
  cleanup: close(fd); exit(0);
}
  • load_modulecompanion.c:26-47):dlopen("/proc/self/fd/<library_fd>") + dlsym("zygisk_companion_entry")。用 /proc/self/fd/ 路径 dlopen 是经典技巧——无需把 .so 落地到可访问路径。
  • 之后进入循环:每收到一个被转交的 APP 客户端 fd,就起一个分离线程执行 entry_thread → 调用模块的 module_entry(client_fd)
  • entry_threadcompanion.c:50-81)有个细节:用 fstat 比对调用前后 fd 的 inode,只有当 fd 仍是原来那个文件时才 close,避免模块自己已经关闭/复用了 fd 导致的双重关闭。

6.3 RequestCompanionSocket:把请求"接力"给 companion

主循环里 RequestCompanionSocket 的处理(zygiskd.c:485-547):

struct Module *module = &context.modules[index];
if (module->companion >= 0) {
  if (!check_unix_socket(module->companion, false)) {   // companion 崩了?
    close(module->companion); module->companion = -1;
  }
}
if (module->companion <= -1) {
  module->companion = spawn_companion(argv, module->name, module->lib_fd);  // 懒启动
}
if (module->companion >= 0) {
  write_fd(module->companion, client_fd);   // 把 APP 的连接 fd 转交给 companion
}

即:companion 是懒启动带健康检查的——第一次请求时才 fork,崩溃后下次请求自动重启。拿到(或新建)companion 连接后,用 write_fd 把当前 APP 的 client_fdSCM_RIGHTS 直接送进 companion,此后 APP 与 companion 就在这个 fd 上直连对话,zygiskd 不再介入数据面。


七、Mount Namespace 管理与 Root 隐藏

这是 zygiskd 最精巧的部分之一,对应 UpdateMountNamespace 动作(zygiskd.c:582-612)与 utils.csave_mns_fd / umount_root

7.1 为什么需要它

很多 APP(银行、游戏)会检测设备是否 root。隐藏手段之一:让这些 APP 运行在一个看不到任何 root 挂载的 mount namespace 里——/data/adb/modules 的 overlay、magisk 的 worker 挂载等统统不可见。

ReZygisk 维护两种 namespace(constants.h:42-45):

  • Clean:卸载了所有 root 挂载的"干净"命名空间,给需要隐藏 root 的进程(DenyList 命中)。
  • Mounted:保留挂载的正常命名空间,给允许看到模块的进程。

7.2 save_mns_fd:fork 一个进程去"制备"命名空间

// utils.c:732-861(结构节选)
int save_mns_fd(int pid, enum MountNamespaceState mns_state, struct root_impl impl) {
  static int clean_namespace_fd = -1;     // 缓存:只制备一次
  static int mounted_namespace_fd = -1;
  if (mns_state == Clean && clean_namespace_fd != -1) return clean_namespace_fd;
  if (mns_state == Mounted && mounted_namespace_fd != -1) return mounted_namespace_fd;

  socketpair(...); 
  pid_t fork_pid = fork();
  if (fork_pid == 0) {
    switch_mount_namespace(pid);           // 进目标进程的 ns
    if (mns_state == Clean) {
      unshare(CLONE_NEWNS);                // 私有化挂载
      umount_root(impl);                   // 卸载所有 root 挂载
    }
    /* 通过 socket 与父进程握手,保持存活直到父进程 open 了 /proc/<pid>/ns/mnt */
  }
  ...
  snprintf(ns_path, PATH_MAX, "/proc/%d/ns/mnt", fork_pid);
  int ns_fd = open(ns_path, O_RDONLY);     // 抓取这个 ns 的 fd
  ...
  if (mns_state == Clean) clean_namespace_fd = ns_fd;   // 缓存
  else mounted_namespace_fd = ns_fd;
  return ns_fd;
}

思路:fork 一个临时子进程,让它进入目标进程的 namespace;如果要 Clean,就 unshare 出私有挂载视图再 umount_root。父进程随后 open 这个子进程的 /proc/<pid>/ns/mnt 拿到 namespace 的 fd 并缓存static,全局只制备一次)。这个 fd 最终经 UpdateMountNamespace 回传给注入体,注入体再 setns 进去。

注意 UpdateMountNamespace 处理时(zygiskd.c:595-596):当请求 Clean 时,会先顺手制备好 Mounted,再制备 Clean——保证两种都就绪。

7.3 umount_root:精确卸载 root 痕迹

umount_rootutils.c:667-730)先用 parse_mountinfo("self") 解析 /proc/self/mountinfo,再筛选要卸载的挂载点:

// utils.c:690-693(判定逻辑)
if (strcmp(mount.source, source_name) == 0
    || (impl.impl == Magisk && strcmp(mount.source, "worker") == 0)) should_unmount = true;
if (strncmp(mount.target, "/data/adb/modules", strlen("/data/adb/modules")) == 0) should_unmount = true;
if (strncmp(mount.root, "/adb/modules/", strlen("/adb/modules/")) == 0) should_unmount = true;
  • source_name 按 root 实现取值:Magisk→magisk、KernelSU→KSU、APatch→APatchutils.c:678-680);
  • 命中后逆序 umount2(target, MNT_DETACH)utils.c:714-723,逆序是为先卸载子挂载再卸载父挂载)。

parse_mountinfoutils.c:531-665)是一个相当完整的 /proc/<pid>/mountinfo 解析器,用 sscanf%n 技巧定位各字段(id/parent/major:minor/root/target/options/type/source 等),并处理 shared:/master:/propagate_from: 可选字段。


八、Root 适配层 root_impl

root_impl/ 把"Magisk / KernelSU / APatch 三者各自不同的查询方式"统一到一组接口之后,zygiskd.c 只需面向抽象编程。

8.1 数据结构与分发

// root_impl/common.h:10-32
enum root_impls { None, Multiple, KernelSU, APatch, Magisk };

struct root_impl_state {   // 各实现"自检"时填写
  enum RootImplState state;  // Supported / TooOld / Inexistent / Abnormal
  uint8_t variant;
};
struct root_impl {         // 对外汇总结果
  enum root_impls impl;
  uint8_t variant;
};

void get_impl(struct root_impl *uimpl);
bool uid_granted_root(uid_t uid);
bool uid_should_umount(uid_t uid, const char *const process);
bool uid_is_manager(uid_t uid);

注意区分两个结构体:root_impl_state 是每个实现做存在性/版本检测时填的"局部结果";root_implroot_impls_setup 汇总后保存的"全局结论"。(这一点正是本项目通读时最容易看错的地方——务必以 common.h 真实定义为准。)

root_impls_setupcommon.c:12-65)的决策逻辑:

flowchart TB S["root_impls_setup"] --> K["ksu_get_existence(&state_ksu)"] S --> A["apatch_get_existence(&state_apatch)"] S --> M["magisk_get_existence(&state_magisk)"] K & A & M --> C{"Supported 的数量"} C -->|">= 2"| MUL["impl = Multiple<br/>(环境异常,拒绝服务)"] C -->|"= 1, 且为 KSU"| KS["impl = KernelSU<br/>variant = state_ksu.variant"] C -->|"= 1, 且为 APatch"| AP["impl = APatch"] C -->|"= 1, 且为 Magisk"| MG["impl = Magisk"] C -->|"= 0"| NO["impl = None"]

zygiskd_start 开头(zygiskd.c:267-283)若发现 NoneMultiple,会向 monitor 上报 DAEMON_SET_ERROR_INFO 并直接 exit——这就是模块描述里看到"Unsupported environment"的来源。

三个查询接口(uid_granted_root / uid_should_umount / uid_is_manager)都是简单的 switch(impl.impl) 分发到对应实现(common.c:71-120)。root_impl_cleanup 只在 KernelSU 下需要(关闭 ioctl fd,common.c:122-124)。

8.2 KernelSU:ioctl 新接口 + prctl 旧接口双通道

kernelsu.c 是三者中最复杂的,因为 KernelSU 同时存在新旧两套内核交互接口。ksu_get_existencekernelsu.c:73-164)的探测顺序:

flowchart TB G["ksu_get_existence"] --> W{"ro.board.platform<br/>== waydroid ?"} W -->|是| P["跳过 SYS_reboot<br/>直接走 prctl 旧接口"] W -->|否| SR["syscall(SYS_reboot,<br/>MAGIC1, MAGIC2, 0, &ksu_fd)"] SR --> FD{"ksu_fd 有效?"} FD -->|是| NEW["新 ksuctl(ioctl)<br/>ksu_uses_new_ksuctl=true<br/>SET_FEATURE 关闭内核 umount<br/>GET_HOOK_MODE 判 KNext/KOfficial"] FD -->|否| P P --> PV["prctl(0xDEADBEEF,<br/>CMD_GET_VERSION)"] PV --> VER{"version"} VER -->|"0"| ABN["Abnormal"] VER -->|">= MIN_KSU_VERSION"| CK{"/data/adb/ksu/bin/ksud 存在?"} VER -->|"1..MIN-1"| OLD["TooOld"] CK -->|是| SUP["Supported"] CK -->|否| INX["Inexistent"]

几个关键设计点:

  • Waydroid 特判kernelsu.c:77-82):在 Waydroid 上 SYS_reboot 会触发 SIGSYS 把 zygiskd 打崩,因此读 ro.board.platform 判断后直接跳到 prctl 老接口。
  • 新接口探测kernelsu.c:84):syscall(SYS_reboot, KSU_INSTALL_MAGIC1=0xDEADBEEF, KSU_INSTALL_MAGIC2=0xCAFEBABE, 0, &ksu_fd),成功会回填一个 ioctl 用的 fd。
  • 关闭内核 umountkernelsu.c:145-155):通过 KSU_IOCTL_SET_FEATUREkernel_umount 关掉——因为 ReZygisk 要自己用 umount_root 精确控制卸载,不让内核插手。
  • ksud 存在性校验kernelsu.c:100-106 / 135-141):有些定制 ROM 预装了 KernelSU 但用户实际用 Magisk,故要求 /data/adb/ksu/bin/ksud 存在才算 Supported。
  • 变体判定:通过 hook mode(KernelSU Next 有 CMD_HOOK_MODE / KSU_IOCTL_GET_HOOK_MODE)区分 KNextKOfficial
  • manager 判定kernelsu.c:216-256):优先用 CMD_GET_MANAGER_UID/ioctl 拿管理器 UID(对仿冒包名更可靠);回退到 stat 已知包名目录。还处理了 Private Space(UID 10xxxxx% 100000 归一化)。

UID 查询同样区分新旧接口(kernelsu.c:166-214):新接口走 ioctl(ksu_fd, KSU_IOCTL_UID_GRANTED_ROOT/...),旧接口走 prctl(0xDEADBEEF, CMD_UID_GRANTED_ROOT/...),并用一个数组+回写标志位的约定读取结果。

8.3 Magisk:靠 magisk 二进制 + sqlite

magisk.c 通过执行 magisk 命令行来查询(exec_command 抓取 stdout):

  • 存在性magisk.c:22-57):在 /sbin/magisk[32/64]/debug_ramdisk/magisk[32/64] 等候选路径里找到 magisk 二进制,执行 magisk -V 取版本,与 MIN_MAGISK_VERSIONmagisk.h:9,26000)比较。
  • granted rootmagisk.c:59-73):magisk --sqlite "select 1 from policies where uid=<uid> and policy=2 limit 1",有结果即已授权。
  • should umountmagisk.c:75-91):magisk --sqlite "SELECT 1 FROM denylist WHERE '<process>' LIKE process || '%' LIMIT 1"——注意是按进程名前缀匹配 DenyList。
  • managermagisk.c:93-117):查 strings 表的 requester 拿到管理器包名,再 stat 其数据目录比对 UID。

这意味着 Magisk 模式下,每次进程查询都会 fork+exec 一个 magisk 子进程跑 sqlite——这是 Magisk 没有提供内核态查询接口的现实妥协。

8.4 APatch:解析 package_config

apatch.c

  • 存在性apatch.c:14-55):要求 /data/adb/ap/bin/apd 存在、PATH/data/adb/ap/bin,执行 apd -V 取版本,与 MIN_APATCH_VERSION(10657)比较。
  • 配置来源:APatch 把每个包的策略写在 CSV 文件 /data/adb/ap/package_config(列:process, exclude(umount), allow(root), uid)。_apatch_get_package_configapatch.c:78-143)解析它。
  • granted root / should umountapatch.c:145-206):按 UID 匹配配置行读取 allow/exclude。对隔离服务IS_ISOLATED_SERVICE(uid),即 90000 ≤ uid < 1000000)做了额外的进程名前缀匹配兜底,防止隔离进程绕过 DenyList。
  • managerapatch.c:208-219):stat /data/user_de/0/me.bmax.apatch 比对 UID。

8.5 三方案对比

维度 KernelSU Magisk APatch
查询机制 内核 ioctl / prctl(0xDEADBEEF 执行 magisk --sqlite 解析 package_config CSV
最低版本 MIN_KSU_VERSION 26000 10657
变体 KOfficial / KNext
授权查询 ioctl/prctl 返回数组 policies CSV allow
DenyList ioctl/prctl denylist 表(进程名前缀) CSV exclude 列 + 隔离服务兜底
manager ioctl GET_MANAGER_UID / stat 包名 strings.requester + stat stat me.bmax.apatch
需要 cleanup 是(关 ioctl fd)
特殊处理 Waydroid SIGSYS、ksud 校验、Private Space 多路径探测 隔离服务前缀匹配

九、ProcessFlags 与 UID 判定全流程

把第五节的 GetProcessFlags 与第八节的 root_impl 串起来,就是 ReZygisk 对"一个即将特化的 APP 进程该怎么对待"的完整决策。

进程标志位定义(constants.h:25-33):

enum ProcessFlags: uint32_t {
  PROCESS_GRANTED_ROOT     = (1u << 0),
  PROCESS_ON_DENYLIST      = (1u << 1),
  PROCESS_IS_MANAGER       = (1u << 27),
  PROCESS_ROOT_IS_APATCH   = (1u << 28),
  PROCESS_ROOT_IS_KSU      = (1u << 29),
  PROCESS_ROOT_IS_MAGISK   = (1u << 30),
  PROCESS_IS_FIRST_STARTED = (1u << 31)
};

GetProcessFlags 的处理(zygiskd.c:364-419):

flowchart TB R["读 uid + 进程名"] --> F{"first_process?"} F -->|是| FF["flags |= PROCESS_IS_FIRST_STARTED<br/>first_process = false"] F -->|否| MGR FF --> MGR{"uid_is_manager(uid)?"} MGR -->|是| MM["flags |= PROCESS_IS_MANAGER"] MGR -->|否| GR{"uid_granted_root(uid)?"} GR -->|是| GG["flags |= PROCESS_GRANTED_ROOT"] GR -->|否| UM GG --> UM{"uid_should_umount(uid, 进程名)?"} UM -->|是| DD["flags |= PROCESS_ON_DENYLIST"] UM -->|否| RT MM --> RT["按 impl 追加 root 类型位<br/>KSU/APatch/Magisk"] DD --> RT RT --> W["write u32 flags 回客户端"]

要点:

  • 首进程标志:zygiskd 用一个 bool first_processzygiskd.c:321)标记整个生命周期里第一个被查询的进程,置 PROCESS_IS_FIRST_STARTED
  • manager 与普通进程互斥:是管理器就只标 PROCESS_IS_MANAGER,不再判 root/denylist(zygiskd.c:385-394)。
  • root 类型位始终根据当前 impl 追加,注入体据此知道宿主是哪种 root。

注入体拿到这些 flags 后,决定是否给该进程切到 Clean namespace(DenyList 命中)、是否暴露模块等——这部分逻辑在 03 篇展开。


十、小结

zygiskd 是 ReZygisk 的服务中枢,本篇可以浓缩为五句话:

  1. 多角色单二进制:靠子命令区分守护进程 / companion / 调试,32/64 位各跑一份。
  2. 模块即文件描述符:扫描 /data/adb/modules,只持有 <arch>.so 的 fd,不在 daemon 内 dlopen 模块。
  3. 9 个动作的二进制 RPCDaemonSocketAction 是注入体与 daemon 的全部对话;SCM_RIGHTS 传 fd 是其中 companion / 模块目录 / namespace 交接的关键。
  4. companion 懒启动 + fd 接力:双重 fork 把 companion 脱钩为 init 的孩子,按需拉起、崩溃自愈,APP 与 companion 最终直连。
  5. root 抽象 + namespace 隐藏root_impl 抹平三种 root 的差异;save_mns_fd/umount_root 制备并缓存 Clean/Mounted 两种挂载命名空间,实现按进程隐藏 root。

下一篇(02)将进入"代码是如何被塞进 zygote 的"——monitor 如何 ptrace 追踪 init、捕获 zygote,以及 ptracer 如何用自定义链接器 CSOLoader 完成远程注入。

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。