AI智能摘要
项目在 bringup 阶段发现,将手机 SELinux 设为 permissive 后系统会在进入 Android 前死机。通过 fulldump 与内核日志可见 panic 原因是 UBSAN 报告的数组越界,触发点位于 uzram 模块的 zram_submit_bio。借助自研工具 kernel-panic-killer,AI 自动还原异常路径:反汇编发现 zram_submit_bio 中通过 current_algo 计算压缩算法索引,逻辑为 algo = current_algo; 访问 zram->comps[algo - 1]。由于 current_algo 默认为未初始化的
此摘要由AI分析文章内容生成,仅供参考。

问题背景

  • 在项目bringup时发现,将手机的selinux权限设为 permissive 手机会死机

  • bringup期间,手机进入kernel,但是还无法进入android界面

问题分析

拿到fulldump的第一时间,我们可以立即查看内核日志,查看一下是否有很直观的报错日志,于是我们可以看到如下的日志:

[exception_reboot_reason]: kernel_crash
[exception_panic_reason]: UBSAN: array index out of bounds: Fatal exception
[exception_time]: 1970-01-01_08-00-29
[exception_file_info]: not-bugon
[exception_task_id]: 926
[exception_task_family]: [mkswap, 926][system_perf_ini, 922]
[exception_pc_symbol]: [<ffffffd00f2b27a4>] zram_submit_bio+0xee4/0xf28 [uzram]
[exception_stack_info]: [<ffffffd00dfe3260>] prepare_exception_info+0x290/0x5f8 [sysdump]      
[<ffffffd00dfe6904>] sysdump_panic_event+0x420/0x5b0 [sysdump] 
[<ffffffd010084e1c>] notifier_call_chain+0x8c/0x170    
[<ffffffd0100852ac>] atomic_notifier_call_chain+0x44/0x68      
[<ffffffd00ff55ce4>] vpanic+0x180/0x3b4
[<ffffffd00ff55b60>] panic+0x50/0x54   
[<ffffffd00ff54ce8>] die+0xe0/0xe8     
[<ffffffd00ff54ef4>] ubsan_brk_handler+0x34/0x4c       
[<ffffffd00ff72500>] call_el1_break_hook+0x6c/0xb8     
[<ffffffd00ff7262c>] do_el1_brk64+0x2c/0x58    
[<ffffffd011316f10>] el1_brk64+0x34/0x54       
[<ffffffd011316b10>] el1h_64_sync_handler+0x6c/0xcc    
[<ffffffd00ff51340>] el1h_64_sync+0x84/0x88    
[<ffffffd00f2b27a4>] zram_submit_bio+0xee4/0xf28 [uzram]       
[<ffffffd01074a1b4>] __submit_bio+0x124/0x23c  
[<ffffffd0107483b4>] submit_bio_noacct_nocheck+0x114/0x2dc     
[<ffffffd0107488d8>] submit_bio_noacct+0x35c/0x54c     
[<ffffffd010748cb8>] submit_bio+0x1f0/0x210    
[<ffffffd01048a98c>] submit_bh_wbc+0x150/0x1bc 
[<ffffffd01048a5d8>] __block_write_full_folio+0x3c8/0x62c      
[<ffffffd01048c954>] block_write_full_folio+0x190/0x1b0
[<ffffffd01073dc70>] blkdev_writepages+0x84/0xe4       
[<ffffffd01031e1f4>] do_writepages+0xd8/0x1a4  
[<ffffffd01030d100>] file_write_and_wait_range+0x1c8/0x1fc     
[<ffffffd01073e3b0>] blkdev_fsync+0x48/0x74    
[<ffffffd010479b68>] vfs_fsync_range+0xc0/0x154
[<ffffffd010479c6c>] __arm64_sys_fsync+0x3c/0x6c       
[<ffffffd00ff94ab8>] invoke_syscall+0x60/0xec  
[<ffffffd00ff949f8>] el0_svc_common+0xb0/0xe0  
[<ffffffd00ff9493c>] do_el0_svc+0x24/0x30      
[<ffffffd011317200>] el0_svc+0x40/0xb0 
[<ffffffd01131714c>] el0t_64_sync_handler+0x68/0xdc  

于是我们可以得到这是一个数组越界的问题,那这个就好解决了,只需要定位到代码行,然后查看一下它是如何造成越界的就基本可以得到根因。所以由于比较简单,下面的分析我都交给了AI,通过自行开发的 kernel-panic-killer 进行探索

AI 分析的过程

**步骤 3: 反汇编 `zram_submit_bio+0xee4`**
- **假设**: 需要找到触发 UBSAN 的具体指令
- **结果**: `brk #0x5512` — UBSAN 的 array-index-out-of-bounds 陷阱指令
- **决策**: 需要回溯找到实际的数组访问代码

**步骤 4: 反汇编 `zram_submit_bio+0xec0`**
- **假设**: 找到 UBSAN 错误报告的参数设置代码
- **结果**: 发现代码模式:
  - `adrp/add` 加载格式字符串
  - `mov w1, w27` — 越界索引参数
  - `mov w2, w25` — 数组边界参数
  - `brk #0x800` — UBSAN 处理器陷阱
  - `bl panic` — 调用 panic
