结合 BusyBox 与完整发行版的内核验证环境

本站之前的两篇文章:

  • 《基于 BusyBox 快速制作内核验证环境》
  • 《基于完整发行版搭建内核验证环境》

分别总结了基于 QEMU 搭建内核验证环境的两种思路。经过这几年下来在日常工作中的实践和体会,总结下两者各自的优劣:

  • 前者构建成本低,操作简单;用户态极简,启动时省去了大量用户态(如 systemd)的初始化工作,启动速度快。
  • 后者有完整的 C 语言运行时环境,可执行动态链接的用户态程序;有完整的包管理,软件资源丰富,可拓展性强。

我个人的偏好是,优先考虑使用前者,毕竟简单高效,有利于反复试错和快速迭代;如果验证场景对 C 语言运行时或者其他用户态基础设施有依赖,再考虑使用后者。

那么,有没有一种方法,能够将两者的优点结合起来,既能极速启动,又可享有完整的用户态基础设施呢?我们考虑这样的一种方案:

  1. 首先,我们参照前者的做法,采用基于 busybox 的极简 initrd 作为 rootfs 完成内核的启动。
  2. 然后,我们可将载有完整发行版的 rootfs 镜像作为一个硬盘接入到虚拟机中。这里涉及到内核对相关块设备的支持,因此要在 initrd 中带有与内核匹配的设备驱动,并在合适的时机完成内核模块的插入。
  3. 最后,我们完成硬盘的挂载,并 chroot 到挂载点上,进入一个以硬盘为根节点的目录空间。后续就可以以平常的操作方式来使用发行版了。

一言以蔽之:在系统启动过程中绕开绝大部分的用户态启动,尽快获得 shell/足够支撑内核验证的环境。从理论上讲,上述方案是完全可行的。

前期准备工作

本文基于 openEuler 的 6.6 内核举例说明。首先,在 openeuler_defconfig 的基础上,设置以下为 =n,减少冗余提升编译速度:

1CONFIG_DRM
2CONFIG_VGA_SWITCHEROO
3CONFIG_HYDCU_FIXUP_HEADER
4CONFIG_SOUND
5CONFIG_USB_SUPPORT
6CONFIG_WLAN
7CONFIG_WAN
8CONFIG_PPP
9CONFIG_CAN_DEV

其次,initrd 作为内核启动阶段的 rootfs,应承载驱动供内核加载:

1make -j$(nproc) # should build 'bzImage' when solely updating kernel
2make -j$(nproc) INSTALL_MOD_PATH=/path/to/busybox/_install INSTALL_MOD_STRIP=1 modules_install
3
4cd /path/to/busybox/_install
5find . | cpio -H newc -o --quiet | gzip -c --quiet > ../../initrd_xxx.cpio.gz # xxx: x86/arm64

关于虚拟机的配置与启动,不同体系结构下有不同的参数和脚本。本文着重讨论 x86 和 arm64 两种。

x86

 1sudo qemu-system-x86_64 \
 2        -m 2G \
 3        -smp 2 \
 4        -enable-kvm \
 5        -kernel ../OLK-6.6/arch/x86/boot/bzImage \
 6        -initrd ./initrd_x86.cpio.gz \
 7        -hda ./root.img \
 8        -append "console=ttyS0 earlyprintk" \
 9        -nic user,model=e1000 \
10        -nographic

通过 -hda 配置的硬盘,x86 虚拟机中识别为 /dev/sda,依赖 ata_piix.kosd_mod.ko 方可识别。initrd 中的 etc/init.d/rcS 如下:

 1mount -t proc none /proc
 2mount -t sysfs none /sys
 3# /sbin/mdev -s
 4mount -t devtmpfs none /dev
 5mount -t devpts devpts /dev/pts
 6
 7# mount -t selinuxfs selinuxfs /sys/fs/selinux/
 8# mount -t debugfs debugfs /sys/kernel/debug/
 9
