1. 执行摘要

本次分析针对一起 Qualcomm Ravelin SNP-AN00 平台上的 kernel panic 进行根因定位。

根因判定: EROFS 压缩文件系统在进行 LZ4 解压缩过程中,z_erofs_lz4_decompress_partial 函数通过 __memcpy 读取压缩源页面时,触发了 kernel 级别的 translation fault。直接原因是源页面和目标页面均已被 page allocator 释放(POISON_FREE 模式 dead000000000400),导致 use-after-free。

竞争路径: 内核压缩内存回收线程 kcompactd0(PID 85, CPU 4)在 crash 前约 544ms 执行了 erofs: managed_cache_releasepage,累计回收了 51355 个页面。EROFS readahead 路径(CPU 7)在解压缩过程中访问了已被回收的页面,形成经典的 page reclaim race。

关于 bitflip 怀疑: 已排除。故障地址 ffffffc054099984 是有效的 kernel linear map 地址,PTE 为零是因为物理页面已被释放而非地址损坏。寄存器中未发现单比特偏差模式。dead000000000400 是内核标准的页面释放毒化值。

置信度: high-probability


2. 分析产物

项目

Dump 模式

split ramdump(6 个 DDRCS 段)

dump_spec

DDRCS0_0.BIN@0x80000000, DDRCS1_0.BIN@0x800000000, DDRCS1_1.BIN@0x880000000, DDRCS2_0.BIN@0x900000000, DDRCS2_1.BIN@0x980000000, DDRCS2_2.BIN@0xa00000000

vmlinux 路径

/mnt/d/workspace/project/M487/vmlinux

模块符号

未提供(仅 warning)

符号状态

ready(vmlinux 可用)

drgn 可用性

不可用(drgn_enabled: false)

平台

Qualcomm Technologies, Inc. Ravelin SNP-AN00

内核版本

5.10.209-qki-consolidate-android12-9-gb43fede8be31-dirty

KASLR 偏移

0x252b200000(从 OCIMEM.BIN 提取)

VA_BITS

39

Crash 会话 ID

995b9abe-4788-4a5b-9b9b-f421340b0fba


3. 会话模式与工具约束

  • Split dump 模式:使用 build_dump_specextract_qcom_kaslr 构建启动参数

  • drgn 不可用:无法使用 drgn 脚本进行高级内存分析(如直接检查 struct page 字段、遍历 VMA 链表等)

  • Crash 命令行为异常:在第一次会话中,部分 crash 命令(如 sysmodlog)返回了不相关的输出(如 kmem -skmem -n 等),疑似 crash binary 的命令映射异常。关闭并重新打开会话后恢复正常

  • bt -l / bt -f 限制:对于 panic 上下文的任务,crash 无法展开完整回溯栈,仅返回任务头信息

  • kmem 对 NULL PTE 地址的限制kmem <faulting_vaddr> 返回 "cannot make virtual-to-physical translation",因为 PTE 为零。需要通过 kmem -P <phys_addr> 使用物理地址查询


4. 调查过程与命令推导

4.1 Intake 阶段

可用信息: 用户提供了 split ramdump 路径、vmlinux、platform=qcom、vabits_actual=39,并怀疑可能存在 bitflip。

执行步骤:

  1. detect_dump_layout → 确认 split dump, Qualcomm 平台, 6 个 DDRCS 文件, OCIMEM 存在

  2. check_symbol_readiness → vmlinux 就绪,模块符号未提供

  3. validate_ramdump_inputs → ready=true,自动推导 dump_spec 和 crash_args

  4. build_dump_spec → 生成 6 段 dump_spec

  5. extract_qcom_kaslr → 从 OCIMEM.BIN 提取 KASLR=0x252b200000

  6. open_vmcore_session → 会话 ID: 995b9abe

4.2 第一轮命令:bt + log

开放问题: crash 的直接原因是什么?调用栈是什么?

执行: btlog | tail -n 300(并行)

结果:

  • bt 仅返回任务头:PID 5432, CPU 7, "e.hwvoipservice"

  • log 包含完整的 panic 调用栈和多个 CPU 的 stopping 信息

关键发现: panic 调用栈显示:

__memcpy+0xac/0x180           ← 故障指令
lz4_decompress+0x18c/0x424
decompress_generic+0x294/0x3a4
z_erofs_vle_unzip+0x5c0/0xa18
z_erofs_submit_and_unzip+0x1e8/0x24c
z_erofs_vle_normalaccess_readpages+0x338/0x47c
read_pages -> page_cache_ra_unbounded -> do_sync_mmap_readahead
filemap_fault -> __do_fault -> do_read_fault
handle_pte_fault -> handle_mm_fault -> do_page_fault
do_translation_fault -> do_mem_abort -> el0_da -> el0_sync

