根据内核文档,用户空间可以通过 veritysetup 命令创建块设备的哈希树,创建 mapped device 并启用 dm-verity 功能,我们来从内核的角度分析一下该命令如何创建激活 mapped device 并启用 dm-verity 功能。
device mapper驱动结构

上图展示了 device mapper 的结构,即一个 mapped device 拥有一个 mapping table ,这个表负责维护 mapped device 与 target device 之间的映射关系,这些 target 既可以是物理设备,也可以是另一个 mapped device 。
内核使用下图中的几个结构体来描述这种映射关系:
mapped_device 描述设备, dm_table 记录 md 设备下面有多少个 target , dm_target 与 target_type 共同描述了 target 的驱动, targe_type 则是存放操作 target 设备的方法。我们的 dm-verity 功能,就是其中一种 target_type 。
dm-verity模块初始化
内核中的 dm-verity 功能实现在内核源码树 drivers/md/dm-verity-target.c 中。
前面说到 dm-verity 功能是作为 target_type 来实现的,内核中的 target_type 使用链表进行管理,使用时通过 target_type.name 进行索引; dm-verity 模块初始化的过程就是将其对应的 target_type 结构体注册到链表上:
/** file: drivers/md/dm-verity-target.c*/static struct target_type verity_target = {.name = "verity",.version = {1, 4, 0},.module = THIS_MODULE,...};static int __init dm_verity_init(void){...r = dm_register_target(&verity_target);...}
/** file: drivers/md/dm-target.c*/static LIST_HEAD(_targets);int dm_register_target(struct target_type *tt){...if (__find_target_type(tt->name))rv = -EEXIST;elselist_add(&tt->list, &_targets);...}
veritysetup的参数
veritysetup 激活 dm-verity 功能需要提供:
mapped device设备名- 数据来源设备节点
- 哈希树设备节点
- 根哈希
这条命令其实包括了两个过程,创建veritysetup create <device name> <data device> <hashtree device> <root hash>
mapped device设备和处理mapped device与data devicehashtree device之间的关系。这两个过程均为使用ioctl向/dev/mapper/control发送命令来实现的。device mapper控制节点
/dev/mapper/control在内核中的描述如下: ```c /*- file: drivers/md/dm-ioctl.c */
static const struct file_operations _ctl_fops = { .open = nonseekable_open, .unlocked_ioctl = dm_ctl_ioctl, .compat_ioctl = dm_compat_ctl_ioctl, .owner = THIS_MODULE, .llseek = noop_llseek, };
static struct miscdevice _dm_misc = { .minor = MAPPER_CTRL_MINOR, .name = DM_NAME, .nodename = DM_DIR “/“ DM_CONTROL_NODE, .fops = &_ctl_fops };
通过 `ioctl` 对其进行访问,内核中的函数调用路径为: `dm_ctl_ioctl -> ctl_ioctl -> lookup_ioctl` 。 `lookup_ioctl` 再根据用户发送的命令,来返回不同的函数指针:```cstatic ioctl_fn lookup_ioctl(unsigned int cmd, int *ioctl_flags){static struct {int cmd;int flags;ioctl_fn fn;} _ioctls[] = {{DM_VERSION_CMD, 0, NULL}, /* version is dealt with elsewhere */...{DM_DEV_CREATE_CMD, IOCTL_FLAGS_NO_PARAMS, dev_create},...{DM_TABLE_LOAD_CMD, 0, table_load},...};if (unlikely(cmd >= ARRAY_SIZE(_ioctls)))return NULL;*ioctl_flags = _ioctls[cmd].flags;return _ioctls[cmd].fn;}
veritysetup 使能 dm-verity 的核心,是通过 ioctl 发送这两个命令: DM_DEV_CREATE_CMD 和 DM_TABLE_LOAD_CMD 。 DM_DEV_CREATE_CMD 就是创建 mapped-device ,我们把重点放在 DM_TABLE_LOAD_CMD 上。
DM_TABLE_LOAD_CMD
我们顺着 DM_TABLE_LOAD_CMD 命令对应的函数 table_load 往下看:
static int table_load(struct dm_ioctl *param, size_t param_size){...md = find_device(param);...r = dm_table_create(&t, get_mode(param), param->target_count, md);...r = populate_table(t, param, param_size);...}
函数的实现比较长,我们只关注上面这三行:首先根据参数,找到对应的 md 设备,然后创建 dm_table 并将其与 md 设备关联,然后将参数继续传递给 populate_table 函数进行处理。
static int populate_table(struct dm_table *table,struct dm_ioctl *param, size_t param_size){...for (i = 0; i < param->target_count; i++) {r = next_target(spec, next, end, &spec, &target_params);...r = dm_table_add_target(table, spec->target_type,(sector_t) spec->sector_start,(sector_t) spec->length,target_params);...next = spec->next;}...}
该函数根据 param 参数中的 target_count ,通过 dm_table_add_target 函数向 dm_table 添加 target ;此处我们要添加的 target 就是 dm-verity-target 。
继续看 dm_table_add_target 的实现:
int dm_table_add_target(struct dm_table *t, const char *type,sector_t start, sector_t len, char *params){...tgt->type = dm_get_target_type(type);...r = tgt->type->ctr(tgt, argc, argv);...}
dm_get_target_type 通过 type 来找到对应的 target_type 结构体,此处的 type 实际上就是 target_type.name ,我们要找到 verity_target ,所以此处传入的 type 为 "verity"。
找到 target_type 后,调用对应的 ctr 函数;对应到 dm-verity 中就是函数 verity_ctr
int verity_ctr(struct dm_target *ti, unsigned argc, char **argv){...r = dm_get_device(ti, argv[1], FMODE_READ, &v->data_dev);...r = dm_get_device(ti, argv[2], FMODE_READ, &v->hash_dev);...}
verity_ctr 才是真正的对 dm-verity 功能进行初始化,包括设置 data device , hash device 等信息,创建 dm_verity 结构体实例;这些操作完成之后,针对前面创建的 md 设备的 dm-verity 功能已经使能,之后对其进行的读写操作,会调用到 verity_target 的 map 函数—— verity_map ,该函数负责处理IO之前的映射关系,设置 bio_end_io 函数指针,即 block io 的完成方法:
int verity_map(struct dm_target *ti, struct bio *bio){...bio->bi_bdev = v->data_dev->bdev;bio->bi_iter.bi_sector = verity_map_sector(v, bio->bi_iter.bi_sector);...bio->bi_end_io = verity_end_io;...
verity_end_io 则是将校验的过程加入到 work_queue 中,这样每次对块设备的访问,都会触发 dm-verity 校验机制。