- **决策**: 需要找到 w27 和 w25 的来源

**步骤 5: 反汇编 `zram_submit_bio+0xc00`**
- **假设**: 找到数组索引和边界的计算代码
- **结果**: 发现关键代码路径:
  - `zram_read_from_zspool` 调用后的错误检查
  - `tbnz w27, #31` — 检查返回值是否为负(错误)
- **决策**: 需要进一步回溯找到压缩算法选择代码

**步骤 6: 反汇编 `zram_submit_bio+0x200`**
- **假设**: 找到压缩算法索引的计算和验证代码
- **结果**: 发现核心代码模式:
  ```
  +548: ldr x8, [x20]                    ; x8 = zram->table
  +552: adrp x10, _MODULE_DATA_START_uzram
  +556: ldr w10, [x10, #3592]            ; w10 = current_algo
  +564: sub w27, w10, #0x1               ; w27 = current_algo - 1
  +572: cmp w27, #0x7                    ; 检查 current_algo - 1 > 7
  +588: b.cs zram_submit_bio+3812        ; 如果越界,跳转到 UBSAN/panic
  +596: ldr x0, [x8, w27, uxtw #3]      ; 加载 compress_streams[current_algo-1]
  ```
- **决策**: 关键发现!需要验证 `current_algo` 的值

**步骤 7: 内存读取 `0xffffffd00f2b7e08`**
- **假设**: 读取 `current_algo` 变量的值
- **结果**: `0x00000000` — **current_algo 为 0!**
- **决策**: 确认根因!current_algo = 0 导致 current_algo - 1 = 0xFFFFFFFF(无符号下溢)

**步骤 8: 符号验证**
- `sym 0xffffffd00f2b7e08` → `current_algo [uzram]` ✅
- `sym 0xffffffd00f2b7e10` → `huge_class_size [uzram]` = 3625(已正常初始化)✅
- `sym 0xffffffd00f2b7e20` → `log_level [uzram]` = 2(正常)✅

AI很快速的就恢复异常现象,并反汇编calltrace,将出现越界的变量找到了,就是这个 current_algo

源码分析

将源码提供给工具后,AI的分析如下

通过 `/data/share-data/admin/src/zram_drv.c` 源码验证:

1. **`current_algo` 声明**(第 77 行): `static u32 current_algo;` — 默认值为 0
2. **写路径**(第 2018-2021 行): `zram->comps[algo - 1]` — 当 algo=0 时,索引为 0xFFFFFFFF
3. **读路径**(第 1813-1816 行): `zram->comps[prio - 1]` — 同样的越界风险
4. **设置路径**(第 1344-1350 行): `current_algo = prios + 1` — 仅通过 sysfs 写入触发
5. **模块初始化**(第 3152-3154 行): 设置 `comp_algs` 但不设置 `current_algo`

AI分析的证据链

Panic 签名

