grub 启动项
常规 grub 做三件事:加载 vmlinuz、加载 initrd、传 cmdline。来看一条 grub 配置,这是在一台实例 MockInstance 上跑的若干启动项中新增的一条,写入了启动 verity 与 verity 校验的几个值:
# /etc/grub.d/40_custom
menuentry 'MockInstance verity' {
search --no-floppy --fs-uuid --set=root d4ed479e-...
linux /boot/vmlinuz-6.8.0-117-generic \
systemd.verity=1 \
roothash=e4cc62a3...0c82 \
systemd.verity_root_data=/dev/disk/by-id/nvme-...4e \
systemd.verity_root_hash=/dev/disk/by-id/nvme-...4f \
systemd.verity_root_options=panic-on-corruption \
console=tty0 console=ttyS0,115200n8 ...
initrd /boot/MockInstance.initrd
}
cmdline 里那三个参数:roothash、verity_root_data、verity_root_hash。一个哈希值,两块盘。最后系统跑起来时,根文件系统是一个只读的、每读一个 block 都会被内核校验哈希的设备。从"grub 传了一行字符串"到"内核在逐块校验 rootfs",中间发生的事全在 initrd 里。
这几个文件首先是 mkosi 构建时的产物。在下面的脚本中被塞入(修改)了上面的启动项:
# ~/MockInstance-mkosi/deploy.sh
RH=$(cat MockInstance.roothash)
sed -i "s/roothash=[a-f0-9]*/roothash=$RH/" /etc/grub.d/40_custom
update-grub
initrd
initrd 本身是一个 cpio 归档,mkosi 用 zstd 压缩,所以是 initrd.cpio.zst。解压出来是一棵完整目录树,有 /usr/bin、/usr/lib、systemd 的各种单元,是一套能跑起来的精简根系统。
为什么需要这么个临时根?因为真正的 rootfs 此刻还挂不上。它预备在一个 dm-verity 设备上,而这个设备得有步骤去建,建它需要用户态工具和 systemd 单元,需要有个文件系统装着才能跑。initrd 就是这个完全在内存里、不依赖任何磁盘的根,专门用来把磁盘上的 rootfs 准备好。
cmdline 怎么传到 initrd 里?
systemd 在 initrd 里当 PID 1,所以问题可以转换为:cmdline 怎么传到 initrd 阶段的 systemd 里?
grub 把那行 cmdline 交给内核,内核启动后原样放进 /proc/cmdline:
$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.8.0-117-generic systemd.verity=1 \
roothash=e4cc62a3...0c82 \
systemd.verity_root_data=/dev/disk/by-id/nvme-...4e \
systemd.verity_root_hash=/dev/disk/by-id/nvme-...4f \
systemd.verity_root_options=panic-on-corruption ...
console=、iommu=pt 属于内核关注的内容。systemd.verity* 放进 /proc/cmdline,真正读它的是 initrd 里的 systemd,从前缀可看出来。
链路:grub → kernel → /proc/cmdline → initrd 的 systemd。
initrd 里的几步
initrd 的最终目标是把 /sysroot 准备好。围绕 verity,有两个 generator 和一个 helper 在协作。
第一步:veritysetup-generator 读 cmdline,写出一个 service
systemd 启动早期某一阶段跑所有 generator。
systemd-veritysetup-generator 读 /proc/cmdline,获取到 systemd.verity=1 和那三个参数,直接生成一个完整的 service 文件,落在 /run 下:
$ find /run/systemd/generator* -name '*verity*'
/run/systemd/generator/systemd-veritysetup@root.service
/run/systemd/generator/veritysetup.target.requires/systemd-veritysetup@root.service
这个 service 没有预先写好放在镜像里,而是由 generator 在启动时生成。文件头部自己标了出处:
$ head -3 /run/systemd/generator/systemd-veritysetup@root.service
# /run/systemd/generator/systemd-veritysetup@root.service
# Automatically generated by systemd-veritysetup-generator
generator 还在 veritysetup.target.requires/ 下建了个软链接,把这个 service 挂进 veritysetup.target 的依赖里,这样它会在启动时被拉起来。systemd 体系里很多服务也是根据 cmdline 实时生成出来的。
第二步:service 调 systemd-veritysetup attach,建出 /dev/mapper/root
看这个生成出来的 service 的 ExecStart:
$ systemctl cat systemd-veritysetup@root.service
...
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-veritysetup attach 'root' \
'/dev/disk/by-id/nvme-...4e' \
'/dev/disk/by-id/nvme-...4f' \
'e4cc62a3...0c82' \
'panic-on-corruption'
ExecStop=/usr/lib/systemd/systemd-veritysetup detach 'root'
调的是 /usr/lib/systemd/systemd-veritysetup attach,五个参数依次是:映射名 root、data 盘、hash 盘、roothash、options。这对应上构建机上 ~/MockInstance-mkosi/deploy.sh 的 veritysetup open 等 verity 干的事(都是建立 verity 映射)。注意构建机那里只是做安全校验,且实际到了 initrd 阶段时,veritysetup 不来自于安装的 cryptsetup 套件,而是 systemd 自带的 helper 工具。
另外,Type=oneshot 加 RemainAfterExit=yes,虽然 attach 磁盘跑完就退出,但 systemd 把它记成"仍然 active",因为它建立的设备一直在。看状态:
$ systemctl status systemd-veritysetup@root.service
● systemd-veritysetup@root.service - Integrity Protection Setup for root
Loaded: loaded (/proc/cmdline; generated)
Active: active (exited) since ... ; ...
Main PID: 204 (code=exited, status=0/SUCCESS)
Type=oneshot 自不必说。默认情况下 oneshot 命令一退出,service 就掉成 inactive。RemainAfterExit=yes 对于其他 unit 之间能则会改变依赖关系:B 要在 A 之后跑(After=A),或者 B 需要 A(Requires=A)。active (exited) 的 active 实际上是成功结束(有个 pending 状态是 activating),而 Requires 会认为 inactive 是不满足条件的。所以 RemainAfterExit=yes 要加这个。
第三步:内核 dm-verity 接管,逐块校验
systemd-veritysetup attach 执行后,内核里多出一个 /dev/mapper/root 设备,它背后挂的是内核的 dm-verity target——可以从两处确认:/sys/block/dm-0/dm/uuid 是 CRYPT-VERITY-...-root(device-mapper 给 verity 设备的固定前缀),dmesg 里有 device-mapper: verity: sha256 using implementation "sha256-ni"(内核 verity target 已初始化,用 sha256 硬件指令做校验)。从这以后,读这个设备的任何一个 block,内核都会顺着 hash 盘的 Merkle tree 算哈希、一路验到 roothash,对不上就触发 cmdline 里 panic-on-corruption 指定的行为(直接 panic)。用户态的 attach 只负责把设备建起来、建完就退,运行期的逐块校验全在内核 dm-verity 里做。
第四步:fstab-generator 生成的挂载单元把设备挂成 /sysroot
systemd-fstab-generator 跟 veritysetup-generator 是同一批 generator,都在第一步那个早期阶段跑完。它也读 cmdline,生成了一个把 /dev/mapper/root 挂到 /sysroot 的 mount 单元(dmesg 里 systemd-fstab-generator[105]: Using verity root device /dev/mapper/root. 是它在那个阶段配的)。两个 generator 都在早期跑,分工是:veritysetup-generator 生成"建设备"的 service,fstab-generator 生成"挂设备"的 mount 单元。有先后的是这两个单元的执行:先 attach 建出 /dev/mapper/root,再把它挂上去。这个设备上是只读的 erofs,挂到 initrd 里那个空的 /sysroot,findmnt / 看到的 /dev/mapper/root erofs ro 就跟构建时推进去的 zeroseal.erofs 对上了。
switch_root
/sysroot 挂好,initrd 的活干完了。
systemd 执行 switch_root,把根从 initrd 的内存文件系统切到 /sysroot,然后 exec /sysroot 里 rootfs 自带的 systemd。新 systemd 接管 PID 1,老的被替换,initrd 占的内存被释放。
从这一刻起,系统跑在那个只读、逐块校验的 rootfs 上。