Skip to content

Commit ad9b6ee

Browse files
committed
aarch64: Add AArch64 Kernel Control Flow Integrity implementation
Implement AArch64-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 4-byte instruction alignment). - Trap debugging through ESR (Exception Syndrome Register) encoding in BRK instruction immediate values for precise failure analysis. - Scratch register allocation using w16/w17 (x16/x17) following AArch64 procedure call standard for intra-procedure-call registers. - Support for both regular calls (BLR) and sibling calls (BR) with appropriate register usage and jump instructions. - Atomic bundled KCFI check + call/branch sequences using UNSPECV_KCFI_CHECK to prevent optimizer separation and maintain security properties. Assembly Code Pattern for AArch64: ldur w16, [target, #-4] ; Load actual type ID from preamble mov w17, #type_id_low ; Load expected type (lower 16 bits) movk w17, #type_id_high, lsl gcc-mirror#16 ; Load upper 16 bits if needed cmp w16, w17 ; Compare type IDs directly b.eq .Lpass ; Branch if types match .Ltrap: brk #esr_value ; Enhanced trap with register info .Lpass: blr/br target ; Execute validated indirect transfer ESR (Exception Syndrome Register) Integration: - BRK instruction immediate encoding format: 0x8000 | ((TypeIndex & 31) << 5) | (AddrIndex & 31) - TypeIndex indicates which W register contains expected type (W17 = 17) - AddrIndex indicates which X register contains target address (0-30) - Example: brk #33313 (0x8221) = expected type in W17, target address in X1 Like x86, the callback initialization in aarch64_override_options() seem hacky. Is there a better place for this? Build and run tested with Linux kernel ARCH=arm64. Signed-off-by: Kees Cook <[email protected]>
1 parent bf9c9d2 commit ad9b6ee

File tree

4 files changed

+234
-8
lines changed

4 files changed

+234
-8
lines changed

gcc/config/aarch64/aarch64-protos.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,7 @@ tree aarch64_resolve_overloaded_builtin_general (location_t, tree, void *);
12611261

12621262
const char *aarch64_sls_barrier (int);
12631263
const char *aarch64_indirect_call_asm (rtx);
1264+
const char *aarch64_indirect_branch_asm (rtx);
12641265
extern bool aarch64_harden_sls_retbr_p (void);
12651266
extern bool aarch64_harden_sls_blr_p (void);
12661267

@@ -1284,4 +1285,9 @@ extern unsigned aarch64_stack_alignment (const_tree exp, unsigned align);
12841285
extern rtx aarch64_gen_compare_zero_and_branch (rtx_code code, rtx x,
12851286
rtx_code_label *label);
12861287

1288+
/* KCFI support. */
1289+
extern void aarch64_kcfi_init (void);
1290+
extern void kcfi_emit_trap_with_section (FILE *file, rtx trap_label_rtx);
1291+
extern const char *aarch64_output_kcfi_insn (rtx *, bool);
1292+
12871293
#endif /* GCC_AARCH64_PROTOS_H */

gcc/config/aarch64/aarch64.cc

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
#include "rtlanal.h"
8484
#include "tree-dfa.h"
8585
#include "asan.h"
86+
#include "kcfi.h"
8687
#include "aarch64-elf-metadata.h"
8788
#include "aarch64-feature-deps.h"
8889
#include "config/arm/aarch-common.h"
@@ -25590,6 +25591,9 @@ aarch64_declare_function_name (FILE *stream, const char* name,
2559025591

2559125592
aarch64_asm_output_variant_pcs (stream, fndecl, name);
2559225593

25594+
/* Emit KCFI preamble for non-patchable functions. */
25595+
kcfi_emit_preamble_if_needed (stream, fndecl, false, 0, name);
25596+
2559325597
/* Don't forget the type directive for ELF. */
2559425598
ASM_OUTPUT_TYPE_DIRECTIVE (stream, name, "function");
2559525599
ASM_OUTPUT_FUNCTION_LABEL (stream, name, fndecl);
@@ -30630,6 +30634,14 @@ aarch64_indirect_call_asm (rtx addr)
3063030634
return "";
3063130635
}
3063230636

30637+
const char *
30638+
aarch64_indirect_branch_asm (rtx addr)
30639+
{
30640+
gcc_assert (REG_P (addr));
30641+
output_asm_insn ("br\t%0", &addr);
30642+
return aarch64_sls_barrier (aarch64_harden_sls_retbr_p ());
30643+
}
30644+
3063330645
/* Emit the assembly instruction to load the thread pointer into DEST.
3063430646
Select between different tpidr_elN registers depending on -mtp= setting. */
3063530647

@@ -32823,6 +32835,172 @@ aarch64_libgcc_floating_mode_supported_p
3282332835
#undef TARGET_DOCUMENTATION_NAME
3282432836
#define TARGET_DOCUMENTATION_NAME "AArch64"
3282532837

32838+
32839+
/* AArch64 doesn't need prefix NOPs (instructions are already 4-byte aligned) */
32840+
static int
32841+
aarch64_kcfi_calculate_prefix_nops (HOST_WIDE_INT prefix_nops ATTRIBUTE_UNUSED)
32842+
{
32843+
/* AArch64 instructions are 4-byte aligned, no prefix NOPs needed for KCFI preamble. */
32844+
return 0;
32845+
}
32846+
32847+
/* Emit AArch64-specific type ID instruction. */
32848+
static void
32849+
aarch64_kcfi_emit_type_id_instruction (FILE *file, uint32_t type_id)
32850+
{
32851+
/* Emit type ID as a 32-bit word. */
32852+
fprintf (file, "\t.word 0x%08x\n", type_id);
32853+
}
32854+
32855+
/* Helper function for AArch64 KCFI check sequence generation. */
32856+
const char *
32857+
aarch64_output_kcfi_insn (rtx *operands, bool is_sibcall)
32858+
{
32859+
uint32_t type_id = INTVAL (operands[1]);
32860+
HOST_WIDE_INT prefix_nops = INTVAL (operands[2]);
32861+
HOST_WIDE_INT offset = -(4 + prefix_nops);
32862+
32863+
/* Generate labels internally. */
32864+
rtx trap_label = gen_label_rtx ();
32865+
rtx call_label = gen_label_rtx ();
32866+
32867+
/* Get label numbers for custom naming. */
32868+
int trap_labelno = CODE_LABEL_NUMBER (trap_label);
32869+
int call_labelno = CODE_LABEL_NUMBER (call_label);
32870+
32871+
/* Generate custom label names. */
32872+
char trap_name[32];
32873+
char call_name[32];
32874+
ASM_GENERATE_INTERNAL_LABEL (trap_name, "Lkcfi_trap", trap_labelno);
32875+
ASM_GENERATE_INTERNAL_LABEL (call_name, "Lkcfi_call", call_labelno);
32876+
32877+
/* AArch64 KCFI check sequence:
32878+
1. Load actual type from function preamble
32879+
2. Load expected type
32880+
3. Compare and branch if equal
32881+
4. Trap if mismatch
32882+
5. Call/branch to target. */
32883+
32884+
static char ldur_buffer[64];
32885+
sprintf (ldur_buffer, "ldur\tw16, [%%0, #%ld]", offset);
32886+
output_asm_insn (ldur_buffer, operands);
32887+
32888+
/* Load expected type - always use mov + movk since type IDs are hashes. */
32889+
static char mov_buffer[64], movk_buffer[64];
32890+
sprintf (mov_buffer, "mov\tw17, #%u", type_id & 0xffff);
32891+
output_asm_insn (mov_buffer, operands);
32892+
sprintf (movk_buffer, "movk\tw17, #%u, lsl #16", (type_id >> 16) & 0xffff);
32893+
output_asm_insn (movk_buffer, operands);
32894+
32895+
output_asm_insn ("cmp\tw16, w17", operands);
32896+
32897+
/* Output conditional branch to call label. */
32898+
fputs ("\tb.eq\t", asm_out_file);
32899+
assemble_name (asm_out_file, call_name);
32900+
fputc ('\n', asm_out_file);
32901+
32902+
/* Output trap label and BRK instruction. */
32903+
ASM_OUTPUT_LABEL (asm_out_file, trap_name);
32904+
32905+
/* Calculate and emit BRK with ESR encoding. */
32906+
unsigned type_index = 17; /* w17 contains expected type. */
32907+
unsigned addr_index = REGNO (operands[0]) - R0_REGNUM;
32908+
unsigned esr_value = 0x8000 | ((type_index & 31) << 5) | (addr_index & 31);
32909+
32910+
static char brk_buffer[32];
32911+
sprintf (brk_buffer, "brk\t#%u", esr_value);
32912+
output_asm_insn (brk_buffer, operands);
32913+
32914+
/* Output call label. */
32915+
ASM_OUTPUT_LABEL (asm_out_file, call_name);
32916+
32917+
/* Return appropriate call instruction based on is_sibcall. */
32918+
if (is_sibcall)
32919+
return aarch64_indirect_branch_asm (operands[0]);
32920+
else
32921+
return aarch64_indirect_call_asm (operands[0]);
32922+
}
32923+
32924+
/* Generate AArch64 KCFI checked call bundle. */
32925+
static rtx
32926+
aarch64_kcfi_gen_checked_call (rtx call_insn, rtx target_reg, uint32_t expected_type,
32927+
HOST_WIDE_INT prefix_nops)
32928+
{
32929+
/* For AArch64, we create an RTL bundle that combines the KCFI check
32930+
with the call instruction in an atomic sequence. */
32931+
32932+
if (!REG_P (target_reg))
32933+
{
32934+
/* If not a register, load it into x16. */
32935+
rtx temp = gen_rtx_REG (Pmode, 16);
32936+
emit_move_insn (temp, target_reg);
32937+
target_reg = temp;
32938+
}
32939+
32940+
/* Generate the bundled KCFI check + call pattern. */
32941+
rtx pattern;
32942+
if (CALL_P (call_insn))
32943+
{
32944+
rtx call_pattern = PATTERN (call_insn);
32945+
32946+
/* Check if it's a sibling call. */
32947+
if (find_reg_note (call_insn, REG_NORETURN, NULL_RTX)
32948+
|| (GET_CODE (call_pattern) == PARALLEL
32949+
&& GET_CODE (XVECEXP (call_pattern, 0, XVECLEN (call_pattern, 0) - 1)) == RETURN))
32950+
{
32951+
/* Generate sibling call bundle. */
32952+
pattern = gen_aarch64_kcfi_checked_sibcall (target_reg,
32953+
gen_int_mode (expected_type, SImode),
32954+
gen_int_mode (prefix_nops, SImode));
32955+
}
32956+
else
32957+
{
32958+
/* Generate regular call bundle. */
32959+
pattern = gen_aarch64_kcfi_checked_call (target_reg,
32960+
gen_int_mode (expected_type, SImode),
32961+
gen_int_mode (prefix_nops, SImode));
32962+
}
32963+
}
32964+
else
32965+
internal_error ("KCFI: Expected call instruction");
32966+
32967+
return pattern;
32968+
}
32969+
32970+
/* Add AArch64-specific register clobbers for KCFI calls. */
32971+
static void
32972+
aarch64_kcfi_add_clobbers (rtx_insn *call_insn)
32973+
{
32974+
/* AArch64 KCFI uses w16 and w17 (x16 and x17) as scratch registers. */
32975+
rtx usage = CALL_INSN_FUNCTION_USAGE (call_insn);
32976+
32977+
/* Add w16 (x16) clobber. */
32978+
clobber_reg (&usage, gen_rtx_REG (SImode, 16));
32979+
32980+
/* Add w17 (x17) clobber. */
32981+
clobber_reg (&usage, gen_rtx_REG (SImode, 17));
32982+
32983+
CALL_INSN_FUNCTION_USAGE (call_insn) = usage;
32984+
32985+
/* Store the KCFI length in the instruction's REG_NOTES for retrieval
32986+
by the length calculation function. */
32987+
bool is_sibcall = CALL_P (call_insn) && SIBLING_CALL_P (call_insn);
32988+
int kcfi_length = is_sibcall ? 28 : 32;
32989+
add_reg_note (call_insn, REG_CALL_KCFI_LENGTH, GEN_INT (kcfi_length));
32990+
}
32991+
32992+
#undef TARGET_KCFI_GEN_KCFI_CHECKED_CALL
32993+
#define TARGET_KCFI_GEN_KCFI_CHECKED_CALL aarch64_kcfi_gen_checked_call
32994+
32995+
#undef TARGET_KCFI_ADD_KCFI_CLOBBERS
32996+
#define TARGET_KCFI_ADD_KCFI_CLOBBERS aarch64_kcfi_add_clobbers
32997+
32998+
#undef TARGET_KCFI_CALCULATE_PREFIX_NOPS
32999+
#define TARGET_KCFI_CALCULATE_PREFIX_NOPS aarch64_kcfi_calculate_prefix_nops
33000+
33001+
#undef TARGET_KCFI_EMIT_TYPE_ID_INSTRUCTION
33002+
#define TARGET_KCFI_EMIT_TYPE_ID_INSTRUCTION aarch64_kcfi_emit_type_id_instruction
33003+
3282633004
struct gcc_target targetm = TARGET_INITIALIZER;
3282733005

3282833006
#include "gt-aarch64.h"

gcc/config/aarch64/aarch64.md

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@
417417
UNSPECV_TCANCEL ; Represent transaction cancel.
418418
UNSPEC_RNDR ; Represent RNDR
419419
UNSPEC_RNDRRS ; Represent RNDRRS
420+
UNSPECV_KCFI_CHECK ; Represent KCFI check bundled with call
420421
]
421422
)
422423