假设更新: 这是一个 EROFS LZ4 解压缩过程中的 translation fault。故障发生在 kernel 模式(el1_abort),由用户空间页面错误(el0_da)触发的 readahead 路径引起。

4.3 定位 Oops 寄存器转储

开放问题: 故障地址是什么?寄存器状态如何?

推导: bt -fbt -l 无法展开栈。log 的最后 300 行不包含 Oops 头部。需要搜索 "Unable to handle" 消息。

执行: log | grep -n "Unable to handle"

结果: 行号 16133,故障地址 ffffffc054099984

后续执行: log | sed -n '16130,16200p' 获取完整 Oops

关键寄存器:

寄存器

含义

x0 (dest)

ffffffc0540a1484

memcpy 目标地址

x1 (src)

ffffffc054099984

memcpy 源地址(故障地址)

x3

0x6e (110)

待拷贝字节数

x10

5a5f000076454170

LZ4 数据(ASCII "pAEv\0_Z",正常)

PTE

0000000000000000

页表项为空

ESR

0x96000007

DABT, translation fault level 3, 读访问

WnR

0

读操作

LR

z_erofs_lz4_decompress_partial+0x404/0x5f0

代码反汇编: 38401423 = LDRB W3, [X1], #1(LZ4 字面量拷贝循环中的第一个源读取)

假设更新: memcpy 在读取源地址的第一个字节时就触发了 fault。PTE 为零意味着物理页面不存在于页表中。需要确定该物理页面是从未分配还是已被释放。

4.4 检查故障页面状态

开放问题: 故障地址对应的物理页面是否存在?如果存在,其状态是什么?

推导: kmem <vaddr> 因 PTE 为零无法翻译。需要手动计算物理地址。

计算: 对于 Qualcomm arm64 平台:

  • PAGE_OFFSET = ffffffc000000000(39-bit VA)

  • PHYS_OFFSET = 0x80000000(Qualcomm 典型值)

  • phys = 0xffffffc054099984 - 0xffffffc000000000 + 0x80000000 = 0xd4099984

执行: kmem -P d4099984

结果:

PAGE        PHYSICAL      MAPPING       INDEX CNT FLAGS
ffffffff01302640  d4099000 dead000000000400        0  0 4000000000000000

关键发现:

  • MAPPING = dead000000000400:这是内核的 POISON_FREE 模式!页面已被释放

  • CNT (refcount) = 0:引用计数为零

  • FLAGS = 4000000000000000:仅 PG_head 位(复合页头)

后续检查: 对目标地址和相邻页面执行相同查询:

d4098000: dead000000000400  refcount=0  ← 前一页
d4099000: dead000000000400  refcount=0  ← 源页面(故障地址)
d40a0000: dead000000000400  refcount=0
d40a1000: dead000000000400  refcount=0  ← 目标页面
d40a2000: dead000000000400  refcount=0  ← 后一页

所有页面均已被释放!d4098000d40a2000 的 40KB+ 连续区域全部为 dead000000000400 毒化模式。

假设更新: 确认为 use-after-free。源页面和目标页面均已被 page allocator 释放。EROFS 解压缩路径访问了已释放的页面。

4.5 寻找释放路径

开放问题: 是什么释放了这些页面?是 inode eviction 还是 page reclaim?

推导: 需要找到 EROFS inode 并检查其状态,同时搜索 log 中的 EROFS 相关消息。

执行:

  1. files 5432 → 获取任务打开的文件列表,发现大量 /first_stage_ramdisk/ 路径的 EROFS 文件

  2. vm 5432 → 获取 VMA 列表,发现 EROFS 文件的 mmap 映射(如 boot.oatboot.vdex 等)

  3. struct vm_area_struct.vm_file <vma>0xffffff8068724240

  4. struct file.f_mapping <file>0xffffff87802a30c0

  5. struct address_space.host <mapping> → inode 0xffffff87802a2e68

  6. struct inode.i_count,i_nlink,i_ino <inode> → i_count=1, i_nlink=1, i_ino=32757962

Inode 状态: 健康(i_count=1, i_nlink=1)。排除了 inode eviction 假设。

关键 Log 发现:

[180317.592489] [2026:03:23 13:04:28](4)[85:kcompactd0]erofs: managed_cache_releasepage, nr_cached_pages = 3264 shrink[cnt = 51355 runs = 131694]

这是确凿证据! kcompactd0(PID 85, CPU 4)在 crash 前约 544ms 执行了 erofs: managed_cache_releasepage,累计回收了 51355 个页面,执行了 131694 次回收操作。

假设收敛: 根因确认为 EROFS managed cache 页面回收与 readahead 解压缩之间的竞争条件。


5. 直接证据

5.1 Oops 寄存器转储

