On Fri, Feb 14, 2025 at 10:57:51AM +0100, Peter Zijlstra wrote: > On Thu, Feb 13, 2025 at 12:53:28PM -0800, Kees Cook wrote: > > > Right, the "if they can control a function pointer" is the part I'm > > focusing on. This attack depends on making an indirect call with a > > controlled pointer. Non-FineIBT CFI will protect against that step, > > so I think this is only an issue for IBT-only and FineIBT, but not CFI > > nor CFI+IBT. > > Yes, the whole caller side validation should stop this.
And I think we can retro-fit that in FineIBT. Notably the current call sites look like: 0000000000000060 <fineibt_caller>: 60: 41 ba 78 56 34 12 mov $0x12345678,%r10d 66: 49 83 eb 10 sub $0x10,%r11 6a: 0f 1f 40 00 nopl 0x0(%rax) 6e: 41 ff d3 call *%r11 71: 0f 1f 00 nopl (%rax) Of which the last 6 bytes are the retpoline site (starting at 0x6e). It is trivially possible to re-arrange things to have both nops next to one another, giving us 7 bytes to muck about with. And I think we can just about manage to do a caller side hash validation in them bytes like: 0000000000000080 <fineibt_paranoid>: 80: 41 ba 78 56 34 12 mov $0x12345678,%r10d 86: 49 83 eb 10 sub $0x10,%r11 8a: 45 3b 53 07 cmp 0x7(%r11),%r10d 8e: 74 01 je 91 <fineibt_paranoid+0x11> 90: ea (bad) 91: 41 ff d3 call *%r11 And while this is somewhat daft, it would close the hole vs this entry point swizzle afaict, no? Patch against tip/x86/core (which includes the latest ibt bits as per this morning). Boots and builds the next kernel on my ADL. --- arch/x86/include/asm/bug.h | 1 + arch/x86/include/asm/cfi.h | 8 ++-- arch/x86/kernel/alternative.c | 107 +++++++++++++++++++++++++++++++++++++++--- arch/x86/kernel/cfi.c | 4 +- arch/x86/kernel/traps.c | 13 ++++- 5 files changed, 120 insertions(+), 13 deletions(-) diff --git a/arch/x86/include/asm/bug.h b/arch/x86/include/asm/bug.h index 1a5e4b372694..bc8a2ca3c82e 100644 --- a/arch/x86/include/asm/bug.h +++ b/arch/x86/include/asm/bug.h @@ -25,6 +25,7 @@ #define BUG_UD2 0xfffe #define BUG_UD1 0xfffd #define BUG_UD1_UBSAN 0xfffc +#define BUG_EA 0xffea #ifdef CONFIG_GENERIC_BUG diff --git a/arch/x86/include/asm/cfi.h b/arch/x86/include/asm/cfi.h index 7dd5ab239c87..550f75450e43 100644 --- a/arch/x86/include/asm/cfi.h +++ b/arch/x86/include/asm/cfi.h @@ -104,7 +104,7 @@ extern enum cfi_mode cfi_mode; struct pt_regs; #ifdef CONFIG_CFI_CLANG -enum bug_trap_type handle_cfi_failure(struct pt_regs *regs); +enum bug_trap_type handle_cfi_failure(int ud_type, struct pt_regs *regs); #define __bpfcall extern u32 cfi_bpf_hash; extern u32 cfi_bpf_subprog_hash; @@ -127,10 +127,10 @@ static inline int cfi_get_offset(void) extern u32 cfi_get_func_hash(void *func); #ifdef CONFIG_FINEIBT -extern bool decode_fineibt_insn(struct pt_regs *regs, unsigned long *target, u32 *type); +extern bool decode_fineibt_insn(int ud_type, struct pt_regs *regs, unsigned long *target, u32 *type); #else static inline bool -decode_fineibt_insn(struct pt_regs *regs, unsigned long *target, u32 *type) +decode_fineibt_insn(int ud_type, struct pt_regs *regs, unsigned long *target, u32 *type) { return false; } @@ -138,7 +138,7 @@ decode_fineibt_insn(struct pt_regs *regs, unsigned long *target, u32 *type) #endif #else -static inline enum bug_trap_type handle_cfi_failure(struct pt_regs *regs) +static inline enum bug_trap_type handle_cfi_failure(int ud_type, struct pt_regs *regs) { return BUG_TRAP_TYPE_NONE; } diff --git a/arch/x86/kernel/alternative.c b/arch/x86/kernel/alternative.c index 247ee5ffbff4..9e327b5e9f75 100644 --- a/arch/x86/kernel/alternative.c +++ b/arch/x86/kernel/alternative.c @@ -741,6 +741,11 @@ void __init_or_module noinline apply_retpolines(s32 *start, s32 *end) op2 = insn.opcode.bytes[1]; switch (op1) { + case 0x70 ... 0x7f: /* Jcc.d8 */ + /* See cfi_paranoid. */ + WARN_ON_ONCE(cfi_mode != CFI_FINEIBT); + continue; + case CALL_INSN_OPCODE: case JMP32_INSN_OPCODE: break; @@ -983,6 +988,8 @@ u32 cfi_get_func_hash(void *func) static bool cfi_rand __ro_after_init = true; static u32 cfi_seed __ro_after_init; +static bool cfi_paranoid __ro_after_init = false; + /* * Re-hash the CFI hash with a boot-time seed while making sure the result is * not a valid ENDBR instruction. @@ -1022,6 +1029,8 @@ static __init int cfi_parse_cmdline(char *str) cfi_mode = CFI_FINEIBT; } else if (!strcmp(str, "norand")) { cfi_rand = false; + } else if (!strcmp(str, "paranoid")) { + cfi_paranoid = true; } else { pr_err("Ignoring unknown cfi option (%s).", str); } @@ -1097,6 +1106,29 @@ extern u8 fineibt_caller_end[]; #define fineibt_caller_jmp (fineibt_caller_size - 2) +asm( ".pushsection .rodata \n" + "fineibt_paranoid_start: \n" + " movl $0x12345678, %r10d \n" + " sub $16, %r11 \n" + " cmpl 7(%r11), %r10d \n" + " je fineibt_paranoid_call \n" + "fineibt_paranoid_trap: \n" + " .byte 0xea \n" + "fineibt_paranoid_call: \n" + " call *%r11 \n" + "fineibt_paranoid_end: \n" + ".popsection \n" +); + +extern u8 fineibt_paranoid_start[]; +extern u8 fineibt_paranoid_trap[]; +extern u8 fineibt_paranoid_call[]; +extern u8 fineibt_paranoid_end[]; + +#define fineibt_paranoid_size (fineibt_paranoid_end - fineibt_paranoid_start) +#define fineibt_paranoid_ud (fineibt_paranoid_trap - fineibt_paranoid_start) +#define fineibt_paranoid_ind (fineibt_paranoid_call - fineibt_paranoid_start) + static u32 decode_preamble_hash(void *addr) { u8 *p = addr; @@ -1260,18 +1292,48 @@ static int cfi_rewrite_callers(s32 *start, s32 *end) { s32 *s; + BUG_ON(fineibt_paranoid_size != 20); + for (s = start; s < end; s++) { void *addr = (void *)s + *s; + struct insn insn; + u8 bytes[20]; u32 hash; + int ret; + u8 op; addr -= fineibt_caller_size; hash = decode_caller_hash(addr); - if (hash) { + if (!hash) + continue; + + if (!cfi_paranoid) { text_poke_early(addr, fineibt_caller_start, fineibt_caller_size); WARN_ON(*(u32 *)(addr + fineibt_caller_hash) != 0x12345678); text_poke_early(addr + fineibt_caller_hash, &hash, 4); + /* rely on apply_retpolines() */ + continue; } - /* rely on apply_retpolines() */ + + /* cfi_paranoid */ + ret = insn_decode_kernel(&insn, addr + fineibt_caller_size); + if (WARN_ON_ONCE(ret < 0)) + continue; + + op = insn.opcode.bytes[0]; + if (op != CALL_INSN_OPCODE && op != JMP32_INSN_OPCODE) { + WARN_ON_ONCE(1); + continue; + } + + memcpy(bytes, fineibt_paranoid_start, fineibt_paranoid_size); + memcpy(bytes + fineibt_caller_hash, &hash, 4); + + ret = emit_indirect(op, 11, bytes + fineibt_paranoid_ind); + if (WARN_ON_ONCE(ret != 3)) + continue; + + text_poke_early(addr, bytes, fineibt_paranoid_size); } return 0; @@ -1288,8 +1350,11 @@ static void __apply_fineibt(s32 *start_retpoline, s32 *end_retpoline, if (cfi_mode == CFI_AUTO) { cfi_mode = CFI_KCFI; - if (HAS_KERNEL_IBT && cpu_feature_enabled(X86_FEATURE_IBT)) + if (HAS_KERNEL_IBT && cpu_feature_enabled(X86_FEATURE_IBT)) { + if (!cpu_feature_enabled(X86_FEATURE_FRED)) + cfi_paranoid = true; cfi_mode = CFI_FINEIBT; + } } /* @@ -1346,8 +1411,10 @@ static void __apply_fineibt(s32 *start_retpoline, s32 *end_retpoline, /* now that nobody targets func()+0, remove ENDBR there */ cfi_rewrite_endbr(start_cfi, end_cfi); - if (builtin) - pr_info("Using FineIBT CFI\n"); + if (builtin) { + pr_info("Using FineIBT %s CFI\n", + cfi_paranoid ? "paranoid" : ""); + } return; default: @@ -1420,7 +1487,8 @@ static void poison_cfi(void *addr) * We check the preamble by checking for the ENDBR instruction relative to the * UD2 instruction. */ -bool decode_fineibt_insn(struct pt_regs *regs, unsigned long *target, u32 *type) +static bool decode_fineibt_preamble(int ud_type, struct pt_regs *regs, + unsigned long *target, u32 *type) { unsigned long addr = regs->ip - fineibt_preamble_ud2; u32 endbr, hash; @@ -1440,6 +1508,33 @@ bool decode_fineibt_insn(struct pt_regs *regs, unsigned long *target, u32 *type) return false; } +/* + * regs->ip points to a 0xea instruction from the fineibt_paranoid_start[] + * sequence. + */ +static bool decode_fineibt_paranoid(int ud_type, struct pt_regs *regs, + unsigned long *target, u32 *type) +{ + unsigned long addr = regs->ip - fineibt_paranoid_ud; + u32 hash; + + __get_kernel_nofault(&hash, addr + fineibt_caller_hash, u32, Efault); + *target = regs->r11 + 16; + *type = regs->r10; + return true; + +Efault: + return false; +} + +bool decode_fineibt_insn(int ud_type, struct pt_regs *regs, + unsigned long *target, u32 *type) +{ + if (ud_type == BUG_EA) + return decode_fineibt_paranoid(ud_type, regs, target, type); + return decode_fineibt_preamble(ud_type, regs, target, type); +} + #else static void __apply_fineibt(s32 *start_retpoline, s32 *end_retpoline, diff --git a/arch/x86/kernel/cfi.c b/arch/x86/kernel/cfi.c index f6905bef0af8..f9eb7465eec6 100644 --- a/arch/x86/kernel/cfi.c +++ b/arch/x86/kernel/cfi.c @@ -65,7 +65,7 @@ static bool decode_cfi_insn(struct pt_regs *regs, unsigned long *target, * Checks if a ud2 trap is because of a CFI failure, and handles the trap * if needed. Returns a bug_trap_type value similarly to report_bug. */ -enum bug_trap_type handle_cfi_failure(struct pt_regs *regs) +enum bug_trap_type handle_cfi_failure(int ud_type, struct pt_regs *regs) { unsigned long target; u32 type; @@ -81,7 +81,7 @@ enum bug_trap_type handle_cfi_failure(struct pt_regs *regs) break; case CFI_FINEIBT: - if (!decode_fineibt_insn(regs, &target, &type)) + if (!decode_fineibt_insn(ud_type, regs, &target, &type)) return BUG_TRAP_TYPE_NONE; break; diff --git a/arch/x86/kernel/traps.c b/arch/x86/kernel/traps.c index 05b86c05e446..500030ab8036 100644 --- a/arch/x86/kernel/traps.c +++ b/arch/x86/kernel/traps.c @@ -113,6 +113,10 @@ __always_inline int decode_bug(unsigned long addr, s32 *imm, int *len) v = *(u8 *)(addr++); if (v == INSN_ASOP) v = *(u8 *)(addr++); + if (v == 0xea) { + *len = addr - start; + return BUG_EA; + } if (v != OPCODE_ESCAPE) return BUG_NONE; @@ -308,9 +312,16 @@ static noinstr bool handle_bug(struct pt_regs *regs) raw_local_irq_enable(); switch (ud_type) { + case BUG_EA: + if (handle_cfi_failure(ud_type, regs) == BUG_TRAP_TYPE_WARN) { + regs->ip += ud_len; + handled = true; + } + break; + case BUG_UD2: if (report_bug(regs->ip, regs) == BUG_TRAP_TYPE_WARN || - handle_cfi_failure(regs) == BUG_TRAP_TYPE_WARN) { + handle_cfi_failure(ud_type, regs) == BUG_TRAP_TYPE_WARN) { regs->ip += ud_len; handled = true; }