10modprobe e1000
11ip addr add 10.0.2.3/24 dev eth0
12ip link set eth0 up
13ip route add default via 10.0.2.2 dev eth0
14
15# If some devices can't be probed, try the kernel
16# with defconfig and deduce needed ko from kmsg.
17# For /dev/sd*
18modprobe ata_piix
19modprobe sd_mod
20# For mounting ext4 fs
21modprobe ext4
22
23###################################################
24# For bpf test case
25###################################################
26mkdir -p /mnt
27mount /dev/sda /mnt
28mount -t proc none /mnt/proc
29mount -t sysfs none /mnt/sys
30mount -t bpf none /mnt/sys/fs/bpf/
31chroot /mnt /bin/bash

ARM64

ARM64 上 QEMU 也是可以开 KVM 的:首先查看宿主机的 dmesg,确认 KVM 初始化成功;其次虚拟机应配置 -cpu host

 1# `-cpu` should be `host` to make `-enable-kvm` work
 2qemu-system-aarch64 \
 3        -M virt \
 4        -cpu host \
 5        -m 2G \
 6        -smp 2 \
 7        -enable-kvm \
 8        -kernel ../OLK-6.6/arch/arm64/boot/Image \
 9        -initrd ./initrd_arm64.cpio.gz \
10        -hda ./root.img \
11        -append "console=ttyAMA0 earlyprintk" \
12        -nic user,model=e1000 \
13        -nographic

而在 arm64 虚拟机中,-hda 被识别为 /dev/vda,依赖 virtio_pci.kovirtio_blk.ko 方可识别。initrd 中的 etc/init.d/rcS 如下:

 1mount -t proc none /proc
 2mount -t sysfs none /sys
 3# /sbin/mdev -s
 4mount -t devtmpfs none /dev
 5mkdir -p /dev/pts
 6mount -t devpts devpts /dev/pts
 7
 8# mount -t selinuxfs selinuxfs /sys/fs/selinux/
 9# mount -t debugfs debugfs /sys/kernel/debug/
10
11modprobe e1000
12ip addr add 10.0.2.3/24 dev eth0
13ip link set eth0 up
14ip route add default via 10.0.2.2 dev eth0
15
16# If some devices can't be probed, try the kernel
17# with defconfig and deduce needed ko from kmsg.
18# For /dev/vd*
19modprobe virtio_pci
20modprobe virtio_blk
21# For mounting ext4 fs
22modprobe ext4
23
24###################################################
25# For bpf test case
26###################################################
27mkdir -p /mnt
28mount /dev/vda /mnt
29mount -t proc none /mnt/proc
30mount -t sysfs none /mnt/sys
31mount -t bpf none /mnt/sys/fs/bpf/
32chroot /mnt /bin/bash

附录

1、如何识别缺失的驱动

关于某种设备,假如在 QEMU 里加了而内核没有识别(i.e. 相应的设备节点没有出现),大概率是因为驱动缺失(i.e. 依赖的的内核模块未插入)。若是不知道其具体依赖什么 ko,最简单的方法就是构建一个尽可能多 CONFIG_xxx=y 的内核,确认设备可识别后,通过内核日志定位到相关的内核代码,从而找到具体的模块。

2、完整发行版 rootfs 镜像来源

  • Ubuntu Base (doc , releases ),直接就是文件/目录结构的压缩包,而非磁盘镜像。适合自行制作镜像后直接解压填入内容
  • Fedora Server ,选择 QCow2 或 Raw 格式的磁盘镜像,并确保内核支持其中的文件系统格式
    • QCow2:通过 qemu-nbd --connect=/dev/nbdX 将镜像连接到 NBD 设备,通过 lsblk -f 等方式查看文件系统类型
    • Raw:通过 fdisk -l 获得分区起始偏移量(sector_size * start),通过 mount -o offset=xxx ... 挂载(参考链接 )后查看文件系统类型
  • Ubuntu Cloud Images ,选择 QCow2 格式的镜像,并确保内核支持其中的文件系统格式
  • 内核源码 tools/testing/selftests/bpf/vmtest.sh 中的 INDEX_URL 记录了 eBPF 的 CI 环境所用的 rootfs,有 x86/arm64 的,可下载直接使用
  • Alpine Linux ,基于 musl 和 busybox,不推荐

3、Busybox 环境下配置 DNS

DNS in busybox: (link )

1~ # cat /etc/resolv.conf
2nameserver 192.168.13.5
3
4~ # nslookup google.com

内核符号重定位:从 CVE-2024-26816 说开去