小米 17 Ultra 掉 Root 修复记|不下载 10.4GB 线刷包,只从里面抠出 5MB
事情是怎么开始的
起因说起来很朴素:更新完系统,忘了处理槽位就顺手重启了。
HyperOS 的 OTA 走的是 A/B 双分区机制——更新的时候,新系统不会覆盖当前正在运行的那个槽,而是先写进另一个空闲的槽位,然后打个标记:下次启动从这个新槽进。重启之后,系统才真正切过去生效。
而这台机器上,root 是挂在 init_boot 分区上的,init_boot 每个槽位各有一份(init_boot_a / init_boot_b),两份互不共享。之前打好的 KernelSU 补丁只活在原来那个槽里;更新时写进新槽的那份 init_boot 是官方原版,干干净净,没有内核模块。
所以 A/B 机型更新系统的正经姿势是:在重启生效之前先处理槽位,把 root 补丁同步到即将启用的那个新槽(SukiSU / KernelSU 里的「安装到未使用的槽位(OTA 后)」就是专门干这事的)。这一步做了,更新完重启,新系统带着 root 一起起来,什么都没发生。
这次就是漏了这一步。更新包下完、进度条走完、系统提示「重启以完成更新」,然后就直接点了重启,压根没想起来槽位这回事。
(顺便说一句,这台机器跑的是内测版本,这一点在后面会变成一个不小的麻烦。)
重启完再打开 SukiSU Ultra,赫然一行字:未安装。root 掉了。

