概要
kcovは、カーネルのソースコードのカバレッジを測定するツールである。
本記事では、kcovに関係する次の二つの内容が含まれている。
はじめに
ソフトウェアのコードカバレッジを計測することは、プログラムの品質を確認するための要素の一つになる。 Linuxカーネルのような規模が大きいプログラムとなると、簡単に定量的な結果を残せるコードカバレッジは有用であると考えられる。
kcovは、Linux v4.6から導入されたファジングのカーネルコードカバレッジ機能である。 同様のツールとしてgcovもあるが、カーネルのソースコードをカバレッジを計測する場合には「コードブロックが膨大である」「他プロセス(スレッド)が常に動作している」という点で、kcovが優位性がある。
そこで、本記事ではkcovのドキュメントに沿ってソースコードのカバレッジを計測してみる。 また、内部でkcovを利用するファジングツールsyzkallerの実行結果も確認してみる。
実験環境
本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。
環境 | 概要 |
---|---|
CPU | AMD Ryzen 3 3300X |
RAM | DDR4-2666 16GB ×2 |
OS | Ubuntu Desktop 20.04.04 |
kernel | 5.13.0-40-generic |
QEMU | 4.2.1 (Debian 1:4.2-3ubuntu6.21) |
使用方法
kcovのインターフェースがdebugfsを経由したIOCTLによるものとなっている。
次のカーネルコンフィグにより、debugfs直下にkcov
がインターフェースとして生成される。
CONFIG_KCOV=y
kcovは、次のIOCTLの操作を提供している。
API名 | group | value | len | 概要 |
---|---|---|---|---|
KCOV_INIT_TRACE | 'c' | 1 | unsigned long |
kcovの初期セットアップ |
KCOV_ENABLE | 'c' | 100 | 現在のプロセスから発行されたsyscallに対するカバレッジの取得を開始する | |
KCOV_DISABLE | 'c' | 101 | カバレッジの所得を終了する | |
KCOV_REMOTE_ENABLE | 'c' | 102 | カーネルコードの任意の部分に対するカバレッジを取得を開始する |
kcovによるコードカバレッジの確認
Host OS(x86_64)上に、QEMUによるGuest OS(armhf)にkcovを実施する環境を構築する。
このとき、Guest OSは次のコマンドにより生成した。
qemu-system-arm
-M vexpress-a9 \
-smp 1 \
-m 1024 \
-kernel ${KIMAGE_DIR}/zImage \
-dtb ${KIMAGE_DIR}/dts/vexpress-v2p-ca9.dtb \
-drive file=${SD_IMAGE},if=sd,format=raw \
-append "rootwait root=/dev/nfs console=ttyAMA0 ip=on rw" \
-net nic,model=lan9118 -net user \
-nographic
また、NFSroot先は Ubuntu Base 20.04.4 LTS (armhf) を基に、Host PC上に格納してあり、binutilsパッケージを追加している。
測定プログラムは公式ドキュメントにあるものをそのまま利用する。
測定プログラムcoverage.cはread(-1, NULL, 0);
(46行目)を実行したときのカバレッジを測定する。
// 1: #include <stdio.h> #include <stddef.h> #include <stdint.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> #include <fcntl.h> #include <linux/types.h> #define KCOV_INIT_TRACE _IOR('c', 1, unsigned long) #define KCOV_ENABLE _IO('c', 100) #define KCOV_DISABLE _IO('c', 101) #define COVER_SIZE (64<<10) #define KCOV_TRACE_PC 0 #define KCOV_TRACE_CMP 1 int main(int argc, char **argv) { int fd; unsigned long *cover, n, i; /* A single fd descriptor allows coverage collection on a single * thread. */ fd = open("/sys/kernel/debug/kcov", O_RDWR); if (fd == -1) perror("open"), exit(1); /* Setup trace mode and trace size. */ if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE)) perror("ioctl"), exit(1); /* Mmap buffer shared between kernel- and user-space. */ cover = (unsigned long*)mmap(NULL, COVER_SIZE * sizeof(unsigned long), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if ((void*)cover == MAP_FAILED) perror("mmap"), exit(1); /* Enable coverage collection on the current thread. */ if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC)) perror("ioctl"), exit(1); /* Reset coverage from the tail of the ioctl() call. */ __atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED); /* That's the target syscal call. */ read(-1, NULL, 0); /* Read number of PCs collected. */ n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED); for (i = 0; i < n; i++) printf("0x%lx\n", cover[i + 1]); /* Disable coverage collection for the current thread. After this call * coverage can be enabled for a different thread. */ if (ioctl(fd, KCOV_DISABLE, 0)) perror("ioctl"), exit(1); /* Free resources. */ if (munmap(cover, COVER_SIZE * sizeof(unsigned long))) perror("munmap"), exit(1); if (close(fd)) perror("close"), exit(1); return 0; }
その後、測定プログラムをクロスコンパイルしたバイナリcoverage
と、Guest OSのカーネルバイナリvmlinux
をGuest OSのrootfsにコピーする。
測定プログラムcoverageではアドレスで出力されるため、addr2lineにパイプすることで解読できるような形で出力させる。
# ./coverage | addr2line -e vmlinux
/home/leava/linux/fs/read_write.c:644
/home/leava/linux/./include/linux/file.h:75
/home/leava/linux/fs/file.c:915
/home/leava/linux/fs/file.c:901
/home/leava/linux/./include/linux/fdtable.h:85
/home/leava/linux/fs/file.c:912
/home/leava/linux/fs/file.c:936
/home/leava/linux/fs/read_write.c:640
出力結果のファイル名と行数は、測定対象のシステムコールを実行したときに実行された行番号となる。
ただし、新しめのLInuxカーネルでは、デフォルトでGCCの最適化オプション(-O2
)が指定されているため、コードカバレッジの状態と実際のソースコードが異なることがある。
syzkallerによるコードカバレッジの確認
kcovはあくまでカーネルの一機能であり、ユーザが使うツールとしては利便性の面ではあまり優れていない。 そのため、kcov単体で使うのではなく、kcovを内部で利用しているツールを使うことが好ましい。
syzkallerはGoogleが開発したカーネルのファジングツールの一つとなっている。
syzkaller自体はGo言語で記述されたプログラムである。 内部でkcovでなどカーネルの機能を利用しているため、ファジングした結果のコードカバレッジも取得することができる。
Host OS(x86_64)上で、QEMUによるGuest OS(arm64)にksyzkallerを実施する。
公式ドキュメントにあるSetup: Linux host, QEMU vm, arm64 kernelに沿って実施する。
項目 | 概要 | 補足 |
---|---|---|
rootfs | buildroot 2022.02.1 | qemu_aarch64_virt_defconfig をベースとする |
kernel | Linux v5.15 | defconfigをベースとする |
toolchain | AArch64 FNU/Linux 2021.07 cross compiler | |
syzkaller | dc9e52595336dbe32f9a20f5da9f09cb8172cd21 |
Go実行環境のインストール
公式サイトの手順通りにHost OSにGo実行環境をインストールする。
leava@ubuntu:/work/$ wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz
leava@ubuntu:/work/$ tar -C /usr/local -xzf go1.18.1.linux-amd64.tar.gz
Goの実行に必要なディレクトリにPATHを通す
leava@ubuntu:/work/$ export PATH=$PATH:/usr/local/go/bin
Goの実行環境が正しくインストールされているか確認する
leava@ubuntu:/work/$ go version
leava@ubuntu:/work/$ go version go1.18.1 linux/amd64
buildrootによるrootfsの生成
buildrootのソースコードを取得する
leava@ubuntu:/work/$ wget https://buildroot.uclibc.org/downloads/buildroot-2022.02.1.tar.gz
leava@ubuntu:/work/$ tar xf buildroot-2022.02.1.tar.gz
leava@ubuntu:/work/$ cd buildroot-2022.02.1
デフォルトのコンフィグを生成する
leava@ubuntu:/work/buildroot-2022.02.1/$ make qemu_aarch64_virt_defconfig
syzkallerに必要なコンフィグを修正する
Target options
Target Architecture - Aarch64 (little endian)
Toolchain
Toolchain type (External toolchain)
System Configuration
[*] Enable root login with password
(password) Root password
[*] Run a getty (login prompt) after boot
(ttyAMA0) TTY port
Target packages
[*] Show packages that are also provided by busybox
Networking applications
[*] dhcpcd
[*] iproute2
[*] openssh
Filesystem images
[*] ext2/3/4 root filesystem
ext2/3/4 variant (ext3)
(60M) exact size
[*] tar the root filesystem
buildrootでビルドする
leava@ubuntu:/work/buildroot-2022.02.1/$ make
成果物を確認する
leava@ubuntu:/work/buildroot-2022.02.1/$ ls -l output/images
total 50668
-rw-r--r-- 1 blue root 10883584 May 3 23:41 Image
-rw-r--r-- 1 blue root 62914560 May 4 09:12 rootfs.ext2
lrwxrwxrwx 1 blue root 11 May 3 23:41 rootfs.ext3 -> rootfs.ext2
-rw-r--r-- 1 blue root 16640000 May 3 23:41 rootfs.tar
-rwxr-xr-x 1 blue root 486 May 4 09:12 start-qemu.sh
Linux Kernelのビルド
leava@ubuntu:/work/$ wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.gz
leava@ubuntu:/work/$ tar xf linux-5.15.tar.gz
leava@ubuntu:/work/$ cd linux-5.15
デフォルトのコンフィグを生成する
leava@ubuntu:/work/linux-5.15/$ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig
syzkallerに必要なコンフィグを修正する
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_REDUCED=n
CONFIG_KCOV=y
CONFIG_KCOV_INSTRUMENT_ALL=y
CONFIG_KASAN=y
CONFIG_CMDLINE="console=ttyAMA0"
CONFIG_FAULT_INJECTION=y
Linuxをビルドする
leava@ubuntu:/work/linux-5.15/$ ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-make -j$(nproc)
成果物を確認する
leava@ubuntu:/work/linux-5.15/$ ls -la arch/arm64/boot/Image
-rw-r--r-- 1 blue root 82354688 May 4 15:38 arch/arm64/boot/Image
rootfsのカスタマイズ
QEMUで作成したカーネルを起動させる (root:password
でログインが可能)
leava@ubuntu:/work/$ qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
-nographic -smp 1 \
-hda /path/to/rootfs.ext3 \
-kernel /path/to/arch/arm64/boot/Image \
-append "console=ttyAMA0 root=/dev/vda oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ" \
-m 2048 \
-net user,hostfwd=tcp::10023-:22 -net nic
initスクリプトに次の処理を追加する (/etc/init.d/S50sshd
)
ifconfig eth0 up
dhcpcd
mount -t debugfs none /sys/kernel/debug
chmod 777 /sys/kernel/debug/kcov
sshdに次の設定を追加する (/etc/ssh/sshd_config
)
PermitRootLogin yes
PubkeyAuthentication yes
PasswordAuthentication yes
sshdの設定更新のために、Guest OSを再起動する。
Guest OSのために、Host OS上で公開鍵ペアを作成し、Guest OSに送る。
leava@ubuntu:/work/$ ssh-keygen
leava@ubuntu:/work/$ ssh-copy-id -i id_rsa.pub root@localhost -p 10023
Host OSからGuest OSに対して、sshできることを確認できたらGuest OSの電源を落とす。
syzkallerのビルド
ツールチェインの取得
leava@ubuntu:/work/$ wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz
leava@ubuntu:/work/$ tar xf gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu.tar.xz
syzkallerのソースコードを取得する
leava@ubuntu:/work/$ git clone https://github.com/google/syzkaller.git
leava@ubuntu:/work/syzkaller/$ git clone https://github.com/google/syzkaller.git
syzkallerをビルドする
leava@ubuntu:/work/syzkaller/$ CC=/work/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-gnu-g++
leava@ubuntu:/work/syzkaller/$ make TARGETARCH=arm64
syzkallerを実行する
syzkallerの設定ファイル (my.cfg) を用意する。
// 1: { "name": "QEMU-aarch64", "target": "linux/arm64", "http": ":56700", "workdir": "/work/syzkaller/workdir", "kernel_obj": "/work/linux-5.15", "image": "/work/buildroot-2022.02.1/output/images/rootfs.ext3", "sshkey": "~/.ssh/id_rsa", "syzkaller": "/work/syzkaller", "procs": 1, "type": "qemu", "disable_syscalls": ["keyctl", "add_key", "request_key"], "suppressions": ["some known bug"], "vm": { "count": 1, "qemu": "qemu-system-aarch64", "cmdline": "console=ttyAMA0 root=/dev/vda", "kernel": "/work/linux-5.15/arch/arm64/boot/Image", "cpu": 2, "mem": 2048 } }
用意したコンフィグファイルを入力として、syz-managerを実行することで、ファジングテストが開始する。
leava@ubuntu:/work/syzkaller/$ sudo bin/syz-manager -config=my.cfg
テストが開始してからしばらく待った後に、ブラウザから http://127.0.0.1:56700/
にアクセスすると、次のようなWebページが表示される。
coverageにある数字をクリックすると次のようなWebページが表示される。
おわりに
本記事では、kcovによるソースコードカバレッジの取得方法について確認した。
また、ファジングツールの一つsyzkallerで、ファジングテストの結果におけるカバレッジの取得方法についても確認した。
変更履歴
- 2022/5/5: 記事公開
参考文献
- syzkallerを実際に使っている記事
- syzkallerを調査している記事