kunit: cfi: Add test for kCFI indirect-call type checks
Kees Cook
maintainerPATCH 1/1 kunit: cfi: Add test for kCFI indirect-call type checks
drivers/misc/lkdtm/cfi.c already exercises kCFI's forward-edge check via a debugfs trigger, but it is awkward to run from automated CI and is gated on LKDTM being built in. Add a self-contained kunit test that performs the same kind of indirect call through a deliberately-cast function pointer and validates that kCFI catches the mismatch, plus coverage that well-typed indirect calls are left undisturbed.
A deliberate kCFI violation is normally fatal: with CONFIG_CFI_PERMISSIVE=n it kills the calling thread, and on architectures where an in-kernel breakpoint is taken in NMI context (e.g. riscv) it cannot even do that and panics the machine instead. To test the check without depending on any of that, when CONFIG_CFI_KUNIT_TEST=y, kernel/cfi.c grows a small hook, cfi_kunit_set_failure_hook(), consulted by report_cfi_failure() on every kCFI trap. When the registered hook counts the trap, the report is suppressed and BUG_TRAP_TYPE_WARN is returned, so the arch trap handler skips the trapping instruction and resumes the thread: the same "report and continue" path as CFI_PERMISSIVE=y, but independent of how CFI_PERMISSIVE is configured. The hook only ever claims a failure that fired during the test that armed it (matched via the current task's kunit pointer), so any other CFI failure behaves normally. It runs in trap context, possibly NMI-like, so it stays lock-free.
With that in place, the kunit kCFI test adds the following tests:
- forward_proto: an indirect call through a "void (*)(int *)" pointer to an "int (*)(int *)" callee, which must trip kCFI exactly once.
- baseline: the same call with a matched prototype must not trip kCFI and must increment its counter (to show it actually got called).
- arity sweep: matched-prototype indirect tail calls across increasing arity (1 to 7 arguments) must not trip kCFI and must return the right values. kCFI's instrumentation uses scratch registers to perform the typeid lookup and validation, which can compete with the argument registers on register-starved ABIs (e.g. arm32's r0-r3, which also forces a spill to the stack). A GCC arm32 kCFI codegen bug was observed where the callee pointer never reached the call register in a high-arity indirect tail call, leaving the kCFI prologue to read its typeid from a stale register and trapping a perfectly well-typed call.
The test lives in the new kernel/tests/ subdirectory rather than under lib/tests/, since the code under test (kernel/cfi.c) lives in kernel/; kernel/Makefile is taught to descend into tests/ when CONFIG_KUNIT is set. The Kconfig sits next to "config CFI" and "config CFI_PERMISSIVE" in arch/Kconfig and depends only on "KUNIT": the well-typed baseline and arity cases exercise indirect-call codegen regardless of CFI, while the mismatch case skips at runtime when CONFIG_CFI=n. The hook makes that case work with CFI_PERMISSIVE either enabled or disabled.
This test is the first place that includes <linux/cfi.h> without <linux/uaccess.h> already being in scope, so cfi.h is made self-contained by including <linux/uaccess.h> for the get_kernel_nofault() it uses in cfi_get_func_hash().
The mismatched-prototype call uses the same "(void *)" intermediate cast that LKDTM uses, which is enough to silence -Wcast-function-type on the intentional mis-cast.
Build and boot tested with GCC 17.0.0 20260615 (with my experimental kCFI series); all three kunit cases pass under qemu via tools/testing/kunit/kunit.py for ARCH=x86_64 (CFI_PERMISSIVE both n and y), ARCH=arm64, ARCH=arm, and ARCH=riscv.
---
Cc: Sami Tolvanen <[email protected]>
Cc: Nathan Chancellor <[email protected]>
Cc: Arnd Bergmann <[email protected]>
Cc: Brendan Higgins <[email protected]>
Cc: David Gow <[email protected]>
Cc: Rae Moar <[email protected]>
Cc: [email protected]
Cc: [email protected]
--- arch/Kconfig | 15 +++ kernel/Makefile | 1 + kernel/tests/Makefile | 5 + include/linux/cfi.h | 17 +++ kernel/cfi.c | 28 ++++ kernel/tests/cfi_kunit.c | 268 +++++++++++++++++++++++++++++++++++++++ MAINTAINERS | 1 + 7 files changed, 335 insertions(+) create mode 100644 kernel/tests/Makefile create mode 100644 kernel/tests/cfi_kunit.c
arch/Kconfig +15 -0
@@ -983,6 +983,21 @@ config CFI_PERMISSIVE If unsure, say N. +config CFI_KUNIT_TEST + tristate "KUnit test kCFI indirect-call type checks at runtime" if !KUNIT_ALL_TESTS + depends on KUNIT + default KUNIT_ALL_TESTS + help + Builds a KUnit test that triggers kCFI type mismatches on real + indirect calls and verifies that the violations are detected, and + that well-typed indirect calls (including high-arity ones) are not + disturbed. The test registers a hook in the kCFI failure path so + its deliberate violations are counted and survived on its own + threads, so it works with CFI_PERMISSIVE either enabled or disabled. + + For the fatal-trap behavior of a real violation, see LKDTM's "CFI_*" + tests. + config HAVE_ARCH_WITHIN_STACK_FRAMES bool help
kernel/Makefile +1 -0
@@ -59,6 +59,7 @@ obj-y += dma/ obj-y += entry/ obj-y += unwind/ obj-$(CONFIG_MODULES) += module/ +obj-$(CONFIG_KUNIT) += tests/ obj-$(CONFIG_KCMP) += kcmp.o obj-$(CONFIG_FREEZER) += freezer.o
kernel/tests/Makefile new file +5 -0
@@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for tests of kernel/ functions. + +obj-$(CONFIG_CFI_KUNIT_TEST) += cfi_kunit.o
include/linux/cfi.h +17 -0
@@ -24,6 +24,18 @@ static inline enum bug_trap_type report_cfi_failure_noaddr(struct pt_regs *regs,return report_cfi_failure (regs, addr,NULL ,0 ); } +#if IS_ENABLED (CONFIG_CFI_KUNIT_TEST ) +/* +* Register a hook consulted by report_cfi_failure() on every kCFI trap. If +* the hook returns true, the failure is treated as handled: the report is +* suppressed and BUG_TRAP_TYPE_WARN is returned so the arch trap handler +* skips the trapping instruction and resumes, regardless of CFI_PERMISSIVE. +* This lets the kCFI KUnit test count deliberate violations on its own +* threads without killing them. Pass NULL to unregister. +*/ +void cfi_kunit_set_failure_hook (bool (*hook)(void )); +#endif +#ifndef cfi_get_offset/* * Returns the CFI prefix offset. By default, the compiler emits only
@@ -58,6 +70,11 @@ extern u32 cfi_bpf_subprog_hash;static inline int cfi_get_offset (void ) {return 0 ; }static inline u32 cfi_get_func_hash (void *func) {return 0 ; } +#if IS_ENABLED (CONFIG_CFI_KUNIT_TEST ) +/* No kCFI traps to hook when CONFIG_CFI=n; the test skips at runtime. */ +static inline void cfi_kunit_set_failure_hook (bool (*hook)(void )) { } +#endif +#define cfi_bpf_hash 0U#define cfi_bpf_subprog_hash 0U
kernel/cfi.c +28 -0
@@ -11,9 +11,37 @@bool cfi_warn__ro_after_init =IS_ENABLED (CONFIG_CFI_PERMISSIVE ); +#if IS_ENABLED (CONFIG_CFI_KUNIT_TEST ) +static bool (*cfi_kunit_failure_hook)(void ); + +void cfi_kunit_set_failure_hook (bool (*hook)(void )) +{ +WRITE_ONCE (cfi_kunit_failure_hook, hook); +} +EXPORT_SYMBOL_GPL (cfi_kunit_set_failure_hook); + +static bool cfi_kunit_handled (void ) +{ +bool (*hook)(void ) =READ_ONCE (cfi_kunit_failure_hook); + +return hook &&hook (); +} +#else +static inline bool cfi_kunit_handled (void ) {return false; } +#endif +enum bug_trap_type report_cfi_failure (struct pt_regs *regs,unsigned long addr,unsigned long *target,u32 type) { +/* +* Let a registered KUnit test consume and count its own deliberate +* violations. If it claims the failure, suppress the report and tell +* the arch handler to skip the trap and resume the thread, regardless +* of CFI_PERMISSIVE. +*/ +if (cfi_kunit_handled ()) +return BUG_TRAP_TYPE_WARN ; +if (target)pr_err ("CFI failure at %pS (target: %pS; expected type: 0x%08x)\n" , (void *)addr, (void *)*target, type);
kernel/tests/cfi_kunit.c new file +268 -0
268 lines not on this page · raw message
MAINTAINERS +1 -0
@@ -6252,6 +6252,7 @@ B: https://github.com/ClangBuiltLinux/linux/issues T: git git://git.kernel.org/pub/scm/linux/kernel/git/kees/linux.git for-next/hardening F: include/linux/cfi.h F: kernel/cfi.c +F: kernel/tests/cfi_kunit.c CLANG-FORMAT FILE M: Miguel Ojeda <[email protected]>
Signature · 1 line
2.34.1