lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <CALvbMcDm5i2-+Q-XBG16z1s5P67sJVS=bvpozUqqobAvO+xW4g@mail.gmail.com>
Date: Sat, 13 Sep 2025 16:58:26 -0700
From: Andrew Pinski <andrew.pinski@....qualcomm.com>
To: Kees Cook <kees@...nel.org>
Cc: Qing Zhao <qing.zhao@...cle.com>, Andrew Pinski <pinskia@...il.com>,
        Jakub Jelinek <jakub@...hat.com>, Martin Uecker <uecker@...raz.at>,
        Richard Biener <rguenther@...e.de>, Joseph Myers <josmyers@...hat.com>,
        Peter Zijlstra <peterz@...radead.org>, Jan Hubicka <hubicka@....cz>,
        Richard Earnshaw <richard.earnshaw@....com>,
        Richard Sandiford <richard.sandiford@....com>,
        Marcus Shawcroft <marcus.shawcroft@....com>,
        Kyrylo Tkachov <kyrylo.tkachov@....com>,
        Kito Cheng <kito.cheng@...il.com>, Palmer Dabbelt <palmer@...belt.com>,
        Andrew Waterman <andrew@...ive.com>,
        Jim Wilson <jim.wilson.gcc@...il.com>,
        Dan Li <ashimida.1990@...il.com>,
        Sami Tolvanen <samitolvanen@...gle.com>,
        Ramon de C Valle <rcvalle@...gle.com>,
        Joao Moreira <joao@...rdrivepizza.com>,
        Nathan Chancellor <nathan@...nel.org>,
        Bill Wendling <morbo@...gle.com>, gcc-patches@....gnu.org,
        linux-hardening@...r.kernel.org
Subject: Re: [PATCH v3 7/7] kcfi: Add regression test suite

