Files
linux/scripts/livepatch/klp-build
Josh Poimboeuf 78c268f378 livepatch/klp-build: Fix klp-build vs CONFIG_MODULE_SRCVERSION_ALL
When building a patch to a single-file kernel module with
CONFIG_MODULE_SRCVERSION_ALL enabled, the klp-build module link fails in
modpost:

  Diffing objects
  drivers/md/raid0.o: changed function: raid0_run
  Building patch module: livepatch-0001-patch-raid0_run.ko
  drivers/md/raid0.c: No such file or directory
  ...

The problem here is that klp-build copied drivers/md/.raid0.o.cmd to the
module build directory, but it didn't also copy over the input source
file listed in the .cmd file:

  source_drivers/md/raid0.o := drivers/md/raid0.c

So modpost dies due to the missing .c file which is needed for
calculating checksums for CONFIG_MODULE_SRCVERSION_ALL.

Instead of copying the original .cmd file, just create an empty one.
Modpost only requires that it exists.  The original object's build
dependencies are irrelevant for the frankenobjects used by klp-build.

Fixes: 24ebfcd65a ("livepatch/klp-build: Introduce klp-build script for generating livepatch modules")
Reported-by: Song Liu <song@kernel.org>
Tested-by: Song Liu <song@kernel.org>
Link: https://patch.msgid.link/c41b6629e02775e4c1015259aa36065b3fe2f0f3.1769471792.git.jpoimboe@kernel.org
Signed-off-by: Josh Poimboeuf <jpoimboe@kernel.org>
2026-01-27 08:20:51 -08:00

832 lines
18 KiB
Bash
Executable File