顺手确认一下系统落在哪个槽:
adb shell "getprop ro.boot.slot_suffix"
# _a ← 已经切到新槽了
💡 先说明一件事:这不是变砖。系统一切正常——能开机、能联网、数据全在、版本也没错,只是正在运行的那份 init_boot 里没有 KernelSU 而已。丢的是 root,不是系统。
截图里那串系统指纹(CP2A.260605.016 / OS4.0.0.27.XPACNXM)后面会变成整个修复过程的关键。
root 掉了最难受的地方在于——想修 root,得先有 root。
KernelSU / SukiSU 是把内核模块注入到 init_boot 分区的 ramdisk 里的,所以正确的修复姿势是把手机上原始的那份 init_boot.img 导出来重新打补丁。而导出分区最直接的办法是 dd:
adb shell "dd if=/dev/block/by-name/init_boot_a of=/sdcard/init_boot.img bs=4096"
# dd: /dev/block/by-name/init_boot_a: Permission denied
读块设备需要 root,现在没 root,读不了;读不了就修不了 root。标准的死循环。
所以只剩一条路可走:从官方固件包里把这一版的原版 init_boot 提取出来。
第一步:先确认版本,这是生死线
刷 init_boot 这件事,版本必须和手机上正在跑的系统完全一致。差一个小版本都可能刷完直接卡 fastboot 开不了机,因为 init_boot 里装的就是这一版系统的 ramdisk,和内核、系统镜像对不上就起不来。
所以第一件事不是去下载,而是先把手机上的版本号读准确,一个字都不能靠记忆:
adb shell "getprop ro.product.device"
adb shell "getprop ro.build.display.id"
adb shell "getprop ro.build.version.incremental"
实测结果:
| 项目 | 实测值 |
|---|---|
| 机型 / 代号 | 小米 17 Ultra / nezha |
| 系统版本 | OS4.0.0.27.XPACNXM |
| Android 构建号 | CP2A.260605.016 |
| Android 版本 | 17(SDK 37) |
| 当前槽位 | _a |
| Bootloader | 已解锁 |
这里有个特别容易踩的坑:HyperOS 的 OS4.0.0.27.XPACNXM 和小米的构建号 CP2A.260605.016 是两套编号,指的是同一个版本。找固件的时候两个都要能对上,不然很容易下错包。
第二步:翻了一圈,是个坏消息
按常规思路,去下这个版本的线刷包(fastboot 包,tgz 格式),解压进 images/ 文件夹就能直接拿到原版 init_boot.img,这是最省事的办法。
结果全网翻了个遍,OS4.0.0.27.XPACNXM 的线刷包根本不存在。
原因其实很简单:这台机器跑的是内测版本。
小米的线刷包是跟着正式版 / 稳定版走的,内测版本不进公开渠道,自然也不会发对应的线刷包——在公开镜像站上翻到天荒地老也不会有的。内测版能拿到的,只有通过内测通道推下来的 OTA 卡刷包。
| 版本类型 | 官方是否提供线刷包 |
|---|---|
| 稳定版 / 正式版 | ✅ 有 fastboot 线刷包(.tgz) |
| 内测版 / Beta | ❌ 通常只有 OTA 卡刷包(.zip) |
能翻到的公开线刷包里,最新的只到 OS3.0.309.0.WPACNXM(Android 16 正式版),而手上这台跑的是 Android 17 的内测版——版本号和 Android 大版本都对不上。
⚠️ 随手抓一个
OS3.0.309.0.WPACNXM(Android 16)的 init_boot 往这台机器上刷,那就是标准的"刷完卡 fastboot 变砖"。
所以内测版掉 root,从一开始就注定走不了"下个线刷包、解压完事"这条路——它没有线刷包可用,只能从卡刷包里精确提取。
好消息是,内测版的 OTA 卡刷包和机器上跑的版本是完全一致的:
nezha-ota_full-OS4.0.0.27.XPACNXM-user-17.0-8965156a6f.zip (约 10.4 GB)
要的东西就在里面,只是这个包有点大。
第三步:能偷懒的路,全被堵死了
在老老实实下载 10.4GB 之前,先试了几条捷径:
| 思路 | 结果 |
|---|---|
fastboot fetch init_boot_a 直接从设备导出分区 |
❌ Device does not support fetch command |
| 重启进 fastbootd(用户空间 fastboot)再 fetch | ❌ fetch not supported on user builds |
免 root 直接 dd 读分区 |
❌ Permission denied |
fetch 是 Android 平台专门为"从设备导出分区镜像"设计的命令,理论上不用下整个固件包就能把分区拿出来——可惜小米的 bootloader 压根没实现它。
然后实测了一下这个包的下载速度:150 ~ 300 KB/s。10.4GB 得下十几个小时,不能接受。
那就换个思路。
第四步:只下载真正需要的那几 MB
既然下载慢,那就别下整个包。
两个关键发现:
- 小米的 CDN 支持 HTTP Range,也就是可以只请求文件里的某一段字节;
- 这个 zip 里的
payload.bin是未压缩存储的(compression method = 0)。
第二点才是重点。zip 内的文件如果没压缩,它的字节偏移就是可以直接算出来的,意味着我可以精确定位到包里任意一段数据,只把它拉下来。
整个包的结构大致是这样:
nezha-ota_full-OS4.0.0.27.XPACNXM-xxx.zip (10,430,488,299 bytes)
├── payload.bin @ offset 5149
│ ├── manifest (protobuf) 377,921 bytes ← 分区清单:每个分区的数据块位置 + SHA-256
│ └── data blobs ← 真正的镜像数据(XZ / BZ2 分块)
├── payload_properties.txt @ 10430746828
└── ... 其他 OTA 元数据
所以整个流程是这样的:
① 拉 zip 尾部 256KB → 解析 ZIP 中央目录,定位 payload.bin 偏移与大小
② 拉 payload 头部 3MB → 拿到 protobuf manifest
③ 解析 manifest → 找到 init_boot 的 4 个数据块(2×XZ + 2×BZ2,共约 2MB)
④ 按块拉取这 2MB → 每一块用 manifest 里的 SHA-256 立刻校验
⑤ 解压 + 按序拼接 → 得到完整的 init_boot.img
第 ④ 步是整个过程里最舒服的地方:manifest 给每个数据块都附了 SHA-256,所以拉回来的每一块都能当场验证真伪,不用担心网络传输出错。
最后 4 个块 4/4 全部 SHA-256 匹配,重组出来的镜像:
| 校验项 | 结果 |
|---|---|
| 文件大小 | 8,388,608 字节(正好 8 MiB) |
| magic | ANDROID! ✅ |
| kernel_size / ramdisk_size | 0 / 2,588,280 |
| header_version | 4 |
这里 kernel_size = 0 这个细节要单独说一下:它是 GKI 机型的典型特征——内核在 boot 分区里,ramdisk 在 init_boot 分区里。所以这类机器修 root 只能修补 init_boot,绝对不能碰 boot.img(网上很多教程还在教人修补 boot,刷进去直接黑屏)。
整个过程累计只从 CDN 拉取了不到 7MB 数据,对比 10.4GB 的完整包,省掉了 99.9% 的流量。
第五步:用 SukiSU Ultra 打补丁
把提取出来的原版镜像推回手机:
adb push init_boot_stock.img /sdcard/Download/
然后打开 SukiSU Ultra → 点【未安装】→【安装】:

