Live Patching
 help / color / mirror / Atom feed
From: Joe Lawrence <joe.lawrence@redhat.com>
To: live-patching@vger.kernel.org
Cc: Josh Poimboeuf <jpoimboe@kernel.org>, Song Liu <song@kernel.org>,
	Miroslav Benes <mbenes@suse.cz>, Petr Mladek <pmladek@suse.com>
Subject: [RFC 4/4] livepatch/klp-build: add basic out-of-tree module patching support
Date: Tue, 12 May 2026 18:11:02 -0400	[thread overview]
Message-ID: <20260512221102.2720763-5-joe.lawrence@redhat.com> (raw)
In-Reply-To: <20260512221102.2720763-1-joe.lawrence@redhat.com>

klp-build is currently limited to patching in-tree kernel modules.
Introduce a -M/--module-dir option to enable livepatch generation for
basic out-of-tree (OOT) modules.  This requires the associated kernel
tree to be pre-configured (e.g., 'make modules_prepare').

The OOT workflow is as follows:

  cd /path/to/kernel
  ./scripts/livepatch/klp-build -M /path/to/mymodule my-fix.patch

With this option, klp-build performs two builds (original and patched)
of the OOT module via 'make M=...' instead of a full kernel rebuild.
The resulting objects are then processed and diffed to produce the
final livepatch .ko.

While this enhancement does not yet cover every OOT scenario, it
provides a functional baseline and enables OOT unit-testing for
the 'objtool klp diff' command.

Current limitations include:
 - Separate build directories (make O=) are not yet supported.
 - No passthrough for extra Kbuild variables (e.g., EXTRA_CFLAGS or
   specific CONFIG_* overrides like DKMS supports).
 - OOT module source must be contained within a single directory.
   Multi-directory layouts are not handled.

Assisted-by: Cursor:claude-4.6-opus
Signed-off-by: Joe Lawrence <joe.lawrence@redhat.com>
---
 scripts/livepatch/klp-build | 90 ++++++++++++++++++++++++++++---------
 1 file changed, 69 insertions(+), 21 deletions(-)

diff --git a/scripts/livepatch/klp-build b/scripts/livepatch/klp-build
index 10145b1dd089..db4da64f2b9f 100755
--- a/scripts/livepatch/klp-build
+++ b/scripts/livepatch/klp-build
@@ -21,6 +21,7 @@ shopt -s lastpipe
 
 unset DEBUG_CLONE DIFF_CHECKSUM SKIP_CLEANUP VERBOSE XTRACE
 
+MODULE_DIR=""
 REPLACE=1
 SHORT_CIRCUIT=0
 JOBS="$(getconf _NPROCESSORS_ONLN)"
@@ -137,6 +138,7 @@ Options:
 
 Advanced Options:
    -d, --debug			Show symbol/reloc cloning decisions
+   -M, --module-dir=<DIR>	Out-of-tree module source directory
    -S, --short-circuit=STEP	Start at build step (requires prior --keep-tmp)
 				   1|orig		Build original kernel (default)
 				   2|patched		Build patched kernel