Unable to handle kernel paging request at virtual address ffffffc054099984
Mem abort info:
  ESR = 0x96000007
  EC = 0x25: DABT (current EL), IL = 32 bits
  SET = 0, FnV = 0
  EA = 0, S1PTW = 0
Data abort info:
  ISV = 0, ISS = 0x00000007
  CM = 0, WnR = 0
swapper pgtable: 4k pages, 39-bit VAs, pgdp=00000000aab05000
[ffffffc054099984] pgd=0000000a12df6003, p4d=0000000a12df6003, pud=0000000a12df6003, pmd=00000000b12ed003, pte=0000000000000000

CPU: 7 PID: 5432 Comm: e.hwvoipservice Tainted: G  W  O  5.10.209-qki-consolidate-android12-9-gb43fede8be31-dirty #1
pc : __memcpy+0xac/0x180
lr : z_erofs_lz4_decompress_partial+0x404/0x5f0
sp : ffffffc0392230a0
x0 : ffffffc0540a1484  x1 : ffffffc054099984
x2 : 0000000000000001  x3 : 000000000000006e
x10: 5a5f000076454170

Code: 36080062 78402423 780024c3 36000542 (38401423)

5.2 故障页面物理状态

物理地址

MAPPING

CNT

FLAGS

含义

d4098000

dead000000000400

0

4000000000000000

已释放(POISON_FREE)

d4099000

dead000000000400

0

4000000000000000

源页面,已释放

d40a0000

dead000000000400

0

4000000000000000

已释放

d40a1000

dead000000000400

0

4000000000000000

目标页面,已释放

d40a2000

dead000000000400

0

4000000000000000

已释放

5.3 kcompactd0 EROFS 页面回收日志

[180317.592489] [2026:03:23 13:04:28](4)[85:kcompactd0]erofs: managed_cache_releasepage, nr_cached_pages = 3264 shrink[cnt = 51355 runs = 131694]
  • 时间戳:crash 前约 544ms

  • CPU 4,PID 85(kcompactd0)

  • 累计回收:51355 个页面,131694 次回收操作

5.4 Inode 状态

字段

含义

i_count

1

引用计数正常,未被 evict

i_nlink

1

仍有链接

i_ino

32757962

inode 号

i_mode

33188 (0o100644)

普通文件

i_size

3361008 (~3.2MB)

文件大小

5.5 Bitflip 证据评估

  • 故障地址 ffffffc054099984 是有效的 kernel linear map 地址(~84MB 偏移)

  • PTE 为完全零值(非损坏的 PTE),说明页面已被完全释放

  • x10 中的 5a5f000076454170 是正常的 LZ4 压缩数据(ASCII "pAEv\0_Z")

  • 所有寄存器地址均在预期的 kernel linear map 范围内

  • 相邻 5 个页面全部显示相同的 dead000000000400 毒化模式

  • 结论:无 bitflip 证据


6. 推理与根因评估

6.1 根因机制

Use-After-Free:EROFS managed cache 页面回收与 readahead 解压缩竞争

EROFS 是 Android 系统中用于 first_stage_ramdisk 的只读压缩文件系统。当用户进程(e.hwvoipservice)通过 mmap 访问 EROFS 上的文件(如 boot.oat)时,内核的 page fault 处理路径会触发 readahead,进而调用 EROFS 的 z_erofs_vle_normalaccess_readpages 进行压缩页面的读取和 LZ4 解压缩。

EROFS 维护了一个 managed page cache 用于缓存压缩数据页面。当系统内存压力增大时,kcompactd0 通过 managed_cache_releasepage 回收这些缓存页面。

竞争时序:

  1. T0: 用户进程访问 EROFS 文件的 mmap 区域,触发 page fault

  2. T1: readahead 路径分配压缩源页面和解压缩目标页面

  3. T2: kcompactd0 开始回收 EROFS managed cache 页面(包括正在使用的页面)

  4. T3: 源页面和目标页面被释放,PTE 被清除,页面内容被毒化为 dead000000000400

  5. T4: z_erofs_lz4_decompress_partial 调用 __memcpy 读取已释放的源页面

  6. T5: Translation fault → Oops → Kernel panic

6.2 竞争条件分析

竞争发生在两个路径之间:

  • 路径 A(CPU 7, PID 5432)el0_dado_page_faultfilemap_faultdo_sync_mmap_readaheadpage_cache_ra_unboundedread_pagesz_erofs_vle_normalaccess_readpagesz_erofs_submit_and_unzipz_erofs_vle_unzipdecompress_genericlz4_decompressz_erofs_lz4_decompress_partial__memcpy在此处 fault

  • 路径 B(CPU 4, PID 85)kcompactd0 → ... → erofs: managed_cache_releasepage释放页面

EROFS 的 managed_cache_releasepage 函数在释放页面时,可能没有充分检查页面是否正在被解压缩路径使用。这是一个 EROFS 子系统内部的页面生命周期管理缺陷。

