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 <[email protected]>
---
 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


Reply via email to