#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Build a livepatch module
# shellcheck disable=SC1090,SC2155
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 errexit
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+errexit can catch errors.
shopt -s lastpipe
unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP XTRACE
REPLACE=1
SHORT_CIRCUIT=0
JOBS="$(getconf _NPROCESSORS_ONLN)"
VERBOSE="-s"
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"
SRC="$(pwd)"
OBJ="$(pwd)"
CONFIG="$OBJ/.config"
TMP_DIR="$OBJ/klp-tmp"
ORIG_DIR="$TMP_DIR/orig"
PATCHED_DIR="$TMP_DIR/patched"
DIFF_DIR="$TMP_DIR/diff"
KMOD_DIR="$TMP_DIR/kmod"
STASH_DIR="$TMP_DIR/stash"
TIMESTAMP="$TMP_DIR/timestamp"
PATCH_TMP_DIR="$TMP_DIR/tmp"
KLP_DIFF_LOG="$DIFF_DIR/diff.log"
grep0() {
command grep "$@" || true
}
status() {
echo "$*"
}
warn() {
echo "error: $SCRIPT: $*" >&2
}
die() {
warn "$@"
exit 1
}
declare -a STASHED_FILES
stash_file() {
local file="$1"
local rel_file="${file#"$SRC"/}"
[[ ! -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" "$SRC/$file" || warn "can't restore file: $file"
done
STASHED_FILES=()
}
cleanup() {
set +o nounset
revert_patches "--recount"
restore_files
[[ "$KEEP_TMP" -eq 0 ]] && rm -rf "$TMP_DIR"
return 0
}
trap_err() {
warn "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|diff Diff objects
4|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
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="V=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 | diff) SHORT_CIRCUIT=3; ;;
4 | mod) SHORT_CIRCUIT=4; ;;
*) 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 ]]; then
usage
exit 1
fi
KEEP_TMP="$keep_tmp"
PATCHES=("$@")
}
# 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"
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 git working tree.
set_kernelversion() {
local file="$SRC/scripts/setlocalversion"
local localversion
stash_file "$file"
localversion="$(cd "$SRC" && make --no-print-directory kernelversion)"
localversion="$(cd "$SRC" && KERNELVERSION="$localversion" ./scripts/setlocalversion)"
[[ -z "$localversion" ]] && die "setlocalversion failed"
sed -i "2i echo $localversion; exit 0" scripts/setlocalversion
}
get_patch_files() {
local patch="$1"
grep0 -E '^(--- |\+\+\+ )' "$patch" \
| gawk '{print $2}' \
| sed 's|^[^/]*/||' \
| sort -u
}
# Make sure git re-stats the changed files
git_refresh() {
local patch="$1"
local files=()
[[ ! -e "$SRC/.git" ]] && return
get_patch_files "$patch" | mapfile -t files
(
cd "$SRC"
git update-index -q --refresh -- "${files[@]}"
)
}
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/*|*.S)
die "unsupported patch to $file"
;;
esac
done
done
}
apply_patch() {
local patch="$1"
shift
local extra_args=("$@")
[[ ! -f "$patch" ]] && die "$patch doesn't exist"
(
cd "$SRC"
# The sed strips the version signature from 'git format-patch',
# otherwise 'git apply --recount' warns.
sed -n '/^-- /q;p' "$patch" |
git apply "${extra_args[@]}"
)
APPLIED_PATCHES+=("$patch")
}
revert_patch() {
local patch="$1"
shift
local extra_args=("$@")
local tmp=()
(
cd "$SRC"
sed -n '/^-- /q;p' "$patch" |
git apply --reverse "${extra_args[@]}"
)
git_refresh "$patch"
for p in "${APPLIED_PATCHES[@]}"; do
[[ "$p" == "$patch" ]] && continue
tmp+=("$p")
done
APPLIED_PATCHES=("${tmp[@]}")
}
apply_patches() {
local patch
for patch in "${PATCHES[@]}"; do
apply_patch "$patch"
done
}
revert_patches() {
local extra_args=("$@")
local patches=("${APPLIED_PATCHES[@]}")
for (( i=${#patches[@]}-1 ; i>=0 ; i-- )) ; do
revert_patch "${patches[$i]}" "${extra_args[@]}"
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.
[[ ! "$SRC" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory"
[[ ! "$OBJ" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory"
(( SHORT_CIRCUIT <= 1 )) && rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"
APPLIED_PATCHES=()
[[ -x "$FIX_PATCH_LINES" ]] || die "can't find fix-patch-lines"
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 files=()
rm -rf "$tmpdir"
mkdir -p "$tmpdir/a"
mkdir -p "$tmpdir/b"
# Get all source files affected by the patch
get_patch_files "$patch" | mapfile -t files
# Copy orig source files to 'a'
( cd "$SRC" && echo "${files[@]}" | xargs cp --parents --target-directory="$tmpdir/a" )
# Copy patched source files to 'b'
apply_patch "$patch" --recount
( cd "$SRC" && echo "${files[@]}" | xargs cp --parents --target-directory="$tmpdir/b" )
revert_patch "$patch" --recount
# Diff 'a' and 'b' to make a clean patch
( cd "$tmpdir" && git diff --no-index --no-prefix 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" > "$new_patch"
refresh_patch "$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")
(
cd "$SRC"
"${cmd[@]}"
)
}
build_kernel() {
local log="$TMP_DIR/build.log"
local objtool_args=()
local cmd=()
objtool_args=("--checksum")
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+=("$VERBOSE")
cmd+=("-j$JOBS")
cmd+=("KCFLAGS=-ffunction-sections -fdata-sections")
cmd+=("OBJTOOL_ARGS=${objtool_args[*]}")
cmd+=("vmlinux")
cmd+=("modules")
(
cd "$SRC"
"${cmd[@]}" \
1> >(tee -a "$log") \
2> >(tee -a "$log" | grep0 -v "modpost.*undefined!" >&2)
)
}
find_objects() {
local opts=("$@")
# Find root-level vmlinux.o and non-root-level .ko files,
# excluding klp-tmp/ and .git/
find "$OBJ" \( -path "$TMP_DIR" -o -path "$OBJ/.git" -o -regex "$OBJ/[^/][^/]*\.ko" \) -prune -o \
-type f "${opts[@]}" \
\( -name "*.ko" -o -path "$OBJ/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 orig objects"
for _file in "${files[@]}"; do
local rel_file="${_file/.ko/.o}"
local file="$OBJ/$rel_file"
local file_dir="$(dirname "$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
mv -f "$TMP_DIR/build.log" "$ORIG_DIR"
touch "$TIMESTAMP"
}
# 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="$OBJ/$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"
}
# 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_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_DIR"/}"
local orig_file="$rel_file"
local patched_file="$PATCHED_DIR/$rel_file"
local out_file="$DIFF_DIR/$rel_file"
local filter=()
local cmd=()
mkdir -p "$(dirname "$out_file")"
cmd=("$SRC/tools/objtool/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_DIR"
"${cmd[@]}" \
1> >(tee -a "$log") \
2> >(tee -a "$log" | "${filter[@]}" >&2) || \
die "objtool klp diff failed"
)
done
}
# For each changed object, run objtool 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=("$SRC/tools/objtool/objtool")
cmd+=("--checksum")
cmd+=("--link")
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 --debug-checksum failed" )
cd "$PATCHED_DIR"
"${cmd[@]}" "$opt" "$file" &> "$patched_log" || \
( cat "$patched_log" >&2; die "objtool --debug-checksum failed" )
)
for func in ${funcs[$file]}; do
diff <( grep0 -E "^DEBUG: .*checksum: $func " "$orig_log" | sed "s|$ORIG_DIR/||") \
<( grep0 -E "^DEBUG: .*checksum: $func " "$patched_log" | sed "s|$PATCHED_DIR/||") \
| gawk '/^< DEBUG: / {
gsub(/:/, "")
printf "%s: %s: %s\n", $3, $5, $6
exit
}' || 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 "$SRC/scripts/livepatch/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")
cmd+=("$VERBOSE")
cmd+=("-j$JOBS")
cmd+=("--directory=.")
cmd+=("M=$KMOD_DIR")
cmd+=("KCFLAGS=${cflags[*]}")
# Build a "normal" kernel module with init.c and the diffed objects
(
cd "$SRC"
"${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
"$SRC/tools/objtool/objtool" klp post-link "$kmod_file" || die "objtool klp post-link failed"
cp -f "$kmod_file" "$OUTFILE"
}
################################################################################
process_args "$@"
do_init
if (( SHORT_CIRCUIT <= 1 )); then
status "Validating patch(es)"
validate_patches
status "Building original kernel"
clean_kernel
build_kernel
status "Copying original object files"
copy_orig_objects
fi
if (( SHORT_CIRCUIT <= 2 )); then
status "Fixing patch(es)"
fix_patches
apply_patches
status "Building patched kernel"
build_kernel
revert_patches
status "Copying patched object files"
copy_patched_objects
fi
if (( SHORT_CIRCUIT <= 3 )); then
status "Diffing objects"
diff_objects
if [[ -v DIFF_CHECKSUM ]]; then
status "Finding first changed instructions"
diff_checksums
fi
fi
if (( SHORT_CIRCUIT <= 4 )); then
status "Building patch module: $OUTFILE"
build_patch_module
fi
status "SUCCESS"