英語

KCOV: 程式碼覆蓋率用於模糊測試

KCOV 收集並公開核心程式碼覆蓋率資訊,其形式適合用於覆蓋引導的模糊測試。執行核心的覆蓋率資料透過 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 支援從以下上下文中收集遠端覆蓋率

  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_struct 中的 kcov_handle 欄位中,並且需要透過自定義核心程式碼修改傳遞給新生成的本地任務。這些任務應該反過來在其 kcov_remote_startkcov_remote_stop 註釋中使用傳遞的控制代碼。

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);

    n = __atomic_load_n(&cover[0], __ATOMIC_RELAXED);
    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;
}