diff options
| -rw-r--r-- | tools/objtool/tests/lib.sh | 364 |
1 files changed, 360 insertions, 4 deletions
diff --git a/tools/objtool/tests/lib.sh b/tools/objtool/tests/lib.sh index a4513ae8dfce..817915e38a35 100644 --- a/tools/objtool/tests/lib.sh +++ b/tools/objtool/tests/lib.sh @@ -216,9 +216,42 @@ setup() export_syms() { : > "$workdir/Module.symvers" + add_exports vmlinux "$@" +} + +# add_exports <object> [symbol...] +# +# Append exports owned by one object, without clearing what is already there, +# so a test can describe a kernel where several objects export things. +# +# Which object owns a symbol is not cosmetic: a reference to a vmlinux symbol +# is applied when the patch module loads, and a reference to a module's symbol +# when that patched module loads, so klp diff files them in different sections. +add_exports() +{ + local owner="$1"; shift + + add_exports_ns "$owner" "" "$@" +} + +# add_exports_ns <object> <namespace> [symbol...] +# +# Exports in a symbol namespace, the last field of a Module.symvers line. +# +# A "module:<names>" namespace is EXPORT_SYMBOL_FOR_MODULES(), where the module +# loader grants access by matching the importing module's name against the +# list. A livepatch module is never on that list, so such a symbol has to be +# referenced the way an unexported one is. Ordinary namespaces are not +# special here. +add_exports_ns() +{ + local owner="$1" ns="$2"; shift 2 + + local sym + for sym in "$@"; do - printf '0x00000000\t%s\tvmlinux\tEXPORT_SYMBOL\t\n' \ - "$sym" >> "$workdir/Module.symvers" + printf '0x00000000\t%s\t%s\tEXPORT_SYMBOL\t%s\n' \ + "$sym" "$owner" "$ns" >> "$workdir/Module.symvers" done } @@ -481,12 +514,87 @@ out_relocs() { $READELF -r -W "$workdir/out.o" 2>/dev/null; } out_symbols() { $READELF -s -W "$workdir/out.o" 2>/dev/null; } diff_log() { cat "$workdir/diff.log"; } +# out_strings <section> +# +# The strings in one section of the output, for the names livepatch matches on. +out_strings() { $READELF -p "$1" "$workdir/out.o" 2>/dev/null; } + +# Checks on the input objects, to run before klp diff. The two forms differ in +# what an absent construct means: +# +# require_* the compiler cannot produce it here -> skip +# assert_* the fixture is supposed to produce it -> fail + +in_sections() { $READELF -S -W "$workdir/$1" 2>/dev/null; } +in_symbols() { $READELF -s -W "$workdir/$1" 2>/dev/null; } +in_relocs() { $READELF -r -W "$workdir/$1" 2>/dev/null; } + +# count_input_symbols <object> <name> +# +# How many object symbols of exactly that name the input has. Deliberately not +# a grep: readelf lists section symbols too, and a newer binutils prints their +# name -- ".data.<name>" -- where an older one leaves the column blank. A dot +# is not a word character, so "grep -w <name>" counts that line as well, and +# the same object gives a different answer depending on which readelf reads it. +count_input_symbols() +{ + in_symbols "$1" | awk -v n="$2" '$4 == "OBJECT" && $8 == n' | wc -l +} + +# re_quote <string> +# +# A string as a literal basic regular expression. Nearly every name these +# assertions match on contains a dot -- .text.target, .klp.rela.vmlinux -- and +# an unescaped dot matches any character, so an assertion for one section can be +# satisfied by a different one whose name merely lines up. +re_quote() { printf '%s' "$1" | sed 's|[].[^$*\\/]|\\&|g'; } + +has_input_section() { in_sections "$1" | grep -q "[[:space:]]$(re_quote "$2")[[:space:]]"; } +has_input_symbol() { in_symbols "$1" | 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:]]$1[[:space:]]" || + out_sections | grep -q "[[:space:]]$(re_quote "$1")[[:space:]]" || fail "expected section '$1' in output" } +assert_no_section() +{ + out_sections | grep -q "[[:space:]]$(re_quote "$1")[[:space:]]" && + fail "unexpected section '$1' in output" + return 0 +} + assert_patched() { assert_section ".text.$1" @@ -494,7 +602,255 @@ assert_patched() assert_not_patched() { - out_sections | grep -q "[[:space:]].text.$1[[:space:]]" && + out_sections | grep -q "[[:space:]]$(re_quote ".text.$1")[[:space:]]" && fail "function '$1' should not have been cloned" return 0 } + +# section_relocs <section> +# +# The relocations against one section. readelf prints every relocation section +# in turn, so a test asking about ".smp_locks" has to cut its block out of the +# listing first. +section_relocs() +{ + 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 <section> <count> +# +# Counts relocation entries, not header or blank lines: whether a special +# section entry was extracted once, twice or not at all is usually the whole +# question. +# +# A count of zero is ambiguous on its own -- a section with no relocations and +# no section at all both read as zero -- so require the section to exist. A +# test expecting nothing there wants assert_no_section. +assert_reloc_count() +{ + 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 <symbol> [object] +# +# A klp symbol is named .klp.sym.<object>.<symbol>,<sympos>. The object +# defaults to any, since most tests care that the reference was converted at +# all rather than which object it resolved against. +assert_klp_sym() +{ + out_symbols | grep -q "\.klp\.sym\.${2:-[^.]*}\.$(re_quote "$1")," || + fail "expected klp symbol for '$1'" +} + +# assert_klp_sympos <symbol> <sympos> +# +# The number after the comma in .klp.sym.<object>.<symbol>,<sympos> says which +# of several same-named symbols livepatch should resolve to, counting from 1; +# 0 means the name is unique and no disambiguation is needed. Resolving to the +# wrong one is not a load failure, it is a patch quietly wired to the wrong +# object. +assert_klp_sympos() +{ + out_symbols | grep -qE "\.klp\.sym\.[^.]+\.$(re_quote "$1"),$2([[:space:]]|\$)" || + fail "expected klp symbol for '$1' with sympos $2, found:$( + out_symbols | grep -o "\.klp\.sym\.[^.]*\.$(re_quote "$1"),[0-9]*" | + sort -u | tr '\n' ' ')" +} + +assert_no_klp_sym() +{ + out_symbols | grep -q "\.klp\.sym\.${2:-[^.]*}\.$(re_quote "$1")," && + fail "unexpected klp symbol for '$1'" + 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 <regex> +# +# klp diff's combined output, for tests asserting on a diagnostic. Error +# messages are part of the interface when the whole point is that a construct +# gets rejected, and a rejection for the wrong reason is not a pass. +assert_diff_log() +{ + diff_log | grep -qE -- "$1" || + fail "expected '$1' in klp diff output: $(tail -2 "$workdir/diff.log")" +} + +# checksum_of <object> <symbol> +# +# The checksum "klp checksum" recorded for one symbol, as a hex string. +# +# .discard.sym_checksum is an array of { u64 addr; u64 checksum; }, where addr +# is the target of a relocation naming the symbol. Nothing in the section +# itself says which symbol an entry belongs to, so the relocation is what +# locates the entry; the checksum is the eight bytes after it. +# Callers use this in a command substitution, where fail() would only exit the +# subshell and the test would carry on with an empty checksum. So this returns +# non-zero and prints nothing, and the assertions below check for that. 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 <symbol> / assert_checksum_matches <symbol> +# +# Compare what klp checksum recorded for a symbol in the original against the +# patched object. This is what decides whether klp diff treats a function as +# changed, so a test asserting only that the right functions were cloned cannot +# tell a correct checksum from one which happens to differ. +checksum_pair() +{ + 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 <section> <letter> +# +# SHF_RELA_LIVEPATCH is OS-specific, so readelf renders it as "o". A klp rela +# section which lost it is an ordinary rela section, which the linker may apply +# and the livepatch code will not. +assert_section_flag() +{ + 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 <object> <section> +# +# post-link names the converted sections .klp.rela.<object>.<section>, one per +# base section. Also checks SHF_RELA_LIVEPATCH, since the name alone is not +# what makes the kernel process it. +assert_klp_rela() +{ + 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 <symbol> +# +# Symbols a klp relocation resolves against live in SHN_LIVEPATCH, which +# readelf prints as "OS [0xff20]" -- llvm-readelf without the space, so match +# either. The kernel resolves these itself at patch load; anything else is a +# symbol the module loader will try, and fail, to resolve normally. +assert_livepatch_sym() +{ + out_symbols | grep -E 'OS ?\[0xff20\]' | + grep -qE "\.klp\.sym\.[^.]+\.$(re_quote "$1")," || + fail "expected a klp symbol for '$1' in SHN_LIVEPATCH" +} |