[37.925284] Internal error: UBSAN: array index out of bounds: 00000000f2005512 [#1]  SMP

[38.262128] Kernel panic - not syncing: UBSAN: array index out of bounds: Fatal exception

故障上下文

CPU: 4 UID: 0 PID: 926 Comm: mkswap Tainted: G W OE 6.18.16 #1 PREEMPT
pc : zram_submit_bio+0xee4/0xf28 [uzram]
lr : zram_submit_bio+0x6c/0xf28 [uzram]
sp : ffffffc08536b780

current_algo 内存状态

sym 0xffffffd00f2b7e08 → current_algo [uzram]
rd -x 0xffffffd00f2b7e08 → 0x0000000000000000 (值为 0)

相邻变量状态

sym 0xffffffd00f2b7e10 → huge_class_size [uzram] = 3625 (已初始化)
sym 0xffffffd00f2b7e20 → log_level [uzram] = 2 (正常)

内核日志时间线

[24.174343] modprobe: Loading module /vendor/lib/modules/uzram.ko
[24.195410] zram: Added device: zram0
[24.196556] zram: zram to flash mode 2
[24.200958] initcall: init_module [uzram] time=8.696269ms
[37.879205] zram0: detected capacity change from 0 to 12582912
[37.925235] type=1400 audit(37.875:110): avc: denied { read write } for comm="mkswap" ... permissive=1
[37.925284] Internal error: UBSAN: array index out of bounds

反汇编证据

zram_submit_bio+556: ldr w10, [x10, #3592]   ; w10 = current_algo = 0
zram_submit_bio+564: sub w27, w10, #0x1       ; w27 = 0 - 1 = 0xFFFFFFFF
zram_submit_bio+572: cmp w27, #0x7            ; 0xFFFFFFFF > 7 → carry set
zram_submit_bio+588: b.cs zram_submit_bio+3812 ; 分支跳转 → BRK #0x5512 → panic

源码证据

// zram_drv.c 第 77 行
static u32 current_algo;  // 默认值为 0

// zram_drv.c 第 2018-2021 行(写路径)
algo = current_algo;                          // algo = 0
zram_set_comp(zram, index, algo);
zstrm = zcomp_stream_get(zram->comps[algo - 1]);  // comps[0xFFFFFFFF] → 越界!

// zram_drv.c 第 1344-1349 行(设置路径)
for (prios = 0; prios < backends_count; prios++) {
    if (!strcmp(backends[prios]->name, compressor)) {
        current_algo = prios + 1;  // 仅通过 sysfs 写入设置
        break;
    }
}

推理与根因评估

崩溃机制

CONFIG_UNISOC_ZRAM_DYNAMIC_COMP 启用时,uzram 模块使用 current_algo 全局变量跟踪当前选择的压缩算法。该变量是 1-based 索引(1 = 第一个算法,2 = 第二个,依此类推),默认值为 0 表示"未选择"。

写路径 zram_write_page 使用 current_algo - 1 作为 zram->comps[] 数组的索引。当 current_algo = 0 时:

  • current_algo - 1 = 0 - 1 = 0xFFFFFFFF(u32 无符号下溢)

  • zram->comps[0xFFFFFFFF] → 数组越界访问

  • UBSAN 检测到越界,触发 brk #0x5512 陷阱

  • 内核 panic

根因评估

直接原因: current_algomkswap 提交写 I/O 时为 0。

根本原因: current_algo 只能通过用户空间写入 /sys/block/zram0/comp_algorithm sysfs 节点来设置。在正常启动流程中,init 脚本应按以下顺序执行:

  1. echo lzo > /sys/block/zram0/comp_algorithm — 设置压缩算法

  2. echo <size> > /sys/block/zram0/disksize — 设置磁盘大小

  3. mkswap /dev/block/zram0 — 格式化 swap

在此案例中,步骤 1 从未执行(日志中无 comp_algorithm 记录),但步骤 2 和 3 执行了,导致 panic。

SELinux 关联: SELinux 策略可能阻止了 comp_algorithm 的设置(如果 system_perf_init 域没有 sysfs_zram 写权限),或者 init 脚本本身存在顺序错误。在 enforcing 模式下,mkswap 的 I/O 操作会被阻止,不会触发 panic;在 permissive 模式下,操作被允许,暴露了这个 bug。

越界访问证明状态

证明项

状态

确认的访问站点

zram_write_pagezram->comps[algo - 1]

被访问的数组

zram->comps[](压缩流指针数组)

索引变量

algo(来自 current_algo

运行时索引值

已恢复:current_algo = 0algo - 1 = 0xFFFFFFFF

合法边界

backends_count(已注册的压缩后端数量,从 dump 中 compress_streams 有 6 个有效条目推断)

证明来源

内存直接读取 + 反汇编 + 源码三重验证

结论: 越界访问已确认。current_algo = 0 是直接证据,0xFFFFFFFF 索引是算术推导,源码验证了完整的因果链。

为何selinux打开就出现了呢?

selinux被设置为 permissive后,服务 system_perf_init 回去执行 mkswap 对 zram0 操作,而zram0因为背景还处于bringup期间,系统还无法启动

zram0的 com_algorithm 算法还没有被设置,导致内核的uzram驱动中的 current_algo 还是0,这样就出现了溢出

但是说到底内核的代码还是存在缺陷的!因为我们要记住一点,无论任何情况内核都应该避免出现死机的情况!

代码修复

zram_write_pageread_compressed_page 中添加 current_algo 有效性检查:

// zram_drv.c 第 2018 行附近

#if IS_ENABLED(CONFIG_UNISOC_ZRAM_DYNAMIC_COMP)

    algo = current_algo;

    if (unlikely(algo == 0)) {

        pr_err_ratelimited("uzram: current_algo not configured, rejecting write\n");

        return -EINVAL;

    }

    zram_set_comp(zram, index, algo);

    zstrm = zcomp_stream_get(zram->comps[algo - 1]);

crash关键指令附录

序号

命令

目的

关键结果

1

bt

获取 backtrace

仅返回 PID 信息,无 stack frames

2

log | tail -n 300

获取 panic 日志

发现 UBSAN panic 签名和故障函数

3

dis zram_submit_bio+0xee4 30

反汇编故障点

brk #0x5512 — UBSAN 陷阱

4

dis zram_submit_bio+0xec0 40

回溯 UBSAN 参数

发现 w27/w25 参数设置

5

dis zram_submit_bio+0xc00 100

查找数组访问代码

发现压缩流数组访问模式

6

dis zram_submit_bio+0x200 100

查找 current_algo 加载

发现 ldr w10, [x10, #3592]

7

rd -x 0xffffffd00f2b7e08

读取 current_algo 值

值为 0 — 根因确认

8

sym 0xffffffd00f2b7e08

验证符号

current_algo [uzram]

9

sym 0xffffffd00f2b7e10

检查相邻变量

huge_class_size = 3625(正常)

10

log | grep -i "uzram|zram"

检查模块初始化日志

comp_algorithm 设置记录

11

dis comp_algorithm_store 30

查看算法设置函数

确认通过 sysfs 写入设置 current_algo