6.3 排除的假设

假设

排除理由

Bitflip

无单比特偏差模式,dead000000000400 是标准毒化值,所有相邻页面一致

EROFS 元数据损坏

源和目标页面地址在同一个连续区域,且都已被释放,不符合元数据损坏模式

Inode eviction

i_count=1, i_nlink=1,inode 仍被正常引用

NULL 指针解引用

故障地址 ffffffc054099984 是有效的 kernel linear map 地址,非 NULL 附近

内存越界

地址模式正常,不呈现 OOB 特征


7. 置信度、限制与分析天花板

置信度标签: high-probability

置信度说明:

  • 直接证据(页面状态、log 消息、寄存器)完整且一致

  • 竞争路径在时间和空间上都有明确的证据支持

  • 页面毒化模式 dead000000000400 是内核标准值,非异常

为什么不是 confirmed:

  1. drgn 不可用,无法直接检查 EROFS 内部数据结构(如 z_erofs_pagevec、压缩 extent 描述符等)

  2. 无法确定 managed_cache_releasepage 是否缺少对 in-use 页面的引用检查

  3. 无法确认是否为已知的 EROFS 上游 bug 还是 vendor 定制引入的问题

  4. 竞争的精确时序无法从 dump 中恢复

分析天花板:

  • 无法确定 managed_cache_releasepage 的具体代码路径(需要源码分析)

  • 无法确定是否存在其他并发的页面回收路径(如 direct reclaim)

  • 无法确认 EROFS 的页面引用计数管理是否在所有路径上都正确

如果需要更高置信度的下一步:

  1. 获取 EROFS 源码,审查 managed_cache_releasepage 的页面引用检查逻辑

  2. 检查 z_erofs_vle_normalaccess_readpages 中压缩页面的锁定和引用计数管理

  3. 在 kernel 5.10.209 的 EROFS 代码中搜索已知的页面生命周期 bug


8. 建议措施与缓解方案

缓解措施

时间维度

负责方

范围

证据关联

预期效果

风险/权衡

验证计划

回滚路径

升级 EROFS 至包含页面生命周期修复的上游版本

中期

BSP/内核团队

EROFS 子系统

managed_cache_releasepage log + UAF 页面状态

消除竞争条件

可能引入新的兼容性问题

压力测试 + 内存压力场景复现

回退到当前版本

增加 EROFS compressed page 的引用计数保护

短期

BSP/内核团队

EROFS decompression 路径

源和目标页面 refcount=0 且正在使用

防止页面在解压缩期间被回收

可能增加内存占用

检查解压缩期间的 refcount 变化

移除额外的 get_page/put_page

managed_cache_releasepage 中检查页面是否在 decompression workqueue 中

短期

BSP/内核团队

EROFS cache management

kcompactd0 与 readahead 的竞争

阻止释放正在使用的页面

可能延迟页面回收

添加 PG_locked 或自定义标志检查

移除检查逻辑

增加系统内存或调整 compaction 策略以降低 kcompactd 压力

短期

系统团队

系统配置

shrink cnt=51355 表明频繁回收

减少竞争窗口

增加内存占用

监控 kcompactd 运行频率

恢复默认配置


9. 关键命令附录

命令

目的

关键结果

bt

获取当前任务回溯

仅返回任务头(panic 上下文限制)

log \| tail -n 300

获取最近内核日志

发现 panic 调用栈和多个 CPU stopping 信息

log \| grep -n "Unable to handle"

定位 Oops 行号

行号 16133,故障地址 ffffffc054099984

log \| sed -n '16130,16200p'

获取完整 Oops

完整寄存器转储和调用栈

kmem ffffffc054099984

查询故障地址页面状态

"cannot make virtual-to-physical translation"

kmem -P d4099984

查询物理页面状态

MAPPING=dead000000000400, CNT=0(已释放)

kmem -P d40a1000

查询目标页面状态

MAPPING=dead000000000400, CNT=0(已释放)

files 5432

获取任务打开的文件

大量 /first_stage_ramdisk/ EROFS 文件

vm 5432

获取任务 VMA 列表

EROFS 文件的 mmap 映射(boot.oat 等)

struct vm_area_struct.vm_file

追踪到 file 结构

vm_file = 0xffffff8068724240

struct file.f_mapping

追踪到 address_space

f_mapping = 0xffffff87802a30c0

struct address_space.host

追踪到 inode

host = 0xffffff87802a2e68

struct inode.i_count,i_nlink

检查 inode 状态

i_count=1, i_nlink=1(健康)


Report generated at: 2026-05-13 18:15
Session ID: 995b9abe-4788-4a5b-9b9b-f421340b0fba
Analyst: bsp-stability-ai by iliuqi
Author homepage: https://www.iliuqi.com