注意看,SukiSU 自己在这里就写了「建议选择 init_boot 分区镜像」,跟前面对 GKI 的判断完全一致。
点【选择一个文件】,选中刚推过去的 init_boot_stock.img,下一步会弹出 KMI 选择框:

KMI(Kernel Module Interface)必须选当前设备的版本,这里自动选中的是 android16-6.12,对应本机内核 6.12.69-android16-...。列表里从 android12-5.10 一路排下来,选错任何一个,模块都加载不起来。
确认之后就是自动修补:

日志里能清楚看到它做了什么:
- Unpacking boot image
- KMI: android16-6.12
- Adding KernelSU LKM
- Removing spoof_release config
- Removing spoof_version config
- Repacking boot image
- Output file is written to
- /storage/emulated/0/Download/kernelsu_patched_20260918_020239.img
- Done!
把修补前后的镜像拉回电脑对比一下:
| 项目 | 原版 | 修补后 |
|---|---|---|
| 文件大小 | 8,388,608 | 8,388,608 |
| ramdisk_size | 2,588,280 | 2,974,909(+386 KB) |
ksud / kernelsu 字符串 |
0 处 | 4 处 |
ramdisk 变大了一圈、多出 KernelSU 相关字符串——说明 LKM 确实被注入进去了。
第六步:刷入并验证
adb reboot bootloader
fastboot flash init_boot kernelsu_patched.img
fastboot reboot
刷入返回 OKAY,重启后系统版本没有任何变化(仍是 OS4.0.0.27.XPACNXM),内核模块也正常挂载了:
$ adb shell cat /proc/modules | grep -i ksu
kernelsu 200704 1 - Live
打开 SukiSU Ultra,主页显示 工作中 <LKM>:

