第 43 章 LFI 进程内沙箱 Android 一直使用其最强隔离手段 —— 独立进程,来隔离不可信的原生代码。软件媒体解码器就是典型例子。由于格式异常的音频或视频帧会导致 C 语言解码器出现缓冲区溢出,AOSP 将软件解码器运行在经过加固的独立 APEX 进程(com.android.media.swcodec)中,通过 Binder 进行通信。这样一来,libopus 或 AAC 解码器中的内存损坏漏洞就无法侵入应用或媒体服务进程。这种隔离确实有效,但并非没有代价:每一帧解码数据都要跨越进程边界,缓冲区依靠 ashmem 与 Binder 完成共享,同时还要创建、调度并维持一整个进程。Android 17 新增了一种更轻量的隔离原语:轻量级故障隔离(LFI)。LFI 将不可信原生库的内存访问与控制流限制在宿主进程地址空间内的一块预留区域。该限制由机器码强制执行,验证器可以证明这些机器码无法跳出该区域。不可信解码器运行在同一进程内,但可以证明它无法读写沙箱之外的内存、无法跳转到沙箱外的代码,也无法执行原始系统调用。无需额外进程、无需 Binder 跳转、不存在跨地址空间的缓冲区拷贝,即便解码器存在内存安全漏洞,破坏范围也被限制在沙箱内部。首个正式落地的使用场景正是当初催生 swcodec 的场景:媒体 APEX 内部运行软件 Opus 解码器,使用 LFI 沙箱替代独立进程模型(也可二者并存)。本章讲解 LFI 的定义、适用的威胁模型、树内粘合代码(system/lfi)与外部工具链(external/lfi)之间验证器、运行时、绑定层的分工、沙箱解码器的编译与加载流程、用于构建的 Soong LFI 工具链,以及将不可信代码重新放回原进程所带来的安全取舍。43.1 LFI 简介与威胁模型43.1.1 现代化的软件故障隔离LFI 属于软件故障隔离(SFI)方案:核心思想是,只要不可信机器码的每一次内存访问、每一次控制转移都被指令本身约束在沙箱区域内,就可以在宿主进程地址空间内安全执行。经典 SFI(Google Native Client 及其前身)的实现方式是:在加载、存储、跳转前对地址高位做掩码运算,让沙箱内的指针无法指向非 2 的幂次对齐区域之外的内存。LFI 是该技术路线现代化的学术研究衍生实现;system/lfi/README.md 中引用了斯坦福大学 LFI 论文与 LLVM LFI 文档作为背景资料。其核心安全特性并不依赖信任不可信代码,而是依赖平台信任的两部分组件:编译器 Pass,仅输出沙箱安全的指令序列(掩码地址、受限控制流、保存沙箱基址的保留寄存器)。验证器,逐条重新校验最终二进制文件指令,拒绝加载任何存在逃逸风险的代码。因此即便库本身存在恶意逻辑或是编译出错,也无法绕过校验。由于安全保障在加载阶段由验证器重新确认,威胁模型不要求信任生成二进制文件的工具链。验证器代码量小、可审计,是真正的信任根。43.1.2 威胁模型:无需第二个进程的内存安全隔离LFI 所要保护的资产是宿主进程。对于首个使用场景,即媒体 swcodec 进程,同时也包括该进程持有的缓冲区与凭证。攻击者输入是格式异常的媒体码流,会在不可信 C 语言解码器内部触发未定义行为(越界读写、释放后使用、类型混淆)。在没有 LFI 的情况下,平台唯一的结构性方案是把解码器放到另一个进程,让内存损坏漏洞的破坏范围局限在这个牺牲进程内。LFI 提供了另一种隔离边界:解码器运行在同一进程,但经过验证的机器码保证它只能访问沙箱区域,只能跳转到沙箱内部经过校验的目标,不能发起任意系统调用;所有 “系统调用” 都会转为回调至受信任运行时,由运行时决定是否放行。LFI不能抵御侧信道造成的信息泄露,也不能防御解码器业务逻辑本身存在的逻辑漏洞。它是一层内存安全边界:它将 “解码器存在堆溢出” 从可攻陷整个进程的漏洞,转变为局限在沙箱内部的故障。源码中也如实体现这一点:MediaCodecInfo::getSecurityModel()将 LFI 路径标记为SECURITY_MODEL_MEMORY_SAFE,和独立进程沙箱模式SECURITY_MODEL_SANDBOXED做明确区分,并不宣称二者等价。下图对比了针对同一个不可信解码器的两种隔离策略。43.2 验证器、运行时与绑定层的分工Android 中的 LFI 代码分布在两套代码树,承担完全不同的职责。树内的system/lfi仓库存放所有沙箱库都需要的共享粘合代码。external/lfi树导入上游工具链,包含验证器、运行时、绑定生成器以及指令解码器。理解各组件职责,是看懂后续解码器集成逻辑的关键。43.2.1 外部工具链(external/lfi)external/lfi是导入的上游依赖。平台对其做集成,但不会修改内部实现。一共包含 6 个组件,均为上游项目镜像到 AOSP。lfi‑verifier(编译生成 lfi‑verifier 库与主机工具 lfi‑verify)。验证器是信任根。在运行时执行代码前,扫描编译后的代码段,判断每一条指令是否满足沙箱安全要求。对外接口是external/lfi/lfi‑verifier/src/include/lfiv.h中少量面向不同架构的入口函数:bool lfiv_verify_arm64(char *code, size_t size, uintptr_t addr, struct LFIVOptions *opts); bool lfiv_verify_x64(char *code, size_t size, uintptr_t addr, struct LFIVOptions *opts); bool lfiv_verify_riscv64(char *code, size_t size, uintptr_t addr, struct LFIVOptions *opts);LFIVOptions结构体用于选择沙箱模式。包含两种沙箱类型:LFI_BOX_FULL同时约束加载、存储以及控制流;LFI_BOX_STORES是弱模式,仅约束存储操作。还可以指定上下文寄存器ctxreg(arm64 为 x25,x64 为 r15),沙箱代码禁止修改该寄存器,寄存器保存沙箱基地址。验证器依赖下述指令解码器完成指令解析。lfi‑runtime(编译生成 liblfi)。运行时在执行期管理沙箱。根据external/lfi/lfi‑runtime/README.md,分为核心层与 Linux 层;核心层负责预留虚拟地址空间、映射沙箱内存、完成沙箱内外的控制流切换;Linux 层在核心层之上实现 Linux 模拟层(处理主机调用)。核心对象模型在external/lfi/lfi‑runtime/core/include/lfi_core.h中定义三个结构体:LFIEngine:管理一大片虚拟内存池,负责从该池中分配沙箱实例。LFIBox:为单个沙箱预留的虚拟地址区间,一个沙箱对应一个 LFIBox 对象。LFIContext:沙箱执行上下文,保存沙箱寄存器、线程指针、栈,以及对应 LFIBox 的引用;每个沙箱线程拥有一个上下文。LFIOptions结构体携带沙箱大小、stores_only开关(必须和验证器配置保持一致),还有一个明确标记为不安全的no_verify