From: Song Liu <song@kernel.org> Date: 2026-09-14 06:25:30
This is the unit test part of a test framework for the klp-build toolchain.
The other part of the test framework will be some integration tests,
similar to an earlier propose [1].
This is joint work by Puranjay Mohan, Joe Lawrence, and myself (Song Liu).
Note that, while only two patches here are under Joe's authorship, his
contribution to this set is much bigger than the number represents. A lot
of these tests are based on Joe's earlier work in this area [2].
As the Assisted-by tags show, a lot of the work here is done with assists
of AI. We actually designed the framework to so that we expect AI to write
these tests in the future. AI also contributed in the design of the test
framework. However, we (human) carefully reviewed the test framework. Also,
this cover letter is 100% written by a human being (Song Liu).
Here are some key design choices we made for this unit tests framework:
1. We do not need different unit test here for different kernel CONFIG_*
options. Instead, the tests cover different CONFIG_* options by passing
different arguments to the compiler and objtool.
2. The test is NOT designed to be backward compatible. Similar to objtool
itself, the unit tests are delivered with the kernel tree. The user of
these tests should test the objtool with the unit tests in the same
branch. Once this test framework lands, future changes and fixes to the
objtool klp command should land with unit tests that cover the changes.
3. It is possible to run these tests on a different architecture with
proper cross compilers. However, for automated CI, we are planning to
focus on native tests. Please refer to "Future work" section below for
more information about the CI.
4. AI will help write tests. We found AI is very cable to write good tests.
tools/objtool/Documentation/klp-write-tests.txt is added for AI to read,
so that AI can write tests for this test framework.
Future work
1. The integration test simiar to [1]. Unlike the unit tests, the
integration tests will need separate runs to cover different kernel
CONFIG_* combinations.
2. CI. The plan is to build a lore => patchwork => GitHub CI framework.
This is the model used by subsystems such as netdev, bpf, and risc-v.
We also have some ideas on "who will pay for the CI" issue, but I would
rather not promise anything yet.
3. More tests.
Thanks,
Song
[1] https://lore.kernel.org/live-patching/20260226005436.379303-9-song@kernel.org/
[2] https://github.com/joe-lawrence/klp-build-unit-tests
Changes v1 => v2:
1. Fix issues reported by Sashiko.
v1: https://lore.kernel.org/live-patching/20260911184305.1457308-1-song@kernel.org/
Joe Lawrence (2):
objtool: Keep failing test workdirs by default
objtool: Forward toolchain variables to the klp test runner
Puranjay Mohan (16):
objtool: Add test harness for the klp subcommands
objtool/klp: Add test for rejecting changed data
objtool/klp: Add test for newly introduced data
objtool/klp: Add test for newly introduced functions
objtool/klp: Add test for static local correlation
objtool/klp: Add test for cold function halves
objtool/klp: Add test for special section extraction
objtool/klp: Add test for selective special section extraction
objtool/klp: Add test for jump table key relocations
objtool/klp: Add test for rejecting module-owned static branch keys
objtool/klp: Add test for rejecting module-owned static call keys
objtool/klp: Add test for symids in discarded sections
objtool/klp: Add test for rejecting references to init code/data
objtool/klp: Add test for correlation across ThinLTO name mangling
objtool/klp: Add test for objects without .modinfo
objtool/klp: Add test for unchecksummed input
Song Liu (40):
objtool/klp: Check the klp test environment once, before any test
objtool/klp: Group the klp tests by architecture
objtool/klp: Classify klp test outcomes
objtool/klp: Build klp test fixtures through the harness
objtool/klp: Grow the klp test harness vocabulary
objtool/klp: Give each run one working directory, one per test inside
it
objtool/klp: Run the klp tests under set -u
objtool/klp: Document the klp test harness
objtool/klp: Add klp diff and post-link regression tests
objtool/klp: Add test for klp reloc section naming in module objects
objtool/klp: Add test for vmlinux relocs in a patched module
objtool/klp: Add test for Module.symvers path normalization
objtool/klp: Add test for the contents of the klp_funcs list
objtool/klp: Add test for EXPORT_SYMBOL_FOR_MODULES references
objtool/klp: Add test for new references to exported symbols
objtool/klp: Add test for empty x86 alternative replacements
objtool/klp: Add test for recorded checksum values
objtool/klp: Add test for position-independent checksums
objtool/klp: Add test for sympos in module objects
objtool/klp: Add test for sympos resolved against a linked vmlinux
objtool/klp: Add test for static locals which must not be correlated
objtool/klp: Add test for __bug_table, __ex_table and __mcount_loc
extraction
objtool/klp: Add test for kCFI prefix symbols and traps
objtool/klp: Add test for symbols whose linkage the patch changes
objtool/klp: Test rejection of a file-local static branch key
objtool/klp: Test a hand-built livepatch module's static call keys
objtool/klp: Test text annotations on alternative replacements
objtool/klp: Add test for data object checksums
objtool/klp: Add test for symbols with no checksum entry of their own
objtool/klp: Add test for a static branch introduced by the patch
objtool/klp: Add test for tracepoint and pr_debug static branch keys
objtool/klp: Add test for a static call introduced by the patch
objtool/klp: Add test for instruction operand checksums
objtool/klp: Add test for alternative replacement code in checksums
objtool/klp: Add test for the alignment of cloned data sections
objtool/klp: Add test for a patch which strips a data annotation
objtool/klp: Add test for absolute and __ADDRESSABLE symbols
objtool/klp: Add test for UBSAN metadata in an unchanged function
objtool/klp: Add test for Clang switch jump tables
objtool/klp: Add test for ThinLTO symbols sharing a demangled name
.../objtool/Documentation/klp-test-design.txt | 276 ++++++
.../objtool/Documentation/klp-write-tests.txt | 260 ++++++
tools/objtool/Makefile | 7 +-
.../generic/fixtures/abs_and_addressable.c | 44 +
tools/objtool/tests/generic/fixtures/basic.c | 20 +
.../tests/generic/fixtures/changed_data.c | 16 +
.../tests/generic/fixtures/checksum_data.c | 116 +++
.../tests/generic/fixtures/checksum_insn.c | 78 ++
.../generic/fixtures/checksum_position.c | 45 +
.../tests/generic/fixtures/checksum_skip.c | 47 +
.../tests/generic/fixtures/cold_function.c | 21 +
.../tests/generic/fixtures/cross_module.c | 25 +
.../tests/generic/fixtures/data_alignment.c | 29 +
.../tests/generic/fixtures/function_removal.c | 25 +
.../tests/generic/fixtures/init_reference.c | 16 +
.../tests/generic/fixtures/jump_label.c | 76 ++
.../tests/generic/fixtures/klp_funcs.c | 31 +
.../tests/generic/fixtures/local_to_global.c | 34 +
.../objtool/tests/generic/fixtures/new_data.c | 18 +
.../tests/generic/fixtures/new_export_ref.c | 20 +
.../tests/generic/fixtures/new_function.c | 21 +
.../tests/generic/fixtures/no_modinfo.c | 11 +
.../tests/generic/fixtures/special_section.c | 24 +
.../generic/fixtures/special_section_shared.c | 31 +
.../tests/generic/fixtures/static_call.c | 53 ++
.../tests/generic/fixtures/static_local.c | 17 +
.../fixtures/static_local_uncorrelated.c | 41 +
.../tests/generic/fixtures/switch_rodata.c | 31 +
.../tests/generic/fixtures/symid_discarded.c | 25 +
.../tests/generic/fixtures/sympos_dup.c | 32 +
.../tests/generic/fixtures/sympos_vmlinux.c | 40 +
.../generic/fixtures/thinlto_ambiguity.c | 57 ++
.../tests/generic/fixtures/thinlto_local.c | 39 +
.../tests/generic/fixtures/ubsan_noise.c | 49 +
.../tests/generic/test-abs-and-addressable.sh | 50 +
tools/objtool/tests/generic/test-basic.sh | 17 +
.../tests/generic/test-changed-data.sh | 18 +
.../tests/generic/test-checksum-data.sh | 61 ++
.../tests/generic/test-checksum-debug.sh | 49 +
.../tests/generic/test-checksum-insn.sh | 49 +
.../tests/generic/test-checksum-position.sh | 35 +
.../tests/generic/test-checksum-skip.sh | 72 ++
.../tests/generic/test-checksum-value.sh | 37 +
.../tests/generic/test-cold-function.sh | 34 +
.../tests/generic/test-data-alignment.sh | 40 +
.../generic/test-export-symbol-for-modules.sh | 39 +
.../tests/generic/test-function-removal.sh | 34 +
.../tests/generic/test-init-reference.sh | 27 +
.../generic/test-jump-label-exempt-keys.sh | 51 ++
.../tests/generic/test-jump-label-key.sh | 44 +
.../generic/test-jump-label-module-key.sh | 22 +
.../test-jump-label-module-static-key.sh | 45 +
.../tests/generic/test-jump-label-new-key.sh | 51 ++
.../tests/generic/test-klp-funcs-content.sh | 44 +
.../generic/test-local-to-global-flip.sh | 50 +
.../tests/generic/test-local-vs-export.sh | 32 +
.../tests/generic/test-missing-checksum.sh | 18 +
.../tests/generic/test-missing-modinfo.sh | 16 +
.../tests/generic/test-modname-normalize.sh | 26 +
.../tests/generic/test-module-object.sh | 31 +
.../generic/test-module-vmlinux-reloc.sh | 40 +
tools/objtool/tests/generic/test-new-data.sh | 26 +
.../tests/generic/test-new-export-ref.sh | 34 +
.../tests/generic/test-new-function.sh | 17 +
tools/objtool/tests/generic/test-post-link.sh | 39 +
.../generic/test-special-section-shared.sh | 26 +
.../tests/generic/test-special-section.sh | 20 +
.../test-static-call-annotate-stripped.sh | 42 +
.../generic/test-static-call-module-key.sh | 25 +
.../tests/generic/test-static-call-new.sh | 45 +
.../generic/test-static-local-uncorrelated.sh | 40 +
.../tests/generic/test-static-local.sh | 24 +
.../tests/generic/test-switch-rodata.sh | 53 ++
.../tests/generic/test-symid-discarded.sh | 44 +
.../tests/generic/test-sympos-vmlinux.sh | 57 ++
tools/objtool/tests/generic/test-sympos.sh | 51 ++
.../tests/generic/test-symvers-parse-error.sh | 23 +
.../tests/generic/test-thinlto-ambiguity.sh | 77 ++
.../tests/generic/test-thinlto-local.sh | 48 +
.../objtool/tests/generic/test-ubsan-noise.sh | 48 +
tools/objtool/tests/lib.sh | 858 ++++++++++++++++++
tools/objtool/tests/run-tests.sh | 225 +++++
.../objtool/tests/x86/fixtures/alt_annotate.c | 57 ++
.../objtool/tests/x86/fixtures/checksum_alt.c | 66 ++
.../tests/x86/fixtures/empty_alternative.c | 77 ++
tools/objtool/tests/x86/fixtures/kcfi.c | 39 +
.../tests/x86/fixtures/special_sections.c | 77 ++
.../tests/x86/fixtures/static_call_no_key.c | 32 +
.../objtool/tests/x86/test-alt-annotation.sh | 38 +
tools/objtool/tests/x86/test-checksum-alt.sh | 45 +
.../tests/x86/test-empty-alternative.sh | 31 +
tools/objtool/tests/x86/test-kcfi.sh | 39 +
.../tests/x86/test-manual-klp-static-call.sh | 40 +
.../tests/x86/test-special-sections.sh | 42 +
94 files changed, 5111 insertions(+), 1 deletion(-)
create mode 100644 tools/objtool/Documentation/klp-test-design.txt
create mode 100644 tools/objtool/Documentation/klp-write-tests.txt
create mode 100644 tools/objtool/tests/generic/fixtures/abs_and_addressable.c
create mode 100644 tools/objtool/tests/generic/fixtures/basic.c
create mode 100644 tools/objtool/tests/generic/fixtures/changed_data.c
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_data.c
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_insn.c
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_position.c
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_skip.c
create mode 100644 tools/objtool/tests/generic/fixtures/cold_function.c
create mode 100644 tools/objtool/tests/generic/fixtures/cross_module.c
create mode 100644 tools/objtool/tests/generic/fixtures/data_alignment.c
create mode 100644 tools/objtool/tests/generic/fixtures/function_removal.c
create mode 100644 tools/objtool/tests/generic/fixtures/init_reference.c
create mode 100644 tools/objtool/tests/generic/fixtures/jump_label.c
create mode 100644 tools/objtool/tests/generic/fixtures/klp_funcs.c
create mode 100644 tools/objtool/tests/generic/fixtures/local_to_global.c
create mode 100644 tools/objtool/tests/generic/fixtures/new_data.c
create mode 100644 tools/objtool/tests/generic/fixtures/new_export_ref.c
create mode 100644 tools/objtool/tests/generic/fixtures/new_function.c
create mode 100644 tools/objtool/tests/generic/fixtures/no_modinfo.c
create mode 100644 tools/objtool/tests/generic/fixtures/special_section.c
create mode 100644 tools/objtool/tests/generic/fixtures/special_section_shared.c
create mode 100644 tools/objtool/tests/generic/fixtures/static_call.c
create mode 100644 tools/objtool/tests/generic/fixtures/static_local.c
create mode 100644 tools/objtool/tests/generic/fixtures/static_local_uncorrelated.c
create mode 100644 tools/objtool/tests/generic/fixtures/switch_rodata.c
create mode 100644 tools/objtool/tests/generic/fixtures/symid_discarded.c
create mode 100644 tools/objtool/tests/generic/fixtures/sympos_dup.c
create mode 100644 tools/objtool/tests/generic/fixtures/sympos_vmlinux.c
create mode 100644 tools/objtool/tests/generic/fixtures/thinlto_ambiguity.c
create mode 100644 tools/objtool/tests/generic/fixtures/thinlto_local.c
create mode 100644 tools/objtool/tests/generic/fixtures/ubsan_noise.c
create mode 100755 tools/objtool/tests/generic/test-abs-and-addressable.sh
create mode 100755 tools/objtool/tests/generic/test-basic.sh
create mode 100755 tools/objtool/tests/generic/test-changed-data.sh
create mode 100755 tools/objtool/tests/generic/test-checksum-data.sh
create mode 100755 tools/objtool/tests/generic/test-checksum-debug.sh
create mode 100755 tools/objtool/tests/generic/test-checksum-insn.sh
create mode 100755 tools/objtool/tests/generic/test-checksum-position.sh
create mode 100755 tools/objtool/tests/generic/test-checksum-skip.sh
create mode 100755 tools/objtool/tests/generic/test-checksum-value.sh
create mode 100755 tools/objtool/tests/generic/test-cold-function.sh
create mode 100755 tools/objtool/tests/generic/test-data-alignment.sh
create mode 100755 tools/objtool/tests/generic/test-export-symbol-for-modules.sh
create mode 100755 tools/objtool/tests/generic/test-function-removal.sh
create mode 100755 tools/objtool/tests/generic/test-init-reference.sh
create mode 100755 tools/objtool/tests/generic/test-jump-label-exempt-keys.sh
create mode 100755 tools/objtool/tests/generic/test-jump-label-key.sh
create mode 100755 tools/objtool/tests/generic/test-jump-label-module-key.sh
create mode 100755 tools/objtool/tests/generic/test-jump-label-module-static-key.sh
create mode 100755 tools/objtool/tests/generic/test-jump-label-new-key.sh
create mode 100755 tools/objtool/tests/generic/test-klp-funcs-content.sh
create mode 100755 tools/objtool/tests/generic/test-local-to-global-flip.sh
create mode 100755 tools/objtool/tests/generic/test-local-vs-export.sh
create mode 100755 tools/objtool/tests/generic/test-missing-checksum.sh
create mode 100755 tools/objtool/tests/generic/test-missing-modinfo.sh
create mode 100755 tools/objtool/tests/generic/test-modname-normalize.sh
create mode 100755 tools/objtool/tests/generic/test-module-object.sh
create mode 100755 tools/objtool/tests/generic/test-module-vmlinux-reloc.sh
create mode 100755 tools/objtool/tests/generic/test-new-data.sh
create mode 100755 tools/objtool/tests/generic/test-new-export-ref.sh
create mode 100755 tools/objtool/tests/generic/test-new-function.sh
create mode 100755 tools/objtool/tests/generic/test-post-link.sh
create mode 100755 tools/objtool/tests/generic/test-special-section-shared.sh
create mode 100755 tools/objtool/tests/generic/test-special-section.sh
create mode 100755 tools/objtool/tests/generic/test-static-call-annotate-stripped.sh
create mode 100755 tools/objtool/tests/generic/test-static-call-module-key.sh
create mode 100755 tools/objtool/tests/generic/test-static-call-new.sh
create mode 100755 tools/objtool/tests/generic/test-static-local-uncorrelated.sh
create mode 100755 tools/objtool/tests/generic/test-static-local.sh
create mode 100755 tools/objtool/tests/generic/test-switch-rodata.sh
create mode 100755 tools/objtool/tests/generic/test-symid-discarded.sh
create mode 100755 tools/objtool/tests/generic/test-sympos-vmlinux.sh
create mode 100755 tools/objtool/tests/generic/test-sympos.sh
create mode 100755 tools/objtool/tests/generic/test-symvers-parse-error.sh
create mode 100755 tools/objtool/tests/generic/test-thinlto-ambiguity.sh
create mode 100755 tools/objtool/tests/generic/test-thinlto-local.sh
create mode 100755 tools/objtool/tests/generic/test-ubsan-noise.sh
create mode 100644 tools/objtool/tests/lib.sh
create mode 100755 tools/objtool/tests/run-tests.sh
create mode 100644 tools/objtool/tests/x86/fixtures/alt_annotate.c
create mode 100644 tools/objtool/tests/x86/fixtures/checksum_alt.c
create mode 100644 tools/objtool/tests/x86/fixtures/empty_alternative.c
create mode 100644 tools/objtool/tests/x86/fixtures/kcfi.c
create mode 100644 tools/objtool/tests/x86/fixtures/special_sections.c
create mode 100644 tools/objtool/tests/x86/fixtures/static_call_no_key.c
create mode 100755 tools/objtool/tests/x86/test-alt-annotation.sh
create mode 100755 tools/objtool/tests/x86/test-checksum-alt.sh
create mode 100755 tools/objtool/tests/x86/test-empty-alternative.sh
create mode 100755 tools/objtool/tests/x86/test-kcfi.sh
create mode 100755 tools/objtool/tests/x86/test-manual-klp-static-call.sh
create mode 100755 tools/objtool/tests/x86/test-special-sections.sh
--
2.53.0-Meta
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:25:35
From: Puranjay Mohan <puranjay@kernel.org>
"objtool klp checksum" and "objtool klp diff" work on object files alone,
with no kernel, vmlinux or configuration involved, so they can be tested
directly. That is worth doing: most klp generation bugs so far have been in
symbol correlation, special section extraction and relocation conversion,
and several of them failed silently, producing a livepatch which built
cleanly but was missing data.
A test compiles a fixture twice, as the original and (with -DPATCHED) the
patched object, runs both through klp checksum, diffs them and asserts on
the result. Fixtures are compiled at test time rather than committed as
binaries: codegen varies between compilers and architectures, and that
variation is where a good number of these bugs come from.
Assertions check properties rather than compare against recorded output.
Golden files would need re-recording for every compiler change and would
report churn instead of regressions.
klp diff resolves symbols against Module.symvers, so the harness writes
one. Whether a symbol is listed there decides between an ordinary
relocation and a klp relocation, which makes it the main knob tests use.
Run with:
make -C tools/objtool tests
Tests skip when objtool was built without klp support or when a fixture
does not build for the target architecture. A missing objtool binary fails
instead of skipping, since that means a broken invocation rather than an
environment which cannot run the test.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Co-developed-by: Joe Lawrence <joe.lawrence@redhat.com>
Signed-off-by: Joe Lawrence <joe.lawrence@redhat.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/Makefile | 5 +-
tools/objtool/tests/fixtures/basic.c | 20 +++
tools/objtool/tests/lib.sh | 175 +++++++++++++++++++++++++++
tools/objtool/tests/run-tests.sh | 20 +++
tools/objtool/tests/test-basic.sh | 17 +++
5 files changed, 236 insertions(+), 1 deletion(-)
create mode 100644 tools/objtool/tests/fixtures/basic.c
create mode 100644 tools/objtool/tests/lib.sh
create mode 100755 tools/objtool/tests/run-tests.sh
create mode 100755 tools/objtool/tests/test-basic.sh
@@ -0,0 +1,20 @@+// SPDX-License-Identifier: GPL-2.0+/* One changed function and one unchanged function. */++/* klp diff takes the object's module name from .modinfo */+staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++intuntouched(intx)+{+returnx*3;+}++intchanged(intx)+{+#ifdef PATCHED+returnx+2;+#else+returnx+1;+#endif+}
@@ -0,0 +1,175 @@+# SPDX-License-Identifier: GPL-2.0+#+# Helpers for the objtool klp tests. A test builds a fixture twice, as the+# original and (with -DPATCHED) the patched object, runs both through+# "klp checksum" and diffs them, then asserts on the result.+#+# Assertions check properties rather than compare against recorded output:+# codegen varies between compilers and golden files would report churn instead+# of regressions.++TESTS_DIR="$(cd"$(dirname"${BASH_SOURCE[0]}")"&&pwd)"+FIXTURES_DIR="$TESTS_DIR/fixtures"++OBJTOOL="${OBJTOOL:-$TESTS_DIR/../objtool}"+CC="${CC:-gcc}"++# klp-build compiles the kernel this way; klp diff needs per-symbol sections to+# extract individual functions.+FIXTURE_CFLAGS="-c -O2 -ffunction-sections -fdata-sections -fno-asynchronous-unwind-tables"++test_name="$(basename"$0".sh)"+workdir=++pass(){echo"ok - $test_name${1:+: $1}";exit0;}+fail(){echo"not ok - $test_name: $1">&2;exit1;}+skip(){echo"ok - $test_name # SKIP $1";exit0;}++cleanup(){[-n"$workdir"]&&rm-rf"$workdir";}++# setup [exported symbol...]+setup()+{+# A relative $OBJTOOL is relative to the objtool directory, not to the+# tests which run from tests/.+[-x"$OBJTOOL"]||[!-x"$TESTS_DIR/../$OBJTOOL"]||+OBJTOOL="$TESTS_DIR/../$OBJTOOL"++# Not finding objtool is a broken invocation, not an environment which+# cannot run the test. Skipping here would read as a pass.+[-x"$OBJTOOL"]||fail"objtool not found at '$OBJTOOL', build it first"++"$OBJTOOL"klp2>&1|grep-qchecksum||+skip"objtool built without klp support (needs libxxhash)"+command-v"${CC%% *}">/dev/null||skip"no compiler ($CC)"++workdir="$(mktemp-d)"||fail"mktemp failed"+trapcleanupEXIT++export_syms"$@"+}++# export_syms [symbol...]+#+# Rewrite Module.symvers so exactly these symbols are exported by vmlinux.+# Whether a symbol is listed decides between an ordinary relocation and a klp+# relocation, so tests flip it to cover both.+export_syms()+{+:>"$workdir/Module.symvers"+forsymin"$@";do+printf'0x00000000\t%s\tvmlinux\tEXPORT_SYMBOL\t\n'\+"$sym">>"$workdir/Module.symvers"+done+}++# build_pair <fixture.c> [cflags...]+build_pair()+{+localfixture="$FIXTURES_DIR/$1";shift++[-f"$fixture"]||fail"missing fixture $fixture"++$CC$FIXTURE_CFLAGS"$@"-o"$workdir/orig.o""$fixture"2>"$workdir/cc.log"||+skip"fixture does not build here: $(tail-1"$workdir/cc.log")"+$CC$FIXTURE_CFLAGS"$@"-DPATCHED-o"$workdir/patched.o""$fixture"2>"$workdir/cc.log"||+skip"fixture does not build here: $(tail-1"$workdir/cc.log")"+}++# run_diff [expected exit status]+run_diff()+{+localexpect="${1:-0}"rc=0++# Checksums live in the objects, so only generate them once even when a+# test diffs the same pair again with a different Module.symvers.+if[!-e"$workdir/.checksummed"];then+"$OBJTOOL"klpchecksum"$workdir/orig.o"||+fail"klp checksum orig.o failed"+"$OBJTOOL"klpchecksum"$workdir/patched.o"||+fail"klp checksum patched.o failed"+touch"$workdir/.checksummed"+fi++# klp diff looks for Module.symvers relative to the working directory.+(cd"$workdir"&&"$OBJTOOL"klpdifforig.opatched.oout.o)\+>"$workdir/diff.log"2>&1||rc=$?++["$rc"="$expect"]||+fail"klp diff exited $rc, expected $expect: $(tail-2"$workdir/diff.log")"+}++cc_supports()+{+echo'int f(void) { return 0; }'>"$workdir/flagtest.c"+$CC$1-c"$workdir/flagtest.c"-o"$workdir/flagtest.o"2>/dev/null+}++# partial_link <output> <object...>+#+# "ld -r" through the compiler driver so the link targets the same+# architecture as the objects.+partial_link()+{+localout="$1";shift++$CC-r-nostdlib-o"$out""$@"2>/dev/null||+$CC-r-nostdlib-fuse-ld=lld-o"$out""$@"2>/dev/null+}++# find_thinlto_toolchain+#+# Set $THIN_CC and $THIN_LD to a clang and lld from the same LLVM release. A+# mismatched pair fails with "Invalid summary version", which reads like a+# broken test rather than a broken environment.+find_thinlto_toolchain()+{+localccldver++forccin"${THIN_CC:-}""$CC"clang;do+[-n"$cc"]||continue+command-v"${cc%% *}">/dev/null2>&1||continue++ver=$($cc-dumpversion2>/dev/null|cut-d.-f1)++forldin"${THIN_LD:-}""ld.lld-$ver"ld.lld;do+[-n"$ld"]||continue+command-v"$ld">/dev/null2>&1||continue++echo'int probe(void) { return 0; }'>"$workdir/probe.c"+$cc-flto=thin-O2-c"$workdir/probe.c"\+-o"$workdir/probe.o"2>/dev/null||continue+"$ld"-r"$workdir/probe.o"-o"$workdir/probe.elf"\+2>/dev/null||continue++THIN_CC="$cc"+THIN_LD="$ld"+return0+done+done++return1+}++out_sections(){readelf-S-W"$workdir/out.o"2>/dev/null;}+out_relocs(){readelf-r-W"$workdir/out.o"2>/dev/null;}+out_symbols(){readelf-s-W"$workdir/out.o"2>/dev/null;}+diff_log(){cat"$workdir/diff.log";}++assert_section()+{+out_sections|grep-q"[[:space:]]$1[[:space:]]"||+fail"expected section '$1' in output"+}++assert_patched()+{+assert_section".text.$1"+}++assert_not_patched()+{+out_sections|grep-q"[[:space:]].text.$1[[:space:]]"&&+fail"function '$1' should not have been cloned"+return0+}
@@ -0,0 +1,20 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Run the objtool klp tests. Each test-*.sh prints one TAP result line.++set-u++cd"$(dirname"$0")"||exit1++tests=(test-*.sh)+["${tests[0]}"="test-*.sh"]&&{echo"1..0 # SKIP no tests found";exit0;}++echo"1..${#tests[@]}"++rc=0+fortin"${tests[@]}";do+./"$t"||rc=1+done++exit$rc
@@ -0,0 +1,17 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Only functions whose code changed get cloned into the patch.++."$(dirname"$0")/lib.sh"++setup+build_pairbasic.c+run_diff++assert_patchedchanged+assert_not_patcheduntouched+assert_section".init.klp_funcs"+assert_section".init.klp_objects"++pass"changed function cloned, unchanged function left alone"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:25:40
Every test checked for itself that objtool exists, was built with klp
support, and that $CC runs. Three problems with that: it is the same work
done 40 times, a missing objtool reads as a per-test skip rather than as a
suite which cannot run, and a run in which everything skipped still exits
0.
Do it once, before any test, in klp_preflight() in lib.sh, and export the
answers where the tests can read them. If the suite cannot run the whole
run fails and says why; a test which gets as far as running can assume its
environment.
Nothing is written down, so nothing can go stale. Sourcing lib.sh runs the
checks, which means a test run by hand establishes its own answers rather
than inheriting those of some earlier run with a different CC, and a test
is never handed an empty value for where objtool is or which architecture
it is on: either the exports are set or the run has already failed.
preflight answers only whether the suite can run at all -- not what the
compiler is capable of. A test needing a particular compiler feature
probes for it and skips; that costs one compile and keeps the reason next
to the test that has to justify it.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/Makefile | 3 +-
tools/objtool/tests/lib.sh | 114 ++++++++++++++++++++++++++-----
tools/objtool/tests/run-tests.sh | 7 ++
3 files changed, 106 insertions(+), 18 deletions(-)
@@ -11,8 +11,98 @@TESTS_DIR="$(cd"$(dirname"${BASH_SOURCE[0]}")"&&pwd)"FIXTURES_DIR="$TESTS_DIR/fixtures"+# The kernel's convention: CROSS_COMPILE is the one knob, with per-tool+# overrides for what it does not cover. objtool itself is always a host binary+# -- it is built with HOSTCC and only reads ELF -- so an arm64 machine can run+# the x86 tests against x86 objects given a compiler that emits them.+#+# readelf reads any target, so it rarely needs overriding -- and it must stay+# GNU readelf, whose column layout the assertions parse; llvm-readelf spaces+# them differently. BFD's objcopy is usually built for the host's target+# alone, and llvm-objcopy is the target-agnostic replacement.+CROSS_COMPILE="${CROSS_COMPILE:-}"+CC="${CC:-${CROSS_COMPILE}gcc}"+LD="${LD:-${CROSS_COMPILE}ld}"+READELF="${READELF:-${CROSS_COMPILE}readelf}"+OBJCOPY="${OBJCOPY:-${CROSS_COMPILE}objcopy}"+OBJTOOL="${OBJTOOL:-$TESTS_DIR/../objtool}"-CC="${CC:-gcc}"++# klp_preflight+#+# Check the environment once, before any test runs, and report what was found.+#+klp_preflight()+{+localtmptoolcc_versionhostcc_arch++bail(){echo"Bail out! $*">&2;exit1;}++# A relative $OBJTOOL is relative to the objtool directory, not tests/.+[-x"$OBJTOOL"]||[!-x"$TESTS_DIR/../$OBJTOOL"]||+OBJTOOL="$TESTS_DIR/../$OBJTOOL"++[-x"$OBJTOOL"]||+bail"objtool not found at '$OBJTOOL' -- build it first"++"$OBJTOOL"klp2>&1|grep-qchecksum||+bail"objtool was built without klp support; install libxxhash (>= 0.8) and rebuild"++command-v"${CC%% *}">/dev/null||bail"compiler not found: $CC"++fortoolin"$READELF""$OBJCOPY""$LD";do+command-v"$tool">/dev/null||bail"$tool not found"+done++tmp="$(mktemp-d)"||bail"mktemp failed"+echo'int probe(void) { return 0; }'>"$tmp/probe.c"+$CC-c-o"$tmp/probe.o""$tmp/probe.c"2>/dev/null||+{rm-rf"$tmp";bail"$CC cannot compile a trivial object";}++# $CC, $ARCH and objtool have to agree about the target, and cross runs+# are where they stop agreeing: plain "CC=clang ARCH=x86_64" on an arm64+# box selects the x86 tests and then builds arm64 objects, because clang+# needs --target= to emit anything but the host's.+#+# Ask objtool rather than comparing machine names. It rejects an object+# it was not built for -- "unexpected ELF machine type" -- so one check+# covers every way the three can disagree, and says so once instead of+# failing every test for the same reason.+"$OBJTOOL"klpchecksum"$tmp/probe.o">/dev/null2>&1||+{rm-rf"$tmp"+bail"objtool rejects an object built by '$CC'; they target"\+"different architectures (set CROSS_COMPILE, or"\+"--target= for clang)";}++# BFD objcopy is usually built for the host's target alone, and+# checksum_of() needs it to read the object under test.+$OBJCOPY-Obinary--only-section=.text"$tmp/probe.o""$tmp/probe.bin"2>/dev/null||+{rm-rf"$tmp"+bail"$OBJCOPY cannot read objects built by '$CC'; install"\+"binutils-multiarch or set OBJCOPY=llvm-objcopy";}+# $ARCH only chooses which directory of tests runs, so it can disagree+# with what $CC builds without objtool noticing -- and the result is the+# wrong set of tests, quietly.+case"$($READELF-hW"$tmp/probe.o"|sed-n's/.*Machine: *//p')"in+*X86-64*|*Intel*80386*)cc_arch=x86;;+*AArch64*)cc_arch=arm64;;+*)cc_arch=;;+esac+rm-rf"$tmp"++KLP_TEST_PREFLIGHT=done+exportOBJTOOLCCKLP_TEST_PREFLIGHT++cc_version="$($CC--version2>/dev/null|head-1)"+cat<<EOF+# preflight+# objtool $OBJTOOL (klp: yes)+# compiler $cc_version+# arch $KLP_TEST_ARCH$([ "$arch" = "$host" ] || echo " (host $host, cross)")+EOF+}++[-n"${KLP_TEST_PREFLIGHT:-}"]||klp_preflight# klp-build compiles the kernel this way; klp diff needs per-symbol sections to# extract individual functions.
@@ -30,19 +120,9 @@ cleanup() { [ -n "$workdir" ] && rm -rf "$workdir"; }# setup [exported symbol...] setup(){-# A relative $OBJTOOL is relative to the objtool directory, not to the-# tests which run from tests/.-[-x"$OBJTOOL"]||[!-x"$TESTS_DIR/../$OBJTOOL"]||-OBJTOOL="$TESTS_DIR/../$OBJTOOL"--# Not finding objtool is a broken invocation, not an environment which-# cannot run the test. Skipping here would read as a pass.-[-x"$OBJTOOL"]||fail"objtool not found at '$OBJTOOL', build it first"--"$OBJTOOL"klp2>&1|grep-qchecksum||-skip"objtool built without klp support (needs libxxhash)"-command-v"${CC%% *}">/dev/null||skip"no compiler ($CC)"-+# The environment was checked once when this file was sourced, so there+# is nothing to verify here: objtool exists at the resolved path, has+# klp support, and $CC works.workdir="$(mktemp-d)"||fail"mktemp failed"trapcleanupEXIT
@@ -2,6 +2,9 @@# SPDX-License-Identifier: GPL-2.0## Run the objtool klp tests. Each test-*.sh prints one TAP result line.+#+# The harness checks the environment once up front and fails the run if the+# suite cannot execute, rather than letting every test skip and exit 0.set-u
@@ -10,6 +13,10 @@ cd "$(dirname "$0")" || exit 1tests=(test-*.sh)["${tests[0]}"="test-*.sh"]&&{echo"1..0 # SKIP no tests found";exit0;}+# Sourcing the harness runs its preflight, and exports what it found so the+# tests inherit it rather than working it out again.+../lib.sh+echo"1..${#tests[@]}"rc=0
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:25:46
Which architecture a test is for is expressed by where it lives: tests are
in generic/ or in a directory named for their architecture, each carrying
its own fixtures, and the runner executes generic/ plus the one that
matches. A test which cannot apply here is then not run at all, rather
than running in order to announce that it did not.
Layout does this better than a declaration would. There is no x86_only(),
and no lookup letting a fixtures/<arch>/ file shadow a generic one of the
same name -- an arch-specific test simply carries its own fixtures.
Compilers cannot work the same way: CI varies CC over the same tree, so a
compiler requirement stays a declaration in the test.
What a run leaves out is reported once:
# not run: 5 tests in x86/ (this run is arm64)
Silence would have been cheaper and wrong. A run covering less than the
tree holds must not look like a run that covered all of it.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/{ => generic}/fixtures/basic.c | 0
.../objtool/tests/{ => generic}/test-basic.sh | 2 +-
tools/objtool/tests/lib.sh | 27 ++++++++++++--
tools/objtool/tests/run-tests.sh | 36 ++++++++++++++++---
4 files changed, 56 insertions(+), 9 deletions(-)
rename tools/objtool/tests/{ => generic}/fixtures/basic.c (100%)
rename tools/objtool/tests/{ => generic}/test-basic.sh (91%)
diff --git a/tools/objtool/tests/fixtures/basic.c b/tools/objtool/tests/generic/fixtures/basic.csimilarity index 100%rename from tools/objtool/tests/fixtures/basic.crename to tools/objtool/tests/generic/fixtures/basic.cdiff --git a/tools/objtool/tests/test-basic.sh b/tools/objtool/tests/generic/test-basic.shsimilarity index 91%rename from tools/objtool/tests/test-basic.shrename to tools/objtool/tests/generic/test-basic.shindex 6b769962399a..562edfbc8646 100755--- a/tools/objtool/tests/test-basic.sh+++ b/tools/objtool/tests/generic/test-basic.sh
@@ -3,7 +3,7 @@## Only functions whose code changed get cloned into the patch.-."$(dirname"$0")/lib.sh"+."$(dirname"$0")/../lib.sh" setup build_pairbasic.c
@@ -9,7 +9,10 @@# of regressions.TESTS_DIR="$(cd"$(dirname"${BASH_SOURCE[0]}")"&&pwd)"-FIXTURES_DIR="$TESTS_DIR/fixtures"++# Tests live in generic/ or in an architecture directory beside it, and each+# carries its own fixtures.+FIXTURES_DIR="$(cd"$(dirname"$0")/fixtures"2>/dev/null&&pwd)"# The kernel's convention: CROSS_COMPILE is the one knob, with per-tool# overrides for what it does not cover. objtool itself is always a host binary
@@ -90,8 +93,26 @@ klp_preflight()esacrm-rf"$tmp"+# Normalize to the kernel's SRCARCH.+case"${ARCH:-$(uname-m)}"in+x86_64|i?86)arch=x86;;+aarch64*)arch=arm64;;+*)arch="${ARCH:-$(uname-m)}";;+esac++case"$(uname-m)"in+x86_64|i?86)host=x86;;+aarch64*)host=arm64;;+*)host="$(uname-m)";;+esac++[-z"$cc_arch"]||["$cc_arch"="$arch"]||+bail"ARCH says $arch but '$CC' builds $cc_arch objects;"\+"the $arch tests would run against the wrong architecture"++KLP_TEST_ARCH="$arch"KLP_TEST_PREFLIGHT=done-exportOBJTOOLCCKLP_TEST_PREFLIGHT+exportOBJTOOLCCKLP_TEST_ARCHKLP_TEST_PREFLIGHTcc_version="$($CC--version2>/dev/null|head-1)"cat<<EOF
@@ -3,6 +3,11 @@## Run the objtool klp tests. Each test-*.sh prints one TAP result line.#+# Tests live in generic/ and in a directory per architecture. A run executes+# generic/ plus the one matching this architecture, so a test which cannot+# apply here is not run rather than reporting a skip; what was left out is+# reported once, as a comment, so differing coverage is still visible.+## The harness checks the environment once up front and fails the run if the# suite cannot execute, rather than letting every test skip and exit 0.
@@ -10,13 +15,34 @@ set -ucd"$(dirname"$0")"||exit1-tests=(test-*.sh)-["${tests[0]}"="test-*.sh"]&&{echo"1..0 # SKIP no tests found";exit0;}--# Sourcing the harness runs its preflight, and exports what it found so the-# tests inherit it rather than working it out again.+# Sourcing the harness runs its preflight, which decides which architecture+# this run is for -- so the test list cannot be built before it has, and the+# tests inherit the answers rather than working them out again. ../lib.sh+dirs=(generic)+[-d"$KLP_TEST_ARCH"]&&dirs+=("$KLP_TEST_ARCH")++tests=()+fordin"${dirs[@]}";do+fortin"$d"/test-*.sh;do+[-f"$t"]&&tests+=("$t")+done+done+["${#tests[@]}"-gt0]||{echo"1..0 # SKIP no tests found";exit0;}++# Tests for another architecture are absent from this run entirely. Say how+# many, so a run which covers less than the tree holds does not look like one+# that covers all of it.+fordin*/;do+d="${d%/}"+case"$d"ingeneric|"$KLP_TEST_ARCH")continue;;esac+n=$(ls"$d"/test-*.sh2>/dev/null|wc-l)+["$n"-gt0]||continue+echo"# not run: $n test$(["$n"=1]||echos) in $d/"\+"(this run is $KLP_TEST_ARCH)"+done+echo"1..${#tests[@]}"rc=0
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:25:51
A test could pass, fail, or skip, and the runner reported only the exit
status of the last one to fail. Skips are the interesting case: they are
how a suite quietly stops testing anything.
Three kinds, and the runner counts them separately:
declared the test said in advance it does not apply here, e.g. gcc_only
on a clang run. Expected indefinitely.
probe the construct did not turn up in the built object this time.
Weaker: one which becomes permanent is a fixture that stopped
testing anything.
undeclared counted as a failure. A test which gives up for a reason it
never declared is a hole, not an outcome.
xfail and xpass come with them, so a known failure is reported rather than
commented out and forgotten, and one which starts passing says so instead
of going quietly green.
The result line is what gets classified, not the whole of a test's output:
objtool warns on stderr and the runner captures it, and a stray line ahead
of the result would otherwise leave the exit status to decide -- counting
an expected failure, which exits 0, as a pass.
Every test owes the plan exactly one result line, so a substituted verdict
replaces the test's own rather than being printed beside it, and a test
which dies before reporting gets a result of its own instead of being
inferred from its exit status.
Also: a TAP version line, LC_ALL=C so the order tests run in and the
behaviour of grep's character ranges inside them do not depend on the
invoking shell's locale, and fail() on stdout with every other verdict.
Co-developed-by: Puranjay Mohan <puranjay@kernel.org>
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/tests/lib.sh | 67 ++++++++++++--
tools/objtool/tests/run-tests.sh | 148 ++++++++++++++++++++++++++-----
2 files changed, 186 insertions(+), 29 deletions(-)
@@ -133,8 +133,38 @@ test_name="$(basename "$0" .sh)"workdir= pass(){echo"ok - $test_name${1:+: $1}";exit0;}-fail(){echo"not ok - $test_name: $1">&2;exit1;}-skip(){echo"ok - $test_name # SKIP $1";exit0;}+fail(){echo"not ok - $test_name: $1";exit1;}++# Two kinds of skip, and the runner tells them apart.+#+# declared_skip the test said in advance it does not apply here, e.g.+# gcc_only on a clang run. Expected indefinitely.+# probe_skip the construct did not turn up in the built object this+# time. Weaker: it may appear on another compiler version,+# and one which becomes permanent is a fixture that quietly+# stopped testing anything.+#+# A bare skip() is neither, and the runner counts it as a failure: a test which+# gives up for a reason it never declared is a hole, not an outcome.+declared_skip(){echo"ok - $test_name # SKIP (declared) $1";exit0;}+probe_skip(){echo"ok - $test_name # SKIP (probe) $1";exit0;}+skip(){echo"ok - $test_name # SKIP $1";exit0;}++# TAP directives. A test which is known to fail reports it rather than being+# commented out and forgotten, and one which starts passing again says so+# instead of quietly going green: the expectation has to be removed by hand,+# which is the point.+xfail()+{+echo"not ok - $test_name${1:+: $1} # TODO known failure"+exit0+}++xpass()+{+echo"ok - $test_name${1:+: $1} # TODO expected failure, but passed"+exit1+} cleanup(){[-n"$workdir"]&&rm-rf"$workdir";}
@@ -172,9 +219,9 @@ build_pair()[-f"$fixture"]||fail"missing fixture $fixture"$CC$FIXTURE_CFLAGS"$@"-o"$workdir/orig.o""$fixture"2>"$workdir/cc.log"||-skip"fixture does not build here: $(tail-1"$workdir/cc.log")"+probe_skip"fixture does not build here: $(tail-1"$workdir/cc.log")"$CC$FIXTURE_CFLAGS"$@"-DPATCHED-o"$workdir/patched.o""$fixture"2>"$workdir/cc.log"||-skip"fixture does not build here: $(tail-1"$workdir/cc.log")"+probe_skip"fixture does not build here: $(tail-1"$workdir/cc.log")"}# run_diff [expected exit status]
@@ -220,16 +267,20 @@ partial_link()# find_thinlto_toolchain#-# Set $THIN_CC and $THIN_LD to a clang and lld from the same LLVM release. A+# Set $THIN_LD to an lld from the same LLVM release as $CC (or THIN_CC). A# mismatched pair fails with "Invalid summary version", which reads like a# broken test rather than a broken environment.+#+# ThinLTO is clang-only; callers must use clang_only before calling this.+# Only $CC (or an explicit THIN_CC override) is consulted -- the harness does+# not search for a second compiler beside a gcc $CC. find_thinlto_toolchain(){-localccldver+localccverld-forccin"${THIN_CC:-}""$CC"clang;do+forccin"${THIN_CC:-}""$CC";do[-n"$cc"]||continue-command-v"${cc%% *}">/dev/null2>&1||continue+command-v"${cc%% *}">/dev/null2>&1||return1ver=$($cc-dumpversion2>/dev/null|cut-d.-f1)
@@ -8,13 +8,51 @@# apply here is not run rather than reporting a skip; what was left out is# reported once, as a comment, so differing coverage is still visible.#+# A run covers one compiler and one architecture; CI runs the combinations.# The harness checks the environment once up front and fails the run if the# suite cannot execute, rather than letting every test skip and exit 0.set-u+# Determinism: the order test-*.sh expands in, and how grep's character ranges+# and sort's collation behave inside the tests, are all locale-dependent. A+# suite whose results depend on the invoking shell's locale is a suite whose+# failures cannot be reproduced.+exportLC_ALL=C++usage()+{+cat<<EOF+usage:$(basename"$0")[test...]++Runtheobjtoolklptestsforthisarchitecture:everythingingeneric/,plus+everythinginthedirectorynamedforit.Withnoarguments,runsallofthem.+Atestmaybenamedwithorwithoutits"test-"prefixand".sh"suffix,andis+lookedforinbothdirectories.++Environment:+OBJTOOLobjtoolbinarytotest(default../objtool)+CCcompilerusedtobuildfixtures(defaultgcc)+ARCHarchitecturethetestsarefor(default:uname-m)++Atestwhichneedssomethingofitsownsayssoinitsskipmessage.+EOF+exit"${1:-0}"+}+cd"$(dirname"$0")"||exit1+while[$#-gt0];do+case"$1"in+-h|--help)usage;;+--)shift;break;;+-*)echo"unknown option: $1">&2;usage1;;+*)break;;+esac+done++echo"TAP version 13"+# Sourcing the harness runs its preflight, which decides which architecture# this run is for -- so the test list cannot be built before it has, and the# tests inherit the answers rather than working them out again.
@@ -23,31 +61,99 @@ cd "$(dirname "$0")" || exit 1dirs=(generic)[-d"$KLP_TEST_ARCH"]&&dirs+=("$KLP_TEST_ARCH")-tests=()-fordin"${dirs[@]}";do-fortin"$d"/test-*.sh;do-[-f"$t"]&&tests+=("$t")+if[$#-gt0];then+tests=()+forargin"$@";do+name="test-${arg#test-}";name="${name%.sh}.sh"+found=+fordin"${dirs[@]}";do+[-f"$d/$name"]||continue+[-x"$d/$name"]||+{echo"not executable: $d/$name">&2;exit1;}+tests+=("$d/$name");found=y+done+[-n"$found"]||+{echo"no such test for $KLP_TEST_ARCH: $arg">&2;exit1;}done-done-["${#tests[@]}"-gt0]||{echo"1..0 # SKIP no tests found";exit0;}--# Tests for another architecture are absent from this run entirely. Say how-# many, so a run which covers less than the tree holds does not look like one-# that covers all of it.-fordin*/;do-d="${d%/}"-case"$d"ingeneric|"$KLP_TEST_ARCH")continue;;esac-n=$(ls"$d"/test-*.sh2>/dev/null|wc-l)-["$n"-gt0]||continue-echo"# not run: $n test$(["$n"=1]||echos) in $d/"\-"(this run is $KLP_TEST_ARCH)"-done+else+tests=()+fordin"${dirs[@]}";do+fortin"$d"/test-*.sh;do+[-f"$t"]&&tests+=("$t")+done+done+["${#tests[@]}"-gt0]||+{echo"1..0 # SKIP no tests found";exit0;}++# Tests for another architecture are absent from this run entirely. Say+# how many, so a run which covers less than the tree holds does not look+# like one that covers all of it.+fordin*/;do+d="${d%/}"+case"$d"ingeneric|"$KLP_TEST_ARCH")continue;;esac+n=$(ls"$d"/test-*.sh2>/dev/null|wc-l)+["$n"-gt0]||continue+echo"# not run: $n test$(["$n"=1]||echos) in $d/"\+"(this run is $KLP_TEST_ARCH)"+done+fiecho"1..${#tests[@]}"-rc=0+pass=0fail=0static_skip=0probe_skip=0xfail=0xpass=0+fortin"${tests[@]}";do-./"$t"||rc=1+out="$(./"$t"2>&1)"+rc=$?++# A test prints one result line, but it is not necessarily the only+# thing it prints: objtool warns on stderr, and the runner captures+# that. Classify the result line itself rather than the whole of the+# output, or a stray line ahead of it makes every pattern below miss and+# the exit status decide -- which would count an expected failure, which+# exits 0, as a pass.+result="$(printf'%s\n'"$out"|grep-E'^(ok|not ok)'|tail-1)"+rest="$(printf'%s\n'"$out"|grep-Ev'^(ok|not ok)')"++# Classify from the result line, not the exit status: a skip and a pass+# both exit 0, and telling them apart is the point of counting.+#+# The two skip kinds differ in what they promise. A static skip was+# declared before the test ran ("clang does not do this"), so it is+# expected indefinitely. A probe skip means the construct did not turn+# up this time, which is weaker and worth watching: one that becomes+# permanent is a fixture that quietly stopped testing anything.+case"$result"in+*"# SKIP (declared)"*)static_skip=$((static_skip+1));;+*"# SKIP (probe)"*)probe_skip=$((probe_skip+1));;+*"# SKIP"*)+# An undeclared skip: the test gave up for a reason it never+# said it might. That is a hole, not an expected outcome.+#+# Replace the line rather than adding one. Every test owes the+# plan exactly one result, and a consumer counting them is+# entitled to say so when the totals disagree.+rest="$rest${rest:+$'\n'}was: $result"+result="not ok - $(basename"$t".sh): undeclared skip"+result="$result (use gcc_only/clang_only or require_input_*)"+fail=$((fail+1));;+"not ok"*"# TODO"*)xfail=$((xfail+1));;+"ok"*"# TODO"*)xpass=$((xpass+1));;+"not ok"*)fail=$((fail+1));;+"ok"*)pass=$((pass+1));;+*)+# No result line at all: the test died before reporting.+rest="$rest${rest:+$'\n'}exited $rc without a result line"+result="not ok - $(basename"$t".sh): no TAP result"+fail=$((fail+1));;+esac++echo"$result"+[-n"$rest"]&&printf'%s\n'"$rest"|sed's/^[^#]/# &/'+done-exit$rc+echo"# pass:$pass fail:$fail static-skip:$static_skip"\+"probe-skip:$probe_skip xfail:$xfail xpass:$xpass"++["$fail"=0]&&["$xpass"=0]
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:25:56
Compiling a fixture is the one thing every test does, and it has more
invariants than it looks.
- The flags describe the kernel a fixture stands in for, and each is
written down with its reason. That substitution is the whole mechanism
by which these tests cover configurations without building a kernel --
objtool reads no .config, so a configuration reaches it only as
compiler flags and the bytes they produce -- and a flag with no stated
motive is indistinguishable from a mistake.
- -c is not one of them. Producing an object rather than a program is
the helper's contract, not something a test may reconsider, so it lives
at the compile and an override cannot drop it.
- A fixture which will not compile is a failure, not a skip. It is far
more often a mistake in the fixture than a compiler which cannot
express the construct, and the two are indistinguishable once reported
as a skip.
- A newly produced object has no checksums in it, so producing one
invalidates the record that checksumming has already been done. Left
to itself that invariant ends up in each test which rebuilds.
build_one handles a single object for the tests which need more than two or
different names; build_pair and build_module_pair are expressed in terms of
it, so none of the above can be lost by going around them.
run_checksum comes out of run_diff for the same reason: a test which wants
checksums without a diff should not have to run one.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/tests/lib.sh | 175 +++++++++++++++++++++++++++++++++----
1 file changed, 156 insertions(+), 19 deletions(-)
@@ -125,13 +125,26 @@ EOF[-n"${KLP_TEST_PREFLIGHT:-}"]||klp_preflight-# klp-build compiles the kernel this way; klp diff needs per-symbol sections to-# extract individual functions.-FIXTURE_CFLAGS="-c -O2 -ffunction-sections -fdata-sections -fno-asynchronous-unwind-tables"+# What every fixture is built with. These describe the kernel a fixture stands+# in for; -c is build_one's contract rather than a property of that kernel, so+# it lives at the compile where an override cannot drop it.+#+# -O2 the kernel's default+# -ffunction-sections -fdata-sections klp-build passes these itself, through+# KCFLAGS, whatever the configuration+# -fno-asynchronous-unwind-tables arch/x86/Makefile sets this always, so+# kernel objects carry no .eh_frame+#+# A test overrides it; see tools/objtool/Documentation/klp-write-tests.txt.+FIXTURE_CFLAGS="-O2 -ffunction-sections -fdata-sections \+-fno-asynchronous-unwind-tables"test_name="$(basename"$0".sh)"workdir=+orig_obj=orig.o+patched_obj=patched.o+ pass(){echo"ok - $test_name${1:+: $1}";exit0;} fail(){echo"not ok - $test_name: $1";exit1;}
@@ -211,17 +224,64 @@ clang_only()declared_skip"clang only${1:+: $1}"}+# build_one <fixture.c> <output object> [cflags...]+build_one()+{+localfixtureout+fixture="$FIXTURES_DIR/$1"+out="$workdir/$2"+shift2++[-f"$fixture"]||fail"missing fixture $fixture"++# run_checksum only runs once per workdir. A fresh object has no+# checksums in it, so anything built now needs that to happen again.+rm-f"$workdir/.checksummed"++$CC-c$FIXTURE_CFLAGS"$@"-o"$out""$fixture"2>"$workdir/cc.log"||+fail"$(basename"$fixture") does not build: $(tail-1"$workdir/cc.log")"+}+# build_pair <fixture.c> [cflags...] build_pair(){-localfixture="$FIXTURES_DIR/$1";shift+localfixture="$1";shift-[-f"$fixture"]||fail"missing fixture $fixture"+build_one"$fixture"orig.o"$@"+build_one"$fixture"patched.o"$@"-DPATCHED+}-$CC$FIXTURE_CFLAGS"$@"-o"$workdir/orig.o""$fixture"2>"$workdir/cc.log"||-probe_skip"fixture does not build here: $(tail-1"$workdir/cc.log")"-$CC$FIXTURE_CFLAGS"$@"-DPATCHED-o"$workdir/patched.o""$fixture"2>"$workdir/cc.log"||-probe_skip"fixture does not build here: $(tail-1"$workdir/cc.log")"+# run_objtool_check <objtool arguments...>+#+# Run objtool's ordinary check pass over the pair, as the kernel build does.+#+# Some of what klp diff consumes is produced by this pass rather than by the+# compiler: .static_call_sites, .mcount_loc, .ibt_endbr_seal, ORC.+#+# Only module objects see it before klp-build -- with CONFIG_KLP_BUILD the+# per-object pass is deferred, so built-in objects reach klp diff exactly as+# the compiler left them.+run_objtool_check()+{+localobj++forobjin"$orig_obj""$patched_obj";do+"$OBJTOOL""$@""$workdir/$obj"||+fail"objtool $* failed on $obj"+done+}++run_checksum()+{+# Checksums live in the objects, so only generate them once even when a+# test diffs the same pair again with a different Module.symvers.+[-e"$workdir/.checksummed"]&&return0++"$OBJTOOL"klpchecksum"$workdir/$orig_obj"||+fail"klp checksum $orig_obj failed"+"$OBJTOOL"klpchecksum"$workdir/$patched_obj"||+fail"klp checksum $patched_obj failed"+touch"$workdir/.checksummed"}# run_diff [expected exit status]
@@ -229,18 +289,10 @@ run_diff(){localexpect="${1:-0}"rc=0-# Checksums live in the objects, so only generate them once even when a-# test diffs the same pair again with a different Module.symvers.-if[!-e"$workdir/.checksummed"];then-"$OBJTOOL"klpchecksum"$workdir/orig.o"||-fail"klp checksum orig.o failed"-"$OBJTOOL"klpchecksum"$workdir/patched.o"||-fail"klp checksum patched.o failed"-touch"$workdir/.checksummed"-fi+run_checksum# klp diff looks for Module.symvers relative to the working directory.-(cd"$workdir"&&"$OBJTOOL"klpdifforig.opatched.oout.o)\+(cd"$workdir"&&"$OBJTOOL"klpdiff"$orig_obj""$patched_obj"out.o)\>"$workdir/diff.log"2>&1||rc=$?["$rc"="$expect"]||
@@ -261,10 +313,95 @@ partial_link(){localout="$1";shift+rm-f"$workdir/.checksummed"+$CC-r-nostdlib-o"$out""$@"2>/dev/null||$CC-r-nostdlib-fuse-ld=lld-o"$out""$@"2>/dev/null}+# link_vmlinux <output> <object...>+#+# Link objects into an executable, the way the kernel's final link produces+# vmlinux from vmlinux.o. Entry point 0 and no libc: nothing runs it, it only+# has to be a linked image with resolved addresses.+#+# The sub-sections have to come out in name order rather than object order,+# the way the kernel's linker script gathers .text.unlikely and .data.. apart+# from the rest. That reordering is the entire reason .klp.symid exists: a+# link which preserves order cannot tell a correct sympos from one that merely+# counted, and the caller checks the two orders really did diverge.+#+# A linker script rather than --sort-section=name, because lld accepts that+# option and ignores it -- so on a host where only lld can link the target, the+# test would quietly stop testing the thing it is named for.+#+# Three attempts because a cross run has neither $LD nor the compiler's default+# linker able to touch the target: on an arm64 host linking x86 objects, only+# lld will do it.+link_vmlinux()+{+localout="$1"lds="$workdir/sort.lds";shift++echo'SECTIONS { .data : { *(SORT_BY_NAME(.data.*)) } }'>"$lds"++$LD-e0-T"$lds"-o"$out""$@"2>/dev/null||+$CC-nostdlib-Wl,-e,0-Wl,-T,"$lds"\+-o"$out""$@"2>/dev/null||+$CC-nostdlib-fuse-ld=lld-Wl,-e,0-Wl,-T,"$lds"\+-o"$out""$@"2>/dev/null+}++# make_vmlinux_pair <orig object...> -- <patched object...>+#+# Build the vmlinux.o / vmlinux pair klp diff needs to resolve sympos the way+# it does for built-in code, and point the diff at it.+#+# For a module, sympos is a count in symbol table order, which klp diff can do+# from the object alone. vmlinux is different: the final link reorders+# sub-sections, so the position comes from the linked image, bridged by+# .klp.symid. klp diff only looks for that when the object it was handed is+# called vmlinux.o and a vmlinux sits beside it -- so both the name and the+# linked image matter.+make_vmlinux_pair()+{+localorig=()patched=()seen=arg++forargin"$@";do+if["$arg"=--];thenseen=y;continue;fi+if[-n"$seen"];thenpatched+=("$arg");elseorig+=("$arg");fi+done++partial_link"$workdir/vmlinux.o""${orig[@]}"||+probe_skip"partial link unavailable"+partial_link"$workdir/patched.o""${patched[@]}"||+probe_skip"partial link unavailable"++"$OBJTOOL"--klp-symids--link"$workdir/vmlinux.o"||+fail"objtool --klp-symids failed"++link_vmlinux"$workdir/vmlinux""$workdir/vmlinux.o"||+probe_skip"cannot link a vmlinux here"++orig_obj=vmlinux.o+}++# build_module_pair <fixture.c> <module name> [cflags...]+#+# Build the pair as objects belonging to a module rather than to vmlinux. klp+# diff reads the object's module name from .modinfo, and that decides which+# object a relocation is attributed to and whether a reference counts as+# cross-module, so a good deal of the code has a module path the vmlinux+# fixtures never reach.+#+# The fixture defines its .modinfo name from MODNAME. Passing that through+# -D needs two levels of quoting, which is easy to get wrong at the call site.+build_module_pair()+{+localfixture="$1"modname="$2";shift2++build_pair"$fixture"-DMODNAME="\"$modname\"""$@"+}+# find_thinlto_toolchain## Set $THIN_LD to an lld from the same LLVM release as $CC (or THIN_CC). A
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:02
Assertions for what klp diff produces, and for what went into it.
The output side covers the things a livepatch is made of: cloned sections
and symbols, klp symbols and their sympos, tombstones, relocations by
section and by count, the converted .klp.rela sections and their
SHF_RELA_LIVEPATCH flag, SHN_LIVEPATCH symbols, recorded checksums, and klp
diff's own diagnostics -- a rejection for the wrong reason is not a pass.
The input side matters just as much, and is easier to forget. A test which
asserts only on the output passes when the compiler never emitted the
construct under test, and reads as coverage it does not have. So there are
two forms, and the difference between them is a statement about why the
thing might be absent:
require_* the compiler cannot produce it here -> skip
assert_* the fixture is supposed to produce it -> fail
Names are quoted before they reach grep. Nearly every name here contains
dots -- .text.target, .klp.rela.vmlinux -- and an unescaped dot matches any
character, so an assertion for one section could be satisfied by a
different one whose name merely lines up.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/tests/lib.sh | 360 ++++++++++++++++++++++++++++++++++++-
1 file changed, 356 insertions(+), 4 deletions(-)
@@ -201,9 +201,42 @@ setup() export_syms(){:>"$workdir/Module.symvers"+add_exportsvmlinux"$@"+}++# add_exports <object> [symbol...]+#+# Append exports owned by one object, without clearing what is already there,+# so a test can describe a kernel where several objects export things.+#+# Which object owns a symbol is not cosmetic: a reference to a vmlinux symbol+# is applied when the patch module loads, and a reference to a module's symbol+# when that patched module loads, so klp diff files them in different sections.+add_exports()+{+localowner="$1";shift++add_exports_ns"$owner""""$@"+}++# add_exports_ns <object> <namespace> [symbol...]+#+# Exports in a symbol namespace, the last field of a Module.symvers line.+#+# A "module:<names>" namespace is EXPORT_SYMBOL_FOR_MODULES(), where the module+# loader grants access by matching the importing module's name against the+# list. A livepatch module is never on that list, so such a symbol has to be+# referenced the way an unexported one is. Ordinary namespaces are not+# special here.+add_exports_ns()+{+localowner="$1"ns="$2";shift2++localsym+forsymin"$@";do-printf'0x00000000\t%s\tvmlinux\tEXPORT_SYMBOL\t\n'\-"$sym">>"$workdir/Module.symvers"+printf'0x00000000\t%s\t%s\tEXPORT_SYMBOL\t%s\n'\+"$sym""$owner""$ns">>"$workdir/Module.symvers"done}
@@ -445,12 +478,87 @@ out_relocs() { $READELF -r -W "$workdir/out.o" 2>/dev/null; } out_symbols(){$READELF-s-W"$workdir/out.o"2>/dev/null;} diff_log(){cat"$workdir/diff.log";}+# out_strings <section>+#+# The strings in one section of the output, for the names livepatch matches on.+out_strings(){$READELF-p"$1""$workdir/out.o"2>/dev/null;}++# Checks on the input objects, to run before klp diff. The two forms differ in+# what an absent construct means:+#+# require_* the compiler cannot produce it here -> skip+# assert_* the fixture is supposed to produce it -> fail++in_sections(){$READELF-S-W"$workdir/$1"2>/dev/null;}+in_symbols(){$READELF-s-W"$workdir/$1"2>/dev/null;}+in_relocs(){$READELF-r-W"$workdir/$1"2>/dev/null;}++# count_input_symbols <object> <name>+#+# How many object symbols of exactly that name the input has. Deliberately not+# a grep: readelf lists section symbols too, and a newer binutils prints their+# name -- ".data.<name>" -- where an older one leaves the column blank. A dot+# is not a word character, so "grep -w <name>" counts that line as well, and+# the same object gives a different answer depending on which readelf reads it.+count_input_symbols()+{+in_symbols"$1"|awk-vn="$2"'$4 == "OBJECT" && $8 == n'|wc-l+}++# re_quote <string>+#+# A string as a literal basic regular expression. Nearly every name these+# assertions match on contains a dot -- .text.target, .klp.rela.vmlinux -- and+# an unescaped dot matches any character, so an assertion for one section can be+# satisfied by a different one whose name merely lines up.+re_quote(){printf'%s'"$1"|sed's/[].[^$*\\/]/\\&/g';}++has_input_section(){in_sections"$1"|grep-q"[[:space:]]$(re_quote"$2")[[:space:]]";}+has_input_symbol(){in_symbols"$1"|grep-qw--"$(re_quote"$2")";}++assert_input_section()+{+localobj++forobjin"$orig_obj""$patched_obj";do+has_input_section"$obj""$1"||+fail"fixture produced no section '$1' in $obj"+done+}++assert_input_symbol()+{+localobj++forobjin"$orig_obj""$patched_obj";do+has_input_symbol"$obj""$1"||+fail"fixture produced no symbol '$1' in $obj"+done+}++require_input_section()+{+localobj++forobjin"$orig_obj""$patched_obj";do+has_input_section"$obj""$1"||+probe_skip"compiler produced no section '$1' here"+done+}+ assert_section(){-out_sections|grep-q"[[:space:]]$1[[:space:]]"||+out_sections|grep-q"[[:space:]]$(re_quote"$1")[[:space:]]"||fail"expected section '$1' in output"}+assert_no_section()+{+out_sections|grep-q"[[:space:]]$(re_quote"$1")[[:space:]]"&&+fail"unexpected section '$1' in output"+return0+}+ assert_patched(){assert_section".text.$1"
@@ -458,7 +566,251 @@ assert_patched() assert_not_patched(){-out_sections|grep-q"[[:space:]].text.$1[[:space:]]"&&+out_sections|grep-q"[[:space:]]$(re_quote".text.$1")[[:space:]]"&&fail"function '$1' should not have been cloned"return0}++# section_relocs <section>+#+# The relocations against one section. readelf prints every relocation section+# in turn, so a test asking about ".smp_locks" has to cut its block out of the+# listing first.+section_relocs()+{+localsec="${1//./\\.}"++out_relocs|awk"/rela$sec'/,/^\$/"+}++assert_reloc_sym()+{+section_relocs"$1"|grep-qw--"$(re_quote"$2")"||+fail"expected a relocation to '$2' in '$1'"+}++assert_no_reloc_sym()+{+section_relocs"$1"|grep-qw--"$(re_quote"$2")"&&+fail"unexpected relocation to '$2' in '$1'"+return0+}++# assert_reloc_count <section> <count>+#+# Counts relocation entries, not header or blank lines: whether a special+# section entry was extracted once, twice or not at all is usually the whole+# question.+#+# A count of zero is ambiguous on its own -- a section with no relocations and+# no section at all both read as zero -- so require the section to exist. A+# test expecting nothing there wants assert_no_section.+assert_reloc_count()+{+localn++assert_section"$1"++n="$(section_relocs"$1"|grep-cE'^[0-9a-f]{8,}')"+["$n"="$2"]||+fail"expected $2 relocations in '$1', found $n"+}++# assert_klp_sym <symbol> [object]+#+# A klp symbol is named .klp.sym.<object>.<symbol>,<sympos>. The object+# defaults to any, since most tests care that the reference was converted at+# all rather than which object it resolved against.+assert_klp_sym()+{+out_symbols|grep-q"\.klp\.sym\.${2:-[^.]*}\.$(re_quote"$1"),"||+fail"expected klp symbol for '$1'"+}++# assert_klp_sympos <symbol> <sympos>+#+# The number after the comma in .klp.sym.<object>.<symbol>,<sympos> says which+# of several same-named symbols livepatch should resolve to, counting from 1;+# 0 means the name is unique and no disambiguation is needed. Resolving to the+# wrong one is not a load failure, it is a patch quietly wired to the wrong+# object.+assert_klp_sympos()+{+out_symbols|grep-qE"\.klp\.sym\.[^.]+\.$(re_quote"$1"),$2([[:space:]]|\$)"||+fail"expected klp symbol for '$1' with sympos $2, found:$(+out_symbols|grep-o"\.klp\.sym\.[^.]*\.$(re_quote"$1"),[0-9]*"|+sort-u|tr'\n'' ')"+}++assert_no_klp_sym()+{+out_symbols|grep-q"\.klp\.sym\.${2:-[^.]*}\.$(re_quote"$1"),"&&+fail"unexpected klp symbol for '$1'"+return0+}++assert_tombstone()+{+out_symbols|grep-qE"\.klp\.tombstone\.$(re_quote"$1")([[:space:]]|\$)"||+fail"expected a tombstone for '$1'"+}++assert_symbol()+{+out_symbols|grep-qw--"$(re_quote"$1")"||+fail"expected symbol '$1' in output"+}++assert_no_symbol()+{+out_symbols|grep-qw--"$(re_quote"$1")"&&+fail"unexpected symbol '$1' in output"+return0+}++# assert_diff_log <regex>+#+# klp diff's combined output, for tests asserting on a diagnostic. Error+# messages are part of the interface when the whole point is that a construct+# gets rejected, and a rejection for the wrong reason is not a pass.+assert_diff_log()+{+diff_log|grep-qE--"$1"||+fail"expected '$1' in klp diff output: $(tail-2"$workdir/diff.log")"+}++# checksum_of <object> <symbol>+#+# The checksum "klp checksum" recorded for one symbol, as a hex string.+#+# .discard.sym_checksum is an array of { u64 addr; u64 checksum; }, where addr+# is the target of a relocation naming the symbol. Nothing in the section+# itself says which symbol an entry belongs to, so the relocation is what+# locates the entry; the checksum is the eight bytes after it.+# Callers use this in a command substitution, where fail() would only exit the+# subshell and the test would carry on with an empty checksum. So this returns+# non-zero and prints nothing, and the assertions below check for that.+checksum_of()+{+localobj="$workdir/$1"sym="$2"off++run_checksum++off="$($READELF-rW"$obj"2>/dev/null|+awk-vs="$sym"'/rela\.discard\.sym_checksum/,/^$/{+if($5==s){print$1;exit}+}')"++[-n"$off"]||return1++$OBJCOPY-Obinary--only-section=.discard.sym_checksum\+"$obj""$workdir/checksums.bin"2>/dev/null||return1++ddif="$workdir/checksums.bin"bs=1skip=$((16#$off+8))count=8\+status=none|od-An-tx1|tr-d' \n'+}++# assert_checksum_differs <symbol> / assert_checksum_matches <symbol>+#+# Compare what klp checksum recorded for a symbol in the original against the+# patched object. This is what decides whether klp diff treats a function as+# changed, so a test asserting only that the right functions were cloned cannot+# tell a correct checksum from one which happens to differ.+checksum_pair()+{+orig_checksum="$(checksum_of"$orig_obj""$1")"+patched_checksum="$(checksum_of"$patched_obj""$1")"++[-n"$orig_checksum"]||+fail"no checksum recorded for '$1' in $orig_obj"+[-n"$patched_checksum"]||+fail"no checksum recorded for '$1' in $patched_obj"+}++assert_checksum_differs()+{+checksum_pair"$1"++["$orig_checksum"!="$patched_checksum"]||+fail"checksum for '$1' unchanged at $orig_checksum, expected it to differ"+}++assert_checksum_matches()+{+checksum_pair"$1"++["$orig_checksum"="$patched_checksum"]||+fail"checksum for '$1' changed from $orig_checksum to"\+"$patched_checksum, expected no change"+}++# run_post_link [expected exit status]+#+# klp post-link runs last in a livepatch build, converting the intermediate+# __klp_relocs.* sections into the .klp.rela.* form the kernel consumes. It+# needs nothing but an object containing those sections, which is what klp diff+# produces, so it runs on out.o here rather than on a built module. Rewrites+# out.o in place, so the out_* helpers show the result afterwards.+run_post_link()+{+localexpect="${1:-0}"rc=0++"$OBJTOOL"klppost-link"$workdir/out.o"\+>"$workdir/post-link.log"2>&1||rc=$?++["$rc"="$expect"]||+fail"klp post-link exited $rc, expected $expect:"\+"$(tail-2"$workdir/post-link.log")"+}++# The flags readelf prints for a section, or nothing when it has none. The+# leading "[nn]" index is stripped first so the columns can be counted.+section_flags()+{+out_sections|sed's/^ *\[[ 0-9]*\] *//'|+awk-vs="$1"'$1 == s && $7 ~ /^[A-Za-z]+$/ { print $7 }'+}++# assert_section_flag <section> <letter>+#+# SHF_RELA_LIVEPATCH is OS-specific, so readelf renders it as "o". A klp rela+# section which lost it is an ordinary rela section, which the linker may apply+# and the livepatch code will not.+assert_section_flag()+{+localflags;flags="$(section_flags"$1")"++[-n"$flags"]||+fail"section '$1' has no flags, expected '$2'"+case"$flags"in+*"$2"*);;+*)fail"section '$1' has flags '$flags', expected '$2'";;+esac+}++# assert_klp_rela <object> <section>+#+# post-link names the converted sections .klp.rela.<object>.<section>, one per+# base section. Also checks SHF_RELA_LIVEPATCH, since the name alone is not+# what makes the kernel process it.+assert_klp_rela()+{+localname=".klp.rela.$1.$2"++out_sections|grep-q"[[:space:]]$(re_quote"$name")[[:space:]]"||+fail"expected section '$name' in output"++assert_section_flag"$name"o+}++# assert_livepatch_sym <symbol>+#+# Symbols a klp relocation resolves against live in SHN_LIVEPATCH, which+# readelf prints as "OS [0xff20]" -- llvm-readelf without the space, so match+# either. The kernel resolves these itself at patch load; anything else is a+# symbol the module loader will try, and fail, to resolve normally.+assert_livepatch_sym()+{+out_symbols|grep-E'OS ?\[0xff20\]'|grep-qw--"$(re_quote"$1")"||+fail"expected '$1' to be an SHN_LIVEPATCH symbol"+}
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:07
Each test made its own mktemp directory, so a run scattered them through
/tmp among everything else using the same names, and there was no way to
inspect what a failing test built -- the objects are gone the moment it
exits, which is right every time but the one that matters.
Give the run one directory and each test a subdirectory of it, named for
the test and nested the way the source is:
/tmp/klp-tests.3zC6oyfd/
generic/test-basic/{orig.o,patched.o,out.o,Module.symvers,...}
x86/test-kcfi/...
--keep then reports one path instead of forty, and removing it is one
command. A test run by hand still falls back to a temp directory of its
own, and still says where that went.
Detecting a leak comes free. Cleanup is now "remove each test's directory,
then rmdir the run's", and the rmdir fails if anything is left -- so a test
which dies without running its own cleanup is reported rather than quietly
leaving something behind.
preflight names the directory the tests build under, since mktemp honours
TMPDIR and a run in a container is otherwise silent about where its work
went. The runner says what to do only when it is relevant: on a failure
without --keep, that the option exists; with it, how many directories are
waiting. Usage text is not read while something is broken.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/tests/lib.sh | 23 ++++++++++++++++++-----
tools/objtool/tests/run-tests.sh | 26 ++++++++++++++++++++++++--
2 files changed, 42 insertions(+), 7 deletions(-)
@@ -179,15 +180,27 @@ xpass()exit1}-cleanup(){[-n"$workdir"]&&rm-rf"$workdir";}+cleanup()+{+[-n"$workdir"]||return0++if[-n"${KLP_TEST_KEEP:-}"];then+[-n"${KLP_TEST_WORKDIR:-}"]||echo"# kept $workdir"+return0+fi++rm-rf"$workdir"+}# setup [exported symbol...] setup(){-# The environment was checked once when this file was sourced, so there-# is nothing to verify here: objtool exists at the resolved path, has-# klp support, and $CC works.-workdir="$(mktemp-d)"||fail"mktemp failed"+if[-n"${KLP_TEST_WORKDIR:-}"];then+workdir="$KLP_TEST_WORKDIR"+mkdir-p"$workdir"||fail"cannot create $workdir"+else+workdir="$(mktemp-d)"||fail"mktemp failed"+fitrapcleanupEXITexport_syms"$@"
@@ -98,12 +103,18 @@ elsedonefi+# One directory for the whole run, one per test inside it, mirroring the+# source layout. A run then leaves a single thing behind instead of 39+# scattered among everything else using mktemp.+rundir="$(mktemp-d"${TMPDIR:-/tmp}/klp-tests.XXXXXXXX")"||+{echo"Bail out! cannot create a working directory">&2;exit1;}+echo"1..${#tests[@]}"pass=0fail=0static_skip=0probe_skip=0xfail=0xpass=0fortin"${tests[@]}";do-out="$(./"$t"2>&1)"+out="$(KLP_TEST_WORKDIR="$rundir/${t%.sh}"./"$t"2>&1)"rc=$?# A test prints one result line, but it is not necessarily the only
@@ -156,4 +167,15 @@ doneecho"# pass:$pass fail:$fail static-skip:$static_skip"\"probe-skip:$probe_skip xfail:$xfail xpass:$xpass"+# A failure is the one time the objects matter, and by default they are+# already gone. Say so then rather than in the usage text nobody reads while+# something is broken.+if[-n"${KLP_TEST_KEEP:-}"];then+echo"# working directories kept in $rundir -- inspect, then rm -rf it"+elif!rmdir"$rundir"/*/"$rundir"2>/dev/null;then+echo"# $rundir was not empty; a test did not clean up after itself"+elif["$fail"!=0]||["$xpass"!=0];then+echo"# re-run with --keep to hold on to what a failing test built"+fi+["$fail"=0]&&["$xpass"=0]
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:13
Most of these assertions end in a grep, and an empty pattern matches
anything. So a mistyped variable name in a test does not fail: it expands
to nothing, the grep succeeds, and the test reports a pass having checked
nothing. assert_section "$sectoin_nmae" becomes a search for two adjacent
spaces, which readelf's column padding satisfies on every line.
Refuse unset variables. lib.sh guards the few genuinely optional
expansions it has with ${x:-}.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/tests/lib.sh | 2 ++
1 file changed, 2 insertions(+)
@@ -8,6 +8,8 @@# codegen varies between compilers and golden files would report churn instead# of regressions.+set-u+TESTS_DIR="$(cd"$(dirname"${BASH_SOURCE[0]}")"&&pwd)"# Tests live in generic/ or in an architecture directory beside it, and each
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:18
Two documents, for two different readers.
klp-test-design.txt is for someone deciding whether to trust the suite or
change it: what makes unit tests possible for a pipeline that normally
needs two kernel builds (objtool has no configuration-dependent logic, so a
test reproduces a configuration by reproducing its input), how tests are
selected per architecture, and what the outcome classification is for.
klp-write-tests.txt is the procedure for adding one, and is written to be
handed to someone -- or something -- with the instruction "follow this and
write a test for commit <sha>", or "port the case at <location> from
another harness". It leads with the rule that matters: a test is not
finished until you have watched it fail with the code broken. Then the
traps, each of which this suite has already fallen into -- a fixture the
compiler optimised away, a named char[] which never reaches the string path
because that keys on SHF_STRINGS, per-function sections hiding the movement
a test was about.
Some of this was previously written as comments in lib.sh. That was the
wrong place: guidance on how to write a test is read once by an author,
while every comment in the harness is read by everyone who opens it.
Explanations of why the code is as it is stay where they were.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../objtool/Documentation/klp-test-design.txt | 272 ++++++++++++++++++
.../objtool/Documentation/klp-write-tests.txt | 260 +++++++++++++++++
2 files changed, 532 insertions(+)
create mode 100644 tools/objtool/Documentation/klp-test-design.txt
create mode 100644 tools/objtool/Documentation/klp-write-tests.txt
@@ -0,0 +1,272 @@+.. SPDX-License-Identifier: GPL-2.0++======================================+Design of the objtool klp test harness+======================================++tools/objtool/tests/ holds unit tests for the klp subcommands of+objtool -- ``klp checksum``, ``klp diff``, ``klp post-link`` and+``--klp-symids`` -- which together turn two builds of the kernel into a+livepatch module.++This document explains how the harness is built and why. For the rules to+follow when adding a test, see klp-write-tests.txt.+++TL;DR+=====++One run covers one compiler and one architecture; CI runs the combinations.+Build objtool first -- it needs libelf and libxxhash -- and the same ARCH is+used for both steps.++Natively, with gcc::++ make -C tools/objtool+ make -C tools/objtool tests++Natively, with clang -- LLVM=1 additionally selects the LLVM binutils::++ CC=clang make -C tools/objtool tests+ LLVM=1 make -C tools/objtool tests++Cross, with gcc -- an arm64 host running the x86 tests::++ ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- make -C tools/objtool+ ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- make -C tools/objtool tests++Cross, with clang. It defaults to the host triple however it is invoked, so+--target= is what makes it emit x86; OBJCOPY is needed because BFD's is+usually built for the host's target alone::++ ARCH=x86_64 make -C tools/objtool+ ARCH=x86_64 CC="clang --target=x86_64-linux-gnu" OBJCOPY=llvm-objcopy \+ make -C tools/objtool tests++A run ends with a totals line; anything other than fail:0 is a real result::++ # pass:48 fail:0 static-skip:1 probe-skip:0 xfail:0 xpass:0++Useful extras::++ tools/objtool/tests/run-tests.sh basic # one test, by name+ tools/objtool/tests/run-tests.sh --keep basic # and keep what it built+++Why unit tests are possible at all+==================================++klp-build is a pipeline: build the kernel twice, checksum both, diff them,+link the result. Testing that end to end means two kernel builds per case,+which is too slow to run often and too heavy to keep in the tree.++Three properties make a much cheaper test possible.++**objtool has no configuration-dependent logic.** It never reads ``.config``.+Every ``CONFIG_`` string in its source is a comment or one error message, and+its only build-time conditionals are driven by host libraries and the target+architecture. Configuration reaches objtool through exactly two channels: the+``objtool-args-$(CONFIG_*)`` lines in scripts/Makefile.lib, and the+contents of the object handed to it.++**The klp subcommands use none of the first channel.** Of objtool's options+they consult three -- ``checksum``, ``debug_checksum``, ``dryrun`` -- all from+their own command line. So klp behaviour varies with configuration *only*+through the input object.++**Therefore a test can reproduce any configuration's behaviour by reproducing+its input.** Compile a small freestanding fixture with the flags that+configuration would have used, and objtool cannot tell the difference. No+kernel, no ``.config``, no object cache.++The whole suite runs in a few seconds.+++Shape of a test+===============++Each test compiles one fixture twice -- once plain, once with ``-DPATCHED`` --+runs ``klp checksum`` over both, diffs them, and asserts on properties of the+output object::++ . "$(dirname "$0")/../lib.sh"++ setup+ build_pair basic.c++ assert_input_symbol changed+ run_diff++ assert_patched changed+ assert_not_patched untouched++ pass "changed function cloned, unchanged function left alone"++Assertions check properties, never recorded output. Codegen varies between+compilers and versions, so a golden file would report churn rather than+regressions.+++Layout+======++::++ tools/objtool/tests/+ lib.sh the harness: everything a test may call+ run-tests.sh selects, runs and classifies+ generic/+ test-*.sh+ fixtures/*.c+ x86/+ test-*.sh+ fixtures/*.c++Which architecture a test is for is expressed by where it lives. The runner+executes ``generic/`` plus the directory matching this architecture, so a test+which cannot apply is not run rather than running in order to report that it+did not. There is no ``x86_only`` helper, and no lookup letting an+architecture fixture shadow a generic one: an architecture-specific test+carries its own fixtures.++Compilers cannot be expressed the same way, because CI varies ``CC`` over the+same tree. A compiler requirement stays a declaration inside the test+(``gcc_only``, ``clang_only``).+++The environment is established once+===================================++Sourcing lib.sh runs ``klp_preflight``, which checks that objtool+exists and has klp support, that ``$CC`` works, that the binutils are present,+and which architecture this is. The answers are exported, so:++* ``run-tests.sh`` sources lib.sh too, and therefore knows the+ architecture before it chooses which tests to run;+* each test inherits the answers rather than repeating the work;+* a test run on its own establishes them for itself.++Preflight answers only whether the suite can run at all. A suite which cannot+run must not exit 0 looking like one which passed, so a missing objtool fails+the whole run with a TAP ``Bail out!`` rather than skipping each test in turn.+What a *particular* compiler can do is a different question, left to the test+which cares.+++Outcomes+========++Output is TAP. The distinction the harness cares most about is between kinds+of skip, because a skip is how a suite quietly stops testing anything:++``declared``+ The test said in advance it does not apply -- ``gcc_only`` on a clang run.+ Expected indefinitely.++``probe``+ The construct did not turn up in the built object this time. Weaker: one+ which becomes permanent is a fixture that has stopped testing anything.++``undeclared``+ Counted as a **failure**. A test which gives up for a reason it never+ declared is a hole, not an outcome.++``xfail``/``xpass`` come with them, so a known failure is reported rather than+commented out, and one which starts passing says so instead of going quietly+green.++The runner classifies the TAP result line, not everything a test printed:+objtool warns on stderr and that output is captured, so a stray line ahead of+the result would otherwise leave the exit status to decide -- and an expected+failure exits 0.++A run ends with a totals line::++ # pass:48 fail:0 static-skip:1 probe-skip:0 xfail:0 xpass:0++and reports what it left out::++ # not run: 5 tests in x86/ (this run is arm64)+++Working directories+===================++A run gets one directory; each test gets a subdirectory of it, mirroring the+source layout::++ /tmp/klp-tests.XXXXXXXX/+ generic/test-basic/{orig.o,patched.o,out.o,Module.symvers,...}+ x86/test-kcfi/...++``--keep`` leaves it and reports the one path. Otherwise each test removes its+own directory and the runner ``rmdir``s the run's -- which fails if anything+was left behind, so a test which dies without cleaning up is reported rather+than silently leaking.+++Running+=======++::++ make -C tools/objtool # needs libelf and libxxhash+ make -C tools/objtool tests++ CC=clang make -C tools/objtool tests # the other toolchain+ LLVM=1 make -C tools/objtool tests # and its binutils too++ tools/objtool/tests/run-tests.sh --keep basic # one test, keep its objects++A run covers one compiler and one architecture; CI runs the combinations.++Cross-compiled runs+-------------------++objtool klp is built only where ARCH_HAS_KLP is set, which today means x86 --+so an arm64 machine cannot run any of this natively. It can run all of it+cross, because objtool is a host tool that only reads and rewrites ELF, and+the tests only compile fixtures and inspect the objects. Nothing has to+execute target code.++::++ ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- make -C tools/objtool+ ARCH=x86_64 CROSS_COMPILE=x86_64-linux-gnu- make -C tools/objtool tests++objtool itself stays a native binary: it is built with HOSTCC, not CC, so+setting a cross compiler cannot produce one the host is unable to run. ARCH+selects both the objtool target and the directory of tests to run.++clang needs telling, since it defaults to the host triple however it is+invoked. The fixtures include no kernel headers, so no sysroot is needed:++::++ ARCH=x86_64 CC="clang --target=x86_64-linux-gnu" \+ OBJCOPY=llvm-objcopy make -C tools/objtool tests++CROSS_COMPILE picks the binutils, and each can be overridden on its own.+readelf reads any target and rarely needs overriding; BFD's objcopy is usually+built for the host's alone, hence OBJCOPY=llvm-objcopy above, or install+binutils-multiarch.++Either readelf will do. The assertions read readelf's output, and the two+spell some of it differently -- GNU prints "OS [0xff20]" for SHN_LIVEPATCH+where llvm-readelf prints "OS[0xff20]" -- so they accept both.++Getting this wrong is easy and the harness refuses rather than producing a+misleading result. "CC=clang ARCH=x86_64" alone selects the x86 tests and+then builds arm64 objects; preflight compiles a probe object, hands it to+objtool, and stops the run if they disagree about the architecture, or if+ARCH does not match what the compiler emits.+++What this does not cover+========================++These are unit tests for objtool's klp subcommands. They do not build a+kernel, do not run scripts/livepatch/klp-build, and do not load a+livepatch. Behaviour which only appears when the kernel applies a patch --+the module loader refusing a relocation, late module patching ordering -- has+to be tested by booting, and is out of scope here.
@@ -0,0 +1,260 @@+.. SPDX-License-Identifier: GPL-2.0++=====================================+Writing a test for objtool's klp code+=====================================++Instructions for adding a test to tools/objtool/tests/. Read+klp-test-design.txt first if you need to know how the harness works; this+document is the procedure and the rules.++Two kinds of request bring you here:++* *"write a test for commit <sha>"* -- a fix went in without one.+* *"port the test at <location>, written against another harness"* -- a case+ exists elsewhere and should live in tree.++Both follow the same procedure.+++The one rule that matters+=========================++**A test is not finished until you have watched it fail.**++Break the thing it guards -- revert the fix, or sabotage the exact line -- and+confirm the test fails. Then restore and confirm it passes. A test that has+never failed is not known to test anything, and this suite has produced+several that passed against deliberately broken code:++* an alternatives fixture whose empty entry pointed at its own end label rather+ than the neighbour's replacement, so the bug it guarded made no difference;+* a sympos fixture where symbol-table order and address order agreed, so+ counting and reading the linked image gave the same answer;+* a string fixture using a named ``char[]``, which never reached the+ contents-hashing path because that keys on ``SHF_STRINGS``;+* a static array the compiler proved constant, folded to zero, and emitted no+ relocation for -- so the two builds were byte-identical.++Every one looked correct. Say in the commit message how you verified, and if+you could not isolate the behaviour to a single line, **say that too** rather+than implying otherwise.+++Procedure+=========++1. **Read the fix.** What input reaches the broken line? What is observable+ in the output object when it misbehaves -- a missing section, a relocation+ naming the wrong symbol, an unchanged checksum, a rejected build? If+ nothing is observable, stop and say so; see `When to give up`_.++2. **Decide where it lives.** ``generic/`` unless the fixture needs+ architecture-specific assembly or the behaviour is architecture-specific,+ in which case ``x86/`` (or a new directory named for the architecture).++3. **Write the fixture** in the same directory's ``fixtures/``. Reuse an+ existing one if it already produces the shape; add a ``-D`` knob rather+ than copying a fixture to change one line.++4. **Write the test.** Assert the *premise* before the result -- see+ `State the premise`_.++5. **Verify by breaking the code.** Then restore.++6. **Run the whole suite under both compilers**::++ make -C tools/objtool tests+ CC=clang make -C tools/objtool tests++7. **Commit** the test and its fixture together, alone. One test per commit.+++Writing the fixture+===================++Fixtures are freestanding C. No kernel headers -- write out the kernel+structure by hand if you need one, as the existing special-section fixtures do.++Every fixture needs a ``.modinfo`` name, because klp diff reads the object's+module name from it::++ static const char __modinfo[]+ __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux";++Use ``MODNAME`` if the test needs to vary it. Note that most fixtures hardcode+``vmlinux``: passing ``-DMODNAME`` to one that does silently does nothing and+the test quietly becomes a vmlinux test.++The patched build is selected with ``-DPATCHED``. For a fixture with several+variants, gate each on both, so the original is always the baseline::++ #if defined(PATCHED) && defined(WHICH_CALL)+ r = callee_b(x);+ #else+ r = callee_a(x);+ #endif++and select one per build: ``build_pair foo.c -DWHICH_CALL``. Without the+``defined(PATCHED)`` the flag applies to *both* builds and nothing differs.++Traps that have bitten before+-----------------------------++* **The compiler optimises your fixture away.** A static never written is+ proved constant, its reads folded, and no relocation emitted. Add a writer+ the compiler cannot see through.+* **String literals versus named arrays.** The contents-hashing path keys on+ ``SHF_STRINGS``, which the compiler sets on the mergeable section a *literal*+ lands in, not on a ``char[]`` given a section of its own.+* **Per-function sections hide movement.** With the default+ ``-ffunction-sections`` every function sits at offset 0 of its own section,+ so nothing ever moves. A test about position needs+ ``build_pair foo.c -fno-function-sections``.+* **Special sections need boundaries.** Either an entsize on the section or an+ ``ANNOTATE_DATA_SPECIAL`` annotation, or klp diff reports "missing special+ section entsize or annotations". Their targets need real (global) symbols,+ or it reports "failed to convert reloc sym".+* **Do not hand-write what objtool generates.** ``.static_call_sites``,+ ``.mcount_loc``, ``.ibt_endbr_seal`` and ORC come from objtool's check pass.+ Call ``run_objtool_check --mcount`` and let it build them; a hand-written+ copy tests your reading of the format, not the format.+++Writing the test+================++Start from the shortest existing test, generic/test-basic.sh.++State the premise+-----------------++A test which asserts only on the output passes when the compiler never emitted+the construct in the first place, and reads as coverage it does not have. Say+what the input must contain::++ assert_input_section __jump_table # the fixture must produce it -> fail+ require_input_section .kcfi_traps # this compiler may not -> skip++Prefer ``assert_*``. Reach for ``require_*`` only where absence genuinely+depends on compiler version or flags, and follow it with something+unconditional so the test can never be entirely vacuous.++Where a test would otherwise duplicate a sibling, assert what makes it+different. ``test-jump-label-module-static-key`` checks that the key really is+reached through its section symbol -- without that it is a second copy of+``test-jump-label-module-key``.++Assert both directions+----------------------++Check that the right thing happened *and* that the wrong thing did not. A klp+diff which clones everything is as wrong as one which clones nothing::++ assert_patched changed+ assert_not_patched untouched++Skips+-----++* ``gcc_only``/``clang_only`` -- a settled fact about the compiler. Declared,+ so it reads as expected forever.+* ``probe_skip`` -- this toolchain did not produce the construct. Include what+ to do about it if there is anything::++ probe_skip "no matching clang/lld pair for a ThinLTO link; set THIN_CC and THIN_LD to one"++* A bare ``skip`` is **counted as a failure**. Never use it.++Standing in for a kernel configuration+--------------------------------------++``FIXTURE_CFLAGS`` is what a fixture is built with. Since objtool reads no+``.config``, changing these flags is how a test covers a configuration without+building a kernel. Two ways:++* trailing arguments to ``build_pair``/``build_one``, which win, and cover+ anything expressible as a negation::++ build_pair foo.c -fno-function-sections++* otherwise assign ``FIXTURE_CFLAGS`` before building.++Either way **say in a comment which kernel configuration the change stands in+for**. A flag with no stated motive is indistinguishable from a mistake.+++What the test's comment must say+================================++The comment at the top is the test's justification. It should let a reader+decide, without archaeology, whether a skip or a failure matters. Include:++* **what breaks** in the running kernel if the behaviour regresses -- not the+ mechanism, the consequence;+* **why it is not caught otherwise**, which is usually "nothing fails at build+ time";+* **the fix commit** it guards, if there is one;+* **anything load-bearing about the fixture** that is not obvious, especially+ anything you got wrong first.++That last point is the one people skip. If the fixture has to be built without+per-function sections, or the static must not be named ``__warned``, or the key+must be file-local -- write it down, or the next person will simplify it away.+++Porting a test from another harness+===================================++Read the original's *case*, not its code. The other harness probably builds a+real kernel module; here you write freestanding C. A transliteration will+usually test something else.++* Work out which objtool behaviour the case exercises, then produce that shape+ the cheapest way here.+* Verify by breaking the code, exactly as for a new test -- a port is not+ correct because the original was.+* If the original names a fix commit, cite it.+* Credit the source in the commit message with the trailers the original+ carried, followed by your own.++Sometimes the port shows the case is already covered, and sometimes it shows+the case cannot be reproduced here. Both are results; report them rather than+committing something that passes vacuously.+++When to give up+===============++Some behaviour cannot be reached from a compiled fixture. Say so, with what+you tried, instead of committing a test that passes either way. Examples that+were genuinely abandoned:++* a memory leak -- needs valgrind, not an assertion on ELF;+* ``mkstemp`` with long paths, and other I/O edge cases;+* a NULL dereference reachable only through a debug path;+* changes made redundant by a fallback: removing the code changes no output+ because something else already handles the case;+* a fix whose code has since been rewritten, so there is nothing left to+ revert.++Also stop when the behaviour depends on something outside the fixture's+control -- an ELF library's handling of empty sections, or a compiler version's+naming of anonymous data. A test which passes for you and skips for everyone+else is worse than none.+++Checklist+=========++Before committing:++* the test fails with the code broken, and passes with it fixed+* the whole suite passes under **both** gcc and clang+* the premise is asserted, not assumed+* both directions are asserted where that applies+* no bare ``skip``+* the fixture is in the same directory as the test+* the comment names the consequence, the fix commit, and anything load-bearing+* the commit contains one test and its fixtures, and nothing else+* the commit message says how you verified it
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:23
From: Joe Lawrence <joe.lawrence@redhat.com>
The klp test runner deletes each test's working directory when the test
finishes. On failure that removes diff.log, out.o and the fixture objects
before the developer can look at them, and the only hint was to re-run
(something) with --keep:
$ make -C tools/objtool tests
...
not ok - test-special-section: expected section '.kcfi_traps' in output
...
# pass:49 fail:1 static-skip:1 probe-skip:0 xfail:0 xpass:0
# re-run with --keep to hold on to what a failing test built
Change the default behavior to save failing test working directories. This
is friendlier to the drive-by tester. Provide the power-user with a KEEP
Makefile variable that can optionally retain 'all' or 'none' of those
directories. run-tests.sh --keep remains an alias for KEEP=all.
Signed-off-by: Joe Lawrence <joe.lawrence@redhat.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../objtool/Documentation/klp-test-design.txt | 14 ++--
tools/objtool/Makefile | 2 +-
tools/objtool/tests/lib.sh | 47 ++++++++---
tools/objtool/tests/run-tests.sh | 78 +++++++++++++++----
4 files changed, 108 insertions(+), 33 deletions(-)
@@ -199,10 +199,11 @@ source layout:: generic/test-basic/{orig.o,patched.o,out.o,Module.symvers,...} x86/test-kcfi/...-``--keep`` leaves it and reports the one path. Otherwise each test removes its-own directory and the runner ``rmdir``s the run's -- which fails if anything-was left behind, so a test which dies without cleaning up is reported rather-than silently leaking.+By default (``KEEP=failed``) only failing tests keep their directories; the+runner reports where they are. ``KEEP=all`` keeps every test's directory;+``KEEP=none`` removes them all. The runner ``rmdir``s the run directory when+it is empty -- which fails if anything was left behind unexpectedly, so a test+which dies without cleaning up is reported rather than silently leaking. Running
@@ -216,7 +217,10 @@ Running CC=clang make -C tools/objtool tests # the other toolchain LLVM=1 make -C tools/objtool tests # and its binutils too- tools/objtool/tests/run-tests.sh --keep basic # one test, keep its objects+ make -C tools/objtool tests KEEP=all # keep every test's workdir+ make -C tools/objtool tests KEEP=none # remove all workdirs++ tools/objtool/tests/run-tests.sh basic # one test; failures kept by default A run covers one compiler and one architecture; CI runs the combinations.
@@ -148,8 +148,8 @@ workdir=orig_obj=orig.opatched_obj=patched.o-pass(){echo"ok - $test_name${1:+: $1}";exit0;}-fail(){echo"not ok - $test_name: $1";exit1;}+pass(){KLP_TEST_REPORTED=1;echo"ok - $test_name${1:+: $1}";exit0;}+fail(){KLP_TEST_FAILED=1;echo"not ok - $test_name: $*";exit1;}# Two kinds of skip, and the runner tells them apart.#
@@ -162,8 +162,19 @@ fail() { echo "not ok - $test_name: $1"; exit 1; }## A bare skip() is neither, and the runner counts it as a failure: a test which# gives up for a reason it never declared is a hole, not an outcome.-declared_skip(){echo"ok - $test_name # SKIP (declared) $1";exit0;}-probe_skip(){echo"ok - $test_name # SKIP (probe) $1";exit0;}+declared_skip()+{+KLP_TEST_REPORTED=1+echo"ok - $test_name # SKIP (declared) $1"+exit0+}++probe_skip()+{+KLP_TEST_REPORTED=1+echo"ok - $test_name # SKIP (probe) $1"+exit0+} skip(){echo"ok - $test_name # SKIP $1";exit0;}# TAP directives. A test which is known to fail reports it rather than being
@@ -172,12 +183,14 @@ skip() { echo "ok - $test_name # SKIP $1"; exit 0; }# which is the point. xfail(){+KLP_TEST_REPORTED=1echo"not ok - $test_name${1:+: $1} # TODO known failure"exit0} xpass(){+KLP_TEST_FAILED=1echo"ok - $test_name${1:+: $1} # TODO expected failure, but passed"exit1}
@@ -186,12 +199,26 @@ cleanup(){[-n"$workdir"]||return0-if[-n"${KLP_TEST_KEEP:-}"];then-[-n"${KLP_TEST_WORKDIR:-}"]||echo"# kept $workdir"-return0-fi--rm-rf"$workdir"+case"${KLP_TEST_KEEP:-failed}"in+all)return0;;+none)rm-rf"$workdir";;+failed|*)+# Keep what the runner is going to point at. It counts as a+# failure anything which did not report an expected outcome --+# including a test which died before printing one, and an+# undeclared skip -- and none of those set KLP_TEST_FAILED, so+# the question to ask is whether a result was reported at all.+# An exit status cannot answer it: a test killed by a signal+# runs this trap with the status of whatever ran last.+[-n"${KLP_TEST_REPORTED:-}"]&&[-z"${KLP_TEST_FAILED:-}"]&&{+rm-rf"$workdir"+return0+}+# run on its own there is no runner to say where it was kept+[-n"${KLP_TEST_WORKDIR:-}"]||+echo"# kept $workdir"+;;+esac}# setup [exported symbol...]
@@ -31,13 +31,15 @@ A test may be named with or without its "test-" prefix and ".sh" suffix, and is lookedforinbothdirectories. Options:--k,--keepdonotdeleteeachtest'sworkingdirectory;printitspath,-sotheobjectsafailingtestbuiltcanbelookedat+-k,--keepsameasKEEP=all(seebelow) Environment:OBJTOOLobjtoolbinarytotest(default../objtool)CCcompilerusedtobuildfixtures(defaultgcc)ARCHarchitecturethetestsarefor(default:uname-m)+KEEPfailedkeeponlyfailingtests(default)+allkeepeverytest'sworkingdirectory+noneremoveallworkingdirectories Atestwhichneedssomethingofitsownsayssoinitsskipmessage. EOF
@@ -46,16 +48,28 @@ EOFcd"$(dirname"$0")"||exit1+keep_from_args=while[$#-gt0];docase"$1"in-h|--help)usage;;--k|--keep)exportKLP_TEST_KEEP=1;shift;;+-k|--keep)keep_from_args=all;shift;;--)shift;break;;-*)echo"unknown option: $1">&2;usage1;;*)break;;esacdone+KLP_TEST_KEEP="${KEEP:-failed}"+[-n"$keep_from_args"]&&KLP_TEST_KEEP="$keep_from_args"+case"$KLP_TEST_KEEP"in+all|none|failed);;+*)+echo"invalid KEEP=$KLP_TEST_KEEP (want failed, all, or none)">&2+exit1+;;+esac+exportKLP_TEST_KEEP+echo"TAP version 13"# Sourcing the harness runs its preflight, which decides which architecture
@@ -147,16 +162,16 @@ for t in "${tests[@]}"; dorest="$rest${rest:+$'\n'}was: $result"result="not ok - $(basename"$t".sh): undeclared skip"result="$result (use gcc_only/clang_only or require_input_*)"-fail=$((fail+1));;+fail=$((fail+1));failed_dirs+=("$rundir/${t%.sh}");;"not ok"*"# TODO"*)xfail=$((xfail+1));;-"ok"*"# TODO"*)xpass=$((xpass+1));;-"not ok"*)fail=$((fail+1));;+"ok"*"# TODO"*)xpass=$((xpass+1));failed_dirs+=("$rundir/${t%.sh}");;+"not ok"*)fail=$((fail+1));failed_dirs+=("$rundir/${t%.sh}");;"ok"*)pass=$((pass+1));;*)# No result line at all: the test died before reporting.rest="$rest${rest:+$'\n'}exited $rc without a result line"result="not ok - $(basename"$t".sh): no TAP result"-fail=$((fail+1));;+fail=$((fail+1));failed_dirs+=("$rundir/${t%.sh}");;esacecho"$result"
@@ -167,15 +182,44 @@ doneecho"# pass:$pass fail:$fail static-skip:$static_skip"\"probe-skip:$probe_skip xfail:$xfail xpass:$xpass"-# A failure is the one time the objects matter, and by default they are-# already gone. Say so then rather than in the usage text nobody reads while-# something is broken.-if[-n"${KLP_TEST_KEEP:-}"];then-echo"# working directories kept in $rundir -- inspect, then rm -rf it"-elif!rmdir"$rundir"/*/"$rundir"2>/dev/null;then-echo"# $rundir was not empty; a test did not clean up after itself"-elif["$fail"!=0]||["$xpass"!=0];then-echo"# re-run with --keep to hold on to what a failing test built"-fi+case"$KLP_TEST_KEEP"in+all)+echo"# keep=all: workdirs kept in $rundir"+echo"# inspect: diff.log, readelf -S out.o under each test-* subdirectory"+echo"# cleanup: rm -rf $rundir"+;;+failed)+if["${#failed_dirs[@]}"-gt0];then+echo"# keep=failed: ${#failed_dirs[@]} failing test(s) kept under $rundir:"+fordin"${failed_dirs[@]}";do+echo"# ${d#"$rundir"/}/"+done+echo"# inspect: diff.log readelf -S out.o"+echo"# one test: $PWD/run-tests.sh <name>"+echo"# cleanup: rm -rf $rundir"+else+# Two levels: each test's own directory, then the one per source+# directory holding them. Either rmdir may fail -- the glob stays+# unexpanded when nothing was created -- so ask the directory+# itself whether anything is left rather than trusting the status.+rmdir"$rundir"/*/2>/dev/null+rmdir"$rundir"2>/dev/null+if[-d"$rundir"];then+echo"# $rundir was not empty;"\+"a test did not clean up after itself"+fi+fi+;;+none)+if!rmdir"$rundir"/*/"$rundir"2>/dev/null;then+if["$fail"!=0]||["$xpass"!=0];then+echo"# keep=none: artifacts were removed"\+"(re-run with KEEP=failed or KEEP=all)"+elif[-d"$rundir"];then+echo"# $rundir was not empty; a test did not clean up after itself"+fi+fi+;;+esac["$fail"=0]&&["$xpass"=0]
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:29
From: Joe Lawrence <joe.lawrence@redhat.com>
The tests target already passes ARCH and CROSS_COMPILE into
run-tests.sh. lib.sh uses those to pick a default compiler and
binutils:
CC=${CC:-${CROSS_COMPILE}gcc}
LD=${LD:-${CROSS_COMPILE}ld}
READELF=${READELF:-${CROSS_COMPILE}readelf}
OBJCOPY=${OBJCOPY:-${CROSS_COMPILE}objcopy}
Those defaults only apply when the variables reach the shell that sources
lib.sh. The recipe passed OBJTOOL, ARCH and CROSS_COMPILE explicitly, but
not CC or the binutils. Makefile-assigned values (e.g. from LLVM=1) are
not visible in the recipe environment unless forwarded:
$ LLVM=1 make -C tools/objtool tests
...
# preflight
# compiler gcc (GCC) ...
Pass CC, LD, READELF and OBJCOPY the same way as ARCH and CROSS_COMPILE,
so fixtures are built and inspected with the toolchain make selected:
$ LLVM=1 make -C tools/objtool tests
...
# preflight
# compiler clang version 21.1.8 ...
Signed-off-by: Joe Lawrence <joe.lawrence@redhat.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/Makefile | 1 +
1 file changed, 1 insertion(+)
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:34
From: Puranjay Mohan <puranjay@kernel.org>
Livepatching replaces functions. Nothing can swap a variable which live
code already refers to, so a patch which changes one has to be refused
rather than applied with the old value left in place.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/changed_data.c | 16 ++++++++++++++++
.../objtool/tests/generic/test-changed-data.sh | 18 ++++++++++++++++++
2 files changed, 34 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/changed_data.c
create mode 100755 tools/objtool/tests/generic/test-changed-data.sh
@@ -0,0 +1,16 @@+// SPDX-License-Identifier: GPL-2.0+/* Data whose value differs between the two builds. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++#ifdef PATCHED+intklp_test_data=2;+#else+intklp_test_data=1;+#endif++inttarget(intx)+{+returnx+klp_test_data;+}
@@ -0,0 +1,18 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Livepatching replaces functions, not data. A changed data symbol must be+# rejected.++."$(dirname"$0")/../lib.sh"++setup+build_pairchanged_data.c+run_diff255++diff_log|grep-q'changed data: klp_test_data'||+fail"expected rejection, got: $(diff_log|tail-1)"+[-e"$workdir/out.o"]&&+fail"output object produced for a rejected input"++pass"changed data symbol rejected"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:39
From: Puranjay Mohan <puranjay@kernel.org>
Adding data differs from changing it: nothing in the running kernel refers
to a new variable, so it is safe and has to travel into the livepatch with
the function using it.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../objtool/tests/generic/fixtures/new_data.c | 18 +++++++++++++
tools/objtool/tests/generic/test-new-data.sh | 26 +++++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/new_data.c
create mode 100755 tools/objtool/tests/generic/test-new-data.sh
@@ -0,0 +1,18 @@+// SPDX-License-Identifier: GPL-2.0+/* Data introduced by the patch. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++#ifdef PATCHED+staticconstintklp_new_data[4]__attribute__((used))={1,2,3,4};+#endif++inttarget(intx)+{+#ifdef PATCHED+returnx+klp_new_data[x&3];+#else+returnx;+#endif+}
@@ -0,0 +1,26 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Data added by the patch has no counterpart in the running kernel and must be+# carried into the livepatch.++."$(dirname"$0")/../lib.sh"++setup+build_pairnew_data.c++# State the premise on both sides. The array is new in the patched build and+# absent from the original; if the compiler folded it into the code instead of+# emitting it, the assertion below would fail without saying why.+has_input_symbol"$orig_obj"klp_new_data&&+fail"fixture put klp_new_data in the original; nothing new to carry"+has_input_symbol"$patched_obj"klp_new_data||+fail"compiler did not emit klp_new_data; the fixture tests nothing"++run_diff++assert_patchedtarget+out_symbols|grep-q'klp_new_data'||+fail"new data was not carried into the patch"++pass"new data carried into the patch"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:44
From: Puranjay Mohan <puranjay@kernel.org>
A function added by the patch has no original to correlate against, and
still has to be carried into the livepatch or the changed caller ends up
referencing something which does not exist.
The helper is noinline so the case survives the optimiser; otherwise the
compiler folds it into its only caller and the test covers nothing.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/new_function.c | 21 +++++++++++++++++++
.../tests/generic/test-new-function.sh | 17 +++++++++++++++
2 files changed, 38 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/new_function.c
create mode 100755 tools/objtool/tests/generic/test-new-function.sh
@@ -0,0 +1,21 @@+// SPDX-License-Identifier: GPL-2.0+/* Function introduced by the patch. noinline keeps it from being folded. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++#ifdef PATCHED+static__attribute__((noinline))intklp_new_helper(intx)+{+returnx*7;+}+#endif++inttarget(intx)+{+#ifdef PATCHED+returnklp_new_helper(x);+#else+returnx;+#endif+}
@@ -0,0 +1,17 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A function added by the patch has no original to correlate against and must+# still be carried into the livepatch.++."$(dirname"$0")/../lib.sh"++setup+build_pairnew_function.c+run_diff++assert_patchedtarget+out_symbols|grep-q'klp_new_helper'||+fail"new function was not carried into the patch"++pass"new function carried into the patch with its caller"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:50
From: Puranjay Mohan <puranjay@kernel.org>
A static local must be correlated with its original rather than duplicated.
The replacement has to reach the existing variable through a klp
relocation; a fresh definition would discard whatever state the running
kernel accumulated.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/static_local.c | 17 +++++++++++++
.../tests/generic/test-static-local.sh | 24 +++++++++++++++++++
2 files changed, 41 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/static_local.c
create mode 100755 tools/objtool/tests/generic/test-static-local.sh
@@ -0,0 +1,17 @@+// SPDX-License-Identifier: GPL-2.0+/* Static local in a patched function. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++inttarget(intx)+{+staticintcounter;++counter+=1;+#ifdef PATCHED+returnx+counter+1;+#else+returnx+counter;+#endif+}
@@ -0,0 +1,24 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A static local must be correlated with the original, not duplicated: a second+# copy would discard the state the running kernel accumulated.++."$(dirname"$0")/../lib.sh"++setup+build_pairstatic_local.c++in_symbolsorig.o|grep-q'counter'||+probe_skip"compiler emitted no distinct static local symbol"++run_diff+assert_patchedtarget++out_symbols|grep-q'\.klp\.sym\..*\.counter'||+fail"static local not referenced through a klp relocation"++out_symbols|grep'counter'|grep-qvE'UND|\.klp\.(sym|tombstone)'&&+fail"static local was given a fresh definition"++pass"static local correlated rather than duplicated"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:26:57
From: Puranjay Mohan <puranjay@kernel.org>
The compiler splits unlikely code into a separate foo.cold symbol. Both
halves are the same function and both belong in the livepatch: carrying
only the hot part leaves the cold path branching into unpatched code.
GCC needs -freorder-blocks-and-partition to split reliably. The flag is
probed rather than assumed, and the test skips when the compiler declines
to split at all.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/cold_function.c | 21 ++++++++++++
.../tests/generic/test-cold-function.sh | 34 +++++++++++++++++++
2 files changed, 55 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/cold_function.c
create mode 100755 tools/objtool/tests/generic/test-cold-function.sh
@@ -0,0 +1,21 @@+// SPDX-License-Identifier: GPL-2.0+/* Function the compiler may split into a hot part and a foo.cold part. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++staticvoid__attribute__((cold,noinline))slow_path(intx)+{+__asm__volatile(""::"r"(x));+}++inttarget(intx)+{+if(__builtin_expect(x<0,0))+slow_path(x);+#ifdef PATCHED+returnx+2;+#else+returnx+1;+#endif+}
@@ -0,0 +1,34 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Both halves of a split function belong to the patch; carrying only the hot+# part leaves the cold path branching into unpatched code.++."$(dirname"$0")/../lib.sh"++# Clang does not split functions into a cold part at all, so there is nothing+# for this test to look at there. A given gcc may or may not split, which is a+# version property rather than a compiler choice -- that stays a probe below.+gcc_only"clang does not split functions into a cold part"++setup++split_flag=-freorder-blocks-and-partition+cc_supports"$split_flag"||split_flag=++build_paircold_function.c$split_flag++in_symbolsorig.o|grep-qE'target\.cold'||+probe_skip"compiler did not split the function into a cold part"++run_diff++assert_patchedtarget+# Not a bare name match: had the cold half been left behind, the branch to+# it would appear as an undefined .klp.sym.vmlinux.target.cold, whose name+# contains the one being looked for. Require a defined symbol.+out_symbols|awk'$7!="UND"&&$8~/target\.cold/{found=1}+END{exit!found}'||+fail"cold half was not carried into the patch"++pass"cold half carried into the patch with its parent"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:03
From: Puranjay Mohan <puranjay@kernel.org>
create_fake_symbols() gives each special section entry a symbol so entries
can be extracted individually. Entries with ANNOTATE_DATA_SPECIAL are
handled first; the rest have their boundaries derived from the entry or
relocation size.
The second pass has to key off whether the first one created symbols, not
off whether the section already has something at offset 0. Clang puts an
assembler-local label at the start of .kcfi_traps, and treating that as
already handled means nothing is extracted: klp diff still reports the
changed function and succeeds, but the special section is missing from the
module.
The fixture reproduces the shape without needing CFI or x86.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/special_section.c | 24 +++++++++++++++++++
.../tests/generic/test-special-section.sh | 20 ++++++++++++++++
2 files changed, 44 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/special_section.c
create mode 100755 tools/objtool/tests/generic/test-special-section.sh
@@ -0,0 +1,20 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A .kcfi_traps entry belonging to a patched function must be extracted even+# without ANNOTATE_DATA_SPECIAL and with a local label already at offset 0.++."$(dirname"$0")/../lib.sh"++setup+build_pairspecial_section.c++in_symbolsorig.o|grep-q'trap_marker'||+probe_skip"fixture produced no .kcfi_traps on this arch"++run_diff++assert_patchedtarget+assert_section".kcfi_traps"++pass".kcfi_traps extracted despite a local label at offset 0"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:07
From: Puranjay Mohan <puranjay@kernel.org>
Two functions contribute entries to one special section but only one is
patched. Cloning the neighbouring entry drags in whatever it points at,
which is how a livepatch ends up holding relocations against unrelated
code.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/fixtures/special_section_shared.c | 31 +++++++++++++++++++
.../generic/test-special-section-shared.sh | 26 ++++++++++++++++
2 files changed, 57 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/special_section_shared.c
create mode 100755 tools/objtool/tests/generic/test-special-section-shared.sh
@@ -0,0 +1,31 @@+// SPDX-License-Identifier: GPL-2.0+/* Two functions contribute to one special section; only one is patched. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++intother(intx)+{+asmvolatile(+"2:\n\t"+".pushsection .kcfi_traps, \"a\"\n\t"+".balign 4\n\t"+".long 2b - .\n\t"+".popsection\n\t");+returnx*5;+}++inttarget(intx)+{+asmvolatile(+"1:\n\t"+".pushsection .kcfi_traps, \"a\"\n\t"+".balign 4\n\t"+".long 1b - .\n\t"+".popsection\n\t");+#ifdef PATCHED+returnx+2;+#else+returnx+1;+#endif+}
@@ -0,0 +1,26 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Only the patched function's special section entry may be extracted.++."$(dirname"$0")/../lib.sh"++setup+build_pairspecial_section_shared.c++has_input_sectionorig.o.kcfi_traps||+probe_skip"fixture produced no .kcfi_traps on this arch"++run_diff++assert_patchedtarget+assert_not_patchedother+assert_section".kcfi_traps"++entries="$(out_relocs|awk'/rela\.kcfi_traps/,/^$/'|grep-c'target')"+["$entries"=1]||fail"expected one .kcfi_traps entry, found $entries"++out_relocs|awk'/rela\.kcfi_traps/,/^$/'|grep-q'other'&&+fail"the untouched function's entry was dragged in"++pass"only the patched function's entry extracted"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:12
From: Puranjay Mohan <puranjay@kernel.org>
A cloned __jump_table entry must keep a relocation in its key slot.
objtool parses the table again after klp diff to convert the static
branch's scaffold instruction, and an empty slot leaves it unable to do so.
The scaffold then stays as emitted and the module trips BUG() in
__jump_label_patch() once the key's state differs from its compile-time
default.
Check both halves for a vmlinux-owned key: unexported gives a tombstone
plus a klp relocation, exported gives an ordinary relocation and no klp
machinery.
The fixture defines the key as an STT_OBJECT, which is what the kernel
emits. validate_special_section_klp_reloc() ignores anything else, so a
fixture using an undefined extern would skip the interesting paths.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/jump_label.c | 76 +++++++++++++++++++
.../tests/generic/test-jump-label-key.sh | 44 +++++++++++
2 files changed, 120 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/jump_label.c
create mode 100755 tools/objtool/tests/generic/test-jump-label-key.sh
@@ -0,0 +1,44 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A cloned __jump_table entry must keep a relocation in its key slot, whether+# the key needs a klp relocation or not.++."$(dirname"$0")/../lib.sh"++key=klp_test_key++setup+build_pairjump_label.c++has_input_sectionorig.o__jump_table||+probe_skip"fixture produced no __jump_table on this arch"++key_slot_relocs()+{+out_relocs|awk'/rela__jump_table/,/^$/'|grep-c"^0*8[[:space:]]"+}++# Unexported: klp relocation, key slot holds a tombstone.+export_syms+run_diff++["$(key_slot_relocs)"=1]||+fail"unexported key: key slot has no relocation"+out_relocs|awk'/rela__jump_table/,/^$/'|grep-q"\.klp\.tombstone\.$key"||+fail"unexported key: expected a .klp.tombstone.$key relocation"+out_symbols|grep-q"\.klp\.sym\..*\.$key,"||+fail"unexported key: no .klp.sym reference for the real relocation"++# Exported: ordinary relocation, no klp machinery.+export_syms"$key"+run_diff++["$(key_slot_relocs)"=1]||+fail"exported key: key slot has no relocation"+out_relocs|awk'/rela__jump_table/,/^$/'|grep-q"[[:space:]]$key[[:space:]]*+"||+fail"exported key: expected a direct relocation to $key"+out_symbols|grep-q'\.klp\.tombstone\.'&&+fail"exported key: tombstone emitted for an exported symbol"++pass"key slot populated for exported and unexported vmlinux keys"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:18
From: Puranjay Mohan <puranjay@kernel.org>
A static branch key belonging to a module cannot be reached with a klp
relocation: the patch may be applied after that module's static branch init
has run, which corrupts the code. Emitting one anyway produces a module
which loads and then BUG()s the first time the key changes state, so klp
diff has to refuse it.
The fixture differs from the accepted case only in the module name recorded
in .modinfo.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/test-jump-label-module-key.sh | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-jump-label-module-key.sh
@@ -0,0 +1,22 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A static branch key owned by a module cannot be reached with a klp+# relocation and must be rejected.++."$(dirname"$0")/../lib.sh"++setup+build_pairjump_label.c-DMODNAME='"klp_testmod"'++has_input_sectionorig.o__jump_table||+probe_skip"fixture produced no __jump_table on this arch"++run_diff255++diff_log|grep-q'unsupported static branch key klp_test_key'||+fail"expected rejection, got: $(diff_log|tail-1)"+[-e"$workdir/out.o"]&&+fail"output object produced for a rejected input"++pass"module-owned static branch key rejected"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:24
From: Puranjay Mohan <puranjay@kernel.org>
Static calls carry the same constraint as static branches. Check that a
vmlinux-owned key is accepted and a module-owned one is refused.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/static_call.c | 53 +++++++++++++++++++
.../generic/test-static-call-module-key.sh | 25 +++++++++
2 files changed, 78 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/static_call.c
create mode 100755 tools/objtool/tests/generic/test-static-call-module-key.sh
@@ -0,0 +1,25 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# As for static branches, a static call key owned by a module must be rejected+# while a vmlinux-owned one is accepted.++."$(dirname"$0")/../lib.sh"++setup+build_pairstatic_call.c++has_input_sectionorig.o.static_call_sites||+probe_skip"fixture produced no .static_call_sites on this arch"++run_diff+assert_patchedtarget++rm-f"$workdir/out.o"+build_pairstatic_call.c-DMODNAME='"klp_testmod"'+run_diff255++diff_log|grep-q'unsupported static call key __SCK__klp_test_call'||+fail"expected rejection, got: $(diff_log|tail-1)"++pass"module-owned static call key rejected, vmlinux-owned accepted"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:28
From: Puranjay Mohan <puranjay@kernel.org>
.klp.symid records duplicate-named locals so klp diff can work out their
sympos. Symbols in sections the vmlinux link throws away have to be left
out, or the table references symbols which no longer exist and the link
fails:
`__exitcall_foo' referenced in section `.klp.symid' of vmlinux.o:
defined in discarded section `.exitcall.exit' of vmlinux.o
Two translation units are compiled from one fixture and partially linked so
the result has duplicate locals, which symid_needed() requires. One
duplicate is in a live section and one in .exitcall.exit.
Checking the live duplicate as well keeps the test honest: it would
otherwise pass just as happily if symid generation stopped working
entirely.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/symid_discarded.c | 25 +++++++++++
.../tests/generic/test-symid-discarded.sh | 44 +++++++++++++++++++
2 files changed, 69 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/symid_discarded.c
create mode 100755 tools/objtool/tests/generic/test-symid-discarded.sh
@@ -0,0 +1,44 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# .klp.symid must not reference symbols in sections the vmlinux link discards.+# Each such section has been its own bug, found only when someone built a+# config where a duplicate happened to land there, so cover the whole list+# rather than whichever one was reported last.++."$(dirname"$0")/../lib.sh"++setup++# Allocated sections which vmlinux.lds.h discards unconditionally. A symid+# referencing one of these fails the vmlinux link outright:+#+# `__exitcall_hid_exit' referenced in section `.klp.symid' of vmlinux.o:+# defined in discarded section `.exitcall.exit' of vmlinux.o+forsecin.exitcall.exit.no_trim_symbol;do+build_onesymid_discarded.ca.o\+-DFUNC_NAME=use_a-DDISCARDED_SEC="\"$sec\""+build_onesymid_discarded.cb.o\+-DFUNC_NAME=use_b-DDISCARDED_SEC="\"$sec\""++# --klp-symids only runs on a file named vmlinux.o+rm-f"$workdir/vmlinux.o"+partial_link"$workdir/vmlinux.o""$workdir/a.o""$workdir/b.o"||+probe_skip"partial link unavailable"++"$OBJTOOL"--klp-symids--link"$workdir/vmlinux.o"||+fail"objtool --klp-symids failed"++symids="$(in_relocsvmlinux.o|+awk'/rela.klp.symid/,/^$/')"++# Without this the test would also pass if symid generation stopped+# entirely.+echo"$symids"|grep-q'dup_normal'||+fail"$sec: no symid for the duplicate in a live section"++echo"$symids"|grep-q'dup_discarded'&&+fail"symid emitted for a symbol in discarded section $sec"+done++pass"no symids for symbols in discarded sections"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:32
From: Puranjay Mohan <puranjay@kernel.org>
Init code and data are freed once boot finishes, so a klp relocation
against them can never resolve.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/init_reference.c | 16 +++++++++++
.../tests/generic/test-init-reference.sh | 27 +++++++++++++++++++
2 files changed, 43 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/init_reference.c
create mode 100755 tools/objtool/tests/generic/test-init-reference.sh
@@ -0,0 +1,16 @@+// SPDX-License-Identifier: GPL-2.0+/* Patched function referencing data in an .init section. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++staticintinit_only__attribute__((section(".init.data"),used))=5;++inttarget(intx)+{+#ifdef PATCHED+returnx+init_only+1;+#else+returnx+init_only;+#endif+}
@@ -0,0 +1,27 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Init code and data are freed after boot, so a klp relocation against them can+# never resolve. Such a patch must be rejected.++."$(dirname"$0")/../lib.sh"++setup+build_pairinit_reference.c++# The rejection can only happen if the patched build really does reference the+# init symbol. Were the compiler to fold the read into a constant there would+# be no relocation, klp diff would succeed, and the failure below would look+# like a missing check rather than a fixture which stopped posing the question.+# The reference is relocated against the section symbol rather than against+# init_only, since the variable is file-local -- which is the form klp diff+# has to recognise anyway.+in_relocs"$patched_obj"|grep-qE'(^|[[:space:]])\.init\.data([[:space:]]|$|[[:space:]]*[-+])'||+fail"patched object has no reference into .init.data; the fixture tests nothing"++run_diff255++diff_log|grep-q"can't patch or reference init code/data"||+fail"expected rejection, got: $(diff_log|tail-1)"++pass"reference to init data rejected"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:37
From: Puranjay Mohan <puranjay@kernel.org>
ThinLTO promotes the file-local symbols an imported function touches,
renaming them name.llvm.<hash>. The hash is content derived, so it changes
whenever the module does:
original: counter.llvm.13663304415433785070
patched: counter.llvm.10543383937958011340
Correlating the two objects therefore requires demangling the suffix;
matching raw names would see two unrelated symbols and treat the variable
as new.
The resulting klp relocation also has to name the original symbol, since
that is the one in the running kernel's kallsyms. Naming the patched
build's symbol produces a relocation which can never be resolved.
ThinLTO is a clang feature, so the test declares itself clang-only. It
also needs an lld from the same LLVM release as $CC; a mismatched pair
fails with "Invalid summary version", which reads like a broken test rather
than a broken environment, so probe for a working lld and skip if there is
none.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/thinlto_local.c | 39 +++++++++++++++
.../tests/generic/test-thinlto-local.sh | 48 +++++++++++++++++++
2 files changed, 87 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/thinlto_local.c
create mode 100755 tools/objtool/tests/generic/test-thinlto-local.sh
@@ -0,0 +1,48 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Correlating ThinLTO-promoted locals requires demangling the .llvm.<hash>+# suffix, and the resulting klp relocation must name the original symbol: that+# is the one in the running kernel's kallsyms.++."$(dirname"$0")/../lib.sh"++setup+clang_only"ThinLTO requires clang"++find_thinlto_toolchain||+probe_skip"no matching clang/lld pair for a ThinLTO link; set THIN_LD to one"++build_thinlto()# $1 output object, $2 extra flags+{+$THIN_CC-flto=thin-O2-ffunction-sections-fdata-sections$2\+-c"$FIXTURES_DIR/thinlto_local.c"-o"$workdir/tu_a.o"2>/dev/null||return1+$THIN_CC-flto=thin-O2-ffunction-sections-fdata-sections$2-DTU_B\+-c"$FIXTURES_DIR/thinlto_local.c"-o"$workdir/tu_b.o"2>/dev/null||return1+"$THIN_LD"-r"$workdir/tu_a.o""$workdir/tu_b.o"-o"$1"2>/dev/null||return1+}++build_thinlto"$workdir/orig.o"""||+probe_skip"ThinLTO build failed ($THIN_CC, $THIN_LD)"+build_thinlto"$workdir/patched.o"-DPATCHED||+probe_skip"ThinLTO build failed ($THIN_CC, $THIN_LD)"++orig_sym="$(in_symbolsorig.o|grep-o'counter\.llvm\.[0-9]*'|head-1)"+new_sym="$(in_symbolspatched.o|grep-o'counter\.llvm\.[0-9]*'|head-1)"++[-n"$orig_sym"]&&[-n"$new_sym"]||+probe_skip"$THIN_CC did not promote the local symbol"++# Equal hashes would make plain name matching work, testing nothing.+["$orig_sym"!="$new_sym"]||+probe_skip"$THIN_CC gave the same ThinLTO hash for both builds"++run_diff+assert_patchedtarget++out_symbols|grep-q"\.klp\.sym\.vmlinux\.$orig_sym,"||+fail"expected a klp relocation naming $orig_sym"+out_symbols|grep-q"\.klp\.sym\.vmlinux\.$new_sym,"&&+fail"klp relocation names $new_sym, which the running kernel does not have"++pass"ThinLTO-mangled local correlated across differing hashes ($THIN_CC, $THIN_LD)"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:43
From: Puranjay Mohan <puranjay@kernel.org>
The module name recorded in .modinfo ends up in the livepatch's klp_object,
and also decides whether a static branch key counts as belonging to vmlinux
or to a module. An object without one cannot be diffed.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../objtool/tests/generic/fixtures/no_modinfo.c | 11 +++++++++++
.../tests/generic/test-missing-modinfo.sh | 16 ++++++++++++++++
2 files changed, 27 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/no_modinfo.c
create mode 100755 tools/objtool/tests/generic/test-missing-modinfo.sh
@@ -0,0 +1,16 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# The module name in .modinfo ends up in the livepatch's klp_object, so an+# object without one cannot be diffed.++."$(dirname"$0")/../lib.sh"++setup+build_pairno_modinfo.c+run_diff255++diff_log|grep-q'modinfo'||+fail"expected a complaint about .modinfo, got: $(diff_log|tail-1)"++pass"object without .modinfo rejected"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:48
From: Puranjay Mohan <puranjay@kernel.org>
klp diff decides what changed by comparing the per-function checksums klp
checksum records in .discard.sym_checksum. Handed an object which was
never checksummed it has to say so; silently concluding that nothing
changed would be the worst available answer.
Signed-off-by: Puranjay Mohan <puranjay@kernel.org>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/test-missing-checksum.sh | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-missing-checksum.sh
@@ -0,0 +1,18 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Without .discard.sym_checksum there is nothing to compare; concluding that+# nothing changed would be worse than failing.++."$(dirname"$0")/../lib.sh"++setup+build_pairbasic.c++(cd"$workdir"&&"$OBJTOOL"klpdifforig.opatched.oout.o)\+>"$workdir/diff.log"2>&1&&fail"klp diff accepted an unchecksummed object"++grep-q'sym_checksum'"$workdir/diff.log"||+fail"expected a complaint about the checksum section, got: $(tail-1"$workdir/diff.log")"++pass"unchecksummed input rejected"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:53
Five tests covering behaviour the harness could reach but nothing
exercised. Each was verified to fail against the code it guards, by
reverting the fix or sabotaging the exact line; where a first attempt
passed against broken code, the fixture was wrong and was rebuilt.
post-link first coverage of the subcommand at all
local-vs-export local symbols must not match exports
symvers-parse-error Module.symvers parse error line numbers
checksum-debug the --debug-checksum format klp-build reads
function-removal the "no correlation" path
local-vs-export tests the behavior of commit 86a697572c62 ("objtool/klp:
Don't match local symbols against exports"), and symvers-parse-error that
of commit 51c1de134863 ("objtool/klp: Fix line numbers in Module.symvers
parse errors").
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/function_removal.c | 25 ++++++++++
.../tests/generic/test-checksum-debug.sh | 49 +++++++++++++++++++
.../tests/generic/test-function-removal.sh | 34 +++++++++++++
.../tests/generic/test-local-vs-export.sh | 32 ++++++++++++
tools/objtool/tests/generic/test-post-link.sh | 39 +++++++++++++++
.../tests/generic/test-symvers-parse-error.sh | 23 +++++++++
6 files changed, 202 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/function_removal.c
create mode 100755 tools/objtool/tests/generic/test-checksum-debug.sh
create mode 100755 tools/objtool/tests/generic/test-function-removal.sh
create mode 100755 tools/objtool/tests/generic/test-local-vs-export.sh
create mode 100755 tools/objtool/tests/generic/test-post-link.sh
create mode 100755 tools/objtool/tests/generic/test-symvers-parse-error.sh
@@ -0,0 +1,49 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# "klp checksum --debug-checksum" prints a per-instruction checksum stream,+# and klp-build -f (--show-first-changed) parses it to report where a function+# first differs between the original and patched builds.+#+# It is a debugging aid, so nothing fails when it breaks: klp-build greps the+# stream, and an unmatched grep just yields no output, which reads as "no+# instruction changed". That is exactly how the format drifted out from under+# it once already. Pin the shape klp-build depends on:+#+# DEBUG: <object>: checksum: <func>(): <sym>+0x<offset> <16 hex digits>+#+# and that --dry-run leaves the object alone, since klp-build runs this against+# objects it is going to checksum again for real.++."$(dirname"$0")/../lib.sh"++setup+build_pairbasic.c++before="$(md5sum<"$workdir/orig.o")"++"$OBJTOOL"klpchecksum--dry-run--debug-checksum=changed\+"$workdir/orig.o">"$workdir/debug.log"2>&1||+fail"klp checksum --debug-checksum failed"++# --dry-run has to mean it: klp-build checksums these objects again afterwards,+# and "already has .discard.sym_checksum, skipping" would lose the real run.+["$(md5sum<"$workdir/orig.o")"="$before"]||+fail"--dry-run modified the object"+has_input_sectionorig.o.discard.sym_checksum&&+fail"--dry-run created .discard.sym_checksum"++grep-qE'^DEBUG: .*: checksum: changed\(\): [^ ]+\+0x[0-9a-f]+ [0-9a-f]{16}$'\+"$workdir/debug.log"||+fail"unexpected --debug-checksum format: $(head-1"$workdir/debug.log")"++# This is the pattern klp-build greps with. Keep it working verbatim.+grep-qE"^DEBUG: .*checksum: changed\(\): ""$workdir/debug.log"||+fail"klp-build's --show-first-changed pattern no longer matches"++# Only the requested function, or klp-build attributes instructions to the+# wrong one.+grep-qE'checksum: untouched\(\)'"$workdir/debug.log"&&+fail"--debug-checksum=changed also dumped untouched()"++pass"--debug-checksum format is the one klp-build -f parses"
@@ -0,0 +1,34 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A patch which deletes a function leaves a symbol in the original with no+# counterpart in the patched object. klp diff cannot correlate it, and must+# say so and carry on: livepatching cannot remove code from a running kernel,+# so what matters is that the surviving caller is patched and the deleted+# function is not dragged into the patch module.+#+# Cloning it would be worse than useless -- dead code in the patch, plus+# whatever it references, resolved against a kernel where it may not exist.++."$(dirname"$0")/../lib.sh"++setup+build_pairfunction_removal.c++# One-sided by construction: present in the original, gone from the patched.+has_input_symbolorig.ogoing_away||+fail"fixture has no going_away in the original"+has_input_symbolpatched.ogoing_away&&+fail"fixture still has going_away in the patched object"++run_diff++assert_diff_log'no correlation: going_away'++# The caller changed, so it is patched ...+assert_patchedcaller+# ... and the deleted function comes along in no form at all.+assert_not_patchedgoing_away+assert_no_symbolgoing_away++pass"deleted function reported as uncorrelated and left out of the patch"
@@ -0,0 +1,32 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# find_export() matched on symbol name alone, so a static function or variable+# sharing a name with an export was mistaken for a reference to that export.+# For a vmlinux export that means no klp relocation at all: the normal+# relocation left behind is resolved by the module loader to the vmlinux+# symbol, and the patched code quietly reads and writes the wrong object.+#+# Exports are always global, so a local symbol is never one.++."$(dirname"$0")/../lib.sh"++setup+build_pairstatic_local.c++# The static the fixture uses. Compilers mangle statics variously -- gcc says+# counter.0, clang says target.counter -- so find what this one produced rather+# than assuming a shape.+local_sym="$(in_symbolsorig.o|+awk'$4 == "OBJECT" && $5 == "LOCAL" && $8 ~ /counter/ { print $8; exit }')"+[-n"$local_sym"]||+fail"fixture produced no local 'counter' symbol"++# Contrive the collision: something else exports that same name.+export_syms"$local_sym"counter+run_diff++# Still treated as the local it is, not as the export.+assert_klp_sym"$local_sym"vmlinux++pass"local symbol not mistaken for an export of the same name"
@@ -0,0 +1,39 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# klp post-link converts the intermediate __klp_relocs.* sections into the+# .klp.rela.* form the kernel applies at patch load. Getting this wrong is+# invisible at build time: the module links and loads, and the relocations are+# simply never applied.++."$(dirname"$0")/../lib.sh"++setup+build_pairstatic_local.c++# An unexported symbol is what produces a klp relocation in the first place.+assert_input_symbolcounter+run_diff+assert_section__klp_relocs.vmlinux++# Nothing has converted them yet.+assert_no_section".klp.rela.vmlinux..text.target"++# The original relocation is neutralised by pointing it at a tombstone, which+# is what stops the module loader resolving it behind livepatch's back.+#+# Compilers mangle a static local differently -- gcc says counter.0, clang+# target.counter -- so find what this one produced rather than assuming.+local_sym="$(in_symbolsorig.o|+awk'$4 == "OBJECT" && $5 == "LOCAL" && $8 ~ /counter/ { print $8; exit }')"+[-n"$local_sym"]||fail"fixture produced no local 'counter' symbol"+assert_tombstone"$local_sym"++run_post_link++# One .klp.rela section per base section, carrying SHF_RELA_LIVEPATCH, against+# a symbol in SHN_LIVEPATCH for the kernel to resolve.+assert_klp_relavmlinux.text.target+assert_livepatch_symcounter++pass"klp relocations converted to .klp.rela with SHN_LIVEPATCH symbols"
@@ -0,0 +1,23 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A malformed Module.symvers has to be reported against the line it is on.+# Module.symvers has tens of thousands of lines and is generated, so a wrong+# line number sends whoever has to fix it to the wrong place, and "line 1" is+# wrong in a way that looks plausible.++."$(dirname"$0")/../lib.sh"++setup+build_pairbasic.c++# Three well-formed lines, then one with no tabs at all.+export_symsabc+echo'this line has no fields'>>"$workdir/Module.symvers"++run_diff255++assert_diff_log'malformed Module.symvers'+assert_diff_log'at line 4'++pass"malformed Module.symvers reported against the offending line"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:27:58
klp diff names the intermediate __klp_relocs section after the object the
relocation belongs to, and post-link turns that into
.klp.rela.<object>.<sec>. Name it after the wrong object and the kernel
applies the relocation when the wrong module loads, or never.
The fixture is the first here to honour MODNAME: most hardcode
name=vmlinux, so passing -DMODNAME to them silently does nothing and the
test quietly becomes a vmlinux test.
This tests the behavior of commit 07f14d6af9d7 ("objtool/klp: Fix
cross-module klp relocation section naming").
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/cross_module.c | 25 +++++++++++++++
.../tests/generic/test-module-object.sh | 31 +++++++++++++++++++
2 files changed, 56 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/cross_module.c
create mode 100755 tools/objtool/tests/generic/test-module-object.sh
@@ -0,0 +1,31 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A klp relocation section is named for the object being patched, not for the+# object which happens to own the symbol being referenced. Deriving it from+# the symbol means a cross-module reference lands in a section for an object+# the patch may not even touch, so the relocation is never applied and the call+# goes somewhere arbitrary.++."$(dirname"$0")/../lib.sh"++setup+build_module_paircross_module.cklp_testmod++# The fixture has to have built as a module for any of this to mean anything.+in_sectionsorig.o|grep-q'\.modinfo'||+fail"fixture has no .modinfo"++# other_mod_func belongs to a different module than the one being patched.+add_exportsother_modother_mod_func+run_diff++# Named for the patched object ...+assert_section__klp_relocs.klp_testmod+# ... not for the object owning the symbol.+assert_no_section__klp_relocs.other_mod++run_post_link+assert_klp_relaklp_testmod.text.target++pass"klp relocation section named for the patched object"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:04
The kernel refuses a module-targeted klp relocation which names a vmlinux
symbol. An EXPORT_SYMBOL_FOR_MODULES() symbol needs a klp relocation, so
patching a module function that references one only loads if klp diff files
that relocation under vmlinux rather than under the patched module.
This fails at load, not at build: klp-build produces a module and static
checks of it find nothing wrong. Hence the assertion on which object the
relocation is filed against.
Reported by Dylan Hatch, whose mod-ns-lp branch carries the kernel-side
half of this case.
Signed-off-by: Dylan Hatch <redacted>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/test-module-vmlinux-reloc.sh | 40 +++++++++++++++++++
1 file changed, 40 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-module-vmlinux-reloc.sh
@@ -0,0 +1,40 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Patching a module, where the patched code references a vmlinux symbol which+# needs a klp relocation.+#+# The kernel does not allow a module-targeted klp relocation to reference a+# vmlinux symbol, and a symbol exported with EXPORT_SYMBOL_FOR_MODULES gets a+# klp relocation. Put together, filing that relocation under the patched+# module produces a patch the kernel refuses to apply to its target.+#+# So it goes under vmlinux instead, and is applied when the patch module loads+# rather than when the patched module does. That is the opposite of the rule+# for a reference to a module's symbol, which test-module-object covers; this+# is the other branch of the same decision.++."$(dirname"$0")/../lib.sh"++setup+# The object being patched is a module ...+build_module_paircross_module.cklp_testmod++# ... and the symbol it references belongs to vmlinux, exported in a way that+# still requires a klp relocation.+export_syms+add_exports_nsvmlinuxmodule:kvmother_mod_func+run_diff++# Filed against vmlinux, applied when the patch loads.+assert_section__klp_relocs.vmlinux+assert_klp_symother_mod_funcvmlinux++# Not against the patched module: that is the relocation the kernel rejects.+assert_no_section__klp_relocs.klp_testmod++run_post_link+assert_klp_relavmlinux.text.target+assert_no_section".klp.rela.klp_testmod..text.target"++pass"klp relocation to a vmlinux symbol filed under vmlinux, not the patched module"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:09
Module.symvers names an export's owner by build path, and the kernel knows
modules by their runtime name. Without normalizing one to the other, an
export owned by a module is not recognised as owned by anything, and a
reference that should become a klp relocation stays an ordinary one.
This tests the behavior of commit 8668bf91e050 ("objtool/klp: Normalize
Module.symvers paths to module names").
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/test-modname-normalize.sh | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-modname-normalize.sh
@@ -0,0 +1,26 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Module.symvers records the build-tree path of the object that exports a+# symbol, not the name the module has at runtime: "arch/x86/kvm/kvm-intel",+# where the kernel knows the module as "kvm_intel".+#+# The klp symbol name embeds the owning object, and livepatch matches it+# against loaded modules by name. Left unnormalized it names a module that+# does not exist, and the relocation is never resolved -- at load time, with no+# build-time complaint.++."$(dirname"$0")/../lib.sh"++setup+build_module_paircross_module.cklp_testmod++# A path with directory components, a dash, and no extension.+export_syms+add_exports"arch/x86/kvm/kvm-intel"other_mod_func+run_diff++# Directories stripped, dash to underscore.+assert_klp_symother_mod_funckvm_intel++pass"Module.symvers paths normalized to runtime module names"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:14
The patch list is what livepatch acts on, and asserting only that it exists
does not say it is right. A function that should have been patched and is
missing leaves the bug in place; one that should not be there patches code
nobody changed.
The fixture changes two of three functions and asserts on all three: the
two by name, and the third by its absence. It checks the strings in
.rodata.klp.str1.1 as well as the relocations, since the names the kernel
matches on are real strings.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/klp_funcs.c | 31 +++++++++++++
.../tests/generic/test-klp-funcs-content.sh | 44 +++++++++++++++++++
2 files changed, 75 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/klp_funcs.c
create mode 100755 tools/objtool/tests/generic/test-klp-funcs-content.sh
@@ -0,0 +1,44 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# .init.klp_funcs is the list the kernel walks to decide what to patch, and+# .init.klp_objects points at it. Existing tests assert only that the sections+# exist, which they do whether the list names the right functions, the wrong+# ones, or none at all -- and a patch module with an empty function list loads+# perfectly happily and patches nothing.+#+# Each entry pairs a name string in .rodata.klp.str1.1 with a relocation to the+# new function, so both halves are checkable.++."$(dirname"$0")/../lib.sh"++setup+build_pairklp_funcs.c+run_diff++assert_section.init.klp_funcs+assert_section.init.klp_objects++# Two functions changed, so two entries, each contributing a name relocation+# and a function relocation.+assert_reloc_count.init.klp_funcs4++# The functions that changed are named ...+assert_reloc_sym.init.klp_funcsfirst+assert_reloc_sym.init.klp_funcssecond+# ... and the one that did not is absent, from the list and from the patch.+assert_no_reloc_sym.init.klp_funcsthird+assert_not_patchedthird++# The names the kernel matches on are real strings, not just relocations.+out_strings.rodata.klp.str1.1|+grep-q'\bfirst\b'||+fail"no 'first' string in .rodata.klp.str1.1"+out_strings.rodata.klp.str1.1|+grep-q'\bsecond\b'||+fail"no 'second' string in .rodata.klp.str1.1"++# The object list has to reach the function list, or nothing is walked.+assert_reloc_sym.init.klp_objects.init.klp_funcs++pass"klp_funcs lists exactly the changed functions, by name and relocation"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:19
A symbol exported with EXPORT_SYMBOL_FOR_MODULES() is reachable only by the
modules named in its namespace, and a livepatch module is never one of
them. So a reference to it cannot be an ordinary relocation resolved by the
module loader; it has to be a klp relocation applied at patch time.
Getting this wrong is silent. The module links, loads, and reads the wrong
thing, or fails to load for a reason that does not name the cause.
This tests the behavior of commit 4cd3cfb8b54f ("objtool/klp: Fix
relocations for EXPORT_SYMBOL_FOR_MODULES() symbols") and
commit d8a2860b4a36 ("objtool/klp: Fix vmlinux klp relocations for
EXPORT_SYMBOL_FOR_MODULES()").
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/test-export-symbol-for-modules.sh | 39 +++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-export-symbol-for-modules.sh
@@ -0,0 +1,39 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# EXPORT_SYMBOL_FOR_MODULES() puts a vmlinux symbol in a "module:<names>"+# namespace, and the module loader grants access by matching the importing+# module's name against that list. A livepatch module is never on the list, so+# referencing such a symbol with a normal relocation fails modpost, and if that+# is silenced, fails to load with "Unknown symbol". It needs a klp relocation,+# the same as an unexported symbol.+#+# Ordinary namespaces are not affected: copy_import_ns() propagates the patched+# object's import tags to the patch module, so a normal relocation works.++."$(dirname"$0")/../lib.sh"++setup+build_paircross_module.c++sym=other_mod_func++# Plain vmlinux export: a normal relocation is what we want.+export_syms"$sym"+run_diff+assert_no_klp_sym"$sym"++# Ordinary namespace: still a normal relocation.+export_syms+add_exports_nsvmlinuxMY_NS"$sym"+run_diff+assert_no_klp_sym"$sym"++# module: namespace: has to become a klp relocation.+export_syms+add_exports_nsvmlinuxmodule:kvm"$sym"+run_diff+assert_klp_sym"$sym"vmlinux+assert_section__klp_relocs.vmlinux++pass"EXPORT_SYMBOL_FOR_MODULES symbol referenced with a klp relocation"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:25
A patch may introduce a reference the original object did not have. That
is fine when the export belongs to vmlinux, and not fine when it belongs to
a module: the livepatch would gain a module dependency nobody declared, and
late module patching lets the patch load first.
This tests the behavior of commit 72d76d0c18eb ("objtool/klp: Allow new
references to module exports").
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/new_export_ref.c | 20 +++++++++++
.../tests/generic/test-new-export-ref.sh | 34 +++++++++++++++++++
2 files changed, 54 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/new_export_ref.c
create mode 100755 tools/objtool/tests/generic/test-new-export-ref.sh
@@ -0,0 +1,34 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A reference the patch adds has no counterpart in the original object. klp+# diff used to reject any such reference needing a klp relocation, which ruled+# out patches that call something they did not call before -- a common enough+# thing for a fix to do.+#+# Module.symvers is what makes it safe: it says the symbol exists and who owns+# it. But that is only sufficient for a vmlinux export. A new reference to a+# module's export is a dependency the patch module does not declare, and the+# relocation would resolve only if that module happened to be loaded, so it+# stays an error.++."$(dirname"$0")/../lib.sh"++setup+build_pairnew_export_ref.c++# Exported by vmlinux, in a module: namespace so it needs a klp relocation+# rather than an ordinary one. Allowed.+export_syms+add_exports_nsvmlinuxmodule:kvmnewly_referenced+run_diff+assert_klp_symnewly_referencedvmlinux++# Exported by a module the patched object does not depend on. Rejected, and+# for that reason rather than some other.+export_syms+add_exportsother_modnewly_referenced+run_diff255+assert_diff_log'undeclared module dependency'++pass"new reference allowed to a vmlinux export, rejected for a module export"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:30
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 [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/x86/fixtures/empty_alternative.c | 77 +++++++++++++++++++
.../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
@@ -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_pairempty_alternative.c++assert_input_section.altinstructions+assert_input_section.altinstr_replacement++run_diff++# target's own replacement comes along ...+assert_symboltarget_repl+# ... neighbor's does not, nor what it references.+assert_no_symbolneighbor_repl+assert_no_symbolneighbor_only++pass"empty alternative's replacement offset ignored when cloning"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:35
Whether klp diff treats a function as changed is decided by its checksum,
and until now nothing looked at one. A test asserting only that the right
functions were cloned cannot tell a correct checksum from one that happens
to differ.
Asserts both directions -- the changed function's checksum moves, the
untouched one's does not -- and that checksumming identical input twice
gives the same answer, since otherwise every rebuild reports spurious
changes.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/test-checksum-value.sh | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-checksum-value.sh
@@ -0,0 +1,37 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# The per-function checksums klp checksum records are what klp diff uses to+# decide which functions changed. A checksum covering too little misses a real+# change and the patch silently omits the function; one covering too much, or+# unstable across identical input, clones functions nobody patched and drags+# their dependencies in with them.+#+# test-basic covers which functions got cloned, which is downstream of this and+# passes for either kind of wrong checksum as long as the two errors do not+# happen to cancel. This checks the checksums themselves.++."$(dirname"$0")/../lib.sh"++setup+build_pairbasic.c++assert_input_symbolchanged+assert_input_symboluntouched++run_checksum++# The edited function's checksum has to move, the untouched one's must not.+assert_checksum_differschanged+assert_checksum_matchesuntouched++# And it has to be a function of the code, not of the build: checksumming the+# same input twice has to give the same answer, or every rebuild reports+# spurious changes.+first="$(checksum_oforig.ochanged)"+build_pairbasic.c+run_checksum+["$(checksum_oforig.ochanged)"="$first"]||+fail"checksum for 'changed' differs between builds of identical source"++pass"checksums track the changed function and are stable across rebuilds"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:40
A function that only moves has not changed, and its checksum must not move
with it. Otherwise every patch reports as changed everything that shifted
because something ahead of it grew.
The fixture is built with -fno-function-sections, overriding the harness
default: with per-function sections every function sits at offset 0 of its
own section and nothing ever moves, so the test would prove nothing. It
also calls across to another function rather than looping within itself --
a loop branch keeps the same displacement wherever the function goes, so it
is not position-dependent to begin with.
This tests the behavior of commit cca84cb12908 ("objtool/klp: Fix
position-dependent checksums for non-relocated jumps/calls").
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/fixtures/checksum_position.c | 45 +++++++++++++++++++
.../tests/generic/test-checksum-position.sh | 35 +++++++++++++++
2 files changed, 80 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_position.c
create mode 100755 tools/objtool/tests/generic/test-checksum-position.sh
@@ -0,0 +1,35 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A function's checksum must not depend on where the function sits.+#+# A jump or call without a relocation encodes its target as an offset from the+# instruction. Hashing those bytes makes the checksum change whenever anything+# ahead of the function changes size -- so an unrelated edit elsewhere in the+# file reports this function as changed too, and the patch grows to include it+# and everything it references. Nothing fails; the livepatch is just larger and+# riskier than the patch it came from.+#+# Here the "patch" adds a function ahead of target() and changes nothing else.++."$(dirname"$0")/../lib.sh"++setup++# -fno-function-sections, or each function is at offset 0 of its own section+# and target() never moves.+build_pairchecksum_position.c-fno-function-sections++assert_input_symboltarget++# The fixture is only meaningful if target() actually moved.+orig_off="$(in_symbolsorig.o|awk'$8 == "target" { print $2 }')"+new_off="$(in_symbolspatched.o|awk'$8 == "target" { print $2 }')"+[-n"$orig_off"]&&[-n"$new_off"]||+fail"target symbol not found in both objects"+["$orig_off"!="$new_off"]||+probe_skip"compiler did not move target() between builds"++assert_checksum_matchestarget++pass"checksum unchanged when the function only moves"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:46
klp-sympos.c had no coverage at all. sympos disambiguates same-named
symbols for livepatch, counting from 1, with 0 meaning the name is unique.
Resolving to the wrong one is not a load failure -- it is a patch quietly
wired to the wrong object.
Covers the module path, where the position is a count in symbol table order
and klp diff can work it out from the object alone.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/sympos_dup.c | 32 ++++++++++++
tools/objtool/tests/generic/test-sympos.sh | 51 +++++++++++++++++++
2 files changed, 83 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/sympos_dup.c
create mode 100755 tools/objtool/tests/generic/test-sympos.sh
@@ -0,0 +1,32 @@+// SPDX-License-Identifier: GPL-2.0+/*+*Astaticwhosenamerecursineverytranslationunitthatincludesit.+*Compiledonceforasingle-copyobjectandtwice,partiallylinked,forone+*withduplicates--whichistheonlycasewheresymposisnon-zero.+*+*FUNC_NAMEkeepsthereferencingfunctionsdistinctsobothgetpatched.+*Onlythefirstcopycarries.modinfo;twowouldbeasecondthingto+*disambiguateandisnotwhatthisfixtureisabout.+*/++#ifndef FUNC_NAME+#define FUNC_NAME use_a+#endif++#ifndef NO_MODINFO+staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";+#endif++/* volatile so it survives as an STT_OBJECT rather than being folded away */+staticvolatileintdup_counter=1;++intFUNC_NAME(intx)+{+dup_counter+=x;+#ifdef PATCHED+returndup_counter+1;+#else+returndup_counter;+#endif+}
@@ -0,0 +1,51 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# sympos is what livepatch uses to tell duplicate symbol names apart in the+# patched object: which "dup_counter" of several the relocation means. Get it+# wrong and the patch resolves to the wrong object at load time, silently.+#+# klp_find_sympos() reports 0 when a name is unique and a 1-based position when+# it is not, so both need checking -- always reporting a position, or never,+# each looks right in one of the two cases.+#+# This is the module path, counting symbol table order. vmlinux is reordered+# by the final link and goes through .klp.symid instead; that needs a linked+# vmlinux next to vmlinux.o and is not covered here.++."$(dirname"$0")/../lib.sh"++setup++# One copy: the name is unique, so there is nothing to disambiguate.+build_onesympos_dup.corig.o-DFUNC_NAME=use_a+build_onesympos_dup.cpatched.o-DFUNC_NAME=use_a-DPATCHED+run_diff++assert_klp_symposdup_counter0++# Two copies: positions, in symbol table order.+forpin"""-DPATCHED";do+# shellcheck disable=SC2086+build_onesympos_dup.c"a$p.o"-DFUNC_NAME=use_a$p+# shellcheck disable=SC2086+build_onesympos_dup.c"b$p.o"-DFUNC_NAME=use_b-DNO_MODINFO$p+done+partial_link"$workdir/orig.o""$workdir/a.o""$workdir/b.o"||+probe_skip"partial link unavailable"+partial_link"$workdir/patched.o""$workdir/a-DPATCHED.o""$workdir/b-DPATCHED.o"||+probe_skip"partial link unavailable"++# Without duplicates in the input there is nothing for sympos to number.+["$(count_input_symbolsorig.odup_counter)"=2]||+fail"fixture did not produce two dup_counter symbols"++run_diff++assert_klp_symposdup_counter1+assert_klp_symposdup_counter2+# ... and nothing still claiming the name is unique+out_symbols|grep-qE'\.klp\.sym\.[^.]+\.dup_counter,0([[:space:]]|$)'&&+fail"sympos 0 emitted for a duplicated symbol"++pass"sympos numbers duplicate symbols and stays 0 for unique ones"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:51
vmlinux is not like a module: the final link reorders sub-sections, so a
symbol's position has to come from the linked image rather than from symbol
table order. klp diff bridges that with .klp.symid, and looks for it only
when the object it was handed is called vmlinux.o with a vmlinux beside it.
This was assigned to an end-to-end test on the assumption that it needs a
real kernel build. It needs "ld -r" and "ld -e 0", and takes a fraction of
a second.
The fixture places the static appearing first in the symbol table at the
higher address, and the link passes --sort-section=name to force the
reordering the kernel's linker script performs. Without that the two ways
of computing sympos agree, and a first version passed with the vmlinux path
disabled.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/sympos_vmlinux.c | 40 +++++++++++++
.../tests/generic/test-sympos-vmlinux.sh | 57 +++++++++++++++++++
2 files changed, 97 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/sympos_vmlinux.c
create mode 100755 tools/objtool/tests/generic/test-sympos-vmlinux.sh
@@ -0,0 +1,40 @@+// SPDX-License-Identifier: GPL-2.0+/*+*Twotranslationunitswithasame-namedstatic,placedsothatthelinker+*putsthemintheoppositeordertotheonetheyappearinthesymboltable.+*+*VARSECselectsthesectionthestaticlandsin.Linkingwith+*--sort-section=namethenordersthemalphabeticallyratherthanbyobject+*order,sothefirstsymbolinthesymboltableendsupatthe*higher*+*address.Thatisthewholepoint:countingsymboltableorderandreading+*thelinkedimage'saddressesnowgivedifferentanswers,whichiswhatmakes+*itpossibletotellwhichoneklpdiffused.+*+*Onlyuse_aispatched,soexactlyonesymposisemittedandthereisnothing+*toattribute.+*/++#ifndef FUNC_NAME+#define FUNC_NAME use_a+#endif+#ifndef VARSEC+#define VARSEC ".data.mmm"+#endif++#ifndef NO_MODINFO+staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";+#endif++/* volatile so it survives as an STT_OBJECT rather than being folded away */+staticvolatileintdup_counter__attribute__((section(VARSEC)))=1;++intFUNC_NAME(intx)+{+dup_counter+=x;+#ifdef PATCHED+returndup_counter+1;+#else+returndup_counter;+#endif+}
@@ -0,0 +1,57 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# sympos for vmlinux, which is resolved differently from sympos for a module.+#+# A module's .ko preserves symbol table order, so klp diff can count -- that is+# what test-sympos covers. vmlinux cannot be counted: the final link reorders+# sub-sections, so the order in vmlinux.o is not the order the running kernel+# has. klp diff bridges that with .klp.symid, a table of { id, address }+# emitted into vmlinux.o whose addresses the linker resolves, read back out of+# the linked vmlinux.+#+# Getting it wrong points the relocation at a different symbol of the same+# name. Nothing fails to build or load; the patched code uses the wrong+# object.+#+# The fixture is arranged so the two answers differ: the static that comes+# first in the symbol table is placed at the *higher* address, so counting+# gives 1 and reading the linked image gives 2. Without that, both paths agree+# and the test cannot tell them apart.++."$(dirname"$0")/../lib.sh"++setup++# use_a's static sorts last by section name, use_b's first. Only use_a is+# patched, so exactly one sympos comes out.+build_onesympos_vmlinux.corig_a.o-DFUNC_NAME=use_a-DVARSEC='".data.zzz"'+build_onesympos_vmlinux.cpatched_a.o-DFUNC_NAME=use_a-DVARSEC='".data.zzz"'-DPATCHED+build_onesympos_vmlinux.cb.o-DFUNC_NAME=use_b-DVARSEC='".data.aaa"'-DNO_MODINFO++make_vmlinux_pair"$workdir/orig_a.o""$workdir/b.o"\+--"$workdir/patched_a.o""$workdir/b.o"++["$(count_input_symbolsvmlinux.odup_counter)"=2]||+fail"fixture did not produce two dup_counter symbols"+has_input_sectionvmlinux.o.klp.symid||+fail"objtool --klp-symids emitted no .klp.symid table"+has_input_sectionvmlinux.klp.symid||+fail".klp.symid did not survive the link"++# The premise: symbol table order and address order must disagree, or the test+# proves nothing.+first_addr="$(in_symbolsvmlinux|awk'$8 == "dup_counter" { print $2; exit }')"+low_addr="$(in_symbolsvmlinux|awk'$8 == "dup_counter" { print $2 }'|sort|head-1)"+["$first_addr"!="$low_addr"]||+probe_skip"linker did not reorder the two statics"++assert_input_symboldup_counter+run_diff++# Address order says 2. Counting symbol table order would say 1.+assert_klp_symposdup_counter2+out_symbols|grep-q'dup_counter,1'&&+fail"sympos 1 emitted: counted symbol table order instead of reading the linked vmlinux"++pass"vmlinux sympos taken from the linked image, not from symbol table order"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:28:56
Most static locals have to be correlated so the patched code keeps using
the running kernel's copy. Two kinds must not: anything in .data..once,
the flag behind WARN_ONCE and friends, and the well-known names the kernel
generates for per-instance things (__warned, __key, __func__).
Sharing a .data..once flag means a patch inherits "already warned" from
before it was applied, and the warning it was meant to surface never fires.
The fixture deliberately does not name its .data..once variable __warned:
the name rule would then catch it and the section rule would go untested.
gcc spells these <var>.<id> and Clang <func>.<var>, so both are covered.
This tests the behavior of commit ff529864e738 ("objtool/klp: Fix
.data..once static local non-correlation") and commit 84c304a534b8
("objtool/klp: Fix is_uncorrelated_static_local() for Clang").
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../fixtures/static_local_uncorrelated.c | 41 +++++++++++++++++++
.../generic/test-static-local-uncorrelated.sh | 40 ++++++++++++++++++
2 files changed, 81 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/static_local_uncorrelated.c
create mode 100755 tools/objtool/tests/generic/test-static-local-uncorrelated.sh
@@ -0,0 +1,41 @@+// SPDX-License-Identifier: GPL-2.0+/*+*Staticlocalsofthreekinds,inonepatchedfunction.+*+*Moststaticlocalsmustbecorrelated,sothepatchedcodekeepsusingthe+*runningkernel'scopy.Twokindsmustnot:+*+*-anythingin.data..once,theflagbehindWARN_ONCEandfriends.Sharing+*itwouldmeanapatchinherits"already warned"frombeforethepatch.+*-thewell-knownnamesthekernelgeneratesforsuchthings(__warned,+*__key,__func__,...),whichareper-instancebynature.gccnamesthem+*<var>.<id>andClang<func>.<var>,sobothspellingshavetobecaught.+*/++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++inttarget(intx)+{+/*+*A.data..oncevariablewhosenameis*not*onthelistbelow,so+*onlythesectioncandisqualifyit.Namingit__warnedwouldlet+*thenamerulecatchitandthesectionrulegountested.+*/+staticintonce_flag__attribute__((section(".data..once")));+/* a never-correlate name, in an ordinary section */+staticint__key;+/* and one that must be correlated */+staticintordinary;++if(!once_flag)+once_flag=1;+__key+=x;+ordinary+=x;++#ifdef PATCHED+return__key+ordinary+once_flag+2;+#else+return__key+ordinary+once_flag+1;+#endif+}
@@ -0,0 +1,40 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Some static locals must not be correlated with their counterparts in the+# running kernel; the patched code has to use a fresh copy instead.+#+# .data..once holds the "have we warned yet" flags behind WARN_ONCE. Correlate+# one and the patched function inherits the flag from before the patch, so the+# warning the patch was written to produce never fires. The same goes for the+# names the kernel generates for per-instance state -- __warned, __key,+# __func__ and friends.+#+# Both directions matter, so an ordinary static local is here too: a rule that+# refuses to correlate anything would pass a test that only checks the+# refusals.++."$(dirname"$0")/../lib.sh"++setup+build_pairstatic_local_uncorrelated.c+run_diff++# Compilers mangle static locals differently -- gcc gives __key.1, Clang+# target.__key -- so match on the base name.++# Correlated: referenced through a klp symbol, pointing at the kernel's copy.+out_symbols|grep-q'\.klp\.sym\..*ordinary'||+fail"ordinary static local was not correlated"++# Not correlated: no klp symbol, and a copy cloned into the patch instead.+out_symbols|grep-q'\.klp\.sym\..*__key'&&+fail"__key was correlated; it must use a fresh copy"+out_sections|grep-qE'\.(bss|data)[^ ]*__key'||+fail"__key was neither correlated nor cloned"++out_symbols|grep-q'\.klp\.sym\..*once_flag'&&+fail".data..once variable was correlated; it must use a fresh copy"+assert_section'.data..once'++pass"per-instance static locals cloned, ordinary ones correlated"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:01
Three special sections with no coverage. An entry that is not carried into
the patch is not a build failure: the patched function simply loses its
WARN_ON location, its exception fixup, or its ftrace callsite, and nobody
finds out until one of them is needed.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/x86/fixtures/special_sections.c | 77 +++++++++++++++++++
.../tests/x86/test-special-sections.sh | 42 ++++++++++
2 files changed, 119 insertions(+)
create mode 100644 tools/objtool/tests/x86/fixtures/special_sections.c
create mode 100755 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+forspecin"__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_pairspecial_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_patchedtarget++# Nothing belonging to the function that was not.+assert_not_patchedother+assert_no_reloc_sym"$sec"other++cleanup+done++pass"__bug_table, __ex_table and __mcount_loc entries extracted"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:07
Two properties, asserted separately because they fail differently:
- the __cfi_<func> prefix symbol has to be carried with the patched
function, or the function has no type identity and indirect calls to it
trap;
- its .kcfi_traps entry has to be extracted, or the trap is not
recognised as a CFI failure and a clean report becomes an oops.
test-special-section hand-assembles a .kcfi_traps entry, which is what
makes it catch commit 7df1638df97b ("objtool/klp: Fix .kcfi_traps special
section extraction"). Nothing until now built real kCFI code.
This tests the behavior of commit da4326573ae8 ("objtool/klp: Fix kCFI trap
handling"). Commit f7ceffd21a8a ("objtool/klp: Fix kCFI prefix
finding/cloning") cannot be tested as such, its code having been rewritten
by commit fe6a87e0abac ("objtool: Improve and simplify prefix symbol
detection"); this guards the current implementation's behaviour instead.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
tools/objtool/tests/x86/fixtures/kcfi.c | 39 +++++++++++++++++++++++++
tools/objtool/tests/x86/test-kcfi.sh | 39 +++++++++++++++++++++++++
2 files changed, 78 insertions(+)
create mode 100644 tools/objtool/tests/x86/fixtures/kcfi.c
create mode 100755 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_pairkcfi.c-fsanitize=kcfi++assert_input_section.kcfi_traps+assert_input_symbol__cfi_target++run_diff++assert_patchedtarget++# 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"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:12
A patch can move a symbol between static and global without renaming it:
dropping "static" from a helper so something else can call it, or adding it
to one that is no longer shared. Correlation keys off more than the name,
so a symbol whose binding moved has to still pair with itself.
Failing to is not a build failure. The symbol looks new, and a new data
symbol is either rejected or cloned as a second copy -- at which point the
patched code updates its own private variable while the rest of the kernel
keeps reading the original.
The test covers both directions in one fixture, a function going global and
a variable going static, and asserts the outcome rather than the absence of
a warning: each symbol resolves back to the kernel's copy through a klp
symbol, and neither is cloned into the patch.
An earlier version asserted only that no "no correlation" or "changed data"
message appeared, and passed with correlation deliberately broken. What
the messages say and what the patch contains are not the same question.
Verified to fail with correlation made to require matching symbol bindings.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/local_to_global.c | 34 +++++++++++++
.../generic/test-local-to-global-flip.sh | 50 +++++++++++++++++++
2 files changed, 84 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/local_to_global.c
create mode 100755 tools/objtool/tests/generic/test-local-to-global-flip.sh
@@ -0,0 +1,34 @@+// SPDX-License-Identifier: GPL-2.0+/*+*Afunctionwhichthepatchchangesfromstatictonon-static,andavariable+*thatgoestheotherway.Thenamesareunchanged;onlythebindingmoves.+*/++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++/* noinline, or the static one is folded into its caller and has no symbol */+#ifdef PATCHED+__attribute__((noinline))intflipped_up(intx)/* was static */+#else+__attribute__((noinline))staticintflipped_up(intx)+#endif+{+returnx+1;+}++#ifdef PATCHED+staticvolatileintflipped_down=5;/* was global */+#else+volatileintflipped_down=5;+#endif++intcaller(intx)+{+flipped_down+=x;+#ifdef PATCHED+returnflipped_up(x)+flipped_down+2;+#else+returnflipped_up(x)+flipped_down+1;+#endif+}
@@ -0,0 +1,50 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A patch can change a symbol's linkage without renaming it: dropping "static"+# from a helper so something else can call it, or adding it to one that is no+# longer shared. Correlation keys off more than the name, so a symbol whose+# binding moved can fail to pair with itself.+#+# Failing to correlate is not a build failure. The symbol looks new, and a+# "new" data symbol is either rejected or cloned as a second copy -- at which+# point the patched code updates its own private variable and the rest of the+# kernel keeps reading the original.++."$(dirname"$0")/../lib.sh"++setup+build_pairlocal_to_global.c++# Confirm the fixture really moved the bindings, in both directions.+in_symbolsorig.o|grep-qE'LOCAL.*flipped_up'||+fail"flipped_up is not local in the original"+in_symbolspatched.o|grep-qE'GLOBAL.*flipped_up'||+fail"flipped_up is not global in the patched object"+in_symbolsorig.o|grep-qE'GLOBAL.*flipped_down'||+fail"flipped_down is not global in the original"+in_symbolspatched.o|grep-qE'LOCAL.*flipped_down'||+fail"flipped_down is not local in the patched object"++run_diff++assert_diff_log'changed function: caller'++# Correlated means each pairs with its own counterpart in the original, so the+# patch refers back to the kernel's copy ...+assert_klp_symflipped_upvmlinux+assert_klp_symflipped_downvmlinux++# ... rather than carrying its own. A second copy of flipped_down is the bad+# outcome: patched code would update its private one while the rest of the+# kernel keeps reading the original.+assert_not_patchedflipped_up+assert_no_section.data.flipped_down+assert_no_section.bss.flipped_down++diff_log|grep-q'no correlation'&&+fail"linkage change reported as an uncorrelated symbol"+diff_log|grep-q'changed data'&&+fail"linkage change reported as changed data"++pass"symbols correlated across a change of linkage"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:17
A static branch key owned by a module cannot be reached with a klp reloc:
late module patching allows the livepatch module to load first, leaving the
__jump_table entry unresolved for jump_label_add_module() to dereference.
validate_special_section_klp_reloc() rejects it at build time.
test-jump-label-module-key covers that for a global key. A file-local one
takes a different route to the same check: the compiler references a static
through its section symbol plus an addend, so the key has to be resolved
from the section before it can be recognised as STT_OBJECT at all. Until
commit f9fb44b0ecef ("objtool/klp: Fix detection of corrupt static
branch/call entries") it was not, and the reference was silently emitted.
Give the fixture a STATIC_KEY knob and cover it. The test checks that the
input really does reference the key through its section, since without that
it is only a second copy of the existing test.
Verified by reverting commit f9fb44b0ecef ("objtool/klp: Fix detection of
corrupt static branch/call entries"): klp diff accepts the input and the
test fails, under both gcc and clang.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../test-jump-label-module-static-key.sh | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-jump-label-module-static-key.sh
@@ -0,0 +1,45 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A static branch key owned by a module is rejected whether the key is global+# or file-local.+#+# The rejection matters because late module patching allows the livepatch+# module to load before the module it depends on: the __jump_table klp reloc is+# then unresolved, and jump_label_add_module() dereferences an uninitialized+# pointer. Catching it at build time is the only defence.+#+# test-jump-label-module-key covers the global key. A file-local one reaches+# the same check by a different route: the compiler emits the reference against+# the section symbol plus an addend, so validate_special_section_klp_reloc()+# has to resolve it to the underlying object before it can see a key at all.+# Until it did, a static key was passed over as "not STT_OBJECT" and the+# unsupported reference was emitted with nothing said.+#+# Fixed by f9fb44b0ecef ("objtool/klp: Fix detection of corrupt static+# branch/call entries").++."$(dirname"$0")/../lib.sh"++setup+build_pairjump_label.c-DSTATIC_KEY-DMODNAME='"klp_testmod"'++require_input_section__jump_table++# The premise: the key is reached through its section symbol, not by name.+# Without that this is just a second copy of test-jump-label-module-key.+input_jump_relocs="$(in_relocsorig.o|awk'/rela__jump_table/,/^$/')"++echo"$input_jump_relocs"|grep-qklp_test_key||+fail"fixture produced no __jump_table reference to the key"+echo"$input_jump_relocs"|grep-qE'\.(bss|data)\.klp_test_key'||+probe_skip"compiler referenced the static key by name, not through its section"++run_diff255++diff_log|grep-q'unsupported static branch key klp_test_key'||+fail"expected rejection, got: $(diff_log|tail-1)"+[-e"$workdir/out.o"]&&+fail"output object produced for a rejected input"++pass"module-owned file-local static branch key rejected"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:23
__SCK__* static call keys are not exported; modules are given read-only
access at load time. Livepatch modules built by klp-build do have full
access to theirs, and commit 164c9201e1da ("objtool: Add base objtool
support for livepatch modules") added a check on that basis -- but a
livepatch module can also be written by hand, as everything under
samples/livepatch is, and such a module hits an unexported key as soon as
it does anything expanding to a static call. With
CONFIG_MEM_ALLOC_PROFILING_DEBUG that includes allocating memory, which is
how livepatch-shadow-fix1 came to fail to build.
Cover it, with the plain module as a control: it takes the same path and
has always been accepted, so a test that built only the livepatch variant
could not tell this fix from the check being deleted.
This is objtool's ordinary check pass rather than a klp subcommand, which
is the first test here to exercise it -- and is the point, since that pass
is what runs over a hand-built livepatch module during a normal kernel
build.
Verified by reverting commit f495054bd12e ("objtool/klp: Fix unexported
static call key access for manually built livepatch modules"): objtool
reports "can't find static_call_key symbol: __SCK__klp_test_call" and the
test fails, under both gcc and clang.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/x86/fixtures/static_call_no_key.c | 32 +++++++++++++++
.../tests/x86/test-manual-klp-static-call.sh | 40 +++++++++++++++++++
2 files changed, 72 insertions(+)
create mode 100644 tools/objtool/tests/x86/fixtures/static_call_no_key.c
create mode 100755 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.+fortagin""-DLIVEPATCH;do+build_onestatic_call_no_key.cmod.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"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:28
The kernel annotates instructions inside an ALTERNATIVE's replacement
wherever objtool has to be told something about them -- a retpoline-safe
indirect branch, a deliberately absent ENDBR. klp diff dropped those
annotations, for two reasons: replacement code has no real symbol, so the
NOTYPE fake symbol objtool invents for it was not recognised as worth
keeping a reference to, and .discard.annotate_insn was processed before
.altinstructions, so the replacement it named had no clone yet.
Nothing fails at build time when the annotation goes missing. It surfaces
later as objtool warning about, or rejecting, the very code the annotation
was there to explain.
The fixture keeps the replacement label global, as empty_alternative.c
does, so the relocations name it instead of .altinstr_replacement plus an
addend -- which klp diff rejects outright. The label is still NOTYPE,
which is the property under test. The test asserts the relocation as well
as the section, since an entry whose relocation was dropped would otherwise
pass.
Verified by reverting commit 62a7a01fde87 ("objtool/klp: Fix extraction of
text annotations for alternatives"): the output has no
.discard.annotate_insn at all and the test fails, under both gcc and clang.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../objtool/tests/x86/fixtures/alt_annotate.c | 57 +++++++++++++++++++
.../objtool/tests/x86/test-alt-annotation.sh | 38 +++++++++++++
2 files changed, 95 insertions(+)
create mode 100644 tools/objtool/tests/x86/fixtures/alt_annotate.c
create mode 100755 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_pairalt_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_insntarget_repl++pass"text annotation on an alternative replacement carried into the patch"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:33
klp checksum hashes a data symbol's length, its bytes, and every relocation
it carries -- as the target's name and adjusted addend, except a reference
into a string section, which contributes the string's contents instead.
Nothing covered any of it.
Each is load-bearing, and the failure is always the same shape: a checksum
which ignores one calls a changed object unchanged, klp diff leaves it out
of the patch, and the patched code goes on reading the kernel's old copy.
The string case cannot be caught by hashing bytes: the pointer is
identical, same section and same offset, and only the text it refers to
moved.
One fixture, six variants applied to the patched build alone. Each was
verified by sabotaging the line it covers and watching the test fail:
raw bytes initialiser change
length a .bss object grows; its bytes are never hashed
string contents literal edited in place, pointer untouched
reloc target name pointer moved to another function
reloc addend same array, different index
section-symbol path the same, via a static's section symbol
Two of those needed the fixture rebuilding. An initialised array does not
isolate the length, because growing one changes the hashed bytes too --
hence .bss, where there are none. And a named char[] does not reach the
contents-hashing path at all: that keys on SHF_STRINGS, which the compiler
sets on the mergeable section a literal lands in and not on an array given
a section of its own.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/checksum_data.c | 116 ++++++++++++++++++
.../tests/generic/test-checksum-data.sh | 61 +++++++++
2 files changed, 177 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_data.c
create mode 100755 tools/objtool/tests/generic/test-checksum-data.sh
@@ -0,0 +1,61 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# What a data object's checksum has to cover.+#+# checksum_update_object() hashes the symbol's length, its bytes (when the+# section has any -- .bss does not), and then+# every relocation it carries -- as the target's name plus the adjusted addend,+# except for a reference into a string section, which contributes the string's+# contents instead.+#+# Each of those is load-bearing, and the failure is always the same shape: a+# checksum that ignores one of them calls a changed object unchanged, klp diff+# leaves it out of the patch, and the patched code goes on reading the+# kernel's old copy. Nothing says so at build time.+#+# The string case is the one that cannot be caught by hashing bytes alone. The+# pointer is identical -- same section, same offset -- and only the text it+# refers to moved.+#+# Covers the same ground as corpus/x86_64/checksum-data-basic,+# checksum-data-func-ptr, checksum-data-string-ptr and checksum-string-reloc in+# Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup++# check <flag> <symbol> <what changed>+#+# Build the pair with one difference and require that symbol's checksum to move.+check()+{+build_pairchecksum_data.c"-D$1"+run_checksum++assert_checksum_differs"$2"+}++# The object's own bytes.+checkPLAIN_VALUEplain+# Its length, for a .bss object whose bytes are not hashed at all.+checkLONGERsized+# A relocation's target: same bytes in the object, different symbol named.+checkWHICH_FUNCdescriptor+checkWHICH_STRdescriptor+# The contents of a string the object points at, with the pointer untouched.+checkSTR_CONTENTdescriptor+# A relocation's addend: same target symbol, different offset into it.+checkWHICH_SLOTdescriptor+# The same, for a static reached through its section symbol: the reference has+# to be resolved back to the object before there is a name or offset to hash.+checkWHICH_PRIVdescriptor++# Having shown five things that must change it, show one that must not: an+# unrelated edit elsewhere in the file leaves this object alone.+build_pairchecksum_data.c-DPLAIN_VALUE+run_checksum+assert_checksum_matchesdescriptor++pass"data checksums cover length, bytes, reloc targets and string contents"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:39
.discard.sym_checksum is an array of { address, checksum } looked up by the
address a relocation points at, so the invariant is one entry per address.
calculate_checksums() skips zero-length symbols, aliases and cold parts to
keep it, and nothing checked that it does.
A duplicate entry is not a build failure. It makes the lookup ambiguous,
and whichever checksum loses is never consulted again -- so a function
whose code changed can be read as unchanged and dropped from the patch.
The alias skip is verified the usual way: remove it and the test fails.
The other two are not isolated, and the test says so rather than implying
otherwise. A zero-length symbol is excluded by several of the guards at
once -- its section has no data either -- so removing any one of them
changes nothing observable; the assertion stands as a check on the
behaviour, not on the line that produces it. The cold-part skip needs a
compiler that splits functions and is not reached here at all.
Which of an aliased pair keeps the entry falls out of symbol table order,
so the test requires exactly one of the two rather than naming a winner --
as written first it named real_function and failed, because gcc kept the
alias.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/checksum_skip.c | 47 ++++++++++++
.../tests/generic/test-checksum-skip.sh | 72 +++++++++++++++++++
2 files changed, 119 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_skip.c
create mode 100755 tools/objtool/tests/generic/test-checksum-skip.sh
@@ -0,0 +1,47 @@+// SPDX-License-Identifier: GPL-2.0+/*+*Symbolscalculate_checksums()mustnotgiveanentryoftheirown.+*+*Threekindsareskipped,fortwodifferentreasons:+*+*zero-lengththereisnothingtohash,andanentrykeyedonthe+*symbol'saddresswouldcollidewithwhateverreallylives+*there.+*aliasasecondnameforanaddressalreadychecksummed.+*coldparthashedaspartofitsparent,whichfunc_for_each_insn()+*walksinto,soaseparateentrywoulddouble-countit.+*+*Anentryperaddressistheinvariant:.discard.sym_checksumislookedupby+*theaddressarelocationpointsat,sotwoentriesforoneaddressmakethe+*lookupambiguousandoneofthetwochecksumsunreachable.+*/++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++/* Zero-length: an object symbol of size 0, in a section of its own. */+externcharempty_marker[];+__asm__(".pushsection .data.empty_marker,\"aw\",@progbits\n"+".globl empty_marker\n"+".type empty_marker, @object\n"+"empty_marker:\n"+".size empty_marker, 0\n"+".popsection\n");++intreal_function(intx);+intreal_function(intx)+{+#ifdef PATCHED+returnx+2;+#else+returnx+1;+#endif+}++/* Alias: a second name for real_function's address. */+intalias_function(intx)__attribute__((alias("real_function")));++inttarget(intx)+{+returnreal_function(x)+alias_function(x)+(int)(long)empty_marker;+}
@@ -0,0 +1,72 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Symbols which must not get a checksum entry of their own.+#+# .discard.sym_checksum is an array of { address, checksum } looked up by the+# address a relocation points at, so the invariant is one entry per address.+# calculate_checksums() skips three kinds of symbol to keep it:+#+# zero-length nothing to hash, and its address belongs to whatever really+# lives there+# alias a second name for an address already covered+# cold part hashed into its parent, which func_for_each_insn() walks+# into, so its own entry would double-count+#+# A duplicate entry is not a build failure. It makes the lookup ambiguous, and+# whichever checksum loses is simply never consulted again -- so a function+# whose code changed can be read as unchanged and dropped from the patch.+#+# Of the three, only the alias skip is isolated here: removing it makes this+# test fail. A zero-length symbol is excluded by more than one of the guards+# at once -- its section has no data either -- so no single change makes that+# assertion fail, and it stands as a check on the behaviour rather than on the+# line which produces it. Nothing here reaches the cold-part skip, which+# wants a compiler that splits functions; test-cold-function covers that+# symbol surviving into the patch, not its checksum.+#+# Covers the same ground as corpus/x86_64/checksum-zero-len-sym,+# checksum-alias-skip and checksum-cold-skip in Joe Lawrence's klp-build unit+# test corpus.++."$(dirname"$0")/../lib.sh"++setup+build_pairchecksum_skip.c++assert_input_symbolempty_marker+assert_input_symbolalias_function+run_checksum++# entries_for <object>+#+# The symbol names .discard.sym_checksum has an entry for, one per line.+entries_for()+{+in_relocs"$1"|awk'/rela\.discard\.sym_checksum/,/^$/{+if($1~/^[0-9a-f]{8,}/)print$5+}'+}++entries="$(entries_fororig.o)"++# The control: something real did get an entry, so an empty listing cannot+# make the rest of this pass by default.+echo"$entries"|grep-qxtarget||+fail"no checksum entry for target"++echo"$entries"|grep-qxempty_marker&&+fail"zero-length symbol got a checksum entry"++# One of the two names for that address is kept and the other skipped; which+# one falls out of symbol table order and is not the point. Two would be.+n="$(echo"$entries"|grep-cxE'real_function|alias_function')"+["$n"=1]||+fail"expected 1 checksum entry across real_function and its alias, found $n"++# One entry per address, which is what the skipping is for.+dupes="$(entries_fororig.o|sort|uniq-d)"+[-z"$dupes"]||+fail"two checksum entries for one address: $dupes"++pass"zero-length symbols and aliases get no checksum entry of their own"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:44
Patching a function which already has a static branch and adding one to a
function which had none are different cases. In the second the
__jump_table entry is itself new, so there is nothing in the original to
correlate it against: klp diff has to carry the entry into the patch from
scratch and reach the key the way it reaches any other vmlinux symbol.
Dropping it is silent. The patched function keeps a static branch the
kernel never patches, so it takes the same arm forever whatever the key is
set to.
Give the fixture a NEW_KEY knob which puts the whole branch behind PATCHED,
and assert the premise -- that the original really has no __jump_table --
before asserting the result, since otherwise this is just a second copy of
test-jump-label-key.
Verified by making klp diff skip __jump_table when cloning special
sections: the test fails.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/test-jump-label-new-key.sh | 51 +++++++++++++++++++
1 file changed, 51 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-jump-label-new-key.sh
@@ -0,0 +1,51 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A patch may introduce a static branch where the original function had none.+#+# That is not the same case as patching a function which already has one. The+# __jump_table entry is itself new, so there is no counterpart in the original+# to correlate it against: klp diff has to carry the entry and the key into the+# patch from scratch, and the key has to be reached the way any other reference+# to a vmlinux symbol is.+#+# Get it wrong and the entry is dropped, leaving a static branch the kernel+# never patches -- the code takes the wrong arm forever, silently.+#+# Where the key lives still decides whether that is allowed, exactly as it does+# for a key the original already had: a module-owned one cannot be reached, so+# introducing one has to stop the build rather than emit an entry nothing will+# resolve.+#+# Covers the same ground as corpus/x86_64/static-branch-vmlinux-new and+# static-branch-module-new in Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setupklp_test_key+build_pairjump_label.c-DNEW_KEY++# The premise: the original really has no jump table, and the patched one does.+has_input_sectionorig.o__jump_table&&+fail"fixture put a __jump_table in the original; nothing new to add"+has_input_sectionpatched.o__jump_table||+probe_skip"compiler produced no __jump_table on this arch"++run_diff++assert_patchedtarget+assert_section__jump_table+assert_reloc_sym__jump_tabletarget++# The same new branch, with the key owned by a module. Drop the vmlinux export+# first: while it is exported the key is reachable and being new changes+# nothing, which is what the first version of this got wrong.+export_syms+rm-f"$workdir/out.o"+build_pairjump_label.c-DNEW_KEY-DMODNAME='"klp_testmod"'+run_diff255+assert_diff_log'unsupported static branch key klp_test_key'+[-e"$workdir/out.o"]&&+fail"output object produced for a rejected input"++pass"static branch introduced by the patch carried in, or rejected for a module key"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:50
A module-owned static branch key is normally fatal, because late module
patching lets the livepatch load before the module it depends on and
jump_label_add_module() then dereferences an unresolved entry.
Tracepoints and pr_debug() generate such keys everywhere, so refusing them
outright would make any function containing a trace_*() call or a
pr_debug() unpatchable. klp diff drops the entry, warns, and carries on:
the patched code works with that one tracepoint or debug print permanently
off.
Both halves matter, and the test asserts both. A build which fails is a
function nobody can patch; an entry left in place is the corruption the
rejection exists to prevent.
Give the fixture a KEY_NAME knob so the same static branch can be built
with either special name. Verified by removing each exemption in turn --
the test fails for both. That the entry is then dropped is asserted but
not isolated, and the test says so.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/test-jump-label-exempt-keys.sh | 51 +++++++++++++++++++
1 file changed, 51 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-jump-label-exempt-keys.sh
@@ -0,0 +1,51 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Two kinds of module-owned static branch key are disabled with a warning+# instead of rejected.+#+# A module-local key is normally fatal: late module patching lets the livepatch+# load before the module it depends on, so the unresolved __jump_table entry is+# dereferenced by jump_label_add_module(). test-jump-label-module-key covers+# that rejection.+#+# Tracepoints and pr_debug() generate such keys everywhere, though, and+# refusing them outright would make any function containing a trace_*() call or+# a pr_debug() unpatchable. So klp diff drops the entry, says so, and carries+# on: the patched code keeps working with that one tracepoint or debug print+# permanently off.+#+# Both halves matter. A build that fails is a function nobody can patch; an+# entry left in place is the memory corruption the rejection exists to prevent.+#+# The two exemptions are isolated: remove either and this fails. That the+# entry is then dropped is asserted but not isolated -- making the caller keep+# it anyway produces no output difference here, so that assertion stands as a+# check on the behaviour rather than on the line which produces it.+#+# Covers the same ground as corpus/x86_64/static-call-module-tracepoint and+# pr-debug-unsupported in Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup++# check <key name> <expected warning>+check()+{+build_pairjump_label.c-DKEY_NAME="$1"-DMODNAME='"klp_testmod"'+require_input_section__jump_table++# Accepted, not rejected: this is the whole point.+run_diff+assert_diff_log"$2"++# And the entry is gone, not merely complained about.+assert_patchedtarget+assert_no_section__jump_table+}++check__tracepoint_klp_test'disabling unsupported tracepoint klp_test'+check__UNIQUE_ID_ddebug_klp_test'disabling unsupported pr_debug'++pass"tracepoint and pr_debug keys disabled with a warning, not rejected"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:29:56
The counterpart to the static branch case: a patch may add a static call to
a function which had none, so the .static_call_sites entry is new and there
is nothing in the original to correlate it against.
Where the key lives still decides whether that is allowed. A vmlinux key
is reachable; a module-owned one is not, for the same reason an existing
module key is not -- late module patching lets the livepatch load first and
the unresolved entry is dereferenced when the module arrives. Both halves
are here because they fail in opposite directions: dropping the new entry
leaves a static call the kernel never patches, and accepting a new
module-owned one is the corruption the check exists to prevent.
Both halves are one test over one fixture: built with -DNEW_CALL the key is
vmlinux's and the new entry has to be carried in; built with -DMODNAME as
well the key belongs to a module, klp diff has to reject it, and no output
object may be left behind. The premise is asserted first -- the original
must have no .static_call_sites at all -- since otherwise this is a second
copy of test-static-call-module-key.
Covers the same ground as corpus/x86_64/static-call-vmlinux-new and
corpus/x86_64/static-call-module-new in Joe Lawrence's klp-build unit test
corpus.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/test-static-call-new.sh | 45 +++++++++++++++++++
1 file changed, 45 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-static-call-new.sh
@@ -0,0 +1,45 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A patch may introduce a static call where the original function had none.+#+# The .static_call_sites entry is then new, with nothing in the original to+# correlate it against, so klp diff has to carry it into the patch from+# scratch. Where the key lives still decides whether that is allowed: a+# vmlinux key is reachable, and a module-owned one is not, for the same reason+# an existing module key is not -- late module patching lets the livepatch load+# first, and the unresolved entry is dereferenced when the module arrives.+#+# Both halves are here because they fail in opposite directions. Dropping the+# new entry leaves a static call the kernel never patches; accepting a new+# module-owned one is the corruption the check exists to prevent.+#+# Covers the same ground as corpus/x86_64/static-call-vmlinux-new and+# static-call-module-new in Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup+build_pairstatic_call.c-DNEW_CALL++# The premise: the original really has no static call, the patched one does.+has_input_sectionorig.o.static_call_sites&&+fail"fixture put a .static_call_sites in the original; nothing new to add"+has_input_sectionpatched.o.static_call_sites||+probe_skip"compiler produced no .static_call_sites on this arch"++run_diff+assert_patchedtarget+assert_section.static_call_sites+assert_reloc_sym.static_call_sitestarget++# The same new call, with the key owned by a module: not reachable, so the+# build has to stop rather than emit a relocation nothing will resolve.+rm-f"$workdir/out.o"+build_pairstatic_call.c-DNEW_CALL-DMODNAME='"klp_testmod"'+run_diff255+assert_diff_log'unsupported static call key __SCK__klp_test_call'+[-e"$workdir/out.o"]&&+fail"output object produced for a rejected input"++pass"static call introduced by the patch carried in, or rejected for a module key"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:02
checksum_update_insn() hashes an instruction's bytes and then what any
relocation on it refers to: a string section contributes the string's
contents, anything else the target symbol's name and adjusted addend, with
a reference to a static resolved through its section symbol first.
None of that shows up in the bytes. A rel32 operand is zero in the object
and supplied by the relocation, so calling a different function, editing a
literal the code passes, or reading a different index of an array all leave
the encoded instruction byte-identical. A checksum stopping at the bytes
reports the function unchanged and the patch silently does not contain the
fix.
test-checksum-position is the other half: what must *not* change the
checksum when a function merely moves.
Each of the four is verified by sabotaging the line it covers. The static
case needed a writer the compiler cannot see through -- without one it
proves the array is never written, folds every read to zero, and emits no
relocation at all, so the reference under test does not exist and the
variant passes having compared two identical objects.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/checksum_insn.c | 78 +++++++++++++++++++
.../tests/generic/test-checksum-insn.sh | 49 ++++++++++++
2 files changed, 127 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/checksum_insn.c
create mode 100755 tools/objtool/tests/generic/test-checksum-insn.sh
@@ -0,0 +1,49 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# What a function's checksum has to cover beyond its instruction bytes.+#+# checksum_update_insn() hashes the raw bytes, and then what any relocation on+# the instruction refers to: a string section contributes the string's+# contents, anything else the target symbol's name and the adjusted addend,+# with a reference to a static resolved back through its section symbol first.+#+# None of these show up in the bytes. A rel32 operand is zero in the object+# and supplied by the relocation, so every change below leaves the encoded+# instruction byte-identical. A checksum stopping at the bytes reports the+# function unchanged, klp diff omits it, and the patch silently does not+# contain the fix.+#+# test-checksum-position is the other half of this: it covers what must *not*+# change the checksum when a function merely moves.+#+# Covers the same ground as corpus/x86_64/checksum-reloc-sym,+# checksum-pc-relative-addend, checksum-string-reloc and+# checksum-sec-sym-resolve in Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup++# check <flag> <what it changes>+check()+{+build_pairchecksum_insn.c"-D$1"+run_checksum++# The premise for all of them: the operand is a relocation, not bytes.+assert_checksum_differstarget+}++checkWHICH_CALL# relocation target name+checkSTR_CONTENT# contents of a string the code passes+checkWHICH_SLOT# addend, same target symbol+checkWHICH_PRIV# addend via a static's section symbol++# The converse: rebuilding identical source leaves it alone, so the above is+# not just "any rebuild moves the checksum".+build_pairchecksum_insn.c+run_checksum+assert_checksum_matchestarget++pass"instruction checksums cover reloc targets, addends and string contents"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:07
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. A patch which edits only
the replacement -- code that runs on some CPUs and not others -- still has
to move the function's checksum.
When it does not, klp diff calls the function unchanged and leaves it out.
The patch 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 it presents as a machine-specific bug rather than a missing
patch.
insn->alts is built by objtool's check pass, not by the compiler, so the
pair goes through that first. --mcount is the action used: it is the
cheapest one that does not also need --link.
Two of the three paths are isolated. Skipping the alts walk and dropping
the feature hash both make this fail, and the second of those only exists
inside the alt_group branch, so reaching it proves the grouped path is
taken. The alternative's type is hashed but not varied here -- the fixture
emits one kind of alternative -- so that line is covered without being
isolated, as is the in_alt recursion guard, which wants nested
alternatives.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../objtool/tests/x86/fixtures/checksum_alt.c | 66 +++++++++++++++++++
tools/objtool/tests/x86/test-checksum-alt.sh | 45 +++++++++++++
2 files changed, 111 insertions(+)
create mode 100644 tools/objtool/tests/x86/fixtures/checksum_alt.c
create mode 100755 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_pairchecksum_alt.c"-D$1"+assert_input_section.altinstructions+run_objtool_check--mcount+run_checksum++assert_checksum_differstarget+}++# The replacement instruction itself.+checkALT_REPL+# The feature number, with no instruction anywhere changed.+checkALT_FEATURE++pass"alternative replacement code counts towards the checksum"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:12
A cloned data section has to keep its sh_addralign. Plenty of kernel data
is aligned for correctness rather than speed -- per-CPU variables, anything
touched by an aligned vector move, structures padded to own a cacheline --
and a clone that lands under-aligned either faults on first use or silently
shares a line it was laid out to avoid. Neither shows up until the patch
is loaded on hardware that cares.
The fixture's data is new in the patched build, so klp diff has to clone it
rather than reference the kernel's copy, and it asserts that premise before
asserting the result.
Commit 2f2600decb30 ("objtool/klp: fix data alignment in __clone_symbol()")
cannot be reverted to check this -- the revert is a no-op against the
current code, which has been rewritten since. Verified instead by forcing
the clone's alignment to 1, which the test reports as "alignment 1,
expected 64".
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/data_alignment.c | 29 ++++++++++++++
.../tests/generic/test-data-alignment.sh | 40 +++++++++++++++++++
2 files changed, 69 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/data_alignment.c
create mode 100755 tools/objtool/tests/generic/test-data-alignment.sh
@@ -0,0 +1,40 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A cloned data section keeps its alignment.+#+# Plenty of kernel data is aligned for correctness rather than speed: per-CPU+# variables, anything touched by an aligned vector move, structures padded to+# own a cacheline. A clone that lands under-aligned either faults on first use+# or silently shares a line it was laid out to avoid, and neither shows up+# until the patch is loaded on hardware that cares.+#+# Fixed by 2f2600decb30 ("objtool/klp: Fix alignment of cloned data+# sections").+#+# Covers the same ground as corpus/x86_64/cloned-data-alignment in Joe+# Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup+build_pairdata_alignment.c++# The premise: the compiler really did over-align it, and the object is new in+# the patch so it has to be cloned rather than referenced.+want="$(in_sectionspatched.o|sed's/^ *\[[ 0-9]*\] *//'|+awk'$1 == ".data.aligned_data" { print $NF }')"+["$want"=64]||+probe_skip"compiler gave .data.aligned_data alignment '$want', not 64"+has_input_sectionorig.o.data.aligned_data&&+fail"fixture put aligned_data in the original; nothing to clone"++run_diff+assert_section.data.aligned_data++got="$(out_sections|sed's/^ *\[[ 0-9]*\] *//'|+awk'$1 == ".data.aligned_data" { print $NF }')"+["$got"="$want"]||+fail"cloned .data.aligned_data has alignment $got, expected $want"++pass"cloned data section keeps its alignment"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:18
klp diff needs entry boundaries for a special section: either an entsize,
or ANNOTATE_DATA_SPECIAL annotations naming where each entry starts.
.static_call_sites has no entsize, so the annotations are all there is --
and a patch can remove the last one in a translation unit while leaving the
section itself in place, so that only the patched side has lost them.
The section still has to be handled. Dropping it leaves the patched
function's static call unregistered; misreading its boundaries attaches the
entry to the wrong code. Neither is reported at build time.
Give the fixture a NO_ANNOTATE knob and assert the premise -- annotation
present in the original, absent in the patched object, section present in
both -- before asserting the result.
Fixed by commit 3de711fba73a ("objtool/klp: Fix create_fake_symbols()
skipping entsize-based sections"). Verified by making klp diff skip
.static_call_sites when cloning special sections: the test fails.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../test-static-call-annotate-stripped.sh | 42 +++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100755 tools/objtool/tests/generic/test-static-call-annotate-stripped.sh
@@ -0,0 +1,42 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A patch may remove the last ANNOTATE_DATA_SPECIAL in a translation unit while+# leaving the special section it described in place.+#+# klp diff needs entry boundaries for a special section: either an entsize, or+# annotations naming where each entry starts. .static_call_sites has no+# entsize, so the annotations are all there is -- and when the patched object+# is the only side that lost them, the two sides no longer agree on how the+# section divides up.+#+# The section must still be handled. Dropping it would leave the patched+# function's static call unregistered; misreading its boundaries would attach+# the entry to the wrong code. Either way nothing is reported at build time.+#+# Fixed by commit 3de711fba73a ("objtool/klp: Fix create_fake_symbols()+# skipping entsize-based sections").+#+# Covers the same ground as corpus/x86_64/static-call-annotate-stripped in Joe+# Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup+build_pairstatic_call.c-DNO_ANNOTATE++# The premise: the original describes its entry, the patched one no longer+# does, and both still have the section itself.+has_input_sectionorig.o.discard.annotate_data||+fail"fixture produced no annotation in the original"+has_input_sectionpatched.o.discard.annotate_data&&+fail"patched object still has the annotation; nothing was stripped"+assert_input_section.static_call_sites++run_diff++assert_patchedtarget+assert_section.static_call_sites+assert_reloc_sym.static_call_sitestarget++pass"static call site kept when the patch strips its data annotation"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:23
A SHN_ABS symbol has no section, so any walk of sym->sec which does not
check dereferences NULL, and the kernel has plenty of them -- from linker
scripts and from .set in assembly. __ADDRESSABLE() emits a pointer into
.discard.addressable purely to keep a symbol referenced; it means nothing
to a livepatch and is discarded at link time, but it is a relocation like
any other and gets looked at.
Neither is what the patch changes. What this guards against is not a wrong
answer but a crash or an error on input the kernel produces routinely,
which would make every function near one unpatchable.
Not isolated to a single line, and the test says so: the absolute symbol
here has zero length, so it is excluded before the section check is reached
and removing that check alone changes nothing observable.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/fixtures/abs_and_addressable.c | 44 ++++++++++++++++
.../tests/generic/test-abs-and-addressable.sh | 50 +++++++++++++++++++
2 files changed, 94 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/abs_and_addressable.c
create mode 100755 tools/objtool/tests/generic/test-abs-and-addressable.sh
@@ -0,0 +1,50 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# An absolute symbol and an __ADDRESSABLE() pointer must not disturb the+# function being patched.+#+# A SHN_ABS symbol has no section, so any walk of sym->sec which does not check+# dereferences NULL -- and the kernel has plenty, from linker scripts and from+# .set in assembly. __ADDRESSABLE() emits a pointer into .discard.addressable+# to keep a symbol referenced; it means nothing to a livepatch and is discarded+# at link time, but it is a relocation like any other and gets looked at.+#+# Neither is what the patch changes. The failure this guards against is not a+# wrong answer but a crash or an error on input the kernel produces routinely,+# which would make any function near one unpatchable.+#+# Not isolated to a single guard: the absolute symbol here has zero length, so+# it is excluded before the section check is reached and removing that check+# alone changes nothing observable. This stands as a check on the behaviour+# rather than on the line which produces it.+#+# Covers the same ground as corpus/x86_64/checksum-abs-sym-skip and+# addressable-symbols in Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup+build_pairabs_and_addressable.c++# The premise: the fixture really did produce both.+in_symbolsorig.o|grep-q'ABS.*abs_sym'||+probe_skip"assembler did not make abs_sym absolute here"+assert_input_section.discard.addressable++# Checksumming has to survive them, and still see the function that changed.+run_checksum+assert_checksum_differstarget+assert_checksum_matcheshelper++# So does the diff.+run_diff+assert_patchedtarget+assert_not_patchedhelper++# An absolute symbol has no address to record a checksum against, so it gets+# no entry -- the reference to it is what mattered, not the symbol itself.+in_relocsorig.o|awk'/rela\.discard\.sym_checksum/,/^$/'|grep-qwabs_sym&&+fail"absolute symbol got a checksum entry"++pass"absolute and __ADDRESSABLE symbols do not disturb the patched function"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:28
Every instrumented operation gets a per-callsite metadata object in an
anonymous data section -- .data..Lubsan_data and .data..Lubsan_type from
GCC, .data..L__unnamed_ from Clang -- whose names are compiler-generated
and mean nothing across a rebuild. is_uncorrelated_section() exists so klp
diff does not try to pair them up, and nothing tested it.
The failure it prevents is a false positive, which is the direction this
suite has least coverage of. Metadata belonging to a function nobody
touched compares as different and drags that function into the patch. That
is not a build failure: it is a larger livepatch than intended, pulling in
dependencies with it, and every extra function is one more that can fail to
correlate or to apply.
The fixture is built with -fsanitize=shift, which both compilers
instrument; neither emits a bounds check for an index it can prove in
range. One function changes, the other is byte-identical and carries
instrumentation of its own, and the test asserts the second is left alone.
Verified by removing each rule from is_uncorrelated_section() in turn,
which splits neatly by toolchain: dropping the .data..Lubsan rule fails the
test under gcc, dropping .data..L__unnamed_ fails it under clang. One
test, two code paths, each checked by the compiler that reaches it.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/ubsan_noise.c | 49 +++++++++++++++++++
.../objtool/tests/generic/test-ubsan-noise.sh | 48 ++++++++++++++++++
2 files changed, 97 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/ubsan_noise.c
create mode 100755 tools/objtool/tests/generic/test-ubsan-noise.sh
@@ -0,0 +1,48 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# UBSAN instrumentation in an unchanged function must not make it look changed.+#+# Every instrumented operation gets a per-callsite metadata object in an+# anonymous data section -- .data..Lubsan_data and .data..Lubsan_type from GCC,+# .data..L__unnamed_ from Clang -- whose names are compiler-generated and mean+# nothing across a rebuild. is_uncorrelated_section() exists so klp diff does+# not try to pair them up.+#+# Without that, the metadata belonging to a function nobody touched compares as+# different and drags the function into the patch. A livepatch which replaces+# functions the patch never changed is not a build failure: it is a larger+# patch than intended, taking its dependencies with it, and every extra+# function is one more that can fail to correlate or to apply.+#+# Covers the same ground as corpus/x86_64-ubsan/{ubsan-shift-noise,+# ubsan-metadata-data-section,gcc-ubsan-anonymous-data,ubsan-handler-cloning}+# and corpus/x86_64-llvm-ubsan/{clang-ubsan-bounds-noise,+# clang-ubsan-handler-cloning} in Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup+build_pairubsan_noise.c-fsanitize=shift++# The premise: this compiler really did instrument, and left its metadata in an+# anonymous section. Without that the test is just test-basic again.+ubsan_sec="$(in_sectionsorig.o|+grep-oE'\.data\.\.L(ubsan_data|__unnamed_)[A-Za-z0-9_.]*'|head-1)"+[-n"$ubsan_sec"]||+probe_skip"compiler emitted no anonymous UBSAN data section"+assert_input_symboluntouched++run_diff++# The changed function is patched, and the untouched one is left alone despite+# carrying instrumentation of its own.+assert_patchedtouched+assert_not_patcheduntouched++# The handler the patched code calls has to come with it, or the clone calls+# nothing when its check fires.+out_symbols|grep-q'__ubsan_handle_'||+fail"no __ubsan_handle_* reference in the patched output"++pass"UBSAN metadata in an unchanged function does not drag it into the patch"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:33
For a dense enough switch Clang emits the targets as a table in
.rodata..Lswitch.table.<function> -- named after the function but not part
of it. The patched function indexes into that table, so a clone which does
not bring it along jumps through whatever the kernel's copy holds, which
after a patch that changed the switch is the wrong set of targets. An
indirect jump to a stale address reports nothing at build or load time.
The fixture asserts its own premise twice over, since both halves depend on
what this Clang chose to do: that a table was built rather than a chain of
comparisons, and that the added case actually changed it.
objtool has no switch-specific code -- the table is carried by the general
mechanism for data a cloned function references -- so this guards that
mechanism reaching an easily-mishandled shape rather than a particular
line, and the test says so. Making the table uncorrelated, the nearest
available sabotage, does not change the outcome.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../tests/generic/fixtures/switch_rodata.c | 31 +++++++++++
.../tests/generic/test-switch-rodata.sh | 53 +++++++++++++++++++
2 files changed, 84 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/switch_rodata.c
create mode 100755 tools/objtool/tests/generic/test-switch-rodata.sh
@@ -0,0 +1,53 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# A Clang switch jump table travels with the function it belongs to.+#+# For a dense enough switch Clang emits the targets as a table in+# .rodata..Lswitch.table.<function>, named after the function but not part of+# it. klp diff has to associate the two: the patched function indexes into+# that table, so a clone which does not bring it along jumps through whatever+# the kernel's copy holds -- which, when the patch changed the switch, is the+# wrong set of targets.+#+# That is an indirect jump to a stale address, not a missing symbol, so nothing+# reports it at build or load time.+#+# objtool has no switch-specific code: the table is carried by the general+# mechanism for data a cloned function references. So this is a regression+# test on that mechanism reaching a shape it is easy to get wrong, not a guard+# on a particular line -- making the table uncorrelated, the nearest sabotage,+# does not change the outcome.+#+# Covers the same ground as corpus/x86_64-llvm-switch-rodata/+# clang-switch-rodata-assoc in Joe Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++clang_only"only Clang emits switch jump tables in their own section"++setup+build_pairswitch_rodata.c++# The premise: this Clang really did build a table rather than a chain of+# comparisons, and the added case really did change it.+tbl=.rodata..Lswitch.table.status_to_string+has_input_sectionorig.o"$tbl"||+probe_skip"this clang built no jump table for the switch"+# readelf prefixes each line with "[nn]", which splits into one or two fields+# depending on the index, so strip it before counting columns.+tbl_size()+{+in_sections"$1"|sed's/^ *\[[ 0-9]*\] *//'|+awk-vs="$tbl"'$1 == s { print $5 }'+}+["$(tbl_sizeorig.o)"!="$(tbl_sizepatched.o)"]||+fail"fixture's added case did not change the jump table"++run_diff++assert_patchedstatus_to_string+assert_section"$tbl"+assert_reloc_sym.text.status_to_string"$tbl"++pass"Clang switch jump table carried with the function it belongs to"
From: Song Liu <song@kernel.org> Date: 2026-09-14 06:30:38
A file-local symbol which ThinLTO has to make visible is renamed
helper.llvm.<hash>. With two such helpers in one link, the original and
the patched object hold two each, all four spelled differently, and
demangling gives "helper" for every one of them -- so the name alone cannot
say which corresponds to which.
Three translation units, two with a static helper of the same name and a
third calling into both, which is what forces the promotion. Only one
helper changes: paired correctly that means exactly one is cloned, and
paired the wrong way round the other is, or both are. Had both bodies
changed, both would be cloned either way and the test would prove nothing
-- which is how the first version of this was written.
The outcome is asserted, not the machinery. With the clang tested here the
pairing survives disabling the .llvm.<hash> suffix map and stubbing out
llvm_suffix() entirely, so no single-line sabotage distinguishes it; the
tiered matcher this case was written for is not needed for this shape. The
test says so rather than implying otherwise.
Assisted-by: Claude:claude-opus-4
Based-on-test-by: Joe Lawrence [off-list ref]
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@kernel.org>
---
.../generic/fixtures/thinlto_ambiguity.c | 57 ++++++++++++++
.../tests/generic/test-thinlto-ambiguity.sh | 77 +++++++++++++++++++
2 files changed, 134 insertions(+)
create mode 100644 tools/objtool/tests/generic/fixtures/thinlto_ambiguity.c
create mode 100755 tools/objtool/tests/generic/test-thinlto-ambiguity.sh
@@ -0,0 +1,77 @@+#!/bin/bash+# SPDX-License-Identifier: GPL-2.0+#+# Two ThinLTO-promoted symbols sharing a demangled name must be paired up+# correctly.+#+# A file-local symbol which ThinLTO has to make visible is renamed+# helper.llvm.<hash>. With two such helpers in one link the original and the+# patched object hold two each, all four spelled differently, and demangling+# gives "helper" for all of them -- so the name is not enough to say which+# corresponds to which.+#+# Getting it wrong is silent and specific: the patch is built against the wrong+# body, so one call site gets the other helper's arithmetic. Nothing fails to+# build and nothing fails to load.+#+# test-thinlto-local covers the unambiguous case, one promoted symbol whose+# hash moved. This is the case where demangling alone is not an answer.+#+# The outcome is asserted, not the machinery: with the clang tested here the+# pairing succeeds even with the .llvm.<hash> suffix map disabled and with+# llvm_suffix() stubbed out, so no single-line sabotage distinguishes it. The+# tiered matcher this case was written for is not needed for this shape.+#+# Covers the same ground as corpus/x86_64-llvm-thinlto/+# thin-lto-demangled-ambiguity and thin-lto-demangled-global-match in Joe+# Lawrence's klp-build unit test corpus.++."$(dirname"$0")/../lib.sh"++setup+clang_only"ThinLTO requires clang"++find_thinlto_toolchain||+probe_skip"no matching clang/lld pair for a ThinLTO link; set THIN_LD to one"++build_thinlto()# $1 output object, $2 extra flags+{+localt+fortin""-DTU_B-DTU_C;do+$THIN_CC-flto=thin-O2-ffunction-sections-fdata-sections\+$2$t-c"$FIXTURES_DIR/thinlto_ambiguity.c"\+-o"$workdir/tu$t.o"2>/dev/null||return1+done+"$THIN_LD"-r"$workdir/tu.o""$workdir/tu-DTU_B.o""$workdir/tu-DTU_C.o"\+-o"$1"2>/dev/null||return1+}++build_thinlto"$workdir/orig.o"""||+probe_skip"ThinLTO build failed ($THIN_CC, $THIN_LD)"+build_thinlto"$workdir/patched.o"-DPATCHED||+probe_skip"ThinLTO build failed ($THIN_CC, $THIN_LD)"++# The premise: two promoted helpers per object, and exactly one of them kept+# its hash -- the one the patch did not touch. Without that there is nothing+# to disambiguate.+orig_syms="$(in_symbolsorig.o|grep-oE'helper\.llvm\.[0-9]+'|sort-u)"+new_syms="$(in_symbolspatched.o|grep-oE'helper\.llvm\.[0-9]+'|sort-u)"+["$(echo"$orig_syms"|wc-l)"=2]&&["$(echo"$new_syms"|wc-l)"=2]||+probe_skip"ThinLTO did not promote two distinct helpers here"++kept="$(comm-12<(echo"$orig_syms") <(echo "$new_syms"))"+moved="$(comm-13<(echo"$orig_syms") <(echo "$new_syms"))"+["$(echo"$kept"|wc-w)"=1]&&["$(echo"$moved"|wc-w)"=1]||+probe_skip"expected one helper to keep its hash and one to move"++run_diff++# Exactly one helper is cloned, and it is the one whose body changed. Cloning+# the other, or both, is what a wrong pairing looks like.+assert_not_patched"$kept"++n="$(out_sections|grep-cE'[[:space:]]\.text\.helper\.llvm\.[0-9]+[[:space:]]')"+["$n"=1]||+fail"expected 1 cloned helper, found $n"++pass"ThinLTO helpers sharing a demangled name paired up correctly"
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Incorrect shell logic nesting hides failure hints and masks leaked artifacts in KEEP=none mode
--
commit 3f45b4f5eca60d068a16adeda22b98b456f84450
Author: Joe Lawrence [off-list ref]
objtool: Keep failing test workdirs by default
Change the default test behavior to save failing test working directories
instead of deleting them. Add a KEEP Makefile variable to optionally retain
all or none of these directories.
+none)
+ if ! rmdir "$rundir"/*/ "$rundir" 2>/dev/null; then
+ if [ "$fail" != 0 ] || [ "$xpass" != 0 ]; then
[Severity: Medium]
Does nesting the failure hint inside the ! rmdir check in the KEEP=none
case block prevent the hint from being printed when tests successfully
clean up?
If rmdir succeeds, meaning tests cleaned up properly, this entire block is
skipped. Also, if a test leaks files (causing rmdir to fail) and tests
also fail, won't this logic print the hint but skip the leak warning due to
the elif statement? This would effectively mask the leaked artifacts when
tools/objtool/tests/run-tests.sh is run with KEEP=none.
+ echo "# keep=none: artifacts were removed" \
+ "(re-run with KEEP=failed or KEEP=all)"
+ elif [ -d "$rundir" ]; then
+ echo "# $rundir was not empty; a test did not clean up after itself"
+ fi
+ fi
+ ;;
+esac
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Tool existence checks for READELF, OBJCOPY, and LD will fail if these variables contain CLI arguments, incorrectly aborting the test suite.
--
commit 2ddfaa45c9b45195802f915e9ca42e1e7b224f4b
Author: Song Liu [off-list ref]
objtool/klp: Check the klp test environment once, before any test
This commit adds a preflight check to evaluate the objtool test environment
once before the test suite runs. It exports the environment answers and fails
the entire run if prerequisites are missing, rather than skipping tests
individually.
+ command -v "${CC%% *}" >/dev/null || bail "compiler not found: $CC"
+
+ for tool in "$READELF" "$OBJCOPY" "$LD"; do
+ command -v "$tool" >/dev/null || bail "$tool not found"
+ done
[Severity: Low]
Will this check fail if the toolchain variables contain CLI arguments?
Unlike the compiler check just above it which correctly uses parameter
expansion to strip arguments (`${CC%% *}`), passing the full string to
`command -v` when it contains spaces (e.g., if a developer or CI system
overrides `OBJCOPY="llvm-objcopy --target=arm64"`) will cause `command -v`
to treat the entire string as a single binary name and fail.
Could this incorrectly abort the test suite under valid configurations?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260914062514.926997-1-song@kernel.org?part=2
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The test uses `grep -q` which incorrectly matches section symbols in newer binutils, bypassing the check for the actual object symbol.
- [Medium] The test fixture uses a predictable, linear `static const int` array which the compiler can easily optimize away into arithmetic, causing a false test failure.
--
commit 5c5aee1d82edada34865ca44a6b863b5d7f437f3
Author: Puranjay Mohan [off-list ref]
objtool/klp: Add test for newly introduced data
Adding data differs from changing it: nothing in the running kernel refers
to a new variable, so it is safe and has to travel into the livepatch with
the function using it.
@@ -0,0 +1,18 @@+// SPDX-License-Identifier: GPL-2.0+/* Data introduced by the patch. */++staticconstchar__modinfo[]+__attribute__((section(".modinfo"),used,aligned(1)))="\0name=vmlinux";++#ifdef PATCHED+staticconstintklp_new_data[4]__attribute__((used))={1,2,3,4};+#endif++inttarget(intx)+{+#ifdef PATCHED+returnx+klp_new_data[x&3];
[Severity: Medium]
Can the compiler fold this array lookup into arithmetic? The predictable,
linear sequence of values here makes it a target for array-to-math
optimizations (e.g. GCC at -O2 might compile klp_new_data[x & 3] as
(x & 3) + 1).
If the compiler optimizes this away into scalar arithmetic, it would eliminate
the necessary relocation to klp_new_data in the target() function. Since
objtool klp diff relies on this relocation to pull in the variable,
eliminating it would cause the test to spuriously fail.
+run_diff
+
+assert_patched target
+out_symbols | grep -q 'klp_new_data' ||
+ fail "new data was not carried into the patch"
[Severity: Medium]
Will this test falsely pass if the actual object symbol is dropped but the
section is preserved?
This simple grep bypasses the symbol type check (OBJECT) and matches the
section symbol in newer binutils where readelf prints section names. If
grep -q incorrectly matches the section symbol (like .rodata.klp_new_data),
it would mask regressions in objtool's handling of the object symbol itself.
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The test assertion for the carried cold function incorrectly passes with `llvm-readelf` when the cold half is left behind.
--
commit a04e65cdf7036d7ab3a10a22fc0efb8a0c3873bd
Author: Puranjay Mohan [off-list ref]
objtool/klp: Add test for cold function halves
The compiler splits unlikely code into a separate foo.cold symbol. Both
halves are the same function and both belong in the livepatch. This adds
a test to ensure cold paths branching into unpatched code are handled.
+assert_patched target
+# Not a bare name match: had the cold half been left behind, the branch to
+# it would appear as an undefined .klp.sym.vmlinux.target.cold, whose name
+# contains the one being looked for. Require a defined symbol.
+out_symbols | awk '$7 != "UND" && $8 ~ /target\.cold/ { found = 1 }
+ END { exit !found }' ||
+ fail "cold half was not carried into the patch"
[Severity: Medium]
Will this awk script correctly detect missing cold halves when the test is
run with llvm-readelf?
When the cold half is left behind, klp diff converts unresolved references
into SHN_LIVEPATCH symbols. llvm-readelf formats these as:
OS[0xff20] .klp.sym.vmlinux.target.cold,0
Because there is no space between the section name and the symbol name in
this output format, $7 evaluates to "OS[0xff20]" (which is not "UND") and $8
matches the /target\.cold/ regex.
Does this condition evaluate to true and cause the test to falsely pass when
the cold half is actually left behind?
+
+pass "cold half carried into the patch with its parent"
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Forwarding CC='$(CC)' breaks LLVM cross-compilation because Clang target flags are omitted.
- [Medium] Forwarding READELF forces the use of llvm-readelf under LLVM=1, which explicitly violates test suite assertions.
--
commit be049d84a50dfed10ff53ea94f5daa93c75008b3
Author: Joe Lawrence [off-list ref]
Subject: objtool: Forward toolchain variables to the klp test runner
This commit forwards toolchain variables from the objtool Makefile to the
klp test runner script. This ensures that the tests use the intended compiler
and tools, especially when specific toolchains are selected via environment
variables like LLVM=1.
[Severity: Medium]
When cross-compiling with LLVM=1, does forwarding CC like this drop the Clang
target flags (such as --target=aarch64-linux-gnu) normally passed via
CLANG_CROSS_FLAGS?
The top-level build sets CC to clang and passes target flags separately.
Because only CC is forwarded here, the test script runs clang without any
target flags and builds test fixtures for the host architecture instead.
This mismatch is then caught in tools/objtool/tests/lib.sh during
klp_preflight(), which bails out when it detects that the objects target
a different architecture:
tools/objtool/tests/lib.sh:klp_preflight()
"$OBJTOOL" klp checksum "$tmp/probe.o" >/dev/null 2>&1 ||
{ rm -rf "$tmp"
bail "objtool rejects an object built by '$CC'; they target" \
"different architectures (set CROSS_COMPILE, or" \
"--target= for clang)"; }
[Severity: Medium]
Will forwarding READELF break the test suite parsing when LLVM=1 is used?
The script tools/objtool/tests/lib.sh explicitly documents that it requires
GNU readelf because llvm-readelf spaces columns differently, which breaks the
test assertions:
tools/objtool/tests/lib.sh:
# readelf reads any target, so it rarely needs overriding -- and it must stay
# GNU readelf, whose column layout the assertions parse; llvm-readelf spaces
# them differently.
Forwarding READELF forces llvm-readelf into the script environment under
LLVM=1, which appears to violate this documented test framework constraint.
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Literal `$'\n'` string is appended instead of a newline character during TAP output manipulation.
--
commit df7f112de2ce31cfbac30c00e5d997457d3c2710
Author: Song Liu [off-list ref]
objtool/klp: Classify klp test outcomes
This commit introduces better classification of test outcomes for objtool klp
tests, distinguishing between different types of skips (declared, probe,
undeclared) and TAP directives (xfail, xpass). It modifies the test runner to
explicitly handle these cases and improve result reporting.
@@ -23,31 +61,99 @@ cd "$(dirname "$0")" || exit 1
[ ... ]
+ case "$result" in
+ *"# SKIP (declared)"*) static_skip=$((static_skip + 1)) ;;
+ *"# SKIP (probe)"*) probe_skip=$((probe_skip + 1)) ;;
+ *"# SKIP"*)
+ # An undeclared skip: the test gave up for a reason it never
+ # said it might. That is a hole, not an expected outcome.
+ #
+ # Replace the line rather than adding one. Every test owes the
+ # plan exactly one result, and a consumer counting them is
+ # entitled to say so when the totals disagree.
+ rest="$rest${rest:+$'\n'}was: $result"
[Severity: Low]
Does this append a literal dollar sign and single quotes instead of a newline?
When a test fails via an undeclared skip and has already produced other output,
the test runner attempts to append a newline to the rest string. Due to Bash's
double-quoting rules, the $'\n' syntax is not expanded inside double quotes and
is instead appended as literal characters. This can garble the terminal output.
+ result="not ok - $(basename "$t" .sh): undeclared skip"
+ result="$result (use gcc_only/clang_only or require_input_*)"
+ fail=$((fail + 1)) ;;
+ "not ok"*"# TODO"*) xfail=$((xfail + 1)) ;;
+ "ok"*"# TODO"*) xpass=$((xpass + 1)) ;;
+ "not ok"*) fail=$((fail + 1)) ;;
+ "ok"*) pass=$((pass + 1)) ;;
+ *)
+ # No result line at all: the test died before reporting.
+ rest="$rest${rest:+$'\n'}exited $rc without a result line"
[Severity: Low]
Is it possible this will also append literal characters rather than a newline
when a test dies without outputting a valid TAP result line?
+ result="not ok - $(basename "$t" .sh): no TAP result"
+ fail=$((fail + 1)) ;;
+ esac
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Regex boundary matching using `grep -qw` fails to enforce exact symbol name matches because GNU grep treats characters like `.` and `,` as non-word boundaries.
--
commit cfe07e1e9cf1d7d13ada28e3a4fcdc25e8dd8672
Author: Song Liu [off-list ref]
objtool/klp: Grow the klp test harness vocabulary
This commit expands the objtool livepatch test harness vocabulary by adding
assertions for both input and output objects. It validates properties like
relocations, symbols, sections, and checksums produced by klp diff.
+count_input_symbols()
+{
+ in_symbols "$1" | awk -v n="$2" '$4 == "OBJECT" && $8 == n' | wc -l
+}
+
+# re_quote <string>
+#
+# A string as a literal basic regular expression. Nearly every name these
+# assertions match on contains a dot -- .text.target, .klp.rela.vmlinux -- and
+# an unescaped dot matches any character, so an assertion for one section can be
+# satisfied by a different one whose name merely lines up.
+re_quote() { printf '%s' "$1" | sed 's/[].[^$*\\/]/\\&/g'; }
+
+has_input_section() { in_sections "$1" | grep -q "[[:space:]]$(re_quote "$2")[[:space:]]"; }
+has_input_symbol() { in_symbols "$1" | grep -qw -- "$(re_quote "$2")"; }
[Severity: Medium]
Since GNU grep treats characters like dots as non-word boundaries (as accurately
noted in the comment for count_input_symbols() above), won't this allow false
positive matches?
If a test checks for the symbol "target", couldn't this falsely match a symbol
named ".text.target" or ".klp.tombstone.target"?
[ ... ]
+assert_reloc_sym()
+{
+ section_relocs "$1" | grep -qw -- "$(re_quote "$2")" ||
+ fail "expected a relocation to '$2' in '$1'"
+}
[Severity: Medium]
Will this assertion mistakenly match base sections like ".text.unlikely" if $2
is ".text", because grep -qw relies on word boundaries that do not stop at dots?
[Severity: Medium]
Could this negative assertion falsely fail if a similarly-named dotted symbol
is present? For example, failing to verify the absence of "foo" because
"foo.isra.0" is present.
[ ... ]
+assert_symbol()
+{
+ out_symbols | grep -qw -- "$(re_quote "$1")" ||
+ fail "expected symbol '$1' in output"
+}
[Severity: Medium]
Can this check pass falsely if only a tombstone or section symbol like
".klp.tombstone.foo" exists when verifying the presence of "foo"?
[Severity: Medium]
Similar to assert_no_reloc_sym(), will this incorrectly fail if a cloned
function like "foo.isra.0" exists when asserting the removal of "foo"?
[ ... ]
+assert_livepatch_sym()
+{
+ out_symbols | grep -E 'OS ?\[0xff20\]' | grep -qw -- "$(re_quote "$1")" ||
+ fail "expected '$1' to be an SHN_LIVEPATCH symbol"
+}
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] The global `orig_obj` and `patched_obj` variables are not reset by `build_pair()`, causing state leakage between sequential test segments.
- [Medium] `make_vmlinux_pair()` masks malformed test invocations as toolchain deficiencies, resulting in silent test skips.
- [Medium] `run_objtool_check` fails to invalidate the `.checksummed` cache flag after modifying object files in-place.
--
commit 19435afd8ba9b070d7a97986a8ecde2ed711708b
Author: Song Liu [off-list ref]
Subject: objtool/klp: Build klp test fixtures through the harness
This commit refactors the objtool livepatch testing harness to standardize
fixture compilation, abstracting build and linking routines. It extracts object
creation and checksum generation into helper functions. It also adds logic to
handle vmlinux pairs and properly sequence objtool checks.
[Severity: Medium]
Does this code leave orig_obj pointing to stale data if run after
make_vmlinux_pair()?
If a test sequences its segments so that make_vmlinux_pair() is called first
(which sets the global orig_obj=vmlinux.o), and then build_pair() is called,
build_pair() does not reset orig_obj to orig.o.
Would this cause a subsequent run_diff() to erroneously diff the stale
vmlinux.o instead of the newly built orig.o?
+# run_objtool_check <objtool arguments...>
+#
+# Run objtool's ordinary check pass over the pair, as the kernel build does.
+#
[ ... ]
+run_objtool_check()
+{
+ local obj
+
+ for obj in "$orig_obj" "$patched_obj"; do
+ "$OBJTOOL" "$@" "$workdir/$obj" ||
+ fail "objtool $* failed on $obj"
+ done
+}
[Severity: Medium]
Can this function leave a stale checksum cache in place?
Other functions that create or modify objects, like build_one() and
partial_link(), remove the $workdir/.checksummed marker.
Since run_objtool_check() executes objtool directly on the test objects, which
can append new sections like ORC metadata, should it also remove the
.checksummed file to prevent subsequent run_checksum() calls from skipping
generation and using stale checksum data?
[ ... ]
+make_vmlinux_pair()
+{
+ local orig=() patched=() seen= arg
+
+ for arg in "$@"; do
+ if [ "$arg" = -- ]; then seen=y; continue; fi
+ if [ -n "$seen" ]; then patched+=( "$arg" ); else orig+=( "$arg" ); fi
+ done
+
+ partial_link "$workdir/vmlinux.o" "${orig[@]}" ||
+ probe_skip "partial link unavailable"
[Severity: Medium]
Will this silently mask test definition bugs as toolchain deficiencies?
If a test author accidentally malforms the arguments by omitting the --
delimiter, the $orig or $patched arrays will be empty. Calling partial_link
with empty input arrays would correctly fail, but capturing that failure here
and executing probe_skip masks it.
Doesn't this contradict the commit's design principle that a fixture which will
not compile should be a failure rather than a skip?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260914062514.926997-1-song@kernel.org?part=5
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The test script claims to cover commit d8a2860b4a36 but fails to exercise its logic because it patches vmlinux instead of a module.
--
commit 0018f6f310cd6d22138d9d2b74d24fb1f9be4ab3
Author: Song Liu [off-list ref]
objtool/klp: Add test for EXPORT_SYMBOL_FOR_MODULES references
This commit adds a test script to verify klp relocations for symbols
exported with EXPORT_SYMBOL_FOR_MODULES(). The test aims to cover the
behavior of previous fixes related to klp relocation section naming.
[Severity: Medium]
Does this test actually exercise the module-patching case fixed in
d8a2860b4a36?
By calling build_pair without a -DMODNAME flag, the test fixture defaults to
patching vmlinux. Because the patched object is vmlinux, the
__klp_relocs.vmlinux section is generated regardless of the logic introduced
in d8a2860b4a36.
That commit forces the section to vmlinux when the target is a vmlinux
symbol, overriding the patched module's name. As a result of patching
vmlinux directly here, the test appears to pass even without the fix it
claims to cover.
Should this test pass a module name to build_pair to properly test the module
relocation behavior?
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The jump label test fixture hardcodes a 64-bit PC-relative relocation which causes assembler errors on 32-bit architectures.
--
commit 12aec6304341b6d166d36c18d507d8088ff90b91
Author: Puranjay Mohan [off-list ref]
objtool/klp: Add test for jump table key relocations
A cloned __jump_table entry must keep a relocation in its key slot.
This patch adds tests and fixtures to ensure the relocation stays
properly populated for both exported and unexported vmlinux keys
during a klp diff.
+int target(int x)
+{
+ int r = x;
+
+#if defined(NEW_KEY) && !defined(PATCHED)
+ /* The original has no static branch at all. */
+ return r + 1;
+#else
+ asm goto(
+ "1: nop\n\t"
+ ".pushsection __jump_table, \"aw\"\n\t"
+ ".balign 8\n\t"
+ "912:\n\t"
+ ".pushsection .discard.annotate_data, \"M\", @progbits, 8\n\t"
+ ".long 912b - ., 1\n\t"
+ ".popsection\n\t"
+ ".long 1b - ., %l[l_yes] - .\n\t"
+ ".quad %c0 - .\n\t"
[Severity: Low]
This inline assembly in target() explicitly requests a 64-bit PC-relative
relocation. On 32-bit x86 (which is supported by the test runner), the GNU
assembler cannot represent a 64-bit PC-relative relocation.
Will this cause the test fixture to fail to build when running objtool
tests in a 32-bit environment?
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The test script incorrectly assumes that if target() moves relative to the section start, its relative displacement to callee() must have changed. This allows the test to falsely pass if the compiler shifts both functions by the same amount.
--
commit 3328da26fa650b64515b357106fb0c92f158e5b7
Author: Song Liu [off-list ref]
objtool/klp: Add test for position-independent checksums
A function that only moves has not changed, and its checksum must not move
with it. This commit adds a test to verify position-independent checksums for
non-relocated jumps/calls.
+orig_off="$(in_symbols orig.o | awk '$8 == "target" { print $2 }')"
+new_off="$( in_symbols patched.o | awk '$8 == "target" { print $2 }')"
+[ -n "$orig_off" ] && [ -n "$new_off" ] ||
+ fail "target symbol not found in both objects"
+[ "$orig_off" != "$new_off" ] ||
+ probe_skip "compiler did not move target() between builds"
[Severity: Medium]
Does this check guarantee the relative displacement between target() and
callee() actually changed?
Since the test is built with -O2 (which enables -ftoplevel-reorder) and
-fno-function-sections, the compiler could group globals and statics,
emitting padding() before both target() and callee(). In this case, target()
and callee() would both shift by the exact size of padding().
The script checks if target() moved relative to the section start:
[ "$orig_off" != "$new_off" ]
This would evaluate to true since target() did move, allowing the test to
proceed. However, because the relative displacement between target() and
callee() is unchanged, the encoded call displacement bytes within target()
will be identical in both builds. The checksum will trivially match without
exercising objtool's displacement-masking logic.
+
+assert_checksum_matches target
+
+pass "checksum unchanged when the function only moves"
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Test script relies on non-portable `grep` features (`\b` and `-w` on non-word boundaries), causing false test failures on BSD/macOS environments.
--
commit 4a06859b140cc416baf636400fbe85d7c99ad01c
Author: Song Liu [off-list ref]
objtool/klp: Add test for the contents of the klp_funcs list
The patch list is what livepatch acts on, and asserting only that it exists
does not say it is right. The fixture changes two of three functions and
asserts on all three: the two by name, and the third by its absence.
+# The names the kernel matches on are real strings, not just relocations.
+out_strings .rodata.klp.str1.1 |
+ grep -q '\bfirst\b' ||
+ fail "no 'first' string in .rodata.klp.str1.1"
[Severity: Low]
Is it possible to use POSIX regex features instead of \b here? The \b
extension is a GNU/PCRE feature and is unsupported by BSD grep, which can
cause spurious test failures on non-GNU platforms such as macOS or FreeBSD.
+out_strings .rodata.klp.str1.1 |
+ grep -q '\bsecond\b' ||
+ fail "no 'second' string in .rodata.klp.str1.1"
[Severity: Low]
Does this code also need to avoid the \b extension to prevent false test
failures on environments using BSD grep?
+
+# The object list has to reach the function list, or nothing is walked.
+assert_reloc_sym .init.klp_objects .init.klp_funcs
[Severity: Low]
Will this assertion fail on BSD grep? This passes a section name starting
with '.' to a helper that uses grep -qw.
On BSD grep, the -w option requires a transition between word and non-word
characters, which always fails when the matched pattern begins with a
non-word character like a dot.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260914062514.926997-1-song@kernel.org?part=31
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Test fixture sanity checks for LOCAL symbol binding will produce false positives on newer binutils.
--
commit e2b339e9e0066c89f1967f8ca7015610df269518
Author: Song Liu [off-list ref]
objtool/klp: Add test for symbols whose linkage the patch changes
A patch can move a symbol between static and global without renaming it:
dropping "static" from a helper so something else can call it, or adding it
to one that is no longer shared. Correlation keys off more than the name,
so a symbol whose binding moved has to still pair with itself.
+setup
+build_pair local_to_global.c
+
+# Confirm the fixture really moved the bindings, in both directions.
+in_symbols orig.o | grep -qE 'LOCAL.*flipped_up' ||
+ fail "flipped_up is not local in the original"
[Severity: Medium]
Since this test fixture is compiled with -ffunction-sections and
-fdata-sections (as enforced by FIXTURE_CFLAGS), dedicated sections like
.text.flipped_up are created.
On newer binutils, readelf -s includes the section name for section symbols,
which always have LOCAL binding.
Does grep -qE 'LOCAL.*flipped_up' inadvertently match the section symbol
instead of verifying the actual function symbol's binding? Could this result
in a false positive that masks potential test fixture breakages if the symbol
is actually GLOBAL?
+in_symbols patched.o | grep -qE 'GLOBAL.*flipped_up' ||
+ fail "flipped_up is not global in the patched object"
+in_symbols orig.o | grep -qE 'GLOBAL.*flipped_down' ||
+ fail "flipped_down is not global in the original"
+in_symbols patched.o | grep -qE 'LOCAL.*flipped_down' ||
+ fail "flipped_down is not local in the patched object"
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The generic check for duplicate addresses in the test script uses symbol names instead of actual addresses (symbol value + addend).
--
commit 8da269007d7ca99a4b81784ec070f665fec2ad95
Author: Song Liu [off-list ref]
objtool/klp: Add test for symbols with no checksum entry of their own
This commit adds a test script and a C fixture to verify that objtool
correctly skips checksum entries for zero-length, alias, and cold-part
symbols. It ensures that the invariant of one entry per address in
.discard.sym_checksum is maintained.
+
+# entries_for <object>
+#
+# The symbol names .discard.sym_checksum has an entry for, one per line.
+entries_for()
+{
+ in_relocs "$1" | awk '/rela\.discard\.sym_checksum/,/^$/ {
+ if ($1 ~ /^[0-9a-f]{8,}/) print $5
+ }'
+}
+
+entries="$(entries_for orig.o)"
+
[ ... ]
+
+# One entry per address, which is what the skipping is for.
+dupes="$(entries_for orig.o | sort | uniq -d)"
+[ -z "$dupes" ] ||
+ fail "two checksum entries for one address: $dupes"
[Severity: Low]
Does this generic uniqueness check correctly identify duplicate addresses?
Since entries_for() extracts only the symbol name (column 5 of the readelf
output), this check doesn't seem to evaluate the actual resolved address
(symbol value + addend).
Wouldn't it fail to detect two different symbols mapping to the same address,
or potentially fail spuriously if multiple static functions share a section
symbol with different addends?
Since this is a test script with a tightly controlled C fixture (without
static functions) and the alias case is explicitly tested beforehand, it
doesn't break this specific test case. However, should the check be updated
to use actual resolved addresses instead of just symbol names?
+
+pass "zero-length symbols and aliases get no checksum entry of their own"
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Objtool's klp_find_sympos() for modules counts symbols in init and non-alloc sections, causing livepatch resolution to fail if applied after the target module is initialized.
--
commit 22d70a31297d85e4712560a5e8b6e9de9ffd35be
Author: Song Liu [off-list ref]
Subject: objtool/klp: Add test for sympos in module objects
This commit introduces test coverage for the klp-sympos logic in objtool.
It adds a new fixture and bash script to verify that duplicate symbols are
correctly numbered by sympos in module objects.
[Severity: High]
This is a pre-existing issue, but since this test checks the output of
klp_find_sympos(), I wanted to ask about its symbol counting logic.
Currently, klp_find_sympos() counts all identically-named symbols in the
module's ELF symtab:
tools/objtool/klp-sympos.c:klp_find_sympos() {
...
for_each_sym(elf, s) {
if (!strcmp(s->name, sym->name)) {
nr_matches++;
if (s == sym)
sympos = nr_matches;
else
has_dup = true;
}
}
...
}
Does blindly counting symbols in .init or non-alloc sections cause
livepatch resolution to fail?
When a livepatch is loaded after the target module is initialized, the
kernel's module_kallsyms_on_each_symbol() uses mod->core_kallsyms, which
strips init and non-alloc symbols via is_core_symbol().
If a livepatch targets a module symbol that shares its name with another
symbol located in an .init or non-alloc section, it seems klp_find_sympos()
assigns an inflated sympos (e.g., 2). However, the kernel would only find 1
matching symbol, see a mismatch with sympos=2, and reject the patch with
-EINVAL.
Should klp_find_sympos() be skipping init and non-alloc sections?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260914062514.926997-1-song@kernel.org?part=37
From: Song Liu <song@kernel.org> Date: 2026-09-14 18:25:23
On Sun, Sep 13, 2026 at 11:37 PM [off-list ref] wrote:
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Incorrect shell logic nesting hides failure hints and masks leaked artifacts in KEEP=none mode
--
commit 3f45b4f5eca60d068a16adeda22b98b456f84450
Author: Joe Lawrence [off-list ref]
objtool: Keep failing test workdirs by default
Change the default test behavior to save failing test working directories
instead of deleting them. Add a KEEP Makefile variable to optionally retain
all or none of these directories.
+none)
+ if ! rmdir "$rundir"/*/ "$rundir" 2>/dev/null; then
+ if [ "$fail" != 0 ] || [ "$xpass" != 0 ]; then
[Severity: Medium]
Does nesting the failure hint inside the ! rmdir check in the KEEP=none
case block prevent the hint from being printed when tests successfully
clean up?
If rmdir succeeds, meaning tests cleaned up properly, this entire block is
skipped. Also, if a test leaks files (causing rmdir to fail) and tests
also fail, won't this logic print the hint but skip the leak warning due to
the elif statement? This would effectively mask the leaked artifacts when
tools/objtool/tests/run-tests.sh is run with KEEP=none.
I think this is OK. We still print out "not ok" and "fail: <none zero>".
Thanks,
Song