[PATCH 5/6] objtool: Allow EXPORT_SYMBOL() of __noendbr functions

From: Peter Zijlstra

Date: Mon Nov 02 2026 - 06:42:07 EST


Objtool IBT validation does two things:

- it checks that all text references have an ENDBR instruction (WARN);
- it finds all ENDBR instructions that have no references (SEAL).

The first ensures that all function pointers are of functions that have ENDBR,
since doing indirect calls requires this. The flip side is that you can have
text references that are not function pointers in the sense of later calling
them. Eg. ip_within_syscall_gap() will take the address of symbols, but will
never call them. Any such location that is referenced but does not have ENDBR
requires an ANNOTATE_NOENDBR.

The second is used to create a list of ENDBR instructions to SEAL. That is, at
boot time the kernel will replace all these ENDBR instructions with a NOP
instruction, limiting indirect targets.

Now, EXPORT_SYMBOL*(sym) is counted as a reference. This is important because
even if the kernel itself does not reference the symbol, a module still might.
The flip side is that exporting a __noendbr function needs an ANNOTATE_NOENDBR.

Relax this by supressing the objtool-WARN for .export_symbol references. This
allows exporting of __noendbr without further ANNOTATE_NOENDBR. Remove any such
now redudant annotation.

Reported-by: kernel test robot <lkp@xxxxxxxxx>
Signed-off-by: Peter Zijlstra (Intel) <peterz@xxxxxxxxxxxxx>
---
arch/x86/entry/calling.h | 1 -
arch/x86/entry/entry_64.S | 2 +-
arch/x86/entry/entry_64_compat.S | 6 +++---
arch/x86/lib/clear_page_64.S | 1 -
arch/x86/lib/copy_user_64.S | 1 -
arch/x86/lib/copy_user_uncached_64.S | 1 -
arch/x86/lib/getuser.S | 4 ----
arch/x86/lib/hweight.S | 3 ---
arch/x86/lib/putuser.S | 4 ----
tools/objtool/check.c | 15 ++++++++++++---
10 files changed, 16 insertions(+), 22 deletions(-)

--- a/arch/x86/entry/calling.h
+++ b/arch/x86/entry/calling.h
@@ -432,7 +432,6 @@ For 32-bit we have the following convent
/* rdi: arg1 ... normal C conventions. rax is saved/restored. */
.macro THUNK name, func
SYM_FUNC_START(\name)
- ANNOTATE_NOENDBR
pushq %rbp
movq %rsp, %rbp

--- a/arch/x86/entry/entry_64.S
+++ b/arch/x86/entry/entry_64.S
@@ -95,7 +95,7 @@ SYM_CODE_START(entry_SYSCALL_64)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
- ANNOTATE_NOENDBR
+ ANNOTATE_NOENDBR // ip_within_syscall_gap

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
--- a/arch/x86/entry/entry_64_compat.S
+++ b/arch/x86/entry/entry_64_compat.S
@@ -196,7 +196,7 @@ SYM_CODE_START(entry_SYSCALL_compat)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

