[tip: objtool/core] objtool: Add test harness for the klp subcommands
From: tip-bot2 for Puranjay Mohan
Date: Fri Sep 18 2026 - 06:36:36 EST
The following commit has been merged into the objtool/core branch of tip:
Commit-ID: 8aab0eb12944c076bcbb263fbd865ab0fdb6408e
Gitweb: https://git.kernel.org/tip/8aab0eb12944c076bcbb263fbd865ab0fdb6408e
Author: Puranjay Mohan <puranjay@xxxxxxxxxx>
AuthorDate: Wed, 16 Sep 2026 11:42:54 -07:00
Committer: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
CommitterDate: Wed, 16 Sep 2026 17:13:26 -07:00
objtool: Add test harness for the klp subcommands
"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@xxxxxxxxxx>
Co-developed-by: Joe Lawrence <joe.lawrence@xxxxxxxxxx>
Signed-off-by: Joe Lawrence <joe.lawrence@xxxxxxxxxx>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Song Liu <song@xxxxxxxxxx>
Link: https://patch.msgid.link/20260916184351.2720310-2-song@xxxxxxxxxx
Signed-off-by: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
---
tools/objtool/Makefile | 5 +-
tools/objtool/tests/fixtures/basic.c | 20 +++-
tools/objtool/tests/lib.sh | 179 ++++++++++++++++++++++++++-
tools/objtool/tests/run-tests.sh | 20 +++-
tools/objtool/tests/test-basic.sh | 17 ++-
5 files changed, 240 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
diff --git a/tools/objtool/Makefile b/tools/objtool/Makefile
index a4484fd..f4ec9f8 100644
--- a/tools/objtool/Makefile
+++ b/tools/objtool/Makefile
@@ -151,6 +151,9 @@ clean: $(LIBSUBCMD)-clean
mrproper: clean
$(call QUIET_CLEAN, objtool) $(RM) $(OBJTOOL)
+tests: $(OBJTOOL)
+ $(Q)OBJTOOL=$(abspath $(OBJTOOL)) $(srctree)/tools/objtool/tests/run-tests.sh
+
FORCE:
-.PHONY: clean mrproper FORCE
+.PHONY: clean mrproper tests FORCE
diff --git a/tools/objtool/tests/fixtures/basic.c b/tools/objtool/tests/fixtures/basic.c
new file mode 100644
index 0000000..811529e
--- /dev/null
+++ b/tools/objtool/tests/fixtures/basic.c
@@ -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 */
+static const char __modinfo[]
+ __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux";
+
+int untouched(int x)
+{
+ return x * 3;
+}
+
+int changed(int x)
+{
+#ifdef PATCHED
+ return x + 2;
+#else
+ return x + 1;
+#endif
+}
diff --git a/tools/objtool/tests/lib.sh b/tools/objtool/tests/lib.sh
new file mode 100644
index 0000000..7b29db7
--- /dev/null
+++ b/tools/objtool/tests/lib.sh
@@ -0,0 +1,179 @@
+# 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}"; exit 0; }
+fail() { echo "not ok - $test_name: $1" >&2; exit 1; }
+skip() { echo "ok - $test_name # SKIP $1"; exit 0; }
+
+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"
+
+ # run_diff() runs objtool from inside the test's working directory, so
+ # a relative path would resolve against that instead.
+ OBJTOOL="$(realpath "$OBJTOOL")"
+
+ "$OBJTOOL" klp 2>&1 | grep -q checksum ||
+ skip "objtool built without klp support (needs libxxhash)"
+ command -v "${CC%% *}" >/dev/null || skip "no compiler ($CC)"
+
+ workdir="$(mktemp -d)" || fail "mktemp failed"
+ trap cleanup EXIT
+
+ 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"
+ for sym in "$@"; do
+ printf '0x00000000\t%s\tvmlinux\tEXPORT_SYMBOL\t\n' \
+ "$sym" >> "$workdir/Module.symvers"
+ done
+}
+
+# build_pair <fixture.c> [cflags...]
+build_pair()
+{
+ local fixture="$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()
+{
+ local expect="${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" klp checksum "$workdir/orig.o" ||
+ fail "klp checksum orig.o failed"
+ "$OBJTOOL" klp checksum "$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" klp diff orig.o patched.o out.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()
+{
+ local out="$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()
+{
+ local cc ld ver
+
+ for cc in "${THIN_CC:-}" "$CC" clang; do
+ [ -n "$cc" ] || continue
+ command -v "${cc%% *}" >/dev/null 2>&1 || continue
+
+ ver=$($cc -dumpversion 2>/dev/null | cut -d. -f1)
+
+ for ld in "${THIN_LD:-}" "ld.lld-$ver" ld.lld; do
+ [ -n "$ld" ] || continue
+ command -v "$ld" >/dev/null 2>&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"
+ return 0
+ done
+ done
+
+ return 1
+}
+
+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"
+ return 0
+}
diff --git a/tools/objtool/tests/run-tests.sh b/tools/objtool/tests/run-tests.sh
new file mode 100755
index 0000000..ab1dea5
--- /dev/null
+++ b/tools/objtool/tests/run-tests.sh
@@ -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")" || exit 1
+
+tests=( test-*.sh )
+[ "${tests[0]}" = "test-*.sh" ] && { echo "1..0 # SKIP no tests found"; exit 0; }
+
+echo "1..${#tests[@]}"
+
+rc=0
+for t in "${tests[@]}"; do
+ ./"$t" || rc=1
+done
+
+exit $rc
diff --git a/tools/objtool/tests/test-basic.sh b/tools/objtool/tests/test-basic.sh
new file mode 100755
index 0000000..6b76996
--- /dev/null
+++ b/tools/objtool/tests/test-basic.sh
@@ -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_pair basic.c
+run_diff
+
+assert_patched changed
+assert_not_patched untouched
+assert_section ".init.klp_funcs"
+assert_section ".init.klp_objects"
+
+pass "changed function cloned, unchanged function left alone"