Files
linux/scripts/livepatch/klp-build
Josh Poimboeuf 15fa203ef9 objtool/klp: Fix symbol resolution for duplicate data symbols
find_sympos() calculates a sympos used by livepatch to disambiguate
duplicately-named symbols.  For function symbols, there's a hack which
counts .text.unlikely symbols before other .text symbols, matching the
linker script's section ordering.

Not only is the hack fragile, data symbols can have the same problem.
So for example, adding a reference to pwq_cache in
ep_unregister_pollwait() can trigger a corrupt sympos and a relocation
to the wrong pwq_cache symbol in the livepatch module, resulting in a
crash or undefined behavior.

Remove the existing hack in favor of a fully deterministic solution,
using the new .klp.symid table to derive the symbol-to-id mapping from
the original vmlinux.o and the id-to-address mapping from the
corresponding vmlinux, which can then be used to determine the exact
sympos associated with the original vmlinux.

Modules don't need any special treatment: the .ko has the same
section/symbol ordering as the original whole-archive symbol table.

Fixes: dd590d4d57 ("objtool/klp: Introduce klp diff subcommand for diffing object files")
Reported-by: Ben Procknow <bprockno@redhat.com>
Reported-by: Joe Lawrence <joe.lawrence@redhat.com>
Signed-off-by: Josh Poimboeuf <jpoimboe@kernel.org>
Signed-off-by: Ingo Molnar <mingo@kernel.org>
Cc: live-patching@vger.kernel.org
Link: https://lore.kernel.org/20260710153042.3156788-1-joe.lawrence@redhat.com
Link: https://lore.kernel.org/20260724221730.3126529-1-joe.lawrence@redhat.com
Link: https://patch.msgid.link/919785e3bf2245db02ff6391e735d9cb139170b1.1785727106.git.jpoimboe@kernel.org
2026-08-03 07:12:39 +02:00

944 lines
22 KiB
Bash
Executable File