On Sat, Sep 13, 2025 at 4:36 PM Kees Cook <kees@...nel.org> wrote:
>
> Adds a test suite for KCFI (Kernel Control Flow Integrity) ABI, covering
> core functionality, optimization and code generation, addressing,
> architecture-specific KCFI sequence emission, and integration with
> patchable function entry.
>
> Tests can be run via:
>   make check-c RUNTESTFLAGS='kcfi.exp'
>
> gcc/testsuite/ChangeLog:
>
>         * gcc.dg/kcfi/kcfi-adjacency.c: New test.
>         * gcc.dg/kcfi/kcfi-basics.c: New test.
>         * gcc.dg/kcfi/kcfi-call-sharing.c: New test.
>         * gcc.dg/kcfi/kcfi-cold-partition.c: New test.
>         * gcc.dg/kcfi/kcfi-complex-addressing.c: New test.
>         * gcc.dg/kcfi/kcfi-ipa-robustness.c: New test.
>         * gcc.dg/kcfi/kcfi-move-preservation.c: New test.
>         * gcc.dg/kcfi/kcfi-no-sanitize-inline.c: New test.
>         * gcc.dg/kcfi/kcfi-no-sanitize.c: New test.
>         * gcc.dg/kcfi/kcfi-offset-validation.c: New test.
>         * gcc.dg/kcfi/kcfi-patchable-basic.c: New test.
>         * gcc.dg/kcfi/kcfi-patchable-entry-only.c: New test.
>         * gcc.dg/kcfi/kcfi-patchable-large.c: New test.
>         * gcc.dg/kcfi/kcfi-patchable-medium.c: New test.
>         * gcc.dg/kcfi/kcfi-patchable-prefix-only.c: New test.
>         * gcc.dg/kcfi/kcfi-pic-addressing.c: New test.
>         * gcc.dg/kcfi/kcfi-retpoline-r11.c: New test.
>         * gcc.dg/kcfi/kcfi-runtime.c: New test.
>         * gcc.dg/kcfi/kcfi-tail-calls.c: New test.
>         * gcc.dg/kcfi/kcfi-trap-encoding.c: New test.
>         * gcc.dg/kcfi/kcfi-trap-section.c: New test.
>         * gcc.dg/kcfi/kcfi.exp: New test.
>
> Signed-off-by: Kees Cook <kees@...nel.org>
> ---
>  gcc/testsuite/gcc.dg/kcfi/kcfi-adjacency.c    |  72 +++++++++
>  gcc/testsuite/gcc.dg/kcfi/kcfi-basics.c       | 108 +++++++++++++
>  gcc/testsuite/gcc.dg/kcfi/kcfi-call-sharing.c |  84 ++++++++++
>  .../gcc.dg/kcfi/kcfi-cold-partition.c         | 136 ++++++++++++++++
>  .../gcc.dg/kcfi/kcfi-complex-addressing.c     | 135 ++++++++++++++++
>  .../gcc.dg/kcfi/kcfi-ipa-robustness.c         |  54 +++++++
>  .../gcc.dg/kcfi/kcfi-move-preservation.c      |  55 +++++++
>  .../gcc.dg/kcfi/kcfi-no-sanitize-inline.c     | 100 ++++++++++++
>  gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize.c  |  39 +++++
>  .../gcc.dg/kcfi/kcfi-offset-validation.c      |  48 ++++++
>  .../gcc.dg/kcfi/kcfi-patchable-basic.c        |  70 ++++++++
>  .../gcc.dg/kcfi/kcfi-patchable-entry-only.c   |  62 +++++++
>  .../gcc.dg/kcfi/kcfi-patchable-large.c        |  51 ++++++
>  .../gcc.dg/kcfi/kcfi-patchable-medium.c       |  60 +++++++
>  .../gcc.dg/kcfi/kcfi-patchable-prefix-only.c  |  60 +++++++
>  .../gcc.dg/kcfi/kcfi-pic-addressing.c         | 104 ++++++++++++
>  .../gcc.dg/kcfi/kcfi-retpoline-r11.c          |  50 ++++++
>  gcc/testsuite/gcc.dg/kcfi/kcfi-runtime.c      | 151 ++++++++++++++++++
>  gcc/testsuite/gcc.dg/kcfi/kcfi-tail-calls.c   | 142 ++++++++++++++++
>  .../gcc.dg/kcfi/kcfi-trap-encoding.c          |  54 +++++++
>  gcc/testsuite/gcc.dg/kcfi/kcfi-trap-section.c |  41 +++++
>  gcc/testsuite/gcc.dg/kcfi/kcfi.exp            |  64 ++++++++
>  22 files changed, 1740 insertions(+)
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-adjacency.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-basics.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-call-sharing.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-cold-partition.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-complex-addressing.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-ipa-robustness.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-move-preservation.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize-inline.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-offset-validation.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-basic.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-entry-only.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-large.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-medium.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-prefix-only.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-pic-addressing.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-retpoline-r11.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-runtime.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-tail-calls.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-trap-encoding.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi-trap-section.c
>  create mode 100644 gcc/testsuite/gcc.dg/kcfi/kcfi.exp
>
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-adjacency.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-adjacency.c
> new file mode 100644
> index 000000000000..becb47678df0
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-adjacency.c
> @@ -0,0 +1,72 @@
> +/* Test KCFI check/transfer adjacency - regression test for instruction
> +   insertion.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2" } */
> +
> +/* This test ensures that KCFI security checks remain immediately adjacent
> +   to their corresponding indirect calls/jumps, with no executable instructions
> +   between the type ID check and the control flow transfer. */
> +
> +/* External function pointers to prevent optimization.  */
> +extern void (*complex_func_ptr)(int, int, int, int);
> +extern int (*return_func_ptr)(int, int);
> +
> +/* Function with complex argument preparation that could tempt
> +   the optimizer to insert instructions between KCFI check and call.  */
> +__attribute__((noinline)) void test_complex_args(int a, int b, int c, int d) {
> +    /* Complex argument expressions that might cause instruction scheduling.  */
> +    complex_func_ptr(a * 2, b + c, d - a, (a << 1) | b);
> +}
> +
> +/* Function with return value handling.  */
> +__attribute__((noinline)) int test_return_value(int x, int y) {
> +    /* Return value handling that shouldn't interfere with adjacency.  */
> +    int result = return_func_ptr(x + 1, y * 2);
> +    return result + 1;
> +}
> +
> +/* Test struct field access that caused issues in try-catch.c.  */
> +struct call_info {
> +    void (*handler)(void);
> +    int status;
> +    int data;
> +};
> +
> +extern struct call_info *global_call_info;
> +
> +__attribute__((noinline)) void test_struct_field_call(void) {
> +    /* This pattern caused adjacency issues before the fix.  */
> +    global_call_info->handler();
> +}
> +
> +/* Test conditional indirect call.  */
> +__attribute__((noinline)) void test_conditional_call(int flag) {
> +    if (flag) {
> +        global_call_info->handler();
> +    }
> +}
> +
> +/* Should have KCFI instrumentation for all indirect calls.  */
> +
> +/* x86_64: Complete KCFI check sequence should be present.  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r1[01]d\n\taddl\t[^,]+, %r1[01]d\n\tje\t\.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tud2} { target x86_64-*-* } } } */
> +
> +/* AArch64: Complete KCFI check sequence should be present.  */
> +/* { dg-final { scan-assembler {ldur\tw16, \[x[0-9]+, #-[0-9]+\]\n\tmov\tw17, #[0-9]+\n\tmovk\tw17, #[0-9]+, lsl #16\n\tcmp\tw16, w17\n\tb\.eq\t(\.Lkcfi_call[0-9]+)\n\.Lkcfi_trap[0-9]+:\n\tbrk\t#[0-9]+\n\1:\n\tblr\tx[0-9]+} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Complete KCFI check sequence should be present with stack
> +   spilling.  */
> +/* { dg-final { scan-assembler {push\t\{r0, r1\}\n\tldr\tr0, \[r[0-9]+, #-[0-9]+\]\n\tmovw\tr1, #[0-9]+\n\tmovt\tr1, #[0-9]+\n\tcmp\tr0, r1\n\tpop\t\{r0, r1\}\n\tbeq\t\.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tudf\t#[0-9]+\n\.Lkcfi_call[0-9]+:\n\tblx\tr[0-9]+} { target arm32 } } } */
> +
> +/* RISC-V: Complete KCFI check sequence should be present.  */
> +/* { dg-final { scan-assembler {lw\tt1, -4\([a-z0-9]+\)\n\tlui\tt2, [0-9]+\n\taddiw\tt2, t2, -?[0-9]+\n\tbeq\tt1, t2, \.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tebreak} { target riscv*-*-* } } } */
> +
> +/* Should have trap section with entries.  */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target riscv*-*-* } } } */
> +
> +/* AArch64 should NOT have trap section (uses brk immediate instead) */
> +/* { dg-final { scan-assembler-not {\.kcfi_traps} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit should NOT have trap section (uses udf immediate instead) */
> +/* { dg-final { scan-assembler-not {\.kcfi_traps} { target arm32 } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-basics.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-basics.c
> new file mode 100644
> index 000000000000..b0a9e11f1f3c
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-basics.c
> @@ -0,0 +1,108 @@
> +/* Test basic KCFI functionality - preamble generation.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-falign-functions=16" { target x86_64-*-* } } */
> +
> +/* Extern function declarations - should NOT get KCFI preambles.  */
> +extern void external_func(void);
> +extern int external_func_int(int x);
> +
> +void regular_function(int x) {
> +    /* This should get KCFI preamble.  */
> +}
> +
> +void static_target_function(int x) {
> +    /* Target function that can be called indirectly.  */
> +}
> +
> +__attribute__((nocf_check))
> +void nocf_check_function(int x) {
> +    /* This function has nocf_check attribute - should NOT get KCFI preamble.  */
> +}
> +
> +static void static_caller(void) {
> +    /* Static function that makes an indirect call
> +       Should NOT get KCFI preamble (not address-taken)
> +       But must generate KCFI check for the indirect call.  */
> +    void (*local_ptr)(int) = static_target_function;
> +    local_ptr(42);  /* This should generate KCFI check.  */
> +}
> +
> +/* Make external_func address-taken.  */
> +void (*func_ptr)(int) = regular_function;
> +void (*ext_ptr)(void) = external_func;
> +void (__attribute__((nocf_check)) *nocf_ptr)(int) = nocf_check_function;
> +
> +int main() {
> +    func_ptr(42);
> +    ext_ptr();        /* Indirect call to external_func.  */
> +    external_func_int(10);  /* Direct call to external_func_int.  */
> +    static_caller();  /* Direct call to static function.  */
> +    return 0;
> +}
> +
> +/* Verify KCFI preamble exists for regular_function.  */
> +/* { dg-final { scan-assembler {__cfi_regular_function:} } } */
> +
> +/* Verify KCFI preamble symbol comes before main function symbol.  */
> +/* { dg-final { scan-assembler {__cfi_regular_function:.*regular_function:} } } */
> +
> +/* Target function should have preamble (address-taken).  */
> +/* { dg-final { scan-assembler {__cfi_static_target_function:} } } */
> +
> +/* Static caller should NOT have preamble (it's only called directly,
> +   not address-taken). */
> +/* { dg-final { scan-assembler-not {__cfi_static_caller:} } } */
> +
> +/* Function with nocf_check attribute should NOT have preamble.  */
> +/* { dg-final { scan-assembler-not {__cfi_nocf_check_function:} } } */
> +
> +/* x86_64: Verify type ID in preamble (after NOPs, before function label) */
> +/* { dg-final { scan-assembler {__cfi_regular_function:\n\t+nop\n.*\n\t+movl\t+\$0x[0-9a-f]+, %eax} { target x86_64-*-* } } } */
> +
> +/* AArch64: Verify type ID word in preamble.  */
> +/* { dg-final { scan-assembler {__cfi_regular_function:\n\t\.word\t0x[0-9a-f]+} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Verify type ID word in preamble.  */
> +/* { dg-final { scan-assembler {__cfi_regular_function:\n\t\.word\t0x[0-9a-f]+} { target arm32 } } } */
> +
> +/* RISC-V: Verify type ID word in preamble */
> +/* { dg-final { scan-assembler {__cfi_regular_function:\n\t\.word\t0x[0-9a-f]+} { target riscv*-*-* } } } */
> +
> +/* x86_64: Static function should generate complete KCFI check sequence.  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r10d\n\taddl\t-4\(%r[a-z0-9]+\), %r10d\n\tje\t(\.Lkcfi_call[0-9]+)\n\.Lkcfi_trap[0-9]+:\n\tud2\n.*\n\1:\n\tcall} { target x86_64-*-* } } } */
> +
> +/* AArch64: Static function should generate complete KCFI check sequence.  */
> +/* { dg-final { scan-assembler {ldur\tw16, \[x[0-9]+, #-4\]\n\tmov\tw17, #[0-9]+\n\tmovk\tw17, #[0-9]+, lsl #16\n\tcmp\tw16, w17\n\tb\.eq\t(\.Lkcfi_call[0-9]+)\n\.Lkcfi_trap[0-9]+:\n\tbrk\t#[0-9]+\n\1:\n\tblr} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Static function should generate complete KCFI check sequence
> +   with stack spilling.  */
> +/* { dg-final { scan-assembler {push\t\{r0, r1\}\n\tldr\tr0, \[r[0-9]+, #-4\]\n\tmovw\tr1, #[0-9]+\n\tmovt\tr1, #[0-9]+\n\tcmp\tr0, r1\n\tpop\t\{r0, r1\}\n\tbeq\t\.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tudf\t#[0-9]+\n\.Lkcfi_call[0-9]+:\n\tblx\tr[0-9]+} { target arm32 } } } */
> +
> +/* RISC-V: Static function should generate KCFI check for indirect call.  */
> +/* { dg-final { scan-assembler {lw\tt1, -4\([a-z0-9]+\)\n\tlui\tt2, [0-9]+\n\taddiw\tt2, t2, -?[0-9]+\n\tbeq\tt1, t2, (\.Lkcfi_call[0-9]+)\n\.Lkcfi_trap[0-9]+:\n\tebreak\n\t\.section\t\.kcfi_traps,"ao",@progbits,\.text\n\.Lkcfi_entry[0-9]+:\n\t\.4byte\t\.Lkcfi_trap[0-9]+-\.Lkcfi_entry[0-9]+\n\t\.text\n\1:\n\tjalr} { target riscv*-*-* } } } */
> +
> +/* Extern functions should NOT get KCFI preambles.  */
> +/* { dg-final { scan-assembler-not {__cfi_external_func:} } } */
> +/* { dg-final { scan-assembler-not {__cfi_external_func_int:} } } */
> +
> +/* Local functions should NOT get __kcfi_typeid_ symbols.  */
> +/* Only external declarations that are address-taken should get __kcfi_typeid_ */
> +/* { dg-final { scan-assembler-not {__kcfi_typeid_regular_function} } } */
> +/* { dg-final { scan-assembler-not {__kcfi_typeid_main} } } */
> +
> +/* External address-taken functions should get __kcfi_typeid_ symbols.  */
> +/* { dg-final { scan-assembler {__kcfi_typeid_external_func} } } */
> +
> +/* External functions that are only called directly should NOT get
> +   __kcfi_typeid_ symbols.  */
> +/* { dg-final { scan-assembler-not {__kcfi_typeid_external_func_int} } } */
> +
> +/* Should have trap section for KCFI checks.  */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target riscv*-*-* } } } */
> +
> +/* AArch64 should NOT have trap section (uses brk immediate instead).  */
> +/* { dg-final { scan-assembler-not {\.kcfi_traps} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit should NOT have trap section (uses udf immediate instead).  */
> +/* { dg-final { scan-assembler-not {\.kcfi_traps} { target arm32 } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-call-sharing.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-call-sharing.c
> new file mode 100644
> index 000000000000..f34d5f88547f
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-call-sharing.c
> @@ -0,0 +1,84 @@
> +/* Test KCFI check sharing bug - optimizer incorrectly shares KCFI checks
> +   between different function types.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2" } */
> +
> +/* Reproduce the pattern from Linux kernel internal_create_group where:
> +   - Two different function pointer types (is_visible vs is_bin_visible).
> +   - Both get loaded into the same register (%rcx).
> +   - Optimizer creates shared KCFI check with wrong type ID.
> +   - This causes CFI failures in production kernel.  */
> +
> +struct kobject { int dummy; };
> +struct attribute { int dummy; };
> +struct bin_attribute { int dummy; };
> +
> +struct attribute_group {
> +    const char *name;
> +    // Type ID A
> +    int (*is_visible)(struct kobject *, struct attribute *, int);
> +    // Type ID B
> +    int (*is_bin_visible)(struct kobject *, const struct bin_attribute *, int);
> +    struct attribute **attrs;
> +    const struct bin_attribute **bin_attrs;
> +};
> +
> +/* Function that mimics __first_visible from kernel - gets inlined into
> +   caller.  */
> +static int __first_visible(const struct attribute_group *grp, struct kobject *kobj)
> +{
> +    /* Path 1: Call is_visible function pointer.  */
> +    if (grp->attrs && grp->attrs[0] && grp->is_visible)
> +        return grp->is_visible(kobj, grp->attrs[0], 0);
> +
> +    /* Path 2: Call is_bin_visible function pointer.  */
> +    if (grp->bin_attrs && grp->bin_attrs[0] && grp->is_bin_visible)
> +        return grp->is_bin_visible(kobj, grp->bin_attrs[0], 0);
> +
> +    return 0;
> +}
> +
> +/* Main function that triggers the optimization bug.  */
> +int test_kcfi_check_sharing(struct kobject *kobj, const struct attribute_group *grp)
> +{
> +    /* This should inline __first_visible and create the problematic pattern where:
> +       1. Both function pointers get loaded into same register.
> +       2. Optimizer shares KCFI check between them.
> +       3. Uses wrong type ID for one of the calls.  */
> +    return __first_visible(grp, kobj);
> +}
> +
> +/* Each indirect call should have its own KCFI check with correct type ID.
> +
> +   Should see:
> +   1. KCFI check for is_visible call with is_visible type ID.
> +   2. KCFI check for is_bin_visible call with is_bin_visible type ID.  */
> +
> +/* Verify we have TWO different KCFI check sequences.  */
> +/* Each check should have different type ID constants.  */
> +/* x86: { dg-final { scan-assembler-times {movl\s+\$-?[0-9]+,\s+%r10d} 2 { target i?86-*-* x86_64-*-* } } } */
> +/* AArch64: { dg-final { scan-assembler-times {mov\s+w17, #[0-9]+} 2 { target aarch64*-*-* } } } */
> +/* ARM 32-bit: { dg-final { scan-assembler-times {movw\s+r1, #[0-9]+} 2 { target arm32 } } } */
> +/* RISC-V: { dg-final { scan-assembler-times {lui\tt2, [0-9]+} 2 { target riscv*-*-* } } } */
> +
> +/* Verify the checks use DIFFERENT type IDs (not shared).
> +   We should NOT see the same type ID used twice - that would indicate
> +   sharing bug.  */
> +/* x86: { dg-final { scan-assembler-not {movl\s+\$(-?[0-9]+),\s+%r10d.*movl\s+\$\1,\s+%r10d} { target i?86-*-* x86_64-*-* } } } */
> +/* AArch64: { dg-final { scan-assembler-not {mov\s+w17, #([0-9]+).*mov\s+w17, #\1} { target aarch64*-*-* } } } */
> +/* ARM 32-bit: { dg-final { scan-assembler-not {movw\s+r1, #([0-9]+).*movw\s+r1, #\1} { target arm32 } } } */
> +/* RISC-V: { dg-final { scan-assembler-not {lui\s+t2, ([0-9]+)\s.*lui\s+t2, \1\s} { target riscv*-*-* } } } */
> +
> +/* Verify each call follows its own check (not shared) */
> +/* Should have 2 separate trap instructions.  */
> +/* x86: { dg-final { scan-assembler-times {ud2} 2 { target i?86-*-* x86_64-*-* } } } */
> +/* AArch64: { dg-final { scan-assembler-times {brk\s+#[0-9]+} 2 { target aarch64*-*-* } } } */
> +/* ARM 32-bit: { dg-final { scan-assembler-times {udf\s+#[0-9]+} 2 { target arm32 } } } */
> +/* RISC-V: { dg-final { scan-assembler-times {ebreak} 2 { target riscv*-*-* } } } */
> +
> +/* Verify 2 separate call sites.  */
> +/* x86: { dg-final { scan-assembler-times {jmp\s+\*%[a-z0-9]+} 2 { target i?86-*-* x86_64-*-* } } } */
> +/* AArch64: Allow both blr (regular call) and br (tail call) */
> +/* AArch64: { dg-final { scan-assembler-times {br\tx[0-9]+} 2 { target aarch64*-*-* } } } */
> +/* ARM 32-bit: { dg-final { scan-assembler-times {bx\s+(?:r[0-9]+|ip)} 2 { target arm32 } } } */
> +/* RISC-V: { dg-final { scan-assembler-times {jalr\t[a-z0-9]+} 2 { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-cold-partition.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-cold-partition.c
> new file mode 100644
> index 000000000000..17def558ada4
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-cold-partition.c
> @@ -0,0 +1,136 @@
> +/* Test KCFI cold function and cold partition behavior.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2" } */
> +/* { dg-additional-options "-freorder-blocks-and-partition" { target freorder } } */
> +
> +void regular_function(void) {
> +    /* Regular function should get preamble.  */
> +}
> +
> +/* Cold-attributed function should STILL get preamble (it's a regular
> +   function, just marked cold).  */
> +__attribute__((cold))
> +void cold_attributed_function(void) {
> +    /* This function has cold attribute but should still get KCFI preamble.  */
> +}
> +
> +/* Hot-attributed function should get preamble.  */
> +__attribute__((hot))
> +void hot_attributed_function(void) {
> +    /* This function is explicitly hot and should get KCFI preamble.  */
> +}
> +
> +/* Global to prevent optimization from eliminating cold paths.  */
> +extern void abort(void);
> +
> +/* Additional function to test that normal functions still get preambles.  */
> +__attribute__((noinline))
> +int another_regular_function(int x) {
> +    return x + 42;
> +}
> +
> +/* Function designed to generate cold partitions under optimization.  */
> +__attribute__((noinline))
> +void function_with_cold_partition(int condition) {
> +    /* Hot path - very likely to execute.  */
> +    if (__builtin_expect(condition == 42, 1)) {
> +        /* Simple hot path that optimizer will keep inline.  */
> +        return;
> +    }
> +
> +    /* Cold paths that actually do something to prevent elimination.  */
> +    if (__builtin_expect(condition < 0, 0)) {
> +        /* Error path 1 - call abort to prevent elimination.  */
> +        abort();
> +    }
> +
> +    if (__builtin_expect(condition > 1000000, 0)) {
> +        /* Error path 2 - call abort to prevent elimination.  */
> +        abort();
> +    }
> +
> +    if (__builtin_expect(condition == 999999, 0)) {
> +        /* Error path 3 - more substantial cold code.  */
> +        volatile int sum = 0;
> +        for (volatile int i = 0; i < 100; i++) {
> +            sum += i * condition;
> +        }
> +        if (sum > 0)
> +            abort();
> +    }
> +
> +    /* More cold paths - switch with many unlikely cases.  */
> +    switch (condition) {
> +        case 1000001: case 1000002: case 1000003: case 1000004: case 1000005:
> +        case 1000006: case 1000007: case 1000008: case 1000009: case 1000010:
> +            /* Each case does some work before abort.  */
> +            volatile int work = condition * 2;
> +            if (work > 0) abort();
> +            break;
> +        default:
> +            if (condition != 42) {
> +                /* Fallback cold path - substantial work.  */
> +                volatile int result = 0;
> +                for (volatile int j = 0; j < condition % 50; j++) {
> +                    result += j;
> +                }
> +                if (result >= 0) abort();
> +            }
> +    }
> +}
> +
> +/* Test function pointers to ensure address-taken detection works.  */
> +void test_function_pointers(void) {
> +    void (*regular_ptr)(void) = regular_function;
> +    void (*cold_ptr)(void) = cold_attributed_function;
> +    void (*hot_ptr)(void) = hot_attributed_function;
> +
> +    regular_ptr();
> +    cold_ptr();
> +    hot_ptr();
> +}
> +
> +int main() {
> +    regular_function();
> +    cold_attributed_function();
> +    hot_attributed_function();
> +    function_with_cold_partition(42); /* Normal case - stay in hot path.  */
> +    another_regular_function(5);
> +    test_function_pointers();
> +    return 0;
> +}
> +
> +/* Regular function should have preamble.  */
> +/* { dg-final { scan-assembler "__cfi_regular_function:" } } */
> +
> +/* Cold-attributed function should STILL have preamble (it's a legitimate function) */
> +/* { dg-final { scan-assembler "__cfi_cold_attributed_function:" } } */
> +
> +/* Hot-attributed function should have preamble.  */
> +/* { dg-final { scan-assembler "__cfi_hot_attributed_function:" } } */
> +
> +/* Function that generates cold partitions should have preamble for main entry.  */
> +/* { dg-final { scan-assembler "__cfi_function_with_cold_partition:" } } */
> +
> +/* Address-taken functions should have preambles.  */
> +/* { dg-final { scan-assembler "__cfi_test_function_pointers:" } } */
> +
> +/* The function should generate a .cold partition (only on targets that support freorder) */
> +/* { dg-final { scan-assembler "function_with_cold_partition\\.cold:" { target freorder } } } */
> +
> +/* The .cold partition should NOT get a __cfi_ preamble since it's never
> +   reached via indirect calls.  */
> +/* { dg-final { scan-assembler-not "__cfi_function_with_cold_partition\\.cold:" { target freorder } } } */
> +
> +/* Additional regular function should get preamble.  */
> +/* { dg-final { scan-assembler "__cfi_another_regular_function:" } } */
> +
> +/* Test coverage summary:
> +   1. Cold-attributed function (__attribute__((cold))): SHOULD get preamble
> +   2. Cold partition (-freorder-blocks-and-partition): should NOT get preamble
> +   3. IPA split .part function (split_part=true): Logic in place, would skip if triggered
> +
> +   Note: IPA function splitting (creating .part functions with split_part=true) requires
> +   specific optimization conditions that are difficult to trigger reliably in tests.
> +   The KCFI logic correctly handles this case using the split_part flag check.
> +*/
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-complex-addressing.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-complex-addressing.c
> new file mode 100644
> index 000000000000..b9a8955b0899
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-complex-addressing.c
> @@ -0,0 +1,135 @@
> +/* Test KCFI with complex addressing modes (structure members, array
> +   elements). This is a regression test for the change_address_1 RTL
> +   error that occurred when target_addr was PLUS(reg, offset) instead
> +   of a simple register.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2" } */
> +
> +struct function_table {
> +    int (*callback1)(int);
> +    int (*callback2)(int, int);
> +    void (*callback3)(void);
> +    int (*callback4)(void *, void *, void *, void *, void *, void *);
> +    int data;
> +};
> +
> +static int handler1(int x) {
> +    return x * 2;
> +}
> +
> +static int handler2(int x, int y) {
> +    return x + y;
> +}
> +
> +static void handler3(void) {
> +    /* Empty handler.  */
> +}
> +
> +/* Test indirect calls through structure members - this creates
> +   PLUS(reg, offset) addressing.  */
> +int test_struct_members(struct function_table *table) {
> +    int result = 0;
> +
> +    /* These indirect calls will generate complex addressing modes:
> +     * call *(%rdi)          - callback1 at offset 0
> +     * call *8(%rdi)         - callback2 at offset 8
> +     * call *16(%rdi)        - callback3 at offset 16
> +     * KCFI must handle PLUS(reg, struct_offset) + kcfi_offset.  */
> +
> +    result += table->callback1(10);
> +    result += table->callback2(5, 7);
> +    table->callback3();
> +
> +    return result;
> +}
> +
> +/* Test indirect calls through array elements - another source of
> +   complex addressing.  */
> +typedef int (*func_array_t)(int);
> +
> +int test_array_elements(func_array_t functions[], int index) {
> +    /* This creates addressing like MEM[PLUS(PLUS(reg, index*8), 0)]
> +       which should be simplified to MEM[PLUS(reg, index*8)].  */
> +    return functions[index](42);
> +}
> +
> +/* Test with global structure.  */
> +static struct function_table global_table = {
> +    .callback1 = handler1,
> +    .callback2 = handler2,
> +    .callback3 = handler3,
> +    .data = 100
> +};
> +
> +int test_global_struct(void) {
> +    /* Access through global structure - may generate different
> +       addressing patterns.  */
> +    return global_table.callback1(20) + global_table.callback2(3, 4);
> +}
> +
> +/* Test nested structure access.  */
> +struct nested_table {
> +    struct function_table inner;
> +    int extra_data;
> +};
> +
> +int test_nested_struct(struct nested_table *nested) {
> +    /* Even more complex addressing: nested structure member access.  */
> +    return nested->inner.callback1(15);
> +}
> +
> +int test_many_args(void *one, void *two, void *three, void *four, void *five, void *six)
> +{
> +    return (unsigned long)one + (unsigned long)two + (unsigned long)three
> +          + (unsigned long)four + (unsigned long)five + (unsigned long)six;
> +}
> +
> +int main() {
> +    struct function_table local_table = {
> +        .callback1 = handler1,
> +        .callback2 = handler2,
> +        .callback3 = handler3,
> +        .callback4 = test_many_args,
> +        .data = 50
> +    };
> +
> +    func_array_t func_array[] = { handler1, handler1, handler1 };
> +
> +    int result = 0;
> +    result += test_struct_members(&local_table);
> +    result += test_array_elements(func_array, 1);
> +    result += test_global_struct();
> +
> +    struct nested_table nested = { .inner = local_table, .extra_data = 200 };
> +    result += test_nested_struct(&nested);
> +
> +    result += local_table.callback4(handler1, handler2, handler3, &result, main, &local_table);
> +
> +    return result;
> +}
> +
> +/* Verify that all address-taken functions get KCFI preambles.  */
> +/* { dg-final { scan-assembler {__cfi_handler1:} } } */
> +/* { dg-final { scan-assembler {__cfi_handler2:} } } */
> +/* { dg-final { scan-assembler {__cfi_handler3:} } } */
> +/* { dg-final { scan-assembler {__cfi_test_many_args:} } } */
> +
> +/* x86_64: Verify KCFI checks are generated for indirect calls through
> +   complex addressing.  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r10d\n\taddl\t-4\(%r[a-z0-9]+\), %r10d} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {ud2} { target x86_64-*-* } } } */
> +
> +/* AArch64: Verify KCFI checks for complex addressing.  */
> +/* { dg-final { scan-assembler {ldur\tw16, \[x[0-9]+, #-4\]} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler {brk} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Verify KCFI checks for complex addressing with stack spilling.  */
> +/* { dg-final { scan-assembler {ldr\tr0, \[r[0-9]+, #-4\]} { target arm32 } } } */
> +/* { dg-final { scan-assembler {udf} { target arm32 } } } */
> +
> +/* RISC-V: Verify KCFI check sequence for complex addressing.  */
> +/* { dg-final { scan-assembler {lw\tt1, -4\([a-z0-9]+\)\n\tlui\tt2, [0-9]+\n\taddiw\tt2, t2, -?[0-9]+\n\tbeq\tt1, t2, \.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tebreak} { target riscv*-*-* } } } */
> +
> +/* Should have trap section for x86 and RISC-V only.  */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-ipa-robustness.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-ipa-robustness.c
> new file mode 100644
> index 000000000000..a43bcd4f3e3f
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-ipa-robustness.c
> @@ -0,0 +1,54 @@
> +/* Test KCFI IPA pass robustness with compiler-generated constructs.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2" } */
> +
> +#include <stddef.h>
> +
> +/* Test various compiler-generated constructs that could confuse IPA pass.  */
> +
> +/* static_assert - this was causing the original crash.  */
> +typedef struct {
> +    int field1;
> +    char field2;
> +} test_struct_t;
> +
> +static_assert(offsetof(test_struct_t, field1) == 0, "layout check 1");
> +static_assert(offsetof(test_struct_t, field2) == 4, "layout check 2");
> +static_assert(sizeof(test_struct_t) >= 5, "size check");
> +
> +/* Regular functions that should get KCFI analysis.  */
> +void regular_function(void) {
> +    /* Should get KCFI preamble.  */
> +}
> +
> +static void static_function(void) {
> +    /* With -O2: correctly identified as not address-taken, no preamble.  */
> +}
> +
> +void address_taken_function(void) {
> +    /* Should get KCFI preamble (address taken below) */
> +}
> +
> +/* Function pointer to create address-taken scenario.  */
> +void (*func_ptr)(void) = address_taken_function;
> +
> +/* More static_asserts mixed with function definitions.  */
> +static_assert(sizeof(void*) >= 4, "pointer size check");
> +
> +int main(void) {
> +    regular_function();    /* Direct call.  */
> +    static_function();     /* Direct call to static.  */
> +    func_ptr();            /* Indirect call.  */
> +
> +    static_assert(sizeof(int) == 4, "int size check");
> +
> +    return 0;
> +}
> +
> +/* Verify KCFI preambles are generated appropriately.  */
> +/* { dg-final { scan-assembler "__cfi_regular_function:" } } */
> +/* { dg-final { scan-assembler "__cfi_address_taken_function:" } } */
> +/* { dg-final { scan-assembler "__cfi_main:" } } */
> +
> +/* With -O2: static_function correctly identified as not address-taken.  */
> +/* { dg-final { scan-assembler-not "__cfi_static_function:" } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-move-preservation.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-move-preservation.c
> new file mode 100644
> index 000000000000..50029d136716
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-move-preservation.c
> @@ -0,0 +1,55 @@
> +/* Test that KCFI preserves function pointer moves at -O2 optimization.
> +   This test ensures that the combine pass doesn't incorrectly optimize away
> +   the move instruction needed to transfer function pointers from argument
> +   registers to the target registers used by KCFI patterns.  */
> +
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2 -std=gnu11" } */
> +
> +static int called_count = 0;
> +
> +/* Function taking one argument, returning void.  */
> +static __attribute__((noinline)) void increment_void(int *counter)
> +{
> +    (*counter)++;
> +}
> +
> +/* Function taking one argument, returning int.  */
> +static __attribute__((noinline)) int increment_int(int *counter)
> +{
> +    (*counter)++;
> +    return *counter;
> +}
> +
> +/* Don't allow the compiler to inline the calls.  */
> +static __attribute__((noinline)) void indirect_call(void (*func)(int *))
> +{
> +    func(&called_count);
> +}
> +
> +int main(void)
> +{
> +    /* This should work - matching prototype.  */
> +    indirect_call(increment_void);
> +
> +    /* This should trap - mismatched prototype.  */
> +    indirect_call((void *)increment_int);
> +
> +    return 0;
> +}
> +
> +/* Verify complete KCFI check sequence with preserved move instruction. At
> +   -O2, the combine pass previously optimized away the move from %rdi to %rax,
> +   breaking KCFI. Verify the full sequence is preserved. */
> +
> +/* x86_64: Complete KCFI sequence with move preservation and indirect jump.  */
> +/* { dg-final { scan-assembler {(indirect_call):.*\n.*movq\s+%rdi,\s+(%rax)\n.*movl\s+\$[0-9]+,\s+%r10d\n\taddl\s+-4\(\2\),\s+%r10d\n\tje\s+\.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tud2.*\.Lkcfi_call[0-9]+:\n\tjmp\s+\*\2.*\.size\s+\1,\s+\.-\1} { target x86_64-*-* } } } */
> +
> +/* AArch64: Complete KCFI sequence with move preservation and indirect branch.  */
> +/* { dg-final { scan-assembler {(indirect_call):.*\n.*mov\s+(x[0-9]+),\s+x0\n.*ldur\s+w16,\s+\[\2,\s+#-4\]\n\tmov\s+w17,\s+#[0-9]+\n\tmovk\s+w17,\s+#[0-9]+,\s+lsl\s+#16\n\tcmp\s+w16,\s+w17\n\tb\.eq\s+\.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tbrk\s+#[0-9]+.*\.Lkcfi_call[0-9]+:\n\tbr\s+\2.*\.size\s+\1,\s+\.-\1} { target aarch64*-*-* } } } */
> +
> +/* ARM32: Complete KCFI sequence with move preservation and indirect branch.  */
> +/* { dg-final { scan-assembler {(indirect_call):.*\n.*mov\s+(r[0-9]+),\s+r0\n.*push\s+\{r0,\s+r1\}\n\tldr\s+r0,\s+\[\2,\s+#-4\]\n\tmovw\s+r1,\s+#[0-9]+\n\tmovt\s+r1,\s+#[0-9]+\n\tcmp\s+r0,\s+r1\n\tpop\s+\{r0,\s+r1\}\n\tbeq\s+\.Lkcfi_call[0-9]+\n\.Lkcfi_trap[0-9]+:\n\tudf\s+#[0-9]+.*\.Lkcfi_call[0-9]+:\n\tbx\s+\2.*\.size\s+\1,\s+\.-\1} { target arm32 } } } */
> +
> +/* RISC-V: Complete KCFI sequence with move preservation and indirect jump.  */
> +/* { dg-final { scan-assembler {(indirect_call):.*mv\s+(a[0-9]+),a0.*lw\s+t1,\s+-4\(\2\).*lui\s+t2,\s+[0-9]+.*addiw\s+t2,\s+t2,\s+-?[0-9]+.*beq\s+t1,\s+t2,\s+\.Lkcfi_call[0-9]+.*ebreak.*jalr\s+zero,\s+\2,\s+0.*\.size\s+\1,\s+\.-\1} { target riscv64-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize-inline.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize-inline.c
> new file mode 100644
> index 000000000000..c43d8014ff2d
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize-inline.c
> @@ -0,0 +1,100 @@
> +/* Test that no_sanitize("kcfi") attribute is preserved during inlining.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2" } */
> +
> +extern void external_side_effect(int value);
> +
> +/* Regular function (should get KCFI checks) */
> +__attribute__((noinline))
> +void normal_function(void (*callback)(int))
> +{
> +    /* This indirect call must generate KCFI checks.  */
> +    callback(300);
> +    external_side_effect(300);
> +}
> +
> +/* Regular function marked with no_sanitize("kcfi") (positive control) */
> +__attribute__((noinline, no_sanitize("kcfi")))
> +void sensitive_non_inline_function(void (*callback)(int))
> +{
> +    /* This indirect call should NOT generate KCFI checks.  */
> +    callback(100);
> +    external_side_effect(100);
> +}
> +
> +/* Function marked with both no_sanitize("kcfi") and always_inline.  */
> +__attribute__((always_inline, no_sanitize("kcfi")))
> +static inline void sensitive_inline_function(void (*callback)(int))
> +{
> +    /* This indirect call should NOT generate KCFI checks when inlined.  */
> +    callback(42);
> +    external_side_effect(42);
> +}
> +
> +/* Explicit wrapper for testing sensitive_inline_function behavior.  */
> +__attribute__((noinline))
> +void wrap_sensitive_inline(void (*callback)(int))
> +{
> +    sensitive_inline_function(callback);
> +}
> +
> +/* Function marked with only always_inline (should get KCFI checks) */
> +__attribute__((always_inline))
> +static inline void normal_inline_function(void (*callback)(int))
> +{
> +    /* This indirect call must generate KCFI checks when inlined.  */
> +    callback(200);
> +    external_side_effect(200);
> +}
> +
> +/* Explicit wrapper for testing normal_inline_function behavior.  */
> +__attribute__((noinline))
> +void wrap_normal_inline(void (*callback)(int))
> +{
> +    normal_inline_function(callback);
> +}
> +
> +void test_callback(int value)
> +{
> +    external_side_effect(value);
> +}
> +
> +static void (*volatile function_pointer)(int) = test_callback;
> +
> +int main(void)
> +{
> +    void (*fn_ptr)(int) = function_pointer;
> +
> +    normal_function(fn_ptr);
> +    wrap_normal_inline(fn_ptr);
> +    sensitive_non_inline_function(fn_ptr);
> +    wrap_sensitive_inline(fn_ptr);
> +
> +    return 0;
> +}
> +
> +/* Verify correct number of KCFI checks: exactly 2 */
> +/* { dg-final { scan-assembler-times {ud2} 2 { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-times {brk\s+#[0-9]+} 2 { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler-times {udf\s+#[0-9]+} 2 { target arm32 } } } */
> +/* { dg-final { scan-assembler-times {ebreak} 2 { target riscv*-*-* } } } */
> +
> +/* Positive controls: these should have KCFI checks.  */
> +/* { dg-final { scan-assembler {normal_function:.*ud2.*\.size\s+normal_function} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {wrap_normal_inline:.*ud2.*\.size\s+wrap_normal_inline} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {normal_function:.*brk\s+#[0-9]+.*\.size\s+normal_function} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler {wrap_normal_inline:.*brk\s+#[0-9]+.*\.size\s+wrap_normal_inline} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler {normal_function:.*udf\t#[0-9]+.*\.size\s+normal_function} { target arm32 } } } */
> +/* { dg-final { scan-assembler {wrap_normal_inline:.*udf\t#[0-9]+.*\.size\s+wrap_normal_inline} { target arm32 } } } */
> +/* { dg-final { scan-assembler {normal_function:.*ebreak.*\.size\s+normal_function} { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler {wrap_normal_inline:.*ebreak.*\.size\s+wrap_normal_inline} { target riscv*-*-* } } } */
> +
> +/* Negative controls: these should NOT have KCFI checks.  */
> +/* { dg-final { scan-assembler-not {sensitive_non_inline_function:.*ud2.*\.size\s+sensitive_non_inline_function} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-not {wrap_sensitive_inline:.*ud2.*\.size\s+wrap_sensitive_inline} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-not {sensitive_non_inline_function:.*brk\s+#[0-9]+.*\.size\s+sensitive_non_inline_function} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler-not {wrap_sensitive_inline:.*brk\s+#[0-9]+.*\.size\s+wrap_sensitive_inline} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler-not {sensitive_non_inline_function:[^\n]*udf\t#[0-9]+[^\n]*\.size\tsensitive_non_inline_function} { target arm32 } } } */
> +/* { dg-final { scan-assembler-not {wrap_sensitive_inline:[^\n]*udf\t#[0-9]+[^\n]*\.size\twrap_sensitive_inline} { target arm32 } } } */
> +/* { dg-final { scan-assembler-not {sensitive_non_inline_function:.*ebreak.*\.size\s+sensitive_non_inline_function} { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler-not {wrap_sensitive_inline:.*ebreak.*\.size\s+wrap_sensitive_inline} { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize.c
> new file mode 100644
> index 000000000000..6f1a558c0820
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-no-sanitize.c
> @@ -0,0 +1,39 @@
> +/* Test KCFI with no_sanitize attribute.  */
> +/* { dg-do compile } */
> +
> +void target_function(void) {
> +    /* This should get KCFI preamble.  */
> +}
> +
> +void caller_with_checks(void) {
> +    /* This function should generate KCFI checks.  */
> +    void (*func_ptr)(void) = target_function;
> +    func_ptr();
> +}
> +
> +__attribute__((no_sanitize("kcfi")))
> +void caller_no_checks(void) {
> +    /* This function should NOT generate KCFI checks due to no_sanitize.  */
> +    void (*func_ptr)(void) = target_function;
> +    func_ptr();
> +}
> +
> +int main() {
> +    caller_with_checks();    /* This should generate checks inside.  */
> +    caller_no_checks();      /* This should NOT generate checks inside.  */
> +    return 0;
> +}
> +
> +/* All functions should get preambles regardless of no_sanitize.  */
> +/* { dg-final { scan-assembler "__cfi_target_function:" } } */
> +/* { dg-final { scan-assembler "__cfi_caller_with_checks:" } } */
> +/* { dg-final { scan-assembler "__cfi_caller_no_checks:" } } */
> +/* { dg-final { scan-assembler "__cfi_main:" } } */
> +
> +/* caller_with_checks() should generate KCFI check.
> +   caller_no_checks() should NOT generate KCFI check (no_sanitize).
> +   So a total of exactly 1 KCFI check in the entire program.  */
> +/* { dg-final { scan-assembler-times {addl\t-4\(%r[ad]x\), %r1[01]d} 1 { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-times {ldur\tw16, \[x[0-9]+, #-4\]} 1 { target aarch64-*-* } } } */
> +/* { dg-final { scan-assembler-times {ldr\tr0, \[r[0-9]+, #-4\]} 1 { target arm32 } } } */
> +/* { dg-final { scan-assembler-times {lw\tt1, -[0-9]+\(} 1 { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-offset-validation.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-offset-validation.c
> new file mode 100644
> index 000000000000..f93a042d9752
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-offset-validation.c
> @@ -0,0 +1,48 @@
> +/* Test KCFI call-site offset validation across architectures.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-falign-functions=16" { target x86_64-*-* } } */
> +
> +void target_func_a(void) { }
> +void target_func_b(int x) { }
> +void target_func_c(int x, int y) { }
> +
> +int main() {
> +    void (*ptr_a)(void) = target_func_a;
> +    void (*ptr_b)(int) = target_func_b;
> +    void (*ptr_c)(int, int) = target_func_c;
> +
> +    /* Multiple indirect calls.  */
> +    ptr_a();
> +    ptr_b(1);
> +    ptr_c(1, 2);
> +
> +    return 0;
> +}
> +
> +/* Should have KCFI preambles for all functions.  */
> +/* { dg-final { scan-assembler "__cfi_target_func_a:" } } */
> +/* { dg-final { scan-assembler "__cfi_target_func_b:" } } */
> +/* { dg-final { scan-assembler "__cfi_target_func_c:" } } */
> +
> +/* x86_64: All call sites should use -4 offset for KCFI type ID loads, even
> +   with -falign-functions=16 (we're not using patchable entries here).  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r10d\n\taddl\t-4\(%r[a-z0-9]+\), %r10d} { target x86_64-*-* } } } */
> +
> +/* AArch64: All call sites should use -4 offset.  */
> +/* { dg-final { scan-assembler {ldur\tw16, \[x[0-9]+, #-4\]} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: All call sites should use -4 offset with stack spilling.  */
> +/* { dg-final { scan-assembler {ldr\tr0, \[r[0-9]+, #-4\]} { target arm32 } } } */
> +
> +/* RISC-V: All call sites should use -4 offset.  */
> +/* { dg-final { scan-assembler {lw\tt1, -4\(} { target riscv*-*-* } } } */
> +
> +/* Should have trap section.  */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target riscv*-*-* } } } */
> +
> +/* AArch64 should NOT have trap section (uses brk immediate instead) */
> +/* { dg-final { scan-assembler-not {\.kcfi_traps} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit should NOT have trap section (uses udf immediate instead) */
> +/* { dg-final { scan-assembler-not {\.kcfi_traps} { target arm32 } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-basic.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-basic.c
> new file mode 100644
> index 000000000000..a2d0ef0c6ff6
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-basic.c
> @@ -0,0 +1,70 @@
> +/* Test KCFI with patchable function entries - basic case.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-fpatchable-function-entry=5,2" } */
> +/* { dg-additional-options "-falign-functions=16" { target x86_64-*-* } } */
> +
> +void test_function(int x) {
> +    /* Function should get both KCFI preamble and patchable entries.  */
> +}
> +
> +int main() {
> +    test_function(42);
> +    return 0;
> +}
> +
> +/* Should have KCFI preamble.  */
> +/* { dg-final { scan-assembler "__cfi_test_function:" } } */
> +
> +/* Should have patchable function entry section.  */
> +/* { dg-final { scan-assembler "__patchable_function_entries" } } */
> +
> +/* x86_64: Should have exactly 2 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*\.type} { target x86_64-*-* } } } */
> +
> +/* x86_64: Should have exactly 3 entry NOPs between .cfi_startproc and
> +   pushq.  */
> +/* { dg-final { scan-assembler {\.cfi_startproc\n\t*nop\n\t*nop\n\t*nop\n\t*pushq} { target x86_64-*-* } } } */
> +
> +/* x86_64: KCFI should have exactly 9 NOPs between __cfi_ and movl.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*movl} { target x86_64-*-* } } } */
> +
> +/* x86_64: Validate KCFI type ID is present.  */
> +/* { dg-final { scan-assembler {movl\t\$0x[0-9a-f]+, %eax} { target x86_64-*-* } } } */
> +
> +/* AArch64: Should have exactly 2 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*\.type} { target aarch64*-*-* } } } */
> +
> +/* AArch64: Should have exactly 3 entry NOPs between .cfi_startproc and
> +   stack manipulation.  */
> +/* { dg-final { scan-assembler {\.cfi_startproc\n\t*nop\n\t*nop\n\t*nop\n\t*sub\t*sp} { target aarch64*-*-* } } } */
> +
> +/* AArch64: KCFI should have alignment NOPs then .word immediate.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*nop\n\t*\.word\t0x[0-9a-f]+} { target aarch64*-*-* } } } */
> +
> +/* AArch64: Validate clean KCFI boundary - .word then immediate end/size.  */
> +/* { dg-final { scan-assembler {\.word\t0x[0-9a-f]+\n\.Lcfi_func_end_test_function:\n\t\.size\t__cfi_test_function, \.-__cfi_test_function} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Should have exactly 2 prefix NOPs between .LPFE and .syntax.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*\.syntax} { target arm32 } } } */
> +
> +/* ARM 32-bit: Should have exactly 3 entry NOPs after function label.  */
> +/* { dg-final { scan-assembler {test_function:\n\t*nop\n\t*nop\n\t*nop} { target arm32 } } } */
> +
> +/* ARM 32-bit: KCFI should have alignment NOPs then .word immediate.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*nop\n\t*\.word\t0x[0-9a-f]+} { target arm32 } } } */
> +
> +/* ARM 32-bit: Validate clean KCFI boundary - .word then immediate end/size.  */
> +/* { dg-final { scan-assembler {\.word\t0x[0-9a-f]+\n\.Lcfi_func_end_test_function:\n\t\.size\t__cfi_test_function, \.-__cfi_test_function} { target arm32 } } } */
> +
> +/* RISC-V: Should have exactly 2 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*\.type} { target riscv*-*-* } } } */
> +
> +/* RISC-V: Should have exactly 3 entry NOPs before .cfi_startproc followed
> +   by addi sp.  */
> +/* { dg-final { scan-assembler {nop\n\t*nop\n\t*nop\n\.LFB[0-9]+:\n\t*\.cfi_startproc\n\t*addi\t*sp} { target riscv*-*-* } } } */
> +
> +/* RISC-V: KCFI should have alignment NOPs then .word immediate.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*nop\n\t*\.word\t0x[0-9a-f]+} { target riscv*-*-* } } } */
> +
> +/* RISC-V: Validate clean KCFI boundary - .word then immediate end/size.  */
> +/* { dg-final { scan-assembler {\.word\t0x[0-9a-f]+\n\.Lcfi_func_end_test_function:\n\t\.size\t__cfi_test_function, \.-__cfi_test_function} { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-entry-only.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-entry-only.c
> new file mode 100644
> index 000000000000..62e1926e107e
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-entry-only.c
> @@ -0,0 +1,62 @@
> +/* Test KCFI with patchable function entries - entry NOPs only.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-fpatchable-function-entry=4,0" } */
> +/* { dg-additional-options "-falign-functions=16" { target x86_64-*-* } } */
> +
> +void test_function(void) {
> +}
> +
> +static void caller(void) {
> +    /* Make an indirect call to test callsite offset calculation.  */
> +    void (*func_ptr)(void) = test_function;
> +    func_ptr();
> +}
> +
> +int main() {
> +    test_function();  /* Direct call.  */
> +    caller();         /* Indirect call via static function.  */
> +    return 0;
> +}
> +
> +/* x86_64: Should have KCFI preamble with architecture alignment NOPs (11).  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+nop\n\t+movl\t+\$0x[0-9a-f]+, %eax} { target x86_64-*-* } } } */
> +
> +/* AArch64: Should have KCFI preamble with no alignment NOPs.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*\.word\t0x[0-9a-f]+} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Should have KCFI preamble with no alignment NOPs.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t\.word\t0x[0-9a-f]+} { target arm32 } } } */
> +
> +/* RISC-V: Should have KCFI preamble with no alignment NOPs.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t\.word\t0x[0-9a-f]+} { target riscv*-*-* } } } */
> +
> +/* x86_64: Indirect call should use original prefix NOPs (0) for offset
> +   calculation: -4 offset.  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r10d\n\taddl\t-4\(%r[a-z0-9]+\), %r10d\n\tje\t(\.Lkcfi_call[0-9]+)\n\.Lkcfi_trap[0-9]+:\n\tud2\n.*\n\1:\n\tcall} { target x86_64-*-* } } } */
> +
> +/* x86_64: All 4 NOPs are entry NOPs - should have exactly 4 entry NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*pushq} { target x86_64-*-* } } } */
> +
> +/* AArch64: All 4 NOPs are entry NOPs - should have exactly 4 entry NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*stp} { target aarch64*-*-* } } } */
> +
> +/* AArch64: No alignment NOPs - function type should come immediately before
> +   function.  */
> +/* { dg-final { scan-assembler {\.type\t*test_function, %function\n*test_function:} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: All 4 NOPs are entry NOPs - should have exactly 4 entry NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop} { target arm32 } } } */
> +
> +/* ARM 32-bit: No alignment NOPs - function type should come immediately
> +   before function.  */
> +/* { dg-final { scan-assembler {\.type\t*test_function, %function\n*test_function:} { target arm32 } } } */
> +
> +/* RISC-V: All 4 NOPs are entry NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\.LFB} { target riscv*-*-* } } } */
> +
> +/* RISC-V: No alignment NOPs - function type should come immediately
> +   before function.  */
> +/* { dg-final { scan-assembler {\.type\t*test_function, @function\n*test_function:} { target riscv*-*-* } } } */
> +
> +/* Should have patchable function entry section.  */
> +/* { dg-final { scan-assembler "__patchable_function_entries" } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-large.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-large.c
> new file mode 100644
> index 000000000000..3d5618847840
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-large.c
> @@ -0,0 +1,51 @@
> +/* Test KCFI with large patchable function entries.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-fpatchable-function-entry=11,11" } */
> +/* { dg-additional-options "-falign-functions=16" { target x86_64-*-* } } */
> +
> +void test_function(void) {
> +}
> +
> +int main() {
> +    void (*func_ptr)(void) = test_function;
> +    func_ptr();
> +    return 0;
> +}
> +
> +/* Should have KCFI preamble.  */
> +/* { dg-final { scan-assembler "__cfi_test_function:" } } */
> +
> +/* Should have patchable function entry section.  */
> +/* { dg-final { scan-assembler "__patchable_function_entries" } } */
> +
> +/* x86_64: Should have exactly 11 alignment NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*\.type} { target x86_64-*-* } } } */
> +
> +/* x86_64: Should have 0 entry NOPs - function starts immediately with
> +   pushq.  */
> +/* { dg-final { scan-assembler {test_function:\n\.LFB[0-9]+:\n\t*\.cfi_startproc\n\t*pushq\t*%rbp} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-not {\t*\.weak\t*__kcfi_typeid_test_function\n} { target x86_64-*-* } } } */
> +
> +/* x86_64: KCFI should have 0 entry NOPs - goes directly to typeid movl.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*movl\t\$0x[0-9a-f]+, %eax} { target x86_64-*-* } } } */
> +
> +/* x86_64: Call site should use -15 offset.  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r10d\n\taddl\t-15\(%r[a-z0-9]+\), %r10d} { target x86_64-*-* } } } */
> +
> +/* AArch64: Should have exactly 11 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*\.type} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Should have exactly 11 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop} { target arm32 } } } */
> +
> +/* AArch64: Call site should use -15 offset.  */
> +/* { dg-final { scan-assembler {ldur\tw16, \[x[0-9]+, #-15\]} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Call site should use -15 offset.  */
> +/* { dg-final { scan-assembler {ldr\tr0, \[r[0-9]+, #-15\]} { target arm32 } } } */
> +
> +/* RISC-V: Should have 11 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*\.type} { target riscv*-*-* } } } */
> +
> +/* RISC-V: Call site should use -15 offset (same as x86/AArch64).  */
> +/* { dg-final { scan-assembler {lw\tt1, -15\(} { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-medium.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-medium.c
> new file mode 100644
> index 000000000000..4f00a86dbcb7
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-medium.c
> @@ -0,0 +1,60 @@
> +/* Test KCFI with medium patchable function entries.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-fpatchable-function-entry=8,4" } */
> +/* { dg-additional-options "-falign-functions=16" { target x86_64-*-* } } */
> +
> +void test_function(void) {
> +}
> +
> +int main() {
> +    void (*func_ptr)(void) = test_function;
> +    func_ptr();
> +    return 0;
> +}
> +
> +/* Should have KCFI preamble.  */
> +/* { dg-final { scan-assembler "__cfi_test_function:" } } */
> +
> +/* Should have patchable function entry section.  */
> +/* { dg-final { scan-assembler "__patchable_function_entries" } } */
> +
> +/* x86_64: Should have exactly 4 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*\.type} { target x86_64-*-* } } } */
> +
> +/* x86_64: Should have exactly 4 entry NOPs between .cfi_startproc and
> +   pushq.  */
> +/* { dg-final { scan-assembler {\.cfi_startproc\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*pushq} { target x86_64-*-* } } } */
> +
> +/* x86_64: KCFI should have exactly 7 alignment NOPs between __cfi_ and
> +   typeid movl.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*movl\t\$0x[0-9a-f]+, %eax} { target x86_64-*-* } } } */
> +
> +/* x86_64: Call site should use -8 offset.  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r10d\n\taddl\t-8\(%r[a-z0-9]+\), %r10d} { target x86_64-*-* } } } */
> +
> +/* AArch64: Should have exactly 4 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*\.type} { target aarch64*-*-* } } } */
> +
> +/* AArch64: Should have exactly 4 entry NOPs after .cfi_startproc.  */
> +/* { dg-final { scan-assembler {\.cfi_startproc\n\t*nop\n\t*nop\n\t*nop\n\t*nop} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Should have exactly 4 prefix NOPs between .LPFE and .syntax.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*\.syntax} { target arm32 } } } */
> +
> +/* ARM 32-bit: Should have exactly 4 entry NOPs after function label.  */
> +/* { dg-final { scan-assembler {test_function:\n\t*nop\n\t*nop\n\t*nop\n\t*nop} { target arm32 } } } */
> +
> +/* AArch64: Call site should use -8 offset.  */
> +/* { dg-final { scan-assembler {ldur\tw16, \[x[0-9]+, #-8\]} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Call site should use -8 offset.  */
> +/* { dg-final { scan-assembler {ldr\tr0, \[r[0-9]+, #-8\]} { target arm32 } } } */
> +
> +/* RISC-V: Should have exactly 4 prefix NOPs between .LPFE and .type.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*\.type} { target riscv*-*-* } } } */
> +
> +/* RISC-V: Should have 4 entry NOPs.  */
> +/* { dg-final { scan-assembler {test_function:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\.LFB} { target riscv*-*-* } } } */
> +
> +/* RISC-V: Call site should use -8 offset (same as x86/AArch64) */
> +/* { dg-final { scan-assembler {lw\tt1, -8\(} { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-prefix-only.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-prefix-only.c
> new file mode 100644
> index 000000000000..98c53ef52989
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-patchable-prefix-only.c
> @@ -0,0 +1,60 @@
> +/* Test KCFI with patchable function entries - prefix NOPs only.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-fpatchable-function-entry=3,3" } */
> +/* { dg-additional-options "-falign-functions=16" { target x86_64-*-* } } */
> +
> +void test_function(void) {
> +}
> +
> +int main() {
> +    test_function();
> +    return 0;
> +}
> +
> +/* Should have KCFI preamble.  */
> +/* { dg-final { scan-assembler "__cfi_test_function:" } } */
> +
> +/* x86_64: All 3 NOPs are prefix NOPs - should have exactly 3 prefix NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*\.type\t*test_function} { target x86_64-*-* } } } */
> +
> +/* x86_64: No entry NOPs - function should start immediately with prologue. */
> +/* { dg-final { scan-assembler {test_function:\n\.LFB[0-9]+:\n\t*\.cfi_startproc\n\t*pushq\t*%rbp} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-not {\t*\.weak\t*__kcfi_typeid_test_function\n} { target x86_64-*-* } } } */
> +
> +/* x86_64: should have exactly 8 alignment NOPs.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*nop\n\t*movl} { target x86_64-*-* } } } */
> +
> +/* AArch64: All 3 NOPs are prefix NOPs - should have exactly 3 prefix NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*\.type\t*test_function} { target aarch64*-*-* } } } */
> +
> +/* AArch64: No entry NOPs - function should start immediately with prologue.  */
> +/* { dg-final { scan-assembler {test_function:\n\.LFB[0-9]+:\n\t*\.cfi_startproc\n\t*nop\n\t*ret} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler-not {\t*\.weak\t*__kcfi_typeid_test_function\n} { target aarch64*-*-* } } } */
> +
> +/* AArch64: KCFI type ID should have 1 alignment NOP then word.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*\.word\t0x[0-9a-f]+} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: All 3 NOPs are prefix NOPs - should have exactly 3 prefix NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop} { target arm32 } } } */
> +
> +/* ARM 32-bit: No entry NOPs - function should start immediately with
> +   prologue.  */
> +/* { dg-final { scan-assembler {test_function:} { target arm32 } } } */
> +/* { dg-final { scan-assembler-not {\t*\.weak\t*__kcfi_typeid_test_function\n} { target arm32 } } } */
> +
> +/* ARM 32-bit: KCFI type ID should have 1 alignment NOP then word.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*\.word\t0x[0-9a-f]+} { target arm32 } } } */
> +
> +/* RISC-V: All 3 NOPs are prefix NOPs - should have exactly 3 prefix NOPs.  */
> +/* { dg-final { scan-assembler {\.LPFE[0-9]+:\n\t*nop\n\t*nop\n\t*nop\n\t*\.type\t*test_function} { target riscv*-*-* } } } */
> +
> +/* RISC-V: No entry NOPs - function should start immediately with
> +   .cfi_startproc.  */
> +/* { dg-final { scan-assembler {test_function:\n\.LFB[0-9]+:\n\t*\.cfi_startproc} { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler-not {\t*\.weak\t*__kcfi_typeid_test_function\n} { target riscv*-*-* } } } */
> +
> +/* RISC-V: KCFI type ID should have 1 alignment NOP then word.  */
> +/* { dg-final { scan-assembler {__cfi_test_function:\n\t*nop\n\t*\.word\t0x[0-9a-f]+} { target riscv*-*-* } } } */
> +
> +/* Should have patchable function entry section.  */
> +/* { dg-final { scan-assembler "__patchable_function_entries" } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-pic-addressing.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-pic-addressing.c
> new file mode 100644
> index 000000000000..26323db4572f
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-pic-addressing.c
> @@ -0,0 +1,104 @@
> +/* Test KCFI with position-independent code addressing modes.
> +   This is a regression test for complex addressing like
> +   PLUS(PLUS(...), symbol_ref) which can occur with PIC and caused
> +   change_address_1 RTL errors.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2 -fpic" } */
> +
> +/* Global function pointer table that creates PIC addressing.  */
> +struct callbacks {
> +    int (*handler1)(int);
> +    void (*handler2)(void);
> +    int (*handler3)(int, int);
> +};
> +
> +static int simple_handler(int x) {
> +    return x * 2;
> +}
> +
> +static void void_handler(void) {
> +    /* Empty handler.  */
> +}
> +
> +static int complex_handler(int a, int b) {
> +    return a + b;
> +}
> +
> +/* Global structure that will require PIC addressing.  */
> +struct callbacks global_callbacks = {
> +    .handler1 = simple_handler,
> +    .handler2 = void_handler,
> +    .handler3 = complex_handler
> +};
> +
> +/* Function that uses PIC addressing to access global callbacks.  */
> +int test_pic_addressing(int value) {
> +    /* These indirect calls through global structure create complex
> +       addressing like PLUS(PLUS(GOT_base, symbol_offset), struct_offset)
> +       which previously caused RTL errors in KCFI instrumentation.  */
> +
> +    int result = 0;
> +    result += global_callbacks.handler1(value);
> +
> +    global_callbacks.handler2();
> +
> +    result += global_callbacks.handler3(value, result);
> +
> +    return result;
> +}
> +
> +/* Test with function pointer arrays.  */
> +static int (*func_array[])(int) = {
> +    simple_handler,
> +    simple_handler,
> +    simple_handler
> +};
> +
> +int test_pic_array(int index, int value) {
> +    /* Array access with PIC can also create complex addressing.  */
> +    return func_array[index % 3](value);
> +}
> +
> +/* Test with dynamic PIC addressing.  */
> +struct callbacks *get_callbacks(void) {
> +    return &global_callbacks;
> +}
> +
> +int test_dynamic_pic(int value) {
> +    /* Dynamic access through function call creates very complex addressing.  */
> +    struct callbacks *cb = get_callbacks();
> +    return cb->handler1(value) + cb->handler3(value, value);
> +}
> +
> +int main() {
> +    int result = 0;
> +    result += test_pic_addressing(10);
> +    result += test_pic_array(1, 20);
> +    result += test_dynamic_pic(5);
> +    return result;
> +}
> +
> +/* Verify that all address-taken functions get KCFI preambles.  */
> +/* { dg-final { scan-assembler {__cfi_simple_handler:} } } */
> +/* { dg-final { scan-assembler {__cfi_void_handler:} } } */
> +/* { dg-final { scan-assembler {__cfi_complex_handler:} } } */
> +
> +/* x86_64: Verify KCFI checks are generated.  */
> +/* { dg-final { scan-assembler {movl\t\$-?[0-9]+, %r10d\n\taddl\t-4\(%r[a-z0-9]+\), %r10d} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {ud2} { target x86_64-*-* } } } */
> +
> +/* AArch64: Verify KCFI checks.  */
> +/* { dg-final { scan-assembler {ldur\tw16, \[x[0-9]+, #-4\]} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler {brk} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit: Verify KCFI checks with PIC addressing and stack spilling.  */
> +/* { dg-final { scan-assembler {ldr\tr0, \[r[0-9]+, #-4\]} { target arm32 } } } */
> +/* { dg-final { scan-assembler {udf} { target arm32 } } } */
> +
> +/* RISC-V: Verify KCFI checks are generated.  */
> +/* { dg-final { scan-assembler {lw\tt1, -4\([a-z0-9]+\)\n\tlui\tt2, [0-9]+\n\taddiw\tt2, t2, -?[0-9]+} { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler {ebreak} { target riscv*-*-* } } } */
> +
> +/* Should have trap section.  */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler {\.kcfi_traps} { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-retpoline-r11.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-retpoline-r11.c
> new file mode 100644
> index 000000000000..79e5ca61cdc2
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-retpoline-r11.c
> @@ -0,0 +1,50 @@
> +/* Test KCFI with retpoline thunk-extern flag forces r11 usage.  */
> +/* { dg-do compile { target x86_64-*-* } } */
> +/* { dg-additional-options "-O2 -mindirect-branch=thunk-extern" } */
> +
> +extern int external_target(void);
> +
> +/* Test regular call (not tail call) */
> +__attribute__((noinline))
> +int call_test(int (*func_ptr)(void)) {
> +    /* This indirect call should use r11 when both KCFI and
> +       -mindirect-branch=thunk-extern are enabled.  */
> +    int result = func_ptr();  /* Function parameter prevents direct optimization.  */
> +    return result + 1;  /* Prevent tail call optimization.  */
> +}
> +
> +/* Reference external_target to generate the required symbol.  */
> +int (*external_func_ptr)(void) = external_target;
> +
> +/* Test function for sibcalls (tail calls) */
> +__attribute__((noinline))
> +void sibcall_test(int (**func_ptr)(void)) {
> +    /* This sibcall should use r11 when both KCFI and
> +       -mindirect-branch=thunk-extern are enabled.  */
> +    (*func_ptr)();  /* Tail call - should be optimized to sibcall.  */
> +}
> +
> +/* Should have weak symbol for external function.  */
> +/* { dg-final { scan-assembler "__kcfi_typeid_external_target" } } */
> +
> +/* When both KCFI and -mindirect-branch=thunk-extern are enabled,
> +   indirect calls should always use r11 register and convert to extern thunks.  */
> +/* { dg-final { scan-assembler-times {call\s+__x86_indirect_thunk_r11} 1 } } */
> +
> +/* Sibcalls should also use r11 register and convert to extern thunks.  */
> +/* { dg-final { scan-assembler-times {jmp\s+__x86_indirect_thunk_r11} 1 } } */
> +
> +/* Should have exactly 2 KCFI traps (one per function) */
> +/* { dg-final { scan-assembler-times {ud2} 2 } } */
> +
> +/* Should NOT use other registers for indirect calls.  */
> +/* { dg-final { scan-assembler-not {call\s+\*%rax} } } */
> +/* { dg-final { scan-assembler-not {call\s+\*%rcx} } } */
> +/* { dg-final { scan-assembler-not {call\s+\*%rdx} } } */
> +/* { dg-final { scan-assembler-not {call\s+\*%rdi} } } */
> +
> +/* Should NOT use other registers for sibcalls.  */
> +/* { dg-final { scan-assembler-not {jmp\s+\*%rax} } } */
> +/* { dg-final { scan-assembler-not {jmp\s+\*%rcx} } } */
> +/* { dg-final { scan-assembler-not {jmp\s+\*%rdx} } } */
> +/* { dg-final { scan-assembler-not {jmp\s+\*%rdi} } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-runtime.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-runtime.c
> new file mode 100644
> index 000000000000..6ad8fab5da80
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-runtime.c
> @@ -0,0 +1,151 @@
> +/* Test KCFI runtime behavior: working calls and type mismatch trapping.
> +   { dg-do run { target native } }
> +   { dg-options "-fsanitize=kcfi" } */
> +
> +#include <stdio.h>
> +#include <signal.h>
> +#include <setjmp.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +/* Test functions with different signatures */
> +static int func_int_void(void)
> +{
> +    return 42;
> +}
> +
> +__attribute__((nocf_check))
> +static int func_int_void_nocf_check(void)
> +{
> +    return 42;
> +}
> +
> +static int func_int_int(int x)
> +{
> +    return x * 4;
> +}
> +
> +/* Global state for signal handling */
> +static volatile int trap_occurred = 0;
> +static jmp_buf trap_env;
> +
> +/* Signal handler for KCFI traps */
> +static void trap_handler(int sig)
> +{
> +    trap_occurred = 1;
> +    longjmp(trap_env, 1);
> +}
> +
> +/* Compatible indirect call should work */
> +static int test_compatible_call(void)
> +{
> +    typedef int (*int_void_ptr)(void);
> +    int_void_ptr ptr = func_int_void;
> +
> +    fprintf(stderr, "Calling %s(0x%08x) through %s(0x%08x) ...\n",
> +           __builtin_typeinfo_name(typeof(func_int_void)),
> +           __builtin_typeinfo_hash(typeof(func_int_void)),
> +           __builtin_typeinfo_name(typeof(*ptr)),
> +           __builtin_typeinfo_hash(typeof(*ptr)));
> +
> +    trap_occurred = 0;
> +    /* This should work - same signature */
> +    int result = ptr();
> +
> +    return (trap_occurred == 0 && result == 42) ? 1 : 0;
> +}
> +
> +/* Compatible indirect call to nocf_check should not work */
> +static int test_nocf_check_trap(void)
> +{
> +    trap_occurred = 0;
> +
> +    if (setjmp(trap_env) == 0) {
> +      typedef int (__attribute__((nocf_check)) *int_void_ptr_nocf)(void);
> +      int_void_ptr_nocf ptr = func_int_void_nocf_check;
> +
> +      fprintf(stderr, "Calling %s(0x%08x) through %s(0x%08x) ...\n",
> +             __builtin_typeinfo_name(typeof(func_int_void_nocf_check)),
> +             __builtin_typeinfo_hash(typeof(func_int_void_nocf_check)),
> +             __builtin_typeinfo_name(typeof(*ptr)),
> +             __builtin_typeinfo_hash(typeof(*ptr)));
> +
> +      int result = ptr();
> +
> +      /* If we get here, the trap didn't occur */
> +      return 0;
> +    } else {
> +      /* We caught the trap - this is expected */
> +      return trap_occurred;
> +    }
> +}
> +
> +/* Type mismatch should trap */
> +static int test_type_mismatch_trap(void)
> +{
> +    trap_occurred = 0;
> +
> +    if (setjmp(trap_env) == 0) {
> +      /* Cast func_int_void to incompatible void(*)(void) type */
> +      typedef void (*void_void_ptr)(void);
> +      void_void_ptr ptr = (void_void_ptr)func_int_void;
> +
> +      fprintf(stderr, "Calling %s(0x%08x) through %s(0x%08x) ...\n",
> +             __builtin_typeinfo_name(typeof(func_int_void)),
> +             __builtin_typeinfo_hash(typeof(func_int_void)),
> +             __builtin_typeinfo_name(typeof(*ptr)),
> +             __builtin_typeinfo_hash(typeof(*ptr)));
> +
> +      /* This should trap because type IDs don't match:
> +         - func_int_void has type ID for int(void)
> +         - but we're calling through void(void) pointer type */
> +      ptr();
> +
> +      /* If we get here, the trap didn't occur */
> +      return 0;
> +    } else {
> +      /* We caught the trap - this is expected */
> +      return trap_occurred;
> +    }
> +}
> +
> +int main(void)
> +{
> +    struct sigaction sa = {
> +      .sa_handler = trap_handler,
> +      .sa_flags = SA_NODEFER,
> +    };
> +    int failed = 3;
> +
> +    /* Install trap handler.  */
> +    if (sigaction(SIGILL, &sa, NULL)) {
> +      perror("sigaction");
> +      return 1;
> +    }
> +
> +    /* Compatible call should work */
> +    if (test_compatible_call()) {
> +      printf("OK: matched indirect call succeeded\n");
> +      failed--;
> +    } else {
> +      printf("FAIL\n");
> +    }
> +
> +    /* Using nocf_check should trap */
> +    if (test_nocf_check_trap()) {
> +      printf("OK: indirect call to nocf_check correctly trapped\n");
> +      failed--;
> +    } else {
> +      printf("FAIL\n");
> +    }
> +
> +    /* Type mismatch should trap */
> +    if (test_type_mismatch_trap()) {
> +      printf("OK: mismatched indirect call correctly trapped\n");
> +      failed--;
> +    } else {
> +      printf("FAIL\n");
> +    }
> +
> +    return failed;
> +}
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-tail-calls.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-tail-calls.c
> new file mode 100644
> index 000000000000..e2e3912fffa3
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-tail-calls.c
> @@ -0,0 +1,142 @@
> +/* Test KCFI protection when indirect calls get converted to tail calls.  */
> +/* { dg-do compile } */
> +/* { dg-additional-options "-O2" } */
> +
> +typedef int (*func_ptr_t)(int);
> +typedef void (*void_func_ptr_t)(void);
> +
> +struct function_table {
> +    func_ptr_t process;
> +    void_func_ptr_t cleanup;
> +};
> +
> +/* Target functions.  */
> +int process_data(int x) { return x * 2; }
> +void cleanup_data(void) {}
> +
> +/* Initialize function table.  */
> +volatile struct function_table vtable = {
> +    .process = &process_data,
> +    .cleanup = &cleanup_data
> +};
> +
> +/* Indirect call through struct member that should become tail call.  */
> +int test_struct_indirect_call(int x) {
> +    /* This is an indirect call that should be converted to tail call:
> +       Without -fno-optimize-sibling-calls should become "jmp *vtable+0(%rip)"
> +       With -fno-optimize-sibling-calls should become "call *vtable+0(%rip)"  */
> +    return vtable.process(x);
> +}
> +
> +/* Indirect call through function pointer parameter.  */
> +int test_param_indirect_call(func_ptr_t handler, int x) {
> +    /* This is an indirect call that should be converted to tail call:
> +       Without -fno-optimize-sibling-calls should become "jmp *%rdi"
> +       With -fno-optimize-sibling-calls should be "call *%rdi"  */
> +    return handler(x);
> +}
> +
> +/* Void indirect call through struct member.  */
> +void test_void_indirect_call(void) {
> +    /* This is an indirect call that should be converted to tail call:
> +     * Without -fno-optimize-sibling-calls: should become "jmp *vtable+8(%rip)"
> +     * With -fno-optimize-sibling-calls: should be "call *vtable+8(%rip)"  */
> +    vtable.cleanup();
> +}
> +
> +/* Non-tail call for comparison (should always be call).  */
> +int test_non_tail_indirect_call(func_ptr_t handler, int x) {
> +    /* This should never become a tail call - always "call *%rdi"  */
> +    int result = handler(x);
> +    return result + 1;  /* Prevents tail call optimization.  */
> +}
> +
> +/* Should have KCFI preambles for all functions.  */
> +/* { dg-final { scan-assembler-times "__cfi_process_data:" 1 } } */
> +/* { dg-final { scan-assembler-times "__cfi_cleanup_data:" 1 } } */
> +/* { dg-final { scan-assembler-times "__cfi_test_struct_indirect_call:" 1 } } */
> +/* { dg-final { scan-assembler-times "__cfi_test_param_indirect_call:" 1 } } */
> +/* { dg-final { scan-assembler-times "__cfi_test_void_indirect_call:" 1 } } */
> +/* { dg-final { scan-assembler-times "__cfi_test_non_tail_indirect_call:" 1 } } */
> +
> +/* Should have exactly 4 KCFI checks for indirect calls as
> +   (load type ID + compare).  */
> +/* { dg-final { scan-assembler-times {movl\t\$-?[0-9]+, %r10d} 4 { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-times {addl\t-4\(%r[a-z0-9]+\), %r10d} 4 { target x86_64-*-* } } } */
> +
> +/* Should have exactly 4 trap sections and 4 trap instructions.  */
> +/* { dg-final { scan-assembler-times "\\.kcfi_traps" 4 { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-times "ud2" 4 { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-times "\\.kcfi_traps" 4 { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler-times "ebreak" 4 { target riscv*-*-* } } } */
> +
> +/* Should NOT have unprotected direct jumps to vtable.  */
> +/* { dg-final { scan-assembler-not {jmp\t\*vtable\(%rip\)} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-not {jmp\t\*vtable\+8\(%rip\)} { target x86_64-*-* } } } */
> +
> +/* Should have exactly 3 protected tail calls (jmp through register after
> +   KCFI check).  */
> +/* { dg-final { scan-assembler-times {jmp\t\*%[a-z0-9]+} 3 { target x86_64-*-* } } } */
> +
> +/* Should have exactly 1 regular call (non-tail call case).  */
> +/* { dg-final { scan-assembler-times {call\t\*%[a-z0-9]+} 1 { target x86_64-*-* } } } */
> +
> +/* RISC-V: Should have exactly 4 KCFI checks for indirect calls
> +   (comparison instruction).  */
> +/* { dg-final { scan-assembler-times {beq\tt1, t2, \.Lkcfi_call[0-9]+} 4 { target riscv*-*-* } } } */
> +
> +/* RISC-V: Should have exactly 4 KCFI checks for indirect calls as
> +   (load type ID + compare).  */
> +/* { dg-final { scan-assembler-times {lw\tt1, -4\([a-z0-9]+\)} 4 { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler-times {lui\tt2, [0-9]+} 4 { target riscv*-*-* } } } */
> +
> +/* RISC-V: Should have exactly 3 protected tail calls (jr after
> +   KCFI check - no return address save).  */
> +/* { dg-final { scan-assembler-times {jalr\t(x0|zero), [a-z0-9]+, 0} 3 { target riscv*-*-* } } } */
> +
> +/* RISC-V: Should have exactly 1 regular call (non-tail call case - saves
> +   return address).  */
> +/* { dg-final { scan-assembler-times {jalr\t(x1|ra), [a-z0-9]+, 0} 1 { target riscv*-*-* } } } */
> +
> +/* Type ID loading should use lui + addiw pattern for 32-bit constants.  */
> +/* { dg-final { scan-assembler {lui\tt2, [0-9]+} { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler {addiw\tt2, t2, -?[0-9]+} { target riscv*-*-* } } } */
> +
> +/* Should have exactly 4 KCFI checks for indirect calls (load type ID from
> +   -4 offset + compare).  */
> +/* { dg-final { scan-assembler-times {ldur\tw16, \[x[0-9]+, #-4\]} 4 { target aarch64-*-* } } } */
> +/* { dg-final { scan-assembler-times {cmp\tw16, w17} 4 { target aarch64-*-* } } } */
> +
> +/* Should have exactly 4 trap instructions.  */
> +/* { dg-final { scan-assembler-times {brk\t#[0-9]+} 4 { target aarch64-*-* } } } */
> +
> +/* Should have exactly 3 protected tail calls (br through register after
> +   KCFI check).  */
> +/* { dg-final { scan-assembler-times {br\tx[0-9]+} 3 { target aarch64-*-* } } } */
> +
> +/* Should have exactly 1 regular call (non-tail call case).  */
> +/* { dg-final { scan-assembler-times {blr\tx[0-9]+} 1 { target aarch64-*-* } } } */
> +
> +/* Type ID loading should use mov + movk pattern for 32-bit constants.  */
> +/* { dg-final { scan-assembler {mov\tw17, #[0-9]+} { target aarch64-*-* } } } */
> +/* { dg-final { scan-assembler {movk\tw17, #[0-9]+, lsl #16} { target aarch64-*-* } } } */
> +
> +/* Should have exactly 4 KCFI checks for indirect calls (load type ID from
> +   -4 offset + compare).  */
> +/* { dg-final { scan-assembler-times {ldr\tr0, \[r[0-9]+, #-4\]} 4 { target arm32 } } } */
> +/* { dg-final { scan-assembler-times {cmp\tr0, r1} 4 { target arm32 } } } */
> +
> +/* Should have exactly 4 trap instructions.  */
> +/* { dg-final { scan-assembler-times {udf\t#[0-9]+} 4 { target arm32 } } } */
> +
> +/* Should have exactly 3 protected tail calls (bx through register after
> +   KCFI check).  */
> +/* { dg-final { scan-assembler-times {bx\tr[0-9]+} 3 { target arm32 } } } */
> +
> +/* Should have exactly 1 regular call (non-tail call case).  */
> +/* { dg-final { scan-assembler-times {blx\tr[0-9]+} 1 { target arm32 } } } */
> +
> +/* Type ID loading should use movw + movt pattern for 32-bit constants
> +   into r1.  */
> +/* { dg-final { scan-assembler {movw\tr1, #[0-9]+} { target arm32 } } } */
> +/* { dg-final { scan-assembler {movt\tr1, #[0-9]+} { target arm32 } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-trap-encoding.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-trap-encoding.c
> new file mode 100644
> index 000000000000..f2226fa58ac9
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-trap-encoding.c
> @@ -0,0 +1,54 @@
> +/* Test AArch64 and ARM32 KCFI trap encoding in BRK/UDF instructions.  */
> +/* { dg-do compile { target { aarch64*-*-* || arm32 } } } */
> +
> +void target_function(int x, char y) {
> +}
> +
> +int main() {
> +    void (*func_ptr)(int, char) = target_function;
> +
> +    /* This should generate trap with immediate encoding.  */
> +    func_ptr(42, 'a');
> +
> +    return 0;
> +}
> +
> +/* Should have KCFI preamble.  */
> +/* { dg-final { scan-assembler "__cfi_target_function:" } } */
> +
> +/* AArch64 specific: Should have BRK instruction with proper ESR encoding
> +   ESR format: 0x8000 | ((type_reg & 31) << 5) | (addr_reg & 31)
> +
> +   Test the ESR encoding by checking for the expected value.
> +   Since we know this test uses x2, we expect ESR = 0x8000 | (17<<5) | 2 = 33314
> +
> +   A truly dynamic test would need to extract the register from blr and compute
> +   the corresponding ESR, but DejaGnu's regex limitations make this complex.
> +   This test validates the specific case and documents the encoding.
> +   */
> +/* { dg-final { scan-assembler "blr\\s+x2" { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler "brk\\s+#33314" { target aarch64*-*-* } } } */
> +
> +/* Should have KCFI check with type comparison.  */
> +/* { dg-final { scan-assembler {ldur\t*w16, \[x[0-9]+, #-4\]} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler {cmp\t*w16, w17} { target aarch64*-*-* } } } */
> +
> +/* ARM32 specific: Should have UDF instruction with proper encoding
> +   UDF format: 0x8000 | ((type_reg & 31) << 5) | (addr_reg & 31)
> +
> +   Since ARM32 spills and restores r0/r1 before the trap, the type_reg
> +   field uses 0x1F (31) to indicate "register was spilled" rather than
> +   pointing to a live register. The addr_reg field contains the actual
> +   target register number.
> +
> +   For this test case using r3, we expect:
> +   UDF = 0x8000 | (31 << 5) | 3 = 0x8000 | 0x3E0 | 3 = 33763
> +   */
> +/* { dg-final { scan-assembler "blx\\s+r3" { target arm32 } } } */
> +/* { dg-final { scan-assembler "udf\\s+#33763" { target arm32 } } } */
> +
> +/* Should have register spilling and restoration around type check.  */
> +/* { dg-final { scan-assembler {push\t*\{r0, r1\}} { target arm32 } } } */
> +/* { dg-final { scan-assembler {pop\t*\{r0, r1\}} { target arm32 } } } */
> +/* { dg-final { scan-assembler {ldr\t*r0, \[r[0-9]+, #-4\]} { target arm32 } } } */
> +/* { dg-final { scan-assembler {cmp\t*r0, r1} { target arm32 } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi-trap-section.c b/gcc/testsuite/gcc.dg/kcfi/kcfi-trap-section.c
> new file mode 100644
> index 000000000000..7f5f8a82f3dc
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi-trap-section.c
> @@ -0,0 +1,41 @@
> +/* Test KCFI trap section generation.  */
> +/* { dg-do compile } */
> +
> +void target_function(void) {}
> +
> +int main() {
> +    void (*func_ptr)(void) = target_function;
> +
> +    /* Multiple indirect calls to generate multiple trap entries.  */
> +    func_ptr();
> +    func_ptr();
> +
> +    return 0;
> +}
> +
> +/* Should have KCFI preamble.  */
> +/* { dg-final { scan-assembler "__cfi_target_function:" } } */
> +
> +/* Should have exactly 2 trap labels in code.  */
> +/* { dg-final { scan-assembler-times {\.L[^:]+:\n\s*ud2} 2 { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-times {\.L[^:]+:\n\s*brk} 2 { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler-times {\.L[^:]+:\n\s*udf} 2 { target arm32 } } } */
> +/* { dg-final { scan-assembler-times {\.L[^:]+:\n\s*ebreak} 2 { target riscv*-*-* } } } */
> +
> +/* x86_64: Should have complete .kcfi_traps section sequence with relative
> +   offset and 2 entries.  */
> +/* { dg-final { scan-assembler {\.section\t\.kcfi_traps,"ao",@progbits,\.text\n\.Lkcfi_entry([^:]+):\n\t\.long\t\.Lkcfi_trap([^\s\n]+)-\.Lkcfi_entry\1\n\t\.text} { target x86_64-*-* } } } */
> +/* { dg-final { scan-assembler-times {\.section\t\.kcfi_traps,"ao",@progbits,\.text} 2 { target x86_64-*-* } } } */
> +
> +/* AArch64 should NOT have .kcfi_traps section (uses brk immediate instead) */
> +/* { dg-final { scan-assembler-not {\.section\t+\.kcfi_traps} { target aarch64*-*-* } } } */
> +/* { dg-final { scan-assembler-not {\.long.*-\.L} { target aarch64*-*-* } } } */
> +
> +/* ARM 32-bit should NOT have .kcfi_traps section (uses udf immediate instead) */
> +/* { dg-final { scan-assembler-not {\.section\t+\.kcfi_traps} { target arm32 } } } */
> +/* { dg-final { scan-assembler-not {\.long.*-\.L} { target arm32 } } } */
> +
> +/* RISC-V: Should have complete .kcfi_traps section sequence with relative
> +   offset and 2 entries.  */
> +/* { dg-final { scan-assembler {\.section\t\.kcfi_traps,"ao",@progbits,\.text\n\.Lkcfi_entry([^:]+):\n\t\.4byte\t\.L([^\s\n]+)-\.Lkcfi_entry\1\n\t\.text} { target riscv*-*-* } } } */
> +/* { dg-final { scan-assembler-times {\.section\t\.kcfi_traps,"ao",@progbits,\.text} 2 { target riscv*-*-* } } } */
> diff --git a/gcc/testsuite/gcc.dg/kcfi/kcfi.exp b/gcc/testsuite/gcc.dg/kcfi/kcfi.exp
> new file mode 100644
> index 000000000000..0bbba196c82f
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/kcfi/kcfi.exp
> @@ -0,0 +1,64 @@
> +#   Copyright (C) 2025 Free Software Foundation, Inc.
> +
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 3 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with GCC; see the file COPYING3.  If not see
> +# <http://www.gnu.org/licenses/>.
> +
> +# GCC testsuite for KCFI (Kernel Control Flow Integrity) tests.
> +
> +# Load support procs.
> +load_lib gcc-dg.exp
> +
> +# KCFI is only supported on specific targets
> +if { ![istarget "x86_64-*-*"] \
> +     && ![istarget "aarch64-*-*"] && ![istarget "arm*-*-*"] \
> +     && ![istarget "riscv*-*-*"] } {
> +    return
> +}
> +
> +# Skip tests if x86_64 is running in 32-bit mode (-m32)
> +if { [istarget "x86_64-*-*"] && ![check_effective_target_lp64] } {
> +    return
> +}

This is also wrong, you can have a multi-lib version of i?86 which
support -m64 too.


> +
> +# Skip tests if AArch64 is running in ILP32 mode (-mabi=ilp32)
> +if { [istarget "aarch64-*-*"] && ![check_effective_target_lp64] } {
> +    return
> +}
> +
> +# Skip tests if RISC-V is running in 32-bit mode (riscv32-*)
> +if { [istarget "riscv*-*-*"] && ![check_effective_target_lp64] } {
> +    return
> +}

These should really be part a new check_effective_target_kfci instead
of embeded here.
And then you can just call that from here.

Does kcfi work on non-elf targets, e.g. aarch64-mingw or x86_64-mingw
or x86_64-darwin?  I see you check lp64 and that would reject the
mingw case but not the darwin case.

Thanks,
Andrew


> +
> +# Add KCFI-specific flags to any existing DEFAULT_CFLAGS
> +global DEFAULT_CFLAGS
> +if ![info exists DEFAULT_CFLAGS] then {
> +    set DEFAULT_CFLAGS ""
> +}
> +set DEFAULT_CFLAGS "$DEFAULT_CFLAGS -fsanitize=kcfi"
> +
> +# Add ARM32-specific flags for arm32 targets
> +if [check_effective_target_arm32] {
> +    set DEFAULT_CFLAGS "$DEFAULT_CFLAGS -march=armv7-a -mfloat-abi=soft"

I think the above is incorrect can you explain why you want to change
this? Especially since you might not have soft fp ABI compatiable
glibc installed?
Also it is either arm or aarch32 and not arm32.

Thanks
Andrew


> +}
> +
> +# Initialize `dg'.
> +dg-init
> +
> +# Main loop.
> +dg-runtest [lsort [glob -nocomplain $srcdir/$subdir/*.c]] \
> +       "" $DEFAULT_CFLAGS
> +
> +# All done.
> +dg-finish
> --
> 2.34.1
>

Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