版权声明:本文为博主原创文章,未经博主允许不得转载。
目录(?)[-]
之前做了一个Verified Boot模块相关的工作,但是在网上只有找到google的文档和一个nexus的patch。虽然有patch,但在不同版本的代码上实现起来却可能有一些bug,所以特此记录一下debug这个东西的过程。之前debug的过程一直没找到问题,归根到底还是这个原理没搞清楚就下手,所以我分成原理,接口和应用来说明dm-verity verified boot是怎么去实现。其实接口部分也大多是原理,之所以说接口,是因为我觉得里面含有一些应用的成分。那么本文使用的是6.0的Android,4.1的kernel的代码。
相关的代码
build/tools/releasetools/build_image.py
system/extras/verity/build_verity_tree.cpp
system/extras/verity/build_verity_metadata.py
Android中Verified Boot有分好多种,这里只涉及到了dm-verity这个东西,所以直接从dm-verity那里开始入手,至于bootloader等其他的就不多说。
Due to its large size, the system partition typically cannot be verified similarly to previous parts but must be verified as it’s being accessed instead using the dm-verity kernel driver or a similar solution.
Android官网上一段话就说明了这个问题,可见dm-verity的速度很快。
Android 官网上有这么一张图片说明了dm-verity的流程。当bootloader的verify过了之后就进入到system分区等的verify,这个时候就是dm-verity出场的时候。此时就到了图中start的这个问题,先判断dm-verity是否是enforcing的状态,如果是,就去扫描一把需要verity的分区,具体实现就是验证一下这个对应分区的metadata。如果扫描出没问题,ok,mount系统,如果扫描出有问题,dm-verity会向kernel发一个reboot的信号,并且将dm-verity的状态设置成loggin。重启之后,再回到start的地方,此时dm-verity的状态已经是loggin了,所以走向红色的那一块区域,此时就会显示出一个警告的界面,让用户去选择mount还是不mount。后面的工作都会围绕这张流程图展开。在不同的板块去分解这张流程图的实现。
在了解dm-verity的工作流程之后,我们简要的看一下dm-verity的实现。dm-verity实现的关键在于metadata是如何创建并且存储的。在Android官网上定义了这么一串步骤,如何去创建dm-verity所需要用的步骤。
- Generate an ext4 system image.
- Generate a hash tree for that image.
- Build a dm-verity table for that hash tree.
- Sign that dm-verity table to produce a table signature.
- Bundle the table signature and dm-verity table into verity metadata.
- Concatenate the system image, the verity metadata, and the hash tree.
如何实现Dm-verity
上面给出官网解释了如何去实现这几个步骤,偏原理的解释,在这通过代码来描述一下Dm-verity是如何去实现的。这些实现的步骤都在build_image.py 这个build脚本中。
Generate an ext4 system image
可以在build脚本中通过RunCommand(build_command)生成了ext4的文件系统。至于这个build_command是什么有兴趣的朋友可以去看一看build_image.py,比较长,这里就不贴出来了。
Generate a hash tree for that image
在build脚本中有这么一段是描述了如何去构建hashtree的。实际上,这棵hash tree的作用就是用来描述system.img的变化。
通过build_verity_tree 这个程序去构建hash tree,所以构建hash tree 的算法就在这个build_verity_tree.cpp 文件中, 那么算法怎么实现的,就不去管它了。有点复杂。至于使用hash tree的原因官网上也提到了,最初他使用hash table来实现,后来数据太大之后,效率不好,所以使用hash tree,hash tree在处理大量数据的时候效率就非常高。产生的hash tree的结构就如下所示, 最后只有一个根hash,叶节点就是dm-verity所需要verify的分区的划分的一个个小块,它这里规定了每个块以4k的大小来划分。所以举个例子,要验证的system分区如果有800M,那么就有200万个块。所以说通过叶节点以及他的父hash到根hash就是描述了system.img的变化情况。这样的话用hash table来存效率就很差,所以使用hash tree来存速度更快。最后呢再主要保存这个root hash。
[ root ]
/ . . . \
[entry_0] [entry_1]
/ . . . \ . . . \
[entry_0_0] . . . [entry_0_127] . . . . [entry_1_127]
/ ... \ / . . . \ / \
blk_0 ... blk_127 blk_16256 blk_16383 blk_32640 . . . blk_32767
Build a dm-verity table for that hash tree
Sign that dm-verity table to produce a table signature.
Bundle the table signature and dm-verity table into verity metadata
Ok,可以看到这段代码最终调用到了build_verity_metadata.py这个脚本。看一下他是如何建立metadata的。正好三部曲根上面的三部对应起来。
看一下如何建立verity-table。实际上,verity-table就是为了去描述之前生成的hash tree。
所以建立起来的verity table形如下面这样。说白了,verity-table只是一个描述hashtree的字符串,看一看他是如何描述hash tree的。下面这个例子选自linux 文档,并非实际的system.img的hash tree的verity table。
第一个参数是版本,只有0和1,大多数情况下填1,不去深究。
第二个,第三个参数描述的是所保护的分区,这个例子中dm-verity保护的分区是/dev/sda1。
第四,第五个参数描述的该分区的每个block即hash tree的叶节点的大小,可以看到这里是4k,就是说,以4k为大小划分/dev/sda1为若干个区域。
第六,第七个参数描述了总共有多少个block,也就是hash tree有多少个叶节点。
第八个参数是hash 加密算法,这里使用sha256算法。
第九个参数是hash tree的根hash。
第十个参数是加密算法加的盐。
Ok,到这我们可以看到verity-table描述了叶节点和根hash以及hash的算法等。这样就通过一个字符串就把整棵树的形状就描绘出来了。
1 /dev/sda1 /dev/sda1 4096 4096 262144 262144 sha256 4392712ba01368efdf14b05c76f9e4df0d53664630b5d48632ed17a137f39076
1234000000000000000000000000000000000000000000000000000000000000
verity table建立完后,对他进行签名,签名的格式可以参看Implementing verify boot那一章。签完名就把verity-table,签名信息和hash tree 一同写入到metadata中,最后返回给build脚本。
Concatenate the system image, the verity metadata, and the hash tree.
最后通过append2img这个命令吧metadata.img和metadata_verity.img附在system.img后面,这样就算是实现了dm-verity的metadata建立的过程。
至此,还未分析到流程图的具体流程的实现,那么在下一个段落开始就会去看一下流程图的具体实现。
相关的代码
/kernel/include/upai/linux/dm-ioctl.h
/kernel/include/linux/device-mapper.h
/kernel/driver/md/dm-verity.c
/kernel/driver/md/dm-table.c
/kernel/dirver/md/dm-ioctl.c
/system/core/fs_mgr/fs_mgr_verity.c
要知道如何使用dm-verity,先需要了解一下Device mapeer框架,device mapper为何物呢?Device mapper 是 能让用户自己定制管理块设备的策略的一套框架,使用这套框架去写管理块设备的策略比直接去管理块设备肯定要轻松许多。这里就不多讲Device Mapper原理。下面这篇文章对Device Mapper做了详细的分析。下面仅就简单介绍一下一些需要在后面实现verified boot中需要用到的接口。
Linux 内核中的 Device Mapper 机制
一些重要的结构体
我们知道Device Mapper对用户控件提供的接口就是文件系统的接口,open,read,write啊什么的,和ioctl接口。这里就罗列几个ioctl的命令可以看一下。其他都类似。
dm_ioctl这个结构体定义在dm-ioctl.h中。截取几个字段可以看一下,他主要是用于ioctl的时候传入给内核的参数的集合。其中io就是一个dm-ioctl的对象。用户空间使用ioctl的步骤是,先创建一个dm-ioctl和dm_target_spec对象,然后配置一下他们的参数,然后在dm_target_spec后面跟一个特定设备的特定参数(special param),将三者结合到dm-ioctl上,通过调用一下命令就可以在device mapper中load一个dm设备了。
那么问题来了,ioctl是向device mapper发命令,如何去找到具体的dm设备呢。这个时候另外一个结构就出场了。同样定义在dm-ioctl.h中。
可以看到他有四个字段,那最重要的是target_type[DM_MAX_TYPE_NAME]这个字符串,device mapper就是通过这个字符串从他的设备表中找到相应的dm设备。举个例子,我们这里需要找的是dm-verity,那么这个字符串就是”verity”。那么这个字符串必须是这个嘛?不是的,后面会讲这个字符串的起源地。
target_type 定义在include/linux/device-mapper.h 中,这个结构体就代表一个真实的dm设备。也就是说dm设备最终就是去实现这个接口就ok了,那么对于开发一个dm设备就非常简单了,只需要去实现这个接口,并向device mapper框架注册一把就可以了。
他里面罗列了许多字段,这里就列出了几个字段。ctr就是create函数,dtr:destory,跟Activity中的oncreate和ondestory简直一模一样。其中的name字段就是对应到之前dm_target_spec 的target_type字段。也就是说这个设备的名字的发源地就是在这。你这定义了“a”,那么在用户空间传参数的时候target_type就定义成“a”。
之前简要的介绍了一下Device mapper一些结构体和ioctl的使用,那接下来看看dm-verity在Device Mapper下是如何实现的。其实就是实现一个target_type 这个接口就行了
/kernel/driver/md/dm-verity.c
果然,在verity_target 中name字段定了成了verity,所以我们在用户空间设置dm_target_spec的target_type字段的时候就设置成verity。后面还要涉及到的一个重要的接口就是ctr,他在ioctl load table的时候会去调用到这个接口。当然这个verity_target 对象是在内核模块初始化的时候就被注册到device mapper中。
那么dm-verity的mode的改变是在什么时候进行呢?首先要知道dm-verity有三种工作模式,如下:
EIO:不挂载被破坏的分区。0
LOGGIN:忽略破坏的分区,继续挂载该分区。1
RESTART:发现分区被破坏,直接重启系统。2
那么暂且不谈用户空间如何去对dm-verity进行控制,先看在kernel里dm-verity状态的改变。dm-verity状态的改变是在dm-verity被load的时候发生的,dm-verity load一个dm设备的时候会调用该设备的target type的crt接口。在这里整理下dm-verity的调用流程。ioctl DM_TABLE_LOAD下去,调用到device mapper,device mapper通过传下来的的参数找到dm-verity,最终调用到ctr接口。
ioctl(fd, DM_TABLE_LOAD, io)—–>Device Mapper—通过target_type=“verity”–>dm-verity.ctr()
截取部分代码来看一看dm-verity中crt接口的实现。
在上述代码之前,在crt函数里会先处理之前生成的verity-table,验证其有效性,具体可以参考源代码中完整的代码。那么从上述代码可以看到,dm-verity状态v->mode的改变是由传进去的特定参数决定,之前就讲到用户空间对dm-verity进行控制的话需要传三个东西进去,一个dm-ioctl,一个是dm_target_spec, 还有一个就是特定设备的特定参数,这个特定参数在这里就派上用场了。那么这个参数该如何定义呢,我们可以看一看dm-verity文档中的定义。
Construction Parameters
=============================================
[<#opt_params> ]
在opt_params之前就是之前所提到的由build脚本生成的verity table正好能对起来。再来看一遍verity-table。
1 /dev/sda1 /dev/sda1 4096 4096 262144 262144 sha256 4392712ba01368efdf14b05c76f9e4df0d53664630b5d48632ed17a137f39076
1234000000000000000000000000000000000000000000000000000000000000
那么#opt_params是什么呢,#opt_params代表在verity table 后面需要跟的参数的个数,也就是argc,opt_params代表后面跟的参数,也就是argv,后面的参数有许多种,这里有可选参数的详细介绍。
根据kernel里crt函数中的实现看到,dm-verity对于opt_params一个一个遍历,然后做出相应的控制。dm-verity的mode是根据传进来的参数设置的。在dm-verity也只有在ctr这个地方才能去设置dm-verity的状态。当然如果不设置,他的默认mode是0,也就是EIO模式。
再看一下dm-verity中对应三种状态对是如何工作的。当dm-verity扫描出分区有损坏的时候,会触发dm-verity.c中一个回调函数–verity_handle_err。同样截取部分代码来看一看这个回调函数里如何处理不同的状态。
可以看到,如果是默认状态,就返回1,返回1之后,系统在mount的那边的代码就会不去挂载该被破坏的分区。如果是DM_VERITY_MODE_LOGGING的状态1,就返回0,那么系统在mount那块的代码就会忽略破坏,继续挂载该分区。如果是DM_VERITY_MODE_RESTART,就是状态2, 那么就重启系统。OK,至此就可以看到dm-verity中对于三种状态如何处理了。
OK,这里就解释了流程图这两段逻辑是如何实现的,图1中dm-verity在处于restart模式发生corruption之后重启的逻辑对应于
图2中dm-verity在处于loggin模式发生corruption之后忽略corruption继续挂载系统的逻辑对应于
默认的EIO模式没在这张流程图里看到,显然google没有推荐使用这种模式。其实我觉得这种模式确实没什么用,这种模式在发生corruption之后直接选择不挂载,这样会导致系统hang住,也没什么意义。
知道了dm-verity的相关接口后,就可以在用户空间对他进行初始化了。在Android中fs_mgr_verity负责用户空间对Dm-verity进行控制的,其中有个load_verity_table这个函数展示了如何从用户空间去使用dm-verity。先贴出代码。
根据之前讲的,在device mapper框架下使用dm设备首先要创建一个dm-ioctl对象,并且初始化了这个对象。这个函数就在fs_mgr_verity.c 中可以找到。
紧接着创建一个dm_target_spec对象,把它append在dm-iotcl的后面,初始化一下他的字段,其中可以看到将target_type设置成verity,这样通过ioctl就可以在device mapper框架下找到dm-verity这个设备了。
这个时候已经完成了ioctl dm-verity的两部曲了,最后一步就是加上特定设备的特定参数,在这里就是dm-verity需要的参数。将verity table,opt_params和mode值拼接给
verity_params,就是之前讲的在dm-verity的kernel中的特定设备的参数。
我在这就不详细介绍了,那么我后面需要用到两个参数,一个是ignore_corruption,还有一个restart_on_corruption,这两个值分别代表了之前提到的dm-verity的loggin mode 和 enforcing mode/restart mode。这样我们就可以在用户空间控制dm-verity的运行模式了。这两种不同的参数可以对应到之前kernel里dm-verity ctr中的相应逻辑。
至此,就解释了如何在用户空间设置dm-verity,也就解释了流程图中Setup dm-verity这段逻辑
相关的代码
/system/extras/slideshow/slideshow.cpp
/system/core/fs_mgr/fs_mgr_verity.c
/device/(vendor)/init.rc
/device/(vendor)/fstab.vendor
/system/core/init/builtins.c
有了之前的原理和接口的介绍后,如何去实现就变得非常简单了。AOSP中有一个patch就是去enable它的,当时就是直接使用了这个patch,但是由于之前原理不清楚,加上kernel跟android代码对不上号,导致调试的时候出了一些bug,也不知道怎么搞,所以还是要把官网的文档看熟练,再去实现就很简单了。那这里我就以这个patch来描述一下这个事是如何去实现那张流程图的。先贴出这个patch关键的代码。后面一段段解释相关的代码。
先看patch中fstab一段,在system分区的fs_mgr_flags中添加
verify=/dev/block/platform/msm_sdcc.1/by-name/metadata,那么这个path表示什么呢?这个位置用于保存dm-verity的mode。也就是在流程图中,dm-verity重启之后怎么知道要进入loggin mode 的,就是将这个值存在了这个metadata分区中,metadata分区并不是之前所说的metadata.img,metadata.img是append在system.img之后的,也就是metadata.img是在system分区下,需要注意。
-/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,barrier=1 wait
+/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,barrier=1 wait,verify=/dev/block/platform/msm_sdcc.1/by-name/metadata
fstab的这个patch的工作原理非常简单,如果不加这个地址的话,那么dm-verity会一直工作在EIO状态下,在这个状态下,如果dm-verity检测出分区有毛病,他就不会挂载分区,不会重启和忽略崩溃的分区。那么我们就来看一看如果不加地址他为什么只会工作在EIO状态下,导致流程不会按照既定的流程进行呢?这个不属于这个patch之内实现的东西,但其实也是实现dm-verity流程图相当重要的一部分,所以还是拿出来分析一下。真正设置dm-verity工作模式的代码在fs_mgr_verity.c中的fs_mgr_setup_verity这个函数。不贴出所有代码了,只截取一些关键的代码来分析一下。
进到load_verity_state函数中看一看
再往下调用到read_verity_state,可以看到第一个参数fstab->verity_loc,这个值就是之前从fstab里解析出来的verity后面的地址。如果fstab里没有verity或者verity后面灭有相应地址,那么这个值是null。
可以看到如果路径不存在,直接报错,返回-1,此时load_verity_state(fstab, ¶ms.mode)就小于0,此时mode就设置为0。如果路径存在,ok,从中读取mode的值。
在回过头来看fs_mgr_setup_verity函数中拿到这个mode后干了些什么。
这时候load完mode之后就会走到上面这段代码中,可以看到最终调用到了load_verity_table这个函数。Ok, 这个函数我们之前在接口一章中提到了,去初始化dm_ioctl,dm_target_type两个结构体,并且绑定一个verity_params然后通过一个load table的ioctl命令传给kernel,去设置kernel中dm-verity的工作状态。kernel中dm-verity在handle error的时候就会根据不同的状态作出不同的动作。在上一个段落中已经分析了dm-verity.c中dm verity的ctr函数和handle error函数根据不同的mode如何工作的实现。再一次详细的解释了如何在用户空间实现流程图的这段逻辑。
接着看init.rc中的patch。
on init
+ # Load persistent dm-verity state
+ verity_load_state
on fs
+ # Update dm-verity state and set partition.*.verified properties
+ verity_update_state
+on verity-logging
+ exec u:r:slideshow:s0 -- /sbin/slideshow warning/verity_red_1 warning/verity_red_2
+
第一段是在 init这个action中去调用这个verity_load_state,这个命令最终会调用到中builtins.cpp中的do_verity_load_state,里面去判断dm-verity的mode是不是loggin,如果是loggin in就会触发verity-logging这个action。这个action调用了slideshow程序。不熟悉的可以看一看init.rc的语法和init进程相关的原理。如果不是那么就走其他的流程。
看代码实现
那么do_verity_load_state完成的就是调用的fs_mgr_verity.c中的函数。这个函数就是根据fstab里那个地址结合一些pstore的状态去读取并设置dm-verity的工作模式。这段代码比较简单,不对他进行分析了。这段代码就解释了流程图中第一个判断dm-verity工作模式的判断逻辑。抛开具体代码,就看init.rc中的verity_load_state目的就是为了实现它
那么之前讲到trigger了verity-logging这个action,最后他就去调用到了slideshow 这个程序,那么其实也就是实现了上面这个判断框为N的时候的路径。通过打开slideshow这个app来警告用户system分区已经被破坏,是否要设置loggin模式继续挂载。
那么执行的slideshow程序的功能就是显示两幅警告的图片,一副用来提示你让你选择要不要继续mount文件系统,另一幅提示你你已经忽略警告了。其实这个slideshow只能算一个皮包公司,原生代码里面基本是什么都不干,就显示了两幅图片,但是厂商可以定制这个app,起到一些控制的效果,或者直接替换掉这个slideshow app,直接去实现一个类似recovery的界面都没问题。有兴趣的朋友可以去看一看/system/extras/slideshow/slideshow.cpp 是怎么写的,非常简单,就一个文件,代码也就100多行。slideshow最后实现效果图如下面这样。也就是对应与上面这段流程中的具体实现。
dm-verity的verified boot的流程就分析到这,还有许多不完善的地方,也有许多地方都略过代码分析,主要要把所有相关源代码拿出来分析的话,篇幅就太长,而且太过于繁琐,只是说大致的分析了了一遍dm-verity的那张流程图的实现,当然真要透彻的理解dm-verity的话肯定还是需要去仔细的研究dm-verity和fs_mgr的代码,在每一个段落的开头贴出了相关的代码文件可以参考。这也算是工作到现在唯一一次碰到的没有java的关于Android的代码了,纪念一把。
转自http://blog.csdn.net/u011280717/article/details/51867673