@@ -159,8 +161,8 @@ process_args() {
 	local args
 	local patch
 
-	short="hfj:o:vdS:T"
-	long="help,show-first-changed,jobs:,output:,no-replace,verbose,debug,short-circuit:,keep-tmp"
+	short="hfj:M:o:vdS:T"
+	long="help,show-first-changed,jobs:,module-dir:,output:,no-replace,verbose,debug,short-circuit:,keep-tmp"
 
 	args=$(getopt --options "$short" --longoptions "$long" -- "$@") || {
 		echo; usage; exit
@@ -202,6 +204,10 @@ process_args() {
 				keep_tmp=1
 				shift
 				;;
+			-M | --module-dir)
+				MODULE_DIR="$2"
+				shift 2
+				;;
 			-S | --short-circuit)
 				[[ ! -d "$TMP_DIR" ]] && die "--short-circuit requires preserved klp-tmp dir"
 				keep_tmp=1
@@ -361,11 +367,21 @@ check_unsupported_patches() {
 		get_patch_files "$patch" | mapfile -t files
 
 		for file in "${files[@]}"; do
+			# In and out-of-tree paths to reject
 			case "$file" in
-				lib/*|*/vdso/*|*/realmode/rm/*|*.S)
+				*.S)
 					die "${patch}: unsupported patch to $file"
 					;;
 			esac
+
+			# In-tree paths to reject (based on naming convention)
+			if [[ -z "$MODULE_DIR" ]]; then
+				case "$file" in
+					lib/*|*/vdso/*|*/realmode/rm/*)
+						die "${patch}: unsupported patch to $file"
+						;;
+				esac
+			fi
 		done
 	done
 }
@@ -374,13 +390,14 @@ apply_patch() {
 	local patch="$1"
 	shift
 	local extra_args=("$@")
+	local patch_target="${MODULE_DIR:-$PWD}"
 	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=$?
+	output=$(patch -d "$patch_target" -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"
@@ -390,14 +407,15 @@ apply_patch() {
 	fi
 
 	APPLIED_PATCHES+=("$patch")
-	patch -p1 --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" --silent < "$patch"
+	patch -d "$patch_target" -p1 --no-backup-if-mismatch -r /dev/null "${extra_args[@]}" --silent < "$patch"
 }
 
 revert_patch() {
 	local patch="$1"
+	local patch_target="${MODULE_DIR:-$PWD}"
 	local tmp=()
 
-	patch -p1 -R --force --no-backup-if-mismatch -r /dev/null &> /dev/null < "$patch" || true
+	patch -d "$patch_target" -p1 -R --force --no-backup-if-mismatch -r /dev/null &> /dev/null < "$patch" || true
 
 	for p in "${APPLIED_PATCHES[@]}"; do
 		[[ "$p" == "$patch" ]] && continue
@@ -452,8 +470,6 @@ cross_compile_init() {
 }
 
 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
@@ -470,6 +486,15 @@ do_init() {
 		[[ -f "$DIFF_DIR/.complete" ]] || die "-S $SHORT_CIRCUIT requires completed $DIFF_DIR"
 	fi
 
+	if [[ -n "$MODULE_DIR" ]]; then
+		[[ -d "$MODULE_DIR" ]] || die "module directory not found: $MODULE_DIR"
+		MODULE_DIR="$(realpath "$MODULE_DIR")"
+		[[ -f "$MODULE_DIR/Kbuild" || -f "$MODULE_DIR/Makefile" ]] ||
+			die "no Kbuild or Makefile in $MODULE_DIR"
+		[[ -f "$PWD/Module.symvers" ]] ||
+			die "kernel must be built first (no Module.symvers in $PWD)"
+	fi
+
 	(( SHORT_CIRCUIT <= 1 )) && rm -rf "$TMP_DIR"
 	mkdir -p "$TMP_DIR"
 
@@ -488,6 +513,7 @@ do_init() {
 refresh_patch() {
 	local patch="$1"
 	local tmpdir="$PATCH_TMP_DIR"
+	local patch_target="${MODULE_DIR:-$PWD}"
 	local input_files=()
 	local output_files=()
 
@@ -500,11 +526,11 @@ refresh_patch() {
 	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"
+	( cd "$patch_target" && 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"
+	( cd "$patch_target" && echo "${output_files[@]}" | xargs cp --parents --target-directory="$tmpdir/b" )
 	revert_patch "$patch"
 
 	# Diff 'a' and 'b' to make a clean patch
@@ -546,6 +572,9 @@ clean_kernel() {
 	cmd=("make")
 	cmd+=("--silent")
 	cmd+=("-j$JOBS")
+	if [[ -n "$MODULE_DIR" ]]; then
+		cmd+=("M=$MODULE_DIR")
+	fi
 	cmd+=("clean")
 
 	"${cmd[@]}"
@@ -582,7 +611,11 @@ build_kernel() {
 	fi
 	cmd+=("-j$JOBS")
 	cmd+=("KCFLAGS=-ffunction-sections -fdata-sections")
-	cmd+=("vmlinux")
+	if [[ -n "$MODULE_DIR" ]]; then
+		cmd+=("M=$MODULE_DIR")
+	else
+		cmd+=("vmlinux")
+	fi
 	cmd+=("modules")
 
 	"${cmd[@]}"							\
@@ -594,12 +627,19 @@ build_kernel() {
 find_objects() {
 	local opts=("$@")
 
-	# Find root-level vmlinux.o and non-root-level .ko files,
-	# excluding klp-tmp/ and .git/
-	find "$PWD" \( -path "$TMP_DIR" -o -path "$PWD/.git" -o -regex "$PWD/[^/][^/]*\.ko" \) -prune -o \
-		    -type f "${opts[@]}"				\
-		    \( -name "*.ko" -o -path "$PWD/vmlinux.o" \)	\
-		    -printf '%P\n'
+	if [[ -n "$MODULE_DIR" ]]; then
+		# OOT: find .ko at any depth under the module dir
+		find "$MODULE_DIR" -path "$MODULE_DIR/.git" -prune -o \
+			-type f "${opts[@]}" \
+			-name "*.ko" -printf '%P\n'
+	else
+		# In-tree: find root-level vmlinux.o and non-root-level .ko files,
+		# excluding klp-tmp/ and .git/
+		find "$PWD" \( -path "$TMP_DIR" -o -path "$PWD/.git" -o	-regex "$PWD/[^/][^/]*\.ko" \) -prune -o \
+			    -type f "${opts[@]}"				\
+			    \( -name "*.ko" -o -path "$PWD/vmlinux.o" \)	\
+			    -printf '%P\n'
+	fi
 }
 
 # Copy all .o archives to $ORIG_DIR
@@ -611,10 +651,11 @@ copy_orig_objects() {
 
 	find_objects | mapfile -t files
 
+	local obj_root="${MODULE_DIR:-$PWD}"
 	xtrace_save "copying original objects"
 	for _file in "${files[@]}"; do
 		local rel_file="${_file/.ko/.o}"
-		local file="$PWD/$rel_file"
+		local file="$obj_root/$rel_file"
 		local orig_file="$ORIG_DIR/$rel_file"
 		local orig_dir="$(dirname "$orig_file")"
 
@@ -645,10 +686,11 @@ copy_patched_objects() {
 
 	find_objects "${opts[@]}" | mapfile -t files
 
+	local obj_root="${MODULE_DIR:-$PWD}"
 	xtrace_save "copying changed objects"
 	for _file in "${files[@]}"; do
 		local rel_file="${_file/.ko/.o}"
-		local file="$PWD/$rel_file"
+		local file="$obj_root/$rel_file"
 		local orig_file="$ORIG_DIR/$rel_file"
 		local patched_file="$PATCHED_DIR/$rel_file"
 		local patched_dir="$(dirname "$patched_file")"
@@ -727,6 +769,9 @@ diff_objects() {
 		cmd+=("klp")
 		cmd+=("diff")
 		(( ${#opts[@]} > 0 )) && cmd+=("${opts[@]}")
+
+		[[ -n "$MODULE_DIR" ]] && cmd+=("--symvers" "$PWD/Module.symvers")
+
 		cmd+=("$orig_file")
 		cmd+=("$patched_file")
 		cmd+=("$out_file")
@@ -905,13 +950,16 @@ build_patch_module() {
 process_args "$@"
 do_init
 
+BUILD_TARGET="kernel"
+[[ -n "$MODULE_DIR" ]] && BUILD_TARGET="module ${MODULE_DIR##*/}"
+
 if (( SHORT_CIRCUIT <= 2 )); then
 	status "Validating patch(es)"
 	validate_patches
 fi
 
 if (( SHORT_CIRCUIT <= 1 )); then
-	status "Building original kernel"
+	status "Building original $BUILD_TARGET"
 	clean_kernel
 	build_kernel "original"
 	status "Copying original object files"
@@ -922,7 +970,7 @@ if (( SHORT_CIRCUIT <= 2 )); then
 	status "Fixing patch(es)"
 	fix_patches
 	apply_patches "--silent"
-	status "Building patched kernel"
+	status "Building patched $BUILD_TARGET"
 	build_kernel "patched"
 	revert_patches
 	status "Copying patched object files"
-- 
2.53.0


  parent reply	other threads:[~2026-05-12 22:11 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-12 22:10 [RFC 0/4] klp-build: simple OOT module support Joe Lawrence
2026-05-12 22:10 ` [RFC 1/4] objtool/klp: add --symvers option to klp diff Joe Lawrence
2026-05-12 22:11 ` [RFC 2/4] objtool/klp: allow special section entry size overrides Joe Lawrence
2026-05-12 22:11 ` [RFC 3/4] objtool/klp: add --arch option to display target architecture Joe Lawrence
2026-05-12 22:11 ` Joe Lawrence [this message]
2026-05-12 23:30 ` [RFC 0/4] klp-build: simple OOT module support Song Liu

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260512221102.2720763-5-joe.lawrence@redhat.com \
    --to=joe.lawrence@redhat.com \
    --cc=jpoimboe@kernel.org \
    --cc=live-patching@vger.kernel.org \
    --cc=mbenes@suse.cz \
    --cc=pmladek@suse.com \
    --cc=song@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox