根据内核文档,用户空间可以通过 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 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);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 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;
else
list_add(&tt->list, &_targets);
...
}

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 在内核中的描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* 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_ioctllookup_ioctl 再根据用户发送的命令,来返回不同的函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static 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_CMDDM_TABLE_LOAD_CMDDM_DEV_CREATE_CMD 就是创建 mapped-device ,我们把重点放在 DM_TABLE_LOAD_CMD 上。

DM_TABLE_LOAD_CMD

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

1
2
3
4
5
6
7
8
9
10
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 函数进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 的实现:

1
2
3
4
5
6
7
8
9
int dm_table_add_target(struct dm_table *t, const char *type,
sector_t start, sector_t len, char *params)
{
...
->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

1
2
3
4
5
6
7
8
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 devicehash device 等信息,创建 dm_verity 结构体实例;这些操作完成之后,针对前面创建的 md 设备的 dm-verity 功能已经使能,之后对其进行的读写操作,会调用到 verity_targetmap 函数—— verity_map ,该函数负责处理IO之前的映射关系,设置 bio_end_io 函数指针,即 block io 的完成方法:

1
2
3
4
5
6
7
8
9
10
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 校验机制。