Magisk 内部机制详解:目录结构、启动流程、resetprop 与 SELinux 策略补丁
本文基于 Magisk 仓库的 docs/details.md 展开,系统讲解 Magisk 在设备上的"内部布局":它把二进制与临时数据放在哪里(tmpfs 目录与 /data/adb)、从 pre-init 到 late_start 的启动流程如何分阶段执行、resetprop 如何绕过 property_service 直接改写系统属性、以及它是如何打补丁修改 SELinux 策略来构建 magisk 域沙箱的。读完本文,你将理解 Magisk 各组件在文件系统上的落点、每个启动阶段的触发时机与职责划分,并能从源码层面验证这些行为的实现细节。
一、文件系统布局:两个关键存放位置
Magisk 需要把不同性质的数据分开存放:一部分是每次开机都会重建的临时数据(二进制、挂载点、补丁文件),放在 tmpfs 中;另一部分是跨重启必须保留的数据(模块、数据库、脚本),放在 /data 的非易失存储中。仓库中这两组路径都以编译期常量的形式集中定义,见 consts.hpp 和 consts.rs:
// native/src/include/consts.hpp
#define SECURE_DIR "/data/adb"
#define MODULEROOT SECURE_DIR "/modules"
#define DATABIN SECURE_DIR "/magisk"
#define MAGISKDB SECURE_DIR "/magisk.db"
// tmpfs paths
#define INTLROOT ".magisk"
#define MIRRDIR INTLROOT "/mirror"
#define ROOTOVL INTLROOT "/rootdir"
...
1.1 "Magisk tmpfs 目录"中的路径
Magisk 会挂载一个 tmpfs 目录来存放临时数据。对于存在 /sbin 目录的设备,会优先选择它,因为这个目录同时可以充当一个 overlay,把二进制注入到 PATH 中;从 Android 11 开始,/sbin 可能不再存在,此时 Magisk 使用 /debug_ramdisk 作为基础目录。当前实际使用的目录可以通过 magisk --path 命令查看。该基础目录的直接内容如下:
# In order to get the current base folder Magisk is using,
# use the command `magisk --path`.
# Binaries like magisk, magiskinit, and all symlinks to
# applets are directly stored in this path. This means when
# this is /sbin, these binaries will be directly in PATH.
MAGISKTMP=$(magisk --path)
# Magisk internal stuffs
INTERNALDIR=$MAGISKTMP/.magisk
# /data/adb/modules will be bind mounted here.
# The original folder is not used due to nosuid mount flag.
$INTERNALDIR/modules
# The current Magisk installation config
$INTERNALDIR/config
# Partition mirrors
# Each directory in this path will be mounted with the
# partition of its directory name.
# e.g. system, system_ext, vendor, data ...
$INTERNALDIR/mirror
# Root directory patch files
# On system-as-root devices, / is not writable.
# All pre-init patched files are stored here and bind mounted.
$INTERNALDIR/rootdir
结合 consts.rs 可以看到实际比文档列出更多的内部子目录:
路径(相对于 $MAGISKTMP/.magisk) |
用途 |
|---|---|
config(MAIN_CONFIG) |
当前 Magisk 安装配置 |
modules(MODULEMNT) |
/data/adb/modules 的 bind mount 挂载点,绕开 nosuid 标志 |
mirror(MIRRDIR) |
分区镜像目录,每个子目录按目录名挂载对应分区(system、vendor、data 等) |
preinit(PREINITMIRR) |
pre-init 阶段的分区挂载镜像 |
rootdir(ROOTOVL) |
system-as-root 设备上 / 不可写,pre-init 阶段被补丁修改的文件存放在这里并 bind mount 回去 |
device(DEVICEDIR) |
存放与设备相关的文件,如 socket(MAIN_SOCKET)、log(LOG_PIPE)、preinit 块设备节点 |
worker(WORKERDIR) |
模块安装等工作进程的私有挂载命名空间 |
busybox(BBPATH) |
busybox 及其 applet 符号链接的安装目录 |
从源码看,$INTERNALDIR/modules 的挂载与文档中"因为 nosuid 挂载标志而不能直接使用原目录"的说法对应:module.rs 中的 setup_module_mount 先把 MODULEROOT(即 /data/adb/modules)bind mount 到 tmpfs 下的 MODULEMNT,再以 MS_RDONLY 重新挂载,确保模块文件(很多带 setuid 位的二进制)能正常生效。而 clean_mounts(mount.rs)在启动流程结束后解除这些挂载。
1.2 /data 中的路径
一些二进制与文件必须存放在非易失存储中。为了防检测,所有东西必须存放在 /data 中一个安全且不可察觉的位置。Magisk 选择了 /data/adb,理由有四条(与 bootstages.rs 中 setup_magisk_env 的目录创建逻辑一致):
- 它是现代 Android 上已经存在的目录,因此它的存在不能作为 Magisk 安装过的证据;
- 该目录默认权限为
700、属主为root,非 root 进程以任何方式都无法进入、读取或写入; - 该目录的 SELinux 标签是
u:object_r:adb_data_file:s0,只有极少数进程有权与该标签交互; - 该目录位于设备加密存储(Device encrypted storage)中,在 FBE(文件级加密)设备上,只要数据分区完成挂载就立即可访问。
/data/adb 下的目录布局:
SECURE_DIR=/data/adb
# Folder storing general post-fs-data scripts
$SECURE_DIR/post-fs-data.d
# Folder storing general late_start service scripts
$SECURE_DIR/service.d
# Magisk modules
$SECURE_DIR/modules
# Magisk modules that are pending for upgrade
# Module files are not safe to be modified when mounted
# Modules installed through the Magisk app will be stored here
# and will be merged into $SECURE_DIR/modules in the next reboot
$SECURE_DIR/modules_update
# Database storing settings and root permissions
MAGISKDB=$SECURE_DIR/magisk.db
# All magisk related binaries, including busybox,
# scripts, and magisk binaries. Used in supporting
# module installation, addon.d, the Magisk app etc.
DATABIN=$SECURE_DIR/magisk
几个值得注意的实现细节:
bootstages.rs中的post_fs_data首先检查SECURE_DIR是否存在;在 SDK < 24 的旧设备上可以自行创建,现代设备上如果缺失则直接报错中止(bootstages.rs);restorecon(selinux.rs)会周期性地把/data/adb重新打回adb_data_file标签、把/data/adb/modules下的文件恢复为system_file标签,并把magisk.db的权限改为000——这些都是为了进一步降低目录被识别的概率;modules_update的设计意图是:通过 Magisk App 安装的模块先落在"待升级"目录中,下次重启时才真正合并进$SECURE_DIR/modules,避免在模块已挂载生效的状态下修改其文件。
二、Magisk 启动流程
Magisk 的启动分为三个阶段,全部由 magiskinit、magiskd 以及注入到 init.rc 的服务驱动。
2.1 Pre-Init:替换 init 成为第一个执行的程序
magiskinit 会替换 init 成为系统启动的第一个程序(对应 init.rs 中的 main 入口:当 getpid() == 1 时执行 MagiskInit::start())。文档列出的 pre-init 职责逐条如下,并在源码中都有对应实现:
- 尽早挂载必要分区。在 legacy system-as-root 设备上切换 root 到 system;在 2SI(two-stage init)设备上,把原始
init打补丁,使其把第二阶段 init 文件的执行重定向到 magiskinit,由后者完成分区挂载。- 源码中 init.rs 的
start()先挂载/proc、/sys,然后根据启动条件分流:selinux_setup参数(2SI)、skip_initramfs(legacy SAR)、force_normal_boot(一阶段 init)、存在 recovery(恢复模式直接放弃并还原)等; - 2SI 的劫持有两种手段:
hijack_init_with_switch_root(利用原 init 的SwitchRoot机制把 magiskinit bind mount 到/system/bin/init,见 twostage.rs)和hexpatch_init_for_second_stage——直接在内存映射的/init中把字符串/system/bin/init替换为/data/magiskinit,若/init不可写则把补丁后的副本 bind mount 覆盖上去(twostage.rs)。
- 源码中 init.rs 的
- 把 magisk 服务注入
init.rc,让后续post-fs-data、late_start等触发点能够拉起 magiskd。 - SELinux 策略加载:对于使用 monolithic policy 的设备,直接从
/sepolicy加载补丁后的策略;否则(2SI 设备)用 FIFO 劫持 selinuxfs 节点,设置LD_PRELOAD钩住security_load_policy,并启动守护进程等待 init 尝试加载 sepolicy 的时机。 - 打补丁 sepolicy 规则。如果使用"劫持"方式,则把补丁后的 sepolicy 加载进内核,解除对 init 的阻塞,daemon 退出。
- 执行原始
init继续启动流程,即init.rs末尾的self.exec_init()。
此外源码中还包含文档未展开的防御性分支:充电模式(boot_mode == "charger")与 recovery 模式下 recovery_or_charger 会立即还原 ramdisk init、放弃加载 Magisk,避免在非常规启动路径中累积 bootloop 计数(init.rs)。
2.2 post-fs-data:/data 解密挂载后触发
这个阶段在 post-fs-data 触发——此时 /data 已解密并挂载。magiskd 守护进程被拉起,post-fs-data 脚本被执行,模块文件被"magic mount"(挂载到目标路径)。
源码 bootstages.rs 中 post_fs_data 的完整流程:
preserve_stub_apk处理应用组件伪装;- 检查
/data/adb是否存在,不存在则(旧设备)创建或报错; setup_magisk_env:把备用位置的二进制(/cache/data_adb/magisk、/data/magisk、应用数据目录下的install)迁移到DATABIN,创建post-fs-data.d、service.d等目录,把 busybox 复制到 tmpfs 并busybox --install -s安装 applet 符号链接,同时把magisk32、magiskpolicy复制到 magisk tmp(bootstages.rs);- 安全模式检测:读取
magisk.db中的BootloopCount,若连续 2 次未启动完成、persist.sys.safemode/ro.sys.safemode为 1、或检测到特定按键组合,则禁用所有模块与 Zygisk 后直接返回,让下一次启动"干净"; exec_common_scripts("post-fs-data")执行/data/adb/post-fs-data.d中的通用脚本,随后handle_modules完成模块的 magic mount。
触发时机上有一个细节:boot_stage_handler 在处理 POST_FS_DATA 请求前会调用 check_data()(bootstages.rs),它检查 /proc/mounts 中 /data 是否已挂载,并结合 ro.crypto.state、init.svc.vold 判断加密状态——这正是文档中"当 /data 被解密并挂载时触发"的实现。
2.3 late_start:service 模式
启动后期 late_start class 被触发,Magisk 的"service 模式"随之启动,执行 service 脚本。对应 bootstages.rs:
fn late_start(&self) {
setup_logfile();
info!("** late_start service mode running");
exec_common_scripts(cstr!("service"));
if let Some(module_list) = self.module_list.get() {
exec_module_scripts(cstr!("service"), module_list);
}
}
即先执行 /data/adb/service.d 中的通用服务脚本,再为每个启用模块执行其 service.sh。boot_stage_handler 用 BootState 位标志保证 post-fs-data → late_start → boot-complete 严格串行且只执行一次(bootstages.rs)。boot-complete 阶段还会重置 bootloop 计数、创建 preinit 目录镜像(setup_preinit_dir)、并保证管理器应用就位。
三、Resetprop:绕过 property_service 的属性修改工具
3.1 背景:为什么 setprop 不够用
系统属性通常只允许 init 更新、非 root 进程只读。持有 root 后可以通过 setprop 向由 init 承载的 property_service 发请求来修改属性,但只读属性(ro. 开头,如 ro.build.product)的修改和删除属性依然被禁止。
3.2 实现原理
resetprop 的做法是从 AOSP 中抽出系统属性相关源码并打补丁,允许直接修改 property 区(prop_area),完全绕开 property_service。从源码看(resetprop/mod.rs),它直接链接了 AOSP 的内部符号:
unsafe extern "C" {
#[link_name = "__system_property_find2"]
fn sys_prop_find(key: CharPtr) -> Option<&'static mut PropInfo>;
#[link_name = "__system_property_update2"]
fn sys_prop_update(info: &mut PropInfo, val: CharPtr, val_len: u32) -> i32;
#[link_name = "__system_property_add2"]
fn sys_prop_add(key: CharPtr, key_len: u32, val: CharPtr, val_len: u32) -> i32;
#[link_name = "__system_property_delete"]
fn sys_prop_delete(key: CharPtr, prune: bool) -> i32;
...
}
由于绕过了 property_service,文档指出两个注意事项,与 resetprop/cli.rs 的实现一一对应:
-
on property:foo=bar动作不会触发。如果属性变更不经过property_service,*.rc脚本中注册的动作就不会被触发。resetprop默认的 set 行为与setprop一致——会触发事件(实现方式是先删除属性、再经property_service重新设置);若需要"静默"修改,可用-n标志跳过property_service。源码中的
set方法(cli.rs)展示了这一逻辑:ro.开头的属性强制走skip_svc(直接修改),且如果属性是 long property 或新值超出PROP_VALUE_MAX,会先SYS_PROP.delete(key, false)删除节点再add回去,因为长属性无法直接通过__system_property_update修改。 -
persist 属性的双份存储。
persist.开头的属性(如persist.sys.usb.config)同时存在于prop_area和/data/property两处。默认情况下,删除属性不会移除持久化存储中的副本(重启后属性会恢复);读取属性也不会从持久化存储读取(这与getprop的行为一致)。使用-p标志后,删除会同时清除prop_area与/data/property中的属性,读取也会同时读取两处。对应源码:
delete中if self.persist && key.starts_with("persist.")时调用persist_delete_prop(cli.rs);get中当内存区读不到且开启-p/-P时回落到persist_get_prop(cli.rs);set中绕过 service 写入 persist 属性时会显式调用persist_set_prop落盘。
3.3 完整命令行参考
resetprop 的完整参数表可直接引用 cli.rs 内置的 usage 文本,这也是工具 --help 的实际输出:
resetprop - System Property Manipulation Tool
Usage: resetprop [flags] [arguments...]
Read mode arguments:
(no arguments) print all properties
NAME get property of NAME
Write mode arguments:
NAME VALUE set property NAME as VALUE
-f,--file FILE load and set properties from FILE
-d,--delete NAME delete property
Wait mode arguments (toggled with -w):
NAME wait until property NAME changes
NAME OLD_VALUE if value of property NAME is not OLD_VALUE, get value
or else wait until property NAME changes
General flags:
-h,--help show this message
-v,--verbose print verbose output to stderr
-w switch to wait mode
-c,--compact compact property area (optional: context label)
Read mode flags:
-p also read persistent properties from storage
-P only read persistent properties from storage
-Z get property context instead of value
Write mode flags:
-n set properties bypassing property_service
-p always write persistent prop changes to storage
各模式要点:
- 读模式:无参数打印全部属性(格式
[key]: [value]),单参数读取指定属性;-Z返回属性的 SELinux 上下文而非值(调用__system_property_get_context); - 写模式:
NAME VALUE设置属性;-f FILE从key=value格式文件批量加载;-d NAME删除属性; - 等待模式:
-w NAME阻塞直到该属性发生变化;-w NAME OLD_VALUE在属性等于OLD_VALUE时等待其变化、不等则直接取值。底层通过__system_property_wait+ 属性序列号(serial)实现(cli.rs); -c/--compact对属性区做压缩整理(__system_property_compact),可选传入 context 标签只压缩指定上下文;- 互斥校验:
-w、-c、-f、-d四种操作模式不能同时出现,否则报错(cli.rs)。
此外,resetprop 在 Magisk 内部也被当作属性读写的基础库使用:set_prop/load_prop_file 固定 skip_svc: true(内部调用一律绕过 property_service),get_prop 对 persist. 前缀自动开启持久化回落(cli.rs)。
四、SELinux 策略补丁
Magisk 会打补丁修改原版 sepolicy,确保 root 与 Magisk 的操作能在安全受控的方式进行。核心设计(常量定义见 consts.rs):
- 新增域
magisk(SEPOL_PROC_DOMAIN,上下文u:r:magisk:s0),事实上是permissive 域——magiskd与所有 root shell 都运行在该域中; - 新增文件类型
magisk_file(SEPOL_FILE_TYPE,上下文u:object_r:magisk_file:s0),被配置为允许所有域访问,即"不受限制的文件上下文"; - 另有一个文档未提及的日志管道类型
magisk_log_file,只允许 root 与 zygote 打开,用于日志输出。
4.1 Android 8.0 之前:放权式模型
所有被允许的 su client 域都可以直接连接 magiskd 并与守护进程建立连接,获取远程 root shell。为此 Magisk 还必须放宽若干 ioctl 操作,让 root shell 能正常工作。
4.2 Android 8.0 之后:magisk_client 沙箱模型
为了减少对 Android 沙箱规则的放宽,8.0 之后部署了新的 SELinux 模型:
magisk二进制的文件类型标签为magisk_exec;- 运行在"允许的 su client 域"中的进程执行
magisk二进制(包括su命令)时,通过type_transition规则转入magisk_client域; - 规则严格限制:只有
magisk域进程才能把文件归属为magisk_exec; - 禁止任何域直接连接
magiskd的 socket,访问守护进程的唯一途径是经过一个magisk_client进程。
这些改动让 Magisk 自身的规则与系统其余策略隔离开,同时保持 Android 沙箱完整。文档指出完整规则集位于 sepolicy/rules.cpp——在当前仓库中对应实现是 sepolicy/rules.rs 的 magisk_rules,其中可以看到上述设计的落地:
// Create unconstrained file type
allow(["domain"], [file],
["file", "dir", "fifo_file", "chr_file", "lnk_file", "sock_file"], all);
// Make our root domain unconstrained
allow([proc], [
"fs_type", "dev_type", "file_type", "domain",
"service_manager_type", "hwservice_manager_type", "vndservice_manager_type",
"port_type", "node_type", "property_type"
], all, all);
// Just in case, make the domain permissive
permissive([proc]);
// Allow us to do any ioctl
allowxperm([proc], ["fs_type", "dev_type", "file_type", "domain"],
["blk_file", "fifo_file", "chr_file"], xall);
几个与文档论述直接呼应的规则:allow(["domain"], [file], ...all) 即 magisk_file 对所有域开放;permissive([proc]) 使 magisk 域事实上无约束;allowxperm 全量放开 ioctl(xall 表示 0x0000-0xFFFF),对应"放宽 ioctl 让 root shell 正常工作";allowxperm([proc], [proc], ["tcp_socket", "udp_socket", "rawip_socket"], xall) 则覆盖网络 socket 操作。
同文件还有一些文档未提及但属于该策略补丁一部分的防御性规则:deny(all, ["kernel"], ["security"], ["load_policy"]) 阻止任何域绕过 Magisk 自行加载策略;deny(["init"], ["adb_data_file"], ["dir"], ["search"]) 与 vendor_init 同理,确保 /data/adb 的标签不被 init 的恢复逻辑干扰(rules.rs)。
五、小结
docs/details.md 所描述的四个部分——tmpfs 目录布局、/data/adb 布局、三阶段启动流程、resetprop 与 SELinux 补丁——构成了 Magisk 在设备上的完整"内部地图":
- 目录层:易失数据(magisk 二进制、applet 符号链接、模块挂载点、分区镜像、rootdir 补丁)在
$MAGISKTMP/.magisk,持久数据(modules、magisk.db、busybox、脚本目录)在/data/adb,全部由 consts.hpp 集中定义; - 启动层:
magiskinit以 PID 1 劫持启动完成分区挂载与策略加载(init.rs),magiskd按post-fs-data(环境初始化 + 模块 magic mount)→late_start(service 脚本)→boot-complete(收尾与安全模式计数重置)严格串行推进(bootstages.rs); - 属性层:
resetprop通过链接 AOSP 内部符号直写prop_area,用-n/-p精细控制事件触发与持久化语义(resetprop/cli.rs); - 策略层:permissive 的
magisk域 + 全开放的magisk_file+magisk_client中转模型,把 root 能力与系统沙箱解耦(sepolicy/rules.rs)。
理解这条链路后,排查模块挂载失败、属性修改不生效、启动卡 safe mode 等问题时,就可以直接定位到对应的启动阶段与源码位置。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00