summaryrefslogtreecommitdiff
path: root/tools/objtool/tests/x86
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-20 12:55:04 -1000
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-20 12:55:04 -1000
commit5b6dca02b3e2254d2fd5706d76d7ab4610a67296 (patch)
tree443bf1de39c37775248ae83c2f287c09fe15f749 /tools/objtool/tests/x86
parent776bb6df3f16ab68c40a97dbb8af26af4185d7c0 (diff)
parent945537271df1e3680be9b32af7041ac20f1184fb (diff)
Merge tag 'objtool-core-2026-10-18' of git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip
Pull objtool updates from Ingo Molnar: - Add unit test suite for objtool klp functionality and known failure modes (by Song Liu and Puranjay Mohan, with fixes by Joe Lawrence) * tag 'objtool-core-2026-10-18' of git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip: (58 commits) objtool/klp: Add test for ThinLTO symbols sharing a demangled name objtool/klp: Add test for Clang switch jump tables objtool/klp: Add test for UBSAN metadata in an unchanged function objtool/klp: Add test for absolute and __ADDRESSABLE symbols objtool/klp: Add test for a patch which strips a data annotation objtool/klp: Add test for the alignment of cloned data sections objtool/klp: Add test for alternative replacement code in checksums objtool/klp: Add test for instruction operand checksums objtool/klp: Add test for a static call introduced by the patch objtool/klp: Add test for tracepoint and pr_debug static branch keys objtool/klp: Add test for a static branch introduced by the patch objtool/klp: Add test for symbols with no checksum entry of their own objtool/klp: Add test for data object checksums objtool/klp: Test text annotations on alternative replacements objtool/klp: Test a hand-built livepatch module's static call keys objtool/klp: Test rejection of a file-local static branch key objtool/klp: Add test for symbols whose linkage the patch changes objtool/klp: Add test for kCFI prefix symbols and traps objtool/klp: Add test for __bug_table, __ex_table and __mcount_loc extraction objtool/klp: Add test for static locals which must not be correlated ...
Diffstat (limited to 'tools/objtool/tests/x86')
-rw-r--r--tools/objtool/tests/x86/fixtures/alt_annotate.c57
-rw-r--r--tools/objtool/tests/x86/fixtures/checksum_alt.c66
-rw-r--r--tools/objtool/tests/x86/fixtures/empty_alternative.c77
-rw-r--r--tools/objtool/tests/x86/fixtures/kcfi.c39
-rw-r--r--tools/objtool/tests/x86/fixtures/special_sections.c77
-rw-r--r--tools/objtool/tests/x86/fixtures/static_call_no_key.c32
-rwxr-xr-xtools/objtool/tests/x86/test-alt-annotation.sh38
-rwxr-xr-xtools/objtool/tests/x86/test-checksum-alt.sh45
-rwxr-xr-xtools/objtool/tests/x86/test-empty-alternative.sh31
-rwxr-xr-xtools/objtool/tests/x86/test-kcfi.sh39
-rwxr-xr-xtools/objtool/tests/x86/test-manual-klp-static-call.sh40
-rwxr-xr-xtools/objtool/tests/x86/test-special-sections.sh42
12 files changed, 583 insertions, 0 deletions
diff --git a/tools/objtool/tests/x86/fixtures/alt_annotate.c b/tools/objtool/tests/x86/fixtures/alt_annotate.c
new file mode 100644
index 000000000000..af44d320dcb0
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/alt_annotate.c
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An x86 alternative whose replacement instruction carries a text annotation.
+ *
+ * The kernel does this wherever an ALTERNATIVE contains something objtool has
+ * to be told about -- a retpoline-safe indirect branch, an intentionally
+ * missing ENDBR -- so the .discard.annotate_insn entry references an address
+ * inside .altinstr_replacement rather than inside a function.
+ *
+ * Two things make that awkward for klp diff, and both are why this fixture
+ * exists. Replacement code has no real symbol: objtool invents a NOTYPE fake
+ * symbol for it, so an annotation pointing there does not reference a FUNC.
+ * And .discard.annotate_insn has to be cloned after .altinstructions, or the
+ * replacement it names has no clone to point at yet.
+ *
+ * struct alt_instr is written out by hand as in empty_alternative.c: s32
+ * instr_offset, s32 repl_offset, u32 ft_flags, u8 instrlen, u8 replacementlen,
+ * with an entsize so klp diff can find the entry boundaries.
+ * .discard.annotate_insn entries are s32 offset, s32 type; type 2 is
+ * ANNOTYPE_RETPOLINE_SAFE.
+ *
+ * The replacement label is global so the relocations name it rather than
+ * .altinstr_replacement plus an addend, which klp diff cannot convert. It is
+ * still NOTYPE, which is the shape that matters here.
+ */
+
+static const char __modinfo[]
+ __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux";
+
+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"
+ /* The annotation lands inside the replacement. */
+ ".pushsection .discard.annotate_insn, \"M\", @progbits, 8\n\t"
+ ".long target_repl - .\n\t"
+ ".long 2\n\t"
+ ".popsection\n\t"
+ "target_repl_end:\n\t"
+ ".popsection\n\t"
+ ".pushsection .altinstructions, \"aM\", @progbits, 14\n\t"
+ ".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"
+ ".popsection\n\t");
+#ifdef PATCHED
+ return x + 2;
+#else
+ return x + 1;
+#endif
+}
diff --git a/tools/objtool/tests/x86/fixtures/checksum_alt.c b/tools/objtool/tests/x86/fixtures/checksum_alt.c
new file mode 100644
index 000000000000..ed342d94eb9e
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/checksum_alt.c
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An x86 alternative whose replacement code is part of the patched function's
+ * checksum.
+ *
+ * checksum_update_insn() walks insn->alts after hashing the instruction
+ * itself, hashing the alternative's type and, when the replacement forms a
+ * group, its feature number and every instruction in it. So editing only the
+ * replacement -- code the CPU may or may not ever run -- has to move the
+ * function's checksum.
+ *
+ * It is reached through objtool's own alternative handling, so the object has
+ * to go through the check pass first: insn->alts is built there, not by the
+ * compiler.
+ *
+ * struct alt_instr is written out by hand as in empty_alternative.c: s32
+ * instr_offset, s32 repl_offset, u32 ft_flags, u8 instrlen, u8 replacementlen.
+ *
+ * Variants, applied to the patched build only:
+ *
+ * ALT_REPL the replacement instruction changes; the original does not
+ * ALT_FEATURE the feature number changes; no code changes at all
+ */
+
+static const char __modinfo[]
+ __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux";
+
+/*
+ * Both spellings are two bytes, because a replacement may not be longer than
+ * the instruction it replaces: "xchg %ax, %ax" is 66 90 and two nops are
+ * 90 90. The original below is padded to match.
+ */
+#if defined(PATCHED) && defined(ALT_REPL)
+#define REPL_INSN " nop\n\t nop\n\t"
+#else
+#define REPL_INSN " xchg %ax, %ax\n\t"
+#endif
+
+#if defined(PATCHED) && defined(ALT_FEATURE)
+#define FEATURE "7"
+#else
+#define FEATURE "3"
+#endif
+
+int target(int x)
+{
+ asm volatile(
+ "661: nop\n\t"
+ " nop\n\t"
+ "662:\n\t"
+ ".pushsection .altinstr_replacement, \"ax\"\n\t"
+ ".globl target_repl\n\t"
+ "target_repl:\n\t"
+ REPL_INSN
+ "target_repl_end:\n\t"
+ ".popsection\n\t"
+ ".pushsection .altinstructions, \"aM\", @progbits, 14\n\t"
+ ".long 661b - .\n\t"
+ ".long target_repl - .\n\t"
+ ".long " FEATURE "\n\t"
+ ".byte 662b - 661b\n\t"
+ ".byte target_repl_end - target_repl\n\t"
+ ".popsection\n\t");
+
+ return x + 1;
+}
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 000000000000..9336d74bfa92
--- /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/fixtures/kcfi.c b/tools/objtool/tests/x86/fixtures/kcfi.c
new file mode 100644
index 000000000000..b62fc60634cf
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/kcfi.c
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An indirect call, which under kCFI is preceded by a type check and a trap.
+ *
+ * Clang emits a __cfi_<func> prefix symbol carrying the type hash ahead of
+ * every address-taken function, and records the trap site in .kcfi_traps.
+ * Both belong to the function and both have to come with it into a patch.
+ *
+ * Needs -fsanitize=kcfi, which only Clang has.
+ */
+
+static const char __modinfo[]
+ __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux";
+
+static int impl_a(int x)
+{
+ return x + 1;
+}
+
+static int impl_b(int x)
+{
+ return x * 2;
+}
+
+__attribute__((noinline)) int (*pick(int x))(int)
+{
+ return (x & 1) ? impl_a : impl_b;
+}
+
+int target(int x)
+{
+ int (*fn)(int arg) = pick(x);
+
+#ifdef PATCHED
+ return fn(x) + 2;
+#else
+ return fn(x) + 1;
+#endif
+}
diff --git a/tools/objtool/tests/x86/fixtures/special_sections.c b/tools/objtool/tests/x86/fixtures/special_sections.c
new file mode 100644
index 000000000000..d42798848fda
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/special_sections.c
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A special section entry belonging to a patched function. SPECIAL_SEC picks
+ * which section, since klp diff treats eight of them alike and each needs
+ * extracting for the patched function and no other.
+ *
+ * The entry is written out by hand so the fixture builds without kernel
+ * headers. Only the leading relocation matters to klp diff; the rest is
+ * padded to the section's real entry size, because the entries have to be the
+ * right length for the boundaries between them to fall in the right places.
+ *
+ * SPECIAL_RELOCS covers __ex_table, whose entries relocate both the faulting
+ * instruction and its fixup; objtool rejects one with only the first.
+ */
+
+#ifndef SPECIAL_SEC
+#define SPECIAL_SEC "__bug_table"
+#endif
+#ifndef SPECIAL_ENTSIZE
+#define SPECIAL_ENTSIZE 12
+#endif
+#ifndef SPECIAL_RELOCS
+#define SPECIAL_RELOCS 1
+#endif
+
+#define STR_(x) #x
+#define STR(x) STR_(x)
+
+static const char __modinfo[]
+ __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux";
+
+/*
+ * other() gets an entry of its own, so that "the untouched function's entry
+ * was not dragged in" is a question the section can actually answer. With
+ * only target() contributing, there is nothing for klp diff to leave behind
+ * and the negative assertion holds however the extraction behaves.
+ */
+int other(int x)
+{
+ asm volatile(
+ "3: nop\n\t"
+ "4:\n\t"
+ ".pushsection " SPECIAL_SEC ", \"aM\", @progbits, "
+ STR(SPECIAL_ENTSIZE) "\n\t"
+ ".long 3b - .\n\t"
+#if SPECIAL_RELOCS > 1
+ ".long 4b - .\n\t"
+ ".fill " STR(SPECIAL_ENTSIZE) " - 8, 1, 0\n\t"
+#else
+ ".fill " STR(SPECIAL_ENTSIZE) " - 4, 1, 0\n\t"
+#endif
+ ".popsection\n\t");
+
+ return x + 9;
+}
+
+int target(int x)
+{
+ asm volatile(
+ "1: nop\n\t"
+ "2:\n\t"
+ ".pushsection " SPECIAL_SEC ", \"aM\", @progbits, "
+ STR(SPECIAL_ENTSIZE) "\n\t"
+ ".long 1b - .\n\t"
+#if SPECIAL_RELOCS > 1
+ ".long 2b - .\n\t"
+ ".fill " STR(SPECIAL_ENTSIZE) " - 8, 1, 0\n\t"
+#else
+ ".fill " STR(SPECIAL_ENTSIZE) " - 4, 1, 0\n\t"
+#endif
+ ".popsection\n\t");
+#ifdef PATCHED
+ return x + 2;
+#else
+ return x + 1;
+#endif
+}
diff --git a/tools/objtool/tests/x86/fixtures/static_call_no_key.c b/tools/objtool/tests/x86/fixtures/static_call_no_key.c
new file mode 100644
index 000000000000..748ba7c0f86d
--- /dev/null
+++ b/tools/objtool/tests/x86/fixtures/static_call_no_key.c
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A static call to a trampoline whose key symbol this object cannot see.
+ *
+ * That is the normal situation for a module: __SCK__* keys are not exported,
+ * and read-only access is granted at load time instead. objtool's static call
+ * handling has to accept it for any module, including a livepatch module built
+ * by hand rather than by klp-build.
+ *
+ * LIVEPATCH adds the .modinfo tag which makes objtool treat this as a
+ * livepatch module.
+ */
+
+static const char __modinfo[]
+ __attribute__((section(".modinfo"), used, aligned(1))) =
+#ifdef LIVEPATCH
+ "\0livepatch=Y"
+#endif
+ "\0name=klp_testmod";
+
+/*
+ * The trampoline is undefined here, exactly as it is for a module calling a
+ * static call defined in vmlinux. No __SCK__klp_test_call accompanies it.
+ */
+extern void __SCT__klp_test_call(void);
+
+int target(int x)
+{
+ __asm__ volatile("call __SCT__klp_test_call\n\t" ::: "memory");
+
+ return x + 1;
+}
diff --git a/tools/objtool/tests/x86/test-alt-annotation.sh b/tools/objtool/tests/x86/test-alt-annotation.sh
new file mode 100755
index 000000000000..96760e6df9e5
--- /dev/null
+++ b/tools/objtool/tests/x86/test-alt-annotation.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# A text annotation on an instruction inside an alternative's replacement must
+# be carried into the patch.
+#
+# The kernel annotates replacement code wherever objtool has to be told
+# something about it -- a retpoline-safe indirect branch, a deliberately absent
+# ENDBR. Two things made klp diff drop those annotations:
+#
+# - replacement code has no real symbol, so objtool invents a NOTYPE fake
+# one, and the extraction only kept references to FUNC symbols;
+# - .discard.annotate_insn was processed before .altinstructions, so the
+# replacement it referenced had no clone to point at yet.
+#
+# Nothing fails at build time when the annotation goes missing. It surfaces
+# later as objtool warning about, or rejecting, the patched code it was there
+# to explain.
+#
+# Fixed by 62a7a01fde87 ("objtool/klp: Fix extraction of text annotations for
+# alternatives").
+
+. "$(dirname "$0")/../lib.sh"
+
+setup
+build_pair alt_annotate.c
+
+assert_input_section .altinstructions
+assert_input_section .discard.annotate_insn
+
+run_diff
+
+# The annotation has to survive, and to still name the replacement. Checking
+# only the section would pass on an entry whose relocation was dropped.
+assert_section .discard.annotate_insn
+assert_reloc_sym .discard.annotate_insn target_repl
+
+pass "text annotation on an alternative replacement carried into the patch"
diff --git a/tools/objtool/tests/x86/test-checksum-alt.sh b/tools/objtool/tests/x86/test-checksum-alt.sh
new file mode 100755
index 000000000000..74ebfca2b3ff
--- /dev/null
+++ b/tools/objtool/tests/x86/test-checksum-alt.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# An alternative's replacement code counts towards the checksum of the function
+# it belongs to.
+#
+# checksum_update_insn() walks insn->alts after hashing the instruction itself:
+# the alternative's type, and where the replacement forms a group, its feature
+# number and every instruction in it. So a patch which edits only the
+# replacement -- code that runs on some CPUs and not others -- still has to
+# move the function's checksum.
+#
+# If it does not, klp diff decides the function is unchanged and leaves it out.
+# The patch then ships the old replacement, and the bug is fixed only on
+# machines whose CPU takes the other arm. Which machines those are depends on
+# the feature bit, so the failure looks like a machine-specific bug rather than
+# a missing patch.
+#
+# insn->alts exists only after objtool's check pass, so the pair goes through
+# that first -- the compiler emits none of this structure itself.
+#
+# Covers the same ground as corpus/x86_64/checksum-alt-group,
+# checksum-alt-no-group and checksum-alt-recursion-guard in Joe Lawrence's
+# klp-build unit test corpus.
+
+. "$(dirname "$0")/../lib.sh"
+
+setup
+
+check()
+{
+ build_pair checksum_alt.c "-D$1"
+ assert_input_section .altinstructions
+ run_objtool_check --mcount
+ run_checksum
+
+ assert_checksum_differs target
+}
+
+# The replacement instruction itself.
+check ALT_REPL
+# The feature number, with no instruction anywhere changed.
+check ALT_FEATURE
+
+pass "alternative replacement code counts towards the checksum"
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 000000000000..9d40c3a405af
--- /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"
diff --git a/tools/objtool/tests/x86/test-kcfi.sh b/tools/objtool/tests/x86/test-kcfi.sh
new file mode 100755
index 000000000000..b583ef608747
--- /dev/null
+++ b/tools/objtool/tests/x86/test-kcfi.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Under kCFI an indirect call checks a type hash before jumping, and traps on a
+# mismatch. Two things belong to the calling function and must come with it
+# into a patch:
+#
+# - the __cfi_<func> prefix symbol holding the hash. Lose it and the patched
+# function has no type identity, so indirect calls to it trap.
+# - its .kcfi_traps entry. Lose that and the trap is not recognised as a
+# CFI failure, so what should be a clean report becomes an oops.
+#
+# Neither shows up at build time.
+
+. "$(dirname "$0")/../lib.sh"
+
+clang_only "kCFI is a Clang feature"
+
+setup
+
+# Declared above that this is Clang's; a given Clang may still be too old.
+cc_supports -fsanitize=kcfi ||
+ probe_skip "this clang does not support -fsanitize=kcfi"
+
+build_pair kcfi.c -fsanitize=kcfi
+
+assert_input_section .kcfi_traps
+assert_input_symbol __cfi_target
+
+run_diff
+
+assert_patched target
+
+# The prefix symbol comes with its function ...
+assert_symbol __cfi_target
+# ... and so does the trap entry.
+assert_section .kcfi_traps
+
+pass "kCFI prefix symbol and trap entry carried with the patched function"
diff --git a/tools/objtool/tests/x86/test-manual-klp-static-call.sh b/tools/objtool/tests/x86/test-manual-klp-static-call.sh
new file mode 100755
index 000000000000..6c4d275e3549
--- /dev/null
+++ b/tools/objtool/tests/x86/test-manual-klp-static-call.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# objtool's static call handling must accept a livepatch module which cannot
+# see a static call's key symbol.
+#
+# __SCK__* keys are not exported; modules get read-only access at load time
+# instead. Livepatch modules built by klp-build do have full access to their
+# keys, and a check was added on the strength of that -- but a livepatch module
+# can also be written by hand, and samples/livepatch is full of them. One of
+# those needs a key it cannot see as soon as it does anything that expands to a
+# static call, which with CONFIG_MEM_ALLOC_PROFILING_DEBUG includes allocating
+# memory:
+#
+# samples/livepatch/livepatch-shadow-fix1.o: error: objtool: static_call:
+# can't find static_call_key symbol: __SCK__WARN_trap
+#
+# The module built without the livepatch tag is the control: it takes the same
+# path and has always been accepted, so a test which only built the livepatch
+# one could not tell this fix from the check being removed altogether.
+#
+# Fixed by f495054bd12e ("objtool/klp: Fix unexported static call key access
+# for manually built livepatch modules").
+
+. "$(dirname "$0")/../lib.sh"
+
+setup
+
+# Not a klp subcommand: this is objtool's ordinary check pass, which is what
+# runs over a hand-built livepatch module during a normal kernel build.
+for tag in "" -DLIVEPATCH; do
+ build_one static_call_no_key.c mod.o $tag
+
+ "$OBJTOOL" --module --static-call "$workdir/mod.o" \
+ > "$workdir/objtool.log" 2>&1 ||
+ fail "objtool rejected a ${tag:+livepatch }module which cannot" \
+ "see its static call key: $(tail -1 "$workdir/objtool.log")"
+done
+
+pass "livepatch module accepted without access to its static call key"
diff --git a/tools/objtool/tests/x86/test-special-sections.sh b/tools/objtool/tests/x86/test-special-sections.sh
new file mode 100755
index 000000000000..8dfaa4fc9a36
--- /dev/null
+++ b/tools/objtool/tests/x86/test-special-sections.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# klp diff extracts entries from eight special sections. Between them the
+# existing tests reach .kcfi_traps, __jump_table, .static_call_sites and
+# .altinstructions; __bug_table, __ex_table and __mcount_loc are covered by
+# nothing, though the same extraction code serves all of them.
+#
+# Losing an entry is quiet in every case and wrong in a different way for each:
+# a WARN() in patched code that no longer reports where it came from, an
+# exception fixup that is simply not there when the faulting instruction traps,
+# a function ftrace can no longer see.
+
+. "$(dirname "$0")/../lib.sh"
+
+
+# section, entry size, relocations per entry
+for spec in "__bug_table 12 1" "__ex_table 12 2" "__mcount_loc 8 1"; do
+ set -- $spec
+ sec=$1
+
+ # A fresh workdir per section: run_diff caches its checksums.
+ setup
+ build_pair special_sections.c \
+ -DSPECIAL_SEC="\"$1\"" -DSPECIAL_ENTSIZE="$2" -DSPECIAL_RELOCS="$3"
+
+ assert_input_section "$sec"
+ run_diff
+
+ # Extracted, and pointing at the function that was patched.
+ assert_section "$sec"
+ assert_reloc_sym "$sec" target
+ assert_patched target
+
+ # Nothing belonging to the function that was not.
+ assert_not_patched other
+ assert_no_reloc_sym "$sec" other
+
+ cleanup
+done
+
+pass "__bug_table, __ex_table and __mcount_loc entries extracted"