版权声明:本文为笔者本人「ashimida@」在CSDN的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/123361473更多内容可关注微信公众号
lkdtm(Linux Kernel Dump Test Module)[1] 是一个测试内核功能的内核模块, 其在用户态提供几个接口以方便用户测试系统中如RODATA,CFI等功能是否正常运行; 其在用户态同时还有一个测试框架支持,此框架可以自动从dmesg中获取测试结果(日志)并比对测试是否通过。
注: 后续内容基于 linux 5.15
一、内核部分
lkdtm的内核部分主要包含各个内核功能的测试用例以及用户态接口的定义, 内核部分的所有代码都在 ./drivers/misc/lkdtm/目录下, 可以通过编译选项 CONFIG_LKDTM 将ldktm指定为ko或builtin.
tangyuan@ubuntu:/mnt/disk0/disk0/linux_kernel/mainline/drivers/misc/lkdtm$ ls
bugs.c cfi.c core.c fortify.c heap.c lkdtm.h Makefile modules.order perms.c powerpc.c refcount.c rodata.c stackleak.c usercopy.c
1. 主体代码
core.c是 lkdtm的主体代码,其他文件中基本上都是测试用例。core.c中负责module_init,其定义了多个"crash points",如下:
static struct crashpoint crashpoints[] = {
CRASHPOINT("DIRECT", NULL),
#ifdef CONFIG_KPROBES ## CONFIG_KPROBES 可开启更多用户态接口
CRASHPOINT("INT_HARDWARE_ENTRY", "do_IRQ"),
CRASHPOINT("INT_HW_IRQ_EN", "handle_irq_event"),
CRASHPOINT("INT_TASKLET_ENTRY", "taskletls_action"),
CRASHPOINT("FS_DEVRW", "ll_rw_block"),
CRASHPOINT("MEM_SWAPOUT", "shrink_inactive_list"),
CRASHPOINT("TIMERADD", "hrtimer_start"),
CRASHPOINT("SCSI_QUEUE_RQ", "scsi_queue_rq"),
#endif
};
一个crash point实际上指的是一个用户态的交互接口,通过这些接口就可以触发内核中的lkdtm测试用例,如默认开启的DIRECT接口实际对应 /sys/kernel/debug/provoke-crash/DIRECT 文件 ,当开启CONFIG_LKDTM 时通过如下命令即可触发内核的一个crash:
echo PANIC >/sys/kernel/debug/provoke-crash/DIRECT
所有可用的测试定义在crashtypes数组中:
//./drivers/misc/lkdtm/core.c
static const struct crashtype crashtypes[] = {
CRASHTYPE(PANIC),
CRASHTYPE(BUG),
CRASHTYPE(WARNING),
CRASHTYPE(WARNING_MESSAGE),
CRASHTYPE(EXCEPTION),
CRASHTYPE(LOOP),
CRASHTYPE(EXHAUST_STACK),
CRASHTYPE(CORRUPT_STACK),
CRASHTYPE(CORRUPT_STACK_STRONG),
CRASHTYPE(REPORT_STACK),
CRASHTYPE(CORRUPT_LIST_ADD),
CRASHTYPE(CORRUPT_LIST_DEL),
CRASHTYPE(STACK_GUARD_PAGE_LEADING),
......
要执行对应的用例, 如 CRASHTYPE(PANIC), 在用户态使用如下命令即可:
echo PANIC >/sys/kernel/debug/provoke-crash/DIRECT
其最终会调用到内核lkdtm的测试函数 lkdtm_PANIC:
//./drivers/misc/ldktm/bugs.c
void lkdtm_PANIC(void)
{
panic("dumptest");
}
2. 添加测试用例
在lkdtm中添加测试用例可参考此patch[2]
## 添加CFI的测试用例
From b0eb93cfd516201ccf0e4d36e226cfe1b16cc1fe Mon Sep 17 00:00:00 2001
From: Kees Cook <keescook@chromium.org>
Date: Thu, 8 Aug 2019 11:37:45 -0700
Subject: lkdtm: Add Control Flow Integrity test
This adds a simple test for forward CFI (indirect function calls) with
function prototype granularity (as implemented by Clang's CFI).
Signed-off-by: Kees Cook <keescook@chromium.org>
---
drivers/misc/lkdtm/Makefile | 1 +
drivers/misc/lkdtm/cfi.c | 42 ++++++++++++++++++++++++++++++++++++++++++
drivers/misc/lkdtm/core.c | 1 +
drivers/misc/lkdtm/lkdtm.h | 3 +++
4 files changed, 47 insertions(+)
create mode 100644 drivers/misc/lkdtm/cfi.c
diff --git a/drivers/misc/lkdtm/Makefile b/drivers/misc/lkdtm/Makefile
index fb10eafe9bde7..c70b3822013f4 100644
--- a/drivers/misc/lkdtm/Makefile
+++ b/drivers/misc/lkdtm/Makefile
@@ -9,6 +9,7 @@ lkdtm-$(CONFIG_LKDTM) += refcount.o
lkdtm-$(CONFIG_LKDTM) += rodata_objcopy.o
lkdtm-$(CONFIG_LKDTM) += usercopy.o
lkdtm-$(CONFIG_LKDTM) += stackleak.o
+lkdtm-$(CONFIG_LKDTM) += cfi.o
KASAN_SANITIZE_stackleak.o := n
KCOV_INSTRUMENT_rodata.o := n
diff --git a/drivers/misc/lkdtm/cfi.c b/drivers/misc/lkdtm/cfi.c
new file mode 100644
index 0000000000000..e73ebdbfa8060
--- /dev/null
+++ b/drivers/misc/lkdtm/cfi.c
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * This is for all the tests relating directly to Control Flow Integrity.
+ */
+#include "lkdtm.h"
+
+static int called_count;
+
+/* Function taking one argument, without a return value. */
+static noinline void lkdtm_increment_void(int *counter)
+{
+ (*counter)++;
+}
+
+/* Function taking one argument, returning int. */
+static noinline int lkdtm_increment_int(int *counter)
+{
+ (*counter)++;
+
+ return *counter;
+}
+/*
+ * This tries to call an indirect function with a mismatched prototype.
+ */
+void lkdtm_CFI_FORWARD_PROTO(void)
+{
+ /*
+ * Matches lkdtm_increment_void()'s prototype, but not
+ * lkdtm_increment_int()'s prototype.
+ */
+ void (*func)(int *);
+
+ pr_info("Calling matched prototype ...\n");
+ func = lkdtm_increment_void;
+ func(&called_count);
+
+ pr_info("Calling mismatched prototype ...\n");
+ func = (void *)lkdtm_increment_int;
+ func(&called_count);
+
+ pr_info("Fail: survived mismatched prototype function call!\n");
+}
diff --git a/drivers/misc/lkdtm/core.c b/drivers/misc/lkdtm/core.c
index 66ae6b2a6950c..42136196681eb 100644
--- a/drivers/misc/lkdtm/core.c
+++ b/drivers/misc/lkdtm/core.c
@@ -169,6 +169,7 @@ static const struct crashtype crashtypes[] = {
CRASHTYPE(USERCOPY_KERNEL),
CRASHTYPE(USERCOPY_KERNEL_DS),
CRASHTYPE(STACKLEAK_ERASING),
+ CRASHTYPE(CFI_FORWARD_PROTO),
};
diff --git a/drivers/misc/lkdtm/lkdtm.h b/drivers/misc/lkdtm/lkdtm.h
index 6a284a87a037c..8a25afbdf9549 100644
--- a/drivers/misc/lkdtm/lkdtm.h
+++ b/drivers/misc/lkdtm/lkdtm.h
@@ -95,4 +95,7 @@ void lkdtm_USERCOPY_KERNEL_DS(void);
/* lkdtm_stackleak.c */
void lkdtm_STACKLEAK_ERASING(void);
+/* cfi.c */
+void lkdtm_CFI_FORWARD_PROTO(void);
+
#endif
--
cgit 1.2.3-1.el7
二、用户态部分
lkdtm的用户态部分主要是一个简单的测试框架, 其源码在 ./tools/testing/selftests/lkdtm/ 目录下:
tangyuan@ubuntu:/mnt/disk0/disk0/linux_kernel/mainline/tools/testing/selftests/lkdtm$ ls
config Makefile run.sh stack-entropy.sh tests.txt
这里关键的主要有两个文件: run.sh和tests.txt。
1. 测试框架编译
在源码目录通过如下命令即可编译用户态的测试框架:
make -j$(getconf _NPROCESSORS_ONLN) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -C tools/ selftests
make -j$(getconf _NPROCESSORS_ONLN) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -C tools/ selftests_install
最终编译结果默认输出到 ./tools/testing/selftests/kselftest_install/lkdtm/目录中:
tangyuan@ubuntu:/mnt/disk0/disk0/linux_kernel/mainline/tools/testing/selftests/kselftest_install/lkdtm$ ls
ACCESS_NULL.sh CORRUPT_STACK_STRONG.sh HARDLOCKUP.sh REFCOUNT_DEC_NEGATIVE.sh SLAB_FREE_PAGE.sh USERCOPY_HEAP_WHITELIST_FROM.sh
ACCESS_USERSPACE.sh DOUBLE_FAULT.sh HUNG_TASK.sh REFCOUNT_DEC_SATURATED.sh SLAB_INIT_ON_ALLOC.sh USERCOPY_HEAP_WHITELIST_TO.sh
ARRAY_BOUNDS.sh EXCEPTION.sh LOOP.sh REFCOUNT_DEC_ZERO.sh SLAB_LINEAR_OVERFLOW.sh USERCOPY_KERNEL.sh
ATOMIC_TIMING.sh EXEC_DATA.sh PANIC.sh REFCOUNT_INC_NOT_ZERO_OVERFLOW.sh SOFTLOCKUP.sh USERCOPY_STACK_BEYOND.sh
BUDDY_INIT_ON_ALLOC.sh EXEC_KMALLOC.sh PPC_SLB_MULTIHIT.sh REFCOUNT_INC_NOT_ZERO_SATURATED.sh SPINLOCKUP.sh USERCOPY_STACK_FRAME_FROM.sh
BUG.sh EXEC_NULL.sh READ_AFTER_FREE.sh REFCOUNT_INC_OVERFLOW.sh stack-entropy.sh USERCOPY_STACK_FRAME_TO.sh
CFI_BACKWARD_SHADOW.sh EXEC_RODATA.sh READ_BUDDY_AFTER_FREE.sh REFCOUNT_INC_SATURATED.sh STACK_GUARD_PAGE_LEADING.sh VMALLOC_LINEAR_OVERFLOW.sh
CFI_BACKWARD_SHADOW_WITH_NOSCS.sh EXEC_STACK.sh REFCOUNT_ADD_NOT_ZERO_OVERFLOW.sh REFCOUNT_INC_ZERO.sh STACK_GUARD_PAGE_TRAILING.sh WARNING_MESSAGE.sh
CFI_FORWARD_PROTO.sh EXEC_USERSPACE.sh REFCOUNT_ADD_NOT_ZERO_SATURATED.sh REFCOUNT_SUB_AND_TEST_NEGATIVE.sh STACKLEAK_ERASING.sh WARNING.sh
config EXEC_VMALLOC.sh REFCOUNT_ADD_OVERFLOW.sh REFCOUNT_SUB_AND_TEST_SATURATED.sh tests.txt WRITE_AFTER_FREE.sh
CORRUPT_LIST_ADD.sh EXHAUST_STACK.sh REFCOUNT_ADD_SATURATED.sh REFCOUNT_TIMING.sh UNALIGNED_LOAD_STORE_WRITE.sh WRITE_BUDDY_AFTER_FREE.sh
CORRUPT_LIST_DEL.sh FORTIFIED_OBJECT.sh REFCOUNT_ADD_ZERO.sh REPORT_STACK_CANARY.sh UNSET_SMEP.sh WRITE_KERN.sh
CORRUPT_PAC.sh FORTIFIED_STRSCPY.sh REFCOUNT_DEC_AND_TEST_NEGATIVE.sh SLAB_FREE_CROSS.sh USERCOPY_HEAP_SIZE_FROM.sh WRITE_RO_AFTER_INIT.sh
CORRUPT_STACK.sh FORTIFIED_SUBOBJECT.sh REFCOUNT_DEC_AND_TEST_SATURATED.sh SLAB_FREE_DOUBLE.sh USERCOPY_HEAP_SIZE_TO.sh WRITE_RO.sh
可以看到这里输出了一大堆.sh文件,实际上这些sh都是前面run.sh的副本,run.sh脚本中根据此脚本自身的文件名来决定具体执行哪个case,故这里每一个sh执行时都对应唯一一个test case.
2. 测试框架打包与运行
测试时则将编译出的/tools/testing/selftests/kselftest_install/lkdtm/目录打包到要测试的内核的文件系统中,系统启动后运行对应测试脚本即可,如:
## 运行单个测试用例
/kselftest_install/lkdtm # ./CFI_FORWARD_PROTO.sh
[ 57.236772] lkdtm: Performing direct entry CFI_FORWARD_PROTO
[ 57.237241] lkdtm: Calling matched prototype ...
[ 57.237509] lkdtm: Calling mismatched prototype ...
[ 57.237774] lkdtm: FAIL: survived mismatched prototype function call!
[ 57.238106] lkdtm: This is probably expected, since this kernel (5.17.0-rc5-13981-g0d1e946dc562-dirty aarch64) was built *without* CONFIG_CFI_CLANG=y
CFI_FORWARD_PROTO: missing 'call trace:': [FAIL]
## 也可以通过run_kselftest.sh运行所有测试
/kselftest_install # ./run_kselftest.sh
[ 86.285440] kselftest: Running tests in alsa
TAP version 13
1..1
# selftests: alsa: mixer-test
.......
3. 测试结果检测:
前面测试用例运行时输出的:
CFI_FORWARD_PROTO: missing 'call trace:': [FAIL]
就代表此测试用例执行失败了, 这个检测是通过 run.sh(这里的CFI_FORWARD_PROTO.sh) 配合texts.txt完成的, run.sh是运行脚本,texts.txt中则记录正常的测试结果应该是什么。
1) texts.txt
#PANIC
BUG kernel BUG at
WARNING WARNING:
WARNING_MESSAGE message trigger
EXCEPTION
#LOOP Hangs the system
#EXHAUST_STACK Corrupts memory on failure
#CORRUPT_STACK Crashes entire system on success
#CORRUPT_STACK_STRONG Crashes entire system on success
ARRAY_BOUNDS
CORRUPT_LIST_ADD list_add corruption
CORRUPT_LIST_DEL list_del corruption
STACK_GUARD_PAGE_LEADING
STACK_GUARD_PAGE_TRAILING
REPORT_STACK_CANARY repeat:2 ok: stack canaries differ
UNSET_SMEP pinned CR4 bits changed:
texts.txt以行为单位,每一个行代表一个测试用例的正常输出结果,每一行的格式为:
[#]CRASH_TYPE [repeat:N] [...]
texts.txt中的一行最多存在三个字段,最少则必须存在一个CRASH_TYPE字段,其中:
- CRASH_TYPE: 代表一个测试用例名, 如 我们执行 "echo PANIC > /sys/kernel/debug/provoke-crash/DIRECT" 时, 此时的CRASH_TYPE就是"PANIC"。 若此用例名是以"#" 开头的,则此测试用例的结果会被脚本忽略(通常是因为此测试用例执行后会导致系统crash)。
- [repeat:N]: 这个字段是可选的,如果指定此字段则运行run.sh时会反复触发此用例N次。
- [...]: 此字段是用来匹配输出结果的字符串(输出来自内核printk), 除了前面两个字段外,此行的所有其余内容都属于此字段,如:
## CORRUPT_LIST_DEL 是 用例名
## 此用例成功执行后会通过printk 输出 "list_del corruption"
CORRUPT_LIST_DEL list_del corruption
2) run.sh
run.sh主要作用是用来执行测试用例,并根据texts.txt的内容来判断测试是否成功。
- 执行测试用例
run.sh在install时会为每个测试用例复制一份,run.sh中根据运行时脚本的名字执行对应测试用例,当执行./CFI_FORWARD_PROTO.sh 时, 实际执行的用例为:
echo CFI_FORWARD_PROTO > /sys/kernel/debug/provoke-crash/DIRECT
- 测试结果的判定
在run.sh中的判定规则如下:
- 对于#开头的用例, 不检测结果, 脚本直接退出
- 对于非#开头的用例:
- 执行次数: 若指定了repeat:N, 则此用例执行N次后再统一检测结果
- 要匹配的字段:
- 若没有字段3(即匹配字符串), 则默认其会导致Oops, 默认的匹配字符串(expect)为 "Call trace", 此字段在Oops时会通过printk输出.
- 若指定了字段3, 则匹配字符串(expect)为字段3的全部内容
- 最终匹配:
- 要匹配的内容为执行用例前后dmesg的输出差异
- 若在其中匹配到字符串(expect),则代表用例执行成功,输出 "saw "$expect": ok"
- 若未匹配到expect, 则尝试匹配字符串XFAIL, 若匹配到则输出 "saw 'XFAIL': [SKIP]" (XFAIL代表这是意料之中的失败,不属于测试失败。如一个测试case只在arm64平台可以执行,那么通常在其他平台执行时会输出一个XFAIL,代表此用例本应该失败)。
- 若前面二者均未匹配到, 则输出 "missing "$expect": [FAIL]" ,代表测试执行失败。
- 测试举例
##texts.txt
#PANIC //默认不输出
EXEC_DATA //默认成功时应输出 call trace
CORRUPT_LIST_ADD list_add corruption //成功应输出 list_add corruption
## 执行测试用例
/kselftest_install/lkdtm # ./PANIC.sh
Skipping PANIC: crashes entire system //通常这里情况下应该导致系统crash
/kselftest_install/lkdtm # ./EXEC_DATA.sh
[ 46.318367] lkdtm: Performing direct entry EXEC_DATA
[ 46.319026] lkdtm: attempting ok execution at ffff8000088725d0
[ 46.319790] lkdtm: attempting bad execution at ffff80000a209f58
[ 46.320540] Unable to handle kernel execute from non-executable memory at virtual address ffff80000a209f58
[ 46.321233] Mem abort info:
......
[ 46.338468] Call trace:
[ 46.338733] data_area+0x0/0x40
[ 46.339203] lkdtm_EXEC_DATA+0x24/0x34
[ 46.339533] lkdtm_do_action+0x20/0x3c
[ 46.339851] direct_entry+0x110/0x1b0
[ 46.340125] full_proxy_write+0x68/0xc4
[ 46.344073] ---[ end trace 0000000000000000 ]---
EXEC_DATA: saw 'call trace:': ok //因为匹配到默认字符串'call trace:',用例执行成功
/kselftest_install/lkdtm # ./CORRUPT_LIST_ADD.sh
[ 202.110283] lkdtm: attempting good list addition
[ 202.110566] lkdtm: attempting corrupted list addition
[ 202.110829] lkdtm: list_add() corruption not detected!
[ 202.111103] lkdtm: This is probably expected, since this kernel (5.17.0-rc5-13981-g0d1e946dc562-dirty aarch64) was built *without* CONFIG_DEBUG_LIST=y
CORRUPT_LIST_ADD: missing 'list_add corruption': [FAIL] //本机没有匹配到 'list_add corruption', 用例执行失败
参考资料:
[1] Provoking crashes with Linux Kernel Dump Test Module (LKDTM) — The Linux Kernel documentation
[2] kernel/git/torvalds/linux.git - Linux kernel source tree