一、 device-mapper基本原理介绍
1.1 dm工作原理
1.2 dm实现动态卷(逻辑分区)功能介绍(dm-linear)
1.3 dm 实现完整性校验功能介绍(dm-verity)
1.4 元数据加密(default-key)
1.5 dm实现快照功能介绍(snapshot,snapshot-origin,dm-snapshot-merge,bow)
二、vold介绍
2.1 vold结构总览
2.2 encryptFstab(元数据加解密)
2.3 mountFstab(挂载分区)
2.4 fbeEnable(使能fbe加密)
2.5 initUser0
三、分区挂载流程介绍
3.1 分区挂载顺序总览
3.2 metadata分区挂载流程
3.3 system分区挂载流程
3.4 userdata分区挂载流程
四、 常见问题汇总
4.1 set_policy_failed问题
4.2 init_user0_failed问题
4.3 enablefilecrypto_failed问题
4.4 userdata挂载失败问题
4.5 关机时ServiceManager crash导致vold shutdown超时死机重启
前言:
还没开始深入了解分区挂载原理时,觉得分区挂载涉及的内容应该不会很多。就是在分区表中找到对应的物理块,然后直接mount就完成了,最多就是必要了解一些流程顺序上的内容。
当真正开始走读分区挂载流程代码时,才发现之前的想法太天真了。如果要深入了解分区挂载原理,必要了解的内容很多。
在远古android时期,挂载真的如上面描述的,仅仅是一个挂载(一个物理分区由文件系统管理起来)
但是随着对分区使用效率,安全等提出了越来越多的要求,分区挂载涉及的内容也越来越多。
例如:为了办理system和vender中分区size不能动态调解的问题引入动态分区概念(逻辑分区),将system,vendor中分区打包放在了super分区下;system等关键分区为了防止篡改,在挂载或使用时,必要这些分区进行完整性校验;为了用户数据的安全,对userdata分区中的数据必要加密,加密方式也有较早的FDE( Full-disk encryption全盘加密),发展到现在更精致化的FBE(File-based Encryption文件级加密),而且针对userdata还增加了 metadata(元数据)级别的加密;系统分区也有之前的单分区进recovery下升级,到A/B分区到recovery下升级,再到现在的VAB分区架构,借用snapshot(快照)技能完成升级及回滚等等。
如果要对分区挂载有一个完整系统的了解,上面提到的那些技能都是绕不开的。但是每一项技能深入到代码细节介绍的话是不现实的,而且也不是我们这篇文章的重点。
所以下面我们先对用到的技能做一些原理介绍,了解他们是怎么工作的,不深入代码。对这些技能有基本的了解后,我们对整个分区挂载逻辑深入到代码级别去分析。
一、device-mapper基本原理介绍
1.1 dm工作原理
device-mapper是linux块装备映射技能框架。我们挂载用到的动态卷(逻辑分区),完整性校验(dm-verity),vab升级用到snapshot(快照)技能都离不开它。
它是在块装备上的一层映射,对外来看,它也是一个块装备(假造块装备)。它有三个紧张概念,映射装备(mapped device),映射表(map table),目的装备(traget device)。这样说大概比较抽象。
我们可以种别成图书馆场景来了解一下它的工作原理。
图书馆中摆着一排排书,每一本书就像是我们块装备中的物理块。一排书架可以看做是一个块装备(由一个个物理块构成)。我们在存放书时,按照书的特性进行分类存放,例如经济类的书存放在一个书架,人文历史的书存放在另一个书架。这样的利益就是我们可以根据我们要拿书的种类去快速找到我们必要的书。
但是某一天图书馆的领导换了,他觉得根据书种类搜刮的方法太单一了,还希望能通过书名首字母方式快速找到必要的书。这应该怎么办理呢?有一个办法就是再建一个图书馆,买一模一样的书。然后书架上按照首字母的方式分列。这样你想按首字母找也行,想按类型找也行,显然是不大概的。
这时候有一种办理方案就是建立了一个假造书架(假造块装备),这个假造书架对外来看,和其他书架没有区别,但是现实上面摆放的只是一张张小卡片,这些小卡片上面写的书名以及这本书真正摆放的物理位置(映射表),而且是按照拼音首字母排序的。当我们去这个书架上拿书的时候,图书管理员(device mapper驱动)会根据卡片上的内容,快速到这本书真正摆放的位置(目的装备),拿到真正的书,并给到我们。对我们来说,我们也可以在这个假造书架上拿到真正的书,所以用起来和其他现实的物理书架也没有什么区别。这个就是device-mapper的工作原理。
所以我们看到device-mapper工作只是增加了一层映射,对外来看它也是一个块装备,在现实访问时,device-mapper的区别会根据映射表,去真正块装备(目的装备)上的物理块上帮我们拿到必要的数据返回给我们。
Device-mapper使用起来也比较机动
一个假造书架的目的装备也可以是另一个假造书架,这也很好理解,因为对外来看,假造书架和物理书架使用起来并没有什么区别。
也可以只映射一个物理分区的部门块,同样也可以将两个物理分区中的内容映射到同一个假造块装备中。后面的章节会介绍这种特性的使用。
dmctl list targets
可以检察dm装备支持的不同类型,后面章节介绍的一些功能其实都是不同dm装备类型的应用。dmctl list targets可以看到手机中支持的不同targets
手机中的dm装备及targets列表如下
dm装备名
| target
| my_engineering_a
| linear
| my_heytap_a
| linear
| system_ext_a
| linear
| userdata
| default-key
| vendor_dlkm-verity
| verity
| odm_a
| linear
| system_a
| linear
| vendor-verity
| verity
| vendor_dlkm_a
| linear
| vendor_a
| linear
| system_ext-verity
| verity
| my_region_a
| linear
| my_preload_a
| linear
| my_bigball_a
| linear
| my_carrier_a
| linear
| odm_dlkm_a
| linear
| my_product_a
| linear
| product-verity
| verity
| system-verity
| verity
| product_a
| linear
| my_manifest_a
| linear
| my_company_a
| linear
| my_stock_a
| linear
| 1.2 dm实现动态卷(逻辑分区)功能介绍(dm-linear)
上面一节介绍了device-mapper的基本原理,了解到device-mapper假造装备是通过映射表映射到目的装备的,那在映射物理装备时,是否可以只映射某个物理装备的部门块呢?答案是可以的,逻辑分区就是借助这个功能实现的
物理分区和逻辑分区有什么区别呢?简单理解如下
物理分区是SMT刷机的时候就规划好的,也就是我们常说的分区表内里划分的分区,物理分区巨细是固定的,轻易不会改变
逻辑分区是一个动态卷的概念,一个物理分区可以划分为多个逻辑分区,而且这些逻辑分区的巨细是随时可以改变的。
/dev/block/bootdevice路径下面,sd*开头的都是一个个物理分区
/dev/block/by-name是对应的分区名称
我们在/dev/block/by-name下并没有看到system_ext,vendor之类的分区。是因为Android将system_ext,vendor这些分区看做逻辑分区,现实存放位置都存放在了super这个物理分区中。在机器开机时,借助device-mapper的技能,通过映射表将super分区中的system_ext.img,vendor.img映射成dm装备,之后再去挂载对应的dm装备(其实还有一个verity过程,之后我们再介绍,为了更好理解本解内容,我们先通过adb disable-verity跳过这个过程,重启手机)。
我们通过如下命令检察一下现在system_ext 和vendor的挂载结构
a.检察挂载点
mount | grep -e “/system_ext” -e “/vendor ”
可以看到/system_ext /vendor现实挂载的是dm这个假造装备,类似如下图
b.检察dm devices
dmctl list devices 命令可以检察现在运动的dm装备
Dmctl getpath可以获取对应dm装备的路径
c.检察对应装备的映射表
dmctl table 命令可以检察现在dm装备的映射表内容
以system_ext_a为例
输出内容寄义如下
super分区的装备号为8:6
所以根据上面信息,可以看到system_ext_a为dm映射出来的假造装备,这个假造装备的LBA 0到2048144 线性映射了super这个物理分区,起始LBA地点为1347584的块(super物理分区)
可以得出如挂载图
以及如下映射图
LBA为逻辑地点,单元应该为512字节,我们来验证一下
super分区的LBA:1347584开始的内容是否和映射出来的假造dm-1装备完全一样
取super分区LBA:1347584 开始的1024*512字节的内容与dm-1 LBA:0 开始的1024*512字节的内容对比
dd if=/dev/block/by-name/super of=/sdcard/super_1347584.bin bs=512 count=1024 skip=1347584
dd if=/dev/block/dm-1 of=/sdcard/dm_1.bin bs=512 count=1024
可以看到内容是完全一样的
1.3 dm 实现完整性校验功能介绍(dm-verity)
在之前一节介绍动态卷功能时,我们实验了一条命令adb disable-verity。这条命令的作用是关闭dm-verity 功能。那这个功能又是做什么的呢?
dm-verity是Device mapper架构下的一种目的装备类型,该功能提供对块装备的透明完整性检查。
他工作原理如下
dm装备会把它自己按照4K单元进行分割,之后计算每个4K数据的hash值并保存起来,生成第0层,之后再把第0层的hash值放在一起,依旧按照4K巨细继承计算hash值。生成第1层,以此类推,之后计算成一个root hash值。
Hash tree是保存在对应的分区中的(system,vendor等)。root hash保存到另一个有签名认证的分区中(vbmeta_system,vbmeta_vendor分区)。在开机引导的时候,会对root hash只的vbmeta分区进行签名校验,包管root hash是没有被篡改的。进一步包管整个用于校验的hash tree没有被篡改。
在运行时,当访问到某个块时,会计算这个块的hash值,之后与hash tree中的hash值对比,以此包管镜像的完整性。
下面我们依旧以vendor分区为例,看看整个挂载的结构是什么样的(没有实验adb disable-verity)
先看看vendor分区挂载的是哪个块装备
mount | grep "/vendor "
Vendor分区挂载的是dm-20装备,对应的dm名称为vendor-verity
dmctl getpath vendor-verity
dm-20的目的装备是另一个dm装备,这个也很好理解,dm装备对外来看,也是一个块装备,使用起来和其他块装备没有什么区别,固然在dm映射的时候,目的装备也可以是另一个dm装备
dmctl list devices
dmctl getpath vendor-verity
dmctl getpath vendor_a
dmctl table vendor-verity
dmctl table vendor_a
按照上面我们检察的结果,可以得出如下的vendor分区挂载结构。
1.4 元数据加密(default-key)
在第一节中,我们看到了userdata dm装备的target类型为default-key。这个又是什么功能呢?
default-key 为元数据加密功能。
Userdata存放的是用户的敏感数据,必要进行加密保护。例如我们知道的之前android使用的FDE(全盘加密),到现在使用的FBE(文件系统级加密,后面章节会介绍)。FBE从字面意思也可以看得出,加密的内容是文件内容和文件名,但是其他信息(例如目录结构、文件巨细、权限和创建/修改时间)不会被加密。这些其他信息统称为“文件系统元数据”。
借助dm-default-key,就可以对这些”元数据进行加密”。在元数据密钥可用前 ,userdata分区中的全部内容均是无法读取的。所以元数据密钥不能存放在userdata分区,而存放在metadata这个分区中。
在开机过程中,会先挂载metadata分区,之后使用metadata分区中保存的key对userdata先辈行元数据解密。然后就可以进行挂载了。(userdata分区元数据加密只能在分区初次进行格式化时设置)
1.5 dm实现快照功能介绍(snapshot,snapshot-origin,dm-snapshot-merge,bow)
dm框架的使用十分机动,除了上面提到的将一个物理装备的部门区域,映射成一个dm装备。同样也可以将两个物理分区中的内容映射到同一个假造块装备中。
类似如下结构
这样对外来看,是一个独立的块装备,但现实这个块装备中存放的内容,大概是分散在多个不同的物理分区中。
VAB(假造A/B分区)升级时,就借助的这一特性。
在A/B分区刚出来时,A/B分区是实实在在的两个物理分区。例如system分区必要2GB,那A/B架构就必要预留两个system分区作为互为备份升级。这种对空间的使用率是极低的。
vab架构就很好的办理了这个问题。
Vab应用了dm-snapshot技能,下面是google文档中的一些阐明
使用 dm-snapshot 时,会用到以下四个装备
- 底子装备是被捕获快照的装备。在此页面上,底子装备始终是动态分区,例如 system 或 vendor。
- 写入时复制 (COW) 装备,用于向底子装备记录更改。 该装备的巨细没有限制,只要足够容纳对底子装备的全部更改即可。
- 快照装备,这是使用 snapshot 目的创建的装备。需向快照装备写入的内容将写入 COW 装备。需从快照装备读取的内容将从底子装备或 COW 装备读取,详细取决于所访问的数据是否颠末快照更改。
- 源装备,这是使用 snapshot-origin 目的创建的装备。需从源装备读取的内容将直接从底子装备读取。需向源装备写入的内容将直接写入底子装备,但原始数据将通过写入 COW 装备进行备份。
二、vold介绍
2.1 vold结构总览
Vold(volume Daemon),既Volume守护历程,这个守护历程作为kernel 和framework之间的桥梁,主要处理如下内容
- 开关机过程中各分区的挂载/卸载
- 外部T卡/OTG装备的文件系统挂载/卸载
在mount过程中,涉及的文件系统级加解密(FBE),元数据(metadata)加解密,文件节点创建等也由Vold的进行控制
Vold架构在整个系统中的位置如下
Vold服务是通过rc文件的方式启动的
system/vold/vold.rc文件中定义了vold service
之后在init.rc中,early-fs阶段,start vold启动
Vold相干代码在如下路径下
system/vold/main.cpp
VoldNative service主要处理那些内容呢?
a.StorageManagerService/vdc等通过IVoldListener下发的各种操纵命令,从VoldNativeService.h 文件中,我们可以找到VoldNativeService可以处理的操纵
之后的章节,我们会选取一些一样平常工作中常用到的处理命令,对这些命令提供的本领(机制)进行深入分析,详细这些本领如何使用,在哪里使用等”策略”在后面的章节中讨论
VoldNativeService处理的命令
| 主要功能总结阐明
| encryptFstab
| 生成元数据加密的key,然后对指定分区进行元数据加密,生成对应的dm装备(default-key),之后挂载这个dm装备
| mountFstab
| encryptFstab类似,不过不必要重新生成key,去指定的路径读取存放的元数据解密key,根据key生成dm装备(default-key),之后挂载这个装备
| fbeEnable
| 如果是初次开机
创建 System DE Master Key 和生成 System DE Encryption Policy
后续每次开机
加载System DE Master Key,准备System DE Master storage
每次开机还会创建并加载一个新的per_boot Master Key,准备per_boot Master storage。
| initUser0
| 如果是初次开机(根据user DE key存放路径判定),
创建 User 0 DE Master Key 和生成 User 0 DE Encryption Policy;
创建 User 0 CE Master Key 和生成 User 0 CE Encryption Policy;
创建 User 0 DE Storage,并为这些文件夹设置 User 0 DE Encryption Policy
后续开机
加载 User 0 DE Master Key
准备 User 0 DE Storage,并校验文件夹 Encryption Policy
| 2.2 encryptFstab(元数据加解密)
主要处理函数为
fscrypt_mount_metadata_encrypted。这个函数重点流程如下
重点流程一:
从fstab中读取传入的mount_point,获取挂载点的相干设置参数。然后解析成用于加密的options
主要函数
- auto data_rec = GetEntryForMountPoint(&fstab_default, mount_point);
复制代码 if (!parse_options(data_rec->metadata_encryption_options, &options)) return
false;
重点流程二:
- 在dm设备介绍时,我们了解到了target类型为default-key的dm设备。这类型设备是基于原数据加密的,在metadata解密前,被加密的分区是无法被访问的。所以用于加解密的key就不能存放在被加密的分区。所以encryptFstab进行元数据加密的时候,需要根据指定存解密key的文件夹路径,也就是metadata_key_dir。
复制代码- 这里的重点流程就是根据(needs_encrypt 参数)传入的needs_encrypt参数,判断是新创建一个key还是使用已经存在的key。
复制代码- 情况一,需要进行元数据加密(needs_encrypt=true),那就创建一个KeyGeneration,然后存放到metadata_key_dir下。encryptFstab流程中,needs_encrypt=true,就是这种情况
复制代码- 情况二,不需要进行元数据加密,就会去指定目录读取对应的key,并返回
复制代码- auto gen = needs_encrypt ? makeGen(options) : neverGen();
复制代码- if (!read_key(data_rec->metadata_key_dir, gen, &key))
复制代码 重点流程三:
前面流程走完以后,我们已经获取到用于元数据加解密的key了(无论是新生成的还是已存在的)。下一步就是创建default-key类型的dm装备
主要处理函数
- if (!create_crypto_blk_dev(kDmNameUserdata, blk_device, key, options, &crypto_blkdev,
复制代码 重点流程四:
此时我们已经得到了一个已经完成元数据解密的dm装备了。如果这个dm装备是新创建的(needs_encrypt=true),那就必要根据should_format参数决定是否要格式化这个dm装备了。为什么要格式化它的?
原因是如果一个分区刚刚完成needs_encrypt加密,那这个分区之前保存的任何数据也是无意义的。所以我们可以选择在完整元数据加解密后的第一时间,针对已解密的dm装备进行格式化。
之前dm装备(default-key)介绍时也提到过,数据加密只能在分区初次进行格式化时设置。原因也是一样的。加密会让目的装备中保存的数据无效。
主要代码也比较简单,就直接针对解密后的dm装备进行格式化就可以
重点流程五:
历经前面的步调,我们得到了一个解密后的dm装备,而且用于解密的key也保存到了指定的metadata_key_dir目录下。下一步就针对dm装备进行挂载了
主要的处理函数是:
mount_via_fs_mgr(mount_point.c_str(), crypto_blkdev.c_str());
这个和其他平凡分区挂载一样,主要是判定一些挂载参数,最后通过mount进行挂载。就不在这里展开了
2.3 mountFstab(挂载分区)
MountFstab处理函数与encryptFstab 基本一样,只是传入的参数有一些区别。流程和encryptFstab是类似的,只是不去重新创建元数据加密的key,而是直接去指定路径去读取用于元数据解密的key,之后同样是生成dm装备,然后挂载。这里就不再赘述。重点流程可以看encryptFstab 一章
2.4 fbeEnable(使能fbe加密)
主要处理函数为
fscrypt_initialize_systemwide_keys。这个函数重点流程如下
重点流程一:
装备第一次启动时:
创建 System DE Master Key 和生成 System DE Encryption Policy;
把 System DE Encryption Policy 保存到文件 /data/unencrypted/ref;
后续每次启动时:
加载 System DE Master Key;
主要的函数为
- rtn = retrieveOrGenerateKey(device_key_path, device_key_temp, kEmptyAuthentication,
复制代码- makeGen(options), &device_key);
复制代码 重点流程二:
拿到System DE master Key之后,就会使用加载起来的System DE Master Key,安装到对应的/data目录,注意此时并不是真正的解密,解密是在对文件进行I/O操纵的时候才会去解密。安装可以理解为我们把账key交给将来负责解密的管理员了。之后如果有效户要对 System DE Storage路径下的文件进行操纵,就可以使用安装好的key来完成真正的解密
主要的函数为
- install_storage_key(DATA_MNT_POINT, options, device_key, &device_policy);
复制代码 之后会将key的参考信息及加密策略都存放到/data/unencrypted/ref下,供后续必要设置加密策略的流程中快速读取使用
重点流程三:
上面提到的System DE加密是在初次开机时完成的加密,只要不规复出厂设置这个key就一直不会改变。
除了System DE加密,fscrypt_initialize_systemwide_keys还会安装一个每次开机使用的key
per_boot_key。
主要处理函数为:
- rtn = generateStorageKey(makeGen(options), &per_boot_key);
复制代码 rtn = install_storage_key(DATA_MNT_POINT, options, per_boot_key, &per_
boot_policy);
同样会把per_boot_key的参考信息及加密策略都存放到/data/unencrypted/per_boot_ref下,供后续必要设置加密策略的流程中快速读取使用
2.5 initUser0
这个函数的主要作用就是针对user0用户,进行fbe加解密初始化。主要处理函数为fscrypt_init_user0。下面就分析一下这个函数的重点流程
重点流程一:
准备好存放User.0 DE Storage和User.0 CE Storage key的路径
“/data/misc/vold/user_keys/de”
“/data/misc/vold/user_keys/ce”
之后根据/data/misc/vold/user_keys/de 是否存在,来判定机器是否是初次开机
/data/misc/vold这个路径System DE加密的,在之条件到的流程中已经对这个目录安装好所需的解密keyring,此时已经可以正常访问这个目录下的路径了。在后续的挂载流程分析中,会详细分析这些流程。此时可以以为/data/misc/vold已经完成解密就可以了
情况一:/data/misc/vold/user_keys/de 路径不存在(初次开机)
调用create_and_install_user_keys
通过generateStorageKey生成两个key(de_key,ce_key)
storeKeyAtomically将两个key(de_key,ce_key)存放到对应路径下()/data/misc/vold/user_keys/de,/data/misc/vold/user_keys/ce
之后调用install_storage_key 给指定目录安装对应的storage Encryption Policy
情况二:/data/misc/vold/user_keys/de存在(非初次开机)
正面de_key和ce_key之前已经生成了,就不必要生成了,直接走下面的流程就可以
重点流程二:
至此,两个key已经正常存在了,下一步就是load_all_de_keys,安装de_key
主要处理函数:
- if (!load_all_de_keys()) return false;
复制代码 重点流程三:
- fscrypt_prepare_user_storage("", 0, 0, android::os::IVold::STORAGE_FLAG_DE)
复制代码 准备user 0的DE 目录,并给对应的目录指定Encryption Policy(加密策略)
2.6 unlockUserKey
由于篇幅过长,第三、四章节将在下周发布~
往
期
推
荐
Linux内核并发与同步机制解读(arm64)上
Linux内核并发与同步机制解读(arm64)下
crash实战:手把手教你使用crash分析内核dump
长按关注内核工匠微信
Linux内核黑科技| 技能文章| 精选教程
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |