Implement RISC-V-specific KCFI backend. - Function preamble generation using .word directives for type ID storage at offset from function entry point (no prefix NOPs needed due to natural 4-byte instruction alignment).
- Scratch register allocation using t1/t2 (x6/x7) following RISC-V procedure call standard for temporary registers. - Support for both regular calls (JALR) and sibling calls (JR) with appropriate register usage and jump instructions. - Integration with .kcfi_traps section for debugger/runtime metadata (like x86_64). - Atomic bundled KCFI check + call/jump sequences using UNSPECV_KCFI_CHECK to prevent optimizer separation and maintain security properties. Assembly Code Pattern for RISC-V: lw t1, -4(target_reg) ; Load actual type ID from preamble lui t2, %hi(expected_type) ; Load expected type (upper 20 bits) addiw t2, t2, %lo(expected_type) ; Add lower 12 bits (sign-extended) beq t1, t2, .Lpass ; Branch if types match .Ltrap: ebreak ; Environment break trap on mismatch .Lpass: jalr/jr target_reg ; Execute validated indirect transfer Build tested with Linux kernel ARCH=riscv (I am still building a proper risc-v emulation setup). Run tested via userspace binaries. Signed-off-by: Kees Cook <k...@kernel.org> --- gcc/config/riscv/riscv-protos.h | 1 + gcc/config/riscv/riscv.cc | 157 ++++++++++++++++++++++++++++++++ gcc/config/riscv/riscv.md | 49 ++++++++++ gcc/doc/invoke.texi | 13 +++ 4 files changed, 220 insertions(+) diff --git a/gcc/config/riscv/riscv-protos.h b/gcc/config/riscv/riscv-protos.h index 539321ff95b8..1d343c529934 100644 --- a/gcc/config/riscv/riscv-protos.h +++ b/gcc/config/riscv/riscv-protos.h @@ -126,6 +126,7 @@ extern bool riscv_split_64bit_move_p (rtx, rtx); extern void riscv_split_doubleword_move (rtx, rtx); extern const char *riscv_output_move (rtx, rtx); extern const char *riscv_output_return (); +extern const char *riscv_output_kcfi_checked_call (uint32_t, HOST_WIDE_INT, bool); extern void riscv_declare_function_name (FILE *, const char *, tree); extern void riscv_declare_function_size (FILE *, const char *, tree); extern void riscv_asm_output_alias (FILE *, const tree, const tree); diff --git a/gcc/config/riscv/riscv.cc b/gcc/config/riscv/riscv.cc index 0a9fcef37029..5daa5427568d 100644 --- a/gcc/config/riscv/riscv.cc +++ b/gcc/config/riscv/riscv.cc @@ -81,6 +81,7 @@ along with GCC; see the file COPYING3. If not see #include "cgraph.h" #include "langhooks.h" #include "gimplify.h" +#include "kcfi.h" /* This file should be included last. */ #include "target-def.h" @@ -11156,6 +11157,9 @@ riscv_declare_function_name (FILE *stream, const char *name, tree fndecl) fprintf (stream, "\t# tune = %s\n", local_tune_str); } } + + /* Emit KCFI preamble for non-patchable functions. */ + kcfi_emit_preamble_if_needed (stream, fndecl, false, 0, name); } void @@ -11418,6 +11422,147 @@ riscv_convert_vector_chunks (struct gcc_options *opts) return 1; } +/* KCFI (Kernel Control Flow Integrity) support. */ + +/* Generate KCFI checked call RTL pattern following AArch64 approach. */ +static rtx +riscv_kcfi_gen_checked_call (rtx call_insn, rtx target_reg, uint32_t expected_type, + HOST_WIDE_INT prefix_nops) +{ + /* For RISC-V, we create an RTL bundle that combines the KCFI check + with the call instruction in an atomic sequence. */ + + if (!REG_P (target_reg)) + { + /* If not a register, load it into t1. */ + rtx temp = gen_rtx_REG (Pmode, T1_REGNUM); + emit_move_insn (temp, target_reg); + target_reg = temp; + } + + /* Generate the bundled KCFI check + call pattern. */ + rtx pattern; + if (CALL_P (call_insn)) + { + rtx call_pattern = PATTERN (call_insn); + + /* Create labels used by both call and sibcall patterns. */ + rtx pass_label = gen_label_rtx (); + rtx trap_label = gen_label_rtx (); + + /* Check if it's a sibling call. */ + if (find_reg_note (call_insn, REG_NORETURN, NULL_RTX) + || (GET_CODE (call_pattern) == PARALLEL + && GET_CODE (XVECEXP (call_pattern, 0, XVECLEN (call_pattern, 0) - 1)) == RETURN)) + { + /* Generate sibling call bundle. */ + pattern = gen_riscv_kcfi_checked_sibcall (target_reg, + gen_int_mode (expected_type, SImode), + gen_int_mode (prefix_nops, SImode), + pass_label, + trap_label); + } + else + { + /* Generate regular call bundle. */ + pattern = gen_riscv_kcfi_checked_call (target_reg, + gen_int_mode (expected_type, SImode), + gen_int_mode (prefix_nops, SImode), + pass_label, + trap_label); + } + } + else + { + error ("KCFI: Expected call instruction"); + gcc_unreachable (); + } + + return pattern; +} + +/* Add RISC-V specific register clobbers for KCFI instrumentation. */ +static void +riscv_kcfi_add_clobbers (rtx_insn *call_insn) +{ + /* Add t1/t2 clobbers so register allocator knows they'll be used. */ + rtx usage = CALL_INSN_FUNCTION_USAGE (call_insn); + clobber_reg (&usage, gen_rtx_REG (DImode, T1_REGNUM)); + clobber_reg (&usage, gen_rtx_REG (DImode, T2_REGNUM)); + CALL_INSN_FUNCTION_USAGE (call_insn) = usage; +} + +/* Calculate prefix NOPs (RISC-V doesn't need additional NOPs). */ +static int +riscv_kcfi_calculate_prefix_nops (HOST_WIDE_INT prefix_nops ATTRIBUTE_UNUSED) +{ + /* RISC-V instructions are 4-byte aligned, no additional NOPs needed. */ + return 0; +} + +/* Emit RISC-V type ID instruction. */ +static void +riscv_kcfi_emit_type_id_instruction (FILE *file, uint32_t type_id) +{ + /* Emit .word directive with type ID. */ + fprintf (file, "\t.word\t0x%08x\n", type_id); +} + +/* Output KCFI checked call instruction sequence. */ +const char * +riscv_output_kcfi_checked_call (uint32_t expected_type, HOST_WIDE_INT prefix_nops, bool sibling_call) +{ + static char buf[512]; + + /* Calculate offset for type ID load, accounting for prefix NOPs. */ + HOST_WIDE_INT offset = -(4 + prefix_nops); + + /* Generate unique labels. */ + static int label_counter = 0; + int pass_label_num = ++label_counter; + int trap_label_num = ++label_counter; + + /* Generate the KCFI check sequence: + lw t1, -4(target_reg) # Load actual type from function[-4] + lui t2, %hi(expected_type_id) # Load upper 20 bits of expected type + addiw t2, t2, %lo(expected_type_id) # Add lower 12 bits (sign-extended) + beq t1, t2, .Lpass # Branch if types match + .Ltrap: + ebreak # Environment break (trap on mismatch) + .Lpass: + jalr target_reg # Execute indirect function call + */ + + /* Manually split expected_type as required by agentic/kcfi-riscv.md: + - Upper 20 bits for lui instruction + - Lower 12 bits for addiw instruction (sign-extended) */ + uint32_t hi20 = (expected_type >> 12) & 0xFFFFF; /* Upper 20 bits */ + int32_t lo12 = ((int32_t)(expected_type << 20)) >> 20; /* Lower 12 bits, sign-extended */ + + snprintf (buf, sizeof (buf), + "lw\tt1, %ld(%%0)\n" + "\tlui\tt2, %u\n" + "\taddiw\tt2, t2, %d\n" + "\tbeq\tt1, t2, .Lkcfi_pass_%d\n" + ".Lkcfi_trap_%d:\n" + "\tebreak\n" + "\t.pushsection\t.kcfi_traps,\"ao\",@progbits,.text\n" + ".Lkcfi_trap_entry_%d:\n" + "\t.word\t.Lkcfi_trap_%d - .Lkcfi_trap_entry_%d\n" + "\t.popsection\n" + ".Lkcfi_pass_%d:\n" + "\t%s\t%%0", + offset, hi20, lo12, + pass_label_num, + trap_label_num, + trap_label_num, + trap_label_num, trap_label_num, + pass_label_num, + sibling_call ? "jr" : "jalr"); + + return buf; +} + /* 'Unpack' up the internal tuning structs and update the options in OPTS. The caller must have set up selected_tune and selected_arch as all the other target-specific codegen decisions are @@ -11525,6 +11670,7 @@ riscv_override_options_internal (struct gcc_options *opts) opts->x_flag_cf_protection = (cf_protection_level) (opts->x_flag_cf_protection | CF_SET); } + } /* Implement TARGET_OPTION_OVERRIDE. */ @@ -11715,6 +11861,16 @@ riscv_option_override (void) riscv_override_options_internal (&global_options); + /* Initialize KCFI hooks if KCFI is enabled. */ + if (flag_sanitize & SANITIZE_KCFI) + { + kcfi_target.gen_kcfi_checked_call = riscv_kcfi_gen_checked_call; + kcfi_target.add_kcfi_clobbers = riscv_kcfi_add_clobbers; + kcfi_target.calculate_prefix_nops = riscv_kcfi_calculate_prefix_nops; + kcfi_target.emit_type_id_instruction = riscv_kcfi_emit_type_id_instruction; + /* Note: mask_type_id is NULL - no masking needed for RISC-V. */ + } + /* Save these options as the default ones in case we push and pop them later while processing functions with potential target attributes. */ target_option_default_node = target_option_current_node @@ -15795,6 +15951,7 @@ synthesize_and (rtx operands[3]) #define TARGET_VECTORIZE_BUILTIN_VECTORIZATION_COST \ riscv_builtin_vectorization_cost + #undef TARGET_VECTORIZE_CREATE_COSTS #define TARGET_VECTORIZE_CREATE_COSTS riscv_vectorize_create_costs diff --git a/gcc/config/riscv/riscv.md b/gcc/config/riscv/riscv.md index 578dd43441e2..6e9545e9d003 100644 --- a/gcc/config/riscv/riscv.md +++ b/gcc/config/riscv/riscv.md @@ -152,6 +152,9 @@ ;; XTheadInt unspec UNSPECV_XTHEADINT_PUSH UNSPECV_XTHEADINT_POP + + ;; KCFI unspec + UNSPECV_KCFI_CHECK ]) (define_constants @@ -4078,6 +4081,52 @@ DONE; }) +;; KCFI checked call patterns + +(define_insn "riscv_kcfi_checked_call" + [(parallel [(call (mem:DI (match_operand:DI 0 "register_operand" "r")) + (const_int 0)) + (unspec:DI [(const_int 0)] UNSPEC_CALLEE_CC) + (unspec_volatile:DI [(match_operand:SI 1 "const_int_operand" "n") ; type_id + (match_operand:SI 2 "const_int_operand" "n") ; prefix_nops + (label_ref (match_operand 3)) ; pass label + (label_ref (match_operand 4))] ; trap label + UNSPECV_KCFI_CHECK) + (clobber (reg:DI RETURN_ADDR_REGNUM)) + (clobber (reg:DI T1_REGNUM)) ; t1 - scratch for loaded type + (clobber (reg:DI T2_REGNUM))])] ; t2 - scratch for expected type + "flag_sanitize & SANITIZE_KCFI" + "* + { + uint32_t type_id = INTVAL (operands[1]); + HOST_WIDE_INT prefix_nops = INTVAL (operands[2]); + return riscv_output_kcfi_checked_call (type_id, prefix_nops, false); + }" + [(set_attr "type" "call") + (set_attr "length" "24")]) + +(define_insn "riscv_kcfi_checked_sibcall" + [(parallel [(call (mem:DI (match_operand:DI 0 "register_operand" "r")) + (const_int 0)) + (unspec:DI [(const_int 0)] UNSPEC_CALLEE_CC) + (unspec_volatile:DI [(match_operand:SI 1 "const_int_operand" "n") ; type_id + (match_operand:SI 2 "const_int_operand" "n") ; prefix_nops + (label_ref (match_operand 3)) ; pass label + (label_ref (match_operand 4))] ; trap label + UNSPECV_KCFI_CHECK) + (return) + (clobber (reg:DI T1_REGNUM)) ; t1 - scratch for loaded type + (clobber (reg:DI T2_REGNUM))])] ; t2 - scratch for expected type + "flag_sanitize & SANITIZE_KCFI" + "* + { + uint32_t type_id = INTVAL (operands[1]); + HOST_WIDE_INT prefix_nops = INTVAL (operands[2]); + return riscv_output_kcfi_checked_call (type_id, prefix_nops, true); + }" + [(set_attr "type" "call") + (set_attr "length" "24")]) + (define_insn "nop" [(const_int 0)] "" diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi index 161c7024f842..f82d0464590d 100644 --- a/gcc/doc/invoke.texi +++ b/gcc/doc/invoke.texi @@ -18350,6 +18350,19 @@ trap is taken, allowing the kernel to identify both the KCFI violation and the involved registers for detailed diagnostics (eliminating the need for a separate @code{.kcfi_traps} section as used on x86_64). +On RISC-V, KCFI type identifiers are emitted as a @code{.word ID} +directive (a 32-bit constant) before the function entry, similar to AArch64. +RISC-V's natural 4-byte instruction alignment eliminates the need for +additional padding NOPs. When used with @option{-fpatchable-function-entry}, +the type identifier is placed before any patchable NOPs. The runtime check +loads the actual type using @code{lw t1, OFFSET(target_reg)}, where the +offset accounts for any prefix NOPs, constructs the expected type using +@code{lui} and @code{addiw} instructions into @code{t2}, and compares them +with @code{beq}. Type mismatches trigger an @code{ebreak} instruction. +Like x86_64, RISC-V uses a @code{.kcfi_traps} section to map trap locations +to their corresponding function entry points for debugging (RISC-V lacks +ESR-style trap encoding unlike AArch64). + KCFI is intended primarily for kernel code and may not be suitable for user-space applications that rely on techniques incompatible with strict type checking of indirect calls. -- 2.34.1