ARM: traps: Implement KCFI trap handler for ARM32
Subsystems (2)
PATCH v3 1/1 ARM: traps: Implement KCFI trap handler for ARM32
ARM32 GCC KCFI[1] violations currently show as generic "Oops - undefined instruction" errors, making debugging CFI failures difficult. Add a proper KCFI trap handler similar to the aarch64 implementation to provide clear CFI error messages, including the call target and the expected type.
Clang and GCC trap CFI failures differently on ARM32. Clang lowers its checks to a BKPT, handled via the breakpoint/prefetch-abort path in hw_breakpoint.c, which cannot recover the target or expected type and so must report via report_cfi_failure_noaddr(). GCC instead lowers KCFI checks to a UDF (undefined instruction) whose immediate encodes the registers involved, so the handler can decode it and call the full report_cfi_failure() with the target and expected type.
The GCC ARM32 KCFI implementation uses UDF instructions with a specific encoding pattern:
- UDF instruction format: cccc 0111 1111 imm12 1111 imm4
- 16-bit immediate reconstructed from bits 19-8 and 3-0
- KCFI encoding: 0x8000 | (type_reg_num << 5) | (target_reg_num & 31)
- Bit 15: KCFI trap identifier (0x8000)
- Bits 9-5: Type ID register field (0x1F when unavailable)
- Bits 4-0: Target address register number
When the type register field is 0x1F (the type was spilled rather than kept in a register), the handler walks back up to 6 preceding instructions to recover the 32-bit type ID from the four EOR-immediate instructions the compiler emits, similar to x86 CFI trap reconstruction.
The handler is dispatched directly from do_undefinstr() rather than through register_undef_hook(). A registered hook is only installed once initcalls run, so any KCFI violation that fires earlier in boot would be reported as a generic "undefined instruction"; calling the decoder from the exception handler itself catches failures from the very first instruction. Dispatching directly also lets the handler ignore non-kernel-mode UDFs, preventing a user process from spoofing the KCFI encoding.
Both the UDF handler and the Clang breakpoint handler in hw_breakpoint.c act on the report_cfi_failure*() result identically (oops on a fatal violation, or skip the trapping instruction under CFI permissive mode) so factor that shared tail into arm_cfi_handle_failure(), declared in asm/traps.h.
---
v3:
- rebased on https://lore.kernel.org/lkml/[email protected]/
- drop initcall registration: potentially too late
- share logic with hw_breakpoint.c
v2: https://lore.kernel.org/lkml/[email protected]/#r
Cc: "Russell King (Oracle)" <[email protected]>
Cc: Linus Walleij <[email protected]>
Cc: Nathan Chancellor <[email protected]>
Cc: Sami Tolvanen <[email protected]>
Cc: Zhen Lei <[email protected]>
Cc: Arnd Bergmann <[email protected]>
Cc: "Michał Pecio" <[email protected]>
Cc: Sebastian Andrzej Siewior <[email protected]>
Cc: <[email protected]>
--- arch/arm/include/asm/traps.h | 3 + arch/arm/kernel/hw_breakpoint.c | 40 +++------ arch/arm/kernel/traps.c | 144 ++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 29 deletions(-)
arch/arm/include/asm/traps.h +3 -0
@@ -2,6 +2,7 @@#ifndef _ASMARM_TRAP_H#define _ASMARM_TRAP_H +#include <linux/bug.h> #include <linux/linkage.h> #include <linux/list.h>
@@ -20,6 +21,8 @@ struct undef_hook {void register_undef_hook (struct undef_hook *hook);void unregister_undef_hook (struct undef_hook *hook); +bool arm_cfi_handle_failure (struct pt_regs *regs,enum bug_trap_type type); +static inline int __in_irqentry_text (unsigned long ptr) {extern char __irqentry_text_start[];
arch/arm/kernel/hw_breakpoint.c +11 -29
@@ -904,33 +904,6 @@ static void breakpoint_handler(unsigned long unknown, struct pt_regs *regs)watchpoint_single_step_handler (addr); } -#ifdef CONFIG_CFI -static void hw_breakpoint_cfi_handler (struct pt_regs *regs) -{ -/* -* TODO: implementing target and type to pass to CFI using the more -* elaborate report_cfi_failure() requires compiler work. To be able -* to properly extract target information the compiler needs to -* emit a stable instructions sequence for the CFI checks so we can -* decode the instructions preceding the trap and figure out which -* registers were used. -*/ - -switch (report_cfi_failure_noaddr (regs,instruction_pointer (regs))) { -case BUG_TRAP_TYPE_BUG : -die ("Oops - CFI" , regs,0 ); -break ; -case BUG_TRAP_TYPE_WARN : -/* Skip the breaking instruction */ -instruction_pointer (regs) +=4 ; -break ; -default : -die ("Unknown CFI error" , regs,0 ); -break ; - } -} -#endif -/* * Called from either the Data Abort Handler [watchpoint] or the * Prefetch Abort Handler [breakpoint] with interrupts disabled.
@@ -962,10 +935,19 @@ static int hw_breakpoint_pending(unsigned long addr, unsigned int fsr,break ;#ifdef CONFIG_CFI case ARM_ENTRY_CFI_BREAKPOINT : +/* +* Clang lowers its CFI checks to a plain breakpoint that does not +* encode the call target or expected type, so (unlike the GCC UDF +* handler in traps.c) this can only report via +* report_cfi_failure_noaddr(). Recovering the target would require +* the compiler to emit a stable instruction sequence around the +* check to decode the registers used. +*/ if (user_mode (regs)) ret =1 ;/* Don't handle userspace BKPT */ -else -hw_breakpoint_cfi_handler (regs); +else if (!arm_cfi_handle_failure (regs, +report_cfi_failure_noaddr (regs,instruction_pointer (regs)))) +die ("Unknown CFI error" , regs,0 );break ;#endif default :
arch/arm/kernel/traps.c +144 -0
168 lines not on this page · raw message
Signature · 1 line
2.34.1
Quoted 36 lines · Kees Cook
ARM32 GCC KCFI[1] violations currently show as generic "Oops - undefined instruction" errors, making debugging CFI failures difficult. Add a proper KCFI trap handler similar to the aarch64 implementation to provide clear CFI error messages, including the call target and the expected type.
Clang and GCC trap CFI failures differently on ARM32. Clang lowers its checks to a BKPT, handled via the breakpoint/prefetch-abort path in
hw_breakpoint.c, which cannot recover the target or expected type and so must report viareport_cfi_failure_noaddr(). GCC instead lowers KCFI checks to a UDF (undefined instruction) whose immediate encodes the registers involved, so the handler can decode it and call the fullreport_cfi_failure()with the target and expected type.The GCC ARM32 KCFI implementation uses UDF instructions with a specific encoding pattern:
- UDF instruction format: cccc 0111 1111 imm12 1111 imm4
- 16-bit immediate reconstructed from bits 19-8 and 3-0
- KCFI encoding:0x8000| (type_reg_num<< 5) | (target_reg_num& 31)
- Bit 15: KCFI trap identifier (0x8000)
- Bits 9-5: Type ID register field (0x1Fwhen unavailable)
- Bits 4-0: Target address register numberWhen the type register field is
0x1F(the type was spilled rather than kept in a register), the handler walks back up to 6 preceding instructions to recover the 32-bit type ID from the four EOR-immediate instructions the compiler emits, similar to x86 CFI trap reconstruction.The handler is dispatched directly from
do_undefinstr()rather than throughregister_undef_hook(). A registered hook is only installed once initcalls run, so any KCFI violation that fires earlier in boot would be reported as a generic "undefined instruction"; calling the decoder from the exception handler itself catches failures from the very first instruction. Dispatching directly also lets the handler ignore non-kernel-mode UDFs, preventing a user process from spoofing the KCFI encoding.
Quoted 8 lines
Both the UDF handler and the Clang breakpoint handler in
hw_breakpoint.cact on the report_cfi_failure*() result identically (oops on a fatal violation, or skip the trapping instruction under CFI permissive mode) so factor that shared tail intoarm_cfi_handle_failure(), declared inasm/traps.h.Signed-off-by Kees Cook <[email protected]>
I tested this in QEMU using LKDTM, which shows lkdtm_increment_int() as the target of the failed call.
Signature · 2 lines
Cheers, Nathan
Quoted 4 lines · Kees Cook
ARM32 GCC KCFI[1] violations currently show as generic "Oops - undefined instruction" errors, making debugging CFI failures difficult. Add a proper KCFI trap handler similar to the aarch64 implementation to provide clear CFI error messages, including the call target and the expected type.
Quoted 7 lines
Clang and GCC trap CFI failures differently on ARM32. Clang lowers its checks to a BKPT, handled via the breakpoint/prefetch-abort path in
hw_breakpoint.c, which cannot recover the target or expected type and so must report viareport_cfi_failure_noaddr(). GCC instead lowers KCFI checks to a UDF (undefined instruction) whose immediate encodes the registers involved, so the handler can decode it and call the fullreport_cfi_failure()with the target and expected type.
Aha, that explains a few things.
The UDF is a better approach for ARM I think (Russell will know best).
As noted from earlier conversation the breakpoint usecase overloading on ARM is a mess, and we would certainly like to avoid it if possible, albeit I have a patch in Russell's patch tracker to paper it over.
Do you think that long term we could switch Clang to do what GCC does?
Yours,
Linus Walleij
Quoted 20 lines · Linus Walleij
Quoted 4 lines · Kees Cook
ARM32 GCC KCFI[1] violations currently show as generic "Oops - undefined instruction" errors, making debugging CFI failures difficult. Add a proper KCFI trap handler similar to the aarch64 implementation to provide clear CFI error messages, including the call target and the expected type.
Reviewed-by Linus Walleij <[email protected]>Quoted 7 lines
Clang and GCC trap CFI failures differently on ARM32. Clang lowers its checks to a BKPT, handled via the breakpoint/prefetch-abort path in
hw_breakpoint.c, which cannot recover the target or expected type and so must report viareport_cfi_failure_noaddr(). GCC instead lowers KCFI checks to a UDF (undefined instruction) whose immediate encodes the registers involved, so the handler can decode it and call the fullreport_cfi_failure()with the target and expected type.Aha, that explains a few things.
Quoted 8 lines
The UDF is a better approach for ARM I think (Russell will know best).
As noted from earlier conversation the breakpoint usecase overloading on ARM is a mess, and we would certainly like to avoid it if possible, albeit I have a patch in Russell's patch tracker to paper it over.
Do you think that long term we could switch Clang to do what GCC does?
Yeah, I think that's reasonable.
https://github.com/llvm/llvm-project/issues/211032
https://github.com/ClangBuiltLinux/linux/issues/2176
Signature · 2 lines
Thanks, ~Nick Desaulniers
arch/arm/kernel/traps.c int call_undef_hook(struct pt_regs *regs, unsigned int instr) · line 469
Quoted 10 lines · Kees Cook
arch/arm/kernel/traps.c +10
-0+bool arm_cfi_handle_failure (struct pt_regs *regs,enum bug_trap_type type) +{ +switch (type) { +case BUG_TRAP_TYPE_BUG : +die ("Oops - CFI" , regs,0 ); +break ; +case BUG_TRAP_TYPE_WARN : +/* Permissive mode: skip the trapping instruction and continue. */ +instruction_pointer (regs) +=4 ; +break ;
On Thumb-2, Clang emits a 16-bit BKPT for CFI checks. This skips the following Thumb instruction.
Thanks,
Karl
arch/arm/kernel/traps.c int call_undef_hook(struct pt_regs *regs, unsigned int instr) · line 492
Quoted 3 lines · Kees Cook
arch/arm/kernel/traps.c +3
-0+/* UDF base pattern with KCFI bit: cond=0xe, 0x7f, xxxx, 1xxx, 0xf, xxxx */ +#define CFI_UDF_IMM_BASE 0xe7f008f0 +#define CFI_UDF_IMM_MASK 0xfff008f0/* Mask for UDF + KCFI bit matching */
With this patch applied, KGDB and CONFIG_CFI cannot be used together. Setting a breakpoint causes Oops as CFI failure.
This patch on top of "ARM: breakpoint: CFI breakpoints only on demand", multi_v7_defconfig + CONFIG_CFI=y + KGDB/KGDB_KDB/MAGIC_SYSRQ, THUMB2_KERNEL=n, clang 22.1.8 LLVM=1, qemu-system-arm -M virt -cpu cortex-a15, booted with kgdboc=ttyAMA0.
Then "echo g > /proc/sysrq-trigger":
sysrq: DEBUG
CFI failure at kgdb_breakpoint+0x2c/0x58 (target: kgdb_breakpoint+0x2c/0x58; expected type: 0xc0472d70)
Internal error: Oops - CFI: 0 [#1] SMP ARM Modules linked in: CPU: 0 UID: 0 PID: 1 Comm: init Not tainted 7.2.0-rc3-00278-g38436106b2f5-dirty #1 VOLUNTARY Hardware name: Generic DT based system PC is at kgdb_breakpoint+0x2c/0x58 LR is at kgdb_breakpoint+0x2c/0x58 pc : [<c0472d70>] lr : [<c0472d70>] psr: 60000013 sp : e080dea8 ip : 00000000 fp : 0021a1c0 r10: 00000000 r9 : 00000000 r8 : c2004048 r7 : c2205d9c r6 : 00000000 r5 : c1609558 r4 : c22bb994 r3 : 00000000 r2 : 00000000 r1 : 00000000 r0 : 00000000 Flags: nZCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment none ... kgdb_breakpoint from __handle_sysrq+0x150/0x158
Thanks,
Karl
arch/arm/kernel/traps.c int call_undef_hook(struct pt_regs *regs, unsigned int instr) · line 553
Quoted 6 lines · Kees Cook
arch/arm/kernel/traps.c +6
-0+/* Check for EOR/EORS immediate: cccc 0010 0x1n Rn Rd immediate */ +if ((instr_prev &0x0fe00000 ) ==0x02200000 ) { +/* Extract EOR immediate value and XOR to reconstruct type */ +u32 rotate = (instr_prev >>8 ) &0xf ; +u32 imm8 = instr_prev &0xff ; +u32 imm32 = (imm8 >> (rotate *2 )) | (imm8 << (32 - rotate *2 ));
With CONFIG_UBSAN_SHIFT=y, I reproduced the following in QEMU with GCC v14 KCFI and LKDTM CFI_FORWARD_PROTO:
UBSAN: shift-out-of-bounds in arch/arm/kernel/traps.c:555:48 shift exponent 32 is too large for 32-bit type 'unsigned int'
Could this use ror32() from <linux/bitops.h> instead?
Thanks,
Karl