#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Build a livepatch module
# shellcheck disable=SC1090,SC2155,SC2164
if (( BASH_VERSINFO[0] < 4 || \
(BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
echo "error: this script requires bash 4.4+" >&2
exit 1
fi
set -o errtrace
set -o pipefail
set -o nounset
# Allow doing 'cmd | mapfile -t array' instead of 'mapfile -t array < <(cmd)'.
# This helps keep execution in pipes so pipefail+ERR trap can catch errors.
shopt -s lastpipe
unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP VERBOSE XTRACE
REPLACE=1
SHORT_CIRCUIT=0
JOBS="$(getconf _NPROCESSORS_ONLN)"
shopt -o xtrace | grep -q 'on' && XTRACE=1
# Avoid removing the previous $TMP_DIR until args have been fully processed.
KEEP_TMP=1
SCRIPT="$(basename "$0")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIX_PATCH_LINES="$SCRIPT_DIR/fix-patch-lines"
OBJTOOL="$PWD/tools/objtool/objtool"
CONFIG="$PWD/.config"
TMP_DIR="$PWD/klp-tmp"
ORIG_DIR="$TMP_DIR/1-orig"
PATCHED_DIR="$TMP_DIR/2-patched"
ORIG_CSUM_DIR="$TMP_DIR/3-checksum-orig"
PATCHED_CSUM_DIR="$TMP_DIR/3-checksum-patched"
DIFF_DIR="$TMP_DIR/4-diff"
KMOD_DIR="$TMP_DIR/5-kmod"
STASH_DIR="$TMP_DIR/stash"
TIMESTAMP="$TMP_DIR/timestamp"
PATCH_TMP_DIR="$TMP_DIR/tmp"
KLP_DIFF_LOG="$DIFF_DIR/diff.log"
# Terminal output colors
read -r COLOR_RESET COLOR_BOLD COLOR_ERROR COLOR_WARN <<< ""
if [[ -t 1 && -t 2 ]]; then
COLOR_RESET="\033[0m"
COLOR_BOLD="\033[1m"
COLOR_ERROR="\033[0;31m"
COLOR_WARN="\033[0;33m"
fi
grep0() {
# shellcheck disable=SC2317
command grep "$@" || true
}
# Because pipefail is enabled, the grep0 helper should be used instead of
# grep, otherwise a failed match can propagate to an error.
grep() {
echo "error: $SCRIPT: use grep0 or 'command grep' instead of bare grep" >&2
exit 1
}
status() {
echo -e "${COLOR_BOLD}$*${COLOR_RESET}"
}
warn() {
echo -e "${COLOR_WARN}warning${COLOR_RESET}: $SCRIPT: $*" >&2
}
die() {
echo -e "${COLOR_ERROR}error${COLOR_RESET}: $SCRIPT: $*" >&2
exit 1
}
declare -a STASHED_FILES
stash_file() {
local file="$1"
local rel_file="${file#"$PWD"/}"
[[ ! -e "$file" ]] && die "no file to stash: $file"
mkdir -p "$STASH_DIR/$(dirname "$rel_file")"
cp -f "$file" "$STASH_DIR/$rel_file"
STASHED_FILES+=("$rel_file")
}
restore_files() {
local file
for file in "${STASHED_FILES[@]}"; do
mv -f "$STASH_DIR/$file" "$PWD/$file" || warn "can't restore file: $file"
done
STASHED_FILES=()
}
cleanup() {
set +o nounset
revert_patches
restore_files
[[ "$KEEP_TMP" -eq 0 ]] && rm -rf "$TMP_DIR"
return 0
}
trap_err() {
die "line ${BASH_LINENO[0]}: '$BASH_COMMAND'"
}
trap cleanup EXIT INT TERM HUP
trap trap_err ERR
__usage() {
cat <<EOF
Usage: $SCRIPT [OPTIONS] PATCH_FILE(s)
Generate a livepatch module.
Options:
-f, --show-first-changed Show address of first changed instruction
-j, --jobs=<jobs> Build jobs to run simultaneously [default: $JOBS]
-o, --output=<file.ko> Output file [default: livepatch-<patch-name>.ko]
--no-replace Disable livepatch atomic replace
-v, --verbose Pass V=1 to kernel/module builds
Advanced Options:
-d, --debug Show symbol/reloc cloning decisions
-S, --short-circuit=STEP Start at build step (requires prior --keep-tmp)
1|orig Build original kernel (default)
2|patched Build patched kernel
3|checksum Generate checksums
4|diff Diff objects
5|kmod Build patch module
-T, --keep-tmp Preserve tmp dir on exit
EOF
}
usage() {
__usage >&2
}
process_args() {
local keep_tmp=0
local short
local long
local args
local patch
short="hfj:o:vdS:T"
long="help,show-first-changed,jobs:,output:,no-replace,verbose,debug,short-circuit:,keep-tmp"
args=$(getopt --options "$short" --longoptions "$long" -- "$@") || {
echo; usage; exit
}
eval set -- "$args"
while true; do
case "$1" in
-h | --help)
usage
exit 0
;;
-f | --show-first-changed)
DIFF_CHECKSUM=1
shift
;;
-j | --jobs)
JOBS="$2"
shift 2
;;
-o | --output)
[[ "$2" != *.ko ]] && die "output filename should end with .ko"
OUTFILE="$2"
NAME="$(basename "$OUTFILE")"
NAME="${NAME%.ko}"
NAME="$(module_name_string "$NAME")"
shift 2
;;
--no-replace)
REPLACE=0
shift
;;
-v | --verbose)
VERBOSE=1
shift
;;
-d | --debug)
DEBUG_CLONE=1
keep_tmp=1
shift
;;
-S | --short-circuit)
[[ ! -d "$TMP_DIR" ]] && die "--short-circuit requires preserved klp-tmp dir"
keep_tmp=1
case "$2" in
1 | orig) SHORT_CIRCUIT=1; ;;
2 | patched) SHORT_CIRCUIT=2; ;;
3 | checksum) SHORT_CIRCUIT=3; ;;
4 | diff) SHORT_CIRCUIT=4; ;;
5 | kmod) SHORT_CIRCUIT=5; ;;
*) die "invalid short-circuit step '$2'" ;;
esac
shift 2
;;
-T | --keep-tmp)
keep_tmp=1
shift
;;
--)
shift
break
;;
*)
usage
exit 1
;;
esac
done
if [[ $# -eq 0 ]] && (( SHORT_CIRCUIT <= 2 )); then
usage
exit 1
fi
KEEP_TMP="$keep_tmp"
PATCHES=("$@")
for patch in "${PATCHES[@]}"; do
[[ -f "$patch" ]] || die "$patch doesn't exist"
done
}
# temporarily disable xtrace for especially verbose code
xtrace_save() {
[[ -v XTRACE ]] && set +x
return 0
}
xtrace_restore() {
[[ -v XTRACE ]] && set -x
return 0
}
validate_config() {
xtrace_save "reading .config"
source "$CONFIG" || die "no .config file in $(dirname "$CONFIG")"
xtrace_restore
[[ -v CONFIG_LIVEPATCH ]] || \
die "CONFIG_LIVEPATCH not enabled"
[[ -v CONFIG_KLP_BUILD ]] || \
die "CONFIG_KLP_BUILD not enabled"
[[ -v CONFIG_GCC_PLUGIN_LATENT_ENTROPY ]] && \
die "kernel option 'CONFIG_GCC_PLUGIN_LATENT_ENTROPY' not supported"
[[ -v CONFIG_GCC_PLUGIN_RANDSTRUCT ]] && \
die "kernel option 'CONFIG_GCC_PLUGIN_RANDSTRUCT' not supported"
[[ -v CONFIG_LD_DEAD_CODE_DATA_ELIMINATION ]] && \
die "kernel option 'CONFIG_LD_DEAD_CODE_DATA_ELIMINATION' not supported"
[[ -v CONFIG_AS_IS_LLVM ]] && \
[[ "$CONFIG_AS_VERSION" -lt 200000 ]] && \
die "Clang assembler version < 20 not supported"
[[ -x "$OBJTOOL" ]] && "$OBJTOOL" klp 2>&1 | command grep -q "not implemented" && \
die "objtool not built with KLP support; install xxhash-devel/libxxhash-dev (version >= 0.8) and recompile"
return 0
}
# Only allow alphanumerics and '_' and '-' in the module name. Everything else
# is replaced with '-'. Also truncate to 55 chars so the full name + NUL
# terminator fits in the kernel's 56-byte module name array.
module_name_string() {
echo "${1//[^a-zA-Z0-9_-]/-}" | cut -c 1-55
}
# If the module name wasn't specified on the cmdline with --output, give it a
# name based on the patch name.
set_module_name() {
[[ -v NAME ]] && return 0
if [[ "${#PATCHES[@]}" -eq 1 ]]; then
NAME="$(basename "${PATCHES[0]}")"
NAME="${NAME%.*}"
else
NAME="patch"
fi
NAME="livepatch-$NAME"
NAME="$(module_name_string "$NAME")"
OUTFILE="$NAME.ko"
}
# Hardcode the value printed by the localversion script to prevent patch
# application from appending it with '+' due to a dirty working tree.
set_kernelversion() {
local file="$PWD/scripts/setlocalversion"
local kernelrelease
stash_file "$file"
if [[ -n "$(make -s listnewconfig 2>/dev/null)" ]]; then
die ".config mismatch, check your .config or run 'make olddefconfig'"
fi
make syncconfig &>/dev/null || die "make syncconfig failed"
kernelrelease="$(make -s kernelrelease)"
[[ -z "$kernelrelease" ]] && die "failed to get kernel version"
sed -i "2i echo $kernelrelease; exit 0" scripts/setlocalversion
}
get_patch_input_files() {
local patch="$1"
grep0 -E '^--- ' "$patch" \
| grep0 -v -e '/dev/null' -e '1969-12-31' -e '1970-01-01' \
| gawk '{print $2}' \
| sed 's|^[^/]*/||' \
| sort -u
}
get_patch_output_files() {
local patch="$1"
grep0 -E '^\+\+\+ ' "$patch" \
| grep0 -v -e '/dev/null' -e '1969-12-31' -e '1970-01-01' \
| gawk '{print $2}' \
| sed 's|^[^/]*/||' \
| sort -u
}
get_patch_files() {
local patch="$1"
{ get_patch_input_files "$patch"; get_patch_output_files "$patch"; } \
| sort -u
}
check_unsupported_patches() {
local patch
for patch in "${PATCHES[@]}"; do
local files=()
get_patch_files "$patch" | mapfile -t files
for file in "${files[@]}"; do
case "$file" in
lib/*|*/vdso/*|*/realmode/rm/*|*.S)
die "${patch}: unsupported patch to $file"
;;
esac
done
done
}
apply_patch() {
local patch="$1"
shift
local extra_args=("$@")
local drift_regex="with fuzz|offset [0-9]+ line"
local output
local status
[[ ! -f "$patch" ]] && die "$patch doesn't exist"
status=0
output=$(patch -p1 --dry-run --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" < "$patch" 2>&1) || status=$?
if [[ "$status" -ne 0 ]]; then
echo "$output" >&2
die "$patch did not apply"
elif [[ "$output" =~ $drift_regex ]]; then
[[ -v VERBOSE ]] && echo "$output" >&2
warn "${patch} applied with fuzz"
fi
APPLIED_PATCHES+=("$patch")
patch -p1 --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" --silent < "$patch"
}
revert_patch() {
local patch="$1"
local tmp=()
patch -p1 -R --force --no-backup-if-mismatch -r /dev/null &> /dev/null < "$patch" || true
for p in "${APPLIED_PATCHES[@]}"; do
[[ "$p" == "$patch" ]] && continue
tmp+=("$p")
done
APPLIED_PATCHES=("${tmp[@]}")
}
apply_patches() {
local extra_args=("$@")
local patch
for patch in "${PATCHES[@]}"; do
apply_patch "$patch" "${extra_args[@]}"
done
}
revert_patches() {
local patches=("${APPLIED_PATCHES[@]}")
for (( i=${#patches[@]}-1 ; i>=0 ; i-- )) ; do
revert_patch "${patches[$i]}"
done
APPLIED_PATCHES=()
}
validate_patches() {
check_unsupported_patches
apply_patches
revert_patches
}
do_init() {
# We're not yet smart enough to handle anything other than in-tree
# builds in pwd.
[[ ! "$PWD" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory"
if (( SHORT_CIRCUIT >= 2 )); then
[[ -f "$ORIG_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $ORIG_DIR"
fi
if (( SHORT_CIRCUIT >= 3 )); then
[[ -f "$PATCHED_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $PATCHED_DIR"
fi
if (( SHORT_CIRCUIT >= 4 )); then
[[ -f "$ORIG_CSUM_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $ORIG_CSUM_DIR"
[[ -f "$PATCHED_CSUM_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $PATCHED_CSUM_DIR"
fi
if (( SHORT_CIRCUIT >= 5 )); then
[[ -f "$DIFF_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $DIFF_DIR"
fi
(( SHORT_CIRCUIT <= 1 )) && rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"
APPLIED_PATCHES=()
[[ -x "$FIX_PATCH_LINES" ]] || die "can't find fix-patch-lines"
command -v recountdiff &>/dev/null || die "recountdiff not found (install patchutils)"
validate_config
set_module_name
set_kernelversion
}
# Refresh the patch hunk headers, specifically the line numbers and counts.
refresh_patch() {
local patch="$1"
local tmpdir="$PATCH_TMP_DIR"
local input_files=()
local output_files=()
rm -rf "$tmpdir"
mkdir -p "$tmpdir/a"
mkdir -p "$tmpdir/b"
# Get all source files affected by the patch
get_patch_input_files "$patch" | mapfile -t input_files
get_patch_output_files "$patch" | mapfile -t output_files
# Copy orig source files to 'a'
echo "${input_files[@]}" | xargs cp --parents --target-directory="$tmpdir/a"
# Copy patched source files to 'b'
apply_patch "$patch" "--silent"
echo "${output_files[@]}" | xargs cp --parents --target-directory="$tmpdir/b"
revert_patch "$patch"
# Diff 'a' and 'b' to make a clean patch
( cd "$tmpdir" && diff -Nupr a b > "$patch" ) || true
}
# Copy the patches to a temporary directory, fix their lines so as not to
# affect the __LINE__ macro for otherwise unchanged functions further down the
# file, and update $PATCHES to point to the fixed patches.
fix_patches() {
local idx
local i
rm -f "$TMP_DIR"/*.patch
idx=0001
for i in "${!PATCHES[@]}"; do
local old_patch="${PATCHES[$i]}"
local tmp_patch="$TMP_DIR/tmp.patch"
local patch="${PATCHES[$i]}"
local new_patch
new_patch="$TMP_DIR/$idx-fixed-$(basename "$patch")"
cp -f "$old_patch" "$tmp_patch"
refresh_patch "$tmp_patch"
"$FIX_PATCH_LINES" "$tmp_patch" | recountdiff > "$new_patch"
PATCHES[i]="$new_patch"
rm -f "$tmp_patch"
idx=$(printf "%04d" $(( 10#$idx + 1 )))
done
}
clean_kernel() {
local cmd=()
cmd=("make")
cmd+=("--silent")
cmd+=("-j$JOBS")
cmd+=("clean")
"${cmd[@]}"
}
build_kernel() {
local build="$1"
local log="$TMP_DIR/build.log"
local cmd=()
cmd=("make")
# When a patch to a kernel module references a newly created unexported
# symbol which lives in vmlinux or another kernel module, the patched
# kernel build fails with the following error:
#
# ERROR: modpost: "klp_string" [fs/xfs/xfs.ko] undefined!
#
# The undefined symbols are working as designed in that case. They get
# resolved later when the livepatch module build link pulls all the
# disparate objects together into the same kernel module.
#
# It would be good to have a way to tell modpost to skip checking for
# undefined symbols altogether. For now, just convert the error to a
# warning with KBUILD_MODPOST_WARN, and grep out the warning to avoid
# confusing the user.
#
cmd+=("KBUILD_MODPOST_WARN=1")
cmd+=("KLP_SYMIDS=1")
if [[ -v VERBOSE ]]; then
cmd+=("V=1")
else
cmd+=("-s")
fi
cmd+=("-j$JOBS")
cmd+=("KCFLAGS=-ffunction-sections -fdata-sections")
cmd+=("vmlinux")
cmd+=("modules")
"${cmd[@]}" \
1> >(tee -a "$log") \
2> >(tee -a "$log" | grep0 -v "modpost.*undefined!" >&2) \
|| die "$build kernel build failed"
}
find_objects() {
local opts=("$@")
# Find root-level vmlinux.o and non-root-level .ko files,
# excluding klp-tmp/ and hidden directories.
find "$PWD" -mindepth 1 \
\( -path "$TMP_DIR" -o -name ".*" -o -regex "$PWD/[^/][^/]*\.ko" \) -prune -o \
-type f "${opts[@]}" \
\( -name "*.ko" -o -path "$PWD/vmlinux.o" \) \
-printf '%P\n'
}
# Copy all .o archives to $ORIG_DIR
copy_orig_objects() {
local files=()
rm -rf "$ORIG_DIR"
mkdir -p "$ORIG_DIR"
find_objects | mapfile -t files
xtrace_save "copying original objects"
for _file in "${files[@]}"; do
local rel_file="${_file/.ko/.o}"
local file="$PWD/$rel_file"
local orig_file="$ORIG_DIR/$rel_file"
local orig_dir="$(dirname "$orig_file")"
[[ ! -f "$file" ]] && die "missing $(basename "$file") for $_file"
mkdir -p "$orig_dir"
cp -f "$file" "$orig_dir"
done
xtrace_restore
cp -f "$PWD/vmlinux" "$ORIG_DIR" || die "missing vmlinux"
mv -f "$TMP_DIR/build.log" "$ORIG_DIR"
touch "$TIMESTAMP"
touch "$ORIG_DIR/.complete"
}
# Copy all changed objects to $PATCHED_DIR
copy_patched_objects() {
local files=()
local opts=()
local found=0
rm -rf "$PATCHED_DIR"
mkdir -p "$PATCHED_DIR"
# Note this doesn't work with some configs, thus the 'cmp' below.
opts=("-newer")
opts+=("$TIMESTAMP")
find_objects "${opts[@]}" | mapfile -t files
xtrace_save "copying changed objects"
for _file in "${files[@]}"; do
local rel_file="${_file/.ko/.o}"
local file="$PWD/$rel_file"
local orig_file="$ORIG_DIR/$rel_file"
local patched_file="$PATCHED_DIR/$rel_file"
local patched_dir="$(dirname "$patched_file")"
[[ ! -f "$file" ]] && die "missing $(basename "$file") for $_file"
cmp -s "$orig_file" "$file" && continue
mkdir -p "$patched_dir"
cp -f "$file" "$patched_dir"
found=1
done
xtrace_restore
(( found == 0 )) && die "no changes detected"
mv -f "$TMP_DIR/build.log" "$PATCHED_DIR"
touch "$PATCHED_DIR/.complete"
}
# Copy .o files to a separate directory and run "objtool klp checksum" on each
# copy. The checksums are written to a .discard.sym_checksum section.
#
# If match_dir is given, only process files which also exist there.
generate_checksums() {
local src_dir="$1"
local dest_dir="$2"
local match_dir="${3:-}"
local files=()
local file
rm -rf "$dest_dir"
mkdir -p "$dest_dir"
find "$src_dir" -type f -name "*.o" | mapfile -t files
for file in "${files[@]}"; do
local rel="${file#"$src_dir"/}"
local dest="$dest_dir/$rel"
[[ -n "$match_dir" && ! -f "$match_dir/$rel" ]] && continue
mkdir -p "$(dirname "$dest")"
cp -f "$file" "$dest"
"$OBJTOOL" klp checksum "$dest"
done
[[ -f "$src_dir/vmlinux" ]] && cp -f "$src_dir/vmlinux" "$dest_dir"
touch "$dest_dir/.complete"
}
# Diff changed objects, writing output object to $DIFF_DIR
diff_objects() {
local log="$KLP_DIFF_LOG"
local files=()
local opts=()
rm -rf "$DIFF_DIR"
mkdir -p "$DIFF_DIR"
find "$PATCHED_CSUM_DIR" -type f -name "*.o" | mapfile -t files
[[ ${#files[@]} -eq 0 ]] && die "no changes detected"
[[ -v DEBUG_CLONE ]] && opts=("--debug")
# Diff all changed objects
for file in "${files[@]}"; do
local rel_file="${file#"$PATCHED_CSUM_DIR"/}"
local orig_file="$rel_file"
local patched_file="$PATCHED_CSUM_DIR/$rel_file"
local out_file="$DIFF_DIR/$rel_file"
local filter=()
local cmd=()
mkdir -p "$(dirname "$out_file")"
cmd=("$OBJTOOL")
cmd+=("klp")
cmd+=("diff")
(( ${#opts[@]} > 0 )) && cmd+=("${opts[@]}")
cmd+=("$orig_file")
cmd+=("$patched_file")
cmd+=("$out_file")
if [[ -v DIFF_CHECKSUM ]]; then
filter=("grep0")
filter+=("-Ev")
filter+=("DEBUG: .*checksum: ")
else
filter=("cat")
fi
(
cd "$ORIG_CSUM_DIR"
[[ -v VERBOSE ]] && echo "cd $ORIG_CSUM_DIR && ${cmd[*]}"
"${cmd[@]}" \
1> >(tee -a "$log") \
2> >(tee -a "$log" | "${filter[@]}" >&2) || \
die "objtool klp diff failed"
)
done
touch "$DIFF_DIR/.complete"
}
# For each changed object, run "objtool klp checksum" with --debug-checksum to
# get the per-instruction checksums, and then diff those to find the first
# changed instruction for each function.
diff_checksums() {
local orig_log="$ORIG_DIR/checksum.log"
local patched_log="$PATCHED_DIR/checksum.log"
local -A funcs
local cmd=()
local line
local file
local func
gawk '/\.o: changed function: / {
sub(/:$/, "", $1)
print $1, $NF
}' "$KLP_DIFF_LOG" | mapfile -t lines
for line in "${lines[@]}"; do
read -r file func <<< "$line"
if [[ ! -v funcs["$file"] ]]; then
funcs["$file"]="$func"
else
funcs["$file"]+=" $func"
fi
done
cmd=("$OBJTOOL")
cmd+=("klp" "checksum")
cmd+=("--dry-run")
for file in "${!funcs[@]}"; do
local opt="--debug-checksum=${funcs[$file]// /,}"
(
cd "$ORIG_DIR"
"${cmd[@]}" "$opt" "$file" &> "$orig_log" || \
( cat "$orig_log" >&2; die "objtool klp checksum failed" )
cd "$PATCHED_DIR"
"${cmd[@]}" "$opt" "$file" &> "$patched_log" || \
( cat "$patched_log" >&2; die "objtool klp checksum failed" )
)
for func in ${funcs[$file]}; do
local -a orig patched
paste <(grep0 -E "^DEBUG: .*checksum: $func " "$orig_log") \
<(grep0 -E "^DEBUG: .*checksum: $func " "$patched_log") |
while IFS= read -r line; do
read -ra orig <<< "${line%%$'\t'*}"
read -ra patched <<< "${line#*$'\t'}"
if [[ ${#patched[@]} -eq 0 ]]; then
printf "%s: %s: %s (removed)\n" "${orig[1]%:}" "${orig[3]}" "${orig[-2]}"
break
elif [[ ${#orig[@]} -eq 0 ]]; then
printf "%s: %s: %s (added)\n" "${patched[1]%:}" "${patched[3]}" "${patched[-2]}"
break
fi
[[ "${orig[-1]}" == "${patched[-1]}" ]] && continue
printf "%s: %s: %s" "${orig[1]%:}" "${orig[3]}" "${orig[-2]}"
[[ "${orig[-2]}" != "${patched[-2]}" ]] && \
printf " (patched: %s)" "${patched[-2]}"
printf "\n"
break
done || true
done
done
}
# Build and post-process livepatch module in $KMOD_DIR
build_patch_module() {
local makefile="$KMOD_DIR/Kbuild"
local log="$KMOD_DIR/build.log"
local kmod_file
local cflags=()
local files=()
local cmd=()
rm -rf "$KMOD_DIR"
mkdir -p "$KMOD_DIR"
cp -f "$SCRIPT_DIR/init.c" "$KMOD_DIR"
echo "obj-m := $NAME.o" > "$makefile"
echo -n "$NAME-y := init.o" >> "$makefile"
find "$DIFF_DIR" -type f -name "*.o" | mapfile -t files
[[ ${#files[@]} -eq 0 ]] && die "no changes detected"
for file in "${files[@]}"; do
local rel_file="${file#"$DIFF_DIR"/}"
local orig_file="$ORIG_DIR/$rel_file"
local orig_dir="$(dirname "$orig_file")"
local kmod_file="$KMOD_DIR/$rel_file"
local kmod_dir="$(dirname "$kmod_file")"
local cmd_file="$kmod_dir/.$(basename "$file").cmd"
mkdir -p "$kmod_dir"
cp -f "$file" "$kmod_dir"
# Tell kbuild this is a prebuilt object
cp -f "$file" "${kmod_file}_shipped"
# Make modpost happy
touch "$cmd_file"
echo -n " $rel_file" >> "$makefile"
done
echo >> "$makefile"
cflags=("-ffunction-sections")
cflags+=("-fdata-sections")
[[ $REPLACE -eq 0 ]] && cflags+=("-DKLP_NO_REPLACE")
cmd=("make")
if [[ -v VERBOSE ]]; then
cmd+=("V=1")
else
cmd+=("-s")
fi
cmd+=("-j$JOBS")
cmd+=("--directory=.")
cmd+=("M=$KMOD_DIR")
cmd+=("KCFLAGS=${cflags[*]}")
# Build a "normal" kernel module with init.c and the diffed objects
"${cmd[@]}" \
1> >(tee -a "$log") \
2> >(tee -a "$log" >&2)
kmod_file="$KMOD_DIR/$NAME.ko"
# Save off the intermediate binary for debugging
cp -f "$kmod_file" "$kmod_file.orig"
# Work around issue where slight .config change makes corrupt BTF
objcopy --remove-section=.BTF "$kmod_file"
# Fix (and work around) linker wreckage for klp syms / relocs
"$OBJTOOL" klp post-link "$kmod_file" || die "objtool klp post-link failed"
cp -f "$kmod_file" "$OUTFILE"
}
################################################################################
process_args "$@"
do_init
if (( SHORT_CIRCUIT <= 2 )); then
status "Validating patch(es)"
validate_patches
fi
if (( SHORT_CIRCUIT <= 1 )); then
status "Building original kernel"
clean_kernel
build_kernel "original"
status "Copying original object files"
copy_orig_objects
fi
if (( SHORT_CIRCUIT <= 2 )); then
status "Fixing patch(es)"
fix_patches
apply_patches "--silent"
status "Building patched kernel"
build_kernel "patched"
revert_patches
status "Copying patched object files"
copy_patched_objects
fi
if (( SHORT_CIRCUIT <= 3 )); then
status "Generating original checksums"
generate_checksums "$ORIG_DIR" "$ORIG_CSUM_DIR" "$PATCHED_DIR"
status "Generating patched checksums"
generate_checksums "$PATCHED_DIR" "$PATCHED_CSUM_DIR"
fi
if (( SHORT_CIRCUIT <= 4 )); then
status "Diffing objects"
diff_objects
if [[ -v DIFF_CHECKSUM ]]; then
status "Finding first changed instructions"
diff_checksums
fi
fi
if (( SHORT_CIRCUIT <= 5 )); then
status "Building patch module: $OUTFILE"
build_patch_module
fi
status "SUCCESS"