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

device mapper驱动结构

image.jpeg
上图展示了 device mapper 的结构,即一个 mapped device 拥有一个 mapping table ,这个表负责维护 mapped devicetarget device 之间的映射关系,这些 target 既可以是物理设备,也可以是另一个 mapped device
内核使用下图中的几个结构体来描述这种映射关系:
image.jpeg
mapped_device 描述设备, dm_table 记录 md 设备下面有多少个 targetdm_targettarget_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 结构体注册到链表上:

  1. /*
  2. * file: drivers/md/dm-verity-target.c
  3. */
  4. static struct target_type verity_target = {
  5. .name = "verity",
  6. .version = {1, 4, 0},
  7. .module = THIS_MODULE,
  8. ...
  9. };
  10. static int __init dm_verity_init(void)
  11. {
  12. ...
  13. r = dm_register_target(&verity_target);
  14. ...
  15. }
  1. /*
  2. * file: drivers/md/dm-target.c
  3. */
  4. static LIST_HEAD(_targets);
  5. int dm_register_target(struct target_type *tt)
  6. {
  7. ...
  8. if (__find_target_type(tt->name))
  9. rv = -EEXIST;
  10. else
  11. list_add(&tt->list, &_targets);
  12. ...
  13. }

veritysetup的参数

veritysetup 激活 dm-verity 功能需要提供:

  • mapped device 设备名
  • 数据来源设备节点
  • 哈希树设备节点
  • 根哈希
    1. veritysetup create <device name> <data device> <hashtree device> <root hash>
    这条命令其实包括了两个过程,创建 mapped device 设备和处理 mapped devicedata device hashtree 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 };

  1. 通过 `ioctl` 对其进行访问,内核中的函数调用路径为: `dm_ctl_ioctl -> ctl_ioctl -> lookup_ioctl` `lookup_ioctl` 再根据用户发送的命令,来返回不同的函数指针:
  2. ```c
  3. static ioctl_fn lookup_ioctl(unsigned int cmd, int *ioctl_flags)
  4. {
  5. static struct {
  6. int cmd;
  7. int flags;
  8. ioctl_fn fn;
  9. } _ioctls[] = {
  10. {DM_VERSION_CMD, 0, NULL}, /* version is dealt with elsewhere */
  11. ...
  12. {DM_DEV_CREATE_CMD, IOCTL_FLAGS_NO_PARAMS, dev_create},
  13. ...
  14. {DM_TABLE_LOAD_CMD, 0, table_load},
  15. ...
  16. };
  17. if (unlikely(cmd >= ARRAY_SIZE(_ioctls)))
  18. return NULL;
  19. *ioctl_flags = _ioctls[cmd].flags;
  20. return _ioctls[cmd].fn;
  21. }

veritysetup 使能 dm-verity 的核心,是通过 ioctl 发送这两个命令: DM_DEV_CREATE_CMDDM_TABLE_LOAD_CMDDM_DEV_CREATE_CMD 就是创建 mapped-device ,我们把重点放在 DM_TABLE_LOAD_CMD 上。

DM_TABLE_LOAD_CMD

我们顺着 DM_TABLE_LOAD_CMD 命令对应的函数 table_load 往下看:

  1. static int table_load(struct dm_ioctl *param, size_t param_size)
  2. {
  3. ...
  4. md = find_device(param);
  5. ...
  6. r = dm_table_create(&t, get_mode(param), param->target_count, md);
  7. ...
  8. r = populate_table(t, param, param_size);
  9. ...
  10. }

函数的实现比较长,我们只关注上面这三行:首先根据参数,找到对应的 md 设备,然后创建 dm_table 并将其与 md 设备关联,然后将参数继续传递给 populate_table 函数进行处理。

  1. static int populate_table(struct dm_table *table,
  2. struct dm_ioctl *param, size_t param_size)
  3. {
  4. ...
  5. for (i = 0; i < param->target_count; i++) {
  6. r = next_target(spec, next, end, &spec, &target_params);
  7. ...
  8. r = dm_table_add_target(table, spec->target_type,
  9. (sector_t) spec->sector_start,
  10. (sector_t) spec->length,
  11. target_params);
  12. ...
  13. next = spec->next;
  14. }
  15. ...
  16. }

该函数根据 param 参数中的 target_count ,通过 dm_table_add_target 函数向 dm_table 添加 target ;此处我们要添加的 target 就是 dm-verity-target
继续看 dm_table_add_target 的实现:

  1. int dm_table_add_target(struct dm_table *t, const char *type,
  2. sector_t start, sector_t len, char *params)
  3. {
  4. ...
  5. tgt->type = dm_get_target_type(type);
  6. ...
  7. r = tgt->type->ctr(tgt, argc, argv);
  8. ...
  9. }

dm_get_target_type 通过 type 来找到对应的 target_type 结构体,此处的 type 实际上就是 target_type.name ,我们要找到 verity_target ,所以此处传入的 type"verity"
找到 target_type 后,调用对应的 ctr 函数;对应到 dm-verity 中就是函数 verity_ctr

  1. int verity_ctr(struct dm_target *ti, unsigned argc, char **argv)
  2. {
  3. ...
  4. r = dm_get_device(ti, argv[1], FMODE_READ, &v->data_dev);
  5. ...
  6. r = dm_get_device(ti, argv[2], FMODE_READ, &v->hash_dev);
  7. ...
  8. }

verity_ctr 才是真正的对 dm-verity 功能进行初始化,包括设置 data devicehash device 等信息,创建 dm_verity 结构体实例;这些操作完成之后,针对前面创建的 md 设备的 dm-verity 功能已经使能,之后对其进行的读写操作,会调用到 verity_targetmap 函数—— verity_map ,该函数负责处理IO之前的映射关系,设置 bio_end_io 函数指针,即 block io 的完成方法:

  1. int verity_map(struct dm_target *ti, struct bio *bio)
  2. {
  3. ...
  4. bio->bi_bdev = v->data_dev->bdev;
  5. bio->bi_iter.bi_sector = verity_map_sector(v, bio->bi_iter.bi_sector);
  6. ...
  7. bio->bi_end_io = verity_end_io;
  8. ...

verity_end_io 则是将校验的过程加入到 work_queue 中,这样每次对块设备的访问,都会触发 dm-verity 校验机制。