KCOV:用于模糊测试的代码覆盖率

KCOV 以适合覆盖率引导的模糊测试(coverage-guided fuzzing)的形式收集并公开内核代码覆盖率信息。运行中内核的覆盖率数据通过 kcov debugfs 文件导出。覆盖率收集是以任务为单位启用的,因此 KCOV 可以捕获单个系统调用的精确覆盖率。

请注意, KCOV 的目的并不是收集尽可能多的覆盖率。它的目标是收集相对稳定的、作为系统调用输入函数的覆盖率。为了实现这一目标, 它不会在软/硬中断中(除非启用了远程覆盖率收集,见下文)以及内核某些本质上不确定的部分(例如调度器、锁机制)收集覆盖率。

除了收集代码覆盖率,KCOV 还可以收集比较操作数。详见“比较操作数收集”一节。

除了从系统调用处理程序收集覆盖率数据外,KCOV 还可以为在后台内核任务或软中断中执行的带注释的内核部分收集覆盖率。详见“远程覆盖率收集”一节。

前提条件

KCOV 依赖于编译器插桩,需要 GCC 6.1.0 或更高版本,或者内核支持的任意 Clang 版本。

收集比较操作数需要 GCC 8+ 或 Clang。

要启用 KCOV,请使用以下配置来配置内核

CONFIG_KCOV=y

要启用比较操作数收集,请设置

CONFIG_KCOV_ENABLE_COMPARISONS=y

只有在挂载 debugfs 后,覆盖率数据才可访问

mount -t debugfs none /sys/kernel/debug

覆盖率收集

以下程序演示了如何在测试程序内部使用 KCOV 来收集单个系统调用的覆盖率

#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);
    /* Call the target syscall 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;
}

通过管道传给 addr2line 后,程序的输出如下所示

SyS_read
fs/read_write.c:562
__fdget_pos
fs/file.c:774
__fget_light
fs/file.c:746
__fget_light
fs/file.c:750
__fget_light
fs/file.c:760
__fdget_pos
fs/file.c:784
SyS_read
fs/read_write.c:562

如果程序需要从多个线程(独立地)收集覆盖率,它需要在每个线程中分别打开 /sys/kernel/debug/kcov

该接口设计得很精细,以允许高效地 fork 测试进程。也就是说,父进程打开 /sys/kernel/debug/kcov,启用跟踪模式,对覆盖率缓冲区进行内存映射(mmap),然后在一个循环中 fork 子进程。子进程只需要启用覆盖率即可(当线程退出时,覆盖率会自动禁用)。

比较操作数收集

比较操作数收集与覆盖率收集类似

/* Same includes and defines as above. */

/* Number of 64-bit words per record. */
#define KCOV_WORDS_PER_CMP 4

/*
 * The format for the types of collected comparisons.
 *
 * Bit 0 shows whether one of the arguments is a compile-time constant.
 * Bits 1 & 2 contain log2 of the argument size, up to 8 bytes.
 */

#define KCOV_CMP_CONST          (1 << 0)
#define KCOV_CMP_SIZE(n)        ((n) << 1)
#define KCOV_CMP_MASK           KCOV_CMP_SIZE(3)

