LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

kcovによるカーネルのソースコードカバレッジの分析

概要

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を実施する環境を構築する。

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パッケージを追加している。

測定プログラムは公式ドキュメントにあるものをそのまま利用する。

www.kernel.org

測定プログラム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が開発したカーネルのファジングツールの一つとなっている。

github.com

syzkaller自体はGo言語で記述されたプログラムである。 内部でkcovでなどカーネルの機能を利用しているため、ファジングした結果のコードカバレッジも取得することができる。

Host OS(x86_64)上で、QEMUによるGuest OS(arm64)にksyzkallerを実施する。

syzkaller実行環境

公式ドキュメントにある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実行環境をインストールする。

go.dev

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のビルド

Linuxソースコードを取得する

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ページが表示される。

syzkallerのトップ画面

coverageにある数字をクリックすると次のようなWebページが表示される。

syzkallerによるcoverageの確認

github.com

おわりに

本記事では、kcovによるソースコードカバレッジの取得方法について確認した。
また、ファジングツールの一つsyzkallerで、ファジングテストの結果におけるカバレッジの取得方法についても確認した。

変更履歴

  • 2022/5/5: 記事公開

参考文献