diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-20 12:55:04 -1000 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-20 12:55:04 -1000 |
| commit | 5b6dca02b3e2254d2fd5706d76d7ab4610a67296 (patch) | |
| tree | 443bf1de39c37775248ae83c2f287c09fe15f749 /tools/objtool/tests/generic | |
| parent | 776bb6df3f16ab68c40a97dbb8af26af4185d7c0 (diff) | |
| parent | 945537271df1e3680be9b32af7041ac20f1184fb (diff) | |
Merge tag 'objtool-core-2026-10-18' of git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip
Pull objtool updates from Ingo Molnar:
- Add unit test suite for objtool klp functionality and
known failure modes (by Song Liu and Puranjay Mohan,
with fixes by Joe Lawrence)
* tag 'objtool-core-2026-10-18' of git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip: (58 commits)
objtool/klp: Add test for ThinLTO symbols sharing a demangled name
objtool/klp: Add test for Clang switch jump tables
objtool/klp: Add test for UBSAN metadata in an unchanged function
objtool/klp: Add test for absolute and __ADDRESSABLE symbols
objtool/klp: Add test for a patch which strips a data annotation
objtool/klp: Add test for the alignment of cloned data sections
objtool/klp: Add test for alternative replacement code in checksums
objtool/klp: Add test for instruction operand checksums
objtool/klp: Add test for a static call introduced by the patch
objtool/klp: Add test for tracepoint and pr_debug static branch keys
objtool/klp: Add test for a static branch introduced by the patch
objtool/klp: Add test for symbols with no checksum entry of their own
objtool/klp: Add test for data object checksums
objtool/klp: Test text annotations on alternative replacements
objtool/klp: Test a hand-built livepatch module's static call keys
objtool/klp: Test rejection of a file-local static branch key
objtool/klp: Add test for symbols whose linkage the patch changes
objtool/klp: Add test for kCFI prefix symbols and traps
objtool/klp: Add test for __bug_table, __ex_table and __mcount_loc extraction
objtool/klp: Add test for static locals which must not be correlated
...
Diffstat (limited to 'tools/objtool/tests/generic')
77 files changed, 3000 insertions, 0 deletions
diff --git a/tools/objtool/tests/generic/fixtures/abs_and_addressable.c b/tools/objtool/tests/generic/fixtures/abs_and_addressable.c new file mode 100644 index 000000000000..6392ff99af42 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/abs_and_addressable.c @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Two constructs which appear all over the kernel and must not upset klp + * checksum or klp diff. + * + * An absolute symbol (SHN_ABS) has no section, so anything walking sym->sec + * without checking dereferences NULL. The kernel makes them with linker + * scripts and with .set in asm; VDSO and the fixed-address per-cpu bases are + * the usual sources. + * + * __ADDRESSABLE() emits a pointer into .discard.addressable purely to keep a + * symbol referenced. It is discarded at link time and means nothing to a + * livepatch, but the pointer is a relocation like any other and has to survive + * being looked at. + * + * Neither is the subject of the patch; the point is that their presence does + * not disturb the function that is. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +/* SHN_ABS, referenced from code. */ +extern char abs_sym[]; +__asm__(".globl abs_sym\n" + ".set abs_sym, 0x1234\n"); + +int helper(int x); +int helper(int x) { return x + 1; } + +/* The shape of __ADDRESSABLE(helper). */ +__asm__(".pushsection .discard.addressable, \"aw\"\n" + ".balign 8\n" + ".quad helper\n" + ".popsection\n"); + +int target(int x) +{ +#ifdef PATCHED + return helper(x) + (int)(long)abs_sym + 1; +#else + return helper(x) + (int)(long)abs_sym; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/basic.c b/tools/objtool/tests/generic/fixtures/basic.c new file mode 100644 index 000000000000..811529e7bfb1 --- /dev/null +++ b/tools/objtool/tests/generic/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/generic/fixtures/changed_data.c b/tools/objtool/tests/generic/fixtures/changed_data.c new file mode 100644 index 000000000000..b52461835444 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/changed_data.c @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Data whose value differs between the two builds. */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +#ifdef PATCHED +int klp_test_data = 2; +#else +int klp_test_data = 1; +#endif + +int target(int x) +{ + return x + klp_test_data; +} diff --git a/tools/objtool/tests/generic/fixtures/checksum_data.c b/tools/objtool/tests/generic/fixtures/checksum_data.c new file mode 100644 index 000000000000..6310af5c02d3 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/checksum_data.c @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Data objects whose checksums must move for reasons the raw bytes do not + * show. + * + * checksum_update_object() hashes a data symbol's length and its bytes, and + * then walks its relocations: a reference into a string section contributes + * the string's *contents*, and any other reference contributes the target + * symbol's name and the adjusted addend. So three changes that leave the + * object's own bytes identical still have to change its checksum: + * + * Each variant is selected by a -D on the patched build only, so the original + * is always the baseline: + * + * WHICH_FUNC the function pointer points somewhere else + * WHICH_STR the string pointer points at a different literal + * STR_CONTENT the string it points at is edited in place + * WHICH_SLOT the same array, at a different index: addend only + * WHICH_PRIV likewise, but a static, reached through its section symbol + * + * The last is the interesting one. Nothing in the pointer changes -- same + * section, same offset -- so a checksum that hashed only the relocation and + * not what it referred to would call the object unchanged, and the patched + * kernel would keep the old string. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int callee_a(int x); +int callee_b(int x); +int callee_a(int x) { return x + 1; } +int callee_b(int x) { return x + 2; } + +/* + * String *literals*, not named arrays. The contents-hashing path keys on + * SHF_STRINGS, which the compiler sets on the mergeable .rodata.str1.1 a + * literal lands in and not on a named char[] given a section of its own. A + * fixture using the latter exercises the ordinary name-and-addend path and + * reports nothing when the text changes. + */ +#if defined(PATCHED) && defined(STR_CONTENT) +#define MESSAGE "edited" +#else +#define MESSAGE "original" +#endif + +/* A plain data object: only its own bytes decide the checksum. */ +#if defined(PATCHED) && defined(PLAIN_VALUE) +int plain = 43; +#else +int plain = 42; +#endif + +/* + * A .bss object, where length is the only thing there is to hash: the section + * has no data, so the bytes are skipped and only sym->len distinguishes this + * from an object of another size. An initialised array would not isolate it + * -- growing one changes the hashed bytes as well. + */ +#if defined(PATCHED) && defined(LONGER) +char sized[4]; +#else +char sized[2]; +#endif + +/* + * A reference into the middle of an array: same target symbol, different + * addend. Nothing else in the object changes, so this is the only way to see + * whether the addend is hashed at all. + */ +int slots[4]; + +/* + * A file-local array. A reference to a static lands on its section symbol + * plus an offset, so the hash has to resolve that back to the underlying + * object before it has a name to hash at all -- a different code path from the + * global above, and one that silently contributes nothing when it fails. + */ +static int priv_slots[4]; + +struct desc { + int (*fn)(int arg); + const char *str; + int *slot; + int *priv; +}; + +const struct desc descriptor = { +#if defined(PATCHED) && defined(WHICH_FUNC) + .fn = callee_b, +#else + .fn = callee_a, +#endif +#if defined(PATCHED) && defined(WHICH_STR) + .str = "a different literal", +#else + .str = MESSAGE, +#endif +#if defined(PATCHED) && defined(WHICH_SLOT) + .slot = &slots[2], +#else + .slot = &slots[1], +#endif +#if defined(PATCHED) && defined(WHICH_PRIV) + .priv = &priv_slots[3], +#else + .priv = &priv_slots[1], +#endif +}; + +int target(int x) +{ + return descriptor.fn(x) + plain + sized[0] + (int)descriptor.str[0] + + *descriptor.slot + *descriptor.priv; +} diff --git a/tools/objtool/tests/generic/fixtures/checksum_insn.c b/tools/objtool/tests/generic/fixtures/checksum_insn.c new file mode 100644 index 000000000000..10f70a74a976 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/checksum_insn.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Instruction operands whose change must move a function's checksum even + * though the instruction bytes themselves do not. + * + * checksum_update_insn() hashes the raw bytes and then, when the instruction + * carries a relocation, what that relocation refers to: a string section + * contributes the string's contents, anything else the target symbol's name + * and the adjusted addend. A reference to a static arrives as a section + * symbol and has to be resolved back to the object first. + * + * The bytes are identical in every case below -- a rel32 operand is zero in + * the object and supplied by the relocation -- so a checksum that stopped at + * the bytes would call all of these unchanged. + * + * Each variant applies to the patched build only: + * + * WHICH_CALL calls a different function + * STR_CONTENT passes a literal whose text was edited + * WHICH_SLOT reads a different index of a global array: addend only + * WHICH_PRIV the same, for a static, reached through its section symbol + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int callee_a(int x); +int callee_b(int x); +int sink(const char *s); + +int slots[4]; + +/* + * A file-local array, plus a writer the compiler cannot see through. Without + * one it can prove the array is never written, folds every read to zero, and + * emits no relocation at all -- so the reference this is here to exercise does + * not exist. + */ +static int priv_slots[4]; + +void set_priv(int i, int v); +void set_priv(int i, int v) +{ + priv_slots[i] = v; +} + +#if defined(PATCHED) && defined(STR_CONTENT) +#define MESSAGE "edited" +#else +#define MESSAGE "original" +#endif + +int target(int x) +{ + int r; + +#if defined(PATCHED) && defined(WHICH_CALL) + r = callee_b(x); +#else + r = callee_a(x); +#endif + + r += sink(MESSAGE); + +#if defined(PATCHED) && defined(WHICH_SLOT) + r += slots[2]; +#else + r += slots[1]; +#endif + +#if defined(PATCHED) && defined(WHICH_PRIV) + r += priv_slots[3]; +#else + r += priv_slots[1]; +#endif + + return r; +} diff --git a/tools/objtool/tests/generic/fixtures/checksum_position.c b/tools/objtool/tests/generic/fixtures/checksum_position.c new file mode 100644 index 000000000000..4320bc220592 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/checksum_position.c @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A function whose position in the section changes between the two builds, + * without the function itself changing. + * + * target() calls callee() twice, and a call within the same section needs no + * relocation: the displacement is in the instruction. It is that displacement + * which moves, and hashing those bytes makes the checksum move with it. Both + * must therefore share a section, which is why the test passes + * -fno-function-sections. + * + * What has to change is the distance between the two, and PATCHED changes it + * by aligning them rather than by inserting a function between them. Where a + * compiler puts an added function is its own business: gcc emits these in + * source order, so a function written between callee() and target() separates + * them, but clang emits target() immediately before callee() whatever the + * source says, and an added function lands ahead of both. That moves target() + * without moving it relative to callee(), the displacement comes out identical + * in both builds, and the test passes without having asked anything. + * + * Alignment moves the functions apart on both, and moves neither function's + * own instructions -- which is exactly the distinction under test. + */ + +#ifdef PATCHED +#define MOVED __attribute__((aligned(64))) +#else +#define MOVED +#endif + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +__attribute__((noinline)) MOVED static int callee(int x) +{ + return x * 5 + 1; +} + +__attribute__((noinline)) MOVED int target(int x) +{ + return callee(x) + callee(x + 1); +} diff --git a/tools/objtool/tests/generic/fixtures/checksum_skip.c b/tools/objtool/tests/generic/fixtures/checksum_skip.c new file mode 100644 index 000000000000..973bdc10295d --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/checksum_skip.c @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Symbols calculate_checksums() must not give an entry of their own. + * + * Three kinds are skipped, for two different reasons: + * + * zero-length there is nothing to hash, and an entry keyed on the + * symbol's address would collide with whatever really lives + * there. + * alias a second name for an address already checksummed. + * cold part hashed as part of its parent, which func_for_each_insn() + * walks into, so a separate entry would double-count it. + * + * An entry per address is the invariant: .discard.sym_checksum is looked up by + * the address a relocation points at, so two entries for one address make the + * lookup ambiguous and one of the two checksums unreachable. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +/* Zero-length: an object symbol of size 0, in a section of its own. */ +extern char empty_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"); + +int real_function(int x); +int real_function(int x) +{ +#ifdef PATCHED + return x + 2; +#else + return x + 1; +#endif +} + +/* Alias: a second name for real_function's address. */ +int alias_function(int x) __attribute__((alias("real_function"))); + +int target(int x) +{ + return real_function(x) + alias_function(x) + (int)(long)empty_marker; +} diff --git a/tools/objtool/tests/generic/fixtures/cold_function.c b/tools/objtool/tests/generic/fixtures/cold_function.c new file mode 100644 index 000000000000..f6d410983ce2 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/cold_function.c @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Function the compiler may split into a hot part and a foo.cold part. */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +static void __attribute__((cold, noinline)) slow_path(int x) +{ + __asm__ volatile("" :: "r"(x)); +} + +int target(int x) +{ + if (__builtin_expect(x < 0, 0)) + slow_path(x); +#ifdef PATCHED + return x + 2; +#else + return x + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/cross_module.c b/tools/objtool/tests/generic/fixtures/cross_module.c new file mode 100644 index 000000000000..c170bde0666f --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/cross_module.c @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A function which calls out to another object. MODNAME selects which object + * this one is, so a test can make the caller a module and the callee's owner + * something else. + */ + +#ifndef MODNAME +#define MODNAME "vmlinux" +#endif + +/* klp diff takes the object's module name from .modinfo */ +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=" MODNAME; + +extern int other_mod_func(int x); + +int target(int x) +{ +#ifdef PATCHED + return other_mod_func(x) + 2; +#else + return other_mod_func(x) + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/data_alignment.c b/tools/objtool/tests/generic/fixtures/data_alignment.c new file mode 100644 index 000000000000..900253dfb2cb --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/data_alignment.c @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Data with an alignment stricter than its size. + * + * A cloned data section has to keep its sh_addralign. The kernel has plenty + * of data whose alignment is a correctness property rather than an + * optimisation -- per-CPU variables, anything touched by an aligned SSE move, + * cacheline-aligned locks -- and a clone that lands under-aligned faults or + * silently shares a cacheline it was written to avoid. + * + * The object is new in the patched build, so klp diff has to clone it rather + * than reference the kernel's copy. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +#ifdef PATCHED +int aligned_data[2] __attribute__((aligned(64))) = { 1, 2 }; +#endif + +int target(int x) +{ +#ifdef PATCHED + return x + aligned_data[0]; +#else + return x; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/function_removal.c b/tools/objtool/tests/generic/fixtures/function_removal.c new file mode 100644 index 000000000000..d65ff604c2c5 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/function_removal.c @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A function the patch deletes, along with its only caller's use of it. The + * original has a symbol which the patched object simply does not, so there is + * nothing to correlate it against. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +#ifndef PATCHED +int going_away(int x) +{ + return x + 7; +} +#endif + +int caller(int x) +{ +#ifdef PATCHED + return x + 1; +#else + return going_away(x); +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/init_reference.c b/tools/objtool/tests/generic/fixtures/init_reference.c new file mode 100644 index 000000000000..9e59bdf708d3 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/init_reference.c @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Patched function referencing data in an .init section. */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +/* volatile so the read cannot be folded into a constant, leaving no reference */ +static volatile int init_only __attribute__((section(".init.data"), used)) = 5; + +int target(int x) +{ +#ifdef PATCHED + return x + init_only + 1; +#else + return x + init_only; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/jump_label.c b/tools/objtool/tests/generic/fixtures/jump_label.c new file mode 100644 index 000000000000..ecd3c06912af --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/jump_label.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Static branch in a patched function. The jump table entry is written out by + * hand, mirroring JUMP_TABLE_ENTRY(), so the fixture builds without kernel + * headers. The key is an STT_OBJECT; anything else is ignored by + * validate_special_section_klp_reloc(). + * + * MODNAME selects whether the key is taken to belong to vmlinux or a module. + * + * NEW_KEY puts the whole static branch behind PATCHED, so the patch introduces + * one where the original had none -- a different question from patching code + * that already has a key, because the __jump_table entry itself is new. + * + * KEY_NAME renames the key. Two names are special to + * validate_special_section_klp_reloc(): a __tracepoint_* key and the + * __UNIQUE_ID_ddebug_* one pr_debug() generates are both unsupported in a + * module, but are disabled with a warning rather than rejected, because the + * kernel is full of them and refusing outright would make ordinary functions + * unpatchable. + * + * STATIC_KEY makes the key file-local. That changes the shape of the + * relocation rather than the meaning of the code: a reference to a static lands + * on the section symbol plus an addend, so the key has to be resolved from the + * section before it can be recognised as a key at all. + */ + +#ifndef MODNAME +#define MODNAME "vmlinux" +#endif + +#ifndef KEY_NAME +#define KEY_NAME klp_test_key +#endif + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=" MODNAME; + +#ifdef STATIC_KEY +static long KEY_NAME; +#else +long KEY_NAME; +#endif + +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" + ".popsection\n\t" + : : "i" (&KEY_NAME) : : l_yes); + + r += 1; + goto out; +l_yes: + r += 2; +out: +#endif +#ifdef PATCHED + return r + 100; +#else + return r; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/klp_funcs.c b/tools/objtool/tests/generic/fixtures/klp_funcs.c new file mode 100644 index 000000000000..3f0d3e206cef --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/klp_funcs.c @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Two changed functions and one untouched, so the patch's function list has a + * length worth checking and something that must not appear in it. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int first(int x) +{ +#ifdef PATCHED + return x + 11; +#else + return x + 1; +#endif +} + +int second(int x) +{ +#ifdef PATCHED + return x + 22; +#else + return x + 2; +#endif +} + +int third(int x) +{ + return x + 3; +} diff --git a/tools/objtool/tests/generic/fixtures/local_to_global.c b/tools/objtool/tests/generic/fixtures/local_to_global.c new file mode 100644 index 000000000000..3c9eb9200ce5 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/local_to_global.c @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A function which the patch changes from static to non-static, and a variable + * that goes the other way. The names are unchanged; only the binding moves. + */ + +static const char __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)) int flipped_up(int x) /* was static */ +#else +__attribute__((noinline)) static int flipped_up(int x) +#endif +{ + return x + 1; +} + +#ifdef PATCHED +static volatile int flipped_down = 5; /* was global */ +#else +volatile int flipped_down = 5; +#endif + +int caller(int x) +{ + flipped_down += x; +#ifdef PATCHED + return flipped_up(x) + flipped_down + 2; +#else + return flipped_up(x) + flipped_down + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/new_data.c b/tools/objtool/tests/generic/fixtures/new_data.c new file mode 100644 index 000000000000..ac364622f12a --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/new_data.c @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Data introduced by the patch. */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +#ifdef PATCHED +/* + * Not an arithmetic progression: { 1, 2, 3, 4 } indexed by x & 3 is something + * a compiler can compute instead of load, and then target() has no reference + * to the array and there is nothing for klp diff to carry. + */ +static const int klp_new_data[4] __attribute__((used)) = { 7, 3, 11, 5 }; +#endif + +int target(int x) +{ +#ifdef PATCHED + return x + klp_new_data[x & 3]; +#else + return x; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/new_export_ref.c b/tools/objtool/tests/generic/fixtures/new_export_ref.c new file mode 100644 index 000000000000..73210aacb4ae --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/new_export_ref.c @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A reference which only exists in the patched build. The symbol has no twin + * in the original object, so what klp diff may do with it depends entirely on + * whether Module.symvers says it is exported, and by what. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +extern int newly_referenced(int x); + +/* + * A reference both builds have. When Module.symvers says a module exports + * this one, the original already depends on that module, which is what makes + * a new reference to it safe -- the loader will not let the patched module + * load without it. EXISTING_DEP leaves it out, for the case where there is + * no such dependency to inherit. + */ +extern int existing_dep(int x); + +int target(int x) +{ +#ifdef EXISTING_DEP + int base = existing_dep(x); +#else + int base = x; +#endif + +#ifdef PATCHED + return newly_referenced(base); +#else + return base + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/new_function.c b/tools/objtool/tests/generic/fixtures/new_function.c new file mode 100644 index 000000000000..e7886eaaff3e --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/new_function.c @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Function introduced by the patch. noinline keeps it from being folded. */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +#ifdef PATCHED +static __attribute__((noinline)) int klp_new_helper(int x) +{ + return x * 7; +} +#endif + +int target(int x) +{ +#ifdef PATCHED + return klp_new_helper(x); +#else + return x; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/no_modinfo.c b/tools/objtool/tests/generic/fixtures/no_modinfo.c new file mode 100644 index 000000000000..e46374702b1a --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/no_modinfo.c @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Deliberately has no .modinfo section. */ + +int target(int x) +{ +#ifdef PATCHED + return x + 2; +#else + return x + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/special_section.c b/tools/objtool/tests/generic/fixtures/special_section.c new file mode 100644 index 000000000000..d28c5541e337 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/special_section.c @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Special section entry with no ANNOTATE_DATA_SPECIAL annotation and a local + * label at offset 0, the shape Clang produces for .kcfi_traps. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int target(int x) +{ + asm volatile( + "1:\n\t" + ".pushsection .kcfi_traps, \"a\"\n\t" + ".balign 4\n\t" + "trap_marker:\n\t" + ".long 1b - .\n\t" + ".popsection\n\t"); +#ifdef PATCHED + return x + 2; +#else + return x + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/special_section_shared.c b/tools/objtool/tests/generic/fixtures/special_section_shared.c new file mode 100644 index 000000000000..f54e24f862c6 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/special_section_shared.c @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Two functions contribute to one special section; only one is patched. */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int other(int x) +{ + asm volatile( + "2:\n\t" + ".pushsection .kcfi_traps, \"a\"\n\t" + ".balign 4\n\t" + ".long 2b - .\n\t" + ".popsection\n\t"); + return x * 5; +} + +int target(int x) +{ + asm volatile( + "1:\n\t" + ".pushsection .kcfi_traps, \"a\"\n\t" + ".balign 4\n\t" + ".long 1b - .\n\t" + ".popsection\n\t"); +#ifdef PATCHED + return x + 2; +#else + return x + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/static_call.c b/tools/objtool/tests/generic/fixtures/static_call.c new file mode 100644 index 000000000000..4a0c4c25321e --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/static_call.c @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Static call site in a patched function, laid out by hand as for + * jump_label.c. MODNAME selects whether the key belongs to vmlinux or a + * module. + * + * objtool's check pass would emit the site, and klp-write-tests.txt says to + * let it. Not here: it does not emit the ANNOTATE_DATA_SPECIAL describing + * the entry boundaries -- in the kernel that comes from the static_call + * macros -- and NO_ANNOTATE below has to be able to take it away. A fixture + * which varies the annotation has to write the entry that goes with it. + * + * NO_ANNOTATE drops the ANNOTATE_DATA_SPECIAL block from the patched build, + * leaving .static_call_sites with no annotation to describe its entry + * boundaries. The section carries no entsize either, so klp diff has to fall + * back on the annotations it can still see -- and when the patched object is + * the only one that lost them, the two sides disagree about how the section is + * divided up. + * + * NEW_CALL puts the call site behind PATCHED, so the patch introduces one + * where the original had none. The .static_call_sites entry is then new, with + * nothing in the original to correlate it against. + */ + +#ifndef MODNAME +#define MODNAME "vmlinux" +#endif + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=" MODNAME; + +long __SCK__klp_test_call; + +int target(int x) +{ +#if defined(NEW_CALL) && !defined(PATCHED) + /* The original has no static call at all. */ + return x + 1; +#else + __asm__ volatile( + "1: nop\n\t" + ".pushsection .static_call_sites, \"aw\"\n\t" + ".balign 8\n\t" + "912:\n\t" +#if !(defined(PATCHED) && defined(NO_ANNOTATE)) + ".pushsection .discard.annotate_data, \"M\", @progbits, 8\n\t" + ".long 912b - ., 1\n\t" + ".popsection\n\t" +#endif + ".long 1b - ., %c0 - .\n\t" + ".popsection\n\t" + :: "i" (&__SCK__klp_test_call)); +#endif +#ifdef PATCHED + return x + 2; +#else + return x + 1; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/static_local.c b/tools/objtool/tests/generic/fixtures/static_local.c new file mode 100644 index 000000000000..f2f025d00a39 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/static_local.c @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Static local in a patched function. */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int target(int x) +{ + static int counter; + + counter += 1; +#ifdef PATCHED + return x + counter + 1; +#else + return x + counter; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/static_local_uncorrelated.c b/tools/objtool/tests/generic/fixtures/static_local_uncorrelated.c new file mode 100644 index 000000000000..cb4cdd7a496e --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/static_local_uncorrelated.c @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Static locals of three kinds, in one patched function. + * + * Most static locals must 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. Sharing + * it would mean a patch inherits "already warned" from before the patch. + * - the well-known names the kernel generates for such things (__warned, + * __key, __func__, ...), which are per-instance by nature. gcc names them + * <var>.<id> and Clang <func>.<var>, so both spellings have to be caught. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int target(int x) +{ + /* + * A .data..once variable whose name is *not* on the list below, so + * only the section can disqualify it. Naming it __warned would let + * the name rule catch it and the section rule go untested. + */ + static int once_flag __attribute__((section(".data..once"))); + /* a never-correlate name, in an ordinary section */ + static int __key; + /* and one that must be correlated */ + static int ordinary; + + 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 +} diff --git a/tools/objtool/tests/generic/fixtures/switch_rodata.c b/tools/objtool/tests/generic/fixtures/switch_rodata.c new file mode 100644 index 000000000000..817ddac92d81 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/switch_rodata.c @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A switch dense enough that Clang builds a jump table for it, in a section of + * its own: .rodata..Lswitch.table.<function>. + * + * The table belongs to the function and has to travel with it. It is named + * after the function but is not part of it, so klp diff has to associate the + * two rather than treating the table as unrelated data. + * + * The patch adds a case, which changes the table's contents and length. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; +const char *status_to_string(unsigned int c) +{ + switch (c) { + case 0: return "idle"; + case 1: return "running"; + case 2: return "stopped"; + case 3: return "error"; + case 4: return "paused"; + case 5: return "waiting"; + case 6: return "starting"; + case 7: return "stopping"; +#ifdef PATCHED + case 8: return "completed"; +#endif + } + return "unknown"; +} diff --git a/tools/objtool/tests/generic/fixtures/symid_discarded.c b/tools/objtool/tests/generic/fixtures/symid_discarded.c new file mode 100644 index 000000000000..573cc2d4474b --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/symid_discarded.c @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Compiled twice and partially linked so the result has duplicate locals, + * which is what symid_needed() requires. dup_normal is in a live section, + * dup_discarded in one the vmlinux link throws away. DISCARDED_SEC selects + * which discarded section, since there is more than one and each was its own + * bug. + */ + +#ifndef DISCARDED_SEC +#define DISCARDED_SEC ".exitcall.exit" +#endif + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +static int dup_normal = 1; + +static void *dup_discarded + __attribute__((section(DISCARDED_SEC), used)) = &dup_normal; + +int FUNC_NAME(void) +{ + return dup_normal + (dup_discarded != (void *)0); +} diff --git a/tools/objtool/tests/generic/fixtures/sympos_dup.c b/tools/objtool/tests/generic/fixtures/sympos_dup.c new file mode 100644 index 000000000000..7eded9b12cfc --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/sympos_dup.c @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A static whose name recurs in every translation unit that includes it. + * Compiled once for a single-copy object and twice, partially linked, for one + * with duplicates -- which is the only case where sympos is non-zero. + * + * FUNC_NAME keeps the referencing functions distinct so both get patched. + * Only the first copy carries .modinfo; two would be a second thing to + * disambiguate and is not what this fixture is about. + */ + +#ifndef FUNC_NAME +#define FUNC_NAME use_a +#endif + +#ifndef NO_MODINFO +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; +#endif + +/* volatile so it survives as an STT_OBJECT rather than being folded away */ +static volatile int dup_counter = 1; + +int FUNC_NAME(int x) +{ + dup_counter += x; +#ifdef PATCHED + return dup_counter + 1; +#else + return dup_counter; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/sympos_vmlinux.c b/tools/objtool/tests/generic/fixtures/sympos_vmlinux.c new file mode 100644 index 000000000000..d5e70994c582 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/sympos_vmlinux.c @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Two translation units with a same-named static, placed so that the linker + * puts them in the opposite order to the one they appear in the symbol table. + * + * VARSEC selects the section the static lands in. Linking with + * --sort-section=name then orders them alphabetically rather than by object + * order, so the first symbol in the symbol table ends up at the *higher* + * address. That is the whole point: counting symbol table order and reading + * the linked image's addresses now give different answers, which is what makes + * it possible to tell which one klp diff used. + * + * Only use_a is patched, so exactly one sympos is emitted and there is nothing + * to attribute. + */ + +#ifndef FUNC_NAME +#define FUNC_NAME use_a +#endif +#ifndef VARSEC +#define VARSEC ".data.mmm" +#endif + +#ifndef NO_MODINFO +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; +#endif + +/* volatile so it survives as an STT_OBJECT rather than being folded away */ +static volatile int dup_counter __attribute__((section(VARSEC))) = 1; + +int FUNC_NAME(int x) +{ + dup_counter += x; +#ifdef PATCHED + return dup_counter + 1; +#else + return dup_counter; +#endif +} diff --git a/tools/objtool/tests/generic/fixtures/thinlto_ambiguity.c b/tools/objtool/tests/generic/fixtures/thinlto_ambiguity.c new file mode 100644 index 000000000000..b88830f41a92 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/thinlto_ambiguity.c @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Three translation units linked with ThinLTO, two of which have a file-local + * helper of the same name. + * + * TU_C calls into both of the others, so ThinLTO imports entry_a and entry_b + * and with them the static helper each one calls. A file-local symbol which + * has to become visible is renamed helper.llvm.<hash>, and the hash is content + * derived -- so the two helpers get different hashes from each other, and + * TU_A's gets a different one again after the patch changes it. Only TU_A's + * changes: were both bodies to change, both would be cloned whichever way + * they were paired, and the pairing would not be observable. + * + * That leaves klp diff with two symbols in the original and two in the patched + * object, all four named differently, which have to be paired up correctly. + * Demangling alone gives "helper" for all of them; something else has to + * decide which is which. + * + * Only TU_A's helper changes. That is what makes a wrong pairing observable: + * paired correctly, one helper is changed and the other is not, so exactly one + * is cloned. Paired the wrong way round, both look changed -- or the wrong + * one does. If both bodies changed the outcome would be the same either way + * and the test would prove nothing. + * + * BASE differs between the two so their bodies are not identical to begin + * with. + */ + +#if defined(TU_C) +extern int entry_a(int x); +extern int entry_b(int x); +int glue(int x) { return entry_a(x) + entry_b(x + 1); } +#else +#ifdef TU_B +#define ENTRY entry_b +#define BASE 5 +#else +#define ENTRY entry_a +#define BASE 10 +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; +#endif +static __attribute__((noinline)) int helper(int x, int len) +{ + int sum = 0, i; + + for (i = 0; i < len; i++) +#if defined(PATCHED) && !defined(TU_B) + sum += i * 2 + BASE; /* only TU_A's helper changes */ +#else + sum += i + BASE; +#endif + return sum + x; +} + +int ENTRY(int x) { return helper(x, 4); } +#endif diff --git a/tools/objtool/tests/generic/fixtures/thinlto_local.c b/tools/objtool/tests/generic/fixtures/thinlto_local.c new file mode 100644 index 000000000000..fe9f9e6bf44f --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/thinlto_local.c @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Two translation units (TU_B selects the second) linked with ThinLTO. + * Importing bump() promotes the file-local counter, renaming it + * counter.llvm.<hash>. The hash is content derived, so it differs between the + * original and patched builds. + */ + +#ifdef TU_B + +extern int bump(void); + +int other_entry(void) +{ + return bump() + bump(); +} + +#else + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +static int counter; + +int bump(void) +{ + return ++counter; +} + +int target(void) +{ +#ifdef PATCHED + return counter + 1; +#else + return counter; +#endif +} + +#endif diff --git a/tools/objtool/tests/generic/fixtures/ubsan_noise.c b/tools/objtool/tests/generic/fixtures/ubsan_noise.c new file mode 100644 index 000000000000..bf5999254163 --- /dev/null +++ b/tools/objtool/tests/generic/fixtures/ubsan_noise.c @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A translation unit built with UBSAN, where only one of two functions is + * patched. + * + * 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 -- and a call to a __ubsan_handle_* + * routine. The names are compiler-generated and carry no meaning across a + * rebuild, so klp diff has to treat those sections as uncorrelated rather than + * pairing them up by name. + * + * untouched() is byte-identical in both builds and exists to catch the false + * positive: if the metadata were correlated by name, its shifts would look + * changed and it would be dragged into the patch. + * + * The shifts are what draw the instrumentation. A bounds check would do as + * well but neither compiler emits one for an index it can prove in range. + */ + +static const char __modinfo[] + __attribute__((section(".modinfo"), used, aligned(1))) = "\0name=vmlinux"; + +int shift_by(int v, int n); + +int untouched(int v, int n) +{ + int s = 0; + + s += v << (n & 31); + s += v << ((n + 1) & 31); + s += shift_by(v, n); + + return s; +} + +int touched(int v, int n) +{ + int s = 0; + + s += v << (n & 31); +#ifdef PATCHED + s += v << ((n + 3) & 31); +#else + s += v << ((n + 2) & 31); +#endif + + return s; +} diff --git a/tools/objtool/tests/generic/test-abs-and-addressable.sh b/tools/objtool/tests/generic/test-abs-and-addressable.sh new file mode 100755 index 000000000000..6adb23ed4b88 --- /dev/null +++ b/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_pair abs_and_addressable.c + +# The premise: the fixture really did produce both. +in_symbols orig.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_differs target +assert_checksum_matches helper + +# So does the diff. +run_diff +assert_patched target +assert_not_patched helper + +# 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_relocs orig.o | awk '/rela\.discard\.sym_checksum/,/^$/' | grep -qw abs_sym && + fail "absolute symbol got a checksum entry" + +pass "absolute and __ADDRESSABLE symbols do not disturb the patched function" diff --git a/tools/objtool/tests/generic/test-basic.sh b/tools/objtool/tests/generic/test-basic.sh new file mode 100755 index 000000000000..562edfbc8646 --- /dev/null +++ b/tools/objtool/tests/generic/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" diff --git a/tools/objtool/tests/generic/test-changed-data.sh b/tools/objtool/tests/generic/test-changed-data.sh new file mode 100755 index 000000000000..c5c7381bb409 --- /dev/null +++ b/tools/objtool/tests/generic/test-changed-data.sh @@ -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_pair changed_data.c +run_diff 255 + +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" diff --git a/tools/objtool/tests/generic/test-checksum-data.sh b/tools/objtool/tests/generic/test-checksum-data.sh new file mode 100755 index 000000000000..e915026b79a7 --- /dev/null +++ b/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_pair checksum_data.c "-D$1" + run_checksum + + assert_checksum_differs "$2" +} + +# The object's own bytes. +check PLAIN_VALUE plain +# Its length, for a .bss object whose bytes are not hashed at all. +check LONGER sized +# A relocation's target: same bytes in the object, different symbol named. +check WHICH_FUNC descriptor +check WHICH_STR descriptor +# The contents of a string the object points at, with the pointer untouched. +check STR_CONTENT descriptor +# A relocation's addend: same target symbol, different offset into it. +check WHICH_SLOT descriptor +# 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. +check WHICH_PRIV descriptor + +# 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_pair checksum_data.c -DPLAIN_VALUE +run_checksum +assert_checksum_matches descriptor + +pass "data checksums cover length, bytes, reloc targets and string contents" diff --git a/tools/objtool/tests/generic/test-checksum-debug.sh b/tools/objtool/tests/generic/test-checksum-debug.sh new file mode 100755 index 000000000000..78856b191636 --- /dev/null +++ b/tools/objtool/tests/generic/test-checksum-debug.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_pair basic.c + +before="$(md5sum < "$workdir/orig.o")" + +"$OBJTOOL" klp checksum --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_section orig.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" diff --git a/tools/objtool/tests/generic/test-checksum-insn.sh b/tools/objtool/tests/generic/test-checksum-insn.sh new file mode 100755 index 000000000000..e1c04a1f518a --- /dev/null +++ b/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_pair checksum_insn.c "-D$1" + run_checksum + + # The premise for all of them: the operand is a relocation, not bytes. + assert_checksum_differs target +} + +check WHICH_CALL # relocation target name +check STR_CONTENT # contents of a string the code passes +check WHICH_SLOT # addend, same target symbol +check WHICH_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_pair checksum_insn.c +run_checksum +assert_checksum_matches target + +pass "instruction checksums cover reloc targets, addends and string contents" diff --git a/tools/objtool/tests/generic/test-checksum-position.sh b/tools/objtool/tests/generic/test-checksum-position.sh new file mode 100755 index 000000000000..5ae759e719eb --- /dev/null +++ b/tools/objtool/tests/generic/test-checksum-position.sh @@ -0,0 +1,51 @@ +#!/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" moves target() away from callee() and changes nothing else: +# both are aligned to 64 in the patched build, which shifts them apart without +# touching a byte of either. See the fixture for why it is done that way. + +. "$(dirname "$0")/../lib.sh" + +setup + +# -fno-function-sections, or each function is at offset 0 of its own section +# and target() never moves. +build_pair checksum_position.c -fno-function-sections + +assert_input_symbol target + +# The fixture is only meaningful if the displacement target's calls encode +# actually changed, and that is the distance to callee() -- not target's own +# offset. A compiler which shifted the two by the same amount would move +# target and leave the distance alone, and then the bytes are identical and +# the checksum matches for the uninteresting reason. Ask about the distance. +sym_off() # $1 object, $2 symbol +{ + in_symbols "$1" | awk -v n="$2" '$NF == n { print $2; exit }' +} + +orig_t="$(sym_off orig.o target)"; orig_c="$(sym_off orig.o callee)" +new_t="$(sym_off patched.o target)"; new_c="$(sym_off patched.o callee)" + +[ -n "$orig_t" ] && [ -n "$orig_c" ] && [ -n "$new_t" ] && [ -n "$new_c" ] || + fail "target or callee missing from one of the objects" + +orig_gap=$(( 16#$orig_t - 16#$orig_c )) +new_gap=$(( 16#$new_t - 16#$new_c )) +[ "$orig_gap" != "$new_gap" ] || + probe_skip "this compiler kept target() and callee() the same distance" \ + "apart; the call displacement did not change" + +assert_checksum_matches target + +pass "checksum unchanged when the function only moves" diff --git a/tools/objtool/tests/generic/test-checksum-skip.sh b/tools/objtool/tests/generic/test-checksum-skip.sh new file mode 100755 index 000000000000..f245535a0f36 --- /dev/null +++ b/tools/objtool/tests/generic/test-checksum-skip.sh @@ -0,0 +1,81 @@ +#!/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_pair checksum_skip.c + +assert_input_symbol empty_marker +assert_input_symbol alias_function +run_checksum + +# entries_for <object> [-t] +# +# The symbol names .discard.sym_checksum has an entry for, one per line. With +# -t, what each entry points at instead: the name and its addend, which is the +# address the kernel looks the entry up by. A name alone is not that address, +# since a relocation against a section symbol names the section and puts the +# offset in the addend, and several entries can then share a name honestly. +entries_for() +{ + in_relocs "$1" | awk -v target="${2:-}" '/rela\.discard\.sym_checksum/,/^$/ { + if ($1 !~ /^[0-9a-f]{8,}/) + next + if (target == "-t") + print $5, $6, $7 + else + print $5 + }' +} + +entries="$(entries_for orig.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 -qx target || + fail "no checksum entry for target" + +echo "$entries" | grep -qx empty_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_for orig.o -t | 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" diff --git a/tools/objtool/tests/generic/test-checksum-value.sh b/tools/objtool/tests/generic/test-checksum-value.sh new file mode 100755 index 000000000000..feae6a12e98d --- /dev/null +++ b/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_pair basic.c + +assert_input_symbol changed +assert_input_symbol untouched + +run_checksum + +# The edited function's checksum has to move, the untouched one's must not. +assert_checksum_differs changed +assert_checksum_matches untouched + +# 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_of orig.o changed)" +build_pair basic.c +run_checksum +[ "$(checksum_of orig.o changed)" = "$first" ] || + fail "checksum for 'changed' differs between builds of identical source" + +pass "checksums track the changed function and are stable across rebuilds" diff --git a/tools/objtool/tests/generic/test-cold-function.sh b/tools/objtool/tests/generic/test-cold-function.sh new file mode 100755 index 000000000000..a02652fe5341 --- /dev/null +++ b/tools/objtool/tests/generic/test-cold-function.sh @@ -0,0 +1,39 @@ +#!/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_pair cold_function.c $split_flag + +# Find what the compiler called the cold half -- target.cold, target.cold.0, +# depending on version -- and name it exactly from here on. +cold_sym="$(in_symbols orig.o | awk '$NF ~ /^target\.cold/ { print $NF; exit }')" +[ -n "$cold_sym" ] || + probe_skip "compiler did not split the function into a cold part" + +run_diff + +assert_patched target +# Match the name field exactly, and require it to be defined. Had the cold +# half been left behind, the branch to it would appear as an undefined +# .klp.sym.vmlinux.target.cold,0 -- a different name, which happens to contain +# this one. Asking about a column instead of the name would not tell them +# apart: readelf prints SHN_LIVEPATCH as "OS [0xff20]" and llvm-readelf as +# "OS[0xff20]", so the fields either side of the name shift between the two. +out_symbols | awk -v n="$cold_sym" '$NF == n && $(NF - 1) != "UND"' | grep -q . || + fail "cold half ($cold_sym) was not carried into the patch" + +pass "cold half carried into the patch with its parent" diff --git a/tools/objtool/tests/generic/test-data-alignment.sh b/tools/objtool/tests/generic/test-data-alignment.sh new file mode 100755 index 000000000000..8e389544a3b1 --- /dev/null +++ b/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_pair data_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_sections patched.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_section orig.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" diff --git a/tools/objtool/tests/generic/test-export-symbol-for-modules.sh b/tools/objtool/tests/generic/test-export-symbol-for-modules.sh new file mode 100755 index 000000000000..7e7bdde6a7ac --- /dev/null +++ b/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_pair cross_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_ns vmlinux MY_NS "$sym" +run_diff +assert_no_klp_sym "$sym" + +# module: namespace: has to become a klp relocation. +export_syms +add_exports_ns vmlinux module: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" diff --git a/tools/objtool/tests/generic/test-function-removal.sh b/tools/objtool/tests/generic/test-function-removal.sh new file mode 100755 index 000000000000..df76e81e3936 --- /dev/null +++ b/tools/objtool/tests/generic/test-function-removal.sh @@ -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_pair function_removal.c + +# One-sided by construction: present in the original, gone from the patched. +has_input_symbol orig.o going_away || + fail "fixture has no going_away in the original" +has_input_symbol patched.o going_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_patched caller +# ... and the deleted function comes along in no form at all. +assert_not_patched going_away +assert_no_symbol going_away + +pass "deleted function reported as uncorrelated and left out of the patch" diff --git a/tools/objtool/tests/generic/test-init-reference.sh b/tools/objtool/tests/generic/test-init-reference.sh new file mode 100755 index 000000000000..921cf493cd79 --- /dev/null +++ b/tools/objtool/tests/generic/test-init-reference.sh @@ -0,0 +1,29 @@ +#!/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_pair init_reference.c + +# The rejection can only happen if the patched build really does reference the +# init symbol. The fixture makes init_only volatile so the read cannot be +# folded away, and this checks that it worked: without a relocation klp diff +# would succeed, and the failure below would read as a missing check rather +# than as a fixture which stopped posing the question. +# Either spelling will do. A file-local variable is reached through its +# section symbol -- .init.data -- and a global one by name; which of the two +# the compiler picks is its business, and the reference is what matters. +in_relocs "$patched_obj" | + awk '$5 == ".init.data" || $5 == "init_only"' | grep -q . || + fail "patched object has no reference into .init.data; the fixture tests nothing" + +run_diff 255 + +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" diff --git a/tools/objtool/tests/generic/test-jump-label-exempt-keys.sh b/tools/objtool/tests/generic/test-jump-label-exempt-keys.sh new file mode 100755 index 000000000000..fa2f913d860b --- /dev/null +++ b/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_pair jump_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_patched target + 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" diff --git a/tools/objtool/tests/generic/test-jump-label-key.sh b/tools/objtool/tests/generic/test-jump-label-key.sh new file mode 100755 index 000000000000..142f94bd38ec --- /dev/null +++ b/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_pair jump_label.c + +has_input_section orig.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" diff --git a/tools/objtool/tests/generic/test-jump-label-module-key.sh b/tools/objtool/tests/generic/test-jump-label-module-key.sh new file mode 100755 index 000000000000..7c3f82cdd3c1 --- /dev/null +++ b/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_pair jump_label.c -DMODNAME='"klp_testmod"' + +has_input_section orig.o __jump_table || + probe_skip "fixture produced no __jump_table on this arch" + +run_diff 255 + +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" diff --git a/tools/objtool/tests/generic/test-jump-label-module-static-key.sh b/tools/objtool/tests/generic/test-jump-label-module-static-key.sh new file mode 100755 index 000000000000..7bdcbaf2fa60 --- /dev/null +++ b/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_pair jump_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_relocs orig.o | awk '/rela__jump_table/,/^$/')" + +echo "$input_jump_relocs" | grep -q klp_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_diff 255 + +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" diff --git a/tools/objtool/tests/generic/test-jump-label-new-key.sh b/tools/objtool/tests/generic/test-jump-label-new-key.sh new file mode 100755 index 000000000000..3bc6005fc776 --- /dev/null +++ b/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" + +setup klp_test_key +build_pair jump_label.c -DNEW_KEY + +# The premise: the original really has no jump table, and the patched one does. +has_input_section orig.o __jump_table && + fail "fixture put a __jump_table in the original; nothing new to add" +has_input_section patched.o __jump_table || + probe_skip "compiler produced no __jump_table on this arch" + +run_diff + +assert_patched target +assert_section __jump_table +assert_reloc_sym __jump_table target + +# 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_pair jump_label.c -DNEW_KEY -DMODNAME='"klp_testmod"' +run_diff 255 +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" diff --git a/tools/objtool/tests/generic/test-klp-funcs-content.sh b/tools/objtool/tests/generic/test-klp-funcs-content.sh new file mode 100755 index 000000000000..32fbd5f6a6fa --- /dev/null +++ b/tools/objtool/tests/generic/test-klp-funcs-content.sh @@ -0,0 +1,45 @@ +#!/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_pair klp_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_funcs 4 + +# The functions that changed are named ... +assert_reloc_sym .init.klp_funcs first +assert_reloc_sym .init.klp_funcs second +# ... and the one that did not is absent, from the list and from the patch. +assert_no_reloc_sym .init.klp_funcs third +assert_not_patched third + +# The names the kernel matches on are real strings, not just relocations. +# readelf prints one per line as "[ offset] <string>", so compare the whole +# name: a word-boundary match would also accept ".text.first", since a dot is +# not a word character. +for name in first second; do + out_strings .rodata.klp.str1.1 | awk -v n="$name" '$NF == n' | grep -q . || + fail "no '$name' string in .rodata.klp.str1.1" +done + +# 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" diff --git a/tools/objtool/tests/generic/test-local-to-global-flip.sh b/tools/objtool/tests/generic/test-local-to-global-flip.sh new file mode 100755 index 000000000000..45e92797190b --- /dev/null +++ b/tools/objtool/tests/generic/test-local-to-global-flip.sh @@ -0,0 +1,63 @@ +#!/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_pair local_to_global.c + +# Confirm the fixture really moved the bindings, in both directions. +# +# Match the binding and the name as fields, not as substrings of the line. +# -ffunction-sections and -fdata-sections give these symbols sections of their +# own, and the section symbols -- .text.flipped_up, .data.flipped_down -- are +# always LOCAL, so "does a LOCAL line mention flipped_up" is answered by the +# wrong symbol. GNU readelf 2.35 happens to leave section symbol names blank, +# but llvm-readelf prints them, and a premise that holds on one readelf and +# not the other is no premise at all. +has_binding() # $1 object, $2 binding, $3 symbol +{ + in_symbols "$1" | awk -v b="$2" -v n="$3" '$5 == b && $NF == n' | grep -q . +} + +has_binding orig.o LOCAL flipped_up || + fail "flipped_up is not local in the original" +has_binding patched.o GLOBAL flipped_up || + fail "flipped_up is not global in the patched object" +has_binding orig.o GLOBAL flipped_down || + fail "flipped_down is not global in the original" +has_binding patched.o 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_sym flipped_up vmlinux +assert_klp_sym flipped_down vmlinux + +# ... 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_patched flipped_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" diff --git a/tools/objtool/tests/generic/test-local-vs-export.sh b/tools/objtool/tests/generic/test-local-vs-export.sh new file mode 100755 index 000000000000..5b91101dadd6 --- /dev/null +++ b/tools/objtool/tests/generic/test-local-vs-export.sh @@ -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_pair static_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_symbols orig.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" diff --git a/tools/objtool/tests/generic/test-missing-checksum.sh b/tools/objtool/tests/generic/test-missing-checksum.sh new file mode 100755 index 000000000000..89a05b1869ed --- /dev/null +++ b/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_pair basic.c + +( cd "$workdir" && "$OBJTOOL" klp diff orig.o patched.o out.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" diff --git a/tools/objtool/tests/generic/test-missing-modinfo.sh b/tools/objtool/tests/generic/test-missing-modinfo.sh new file mode 100755 index 000000000000..6f242f7eb756 --- /dev/null +++ b/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_pair no_modinfo.c +run_diff 255 + +diff_log | grep -q 'modinfo' || + fail "expected a complaint about .modinfo, got: $(diff_log | tail -1)" + +pass "object without .modinfo rejected" diff --git a/tools/objtool/tests/generic/test-modname-normalize.sh b/tools/objtool/tests/generic/test-modname-normalize.sh new file mode 100755 index 000000000000..a8059bdbd667 --- /dev/null +++ b/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_pair cross_module.c klp_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_sym other_mod_func kvm_intel + +pass "Module.symvers paths normalized to runtime module names" diff --git a/tools/objtool/tests/generic/test-module-object.sh b/tools/objtool/tests/generic/test-module-object.sh new file mode 100755 index 000000000000..95c8402a523a --- /dev/null +++ b/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_pair cross_module.c klp_testmod + +# The fixture has to have built as a module for any of this to mean anything. +in_sections orig.o | grep -q '\.modinfo' || + fail "fixture has no .modinfo" + +# other_mod_func belongs to a different module than the one being patched. +add_exports other_mod other_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_rela klp_testmod .text.target + +pass "klp relocation section named for the patched object" diff --git a/tools/objtool/tests/generic/test-module-vmlinux-reloc.sh b/tools/objtool/tests/generic/test-module-vmlinux-reloc.sh new file mode 100755 index 000000000000..635a75f6b8ba --- /dev/null +++ b/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_pair cross_module.c klp_testmod + +# ... and the symbol it references belongs to vmlinux, exported in a way that +# still requires a klp relocation. +export_syms +add_exports_ns vmlinux module:kvm other_mod_func +run_diff + +# Filed against vmlinux, applied when the patch loads. +assert_section __klp_relocs.vmlinux +assert_klp_sym other_mod_func vmlinux + +# Not against the patched module: that is the relocation the kernel rejects. +assert_no_section __klp_relocs.klp_testmod + +run_post_link +assert_klp_rela vmlinux .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" diff --git a/tools/objtool/tests/generic/test-new-data.sh b/tools/objtool/tests/generic/test-new-data.sh new file mode 100755 index 000000000000..acb68ff19c99 --- /dev/null +++ b/tools/objtool/tests/generic/test-new-data.sh @@ -0,0 +1,27 @@ +#!/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_pair new_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" +in_relocs "$patched_obj" | grep -q 'klp_new_data' || + fail "target() does not reference klp_new_data; the fixture tests nothing" + +run_diff + +assert_patched target +assert_symbol klp_new_data + +pass "new data carried into the patch" diff --git a/tools/objtool/tests/generic/test-new-export-ref.sh b/tools/objtool/tests/generic/test-new-export-ref.sh new file mode 100755 index 000000000000..f0be2cf87fe3 --- /dev/null +++ b/tools/objtool/tests/generic/test-new-export-ref.sh @@ -0,0 +1,46 @@ +#!/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_pair new_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_ns vmlinux module:kvm newly_referenced +run_diff +assert_klp_sym newly_referenced vmlinux + +# Exported by a module the patched object does not depend on. Rejected, and +# for that reason rather than some other. +export_syms +add_exports other_mod newly_referenced +run_diff 255 +assert_diff_log 'undeclared module dependency' + +# ... unless the original already referenced something that module exports. +# The loader will not let the patched object load without other_mod, so the +# klp relocation has something to resolve against, and klp diff allows it. +# This is the other half of the rule, and it fails in the opposite direction: +# refusing here would reject a patch which is safe to apply. +rm -f "$workdir/out.o" +build_pair new_export_ref.c -DEXISTING_DEP +export_syms +add_exports other_mod newly_referenced existing_dep +run_diff +assert_klp_sym newly_referenced other_mod + +pass "new reference allowed for vmlinux and for a module already depended on" diff --git a/tools/objtool/tests/generic/test-new-function.sh b/tools/objtool/tests/generic/test-new-function.sh new file mode 100755 index 000000000000..b0b7f6443110 --- /dev/null +++ b/tools/objtool/tests/generic/test-new-function.sh @@ -0,0 +1,16 @@ +#!/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_pair new_function.c +run_diff + +assert_patched target +assert_symbol klp_new_helper + +pass "new function carried into the patch with its caller" diff --git a/tools/objtool/tests/generic/test-post-link.sh b/tools/objtool/tests/generic/test-post-link.sh new file mode 100755 index 000000000000..a6d40ae1b18d --- /dev/null +++ b/tools/objtool/tests/generic/test-post-link.sh @@ -0,0 +1,42 @@ +#!/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_pair static_local.c + +# An unexported symbol is what produces a klp relocation in the first place. +# The static local is renamed by the compiler -- counter.0 with gcc -- so the +# fixture's premise is that some local of that shape exists, not that a symbol +# called "counter" does. Find it first; everything below names it. +local_sym="$(in_symbols orig.o | + awk '$4 == "OBJECT" && $5 == "LOCAL" && $8 ~ /counter/ { print $8; exit }')" +[ -n "$local_sym" ] || fail "fixture produced no local 'counter' symbol" + +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. +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_rela vmlinux .text.target +assert_livepatch_sym "$local_sym" + +pass "klp relocations converted to .klp.rela with SHN_LIVEPATCH symbols" diff --git a/tools/objtool/tests/generic/test-special-section-shared.sh b/tools/objtool/tests/generic/test-special-section-shared.sh new file mode 100755 index 000000000000..05c6110b6aef --- /dev/null +++ b/tools/objtool/tests/generic/test-special-section-shared.sh @@ -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_pair special_section_shared.c + +has_input_section orig.o .kcfi_traps || + probe_skip "fixture produced no .kcfi_traps on this arch" + +run_diff + +assert_patched target +assert_not_patched other +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" diff --git a/tools/objtool/tests/generic/test-special-section.sh b/tools/objtool/tests/generic/test-special-section.sh new file mode 100755 index 000000000000..b6a9139c0062 --- /dev/null +++ b/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_pair special_section.c + +in_symbols orig.o | grep -q 'trap_marker' || + probe_skip "fixture produced no .kcfi_traps on this arch" + +run_diff + +assert_patched target +assert_section ".kcfi_traps" + +pass ".kcfi_traps extracted despite a local label at offset 0" diff --git a/tools/objtool/tests/generic/test-static-call-annotate-stripped.sh b/tools/objtool/tests/generic/test-static-call-annotate-stripped.sh new file mode 100755 index 000000000000..818b047ed4b6 --- /dev/null +++ b/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_pair static_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_section orig.o .discard.annotate_data || + fail "fixture produced no annotation in the original" +has_input_section patched.o .discard.annotate_data && + fail "patched object still has the annotation; nothing was stripped" +assert_input_section .static_call_sites + +run_diff + +assert_patched target +assert_section .static_call_sites +assert_reloc_sym .static_call_sites target + +pass "static call site kept when the patch strips its data annotation" diff --git a/tools/objtool/tests/generic/test-static-call-module-key.sh b/tools/objtool/tests/generic/test-static-call-module-key.sh new file mode 100755 index 000000000000..260c4af72e05 --- /dev/null +++ b/tools/objtool/tests/generic/test-static-call-module-key.sh @@ -0,0 +1,36 @@ +#!/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_pair static_call.c + +has_input_section orig.o .static_call_sites || + probe_skip "fixture produced no .static_call_sites on this arch" + +run_diff +assert_patched target + +# The accepted half has to show the entry was carried, not just that the +# function was: dropping the section silently would leave the patched call +# unregistered, and "target was cloned" cannot tell the two apart. +assert_section .static_call_sites +assert_reloc_sym .static_call_sites target + +rm -f "$workdir/out.o" +build_pair static_call.c -DMODNAME='"klp_testmod"' +run_diff 255 + +diff_log | grep -q 'unsupported static call key __SCK__klp_test_call' || + fail "expected rejection, got: $(diff_log | tail -1)" + +# A rejection has to leave nothing behind. out.o was removed above, so +# anything here was written by the run which was supposed to refuse. +[ -e "$workdir/out.o" ] && + fail "output object produced for a rejected input" + +pass "module-owned static call key rejected, vmlinux-owned accepted" diff --git a/tools/objtool/tests/generic/test-static-call-new.sh b/tools/objtool/tests/generic/test-static-call-new.sh new file mode 100755 index 000000000000..f7d2b39c0fec --- /dev/null +++ b/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_pair static_call.c -DNEW_CALL + +# The premise: the original really has no static call, the patched one does. +has_input_section orig.o .static_call_sites && + fail "fixture put a .static_call_sites in the original; nothing new to add" +has_input_section patched.o .static_call_sites || + probe_skip "compiler produced no .static_call_sites on this arch" + +run_diff +assert_patched target +assert_section .static_call_sites +assert_reloc_sym .static_call_sites target + +# 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_pair static_call.c -DNEW_CALL -DMODNAME='"klp_testmod"' +run_diff 255 +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" diff --git a/tools/objtool/tests/generic/test-static-local-uncorrelated.sh b/tools/objtool/tests/generic/test-static-local-uncorrelated.sh new file mode 100755 index 000000000000..d7711587d2d4 --- /dev/null +++ b/tools/objtool/tests/generic/test-static-local-uncorrelated.sh @@ -0,0 +1,41 @@ +#!/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_pair static_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" +# .sbss/.sdata on the architectures with a small-data area. +out_sections | grep -qE '\.s?(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" diff --git a/tools/objtool/tests/generic/test-static-local.sh b/tools/objtool/tests/generic/test-static-local.sh new file mode 100755 index 000000000000..d57efa64dfd9 --- /dev/null +++ b/tools/objtool/tests/generic/test-static-local.sh @@ -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_pair static_local.c + +in_symbols orig.o | grep -q 'counter' || + probe_skip "compiler emitted no distinct static local symbol" + +run_diff +assert_patched target + +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" diff --git a/tools/objtool/tests/generic/test-switch-rodata.sh b/tools/objtool/tests/generic/test-switch-rodata.sh new file mode 100755 index 000000000000..fb27e96c65f6 --- /dev/null +++ b/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_pair switch_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_section orig.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 -v s="$tbl" '$1 == s { print $5 }' +} +[ "$(tbl_size orig.o)" != "$(tbl_size patched.o)" ] || + fail "fixture's added case did not change the jump table" + +run_diff + +assert_patched status_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" diff --git a/tools/objtool/tests/generic/test-symid-discarded.sh b/tools/objtool/tests/generic/test-symid-discarded.sh new file mode 100755 index 000000000000..388a24984ec0 --- /dev/null +++ b/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 +for sec in .exitcall.exit .no_trim_symbol; do + build_one symid_discarded.c a.o \ + -DFUNC_NAME=use_a -DDISCARDED_SEC="\"$sec\"" + build_one symid_discarded.c b.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_relocs vmlinux.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" diff --git a/tools/objtool/tests/generic/test-sympos-vmlinux.sh b/tools/objtool/tests/generic/test-sympos-vmlinux.sh new file mode 100755 index 000000000000..b2b44001446f --- /dev/null +++ b/tools/objtool/tests/generic/test-sympos-vmlinux.sh @@ -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_one sympos_vmlinux.c orig_a.o -DFUNC_NAME=use_a -DVARSEC='".data.zzz"' +build_one sympos_vmlinux.c patched_a.o -DFUNC_NAME=use_a -DVARSEC='".data.zzz"' -DPATCHED +build_one sympos_vmlinux.c b.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_symbols vmlinux.o dup_counter)" = 2 ] || + fail "fixture did not produce two dup_counter symbols" +has_input_section vmlinux.o .klp.symid || + fail "objtool --klp-symids emitted no .klp.symid table" +has_input_section vmlinux .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_symbols vmlinux | awk '$8 == "dup_counter" { print $2; exit }')" +low_addr="$(in_symbols vmlinux | awk '$8 == "dup_counter" { print $2 }' | sort | head -1)" +[ "$first_addr" != "$low_addr" ] || + probe_skip "linker did not reorder the two statics" + +assert_input_symbol dup_counter +run_diff + +# Address order says 2. Counting symbol table order would say 1. +assert_klp_sympos dup_counter 2 +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" diff --git a/tools/objtool/tests/generic/test-sympos.sh b/tools/objtool/tests/generic/test-sympos.sh new file mode 100755 index 000000000000..b71d4930a22a --- /dev/null +++ b/tools/objtool/tests/generic/test-sympos.sh @@ -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_one sympos_dup.c orig.o -DFUNC_NAME=use_a +build_one sympos_dup.c patched.o -DFUNC_NAME=use_a -DPATCHED +run_diff + +assert_klp_sympos dup_counter 0 + +# Two copies: positions, in symbol table order. +for p in "" "-DPATCHED"; do + # shellcheck disable=SC2086 + build_one sympos_dup.c "a$p.o" -DFUNC_NAME=use_a $p + # shellcheck disable=SC2086 + build_one sympos_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_symbols orig.o dup_counter)" = 2 ] || + fail "fixture did not produce two dup_counter symbols" + +run_diff + +assert_klp_sympos dup_counter 1 +assert_klp_sympos dup_counter 2 +# ... 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" diff --git a/tools/objtool/tests/generic/test-symvers-parse-error.sh b/tools/objtool/tests/generic/test-symvers-parse-error.sh new file mode 100755 index 000000000000..d597f28e5617 --- /dev/null +++ b/tools/objtool/tests/generic/test-symvers-parse-error.sh @@ -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_pair basic.c + +# Three well-formed lines, then one with no tabs at all. +export_syms a b c +echo 'this line has no fields' >> "$workdir/Module.symvers" + +run_diff 255 + +assert_diff_log 'malformed Module.symvers' +assert_diff_log 'at line 4' + +pass "malformed Module.symvers reported against the offending line" diff --git a/tools/objtool/tests/generic/test-thinlto-ambiguity.sh b/tools/objtool/tests/generic/test-thinlto-ambiguity.sh new file mode 100755 index 000000000000..34f4f3a58e20 --- /dev/null +++ b/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 +{ + local t + for t in "" -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 || return 1 + done + "$THIN_LD" -r "$workdir/tu.o" "$workdir/tu-DTU_B.o" "$workdir/tu-DTU_C.o" \ + -o "$1" 2>/dev/null || return 1 +} + +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_symbols orig.o | grep -oE 'helper\.llvm\.[0-9]+' | sort -u)" +new_syms="$( in_symbols patched.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" diff --git a/tools/objtool/tests/generic/test-thinlto-local.sh b/tools/objtool/tests/generic/test-thinlto-local.sh new file mode 100755 index 000000000000..a266263676ed --- /dev/null +++ b/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 || return 1 + $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 || return 1 + "$THIN_LD" -r "$workdir/tu_a.o" "$workdir/tu_b.o" -o "$1" 2>/dev/null || return 1 +} + +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_symbols orig.o | grep -o 'counter\.llvm\.[0-9]*' | head -1)" +new_sym="$( in_symbols patched.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_patched target + +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)" diff --git a/tools/objtool/tests/generic/test-ubsan-noise.sh b/tools/objtool/tests/generic/test-ubsan-noise.sh new file mode 100755 index 000000000000..b415eb16bfd2 --- /dev/null +++ b/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_pair ubsan_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_sections orig.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_symbol untouched + +run_diff + +# The changed function is patched, and the untouched one is left alone despite +# carrying instrumentation of its own. +assert_patched touched +assert_not_patched untouched + +# 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" |
