FirstStageMount
本文面向已经读过 FirstStageInit,理解 mount(2)、uevent、device-mapper 和文件系统挂载基本概念的读者。上一篇只说明 FirstStageMain() 何时调用 DoCreateDevices() 与 DoFirstStageMount();本文把这两个调用交给的对象展开,回答一个更窄也更实用的问题:Android 17 如何把 fstab 中的 first_stage_mount 条目变成可挂载路径,并在挂载 /system 后完成根目录切换。
本文不把 AVB、dm-verity、动态分区描述成三个互相独立的“安全功能”。在真实调用链中,fstab 标志决定要等哪些块设备,逻辑分区决定 blk_device 是否改写成 /dev/block/dm-*,AVB hashtree 又可能再次改写它,最后 fs_mgr_do_mount_one() 才消费这个结果。本文不展开 vbmeta 签名格式、liblp 元数据布局或 snapshot COW 算法;这些是后续专题的边界。
读完后,你应能定位 FirstStageMount::Create()、解释 InitDevices() 为什么先找 super/vbmeta、判断某个 mount 失败是可忽略还是 fatal,并用源码测试和设备日志验证“设备没出现”“映射没创建”“校验没通过”“文件系统没挂上”这四种不同故障。
1. 对象边界
FirstStageMount 不是一个只做 mount() 的工具类。它同时持有 fstab、super 分区路径、设备初始化器、AVB key 缓存和 vbmeta 分区列表:
相关源码:
system/core/init/first_stage_mount_android.cppsystem/core/init/first_stage_mount.cppsystem/core/init/block_dev_initializer.cpp
class FirstStageMount {
protected:
using Fstab = android::fs_mgr::Fstab;
using FstabEntry = android::fs_mgr::FstabEntry;
Fstab fstab_;
std::string super_path_;
std::string super_partition_name_;
BlockDevInitializer block_dev_init_;
std::map<std::string, std::vector<std::string>> preload_avb_key_blobs_;
std::vector<std::string> vbmeta_partitions_;
AvbUniquePtr avb_handle_;
};| 状态 | 所有者 | 消费者 | 生效时机 |
|---|---|---|---|
fstab_ | FirstStageMount::Create | 挂载循环、AVB | 对象创建后 |
super_path_ | InitDevices | liblp、snapshot | super symlink 可解析后 |
preload_avb_key_blobs_ | PreloadAvbKeys | 独立 vbmeta 校验 | 切到 /system 前 |
avb_handle_ | InitAvbHandle | hashtree 设置 | 首个 AVB 条目挂载时 |
/dev/block/dm-* | BlockDevInitializer | fs_mgr_do_mount_one | 映射或 hashtree 创建后 |
对象的直接消费者是 FirstStageMain,更深一层的消费者是 fs_mgr、device-mapper 和文件系统驱动。理解这些 owner 边界后,看到 blk_device 被改写时不会误以为 fstab 文件本身被修改。
2. 创建对象
2.1 读取 fstab
Android 17 的 Android 实现使用 ReadDefaultFstab(),随后只保留标记为 first_stage_mount 的条目:
源码文件:system/core/init/first_stage_mount_android.cpp
Result<std::unique_ptr<FirstStageMount>> FirstStageMount::Create(
const std::string& cmdline) {
Fstab fstab;
if (!ReadDefaultFstab(&fstab)) {
return Error() << "failed to read default fstab for first stage mount";
}
bool data_on_userdata = false;
if (auto entry = GetEntryForMountPoint(&fstab, "/data"); entry) {
data_on_userdata = entry->blk_device == "/data/block/by-name/userdata";
}
std::erase_if(fstab, [](auto& entry) {
return !entry.fs_mgr_flags.first_stage_mount;
});
return std::make_unique<FirstStageMountAndroid>(std::move(fstab),
data_on_userdata);
}这里的 fstab 不是“所有分区清单”。/data 会先被观察一次,因为 mount_before_data() 可能要求 apexd 提前初始化 userdata;真正交给 FirstStageMountAndroid 的列表已经过滤过。一个条目没有 first_stage_mount,并不表示它永远不挂载,只表示它不在这个阶段消费。
2.2 构造元数据
基类构造函数读取 super 分区名和设备树中的 vbmeta 列表,并合并 fstab 条目声明的 vbmeta_partition:
源码文件:system/core/init/first_stage_mount.cpp
FirstStageMount::FirstStageMount(Fstab fstab)
: fstab_(std::move(fstab)), avb_handle_(nullptr) {
super_partition_name_ = fs_mgr_get_super_partition_name();
std::string device_tree_vbmeta_parts;
read_android_dt_file("vbmeta/parts", &device_tree_vbmeta_parts);
for (auto&& partition : Split(device_tree_vbmeta_parts, ",")) {
if (!partition.empty()) {
vbmeta_partitions_.emplace_back(std::move(partition));
}
}
for (const auto& entry : fstab_) {
if (!entry.vbmeta_partition.empty()) {
vbmeta_partitions_.emplace_back(entry.vbmeta_partition);
}
}
}vbmeta_partitions_ 的作用在 GetDmVerityDevices() 才显现:如果任何 first-stage 条目要求 AVB,初始化器必须提前找到验证链上的 vbmeta 分区。不能只等待 system/vendor 自身,因为顶层 vbmeta 可能位于独立分区。
3. 设备就绪
3.1 目标集合
基类 DoCreateDevices() 调用 InitDevices();真正被 FirstStageMain() 调用的是 Android 子类 override,它在基类设备初始化之后处理 metadata、动态分区和 snapshot:
相关源码:
system/core/init/first_stage_mount.cppFirstStageMount::DoCreateDevicesFirstStageMount::InitDevices
bool FirstStageMount::DoCreateDevices() {
return InitDevices();
}
bool FirstStageMount::InitDevices() {
if (!block_dev_init_.InitBootDevicesFromPartUuid()) {
return false;
}
std::set<std::string> devices;
GetSuperDeviceName(&devices);
GetExtraBlockDevices(&devices);
if (!GetDmVerityDevices(&devices)) {
return false;
}
if (!InitRequiredDevices(std::move(devices))) {
return false;
}
return true;
}Android override 的顺序如下。/metadata 必须早于 logical partition 创建,因为 snapshot manager 需要先判断 merge 状态:
源码文件:system/core/init/first_stage_mount_android.cpp
bool FirstStageMountAndroid::DoCreateDevices() {
if (!FirstStageMount::DoCreateDevices()) {
return false;
}
auto metadata_partition = std::find_if(
fstab_.begin(), fstab_.end(), [](const auto& entry) {
return entry.mount_point == "/metadata";
});
if (metadata_partition != fstab_.end()) {
if (MountPartition(metadata_partition, true /* erase_same_mounts */)) {
CopyDsuAvbKeys();
}
}
if (!CreateLogicalPartitions()) return false;
return true;
}目标集合中的字符串是分区名,不一定是最终 /dev 路径。BlockDevInitializer 通过 uevent 的 partition_name、设备路径末段或设备名反推它们,再让 DeviceHandler 创建 /dev/block 节点和符号链接。
3.2 Uevent等待
初始化器先重放已经存在于 sysfs 的 uevent,再等待最多 10 秒的新事件:
源码文件:system/core/init/block_dev_initializer.cpp
bool BlockDevInitializer::InitDevices(std::set<std::string> devices) {
auto uevent_callback = [&, this](const Uevent& uevent) -> ListenerAction {
return HandleUevent(uevent, &devices);
};
uevent_listener_.RegenerateUevents(uevent_callback);
if (!devices.empty()) {
LOG(INFO) << __PRETTY_FUNCTION__
<< ": partition(s) not found in /sys, waiting for their uevent(s): "
<< android::base::Join(devices, ", ");
Timer t;
uevent_listener_.Poll(uevent_callback, 10s);
LOG(INFO) << "Wait for partitions returned after " << t;
}
if (!devices.empty()) {
LOG(ERROR) << __PRETTY_FUNCTION__
<< ": partition(s) not found after polling timeout: "
<< android::base::Join(devices, ", ");
return false;
}
return true;
}重放和轮询的组合解释了“设备已经出现但首阶段仍能找到它”的行为。反过来,超时集合非空才是这个函数的失败断言;单条 uevent 被忽略可能只是名称不匹配,并不立即失败。
3.3 设备映射
动态分区和 AVB 需要 device-mapper 设备节点,而 ueventd 还未启动,因此 InitRequiredDevices() 先手工初始化 device-mapper:
源码文件:system/core/init/first_stage_mount.cpp
bool FirstStageMount::InitRequiredDevices(std::set<std::string> devices) {
if (!block_dev_init_.InitDeviceMapper()) {
return false;
}
if (devices.empty()) {
return true;
}
return block_dev_init_.InitDevices(std::move(devices));
}InitDeviceMapper() 通过 /sys/devices/virtual/misc/device-mapper 重放或轮询事件;InitDmDevice() 则对 /sys/block/dm-N 做同样的事情。它们不是创建映射,只是让已经由 libdm 创建的映射获得 /dev 节点。
4. 逻辑分区
4.1 Super路径
当 fstab 中存在 logical 标志时,GetSuperDeviceName() 把 super 分区加入目标集合。设备就绪后,代码解析 /dev/block/by-name/<super>:
源码文件:system/core/init/first_stage_mount.cpp
if (IsDmLinearEnabled()) {
auto super_symlink = "/dev/block/by-name/"s + super_partition_name_;
if (!android::base::Realpath(super_symlink, &super_path_)) {
PLOG(ERROR) << "realpath failed: " << super_symlink;
return false;
}
}因此 super_path_ 在 InitDevices() 结束前无效。任何在此之前读取逻辑分区 metadata 的调用都属于顺序错误。
4.2 Metadata映射
Android 子类的 CreateLogicalPartitions() 先读取 liblp metadata,再初始化 metadata 中列出的 backing partition,最后创建 dm-linear 映射:
源码文件:system/core/init/first_stage_mount_android.cpp
bool FirstStageMountAndroid::CreateLogicalPartitions() {
if (!IsDmLinearEnabled()) return true;
if (super_path_.empty()) {
LOG(ERROR) << "Could not locate logical partition tables in partition "
<< super_partition_name_;
return false;
}
auto metadata = android::fs_mgr::ReadCurrentMetadata(super_path_);
if (!metadata) {
LOG(ERROR) << "Could not read logical partition metadata from "
<< super_path_;
return false;
}
if (!InitDmLinearBackingDevices(*metadata.get())) {
return false;
}
return android::fs_mgr::CreateLogicalPartitions(*metadata.get(), super_path_);
}这里的 CreateLogicalPartitions() 名称容易误导:它不负责挂载文件系统,而是把 super 元数据中的 logical partition 映射成可打开的 block device。真正的挂载仍在后面的 MountPartition()。
4.3 Snapshot分支
如果 SnapshotManager::IsSnapshotManagerNeeded() 返回 true,Android 17 会在读取普通 metadata 前进入 snapshot 分支。该分支可能先初始化 userdata,启动 first-stage snapuserd,再通过 callback 为 dm、dm-user 或 ublk 设备重放 uevent:
源码文件:system/core/init/first_stage_mount_android.cpp
if (!IsMicrodroid() && SnapshotManager::IsSnapshotManagerNeeded()) {
auto init_devices = [this](const std::string& device) -> bool {
if (android::base::StartsWith(device, "/dev/block/dm-")) {
return block_dev_init_.InitDmDevice(device);
}
return block_dev_init_.InitDevices({device});
};
SnapshotManager::MapTempOtaMetadataPartitionIfNeeded(init_devices);
auto sm = SnapshotManager::NewForFirstStageMount();
if (!sm) return false;
if (sm->NeedSnapshotsInFirstStageMount()) {
return CreateSnapshotPartitions(sm.get());
}
}snapshot 不是 logical partition 的必经步骤;它由 OTA 状态决定。use_snapuserd_ 后续会影响 SaveRamdiskPathToSnapuserd() 和旧 ramdisk 清理,说明这条分支的状态会跨越 FirstStageMount 甚至跨越 root switch。
5. AVB链
5.1 设备清单
GetDmVerityDevices() 从 fstab 构造 AVB 所需设备集合。logical 条目不会通过普通 uevent 查找其最终 dm 名称;非 logical 的 /dev/... 条目则加入路径末段。若任一条目设置 avb,还必须有 vbmeta 分区:
源码文件:system/core/init/first_stage_mount.cpp
bool FirstStageMount::GetDmVerityDevices(std::set<std::string>* devices) {
bool need_dm_verity = false;
std::set<std::string> logical_partitions;
for (const auto& fstab_entry : fstab_) {
if (fstab_entry.fs_mgr_flags.avb) {
need_dm_verity = true;
}
if (fstab_entry.fs_type == "overlay") continue;
if (fstab_entry.fs_mgr_flags.logical) {
logical_partitions.emplace(basename(fstab_entry.blk_device.c_str()));
continue;
}
if (!fstab_entry.blk_device.starts_with("/dev/")) continue;
devices->emplace(basename(fstab_entry.blk_device.c_str()));
}
if (need_dm_verity) {
if (vbmeta_partitions_.empty()) {
LOG(ERROR) << "Missing vbmeta partitions";
return false;
}
std::string ab_suffix = fs_mgr_get_slot_suffix();
for (const auto& partition : vbmeta_partitions_) {
std::string partition_name = partition + ab_suffix;
if (logical_partitions.count(partition_name)) continue;
devices->emplace(partition_name);
}
}
return true;
}这里的 avb 标志和 avb_keys、avb_hashtree_digest 不是同一层概念。前者要求内置 AVB 链;后两者可能触发独立 vbmeta 读取,但代码会在真正设置 hashtree 时分支处理。
5.2 Handle缓存
第一次需要 AVB 时,InitAvbHandle() 打开顶层 handle,并把版本放入环境变量:
源码文件:system/core/init/first_stage_mount.cpp
bool FirstStageMount::InitAvbHandle() {
if (avb_handle_) return true;
avb_handle_ = AvbHandle::Open();
if (!avb_handle_) {
PLOG(ERROR) << "Failed to open AvbHandle";
return false;
}
setenv("INIT_AVB_VERSION", avb_handle_->avb_version().c_str(), 1);
return true;
}handle 是对象级缓存,不是每个分区重新打开。SetupSelinux() 之后 SecondStageMain() 会消费 INIT_AVB_VERSION,设置 ro.boot.avb_version 后清除环境变量;这也是首阶段不能提前 unset 的原因。
5.3 Hashtree分支
SetUpDmVerity() 按 fstab 字段选择验证路径:优先 avb_keys,其次 avb,最后是 avb_hashtree_digest;都没有时直接返回 true:
源码文件:system/core/init/first_stage_mount.cpp
if (!fstab_entry->avb_keys.empty()) {
if (!InitAvbHandle()) return false;
auto avb_standalone_handle = AvbHandle::LoadAndVerifyVbmeta(
*fstab_entry, preload_avb_key_blobs_[fstab_entry->avb_keys]);
if (!avb_standalone_handle) {
if (!fstab_entry->fs_mgr_flags.avb) return false;
hashtree_result = avb_handle_->SetUpAvbHashtree(
fstab_entry, false /* wait_for_verity_dev */);
} else {
hashtree_result = avb_standalone_handle->SetUpAvbHashtree(
fstab_entry, false /* wait_for_verity_dev */);
}
} else if (fstab_entry->fs_mgr_flags.avb) {
if (!InitAvbHandle()) return false;
hashtree_result = avb_handle_->SetUpAvbHashtree(
fstab_entry, false /* wait_for_verity_dev */);
} else if (!fstab_entry->avb_hashtree_digest.empty()) {
if (!InitAvbHandle()) return false;
auto avb_standalone_handle = AvbHandle::LoadAndVerifyVbmeta(*fstab_entry);
if (!avb_standalone_handle) return false;
hashtree_result = avb_standalone_handle->SetUpAvbHashtree(
fstab_entry, false /* wait_for_verity_dev */);
} else {
return true;
}成功建立 hashtree 后,AVB handle 会把 fstab_entry->blk_device 改成 /dev/block/dm-*,首阶段再调用 InitDmDevice() 创建节点:
源码文件:system/core/init/first_stage_mount.cpp
switch (hashtree_result) {
case AvbHashtreeResult::kDisabled:
return true;
case AvbHashtreeResult::kSuccess:
return block_dev_init_.InitDmDevice(fstab_entry->blk_device);
default:
return false;
}kDisabled 的含义是顶层 vbmeta 禁用了 hashtree,允许直接挂载;它不是“校验成功”。文章和日志都必须区分 disabled、success 和 failure。
6. 挂载顺序
6.1 System优先
MountPartitions() 首先调用 TrySwitchSystemAsRoot()。只要 fstab 有 /system,就先预加载 AVB key、挂载 /system,然后 SwitchRoot("/system"):
源码文件:system/core/init/first_stage_mount.cpp
bool FirstStageMount::TrySwitchSystemAsRoot() {
UseDsuIfPresent();
PreloadAvbKeys();
auto system_partition = std::find_if(
fstab_.begin(), fstab_.end(), [](const auto& entry) {
return entry.mount_point == "/system";
});
if (system_partition == fstab_.end()) return true;
SaveRamdiskPathToSnapuserd();
if (!MountPartition(system_partition, false /* erase_same_mounts */)) {
PLOG(ERROR) << "Failed to mount /system";
return false;
}
SwitchRoot("/system");
return true;
}PreloadAvbKeys() 必须在切根前执行,因为 key 文件可能只存在于 ramdisk。切根后的路径以 /system 为新根,后续 /vendor、/product 挂载就与 system-as-root 设备使用同一坐标系。
6.2 单条挂载
每个条目的真实顺序是:创建 canonical mount point、更新 logical partition、创建 dm 节点、设置 hashtree、调用 fs_mgr_do_mount_one():
源码文件:system/core/init/first_stage_mount.cpp
bool FirstStageMount::MountPartition(const Fstab::iterator& begin,
bool erase_same_mounts,
Fstab::iterator* end) {
if (end) *end = begin + 1;
if (!fs_mgr_create_canonical_mount_point(begin->mount_point)) return false;
if (begin->fs_mgr_flags.logical) {
if (!fs_mgr_update_logical_partition(&(*begin))) return false;
if (!block_dev_init_.InitDmDevice(begin->blk_device)) return false;
}
if (!SetUpDmVerity(&(*begin))) return false;
bool mounted = (fs_mgr_do_mount_one(*begin) == 0);
Fstab::iterator current = begin + 1;
for (; current != fstab_.end() &&
current->mount_point == begin->mount_point; current++) {
if (!mounted) {
current->blk_device = begin->blk_device;
mounted = (fs_mgr_do_mount_one(*current) == 0);
}
}
if (erase_same_mounts) current = fstab_.erase(begin, current);
if (end) *end = current;
return mounted;
}同一 mount point 的多条 fstab entry 是 fallback 关系:第一条失败才尝试下一条,并复制已经更新过的 blk_device。因此不能把 fstab 行数直接当成挂载次数,也不能把第一条失败日志直接当成最终失败。
6.3 例外条目
主循环跳过三类条目:已在 system 阶段处理的 /system、后处理的 overlayfs、只用于探测 AVB 链的 emmc 原始分区:
源码文件:system/core/init/first_stage_mount.cpp
if (current->mount_point == "/system") {
++current;
continue;
}
if (current->fs_type == "overlay") {
++current;
continue;
}
if (current->fs_type == "emmc") {
++current;
continue;
}普通失败按 no_fail、formattable 和必需分区三类处理:前两类记录并继续,最后一类返回 false。formattable 在这里不是“现在格式化”,而是允许该挂载失败而不阻止首阶段继续。
7. 根切换
SwitchRoot() 的调用者是 TrySwitchSystemAsRoot(),而不是每个分区挂载。system 分区挂上后,旧 ramdisk 成为待释放的旧设备;后续 vendor、product、system_ext 在新根坐标中继续挂载。对于没有 /system 条目的 system-as-root 设备,TrySwitchSystemAsRoot() 直接返回,根已经由内核或早期镜像提供。
Android 子类还会在所有普通条目完成后调用 MountOverlays():
源码文件:system/core/init/first_stage_mount.cpp
bool FirstStageMount::MountPartitions() {
if (!TrySwitchSystemAsRoot()) return false;
if (!SkipMountingPartitions(&fstab_, true /* verbose */)) return false;
for (auto current = fstab_.begin(); current != fstab_.end();) {
// system、overlay、emmc 条目在循环中分别处理或跳过。
// 其余条目调用 MountPartition。
++current;
}
MountOverlays();
return true;
}overlayfs 的设备准备、scratch 映射和挂载由 FirstStageMountAndroid::MountOverlays() 负责,不应在普通 MountPartition() 中提前执行,否则会破坏 /system 根切换后的路径假设。
8. 失败路径
8.1 设备超时
InitDevices() 失败时,先判断目标集合:super、vbmeta、普通分区和额外 userdata 都可能缺失。BlockDevInitializer 的 10 秒超时只证明 uevent 重放和轮询后仍有目标未出现,不证明 fstab 路径写错。排查顺序应是:日志中的剩余分区名、/sys 是否存在对应目录、boot device 严格匹配是否过滤了事件。
8.2 映射失败
super_path_ 为空、liblp metadata 无法读取、backing device 未出现或 snapshot 创建失败,都会在挂载前返回 false。此时不应搜索文件系统 mount 错误,因为 fs_mgr_do_mount_one() 尚未被调用。
8.3 校验失败
AvbHandle::Open() 失败、独立 vbmeta 加载失败且没有 avb fallback、rollback 检测失败或 hashtree 返回未知错误,都会阻止当前分区挂载。只有 kDisabled 明确允许直接挂载;设备 unlocked 只影响特定 standalone image rollback 判断,不等价于关闭全部 AVB。
8.4 挂载失败
一个 mount point 有多条 entry 时,失败可能触发 fallback;没有 fallback 时再按 no_fail、formattable 或必需分区决定是否继续。最终所有必需条目都成功后才进入 overlay 和交接。恢复路径是替代 fstab entry 或修复设备状态,不是让 SwitchRoot() 在未挂载 /system 时强行执行。
9. 测试验证
platform/system/core 在这个基线上没有直接把 FirstStageMount 整条路径包进 host 单元测试。完整测试依赖真实设备、sysfs、device-mapper、AVB metadata 和挂载命名空间,因此不能用一个 parser 或 liblp 单测代替启动证明。本文采用源码分支核对加设备实验,并明确每个实验的断言边界:
| 实验输入 | 核心断言 | 能证明 | 不能证明 |
|---|---|---|---|
| 正常 boot 的 fstab 与 super | 目标 dm 节点存在且分区已挂载 | 设备、映射、挂载链完成 | hashtree 每次 I/O 都正确 |
| 缺失一个必需块设备 | 日志保留超时后的分区名 | 设备就绪阶段失败 | fstab 一定写错 |
| 同 mount point 多条 entry | 第一条失败后第二条成功 | fallback 消费顺序 | 所有设备都应配置 fallback |
no_fail 条目失败 | 后续必需分区仍继续 | 错误被显式降级 | 失败分区功能可用 |
| AVB disabled 启动 | 直接挂载且版本可读取 | 顶层策略允许跳过 hashtree | 校验成功 |
源码入口核对命令如下:
rg -n "FirstStageMount::Create|InitDevices|MountPartition|SetUpDmVerity|TrySwitchSystemAsRoot" \
system/core/init/first_stage_mount.cpp \
system/core/init/first_stage_mount_android.cpp
rg -n "InitDevices\(|InitDmDevice|RegenerateUevents|Poll\(" \
system/core/init/block_dev_initializer.cpp
rg -n "ReadCurrentMetadata|CreateLogicalPartitions|CreateSnapshotPartitions" \
system/core/init/first_stage_mount_android.cpp有设备或 Cuttlefish 环境时,验证输入、owner 和结果:
adb shell 'cat /proc/mounts | grep -E " /system | /vendor | /product "'
adb shell 'ls -l /dev/block/by-name/super /dev/block/dm-* 2>/dev/null'
adb shell 'dmesg | grep -E "partition|dm-|verity|Failed to mount|SwitchRoot"'
adb shell 'getprop ro.boot.avb_version'断言要区分四层结果:目标分区是否出现、dm 节点是否出现、hashtree 是否被设置、文件系统是否真正挂载。ro.boot.avb_version 只能证明 AVB handle 版本被二阶段消费,不能证明每个 fstab 条目都使用了 AVB。
10. 设备边界
本文基于 Android 17 源码证明了 FirstStageMount 的对象状态、fstab 过滤、uevent 设备等待、super 与逻辑分区创建、snapshot 分支、AVB hashtree 设置、/system 优先挂载、fallback 条件和 overlay 后处理。
本文没有证明具体设备的 fstab 来源文件内容、liblp 元数据布局、AVB 密钥链的密码学实现、dm-verity 内核 I/O 行为、snapshot COW 算法或厂商 overlayfs 配置。下一篇应从 FirstStageMountAndroid::CreateLogicalPartitions() 或 SetUpDmVerity() 选择一个单独展开,否则会把多个 owner 的证明边界重新混在一起。
