KernelSU libsu 与 su 兼容层
本文档解读 Manager 应用如何使用 topjohnwu 的 libsu 库调用 ksud,以及"传统 su 体验"在内核与 ksud 两侧的实现。它对应 用户空间实现 中的"通路 B(经 ksud 命令行)"。
阅读前提:当前
ksud是 Rust 二进制,被打包为libksud.so随 APK 分发;Manager 通过 libsu 创建的 root shell 以libksud.so <子命令>形式调用它。请先了解 用户空间实现 的整体架构。
1. libsu 库简介
libsu 是 Magisk 作者 topjohnwu 开发的 Android root 应用开发库,提供:
- 便捷的 root shell 创建与命令执行;
- 进程间服务(IPC);
- root 文件 I/O。
GitHub:https://github.com/topjohnwu/libsu
1.1 版本与依赖
KernelSU 当前使用 libsu 6.0.0(manager/gradle/libs.versions.toml,libsu = "6.0.0"),引入三个 artifact:
// manager/app/build.gradle.kts
implementation(libs.com.github.topjohnwu.libsu.core) // Shell 执行、任务管理
implementation(libs.com.github.topjohnwu.libsu.service) // 跨进程服务
implementation(libs.com.github.topjohnwu.libsu.io) // root 文件 I/O
KernelSU 主要使用 core(执行 ksud 命令、模块管理)。
1.2 ksud 作为 libksud.so 的集成
ksud(Rust)构建后被复制为 manager/app/src/main/jniLibs/<abi>/libksud.so 打包进 APK。运行期取路径:
// KsuCli.kt
private fun getKsuDaemonPath(): String =
ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
为什么用
.so形式打包?Android 只保证nativeLibraryDir下的文件在安装后保留可执行权限并随 APK 更新,把可执行的 ksud 命名为libksud.so即可被 root shell 直接执行。
2. Shell 管理(KsuCli.kt)
文件:manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt。
2.1 Shell 三级回退
fun createRootShell(globalMnt: Boolean = false): Shell {
Shell.enableVerboseLogging = BuildConfig.DEBUG
val builder = Shell.Builder.create()
return try {
// 第一级:用 ksud 自带的 su 入口
if (globalMnt) builder.build(getKsuDaemonPath(), "debug", "su", "-g")
else builder.build(getKsuDaemonPath(), "debug", "su")
} catch (e: Throwable) {
try {
// 第二级:系统 su(globalMnt 时 -mm 全局挂载)
if (globalMnt) builder.build("su", "-mm") else builder.build("su")
} catch (e: Throwable) {
// 第三级:普通 sh(无 root)
builder.build("sh")
}
}
}
ksud debug su [-g] ← KernelSU 自身的 su
↓ 失败
su [-mm] ← 系统已有的 su(兼容其它方案)
↓ 失败
sh ← 无 root,降级运行
- 第一级用
libksud.so debug su,-g表示全局 mount namespace; - 这条命令进内核 →
escape_with_root_profile提权 → ksud exec shell(见 §5)。
2.2 全局 Shell 与临时 Shell
object KsuCli {
val SHELL: Shell = createRootShell() // 应用私有 ns
val GLOBAL_MNT_SHELL: Shell = createRootShell(true) // 全局 ns
}
fun getRootShell(globalMnt: Boolean = false): Shell =
if (globalMnt) KsuCli.GLOBAL_MNT_SHELL else KsuCli.SHELL
inline fun <T> withNewRootShell(globalMnt: Boolean = false, block: Shell.() -> T): T =
createRootShell(globalMnt).use(block) // 用完自动关闭
| Shell | mount namespace | 用途 |
|---|---|---|
SHELL |
应用私有 | 普通命令 |
GLOBAL_MNT_SHELL |
全局(init) | 模块操作、系统级修改、WebUI |
withNewRootShell{} |
每次新建 | 长耗时/隔离操作(如刷写) |
3. execKsud 与通过 ksud 实现的功能
3.1 execKsud 封装
fun execKsud(args: String, newShell: Boolean = false): Boolean =
if (newShell) withNewRootShell { ShellUtils.fastCmdResult(this, "${getKsuDaemonPath()} $args") }
else ShellUtils.fastCmdResult(getRootShell(), "${getKsuDaemonPath()} $args")
即在 root shell 里跑 libksud.so <args> 并返回布尔成功。绝大多数"重操作"都经它落到 ksud。
3.2 KsuCli 通过 ksud 实现的功能一览
| 功能 | ksud 子命令 |
|---|---|
| 安装 ksud 自身 | install --libadbroot <libadbroot.so> |
| 列出模块(JSON) | module list |
| 启用/禁用模块 | module enable/disable <id> |
| 卸载/撤销卸载 | module uninstall/undo-uninstall <id> |
| 刷写模块(流式输出) | module install <zip> |
| 运行模块动作 | module action <id>(全局 ns) |
| 刷写 boot | boot-patch [-f | -b boot.img] [--allow-shell] [--enable-adbd] [-u] [-m lkm.ko | --kmi x] [-o downloads] [--partition p] |
| 还原 boot | boot-restore -f |
| 永久卸载 | uninstall --package-name <pkg> |
| KMI 信息 | boot-info <current-kmi | supported-kmis | is-ab-device | ...> |
| feature 持久值 | feature check <f> / feature get --config <f> |
| sepolicy 校验/读写 | sepolicy check '<rules>' / profile get-sepolicy <pkg> / profile set-sepolicy <pkg> '<rules>' |
| profile 模板 | profile list-templates / get-template / set-template / delete-template |
| 软重启 | soft-reboot |
注意:
boot-patch不再依赖外部libmagiskboot.so——boot 镜像解析/修补由 ksud 内嵌的android-bootimg完成(见 用户空间实现 §3.1、高级特性与安全)。少数查询如getSuperuserCount()走 JNI(Natives),不走 ksud。
3.3 流式输出(刷写进度)
模块/boot 刷写用 libsu 的 CallbackList 把 stdout/stderr 实时回调给 UI:
private fun flashWithIO(cmd: String, onStdout: (String)->Unit, onStderr: (String)->Unit): Shell.Result {
val stdout = object : CallbackList<String?>() { override fun onAddElement(s: String?) { onStdout(s ?: "") } }
val stderr = object : CallbackList<String?>() { override fun onAddElement(s: String?) { onStderr(s ?: "") } }
return withNewRootShell { newJob().add(cmd).to(stdout, stderr).exec() }
}
4. su 兼容层(内核侧)
"调用 /system/bin/su 就能拿 root"是传统 root 应用的预期。KernelSU 用内核 feature KSU_FEATURE_SU_COMPAT(默认开启)实现这一兼容,代码在 kernel/feature/sucompat.c。
4.1 Hook 机制(已不是 Kprobe)
重要更正:旧版用 Kprobe 分别 hook
execve/faccessat/newfstatat/pts_unix98_lookup。当前实现改为 sys_enter tracepoint + ni_syscall 空槽分发器(详见 内核层实现)。对被标记进程的目标 syscall,分发器查路由表syscall_hooks[]调用对应 handler。
sucompat 关注的 syscall:execve、faccessat、newfstatat(setresuid 由 setuid_hook 处理,见 内核层实现、关键流程分析)。只有在 allowlist 中(或 Manager)的 UID 才会触发改写,避免干扰普通应用。
4.2 三类处理
① 存在性/状态查询改写(faccessat / newfstatat):
应用常用 access("/system/bin/su") 或 stat("/system/bin/su") 探测 su 是否存在。sucompat 把这两类调用里指向 /system/bin/su 的路径改写为 /system/bin/sh,让探测"看到一个存在且可访问的目标":
应用: access("/system/bin/su", F_OK)
→ sucompat 改写 path: /system/bin/su → /system/bin/sh
→ 探测成功(sh 一定存在)
② 执行重定向 + 提权(execve):
当授权 UID 执行 /system/bin/su 时,sucompat 把执行目标重定向到 ksud,并在 exec 前调用 escape_with_root_profile() 把进程提权为对应 root profile:
应用: execve("/system/bin/su", argv, envp)
→ 校验 UID 在 allowlist
→ 重定向到 KSUD_PATH (/data/adb/ksud),argv[0] 设为 "su"
→ escape_with_root_profile() 提权(改 cred / 禁 seccomp / 切 SELinux 到 u:r:ksu:s0)
→ 实际执行 ksud(进入 su::root_shell,见 §5)
③ 终端(tty)SELinux 修复 —— 由 file_wrapper 取代旧 devpts hook:
root shell 的 stdin/stdout/stderr 通常是应用域下的 pts,直接交给 u:r:ksu:s0 进程会触发 SELinux 拒绝。旧版用 Kprobe hook pts_unix98_lookup 改 devpts 的 SELinux sid;当前改用 infra/file_wrapper:内核构造 [ksu_fdwrapper] 匿名 inode 把目标 fd 包一层,并把该 inode 的 SELinux sid 设为 ksu_file_sid。ksud 在 su::root_shell 里通过 IOCTL KSU_IOCTL_GET_WRAPPER_FD(即 get_wrapped_fd)取回包装 fd 替换 tty(见 §5)。
4.3 feature 开关
su 兼容可被开关,对应 KSU_FEATURE_SU_COMPAT(id=0):
Natives.isSuEnabled() // 查询(JNI → IOCTL GET_FEATURE)
Natives.setSuEnabled(true) // 开关(JNI → IOCTL SET_FEATURE)
内核侧 set handler 会按需注册/注销 sucompat 的 syscall hook,并更新全局开关 ksu_su_compat_enabled。
5. ksud 的 su 实现(用户侧,su.rs)
文件:userspace/ksud/src/su.rs。当内核把 su 调用重定向到 ksud 后,cli::run 检测到 argv[0] == "su" 即进入 root_shell()。此时进程已在内核里被提权为 root。
5.1 root_shell:Magisk/标准 su 兼容
root_shell() 用 getopts 解析与 Magisk/标准 su 一致的参数:
| 参数 | 说明 |
|---|---|
-c <cmd> |
执行命令后退出 |
-l |
登录 shell |
-p |
保留环境 |
-s <shell> |
指定 shell |
-g <gid> / -G <group> |
主组 / 附加组 |
-M / --mount-master(-mm) |
全局 mount namespace |
-W / --no-wrapper |
不做 tty fd 包装 |
--ksu-no-new-privs |
设置后禁止再次提权 |
-v / -V |
版本信息 |
pre_exec(exec 前在子进程上下文执行)依次做:
umask(022);switch_cgroups()(把进程挪进合适 cgroup);-M时switch_mnt_ns(1)切到 init 的全局 mount ns;- tty fd 包装:对 isatty 的 0/1/2,用
get_wrapped_fd换成[ksu_fdwrapper]包装 fd(解决 §4.2③ 的 SELinux 问题);除非传了-W; set_identity(uid, gid, groups)设置目标身份;- 设置
HOME/USER/LOGNAME/SHELL,PATH追加/data/adb/ksu/bin;存在.ksurc则作为ENV。
--ksu-no-new-privs 会调 set_ksu_no_new_privs()(IOCTL KSU_IOCTL_DISABLE_ESCAPE_TO_ROOT),之后该进程不能再经 sucompat/supercall 提权。
5.2 grant_root(ksud debug su)
createRootShell 第一级用的就是它(su.rs::grant_root):
pub fn grant_root(global_mnt: bool) -> Result<()> {
ksucalls::grant_root()?; // IOCTL KSU_IOCTL_GRANT_ROOT → 内核提权
// 可选切全局 mount ns,PATH 追加 /data/adb/ksu/bin,然后 exec sh
}
5.3 完整调用链
Manager (KsuCli.createRootShell)
→ libsu: build("libksud.so", "debug", "su")
→ ksud: cli::run → debug::su → su::grant_root
→ ksucalls::grant_root ──IOCTL KSU_IOCTL_GRANT_ROOT──▶ 内核
→ 内核 escape_with_root_profile:改 cred / 禁 seccomp / 切 u:r:ksu:s0
→ ksud exec sh(已是 root)
→ libsu 拿到 root shell
→ Manager 在该 shell 上跑命令
6. WebUI:模块网页界面的 root 桥接
模块可附带 webroot/,由 Manager 的 WebView 加载并提供 root 能力。这套机制由两部分组成:
6.1 Java↔JS 桥(WebViewInterface.kt)
manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewInterface.kt 用 @JavascriptInterface 向页面注入全局对象 ksu:
exec(cmd[, options][, callback]):用withNewRootShell(true)(全局 ns)执行命令,结果经webView.loadUrl("javascript:...")回调;processOptions处理cwd(cd)、env(export);spawn(command, args, options, callback):用CallbackList把 stdout/stderr/exit/error 流式回调;toast / fullScreen / enableEdgeToEdge / moduleInfo / listPackages / getPackagesInfo / exit。
6.2 JS 库(js/,npm 包 kernelsu)
供模块作者在 WebUI 里使用的前端库,版本 3.0.2,纯 ES module 无依赖。它封装注入的 ksu.* 对象,导出:
import { exec, spawn, toast, moduleInfo, listPackages } from "kernelsu";
const { errno, stdout } = await exec("id"); // root 执行
const child = spawn("logcat", ["-v", "time"]); // 流式:child.stdout.on('data', ...)
toast("done");
约定:ksu://icon/{packageName} 取应用图标;引入 internal/insets.css 自动开启 edge-to-edge。
7. libsu 高级用法
7.1 Job 链式调用
getRootShell().newJob()
.add("cd /data/adb/ksu")
.add("ls -l")
.exec()
7.2 输出捕获 / 实时回调
val out = ArrayList<String>(); val err = ArrayList<String>()
getRootShell().newJob().add("ls /system").to(out, err).exec()
val cb = object : CallbackList<String?>() {
override fun onAddElement(s: String?) { /* 处理每一行 */ }
}
getRootShell().newJob().add("long_cmd").to(cb, null).exec()
8. 最佳实践与调试
8.1 复用 vs 隔离
- 短命令复用全局
getRootShell(),减少进程开销; - 长耗时/危险操作(刷写、卸载)用
withNewRootShell{}隔离并自动关闭。
8.2 命令注入防护
ksud 命令多以字符串拼接构造,传入用户可控内容时务必转义/校验:
val escaped = filename.replace("\"", "\\\"")
getRootShell().newJob().add("rm \"$escaped\"").exec()
实测发现
KsuCli.setAppProfileTemplate的引号转义存在拼接瑕疵(拼成"$escapedTemplate'"),属可改进点。
8.3 调试
Shell.enableVerboseLogging = BuildConfig.DEBUG // 打印 SHELL_IN/OUT
val r = getRootShell().newJob().add("id").exec() // 期望 uid=0(root)
Log.d(TAG, "root=${getRootShell().isRoot}, code=${r.code}, out=${r.out}")
8.4 拿不到 root 的排查
- 内核是否正常?
Natives.version/Natives.isManager; - UAPI 是否匹配?
Natives.checkUAPIMismatch(); - su 兼容是否开启?
Natives.isSuEnabled(); - 该应用是否授权?App Profile 的
allowSu。
9. 总结
- KernelSU 通过 libsu 6.0.0 创建 root shell,以三级回退
ksud su → 系统 su → sh保证可用性; - "传统 su 体验"是 内核 + ksud 协同:内核
sucompat(tracepoint 分发器,非 Kprobe)改写 su 探测、重定向 su 执行到 ksud 并提权;ksudsu.rs负责 Magisk 兼容参数解析、tty fd 包装、身份设置; - tty 的 SELinux 问题由 file_wrapper(
[ksu_fdwrapper]+get_wrapped_fd) 解决,取代旧的 devpts Kprobe hook; - 模块/boot/sepolicy 等重操作经
execKsud落到libksud.so子命令;WebUI 通过WebViewInterface注入的ksu对象 + js 库kernelsu@3.0.2获得 root 能力。
相关文档:
评论
- 还没有评论,来说点什么吧。