int main(int argc, char **argv)
{
    int fd;
    uint64_t *cover, type, arg1, arg2, is_const, size;
    unsigned long n, i;

    fd = open("/sys/kernel/debug/kcov", O_RDWR);
    if (fd == -1)
            perror("open"), exit(1);
    if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
            perror("ioctl"), exit(1);
    /*
    * Note that the buffer pointer is of type uint64_t*, because all
    * the comparison operands are promoted to uint64_t.
    */
    cover = (uint64_t *)mmap(NULL, COVER_SIZE * sizeof(unsigned long),
                                 PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if ((void*)cover == MAP_FAILED)
            perror("mmap"), exit(1);
    /* Note KCOV_TRACE_CMP instead of KCOV_TRACE_PC. */
    if (ioctl(fd, KCOV_ENABLE, KCOV_TRACE_CMP))
            perror("ioctl"), exit(1);
    __atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
    read(-1, NULL, 0);
    /* Read number of comparisons collected. */
    n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
    for (i = 0; i < n; i++) {
            uint64_t ip;

            type = cover[i * KCOV_WORDS_PER_CMP + 1];
            /* arg1 and arg2 - operands of the comparison. */
            arg1 = cover[i * KCOV_WORDS_PER_CMP + 2];
            arg2 = cover[i * KCOV_WORDS_PER_CMP + 3];
            /* ip - caller address. */
            ip = cover[i * KCOV_WORDS_PER_CMP + 4];
            /* size of the operands. */
            size = 1 << ((type & KCOV_CMP_MASK) >> 1);
            /* is_const - true if either operand is a compile-time constant.*/
            is_const = type & KCOV_CMP_CONST;
            printf("ip: 0x%lx type: 0x%lx, arg1: 0x%lx, arg2: 0x%lx, "
                    "size: %lu, %s\n",
                    ip, type, arg1, arg2, size,
            is_const ? "const" : "non-const");
    }
    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;
}

请注意,KCOV 模式(收集代码覆盖率或比较操作数)是互斥的。

远程覆盖率收集

除了从用户空间进程发起的系统调用处理程序收集覆盖率数据外,KCOV 还可以为在其他上下文中执行的内核部分收集覆盖率——即所谓的“远程”覆盖率。

使用 KCOV 收集远程覆盖率需要

  1. 修改内核代码,使用 kcov_remote_startkcov_remote_stop 对需要收集覆盖率的代码段进行注释。

  2. 在收集覆盖率的用户空间进程中使用 KCOV_REMOTE_ENABLE 而不是 KCOV_ENABLE

kcov_remote_startkcov_remote_stop 注释以及 KCOV_REMOTE_ENABLE ioctl 都接受用于标识特定覆盖率收集段的句柄。句柄的使用方式取决于匹配代码段执行的上下文。

一个线程可以同时使用两个独立的 KCOV 实例来收集远程覆盖率和普通覆盖率。

KCOV 支持从以下上下文中收集远程覆盖率

  1. 全局内核后台任务。这些任务在内核启动期间以有限的实例数被派生(例如,每个 USB HCD 派生一个 USB hub_event 工作线程)。

  2. 局部内核后台任务。这些任务是在用户空间进程与某些内核接口交互时派生的,通常在进程退出时被杀死(例如 vhost 工作线程)。

  3. 软中断。

对于 #1 和 #3,必须选择一个唯一的全局句柄并将其传递给相应的 kcov_remote_start 调用。然后,用户空间进程必须在 kcov_remote_arg 结构的 handles 数组字段中将此句柄传递给 KCOV_REMOTE_ENABLE。这将把所使用的 KCOV 设备附加到由此句柄引用的代码段。可以一次传递多个标识不同代码段的全局句柄。

对于 #2,用户空间进程必须通过 kcov_remote_arg 结构的 common_handle 字段传递一个非零句柄。这个公共句柄会被保存到当前 task_structkcov_handle 字段中,并且需要通过自定义的内核代码修改传递给新派生的局部任务。这些任务反过来应该在其 kcov_remote_startkcov_remote_stop 注释中使用传递的句柄。在内核中,公共句柄被包装在 kcov_common_handle_id 中,在没有 CONFIG_KCOV 的构建中它不占用任何空间;集成此机制的子系统不需要使用任何 #ifdef CONFIG_KCOV 或类似的宏。

对于全局句柄和公共句柄,KCOV 都遵循预定义的格式。每个句柄都是一个 u64 整数。目前,仅使用最高字节和低 4 字节。第 4-7 字节保留且必须为零。

对于全局句柄,句柄的最高字节表示此句柄所属子系统的 ID。例如,KCOV 使用 1 作为 USB 子系统 ID。全局句柄的低 4 字节表示该子系统中任务实例的 ID。例如,每个 hub_event 工作线程都使用 USB 总线编号作为任务实例 ID。

对于公共句柄,由于此类句柄不属于特定子系统,因此使用保留值 0 作为子系统 ID。公共句柄的低 4 字节标识由向 KCOV_REMOTE_ENABLE 传递公共句柄的用户空间进程所派生的所有局部任务的集体实例。

在实践中,如果系统上仅从单个用户空间进程收集覆盖率,则公共句柄实例 ID 可以使用任何值。但是,如果多个进程使用公共句柄,则必须为每个进程使用唯一的实例 ID。一种选择是将进程 ID 用作公共句柄实例 ID。

以下程序演示了如何使用 KCOV 从进程派生的局部任务以及处理 USB 总线 #1 的全局任务中收集覆盖率

/* Same includes and defines as above. */

struct kcov_remote_arg {
    __u32           trace_mode;
    __u32           area_size;
    __u32           num_handles;
    __aligned_u64   common_handle;
    __aligned_u64   handles[0];
};

#define KCOV_INIT_TRACE                     _IOR('c', 1, unsigned long)
#define KCOV_DISABLE                        _IO('c', 101)
#define KCOV_REMOTE_ENABLE          _IOW('c', 102, struct kcov_remote_arg)

#define COVER_SIZE  (64 << 10)

#define KCOV_TRACE_PC       0

#define KCOV_SUBSYSTEM_COMMON       (0x00ull << 56)
#define KCOV_SUBSYSTEM_USB  (0x01ull << 56)

#define KCOV_SUBSYSTEM_MASK (0xffull << 56)
#define KCOV_INSTANCE_MASK  (0xffffffffull)

static inline __u64 kcov_remote_handle(__u64 subsys, __u64 inst)
{
    if (subsys & ~KCOV_SUBSYSTEM_MASK || inst & ~KCOV_INSTANCE_MASK)
            return 0;
    return subsys | inst;
}

#define KCOV_COMMON_ID      0x42
#define KCOV_USB_BUS_NUM    1

int main(int argc, char **argv)
{
    int fd;
    unsigned long *cover, n, i;
    struct kcov_remote_arg *arg;

    fd = open("/sys/kernel/debug/kcov", O_RDWR);
    if (fd == -1)
            perror("open"), exit(1);
    if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE))
            perror("ioctl"), exit(1);
    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 via common handle and from USB bus #1. */
    arg = calloc(1, sizeof(*arg) + sizeof(uint64_t));
    if (!arg)
            perror("calloc"), exit(1);
    arg->trace_mode = KCOV_TRACE_PC;
    arg->area_size = COVER_SIZE;
    arg->num_handles = 1;
    arg->common_handle = kcov_remote_handle(KCOV_SUBSYSTEM_COMMON,
                                                    KCOV_COMMON_ID);
    arg->handles[0] = kcov_remote_handle(KCOV_SUBSYSTEM_USB,
                                            KCOV_USB_BUS_NUM);
    if (ioctl(fd, KCOV_REMOTE_ENABLE, arg))
            perror("ioctl"), free(arg), exit(1);
    free(arg);

    /*
     * Here the user needs to trigger execution of a kernel code section
     * that is either annotated with the common handle, or to trigger some
     * activity on USB bus #1.
     */
    sleep(2);

    /*
     * The load to the coverage count should be an acquire to pair with
     * pair with the corresponding write memory barrier (smp_wmb()) on
     * the kernel-side in kcov_move_area().
     */
    n = __atomic_load_n(&cover[0], __ATOMIC_ACQUIRE);
    for (i = 0; i < n; i++)
            printf("0x%lx\n", cover[i + 1]);
    if (ioctl(fd, KCOV_DISABLE, 0))
            perror("ioctl"), exit(1);
    if (munmap(cover, COVER_SIZE * sizeof(unsigned long)))
            perror("munmap"), exit(1);
    if (close(fd))
            perror("close"), exit(1);
    return 0;
}