Android 17 AOSP 安全补丁移植:从CVE合并到AVB/OTA全链路 每次安全公告发布的那一周基本就是我一周里最忙的时候。盯着补丁列表把一条条 commit 从上游摘下来git cherry-pick一遍遍冲突frameworks/base里改到怀疑人生编译过了开机卡在第一屏串口一开发现是vbmeta的 rollback index 忘了加一。这类活儿干久了会形成一个很朴素的认知Android 17 AOSP 安全补丁移植这件事技术难度不在“会不会打补丁”而在“打完之后系统还能不能正常开机、还能不能通过自校验、还能不能持续维护下去”。尤其是那些不预装闭源服务套件的安全加固发行版——下文我统一用“G 系统”代指这一类以 AOSP 为基座、自己维护更新通道的发行版——它们没有官方那条现成的补丁下发链路所有事情都得自己扛。这篇东西写给两类人看一类是想自己从 AOSP 拉一套代码、跟着月度安全公告做补丁合并的 ROM 维护者另一类是好奇“没有闭源服务套件撑着安全性凭什么不塌”的技术读者。我会把补丁分类、冲突解决、SELinux 策略合并、内核回移、AVB 签名与 OTA 通道这一整条链路拆开讲中间穿插我实际踩过的坑和几段可以直接抄的命令。基础门槛是你要会用repo、看得懂git冲突标记、知道lunch之后m是什么意思。至于更深的东西比如 TEE 里的密钥怎么用我会点到为止不展开。1. 先把安全模型想明白没有闭源服务套件缺的到底是什么1.1 安全补丁的两条来源线别混在一起处理很多人一开始会把“打安全补丁”理解成一件单一的事实际上它至少是两条独立的线。第一条是上游月度安全公告对应的 CVE 补丁公告里会按组件列出受影响的 CVE 编号、严重等级、以及对应的 AOSP 提交。这条线是硬指标是必须跟的。第二条是上游主干和季度分支的常规合并里面大量是防御性改动、内存安全修复、边界检查加固它们不一定有 CVE 编号但对实际攻击面的收敛作用往往比某些 CVE 还大。我早期只跟第一条线结果吃了亏某次一个没有 CVE 编号的binder边界检查改动没合进来后面的一个利用链正好从那里切入。从那以后我的做法是每个季度做一次主干合并的“大扫除”把那些Security、Harden、Fix OOB、Add bounds check关键字的提交筛一遍人工过一遍再决定合不合。这个工作量不小但比事后救火划算得多。注意季度合并不要无脑全量 rebase。上游主干里有大量跟安全无关的重构全量合进来会把你自己那套硬化补丁冲得七零八落回滚成本极高。除了这两条还有第三条容易被忽略的线第三方依赖组件。boringssl、libpng、freetype、harfbuzz、sqlite、skia、各类媒体解码库这些在上游是独立维护的安全公告里也常常单独列。这类组件的特点是版本升级相对独立冲突少但一旦漏掉影响面极大——尤其是媒体解码那一块历史上被利用的次数非常多。1.2 闭源服务套件撤掉之后哪些能力必须自己补上这是整个话题里最容易被外行误解的地方。很多人以为不预装闭源服务套件就等于“少了功能”实际上真正麻烦的是少了一整套配套的安全基础设施。我把它拆成四块来看。第一块是更新通道。官方那套 OTA 下发链路用不了就得自建。自建不是搭个 HTTP 服务器就完事你要解决签名密钥怎么保管、增量 OTA 怎么生成、回滚保护怎么做、灰度怎么控制、用户设备怎么知道有新版本。这一整套东西做不扎实补丁打得再快也到不了用户手上。第二块是应用完整性校验。有些应用会调用系统提供的完整性接口来判断运行环境是否可信。没有那套服务之后这类判断要么失效、要么被拒。G 系统的做法是走硬件级密钥证明利用 TEE 或 StrongBox 里的密钥对一段挑战值签名服务端用预置的公钥验签。这条路技术上可行但要求应用侧配合改造不是所有第三方应用都愿意做。第三块是密钥库与锁屏保护。这块 AOSP 本身就有但要确保硬件支持的密钥库正常工作、锁屏之后的密钥派生链路完整、强身份认证PIN/密码不含生物识别能正确参与密钥派生。这块一旦配置错表面上一切正常实际上密钥保护强度掉了一个档次。第四块是漏洞响应流程。没有厂商在背后兜底就得自己定 SLA公告发布后多久内完成补丁移植、多久内完成回归、多久内推送。我给自己定的目标是公告发布后 72 小时内完成框架层补丁合并、7 天内出可刷镜像、14 天内完成推送。这个节奏在个人或小团队维护的场景下已经是极限了。1.3 为什么是“跟随上游 自建硬化层”而不是自己攒一套选型这个问题我认真纠结过。理论上可以完全脱离上游自己维护一套独立分支想怎么改怎么改。实际做下来会发现这条路走不通原因很直接上游每个月的补丁量在几百到上千条提交量级你自己维护一套分支三个月之后就跟不上节奏了等到半年后再想合冲突多到你只能重开。所以我采用的是三层结构。最底层是上游的 tag比如android-17.0.0_rX这种发布标签作为基线只做只读引用不在上面提交。中间层是发行版自身的改动层通过local_manifest引入一批 overlay 仓库把权限管理、网络控制、传感器隔离这类改动隔离在独立仓库里。最上层是硬化补丁层专门放那些上游还没有、或者永远不会合的自研加固改动单独一个仓库按主题分子目录。这个结构最大的好处是可抛弃。上游哪天把某个硬化改动合进去了我直接把对应的上层的补丁删掉中间层完全不受影响。反过来如果我要放弃某个硬化补丁也只需要动最上层不用去动基线。我在实际维护中发现这套结构能撑住两三年不崩关键就在于层与层之间的边界足够清晰。另一个选型细节是仓库布局。我用的是repo的local_manifest机制把自定义仓库以remove-project加project的方式覆盖上去。这样repo sync的时候上游仓库会被拉到一个独立路径自定义仓库覆盖到实际编译路径两边的 git 历史互不干扰。这个设计听起来简单但很多自建 ROM 的人一开始图省事直接在repo里改改到后面每次同步都要先 stash 一堆东西非常痛苦。2. 补丁分类与关键细节为什么有的补丁五分钟搞定有的能耗掉一整天2.1 四类补丁处理策略完全不同我习惯把每个月的补丁先分类再决定处理顺序。分类标准是“补丁与原代码的耦合度”耦合度越低越快。整理成表大致是这样类型典型位置冲突概率处理策略预估耗时A 类纯逻辑修复system/、libcore/、第三方库低直接 cherry-pick冲突则看上下文5-15 分钟B 类框架层改动frameworks/base、frameworks/av高先定位上游重构点再手工回移30 分钟-2 小时C 类策略与权限模型system/sepolicy、权限管理中高需要同步调整自定义策略1-3 小时D 类内核与驱动内核树、HAL极高需评估硬件兼容性可能放弃半天到数天A 类最好处理基本是git cherry-pick一把过。但我发现很多人会在这类补丁上犯错因为不冲突就直接合了没看这个补丁是否依赖同一批次里的前置提交。上游的补丁有时候是一串单独摘中间一条编译能过但逻辑是断的。我的做法是摘之前先git log --oneline看这一串提交的顺序成组摘。B 类是最耗时的。frameworks/base是各方势力交汇的地方上游在改、发行版自己在改、厂商也在改三方的改动叠在一起冲突解决就不是简单的“取哪边”的问题而是要理解两边的意图之后再合成第三种写法。C 类和 D 类后面单独说。2.2frameworks/base的冲突为什么最难缠先说结论难缠的根本原因不是代码量大而是语义冲突多于文本冲突。文本冲突 git 会标出来你能看到语义冲突 git 不管编译能过、测试能过运行时行为悄悄变了。我遇到过一个典型例子。上游某个补丁在PackageManagerService里加了一处权限检查位置在某个方法的入口。而我们自己的改动层在这个方法里做了一件相关的事把权限来源从“包名”改成“UID 包名”组合。两边的代码语法上都对合完之后编译没问题但只要涉及共享 UID 的场景权限判断就会走错分支。这个 bug 是测试阶段发现的排查花了大半天。从那以后我形成了一套处理frameworks/base冲突的固定流程先用git log --oneline 基线tag..上游tag -- 冲突文件把这一时间段内该文件的所有改动过一遍判断上游是否做过结构性重构。再用git log -S关键标识符 --oneline找某个符号是在哪次提交里引入或删除的避免自己手写一段上游已经有了的代码。冲突解决完之后用git diff跟两边分别对比一次确认没有丢掉任何一边的意图。最后给这次合并写一条清清楚楚的提交信息说明“合并了什么、丢失了什么、为什么这样取舍”。这条信息三个月后自己看会救命。提示git rerere这个功能在处理反复出现的同类冲突时非常省时间开启之后 git 会记住你解决过的冲突块下次遇到同样的块自动套用。还有一类冲突来自Android.bp和AndroidManifest.xml。前者是构建配置后者是组件声明这两个文件的冲突往往表现为“两边都加了东西”解决方式是把两边都保留但要检查顺序和依赖关系。Android.bp里的srcs列表如果两边都追加了文件合完之后要确认这些文件真的存在否则编译报错会很难定位。2.3 SELinux 策略与neverallow这道隐形门槛这部分是最容易让新手卡住的地方代码全合完了编译到最后一步neverallow检查失败报一堆看不懂的规则名。neverallow是编译期的一道强制校验它的作用是阻止某些“绝对不允许出现”的访问关系被写进策略文件。你新增一个系统服务如果这个服务的 domain 访问了不该访问的 typeneverallow就会在编译时把构建打断。这个机制设计的初衷是防止策略被慢慢放宽到失去意义但对做移植的人来说它就是一道道坎。处理这类问题的关键是不要用audit2allow直接生成规则。audit2allow能从审计日志里反推出“要放行这条访问需要什么规则”非常方便但它生成的是最宽松的写法等于把策略撕开一个口子。我见过不少自建 ROM 直接在编译时关掉neverallow检查SELINUX_IGNORE_NEVERALLOWS : true这做法等于把整套类型强制模型废掉了非常危险。正确的做法是回到策略设计本身。新增的服务应该有自己的 domain通过type声明定义通过attribute加入合适的属性组然后写精确到具体class和perm的allow规则。我一般会这样组织# 自定义服务的 domain 定义 type my_hardened_service, domain; type my_hardened_service_exec, exec_type, file_type, vendor_file_type; init_daemon_domain(my_hardened_service) # 精确放行只允许读特定属性目录下的文件 allow my_hardened_service system_file:dir r_dir_perms; allow my_hardened_service my_config_file:file r_file_perms; # 明确禁止网络访问如果这个服务不需要网络 neverallow my_hardened_service self:global_capability_class_set net_admin;最后那条neverallow是我自己加的属于“给自己上枷锁”的写法。加它的原因是策略这东西一旦放宽就很难收回来与其事后审计不如在编译期就把边界焊死。这个习惯是从一次事故之后养成的——某个服务不该有网络权限但因为早期图省事放行了后面一直没人回头清理。另一个常见坑是策略的分层。AOSP 的策略分system/sepolicy和device/厂商/sepolicy两部分前者是公共策略后者是设备特定策略。你在公共策略里加的规则会影响所有设备在设备策略里加的只影响当前设备。移植补丁的时候如果上游改的是公共策略而你的改动在设备策略里两边不会直接冲突但合起来可能产生矛盾。这种情况编译不会报错运行时表现为某个操作被拒绝日志里一条avc: denied。2.4 内核与供应商 blob现实中的妥协点内核这一层的补丁移植坦白说是整套流程里最没有技术美感的部分因为大量问题不是技术问题而是你拿不到源码。上游的内核补丁走的是两条路径一条是长期支持分支的稳定更新一条是 Android 通用内核分支的定期合并。理论上跟着这两条线走就行实际问题是设备厂商的内核树往往基于一个更老的基线而且加了大量私有驱动这些驱动的代码不在你的手上。我的处理策略分三档能拿到源码的部分按 CVE 回移。回移的方法是从长期支持分支里git log --grep找对应提交然后git cherry-pick -x冲突了手工改。-x这个参数很重要它会把原始提交哈希记在提交信息里方便日后追溯。只能拿到二进制模块的部分评估风险等级。如果是高危 CVE 且影响面大考虑禁用对应功能比如某个有问题的文件系统驱动直接在配置里关掉。完全无法处理的部分在更新说明里如实写清楚。这一点我觉得比技术本身更重要——用户有权知道自己的设备在哪个环节存在已知风险。供应商 blob 的问题类似。HAL 接口在 Android 17 这一代已经有比较严格的版本管理但 blob 本身是二进制你没法审计它做了什么。能做的是用 SELinux 把它关在笼子里给它最小的 domain、最少的文件访问权限、明确的属性组让它就算想干坏事也干不成。这套思路在业界叫“最小权限隔离”原理不复杂但要针对每个 blob 单独调策略工作量很大。还有一个绕不开的现实是内核模块化。新一代的通用内核镜像把很多驱动拆成了可加载模块这本来是好事升级内核不用动驱动。但模块和内核的接口是有兼容性约束的内核升上去、模块没跟上轻则某个硬件不工作重则开机就挂。我在实际维护中的做法是内核镜像和模块始终成对发布绝不单独升级其中一个。3. 实操从公告列表到一台能开机的设备3.1 环境与仓库布局先把硬件和系统环境说清楚这块省不得。我的构建机是 32 核 / 64GB 内存 / 2TB NVMe编译一次全量大约 40 分钟带 ccache 命中能压到 15 分钟以内。如果你只有 16GB 内存全量编译会非常痛苦建议配置足够的 swap并且用-j参数控制并发数一般设成核数的 1 到 1.5 倍比较稳。磁盘空间是个容易低估的点。一套 Android 17 的代码加上out目录轻松超过 300GB如果要保留多个版本的out用于对比建议直接上 1TB 以上。我曾经因为磁盘满了导致编译中途失败报的错是“无法写入某个 .o 文件”排查了半天才反应过来。初始化仓库的流程大致是这样mkdir g-hardened cd g-hardened repo init -u https://镜像地址/platform/manifest -b android-17.0.0_r1 --partial-clone --clone-filterblob:limit10M repo sync -j16 --force-sync --no-clone-bundle--partial-clone这个参数我强烈建议加上。AOSP 的完整历史非常庞大按需拉取 blob 能把首次同步时间从上小时压到十几分钟。代价是后续做git log查历史的时候会触发按需下载稍微慢一点但完全值得。然后是local_manifest的配置把自定义仓库覆盖上去!-- .repo/local_manifests/hardened.xml -- manifest remote nameself fetchhttps://自建地址/ / remove-project nameplatform/frameworks/base / project namehardened/frameworks_base pathframeworks/base remoteself revisionhardened-17 / project namehardened/sepolicy pathdevice/hardened/sepolicy remoteself revisionhardened-17 / project namehardened/patchset pathhardened/patchset remoteself revisionmain / /manifesthardened/patchset这个仓库是我单独留的用来放那些还没决定要不要合入的补丁以及每次合并的记录。它的存在让整个流程可追溯任何一个月的补丁合并都能在这个仓库里找到对应的分支和说明。3.2 逐月补丁流水线把手工活变成半自动纯手工摘补丁一个月做一次还行连续做十二个月一定会出错。我的做法是写一个小脚本把流程里机械的部分自动化判断的部分留给人。第一步是把公告里的 CVE 列表整理成结构化数据。公告本身是表格手抄到 CSV 里字段包括 CVE 编号、严重等级、受影响组件、参考链接。这个 CSV 是整个流程的输入。import csv import subprocess import sys def find_commits(cve, repo_path, since_tag, until_tag): 在上游区间内按 CVE 编号搜索提交 cmd [ git, -C, repo_path, log, f{since_tag}..{until_tag}, --oneline, --no-merges, f--grep{cve}, -i, ] out subprocess.run(cmd, capture_outputTrue, textTrue).stdout return [line.split(maxsplit1) for line in out.strip().splitlines()] def classify(component): if component.startswith(Framework): return B if component.startswith(System): return C if component.startswith(Kernel): return D return A with open(asb.csv, newline, encodingutf-8) as f: rows list(csv.DictReader(f)) for row in rows: cve row[cve] kind classify(row[component]) print(f[{kind}] {cve} - {row[component]})这个脚本本身不复杂价值在于它把“找提交”这一步从人工翻日志变成了机器跑。跑完之后输出的列表我会人工过一遍重点看三件事有没有漏、有没有依赖关系、有没有跨仓库的关联补丁。第二步是批量摘取。这里有个技巧先在临时分支上操作成功后再合并到主分支。直接在主分支上cherry-pick一旦中途发现搞错了回滚很麻烦。git -C frameworks/base checkout -b patch-batch-2025-11 origin/hardened-17 for h in $(cat /tmp/commits.txt); do git -C frameworks/base cherry-pick -x $h || { echo CONFLICT: $h /tmp/conflicts.txt git -C frameworks/base cherry-pick --abort } done冲突的提交会被记下来单独处理。我个人的经验是一批补丁里大约 20% 会冲突其中又有一半是“上游改了同一段代码但意图不冲突”这种手工改一下就好剩下的一半才是真正需要动脑子的。3.3 冲突解决的三板斧具体到操作层面我常用的手法有三个按使用频率排序。第一板斧找回引入点。当你不确定某段冲突代码是哪来的用git log -S加上关键字符串能找到引入它的提交。比如git -C frameworks/base log -SisPermissionGranted --oneline -- src/com/android/server/pm/这条命令会列出所有“增加或删除过isPermissionGranted这个标识符”的提交。找到引入点之后git show看一眼当时的上下文往往就能明白这段逻辑的意图冲突也就好解决了。第二板斧两边都要。大部分冲突其实是两边在同一个位置各自加了东西最安全的解决方式是把两边都保留然后调整语法。比如上游加了一处检查我们加了一处日志那就先检查再打日志。这种处理方式看起来笨但它保证了不丢失任何一方的意图。第三板斧推迟。有些补丁实在合不进去不硬合。把它标记成“延迟”记录在案下个月再试。上游经常在下个月会把相关代码重构掉到时候可能就很好合了。我现在的待办里长期挂着三五个这样的补丁最长的挂了一年多最后上游重构之后一条cherry-pick就过了。提示每解决完一个冲突立刻git add然后git cherry-pick --continue不要攒着。攒着的结果是后面忘了哪个文件改到哪一步只能从头再来。3.4 编译、签名与自校验启动代码合完之后就进入编译环节。日常调试用source build/envsetup.sh lunch hardened_device-user m -j24出可发布镜像用m -j24 target-files-package otatoolstarget-files-package产出的是中间产物后续签名、生成 OTA 包都基于它。这一步比日常编译慢不少因为要做完整的打包和校验。签名是整个流程里最需要小心的环节。自建通道意味着你得自己生成并保管签名密钥这套密钥一旦丢了现有用户就再也收不到更新了因为新包的签名跟旧包对不上系统会拒绝安装。我的做法是密钥离线保存在加密介质里构建机上只放加密后的副本构建时临时解密用完立刻清理。自校验启动Verified Boot这块需要单独配。它的工作原理是系统启动时由引导程序校验下一个分区的哈希逐级往上最终由一个根信任锚点保证整条链没被篡改。自建发行版需要替换掉默认的验证密钥换成自己的。配置上主要涉及这几个变量BOARD_AVB_ENABLE : true BOARD_AVB_ALGORITHM : SHA256_RSA4096 BOARD_AVB_KEY_PATH : vendor/hardened/security/avb.pem BOARD_AVB_ROLLBACK_INDEX : 12 BOARD_AVB_ROLLBACK_INDEX_LOCATION : 1ROLLBACK_INDEX这个值是最容易出错的地方。它的作用是防止降级攻击攻击者把一个存在已知漏洞的旧版本镜像刷回去绕过你后来打的补丁。系统会记住见过的最大的 index 值如果新镜像的 index 比记录的小直接拒绝启动。所以每发布一个版本这个值必须加一。我吃过一次亏连着两个版本忘了改第二个版本推下去之后设备直接启动失败只能让测试机重刷。如果想手动验证一下生成出来的镜像可以用avbtool info_image --image out/dist/vbmeta.img输出里会列出各个分区的摘要、算法、rollback index。每次发版前我都会跑一遍确认 index 确实加了一。3.5 OTA 通道与回滚保护OTA 包分全量和增量两种。全量包简单粗暴几百兆到一两个 G用户下载完直接刷增量包只有几十兆但要求目标设备必须处于某个确切的旧版本。我的策略是两者都出增量包走日常推送全量包作为兜底增量失败的时候自动降级。生成增量 OTA 大致是这样ota_from_target_files \ -i out/prev/target_files.zip \ out/current/target_files.zip \ out/ota_incremental.zip全量包去掉-i参数即可。生成之后要做签名签名用的密钥必须和系统镜像的签名密钥一致否则设备会拒绝。推送通道这边除了常规的 HTTPS 下载我还加了一个元数据签名的环节每个 OTA 包对应一个描述文件里面写着版本号、哈希、rollback index、适用设备列表这个描述文件用另一把密钥签名。客户端先验描述文件的签名再验包的哈希两级校验都过了才开始下载。这个设计防的是“中间人替换更新包”这类攻击属于自建通道必须补上的一环。灰度发布我做得比较土按设备标识的哈希取模分 5%、20%、50%、100% 四批推。每批之间观察 48 小时重点看崩溃率和开机失败率。这套机制没有多先进但足够用而且出问题的时候影响面可控。4. 常见问题与排查实录那些文档里不会写的东西4.1 编译期问题速查表编译阶段的报错相对好处理因为信息明确。我把高频问题整理成了表报错特征根本原因处理方式cannot find symbol上游 API 签名变更调用方未同步用git log -S找变更提交按新签名改调用方neverallow规则冲突新代码引入了策略未覆盖的访问补精确的allow禁止用SELINUX_IGNORE_NEVERALLOWSAndroid.bp缺少依赖新增源文件未声明依赖模块查soong报错的具体模块名补static_libs或shared_libsduplicate class两个仓库提供了同名类检查 overlay 是否重复引入或上游已合并同类功能undefined reference链接期符号缺失常见于 C ABI 变更检查vendor侧预编译库是否与当前 ABI 匹配编译到 99% 卡住通常是 dex2oat 或资源打包阶段内存不足降低并发数或给构建机加内存这里面我最想强调的是neverallow那一行。网上大量的教程教人加SELINUX_IGNORE_NEVERALLOWS : true一了百了这个做法在我看来是自毁城墙。neverallow存在的意义就是防止策略被无意中放宽你把它关了等于放弃了这个保护。正确的做法永远是把策略写精确。还有一个隐蔽的编译问题是缓存污染。ccache在跨版本切换的时候偶尔会把旧的目标文件当成命中导致编译过了但运行行为不对。我遇到过最诡异的一次是同一个提交在两台机器上编译出来行为不一致折腾了很久才发现是缓存的问题。现在我的习惯是每次切换基线 tag 之后先执行一次ccache -C清空缓存虽然会慢一次但省掉很多玄学问题。4.2 运行期与开机问题从卡第一屏到莫名重启运行期的问题排查难度高一个数量级因为可观测信息少。我按现象分几类说。卡在第一屏厂商 logo 之前基本可以确定是引导阶段的问题镜像签名不对、vbmeta校验失败、或者内核根本没起来。排查手段是接串口看早期日志如果没有串口就看引导程序有没有提供 recovery 模式或者 fastboot 的日志输出。我遇到过最常见的原因是ROLLBACK_INDEX忘加系统记住了更大的值新镜像被拒。卡在开机动画说明内核起来了init也跑了问题出在system_server启动过程中。这时候logcat可能还没起来需要看dmesg和init的日志重点找avc: denied和FATAL EXCEPTION。avc拒绝是最常见的原因某个系统服务被策略挡住了起不来连带整个system_server崩溃。# 从设备上抓最近一轮的 SELinux 拒绝记录 adb shell dmesg | grep -i avc: denied # 或者用 audit 日志 adb shell cat /sys/fs/selinux/policy /dev/null # 确认策略已加载拿到拒绝记录之后不要直接用audit2allow生成规则先看这个访问本身合不合理。如果是一个自定义服务试图读取用户数据目录那说明服务配置有问题应该改服务而不是放宽策略。如果是正常的、必要的访问再补精确规则。开机后某硬件不工作比如相机打不开、蓝牙搜不到设备通常是 HAL 接口不匹配。上游升级了 HAL 接口版本你的 blob 还是旧版调用就走不通。查的方法是对比dumpsys输出和 HAL 服务的注册情况adb shell dumpsys -l | grep -i camera adb shell service list | grep -i bluetooth如果服务压根没注册说明 HAL 模块加载失败去看dmesg里模块加载的记录。这类问题往往没有优雅的解法要么升级 blob要么回退对应的 HAL 接口改动。随机重启或应用闪退这类问题最难因为它不可复现。我的做法是先区分是内核崩溃还是用户态崩溃内核崩溃会有kernel panic记录用户态崩溃会有 tombstone 文件。前者重点看内核补丁有没有引入回归后者重点看框架层改动。4.3 几条用血换来的维护经验第一条永远保留上一个能正常开机的版本的可刷镜像。这一条听起来是废话但我真的见过有人把唯一一份能用的镜像覆盖掉然后新版本刷挂了设备变砖。我现在的做法是每次发版保留最近三个版本的完整镜像加上对应的签名材料单独存放。第二条补丁合并和功能改动不要放在同一批发布。早期我图省事一次发布里既合安全补丁又加新功能结果出了问题完全没法判断是哪个改动引起的。现在的规矩是安全补丁批次只合安全补丁功能改动走下一个批次中间至少隔一次完整回归。第三条给每次合并写清楚“为什么放弃”。有些补丁合不进去我会在记录里写明白原因上游重构导致无法回移、依赖未合并的前置提交、与自定义改动语义冲突。这比写“已合入”重要得多因为三个月后你会需要这个信息来判断风险。第四条不要迷信自动化。脚本能做的是找提交、批量摘取、跑编译但“这个补丁合进来会不会影响现有行为”这个判断目前没有工具能替你做。我的流水线里凡是涉及frameworks/base和策略层的改动都必须人工过一遍 diff哪怕只有两行。第五条关注上游的重构节奏。上游每年会做几次大的结构性调整比如某个服务从 Java 迁到 Kotlin、某个模块从系统进程拆出去独立。这些调整期是补丁移植最痛苦的时候冲突率会突然飙高。我的应对策略是在调整期主动放缓合并节奏等上游稳定一两个版本再动手反而更省时间。5. 影响范围这套做法到底改变了什么把视角拉远一点看这类不预装闭源服务套件的加固发行版对整个生态的影响其实分三层。最直接的一层是用户侧。用户拿到的是更新的补丁、更严格的权限控制、更少的后台行为代价是部分依赖闭源服务套件的应用可能用不了或者功能受限。这个取舍是否值得取决于用户自己。但从技术角度看它至少证明了“不依赖那套服务安全基线也可以维持”这件事是可行的。中间一层是开发者侧。当越来越多的用户跑在没有闭源服务套件的环境里应用开发者就必须考虑兼容问题。完整性校验要提供替代方案、推送要支持自建通道、定位要用系统 API 而不是厂商服务。这其实是一种健康的压力逼着应用减少对单一服务的依赖。最外层是上游生态。这类发行版经常会把自己发现的漏洞、写的加固补丁反哺回上游。有些改动上游当时不收过两年又收了。这种“先走一步、再回头拉一把”的模式对整体安全水位是有推动作用的。我在实际参与的过程中深有体会一个看起来很小的问题从发现到上游采纳周期经常以年计但一旦采纳受益的是所有设备。至于维护成本这是绕不开的现实问题。以我的情况每个月投入在补丁移植和回归测试上的时间大约 40 到 60 小时遇到上游重构的月份会翻倍。设备型号越多成本越高——每多一个型号就多一套 HAL 兼容性验证、多一套内核适配、多一套回归测试。所以小团队做这件事设备支持范围一定要控制住宁可少支持几个型号也要保证支持的那些能跟上节奏。贪多求全的结果通常是所有型号都拖后腿。最后分享一个我最近的体会。前阵子处理一个frameworks/base的补丁冲突改完之后编译通过、功能正常我差点就放过去了。后来出于习惯又跑了一遍全量的策略检查发现有一处访问被新代码触发了但因为在测试路径上没覆盖到运行时没暴露出来。补了策略之后才放心。这件事让我更确信一件事补丁移植的功夫一半在合代码一半在验证。合得快不算本事验证得全才是。