Skip to contentSkip to conversation

kunit: cfi: Add test for kCFI indirect-call type checks

PATCH Superseded Kees Cook 1 participant 1 message Jun 18 Clang control flow integrity support

v1 of 2 Superseded Latest is v2 →

lore
Share & tools
lore

Kees Cook

author maintainer
#0 lore raw

PATCH 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.

Assisted-by Claude:claude-opus-4-8[1m]

---
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
index e86880045158..c463b6f2960b 100644
@@ -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
index 6785982013dc..448de4fff75c 100644
@@ -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
new file mode 100644
index 000000000000..70f1f9a5c502
@@ -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
index 0f220d29225c..e4e66a9423ca 100644
@@ -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
index 4dad04ead06c..28e5c915ccc9 100644
@@ -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
index c8d4b913f26c..8c704a24136b 100644
@@ -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