8、动态分区与AVB:Android Verified Boot在动态分区中的实现

说实话,AVB(Android Verified Boot)这块内容,我当年刚接触时也觉得挺绕的。尤其是动态分区出来之后,校验链路变得更长了。你想想看,以前system分区就一个孤零零的镜像,校验起来简单粗暴。现在倒好,分区是动态的,镜像得先映射才能访问,那AVB怎么保证整个链条的安全?

嗯,今天我们就来把这个事情彻底讲清楚。

8.1 AVB的基本原理回顾

先简单回顾一下AVB的核心思想。说白了,就是一条信任链——从BootROM开始,一级级往下校验,直到你最终挂载的system分区。

典型的校验链路是这样的:

  1. BootROM → 校验Bootloader
  2. Bootloader → 校验Boot分区
  3. Boot分区中的内核 → 校验system分区

每一级都持有下一级的公钥,或者哈希值。只要有一级被篡改,链条就断了,设备就会进入警告状态或者直接拒绝启动。

我在项目中遇到过一个问题:某款设备刷了第三方ROM后,明明分区没动,但就是卡在开机警告界面。后来排查发现,是AVB的vbmeta分区里的哈希值和实际分区内容对不上了。嗯,这就是典型的信任链断裂。

8.2 动态分区给AVB带来的挑战

动态分区出现之前,system、vendor这些分区都是固定的物理分区。AVB校验时,直接读取分区起始位置的哈希表就行,简单直接。

但动态分区不一样。你想想看,system分区现在是一个super分区里的逻辑卷。它没有固定的物理位置。那AVB怎么知道该读哪块数据?

这里有几个关键问题:

  • 分区映射时机:AVB在校验时,动态分区可能还没映射好
  • 哈希表位置:传统AVB把哈希表放在分区末尾,但动态分区的大小是灵活的
  • vbmeta的指向:vbmeta里记录的哈希值,是针对物理分区还是逻辑分区?

我个人习惯把这个问题拆成两个层面来看:一是存储层面,二是校验时机层面

8.3 存储层面的解决方案

Google的解决思路其实挺巧妙的。他们并没有推翻AVB的原有设计,而是在动态分区的框架下做了适配。

核心思想是:vbmeta依然指向物理分区,但物理分区的内容变成了一个“容器”

具体来说:

传统方案 动态分区方案
system分区 = 物理分区 system分区 = super分区内的逻辑卷
vbmeta直接校验system分区 vbmeta校验super分区中的system逻辑卷
哈希表在system分区末尾 哈希表在super分区内的特定位置

你可能会问:那vbmeta怎么知道system逻辑卷在super分区里的偏移量?

嗯,这里就要提到LP metadata(逻辑分区元数据)了。Bootloader在启动时,会先读取super分区头部的LP metadata,解析出各个逻辑分区的偏移量和大小。然后AVB再根据这些信息去校验对应的逻辑分区。

关键点:LP metadata本身也是受保护的。它位于super分区的固定位置,并且有校验和。如果LP metadata被篡改,AVB在校验阶段就会检测到。

8.4 校验时机的变化

传统AVB的校验时机很明确:内核挂载system分区之前,Bootloader就已经完成了校验。

但动态分区下,事情变得复杂了一些。我画了一张流程图,帮你理解整个校验链路:

动态分区下AVB校验链路 BootROM Bootloader 读取super分区LP metadata 校验vbmeta分区 根据LP metadata校验system/vendor等逻辑分区 注意:LP metadata本身也包含在AVB的校验范围内

看到这个流程,你应该明白了:Bootloader先读LP metadata,再根据它去校验逻辑分区。这个顺序不能乱。如果先校验逻辑分区再读LP metadata,那根本不知道往哪读。

8.5 vbmeta在动态分区中的特殊处理

这里有个细节,我觉得值得单独拿出来讲。传统AVB中,vbmeta里记录的是各个分区的哈希值。但在动态分区下,vbmeta里记录的是什么呢?

答案是:vbmeta里记录的是逻辑分区的哈希值,但哈希值的计算方式变了

具体来说:

  • 传统方式:对整个物理分区计算哈希
  • 动态分区方式:对逻辑分区内的有效数据计算哈希,不包括分区头部的元数据

为什么要这样?因为逻辑分区在super分区内部,它的头部可能包含一些元数据(比如分区名、标志位等)。这些元数据在OTA升级时可能会变化,但分区内的实际数据没变。如果哈希包含了元数据,那每次OTA都会导致哈希变化,AVB就会报错。

避坑指南:我曾经在开发OTA升级方案时,遇到过一个问题——升级后设备一直重启。排查了两天才发现,是vbmeta里的哈希值没有排除逻辑分区的元数据区域。每次升级后,元数据变了,哈希对不上,AVB就认为分区被篡改了。嗯,这个坑踩得值。

8.6 动态分区AVB的代码实现

说了这么多理论,我们来看看实际代码是怎么实现的。在Android源码中,AVB的校验逻辑主要在external/avb/目录下。

对于动态分区,关键函数是avb_slot_verify()。这个函数会遍历vbmeta里记录的所有分区,逐个校验。但在动态分区下,它需要先调用lp_get_partition()来获取逻辑分区的实际位置。

代码逻辑大致如下:

// 伪代码,展示核心逻辑
for each partition in vbmeta:
    if partition is in super:
        // 获取逻辑分区信息
        lp_partition = lp_get_partition(super_fd, partition.name)
        if lp_partition == NULL:
            return AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA
        
        // 计算逻辑分区的偏移量和大小
        offset = lp_partition->linear_extent.offset
        size = lp_partition->linear_extent.num_sectors * 512
        
        // 读取并校验
        hash = calculate_hash(super_fd, offset, size)
        if hash != vbmeta->partitions[i].hash:
            return AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION
    else:
        // 传统物理分区,直接校验
        hash = calculate_hash(partition_fd, 0, partition_size)
        if hash != vbmeta->partitions[i].hash:
            return AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION

你看,核心区别就在于:动态分区需要先查表(LP metadata),再读数据。而传统分区直接读就行。

8.7 OTA升级中的AVB校验

最后聊聊OTA升级时AVB怎么工作。这个我实际做过,有点发言权。

OTA升级时,系统会把新的分区镜像写入super分区。但写入后,AVB的哈希值就变了。所以OTA流程里必须包含一步:更新vbmeta里的哈希值

具体流程是:

  1. 下载OTA包,解压得到新的分区镜像
  2. 将新镜像写入super分区对应的逻辑分区
  3. 重新计算每个逻辑分区的哈希值
  4. 更新vbmeta分区中的哈希表
  5. 重启设备,AVB校验通过

这里有个容易出问题的地方:更新vbmeta的时机。如果先更新vbmeta再写分区,那写入过程中如果断电,vbmeta里的哈希和实际分区内容就不一致了。设备就变砖了。

警告:正确的做法是先写分区数据,最后再更新vbmeta。这样即使写入过程中断电,重启后AVB校验失败,设备还能进入恢复模式重新刷机。如果顺序反了,那就真的变砖了。

嗯,关于动态分区和AVB的结合,核心内容就是这些。说白了,AVB的信任链没有变,只是中间多了一层LP metadata的解析。你只要理解了这层关系,动态分区下的AVB其实没那么复杂。

最后说一句,我建议你在做动态分区相关开发时,一定要把AVB的校验逻辑跑通再往下走。否则后面出了问题,排查起来真的很痛苦。