SYM_INNER_LABEL(entry_SYSCALL_compat_safe_stack, SYM_L_GLOBAL)
- ANNOTATE_NOENDBR
+ ANNOTATE_NOENDBR // ip_within_syscall_gap

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
@@ -260,7 +260,7 @@ SYM_INNER_LABEL(entry_SYSCALL_compat_aft
*/
movq RSP-ORIG_RAX(%rsp), %rsp
SYM_INNER_LABEL(entry_SYSRETL_compat_unsafe_stack, SYM_L_GLOBAL)
- ANNOTATE_NOENDBR
+ ANNOTATE_NOENDBR // ip_within_syscall_gap

/*
* The original userspace %rsp (RSP-ORIG_RAX(%rsp)) is stored
@@ -280,7 +280,7 @@ SYM_INNER_LABEL(entry_SYSRETL_compat_uns
CLEAR_CPU_BUFFERS
sysretl
SYM_INNER_LABEL(entry_SYSRETL_compat_end, SYM_L_GLOBAL)
- ANNOTATE_NOENDBR
+ ANNOTATE_NOENDBR // ip_within_syscall_gap
int3
SYM_CODE_END(entry_SYSCALL_compat)

--- a/arch/x86/lib/clear_page_64.S
+++ b/arch/x86/lib/clear_page_64.S
@@ -42,7 +42,6 @@ EXPORT_SYMBOL_GPL(__clear_pages_unrolled
* rcx: uncleared bytes or 0 if successful.
*/
SYM_FUNC_START(rep_stos_alternative)
- ANNOTATE_NOENDBR
cmpq $64,%rcx
jae .Lunrolled

--- a/arch/x86/lib/copy_user_64.S
+++ b/arch/x86/lib/copy_user_64.S
@@ -32,7 +32,6 @@
* it simpler for us, we can clobber rsi/rdi and rax freely.
*/
SYM_FUNC_START(rep_movs_alternative)
- ANNOTATE_NOENDBR
cmpq $64,%rcx
jae .Llarge

--- a/arch/x86/lib/copy_user_uncached_64.S
+++ b/arch/x86/lib/copy_user_uncached_64.S
@@ -28,7 +28,6 @@
* rax uncopied bytes or 0 if successful.
*/
SYM_FUNC_START(copy_to_nontemporal)
- ANNOTATE_NOENDBR
/* If destination is not 7-byte aligned, we'll have to align it */
testb $7,%dil
jne .Lalign
--- a/arch/x86/lib/getuser.S
+++ b/arch/x86/lib/getuser.S
@@ -58,7 +58,6 @@

.text
SYM_FUNC_START(__get_user_1)
- ANNOTATE_NOENDBR
check_range size=1
ASM_STAC
UACCESS movzbl (%_ASM_AX),%edx
@@ -69,7 +68,6 @@ SYM_FUNC_END(__get_user_1)
EXPORT_SYMBOL(__get_user_1)

SYM_FUNC_START(__get_user_2)
- ANNOTATE_NOENDBR
check_range size=2
ASM_STAC
UACCESS movzwl (%_ASM_AX),%edx
@@ -80,7 +78,6 @@ SYM_FUNC_END(__get_user_2)
EXPORT_SYMBOL(__get_user_2)

SYM_FUNC_START(__get_user_4)
- ANNOTATE_NOENDBR
check_range size=4
ASM_STAC
UACCESS movl (%_ASM_AX),%edx
@@ -91,7 +88,6 @@ SYM_FUNC_END(__get_user_4)
EXPORT_SYMBOL(__get_user_4)

SYM_FUNC_START(__get_user_8)
- ANNOTATE_NOENDBR
#ifndef CONFIG_X86_64
xor %ecx,%ecx
#endif
--- a/arch/x86/lib/hweight.S
+++ b/arch/x86/lib/hweight.S
@@ -10,8 +10,6 @@
* %rdi: w
*/
SYM_FUNC_START(__sw_hweight32)
- ANNOTATE_NOENDBR
-
#ifdef CONFIG_X86_64
movl %edi, %eax # w
#endif
@@ -44,7 +42,6 @@ EXPORT_SYMBOL(__sw_hweight32)
*/
#ifdef CONFIG_X86_64
SYM_FUNC_START(__sw_hweight64)
- ANNOTATE_NOENDBR
pushq %rdi
pushq %rdx

--- a/arch/x86/lib/putuser.S
+++ b/arch/x86/lib/putuser.S
@@ -46,7 +46,6 @@

.text
SYM_FUNC_START(__put_user_1)
- ANNOTATE_NOENDBR
check_range size=1
ASM_STAC
1: movb %al,(%_ASM_CX)
@@ -57,7 +56,6 @@ SYM_FUNC_END(__put_user_1)
EXPORT_SYMBOL(__put_user_1)

SYM_FUNC_START(__put_user_2)
- ANNOTATE_NOENDBR
check_range size=2
ASM_STAC
2: movw %ax,(%_ASM_CX)
@@ -68,7 +66,6 @@ SYM_FUNC_END(__put_user_2)
EXPORT_SYMBOL(__put_user_2)

SYM_FUNC_START(__put_user_4)
- ANNOTATE_NOENDBR
check_range size=4
ASM_STAC
3: movl %eax,(%_ASM_CX)
@@ -79,7 +76,6 @@ SYM_FUNC_END(__put_user_4)
EXPORT_SYMBOL(__put_user_4)

SYM_FUNC_START(__put_user_8)
- ANNOTATE_NOENDBR
check_range size=8
ASM_STAC
4: mov %_ASM_AX,(%_ASM_CX)
--- a/tools/objtool/check.c
+++ b/tools/objtool/check.c
@@ -4597,7 +4597,7 @@ static int validate_ibt_insn(struct objt
}

static int validate_ibt_data_reloc(struct objtool_file *file,
- struct reloc *reloc)
+ struct reloc *reloc, bool warn)
{
struct instruction *dest;

@@ -4611,7 +4611,7 @@ static int validate_ibt_data_reloc(struc
return 0;
}

- if (dest->noendbr)
+ if (dest->noendbr || !warn)
return 0;

WARN_FUNC(reloc->sec->base, reloc_offset(reloc),
@@ -4636,6 +4636,7 @@ static int validate_ibt(struct objtool_f
warnings += validate_ibt_insn(file, insn);

for_each_sec(file->elf, sec) {
+ bool warn = true;

/* Already done by validate_ibt_insn() */
if (is_text_sec(sec))
@@ -4672,6 +4673,14 @@ static int validate_ibt(struct objtool_f
!strcmp(sec->name, "__patchable_function_entries"))
continue;

+ /*
+ * EXPORT_SYMBOL() must be counted towards an ENDBR usage,
+ * such symbols must not be implicitly sealed. But it is fine
+ * to EXPORT an explicitly (__noendbr) sealed function.
+ */
+ if (!strcmp(sec->name, ".export_symbol"))
+ warn = false;
+
for_each_reloc(sec->rsec, reloc) {
struct symbol *sym = find_symbol_containing(reloc->sec->base,
reloc_offset(reloc));
@@ -4679,7 +4688,7 @@ static int validate_ibt(struct objtool_f
if (sym && !strcmp(sym->name, "sys_call_table"))
continue;

- warnings += validate_ibt_data_reloc(file, reloc);
+ warnings += validate_ibt_data_reloc(file, reloc, warn);
}
}