1#!/bin/bash 2# SPDX-License-Identifier: GPL-2.0 3# 4# Build a livepatch module 5 6# shellcheck disable=SC1090,SC2155 7 8if (( BASH_VERSINFO[0] < 4 || \ 9 (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then 10 echo "error: this script requires bash 4.4+" >&2 11 exit 1 12fi 13 14set -o errexit 15set -o errtrace 16set -o pipefail 17set -o nounset 18 19# Allow doing 'cmd | mapfile -t array' instead of 'mapfile -t array < <(cmd)'. 20# This helps keep execution in pipes so pipefail+errexit can catch errors. 21shopt -s lastpipe 22 23unset DEBUG_CLONE SKIP_CLEANUP XTRACE 24 25REPLACE=1 26SHORT_CIRCUIT=0 27JOBS="$(getconf _NPROCESSORS_ONLN)" 28VERBOSE="-s" 29shopt -o xtrace | grep -q 'on' && XTRACE=1 30 31# Avoid removing the previous $TMP_DIR until args have been fully processed. 32KEEP_TMP=1 33 34SCRIPT="$(basename "$0")" 35SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 36FIX_PATCH_LINES="$SCRIPT_DIR/fix-patch-lines" 37 38SRC="$(pwd)" 39OBJ="$(pwd)" 40 41CONFIG="$OBJ/.config" 42TMP_DIR="$OBJ/klp-tmp" 43 44ORIG_DIR="$TMP_DIR/orig" 45PATCHED_DIR="$TMP_DIR/patched" 46DIFF_DIR="$TMP_DIR/diff" 47KMOD_DIR="$TMP_DIR/kmod" 48 49STASH_DIR="$TMP_DIR/stash" 50TIMESTAMP="$TMP_DIR/timestamp" 51PATCH_TMP_DIR="$TMP_DIR/tmp" 52 53KLP_DIFF_LOG="$DIFF_DIR/diff.log" 54 55grep0() { 56 command grep "$@" || true 57} 58 59status() { 60 echo "$*" 61} 62 63warn() { 64 echo "error: $SCRIPT: $*" >&2 65} 66 67die() { 68 warn "$@" 69 exit 1 70} 71 72declare -a STASHED_FILES 73 74stash_file() { 75 local file="$1" 76 local rel_file="${file#"$SRC"/}" 77 78 [[ ! -e "$file" ]] && die "no file to stash: $file" 79 80 mkdir -p "$STASH_DIR/$(dirname "$rel_file")" 81 cp -f "$file" "$STASH_DIR/$rel_file" 82 83 STASHED_FILES+=("$rel_file") 84} 85 86restore_files() { 87 local file 88 89 for file in "${STASHED_FILES[@]}"; do 90 mv -f "$STASH_DIR/$file" "$SRC/$file" || warn "can't restore file: $file" 91 done 92 93 STASHED_FILES=() 94} 95 96cleanup() { 97 set +o nounset 98 revert_patches "--recount" 99 restore_files 100 [[ "$KEEP_TMP" -eq 0 ]] && rm -rf "$TMP_DIR" 101 return 0 102} 103 104trap_err() { 105 warn "line ${BASH_LINENO[0]}: '$BASH_COMMAND'" 106} 107 108trap cleanup EXIT INT TERM HUP 109trap trap_err ERR 110 111__usage() { 112 cat <<EOF 113Usage: $SCRIPT [OPTIONS] PATCH_FILE(s) 114Generate a livepatch module. 115 116Options: 117 -j, --jobs=<jobs> Build jobs to run simultaneously [default: $JOBS] 118 -o, --output=<file.ko> Output file [default: livepatch-<patch-name>.ko] 119 --no-replace Disable livepatch atomic replace 120 -v, --verbose Pass V=1 to kernel/module builds 121 122Advanced Options: 123 -d, --debug Show symbol/reloc cloning decisions 124 -S, --short-circuit=STEP Start at build step (requires prior --keep-tmp) 125 1|orig Build original kernel (default) 126 2|patched Build patched kernel 127 3|diff Diff objects 128 4|kmod Build patch module 129 -T, --keep-tmp Preserve tmp dir on exit 130 131EOF 132} 133 134usage() { 135 __usage >&2 136} 137 138process_args() { 139 local keep_tmp=0 140 local short 141 local long 142 local args 143 144 short="hj:o:vdS:T" 145 long="help,jobs:,output:,no-replace,verbose,debug,short-circuit:,keep-tmp" 146 147 args=$(getopt --options "$short" --longoptions "$long" -- "$@") || { 148 echo; usage; exit 149 } 150 eval set -- "$args" 151 152 while true; do 153 case "$1" in 154 -h | --help) 155 usage 156 exit 0 157 ;; 158 -j | --jobs) 159 JOBS="$2" 160 shift 2 161 ;; 162 -o | --output) 163 [[ "$2" != *.ko ]] && die "output filename should end with .ko" 164 OUTFILE="$2" 165 NAME="$(basename "$OUTFILE")" 166 NAME="${NAME%.ko}" 167 NAME="$(module_name_string "$NAME")" 168 shift 2 169 ;; 170 --no-replace) 171 REPLACE=0 172 shift 173 ;; 174 -v | --verbose) 175 VERBOSE="V=1" 176 shift 177 ;; 178 -d | --debug) 179 DEBUG_CLONE=1 180 keep_tmp=1 181 shift 182 ;; 183 -S | --short-circuit) 184 [[ ! -d "$TMP_DIR" ]] && die "--short-circuit requires preserved klp-tmp dir" 185 keep_tmp=1 186 case "$2" in 187 1 | orig) SHORT_CIRCUIT=1; ;; 188 2 | patched) SHORT_CIRCUIT=2; ;; 189 3 | diff) SHORT_CIRCUIT=3; ;; 190 4 | mod) SHORT_CIRCUIT=4; ;; 191 *) die "invalid short-circuit step '$2'" ;; 192 esac 193 shift 2 194 ;; 195 -T | --keep-tmp) 196 keep_tmp=1 197 shift 198 ;; 199 --) 200 shift 201 break 202 ;; 203 *) 204 usage 205 exit 1 206 ;; 207 esac 208 done 209 210 if [[ $# -eq 0 ]]; then 211 usage 212 exit 1 213 fi 214 215 KEEP_TMP="$keep_tmp" 216 PATCHES=("$@") 217} 218 219# temporarily disable xtrace for especially verbose code 220xtrace_save() { 221 [[ -v XTRACE ]] && set +x 222 return 0 223} 224 225xtrace_restore() { 226 [[ -v XTRACE ]] && set -x 227 return 0 228} 229 230validate_config() { 231 xtrace_save "reading .config" 232 source "$CONFIG" || die "no .config file in $(dirname "$CONFIG")" 233 xtrace_restore 234 235 [[ -v CONFIG_LIVEPATCH ]] || \ 236 die "CONFIG_LIVEPATCH not enabled" 237 238 [[ -v CONFIG_KLP_BUILD ]] || \ 239 die "CONFIG_KLP_BUILD not enabled" 240 241 [[ -v CONFIG_GCC_PLUGIN_LATENT_ENTROPY ]] && \ 242 die "kernel option 'CONFIG_GCC_PLUGIN_LATENT_ENTROPY' not supported" 243 244 [[ -v CONFIG_GCC_PLUGIN_RANDSTRUCT ]] && \ 245 die "kernel option 'CONFIG_GCC_PLUGIN_RANDSTRUCT' not supported" 246 247 return 0 248} 249 250# Only allow alphanumerics and '_' and '-' in the module name. Everything else 251# is replaced with '-'. Also truncate to 55 chars so the full name + NUL 252# terminator fits in the kernel's 56-byte module name array. 253module_name_string() { 254 echo "${1//[^a-zA-Z0-9_-]/-}" | cut -c 1-55 255} 256 257# If the module name wasn't specified on the cmdline with --output, give it a 258# name based on the patch name. 259set_module_name() { 260 [[ -v NAME ]] && return 0 261 262 if [[ "${#PATCHES[@]}" -eq 1 ]]; then 263 NAME="$(basename "${PATCHES[0]}")" 264 NAME="${NAME%.*}" 265 else 266 NAME="patch" 267 fi 268 269 NAME="livepatch-$NAME" 270 NAME="$(module_name_string "$NAME")" 271 272 OUTFILE="$NAME.ko" 273} 274 275# Hardcode the value printed by the localversion script to prevent patch 276# application from appending it with '+' due to a dirty git working tree. 277set_kernelversion() { 278 local file="$SRC/scripts/setlocalversion" 279 local localversion 280 281 stash_file "$file" 282 283 localversion="$(cd "$SRC" && make --no-print-directory kernelversion)" 284 localversion="$(cd "$SRC" && KERNELVERSION="$localversion" ./scripts/setlocalversion)" 285 [[ -z "$localversion" ]] && die "setlocalversion failed" 286 287 sed -i "2i echo $localversion; exit 0" scripts/setlocalversion 288} 289 290get_patch_files() { 291 local patch="$1" 292 293 grep0 -E '^(--- |\+\+\+ )' "$patch" \ 294 | gawk '{print $2}' \ 295 | sed 's|^[^/]*/||' \ 296 | sort -u 297} 298 299# Make sure git re-stats the changed files 300git_refresh() { 301 local patch="$1" 302 local files=() 303 304 [[ ! -e "$SRC/.git" ]] && return 305 306 get_patch_files "$patch" | mapfile -t files 307 308 ( 309 cd "$SRC" 310 git update-index -q --refresh -- "${files[@]}" 311 ) 312} 313 314check_unsupported_patches() { 315 local patch 316 317 for patch in "${PATCHES[@]}"; do 318 local files=() 319 320 get_patch_files "$patch" | mapfile -t files 321 322 for file in "${files[@]}"; do 323 case "$file" in 324 lib/*|*.S) 325 die "unsupported patch to $file" 326 ;; 327 esac 328 done 329 done 330} 331 332apply_patch() { 333 local patch="$1" 334 shift 335 local extra_args=("$@") 336 337 [[ ! -f "$patch" ]] && die "$patch doesn't exist" 338 339 ( 340 cd "$SRC" 341 342 # The sed strips the version signature from 'git format-patch', 343 # otherwise 'git apply --recount' warns. 344 sed -n '/^-- /q;p' "$patch" | 345 git apply "${extra_args[@]}" 346 ) 347 348 APPLIED_PATCHES+=("$patch") 349} 350 351revert_patch() { 352 local patch="$1" 353 shift 354 local extra_args=("$@") 355 local tmp=() 356 357 ( 358 cd "$SRC" 359 360 sed -n '/^-- /q;p' "$patch" | 361 git apply --reverse "${extra_args[@]}" 362 ) 363 git_refresh "$patch" 364 365 for p in "${APPLIED_PATCHES[@]}"; do 366 [[ "$p" == "$patch" ]] && continue 367 tmp+=("$p") 368 done 369 370 APPLIED_PATCHES=("${tmp[@]}") 371} 372 373apply_patches() { 374 local patch 375 376 for patch in "${PATCHES[@]}"; do 377 apply_patch "$patch" 378 done 379} 380 381revert_patches() { 382 local extra_args=("$@") 383 local patches=("${APPLIED_PATCHES[@]}") 384 385 for (( i=${#patches[@]}-1 ; i>=0 ; i-- )) ; do 386 revert_patch "${patches[$i]}" "${extra_args[@]}" 387 done 388 389 APPLIED_PATCHES=() 390} 391 392validate_patches() { 393 check_unsupported_patches 394 apply_patches 395 revert_patches 396} 397 398do_init() { 399 # We're not yet smart enough to handle anything other than in-tree 400 # builds in pwd. 401 [[ ! "$SRC" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory" 402 [[ ! "$OBJ" -ef "$SCRIPT_DIR/../.." ]] && die "please run from the kernel root directory" 403 404 (( SHORT_CIRCUIT <= 1 )) && rm -rf "$TMP_DIR" 405 mkdir -p "$TMP_DIR" 406 407 APPLIED_PATCHES=() 408 409 [[ -x "$FIX_PATCH_LINES" ]] || die "can't find fix-patch-lines" 410 411 validate_config 412 set_module_name 413 set_kernelversion 414} 415 416# Refresh the patch hunk headers, specifically the line numbers and counts. 417refresh_patch() { 418 local patch="$1" 419 local tmpdir="$PATCH_TMP_DIR" 420 local files=() 421 422 rm -rf "$tmpdir" 423 mkdir -p "$tmpdir/a" 424 mkdir -p "$tmpdir/b" 425 426 # Get all source files affected by the patch 427 get_patch_files "$patch" | mapfile -t files 428 429 # Copy orig source files to 'a' 430 ( cd "$SRC" && echo "${files[@]}" | xargs cp --parents --target-directory="$tmpdir/a" ) 431 432 # Copy patched source files to 'b' 433 apply_patch "$patch" --recount 434 ( cd "$SRC" && echo "${files[@]}" | xargs cp --parents --target-directory="$tmpdir/b" ) 435 revert_patch "$patch" --recount 436 437 # Diff 'a' and 'b' to make a clean patch 438 ( cd "$tmpdir" && git diff --no-index --no-prefix a b > "$patch" ) || true 439} 440 441# Copy the patches to a temporary directory, fix their lines so as not to 442# affect the __LINE__ macro for otherwise unchanged functions further down the 443# file, and update $PATCHES to point to the fixed patches. 444fix_patches() { 445 local idx 446 local i 447 448 rm -f "$TMP_DIR"/*.patch 449 450 idx=0001 451 for i in "${!PATCHES[@]}"; do 452 local old_patch="${PATCHES[$i]}" 453 local tmp_patch="$TMP_DIR/tmp.patch" 454 local patch="${PATCHES[$i]}" 455 local new_patch 456 457 new_patch="$TMP_DIR/$idx-fixed-$(basename "$patch")" 458 459 cp -f "$old_patch" "$tmp_patch" 460 refresh_patch "$tmp_patch" 461 "$FIX_PATCH_LINES" "$tmp_patch" > "$new_patch" 462 refresh_patch "$new_patch" 463 464 PATCHES[i]="$new_patch" 465 466 rm -f "$tmp_patch" 467 idx=$(printf "%04d" $(( 10#$idx + 1 ))) 468 done 469} 470 471clean_kernel() { 472 local cmd=() 473 474 cmd=("make") 475 cmd+=("--silent") 476 cmd+=("-j$JOBS") 477 cmd+=("clean") 478 479 ( 480 cd "$SRC" 481 "${cmd[@]}" 482 ) 483} 484 485build_kernel() { 486 local log="$TMP_DIR/build.log" 487 local cmd=() 488 489 cmd=("make") 490 491 # When a patch to a kernel module references a newly created unexported 492 # symbol which lives in vmlinux or another kernel module, the patched 493 # kernel build fails with the following error: 494 # 495 # ERROR: modpost: "klp_string" [fs/xfs/xfs.ko] undefined! 496 # 497 # The undefined symbols are working as designed in that case. They get 498 # resolved later when the livepatch module build link pulls all the 499 # disparate objects together into the same kernel module. 500 # 501 # It would be good to have a way to tell modpost to skip checking for 502 # undefined symbols altogether. For now, just convert the error to a 503 # warning with KBUILD_MODPOST_WARN, and grep out the warning to avoid 504 # confusing the user. 505 # 506 cmd+=("KBUILD_MODPOST_WARN=1") 507 508 cmd+=("$VERBOSE") 509 cmd+=("-j$JOBS") 510 cmd+=("KCFLAGS=-ffunction-sections -fdata-sections") 511 cmd+=("vmlinux") 512 cmd+=("modules") 513 514 ( 515 cd "$SRC" 516 "${cmd[@]}" \ 517 1> >(tee -a "$log") \ 518 2> >(tee -a "$log" | grep0 -v "modpost.*undefined!" >&2) 519 ) 520} 521 522find_objects() { 523 local opts=("$@") 524 525 # Find root-level vmlinux.o and non-root-level .ko files, 526 # excluding klp-tmp/ and .git/ 527 find "$OBJ" \( -path "$TMP_DIR" -o -path "$OBJ/.git" -o -regex "$OBJ/[^/][^/]*\.ko" \) -prune -o \ 528 -type f "${opts[@]}" \ 529 \( -name "*.ko" -o -path "$OBJ/vmlinux.o" \) \ 530 -printf '%P\n' 531} 532 533# Copy all .o archives to $ORIG_DIR 534copy_orig_objects() { 535 local files=() 536 537 rm -rf "$ORIG_DIR" 538 mkdir -p "$ORIG_DIR" 539 540 find_objects | mapfile -t files 541 542 xtrace_save "copying orig objects" 543 for _file in "${files[@]}"; do 544 local rel_file="${_file/.ko/.o}" 545 local file="$OBJ/$rel_file" 546 local file_dir="$(dirname "$file")" 547 local orig_file="$ORIG_DIR/$rel_file" 548 local orig_dir="$(dirname "$orig_file")" 549 local cmd_file="$file_dir/.$(basename "$file").cmd" 550 551 [[ ! -f "$file" ]] && die "missing $(basename "$file") for $_file" 552 553 mkdir -p "$orig_dir" 554 cp -f "$file" "$orig_dir" 555 [[ -e "$cmd_file" ]] && cp -f "$cmd_file" "$orig_dir" 556 done 557 xtrace_restore 558 559 mv -f "$TMP_DIR/build.log" "$ORIG_DIR" 560 touch "$TIMESTAMP" 561} 562 563# Copy all changed objects to $PATCHED_DIR 564copy_patched_objects() { 565 local files=() 566 local opts=() 567 local found=0 568 569 rm -rf "$PATCHED_DIR" 570 mkdir -p "$PATCHED_DIR" 571 572 # Note this doesn't work with some configs, thus the 'cmp' below. 573 opts=("-newer") 574 opts+=("$TIMESTAMP") 575 576 find_objects "${opts[@]}" | mapfile -t files 577 578 xtrace_save "copying changed objects" 579 for _file in "${files[@]}"; do 580 local rel_file="${_file/.ko/.o}" 581 local file="$OBJ/$rel_file" 582 local orig_file="$ORIG_DIR/$rel_file" 583 local patched_file="$PATCHED_DIR/$rel_file" 584 local patched_dir="$(dirname "$patched_file")" 585 586 [[ ! -f "$file" ]] && die "missing $(basename "$file") for $_file" 587 588 cmp -s "$orig_file" "$file" && continue 589 590 mkdir -p "$patched_dir" 591 cp -f "$file" "$patched_dir" 592 found=1 593 done 594 xtrace_restore 595 596 (( found == 0 )) && die "no changes detected" 597 598 mv -f "$TMP_DIR/build.log" "$PATCHED_DIR" 599} 600 601# Diff changed objects, writing output object to $DIFF_DIR 602diff_objects() { 603 local log="$KLP_DIFF_LOG" 604 local files=() 605 local opts=() 606 607 rm -rf "$DIFF_DIR" 608 mkdir -p "$DIFF_DIR" 609 610 find "$PATCHED_DIR" -type f -name "*.o" | mapfile -t files 611 [[ ${#files[@]} -eq 0 ]] && die "no changes detected" 612 613 [[ -v DEBUG_CLONE ]] && opts=("--debug") 614 615 # Diff all changed objects 616 for file in "${files[@]}"; do 617 local rel_file="${file#"$PATCHED_DIR"/}" 618 local orig_file="$rel_file" 619 local patched_file="$PATCHED_DIR/$rel_file" 620 local out_file="$DIFF_DIR/$rel_file" 621 local cmd=() 622 623 mkdir -p "$(dirname "$out_file")" 624 625 cmd=("$SRC/tools/objtool/objtool") 626 cmd+=("klp") 627 cmd+=("diff") 628 (( ${#opts[@]} > 0 )) && cmd+=("${opts[@]}") 629 cmd+=("$orig_file") 630 cmd+=("$patched_file") 631 cmd+=("$out_file") 632 633 ( 634 cd "$ORIG_DIR" 635 "${cmd[@]}" \ 636 1> >(tee -a "$log") \ 637 2> >(tee -a "$log" >&2) || \ 638 die "objtool klp diff failed" 639 ) 640 done 641} 642 643# Build and post-process livepatch module in $KMOD_DIR 644build_patch_module() { 645 local makefile="$KMOD_DIR/Kbuild" 646 local log="$KMOD_DIR/build.log" 647 local kmod_file 648 local cflags=() 649 local files=() 650 local cmd=() 651 652 rm -rf "$KMOD_DIR" 653 mkdir -p "$KMOD_DIR" 654 655 cp -f "$SRC/scripts/livepatch/init.c" "$KMOD_DIR" 656 657 echo "obj-m := $NAME.o" > "$makefile" 658 echo -n "$NAME-y := init.o" >> "$makefile" 659 660 find "$DIFF_DIR" -type f -name "*.o" | mapfile -t files 661 [[ ${#files[@]} -eq 0 ]] && die "no changes detected" 662 663 for file in "${files[@]}"; do 664 local rel_file="${file#"$DIFF_DIR"/}" 665 local orig_file="$ORIG_DIR/$rel_file" 666 local orig_dir="$(dirname "$orig_file")" 667 local kmod_file="$KMOD_DIR/$rel_file" 668 local kmod_dir="$(dirname "$kmod_file")" 669 local cmd_file="$orig_dir/.$(basename "$file").cmd" 670 671 mkdir -p "$kmod_dir" 672 cp -f "$file" "$kmod_dir" 673 [[ -e "$cmd_file" ]] && cp -f "$cmd_file" "$kmod_dir" 674 675 # Tell kbuild this is a prebuilt object 676 cp -f "$file" "${kmod_file}_shipped" 677 678 echo -n " $rel_file" >> "$makefile" 679 done 680 681 echo >> "$makefile" 682 683 cflags=("-ffunction-sections") 684 cflags+=("-fdata-sections") 685 [[ $REPLACE -eq 0 ]] && cflags+=("-DKLP_NO_REPLACE") 686 687 cmd=("make") 688 cmd+=("$VERBOSE") 689 cmd+=("-j$JOBS") 690 cmd+=("--directory=.") 691 cmd+=("M=$KMOD_DIR") 692 cmd+=("KCFLAGS=${cflags[*]}") 693 694 # Build a "normal" kernel module with init.c and the diffed objects 695 ( 696 cd "$SRC" 697 "${cmd[@]}" \ 698 1> >(tee -a "$log") \ 699 2> >(tee -a "$log" >&2) 700 ) 701 702 kmod_file="$KMOD_DIR/$NAME.ko" 703 704 # Save off the intermediate binary for debugging 705 cp -f "$kmod_file" "$kmod_file.orig" 706 707 # Work around issue where slight .config change makes corrupt BTF 708 objcopy --remove-section=.BTF "$kmod_file" 709 710 # Fix (and work around) linker wreckage for klp syms / relocs 711 "$SRC/tools/objtool/objtool" klp post-link "$kmod_file" || die "objtool klp post-link failed" 712 713 cp -f "$kmod_file" "$OUTFILE" 714} 715 716 717################################################################################ 718 719process_args "$@" 720do_init 721 722if (( SHORT_CIRCUIT <= 1 )); then 723 status "Validating patch(es)" 724 validate_patches 725 status "Building original kernel" 726 clean_kernel 727 build_kernel 728 status "Copying original object files" 729 copy_orig_objects 730fi 731 732if (( SHORT_CIRCUIT <= 2 )); then 733 status "Fixing patch(es)" 734 fix_patches 735 apply_patches 736 status "Building patched kernel" 737 build_kernel 738 revert_patches 739 status "Copying patched object files" 740 copy_patched_objects 741fi 742 743if (( SHORT_CIRCUIT <= 3 )); then 744 status "Diffing objects" 745 diff_objects 746fi 747 748if (( SHORT_CIRCUIT <= 4 )); then 749 status "Building patch module: $OUTFILE" 750 build_patch_module 751fi 752 753status "SUCCESS" 754