# SPDX-License-Identifier: GPL-2.0 # # Helpers for the objtool klp tests. A test builds a fixture twice, as the # original and (with -DPATCHED) the patched object, runs both through # "klp checksum" and diffs them, then asserts on the result. # # Assertions check properties rather than compare against recorded output: # codegen varies between compilers and golden files would report churn instead # of regressions. set -u TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Tests live in generic/ or in an architecture directory beside it, and each # carries its own fixtures. FIXTURES_DIR="$(cd "$(dirname "$0")/fixtures" 2>/dev/null && pwd)" # The kernel's convention: CROSS_COMPILE is the one knob, with per-tool # overrides for what it does not cover. objtool itself is always a host binary # -- it is built with HOSTCC and only reads ELF -- so an arm64 machine can run # the x86 tests against x86 objects given a compiler that emits them. # # readelf reads any target, so it rarely needs overriding, and either GNU # readelf or llvm-readelf will do: the assertions match on fields rather than # on columns, and where the two spell something differently -- "OS [0xff20]" # against "OS[0xff20]" for SHN_LIVEPATCH -- they accept both. BFD's objcopy is # usually built for the host's target alone, and llvm-objcopy is the # target-agnostic replacement. CROSS_COMPILE="${CROSS_COMPILE:-}" CC="${CC:-${CROSS_COMPILE}gcc}" LD="${LD:-${CROSS_COMPILE}ld}" READELF="${READELF:-${CROSS_COMPILE}readelf}" OBJCOPY="${OBJCOPY:-${CROSS_COMPILE}objcopy}" OBJTOOL="${OBJTOOL:-$TESTS_DIR/../objtool}" # klp_preflight # # Check the environment once, before any test runs, and report what was found. # klp_preflight() { local tmp tool cc_version arch host cc_arch bail() { echo "Bail out! $*" >&2; exit 1; } # A relative $OBJTOOL is relative to the objtool directory, not tests/. [ -x "$OBJTOOL" ] || [ ! -x "$TESTS_DIR/../$OBJTOOL" ] || OBJTOOL="$TESTS_DIR/../$OBJTOOL" [ -x "$OBJTOOL" ] || bail "objtool not found at '$OBJTOOL' -- build it first" # run_diff() runs objtool from inside the test's working directory, so # a relative path would resolve against that instead. OBJTOOL="$(realpath "$OBJTOOL")" "$OBJTOOL" klp 2>&1 | grep -q checksum || bail "objtool was built without klp support; install libxxhash (>= 0.8) and rebuild" command -v "${CC%% *}" >/dev/null || bail "compiler not found: $CC" for tool in "$READELF" "$OBJCOPY" "$LD"; do command -v "${tool%% *}" >/dev/null || bail "$tool not found" done tmp="$(mktemp -d)" || bail "mktemp failed" echo 'int probe(void) { return 0; }' > "$tmp/probe.c" $CC -c -o "$tmp/probe.o" "$tmp/probe.c" 2>/dev/null || { rm -rf "$tmp"; bail "$CC cannot compile a trivial object"; } # $CC, $ARCH and objtool have to agree about the target, and cross runs # are where they stop agreeing: plain "CC=clang ARCH=x86_64" on an arm64 # box selects the x86 tests and then builds arm64 objects, because clang # needs --target= to emit anything but the host's. # # Ask objtool rather than comparing machine names. It rejects an object # it was not built for -- "unexpected ELF machine type" -- so one check # covers every way the three can disagree, and says so once instead of # failing every test for the same reason. "$OBJTOOL" klp checksum "$tmp/probe.o" >/dev/null 2>&1 || { rm -rf "$tmp" bail "objtool rejects an object built by '$CC'; they target" \ "different architectures (set CROSS_COMPILE, or" \ "--target= for clang)"; } # BFD objcopy is usually built for the host's target alone, and # checksum_of() needs it to read the object under test. $OBJCOPY -O binary --only-section=.text "$tmp/probe.o" "$tmp/probe.bin" 2>/dev/null || { rm -rf "$tmp" bail "$OBJCOPY cannot read objects built by '$CC'; install" \ "binutils-multiarch or set OBJCOPY=llvm-objcopy"; } # $ARCH only chooses which directory of tests runs, so it can disagree # with what $CC builds without objtool noticing -- and the result is the # wrong set of tests, quietly. case "$($READELF -hW "$tmp/probe.o" | sed -n 's/.*Machine: *//p')" in *X86-64*|*Intel*80386*) cc_arch=x86 ;; *AArch64*) cc_arch=arm64 ;; *) cc_arch= ;; esac rm -rf "$tmp" # Normalize to the kernel's SRCARCH. case "${ARCH:-$(uname -m)}" in x86_64|i?86) arch=x86 ;; aarch64*) arch=arm64 ;; *) arch="${ARCH:-$(uname -m)}" ;; esac case "$(uname -m)" in x86_64|i?86) host=x86 ;; aarch64*) host=arm64 ;; *) host="$(uname -m)" ;; esac [ -z "$cc_arch" ] || [ "$cc_arch" = "$arch" ] || bail "ARCH says $arch but '$CC' builds $cc_arch objects;" \ "the $arch tests would run against the wrong architecture" KLP_TEST_ARCH="$arch" KLP_TEST_PREFLIGHT=done export OBJTOOL CC KLP_TEST_ARCH KLP_TEST_PREFLIGHT cc_version="$($CC --version 2>/dev/null | head -1)" cat < "$workdir/Module.symvers" add_exports vmlinux "$@" } # add_exports [symbol...] # # Append exports owned by one object, without clearing what is already there, # so a test can describe a kernel where several objects export things. # # Which object owns a symbol is not cosmetic: a reference to a vmlinux symbol # is applied when the patch module loads, and a reference to a module's symbol # when that patched module loads, so klp diff files them in different sections. add_exports() { local owner="$1"; shift add_exports_ns "$owner" "" "$@" } # add_exports_ns [symbol...] # # Exports in a symbol namespace, the last field of a Module.symvers line. # # A "module:" namespace is EXPORT_SYMBOL_FOR_MODULES(), where the module # loader grants access by matching the importing module's name against the # list. A livepatch module is never on that list, so such a symbol has to be # referenced the way an unexported one is. Ordinary namespaces are not # special here. add_exports_ns() { local owner="$1" ns="$2"; shift 2 local sym for sym in "$@"; do printf '0x00000000\t%s\t%s\tEXPORT_SYMBOL\t%s\n' \ "$sym" "$owner" "$ns" >> "$workdir/Module.symvers" done } # gcc_only / clang_only gcc_only() { case "$($CC --version 2>/dev/null | head -1)" in *[Gg][Cc][Cc]*) return 0 ;; esac declared_skip "gcc only${1:+: $1}" } clang_only() { case "$($CC --version 2>/dev/null | head -1)" in *clang*) return 0 ;; esac declared_skip "clang only${1:+: $1}" } # build_one [cflags...] build_one() { local fixture out fixture="$FIXTURES_DIR/$1" out="$workdir/$2" shift 2 [ -f "$fixture" ] || fail "missing fixture $fixture" # run_checksum only runs once per workdir. A fresh object has no # checksums in it, so anything built now needs that to happen again. rm -f "$workdir/.checksummed" $CC -c $FIXTURE_CFLAGS "$@" -o "$out" "$fixture" 2>"$workdir/cc.log" || fail "$(basename "$fixture") does not build: $(tail -1 "$workdir/cc.log")" } # build_pair [cflags...] build_pair() { local fixture="$1"; shift # Name what this builds. A test may run several segments, and # make_vmlinux_pair() repoints orig_obj at the image it links, so # without this the next run_diff() would still be reading that. orig_obj=orig.o patched_obj=patched.o build_one "$fixture" orig.o "$@" build_one "$fixture" patched.o "$@" -DPATCHED } # run_objtool_check # # Run objtool's ordinary check pass over the pair, as the kernel build does. # # Some of what klp diff consumes is produced by this pass rather than by the # compiler: .static_call_sites, .mcount_loc, .ibt_endbr_seal, ORC. # # Only module objects see it before klp-build -- with CONFIG_KLP_BUILD the # per-object pass is deferred, so built-in objects reach klp diff exactly as # the compiler left them. run_objtool_check() { local obj # This rewrites both objects, so checksums taken before it describe # something that no longer exists. As in build_one(), drop the marker # so run_checksum() takes them again. rm -f "$workdir/.checksummed" for obj in "$orig_obj" "$patched_obj"; do "$OBJTOOL" "$@" "$workdir/$obj" || fail "objtool $* failed on $obj" done } run_checksum() { # Checksums live in the objects, and a test may ask for them more than # once -- diffing the same pair again with a different Module.symvers, # say. objtool does the right thing when asked twice, leaving the # object alone, but it says so, and that warning would be most of what # a passing run prints. Remember instead, and keep quiet. [ -e "$workdir/.checksummed" ] && return 0 "$OBJTOOL" klp checksum "$workdir/$orig_obj" || fail "klp checksum $orig_obj failed" "$OBJTOOL" klp checksum "$workdir/$patched_obj" || fail "klp checksum $patched_obj failed" touch "$workdir/.checksummed" } # run_diff [expected exit status] run_diff() { local expect="${1:-0}" rc=0 run_checksum # klp diff looks for Module.symvers relative to the working directory. ( cd "$workdir" && "$OBJTOOL" klp diff "$orig_obj" "$patched_obj" out.o ) \ > "$workdir/diff.log" 2>&1 || rc=$? [ "$rc" = "$expect" ] || fail "klp diff exited $rc, expected $expect: $(tail -2 "$workdir/diff.log")" } cc_supports() { echo 'int f(void) { return 0; }' > "$workdir/flagtest.c" $CC $1 -c "$workdir/flagtest.c" -o "$workdir/flagtest.o" 2>/dev/null } # partial_link # # "ld -r" through the compiler driver so the link targets the same # architecture as the objects. partial_link() { local out="$1"; shift rm -f "$workdir/.checksummed" $CC -r -nostdlib -o "$out" "$@" 2>/dev/null || $CC -r -nostdlib -fuse-ld=lld -o "$out" "$@" 2>/dev/null } # link_vmlinux # # Link objects into an executable, the way the kernel's final link produces # vmlinux from vmlinux.o. Entry point 0 and no libc: nothing runs it, it only # has to be a linked image with resolved addresses. # # The sub-sections have to come out in name order rather than object order, # the way the kernel's linker script gathers .text.unlikely and .data.. apart # from the rest. That reordering is the entire reason .klp.symid exists: a # link which preserves order cannot tell a correct sympos from one that merely # counted, and the caller checks the two orders really did diverge. # # A linker script rather than --sort-section=name, because lld accepts that # option and ignores it -- so on a host where only lld can link the target, the # test would quietly stop testing the thing it is named for. # # Three attempts because a cross run has neither $LD nor the compiler's default # linker able to touch the target: on an arm64 host linking x86 objects, only # lld will do it. link_vmlinux() { local out="$1" lds="$workdir/sort.lds"; shift echo 'SECTIONS { .data : { *(SORT_BY_NAME(.data.*)) } }' > "$lds" $LD -e 0 -T "$lds" -o "$out" "$@" 2>/dev/null || $CC -nostdlib -Wl,-e,0 -Wl,-T,"$lds" \ -o "$out" "$@" 2>/dev/null || $CC -nostdlib -fuse-ld=lld -Wl,-e,0 -Wl,-T,"$lds" \ -o "$out" "$@" 2>/dev/null } # make_vmlinux_pair -- # # Build the vmlinux.o / vmlinux pair klp diff needs to resolve sympos the way # it does for built-in code, and point the diff at it. # # For a module, sympos is a count in symbol table order, which klp diff can do # from the object alone. vmlinux is different: the final link reorders # sub-sections, so the position comes from the linked image, bridged by # .klp.symid. klp diff only looks for that when the object it was handed is # called vmlinux.o and a vmlinux sits beside it -- so both the name and the # linked image matter. make_vmlinux_pair() { local orig=() patched=() seen= arg for arg in "$@"; do if [ "$arg" = -- ]; then seen=y; continue; fi if [ -n "$seen" ]; then patched+=( "$arg" ); else orig+=( "$arg" ); fi done # Both sides have to have been named. Without this, forgetting the -- # leaves one list empty, the link of nothing fails, and the test skips # saying the toolchain cannot link -- which is a test bug wearing the # costume of an environment one. [ "${#orig[@]}" -gt 0 ] && [ "${#patched[@]}" -gt 0 ] || fail "make_vmlinux_pair needs objects either side of --" partial_link "$workdir/vmlinux.o" "${orig[@]}" || probe_skip "partial link unavailable" partial_link "$workdir/patched.o" "${patched[@]}" || probe_skip "partial link unavailable" "$OBJTOOL" --klp-symids --link "$workdir/vmlinux.o" || fail "objtool --klp-symids failed" link_vmlinux "$workdir/vmlinux" "$workdir/vmlinux.o" || probe_skip "cannot link a vmlinux here" orig_obj=vmlinux.o } # build_module_pair [cflags...] # # Build the pair as objects belonging to a module rather than to vmlinux. klp # diff reads the object's module name from .modinfo, and that decides which # object a relocation is attributed to and whether a reference counts as # cross-module, so a good deal of the code has a module path the vmlinux # fixtures never reach. # # The fixture defines its .modinfo name from MODNAME. Passing that through # -D needs two levels of quoting, which is easy to get wrong at the call site. build_module_pair() { local fixture="$1" modname="$2"; shift 2 build_pair "$fixture" -DMODNAME="\"$modname\"" "$@" } # find_thinlto_toolchain # # Set $THIN_LD to an lld from the same LLVM release as $CC (or THIN_CC). A # mismatched pair fails with "Invalid summary version", which reads like a # broken test rather than a broken environment. # # ThinLTO is clang-only; callers must use clang_only before calling this. # Only $CC (or an explicit THIN_CC override) is consulted -- the harness does # not search for a second compiler beside a gcc $CC. find_thinlto_toolchain() { local cc ver ld for cc in "${THIN_CC:-}" "$CC"; do [ -n "$cc" ] || continue command -v "${cc%% *}" >/dev/null 2>&1 || return 1 ver=$($cc -dumpversion 2>/dev/null | cut -d. -f1) for ld in "${THIN_LD:-}" "ld.lld-$ver" ld.lld; do [ -n "$ld" ] || continue command -v "$ld" >/dev/null 2>&1 || continue echo 'int probe(void) { return 0; }' > "$workdir/probe.c" $cc -flto=thin -O2 -c "$workdir/probe.c" \ -o "$workdir/probe.o" 2>/dev/null || continue "$ld" -r "$workdir/probe.o" -o "$workdir/probe.elf" \ 2>/dev/null || continue THIN_CC="$cc" THIN_LD="$ld" return 0 done done return 1 } out_sections() { $READELF -S -W "$workdir/out.o" 2>/dev/null; } out_relocs() { $READELF -r -W "$workdir/out.o" 2>/dev/null; } out_symbols() { $READELF -s -W "$workdir/out.o" 2>/dev/null; } diff_log() { cat "$workdir/diff.log"; } # out_strings
# # The strings in one section of the output, for the names livepatch matches on. out_strings() { $READELF -p "$1" "$workdir/out.o" 2>/dev/null; } # Checks on the input objects, to run before klp diff. The two forms differ in # what an absent construct means: # # require_* the compiler cannot produce it here -> skip # assert_* the fixture is supposed to produce it -> fail in_sections() { $READELF -S -W "$workdir/$1" 2>/dev/null; } in_symbols() { $READELF -s -W "$workdir/$1" 2>/dev/null; } in_relocs() { $READELF -r -W "$workdir/$1" 2>/dev/null; } # count_input_symbols # # How many object symbols of exactly that name the input has. Deliberately not # a grep: readelf lists section symbols too, and a newer binutils prints their # name -- ".data." -- where an older one leaves the column blank. A dot # is not a word character, so "grep -w " counts that line as well, and # the same object gives a different answer depending on which readelf reads it. count_input_symbols() { in_symbols "$1" | awk -v n="$2" '$4 == "OBJECT" && $8 == n' | wc -l } # re_quote # # A string as a literal basic regular expression. Nearly every name these # assertions match on contains a dot -- .text.target, .klp.rela.vmlinux -- and # an unescaped dot matches any character, so an assertion for one section can be # satisfied by a different one whose name merely lines up. re_quote() { printf '%s' "$1" | sed 's|[].[^$*\\/]|\\&|g'; } has_input_section() { in_sections "$1" | grep -q "[[:space:]]$(re_quote "$2")[[:space:]]"; } has_input_symbol() { in_symbols "$1" | awk -v n="$2" '$NF == n' | grep -q .; } assert_input_section() { local obj for obj in "$orig_obj" "$patched_obj"; do has_input_section "$obj" "$1" || fail "fixture produced no section '$1' in $obj" done } assert_input_symbol() { local obj for obj in "$orig_obj" "$patched_obj"; do has_input_symbol "$obj" "$1" || fail "fixture produced no symbol '$1' in $obj" done } require_input_section() { local obj for obj in "$orig_obj" "$patched_obj"; do has_input_section "$obj" "$1" || probe_skip "compiler produced no section '$1' here" done } assert_section() { out_sections | grep -q "[[:space:]]$(re_quote "$1")[[:space:]]" || fail "expected section '$1' in output" } assert_no_section() { out_sections | grep -q "[[:space:]]$(re_quote "$1")[[:space:]]" && fail "unexpected section '$1' in output" return 0 } assert_patched() { assert_section ".text.$1" } assert_not_patched() { out_sections | grep -q "[[:space:]]$(re_quote ".text.$1")[[:space:]]" && fail "function '$1' should not have been cloned" return 0 } # section_relocs
# # The relocations against one section. readelf prints every relocation section # in turn, so a test asking about ".smp_locks" has to cut its block out of the # listing first. section_relocs() { local sec="${1//./\\.}" out_relocs | awk "/rela$sec'/,/^\$/" } assert_reloc_sym() { section_relocs "$1" | awk -v n="$2" '$5 == n' | grep -q . || fail "expected a relocation to '$2' in '$1'" } assert_no_reloc_sym() { section_relocs "$1" | awk -v n="$2" '$5 == n' | grep -q . && fail "unexpected relocation to '$2' in '$1'" return 0 } # assert_reloc_count
# # Counts relocation entries, not header or blank lines: whether a special # section entry was extracted once, twice or not at all is usually the whole # question. # # A count of zero is ambiguous on its own -- a section with no relocations and # no section at all both read as zero -- so require the section to exist. A # test expecting nothing there wants assert_no_section. assert_reloc_count() { local n assert_section "$1" n="$(section_relocs "$1" | grep -cE '^[0-9a-f]{8,}')" [ "$n" = "$2" ] || fail "expected $2 relocations in '$1', found $n" } # assert_klp_sym [object] # # A klp symbol is named .klp.sym..,. The object # defaults to any, since most tests care that the reference was converted at # all rather than which object it resolved against. assert_klp_sym() { out_symbols | grep -q "\.klp\.sym\.${2:-[^.]*}\.$(re_quote "$1")," || fail "expected klp symbol for '$1'" } # assert_klp_sympos # # The number after the comma in .klp.sym.., says which # of several same-named symbols livepatch should resolve to, counting from 1; # 0 means the name is unique and no disambiguation is needed. Resolving to the # wrong one is not a load failure, it is a patch quietly wired to the wrong # object. assert_klp_sympos() { out_symbols | grep -qE "\.klp\.sym\.[^.]+\.$(re_quote "$1"),$2([[:space:]]|\$)" || fail "expected klp symbol for '$1' with sympos $2, found:$( out_symbols | grep -o "\.klp\.sym\.[^.]*\.$(re_quote "$1"),[0-9]*" | sort -u | tr '\n' ' ')" } assert_no_klp_sym() { out_symbols | grep -q "\.klp\.sym\.${2:-[^.]*}\.$(re_quote "$1")," && fail "unexpected klp symbol for '$1'" return 0 } assert_tombstone() { out_symbols | grep -qE "\.klp\.tombstone\.$(re_quote "$1")([[:space:]]|\$)" || fail "expected a tombstone for '$1'" } assert_symbol() { out_symbols | awk -v n="$1" '$NF == n' | grep -q . || fail "expected symbol '$1' in output" } assert_no_symbol() { out_symbols | awk -v n="$1" '$NF == n' | grep -q . && fail "unexpected symbol '$1' in output" return 0 } # assert_diff_log # # klp diff's combined output, for tests asserting on a diagnostic. Error # messages are part of the interface when the whole point is that a construct # gets rejected, and a rejection for the wrong reason is not a pass. assert_diff_log() { diff_log | grep -qE -- "$1" || fail "expected '$1' in klp diff output: $(tail -2 "$workdir/diff.log")" } # checksum_of # # The checksum "klp checksum" recorded for one symbol, as a hex string. # # .discard.sym_checksum is an array of { u64 addr; u64 checksum; }, where addr # is the target of a relocation naming the symbol. Nothing in the section # itself says which symbol an entry belongs to, so the relocation is what # locates the entry; the checksum is the eight bytes after it. # Callers use this in a command substitution, where fail() would only exit the # subshell and the test would carry on with an empty checksum. So this returns # non-zero and prints nothing, and the assertions below check for that. For # the same reason it does not run run_checksum() itself: that one does call # fail(), and from in here the message would be captured as the checksum # rather than ending the test. The caller runs it first. checksum_of() { local obj="$workdir/$1" sym="$2" off off="$($READELF -rW "$obj" 2>/dev/null | awk -v s="$sym" '/rela\.discard\.sym_checksum/,/^$/ { if ($5 == s) { print $1; exit } }')" [ -n "$off" ] || return 1 $OBJCOPY -O binary --only-section=.discard.sym_checksum \ "$obj" "$workdir/checksums.bin" 2>/dev/null || return 1 dd if="$workdir/checksums.bin" bs=1 skip=$((16#$off + 8)) count=8 \ status=none | od -An -tx1 | tr -d ' \n' } # assert_checksum_differs / assert_checksum_matches # # Compare what klp checksum recorded for a symbol in the original against the # patched object. This is what decides whether klp diff treats a function as # changed, so a test asserting only that the right functions were cloned cannot # tell a correct checksum from one which happens to differ. checksum_pair() { run_checksum orig_checksum="$(checksum_of "$orig_obj" "$1")" patched_checksum="$(checksum_of "$patched_obj" "$1")" [ -n "$orig_checksum" ] || fail "no checksum recorded for '$1' in $orig_obj" [ -n "$patched_checksum" ] || fail "no checksum recorded for '$1' in $patched_obj" } assert_checksum_differs() { checksum_pair "$1" [ "$orig_checksum" != "$patched_checksum" ] || fail "checksum for '$1' unchanged at $orig_checksum, expected it to differ" } assert_checksum_matches() { checksum_pair "$1" [ "$orig_checksum" = "$patched_checksum" ] || fail "checksum for '$1' changed from $orig_checksum to" \ "$patched_checksum, expected no change" } # run_post_link [expected exit status] # # klp post-link runs last in a livepatch build, converting the intermediate # __klp_relocs.* sections into the .klp.rela.* form the kernel consumes. It # needs nothing but an object containing those sections, which is what klp diff # produces, so it runs on out.o here rather than on a built module. Rewrites # out.o in place, so the out_* helpers show the result afterwards. run_post_link() { local expect="${1:-0}" rc=0 "$OBJTOOL" klp post-link "$workdir/out.o" \ > "$workdir/post-link.log" 2>&1 || rc=$? [ "$rc" = "$expect" ] || fail "klp post-link exited $rc, expected $expect:" \ "$(tail -2 "$workdir/post-link.log")" } # The flags readelf prints for a section, or nothing when it has none. The # leading "[nn]" index is stripped first so the columns can be counted. section_flags() { out_sections | sed 's/^ *\[[ 0-9]*\] *//' | awk -v s="$1" '$1 == s && $7 ~ /^[A-Za-z]+$/ { print $7 }' } # assert_section_flag
# # SHF_RELA_LIVEPATCH is OS-specific, so readelf renders it as "o". A klp rela # section which lost it is an ordinary rela section, which the linker may apply # and the livepatch code will not. assert_section_flag() { local flags; flags="$(section_flags "$1")" [ -n "$flags" ] || fail "section '$1' has no flags, expected '$2'" case "$flags" in *"$2"*) ;; *) fail "section '$1' has flags '$flags', expected '$2'" ;; esac } # assert_klp_rela
# # post-link names the converted sections .klp.rela..
, one per # base section. Also checks SHF_RELA_LIVEPATCH, since the name alone is not # what makes the kernel process it. assert_klp_rela() { local name=".klp.rela.$1.$2" out_sections | grep -q "[[:space:]]$(re_quote "$name")[[:space:]]" || fail "expected section '$name' in output" assert_section_flag "$name" o } # assert_livepatch_sym # # Symbols a klp relocation resolves against live in SHN_LIVEPATCH, which # readelf prints as "OS [0xff20]" -- llvm-readelf without the space, so match # either. The kernel resolves these itself at patch load; anything else is a # symbol the module loader will try, and fail, to resolve normally. assert_livepatch_sym() { out_symbols | grep -E 'OS ?\[0xff20\]' | grep -qE "\.klp\.sym\.[^.]+\.$(re_quote "$1")," || fail "expected a klp symbol for '$1' in SHN_LIVEPATCH" }