超级用户、模块列表都回来了,LSPosed 和 Zygisk 也恢复正常。root 到手。
中途踩的两个坑
坑一:HyperOS 把 adb shell input 封死了
SukiSU 那几步点击(点卡片 → 选文件 → 选 KMI → 确认)本来想用 adb shell input tap 全自动跑完,结果:
SecurityException: Injecting to another application requires
INJECT_EVENTS permission
tap、keyevent 全部被拒,连 settings put、pm grant 也一样被拦——这是 HyperOS 的安全策略,必须在开发者选项里打开「USB 调试(安全设置)」才会放开模拟点击。
绕过办法是 monkey 脚本注入:input 被拦,但 monkey -f 允许执行事件脚本文件。
type= raw events
count= 2
speed= 1.0
start data >>-
DispatchPointer(0,0,0,600,620,1,1,0,1,1,0,0)
UserWait(120)
DispatchPointer(0,0,1,600,620,1,1,0,1,1,0,0)
有个很容易忽略的细节:按下和抬起之间必须加 UserWait。按下时长为 0 的点击在 Compose 写的界面上是不生效的,一开始怎么点都没反应就是这个原因。
坑二:MSYS 的路径转换会搞坏传给 adb / curl 的参数
在 Windows 的 Git Bash 里调用 adb.exe、curl.exe 时,/sdcard/Download/xxx 这类参数会被 MSYS 自作主张转换成本地路径(变成 C:/Program Files/Git/sdcard/...),表现就是 push 莫名其妙失败、curl 报 client returned ERROR on write。
后者尤其坑——报错信息看起来像网络问题,实际上是本地写入路径被改坏了,白白排查了半天。解决办法是用 Windows 风格路径,或者设 MSYS2_ARG_CONV_EXCL='*' 直接关掉转换。
回滚姿势
原版镜像一定要留好,而且建议存两份(电脑一份、手机一份):
# 万一以后出问题,刷回原版即可
fastboot flash init_boot init_boot_stock.img
fastboot reboot
刷回官方原版 init_boot 之后,root 消失但系统完全正常,可以重来一遍。
这份原版镜像对内测版尤其重要——内测版本的包不进公开渠道,过了推送窗口就更难找,线刷包又压根没有。这 8MB 的东西某种意义上比系统本身还金贵,建议电脑和手机各存一份——只放手机里的话,万一哪天手机进不去系统,就真拿不出来了。
下次怎么避免再掉一次
这次的坑完全可以绕开,核心就一句话:OTA 和 root 的处理顺序不能颠倒。
| 时机 | 该做的事 |
|---|---|
| 更新包下载完、还没重启 | 打开 SukiSU →【安装】→ 选「安装到未使用的槽位(OTA 后)」,把补丁预写进那个还没启用的槽 |
| 重启之后 | 什么都不用管,root 跟着新系统一起起来 |
SukiSU 的安装界面里本来就摆着这个选项,只是平时用不上,很容易被忽略。另外还有三条保险:
- 别把「重启以完成更新」当成随手一点的按钮。看到这句提示先停三秒,想一下槽位处理了没有——系统更新提示是不会替你考虑 root 的。
- 两个镜像都留着:原版
init_boot_stock.img(出事能刷回去)+ 修补好的kernelsu_patched.img(同版本直接重刷一次就恢复 root)。只要系统版本不变,这两个文件随时能救命。 - 跑内测版的,更要把这一步做扎实:内测版没有公开线刷包兜底,一旦掉了 root,只能像本文这样从 OTA 卡刷包里一点点抠出来,费时费力。正式版掉 root 叫"麻烦",内测版掉 root 叫"真麻烦"。
如果已经掉到无 root 状态了,那就只能回到本文开头,把「提取原版镜像 → SukiSU 打补丁 → fastboot 刷入」这套流程重走一遍。
最后
回头看,这次翻车其实分成两段:掉 root 是更新时漏了槽位那一步,修 root 则卡在版本匹配上。
- 预防靠顺序——A/B 机型 OTA 之前把补丁装进「未使用的槽位」,root 根本不会掉,这一步花不了一分钟;
- 修复靠版本——修 init_boot 之前,版本必须和手机上正在运行的系统一模一样,这是唯一的硬约束。跑内测版的话还要多一层认知:官方不会给内测版发线刷包,别在镜像站上白费功夫,直接从 OTA 卡刷包里提取才是正路。如果当时图省事抓了 Android 16 的线刷包,刷进去就是卡 fastboot;
- 省流量靠巧劲——遇到包太大、下载太慢,不一定非得全量下载。HTTP Range + zip 未压缩存储这组合,是可以直接从远端把需要的那几 MB "抠"出来的。
顺带提醒一句:手里如果存着别的版本的 init_boot.img(比如从搞机工具箱里翻出来的旧包),千万别拿来混用。这次就翻出一个内嵌 OS4.0.0.24.XPACN 的 boot.img,看着像这个机器的,实际是上一个版本的——刷下去就是变砖。