kunit: cfi: Add test for kCFI indirect-call type checks
Kees Cook
maintainerPATCH v2 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.
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.
---
v2: fix sashiko feedback (rcu, smp, test run detection, header fix gone)
https://sashiko.dev/#/patchset/20260618194001.work.490-kees%40kernel.org
v1: https://lore.kernel.org/all/[email protected]/
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 | 49 +++++++ kernel/tests/cfi_kunit.c | 276 +++++++++++++++++++++++++++++++++++++++ MAINTAINERS | 1 + 7 files changed, 364 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 +49 -0
61 lines not on this page · raw message
kernel/tests/cfi_kunit.c new file +276 -0
276 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
Quoted 7 lines · Kees Cook
kernel/cfi.c +0
-0@@ -8,12 +8,61 @@#include <linux/bpf.h> #include <linux/cfi_types.h>
kernel/cfi.c line 19
Quoted 10 lines
kernel/cfi.c +6
-0#include <linux/cfi.h> +#include <linux/rcupdate.h> 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 )) +{
BUG_ON(cfi_kunit_failure_hook);
kernel/cfi.c line 29
Quoted 10 lines
kernel/cfi.c +10
-0+WRITE_ONCE (cfi_kunit_failure_hook, hook); + +/* +* On unregister, wait for any in-flight cfi_kunit_handled() caller to +* finish before the (possibly module-resident) hook can be freed. +*/ +if (!hook) +synchronize_rcu (); +} +EXPORT_SYMBOL_GPL (cfi_kunit_set_failure_hook);
EXPORT_SYMBOL_FOR_MODULES(cfi_kunit_set_failure_hook, "cfi_kunit")
Quoted 24 lines
kernel/cfi.c +23
-0+static bool cfi_kunit_handled (void ) +{ +bool (*hook)(void ); +bool handled = false; + +/* +* Runs in CFI trap context (NMI-like on some arches); RCU is watching +* by this point, and the read-side section pairs with the +* synchronize_rcu() on unregister to keep the hook alive across the +* call. +*/ +rcu_read_lock (); + hook =READ_ONCE (cfi_kunit_failure_hook); +if (hook) + handled =hook (); +rcu_read_unlock (); + +return handled; +} +#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,
kernel/cfi.c line 64
Quoted 10 lines
kernel/cfi.c +8
-0unsigned 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 ;
Somewhat ironic to make an indirect call failure do an indirect call, which can fail...
This is really rather horrible. Also, now all an attacker needs to do is ensure cfi_kunit_handled() unconditionally returns true. IOW, no distro must ever have this KUNIT crap enabled.
Quoted 47 lines · Peter Zijlstra
Quoted 7 lines · Kees Cook
kernel/cfi.c +0
-0@@ -8,12 +8,61 @@#include <linux/bpf.h> #include <linux/cfi_types.h> kernel/cfi.c line 19
Quoted 10 lines
kernel/cfi.c +6
-0#include <linux/cfi.h> +#include <linux/rcupdate.h> 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 )) +{BUG_ON(cfi_kunit_failure_hook);kernel/cfi.c line 29
Quoted 10 lines
kernel/cfi.c +10
-0+WRITE_ONCE (cfi_kunit_failure_hook, hook); + +/* +* On unregister, wait for any in-flight cfi_kunit_handled() caller to +* finish before the (possibly module-resident) hook can be freed. +*/ +if (!hook) +synchronize_rcu (); +} +EXPORT_SYMBOL_GPL (cfi_kunit_set_failure_hook);
EXPORT_SYMBOL_FOR_MODULES(cfi_kunit_set_failure_hook, "cfi_kunit")kernel/cfi.c line 64
Quoted 11 lines
kernel/cfi.c +8
-0enum 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 ;Somewhat ironic to make an indirect call failure do an indirect call, which can fail...
Quoted 3 lines
This is really rather horrible. Also, now all an attacker needs to do is ensure
cfi_kunit_handled()unconditionally returns true. IOW, no distro must ever have this KUNIT crap enabled.
Also, if this lives, the check should at least trip the cfi_warn path, being completely silent is terrible.
That is:
if (cfi_warn || cfi_kunit_handled())
And then we must ensure that this warn is not suppressible by that other kunit crud.
Kees Cook
maintainerQuoted 6 lines · Peter Zijlstra
Quoted 3 lines
This is really rather horrible. Also, now all an attacker needs to do is ensure
cfi_kunit_handled()unconditionally returns true. IOW, no distro must ever have this KUNIT crap enabled.Also, if this lives, the check should at least trip the
cfi_warnpath, being completely silent is terrible.
If anyone actually ships kunit in production, then no, I will NAK my own patch. ;) In that case I will go back to a version I never sent, which uses Kunit's try/catch Oops checker (which doesn't work on riscv). I only did it this way (similar to the fortify kunit testing) so I could get riscv coverage.
Signature · 1 line
Kees Cook
Kees Cook
maintainerQuoted 5 lines · Kees Cook
Quoted 4 lines · Peter Zijlstra
Quoted 3 lines
This is really rather horrible. Also, now all an attacker needs to do is ensure
cfi_kunit_handled()unconditionally returns true. IOW, no distro must ever have this KUNIT crap enabled.
Quoted 8 lines
Quoted 2 lines
Also, if this lives, the check should at least trip the
cfi_warnpath, being completely silent is terrible.If anyone actually ships kunit in production, then no, I will NAK my own patch. ;) In that case I will go back to a version I never sent, which uses Kunit's try/catch Oops checker (which doesn't work on riscv). I only did it this way (similar to the fortify kunit testing) so I could get riscv coverage.
Fedora and Android do appear to ship with CONFIG_KUNIT=m. Debian and Ubuntu don't from what I can see.
So, yeah, NAK.
I'll send v3 (really v0)...
Signature · 1 line
Kees Cook
Kees Cook
maintainerQuoted 3 lines · Peter Zijlstra
This is really rather horrible. Also, now all an attacker needs to do is ensure
cfi_kunit_handled()unconditionally returns true. IOW, no distro must ever have this KUNIT crap enabled.
Isn't that already true? I thought, like LKDTM, nothing ships with KUnit built in?
Signature · 1 line
Kees Cook
Le 19/06/2026 à 5:09 AM, 'Kees Cook' via KUnit Development a écrit :
Quoted 54 lines
drivers/misc/lkdtm/cfi.calready 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=nit 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, whenCONFIG_CFI_KUNIT_TEST=y,kernel/cfi.cgrows a small hook,cfi_kunit_set_failure_hook(), consulted byreport_cfi_failure()on every kCFI trap. When the registered hook counts the trap, the report is suppressed andBUG_TRAP_TYPE_WARNis 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 howCFI_PERMISSIVEis 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 underlib/tests/, since the code under test (kernel/cfi.c) lives inkernel/; kernel/Makefile is taught to descend into tests/ whenCONFIG_KUNITis set. TheKconfigsits next to "config CFI" and "configCFI_PERMISSIVE" inarch/Kconfigand 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 whenCONFIG_CFI=n. The hook makes that case work withCFI_PERMISSIVEeither enabled or disabled.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.
Quoted 8 lines
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.pyfor ARCH=x86_64 (CFI_PERMISSIVEboth n and y), ARCH=arm64, ARCH=arm, and ARCH=riscv.Assisted-by Claude:claude-opus-4-8[1m]Signed-off-by Kees Cook <[email protected]>---
Finally got around to testing this.
My biggest question is whether it's particularly important that this test works with CFI_PERMISSIVE disabled: it seems like it'd be a lot simpler if this just depended on CFI_PERMISSIVE=y.
(And that'd avoid the security worries with Fedora/Android building with CONFIG_KUNIT=m.)
Quoted 129 lines
v2: fix sashiko feedback (rcu, smp, test run detection, header fix gone)
https://sashiko.dev/#/patchset/20260618194001.work.490-kees%40kernel.orgv1: https://lore.kernel.org/all/[email protected]/
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 | 49 +++++++ kernel/tests/cfi_kunit.c | 276 +++++++++++++++++++++++++++++++++++++++ MAINTAINERS | 1 + 7 files changed, 364 insertions(+) create mode 100644 kernel/tests/Makefile create mode 100644 kernel/tests/cfi_kunit.carch/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 helpkernel/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.okernel/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.oinclude/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 0Ukernel/cfi.c +11
-0@@ -8,12 +8,61 @@#include <linux/bpf.h> #include <linux/cfi_types.h> #include <linux/cfi.h> +#include <linux/rcupdate.h> 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); + +/* +* On unregister, wait for any in-flight cfi_kunit_handled() caller to +* finish before the (possibly module-resident) hook can be freed.
kernel/cfi.c line 34
Quoted 10 lines
kernel/cfi.c +10
-0+*/ +if (!hook) +synchronize_rcu (); +} +EXPORT_SYMBOL_GPL (cfi_kunit_set_failure_hook); + +static bool cfi_kunit_handled (void ) +{ +bool (*hook)(void ); +bool handled = false;
It might make sense to check KUnit is running at all here:
if (!static_branch_unlikely(&kunit_running)) return NULL;
All of the other KUnit hooks (see, e.g, include/kunit/test-bug.h) do this, and while this is technically being checked within your hook when the current test is checked, you've already incurred cost the rcu lock and indirect function call by this point.
Quoted 328 lines
kernel/cfi.c +28
-0+ +/* +* Runs in CFI trap context (NMI-like on some arches); RCU is watching +* by this point, and the read-side section pairs with the +* synchronize_rcu() on unregister to keep the hook alive across the +* call. +*/ +rcu_read_lock (); + hook =READ_ONCE (cfi_kunit_failure_hook); +if (hook) + handled =hook (); +rcu_read_unlock (); + + return handled; +} +#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 +276
-0@@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-2.0 +/* +* KUnit test for Kernel Control Flow Integrity (kCFI). +* +* Exercises properties of the compiler's KCFI indirect-call checks: +* +* Mirrors drivers/misc/lkdtm/cfi.c's CFI_FORWARD_PROTO test, but as a +* self-contained kunit suite that drives kernel/cfi.c via the standard +* indirect-call path. For the fatal-trap behavior of a real violation, see +* LKDTM's "CFI_*" tests. +*/ + +#include <kunit/test.h> +#include <kunit/test-bug.h> +#include <linux/cfi.h> + +/* +* The test case currently expecting to count kCFI traps, and its running +* count. Only ever touched for the test that armed cfi_kunit_active, so a +* single counter is safe without locking. +*/ +static struct kunit *cfi_kunit_active; +static int cfi_kunit_trap_count; + +/* +* Consulted from report_cfi_failure() in kCFI trap context, which may be +* NMI-like (e.g. riscv kernel breakpoints), so this must stay lock-free: it +* only reads the current task's kunit pointer and touches module-static +* counters. It claims the failure by counting it and asking the arch handler +* to skip the trap and resume, but only when the trap fired on the very test +* that armed us. Any other CFI failure is left to behave normally. +*/ +static bool cfi_kunit_failure_hook (void ) +{ +struct kunit *test =READ_ONCE (cfi_kunit_active); + +/* +* Claim the failure only when a test is armed and it is the one +* running on this thread. Without the NULL check, a real CFI violation +* on a background thread (where kunit_get_current_test() is also NULL) +* while no test is active would match and be wrongly suppressed. +*/ +if (!test ||kunit_get_current_test () != test) +return false; + +WRITE_ONCE (cfi_kunit_trap_count, cfi_kunit_trap_count +1 ); +return true; +} + +static int called_count; + +/* +* Two same-arity, same-arg-type callees with deliberately different return +* types so that kCFI's type-hash check at the call site catches the cast. +*/ +static noinline void cfi_increment_void(int *counter) +{ + (*counter)++; +} + +static noinline int cfi_increment_int(int *counter) +{ + (*counter)++; +return *counter; +} + +/* +* The indirect call site. Type of the function pointer is what kCFI +* compares against the hash baked into the callee's __cfi_<name> prefix. +*/ +static noinline void cfi_indirect_call(void (*func)(int *)) +{ +func (&called_count); +} + +/* +* Increasing-arity callees. Each returns a position-weighted sum of its +* arguments so that a dropped, reordered, or zeroed argument produces a wrong +* result rather than a coincidental match. Called with args (1, 2, 3, ...), +* cfi_arityN() returns sum(i*i) for i in 1..N. +*/ +static noinline int cfi_arity1(int a) +{ +return a; +} + +static noinline int cfi_arity2(int a, int b) +{ +return a +2 * b; +} + +static noinline int cfi_arity3(int a, int b, int c) +{ +return a +2 * b +3 * c; +} + +static noinline int cfi_arity4(int a, int b, int c, int d) +{ +return a +2 * b +3 * c +4 * d; +} + +static noinline int cfi_arity5(int a, int b, int c, int d, int e) +{ +return a +2 * b +3 * c +4 * d +5 * e; +} + +static noinline int cfi_arity6(int a, int b, int c, int d, int e, int f) +{ +return a +2 * b +3 * c +4 * d +5 * e +6 * f; +} + +static noinline int cfi_arity7(int a, int b, int c, int d, int e, int f, int g) +{ +return a +2 * b +3 * c +4 * d +5 * e +6 * f +7 * g; +} + +/* +* Tail-calling trampolines: each receives the callee as an opaque pointer +* (defeating optimization) plus the arguments, then `return fn(args)` as +* its final statement so the compiler lowers it to an indirect tail call. +* Arity grows so the callee pointer and the kCFI scratch registers +* increasingly contend with argument registers. +*/ +static noinline int cfi_tail_call1(int (*fn)(int), int a) +{ +return fn (a); +} + +static noinline int cfi_tail_call2(int (*fn)(int, int), int a, int b) +{ +return fn (a, b); +} + +static noinline int cfi_tail_call3(int (*fn)(int, int, int), +int a,int b,int c) +{ +return fn (a, b, c); +} + +static noinline int cfi_tail_call4(int (*fn)(int, int, int, int), +int a,int b,int c,int d) +{ +return fn (a, b, c, d); +} + +static noinline int cfi_tail_call5(int (*fn)(int, int, int, int, int), +int a,int b,int c,int d,int e) +{ +return fn (a, b, c, d, e); +} + +static noinline int cfi_tail_call6(int (*fn)(int, int, int, int, int, int), +int a,int b,int c,int d,int e,int f) +{ +return fn (a, b, c, d, e, f); +} + +static noinline int cfi_tail_call7(int (*fn)(int, int, int, int, int, int, int), +int a,int b,int c,int d,int e,int f, +int g) +{ +return fn (a, b, c, d, e, f, g); +} + +#define CFI_MAX_ARITY 7 + +static void cfi_kunit_forward_proto_traps (struct kunit *test) +{ +int before_traps =READ_ONCE (cfi_kunit_trap_count); + +/* Only this case needs kCFI; the well-typed cases below run regardless. */ +if (!IS_ENABLED (CONFIG_CFI )) +kunit_skip (test,"kCFI is not enabled (CONFIG_CFI=n)" ); + +/* +* Force a kCFI type mismatch: the call site expects a callee whose +* __cfi_ prefix encodes "void (*)(int *)", but the actual callee's +* prefix encodes "int (*)(int *)". The (void *) intermediate cast +* follows drivers/misc/lkdtm/cfi.c and sidesteps -Wcast-function-type +* on the deliberate mis-cast. +* +* kCFI must detect this. The failure hook counts the trap and lets us +* survive it, so control returns here normally. +*/ +cfi_indirect_call ((void *)cfi_increment_int); + +KUNIT_EXPECT_EQ_MSG (test,READ_ONCE (cfi_kunit_trap_count), before_traps +1 , +"mismatched-prototype indirect call was not caught by kCFI\n" ); +} + +static void cfi_kunit_baseline_matched_proto (struct kunit *test) +{ +int before_traps =READ_ONCE (cfi_kunit_trap_count); +int before_calls = called_count; + +/* Matched prototype: must NOT trap and must increment the counter. */ +cfi_indirect_call (cfi_increment_void); +KUNIT_EXPECT_EQ (test, called_count, before_calls +1 ); +KUNIT_EXPECT_EQ_MSG (test,READ_ONCE (cfi_kunit_trap_count), before_traps, +"well-typed indirect call spuriously tripped kCFI\n" ); +} + +static void cfi_kunit_arity_matched_calls (struct kunit *test) +{ +/* expected[N] = sum(i*i) for i in 1..N */ +static const int expected[CFI_MAX_ARITY +1 ] = { +0 ,1 ,5 ,14 ,30 ,55 ,91 ,140 , + }; +int before_traps =READ_ONCE (cfi_kunit_trap_count); +int results[CFI_MAX_ARITY +1 ]; +int i; + + results[1 ] =cfi_tail_call1 (cfi_arity1,1 ); + results[2 ] =cfi_tail_call2 (cfi_arity2,1 ,2 ); + results[3 ] =cfi_tail_call3 (cfi_arity3,1 ,2 ,3 ); + results[4 ] =cfi_tail_call4 (cfi_arity4,1 ,2 ,3 ,4 ); + results[5 ] =cfi_tail_call5 (cfi_arity5,1 ,2 ,3 ,4 ,5 ); + results[6 ] =cfi_tail_call6 (cfi_arity6,1 ,2 ,3 ,4 ,5 ,6 ); + results[7 ] =cfi_tail_call7 (cfi_arity7,1 ,2 ,3 ,4 ,5 ,6 ,7 ); + +for (i =1 ; i <=CFI_MAX_ARITY ; i++) +KUNIT_EXPECT_EQ_MSG (test, results[i], expected[i], +"arity-%d matched indirect call returned %d, expected %d\n" , + i, results[i], expected[i]); + +/* +* None of the matched calls may trip kCFI. A spurious trap here is a +* codegen bug, most likely the callee pointer never reaching the call +* register under argument-register pressure. +*/ +KUNIT_EXPECT_EQ_MSG (test,READ_ONCE (cfi_kunit_trap_count), before_traps, +"a matched-prototype indirect call tripped kCFI under register pressure (codegen bug)\n" ); +} + +static int cfi_kunit_init (struct kunit *test) +{ +WRITE_ONCE (cfi_kunit_trap_count,0 ); +WRITE_ONCE (cfi_kunit_active, test); +return 0 ; +} + +static void cfi_kunit_exit (struct kunit *test) +{ +WRITE_ONCE (cfi_kunit_active,NULL ); +} + +static int cfi_kunit_suite_init (struct kunit_suite *suite) +{ +cfi_kunit_set_failure_hook (cfi_kunit_failure_hook); +return 0 ; +} + +static void cfi_kunit_suite_exit (struct kunit_suite *suite) +{ +cfi_kunit_set_failure_hook (NULL ); +} + +static struct kunit_case cfi_kunit_cases[] = { +KUNIT_CASE (cfi_kunit_baseline_matched_proto), +KUNIT_CASE (cfi_kunit_arity_matched_calls), +KUNIT_CASE (cfi_kunit_forward_proto_traps), + {} +}; + +static struct kunit_suite cfi_kunit_suite = { + .name ="cfi" , + .init = cfi_kunit_init, + .exit = cfi_kunit_exit, + .suite_init = cfi_kunit_suite_init, + .suite_exit = cfi_kunit_suite_exit, + .test_cases = cfi_kunit_cases, +}; +kunit_test_suite (cfi_kunit_suite); + +MODULE_DESCRIPTION ("KUnit tests for kCFI indirect-call type checks" ); +MODULE_LICENSE ("GPL" );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]>