[tip: objtool/core] objtool/klp: Add test for empty x86 alternative replacements

From: tip-bot2 for Song Liu

Date: Fri Sep 18 2026 - 06:29:01 EST


The following commit has been merged into the objtool/core branch of tip:

Commit-ID: a249e6986f4cde3c88a09cdd44a7d88d44aea730
Gitweb: https://git.kernel.org/tip/a249e6986f4cde3c88a09cdd44a7d88d44aea730
Author: Song Liu <song@xxxxxxxxxx>
AuthorDate: Wed, 16 Sep 2026 11:43:27 -07:00
Committer: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
CommitterDate: Wed, 16 Sep 2026 17:13:29 -07:00

objtool/klp: Add test for empty x86 alternative replacements

ALTERNATIVE_2("orig", "repl", ft1, "", ft2) produces a second entry whose
replacement is empty. Its replacement offset still carries a relocation,
but the label it points at is the end of the previous replacement -- which
is also where the next one begins. The value is meaningless and only ever
used with a length of zero, so cloning must not follow it.

The first version of this fixture passed with the fix reverted, because the
empty entry pointed at its own end label rather than at the neighbour's
replacement. It has to reach into another function's replacement to
distinguish the behaviour.

This tests the behavior of commit 636f230ce21e ("objtool/klp: Ignore
replacement offset of empty x86 alternatives").

Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence <joe.lawrence@xxxxxxxxxx>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@xxxxxxxxxx>
Link: https://patch.msgid.link/20260916184351.2720310-35-song@xxxxxxxxxx
Signed-off-by: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
---
tools/objtool/tests/x86/fixtures/empty_alternative.c | 77 +++++++++++-
tools/objtool/tests/x86/test-empty-alternative.sh | 31 ++++-
2 files changed, 108 insertions(+)
create mode 100644 tools/objtool/tests/x86/fixtures/empty_alternative.c
create mode 100755 tools/objtool/tests/x86/test-empty-alternative.sh

diff --git a/tools/objtool/tests/x86/fixtures/empty_alternative.c b/tools/objtool/tests/x86/fixtures/empty_alternative.c
new file mode 100644
index 0000000..9336d74
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/empty_alternative.c
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An x86 alternative with an empty replacement, as the second entry of
+ * ALTERNATIVE_2("orig", "repl", ft1, "", ft2) produces. Its replacement
+ * offset still gets a relocation, but the label it points at is the end of the
+ * previous replacement, which is also where the *next* one begins -- here,
+ * neighbor()'s. The value is meaningless; it is only ever used with a length
+ * of zero.
+ *
+ * struct alt_instr is written out by hand so the fixture builds without kernel
+ * headers: s32 instr_offset, s32 repl_offset, u32 ft_flags, u8 instrlen,
+ * u8 replacementlen. The section carries an entsize because klp diff needs
+ * either that or an ANNOTATE_DATA_SPECIAL annotation to find entry boundaries.
+ *
+ * The replacement labels are global so the relocations name them rather than
+ * .altinstr_replacement plus an addend.
+ */
+
+static const char __modinfo[]
+ __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux";
+
+extern int neighbor_only(int x);
+
+int target(int x)
+{
+ asm volatile(
+ "661: nop\n\t"
+ ".pushsection .altinstr_replacement, \"ax\"\n\t"
+ ".globl target_repl\n\t"
+ "target_repl:\n\t"
+ " nop\n\t"
+ "target_repl_end:\n\t"
+ ".popsection\n\t"
+ ".pushsection .altinstructions, \"aM\", @progbits, 14\n\t"
+ /* a real replacement */
+ ".long 661b - .\n\t"
+ ".long target_repl - .\n\t"
+ ".long 0\n\t"
+ ".byte 1\n\t"
+ ".byte target_repl_end - target_repl\n\t"
+ /* an empty one, pointing at neighbor()'s replacement */
+ ".long 661b - .\n\t"
+ ".long neighbor_repl - .\n\t"
+ ".long 0\n\t"
+ ".byte 1\n\t"
+ ".byte 0\n\t"
+ ".popsection\n\t");
+#ifdef PATCHED
+ return x + 2;
+#else
+ return x + 1;
+#endif
+}
+
+/*
+ * Unrelated, unpatched, and referencing a symbol nothing else does, so that
+ * dragging its replacement in is visible.
+ */
+int neighbor(int x)
+{
+ asm volatile(
+ "771: nop\n\t"
+ ".pushsection .altinstr_replacement, \"ax\"\n\t"
+ ".globl neighbor_repl\n\t"
+ "neighbor_repl:\n\t"
+ " call neighbor_only\n\t"
+ "neighbor_repl_end:\n\t"
+ ".popsection\n\t"
+ ".pushsection .altinstructions, \"aM\", @progbits, 14\n\t"
+ ".long 771b - .\n\t"
+ ".long neighbor_repl - .\n\t"
+ ".long 0\n\t"
+ ".byte 1\n\t"
+ ".byte neighbor_repl_end - neighbor_repl\n\t"
+ ".popsection\n\t");
+ return x;
+}
diff --git a/tools/objtool/tests/x86/test-empty-alternative.sh b/tools/objtool/tests/x86/test-empty-alternative.sh
new file mode 100755
index 0000000..9d40c3a
--- /dev/null
+++ b/tools/objtool/tests/x86/test-empty-alternative.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# An x86 alternative with an empty replacement still gets a relocation for its
+# replacement offset, but the label it points at is the end of the previous
+# replacement -- which is also the start of the next one. The value is
+# meaningless, and get_alt_entry() already ignores it.
+#
+# Cloning it drags in an unrelated neighboring replacement and everything that
+# replacement references. In the reported case an empty alternative in
+# meminfo_proc_show() pulled in one from proc_kcore_init(), emitting a klp
+# relocation against init text which is long freed by the time the patch is
+# applied.
+
+. "$(dirname "$0")/../lib.sh"
+
+setup
+build_pair empty_alternative.c
+
+assert_input_section .altinstructions
+assert_input_section .altinstr_replacement
+
+run_diff
+
+# target's own replacement comes along ...
+assert_symbol target_repl
+# ... neighbor's does not, nor what it references.
+assert_no_symbol neighbor_repl
+assert_no_symbol neighbor_only
+
+pass "empty alternative's replacement offset ignored when cloning"