@@ -1326,6 +1327,39 @@
13261327
"brk #1000"
13271328
[(set_attr "type" "trap")])
13281329

1330+
;; KCFI bundled check and call patterns
1331+
;; These combine the KCFI check with the call in an atomic sequence
1332+
1333+
(define_insn "aarch64_kcfi_checked_call"
1334+
[(parallel [(call (mem:DI (match_operand:DI 0 "register_operand" "r"))
1335+
(const_int 0))
1336+
(unspec:DI [(const_int 0)] UNSPEC_CALLEE_ABI)
1337+
(unspec_volatile:DI [(match_operand:SI 1 "const_int_operand" "n") ; type_id
1338+
(match_operand:SI 2 "const_int_operand" "n")] ; prefix_nops
1339+
UNSPECV_KCFI_CHECK)
1340+
(clobber (reg:DI LR_REGNUM))
1341+
(clobber (reg:SI 16)) ; w16 - scratch for loaded type
1342+
(clobber (reg:SI 17))])] ; w17 - scratch for expected type
1343+
"flag_sanitize & SANITIZE_KCFI"
1344+
{ return aarch64_output_kcfi_insn (operands, false); }
1345+
[(set_attr "type" "call")
1346+
(set_attr "length" "24")])
1347+
1348+
(define_insn "aarch64_kcfi_checked_sibcall"
1349+
[(parallel [(call (mem:DI (match_operand:DI 0 "register_operand" "r"))
1350+
(const_int 0))
1351+
(unspec:DI [(const_int 0)] UNSPEC_CALLEE_ABI)
1352+
(unspec_volatile:DI [(match_operand:SI 1 "const_int_operand" "n") ; type_id
1353+
(match_operand:SI 2 "const_int_operand" "n")] ; prefix_nops
1354+
UNSPECV_KCFI_CHECK)
1355+
(return)
1356+
(clobber (reg:SI 16)) ; w16 - scratch for loaded type
1357+
(clobber (reg:SI 17))])] ; w17 - scratch for expected type
1358+
"flag_sanitize & SANITIZE_KCFI"
1359+
{ return aarch64_output_kcfi_insn (operands, true); }
1360+
[(set_attr "type" "branch")
1361+
(set_attr "length" "24")])
1362+
13291363
(define_expand "prologue"
13301364
[(clobber (const_int 0))]
13311365
""
@@ -1558,10 +1592,7 @@
15581592
"SIBLING_CALL_P (insn)"
15591593
{
15601594
if (which_alternative == 0)
1561-
{
1562-
output_asm_insn ("br\\t%0", operands);
1563-
return aarch64_sls_barrier (aarch64_harden_sls_retbr_p ());
1564-
}
1595+
return aarch64_indirect_branch_asm (operands[0]);
15651596
return "b\\t%c0";
15661597
}
15671598
[(set_attr "type" "branch, branch")
@@ -1578,10 +1609,7 @@
15781609
"SIBLING_CALL_P (insn)"
15791610
{
15801611
if (which_alternative == 0)
1581-
{
1582-
output_asm_insn ("br\\t%1", operands);
1583-
return aarch64_sls_barrier (aarch64_harden_sls_retbr_p ());
1584-
}
1612+
return aarch64_indirect_branch_asm (operands[1]);
15851613
return "b\\t%c1";
15861614
}
15871615
[(set_attr "type" "branch, branch")

gcc/doc/invoke.texi

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18424,6 +18424,20 @@ sequences for both the KCFI preamble and the check-call bundle are
1842418424
considered ABI, as the Linux kernel may optionally rewrite these areas
1842518425
at boot time to mitigate detected CPU errata.
1842618426

18427+
On AArch64, KCFI type identifiers are emitted as a @code{.word ID}
18428+
directive (a 32-bit constant) before the function entry. AArch64's
18429+
natural 4-byte instruction alignment eliminates the need for additional
18430+
padding NOPs. When used with @option{-fpatchable-function-entry}, the
18431+
type identifier is placed before any patchable NOPs. The runtime check
18432+
uses @code{x16} and @code{x17} as scratch registers. Type mismatches
18433+
trigger a @code{brk} instruction with an immediate value that encodes
18434+
both the expected type register index and the target address register
18435+
index in the format @code{0x8000 | (type_reg << 5) | addr_reg}. This
18436+
encoding is captured in the ESR (Exception Syndrome Register) when the
18437+
trap is taken, allowing the kernel to identify both the KCFI violation
18438+
and the involved registers for detailed diagnostics (eliminating the need
18439+
for a separate @code{.kcfi_traps} section as used on x86_64).
18440+
1842718441
KCFI is intended primarily for kernel code and may not be suitable
1842818442
for user-space applications that rely on techniques incompatible
1842918443
with strict type checking of indirect calls.

0 commit comments

Comments
 (0)