#!/bin/bash # This script is designed to create a livepatch module for the specified Photon OS kernel version. # Args: # -k: Specifies the kernel version. If not set, builds native livepatch # -p: Patch file list. Need at least one patch file listed here # -n: Output file name. Will be default if not specified. # -o: Output folder for livepatch modules. # -R: Don't set replace flag for the livepatch module. Default is to set the flag. # --export-debuginfo: Save debug files such as patched vmlinux, changed objs, etc. # -h/--help: Prints help message # -d: Use file contents as description field for livepatch module. # --rpm: Package the kernel module as an rpm # --rpm-version: Specify the version number of the rpm # --rpm-release: Specify the release number of the rpm # --rpm-desc: Specify the description file for the rpm. If not set, it will be the same as the module. # # ex) # With all options enabled, and multiple patches: # gen_livepatch -k 4.19.247-2.ph3 -o my_dir -n my_livepatch.ko -p my_patch1.patch my_patch2.patch -D description.txt -R --export-debuginfo # ex) # All default settings. Must supply at least one patch file though. Builds a livepatch for the current kernel version # gen_livepatch -p my_patch.patch # # If GEN_LIVEPATCH_DEBUG is set to 1, the tmp directory will not be deleted at the end of the script. Useful for debugging purposes. set -o pipefail GCC=/usr/bin/gcc BUILD_DIR="/var/opt/gen_livepatch" DEFAULT_OUTPUT_FOLDER="$BUILD_DIR/livepatches" TEMP_DIR="$BUILD_DIR/tmp" SRC_DIR="$TEMP_DIR/rpmbuild/BUILD" VERSION_RELEASE_FLAVOR="" OUTPUT_FOLDER="" LIVEPATCH_NAME="" LIVEPATCH_EXT="_klp" DEBUG_PKGNAME="" SRC_PKGNAME="" SPEC_FILENAME="" VMLINUX_PATH="" STARTING_DIR="$PWD" PH_TAG="" KERNEL_RELEASE="" KERNEL_FLAVOR="" KERNEL_VERSION="" KERNEL_VERSION_RELEASE_TAG="" KERNEL_RELEASE_TAG="" IS_RT=0 NON_REPLACE_FLAG="" EXPORT_DEBUGINFO=0 DEBUGINFO_DIR="$BUILD_DIR/debuginfo" KPATCH_BUILDDIR="$HOME/.kpatch" DESC_FILE="" RPM_DESCFILE="" PACKAGE_AS_RPM=0 RPM_VERSION="" RPM_RELEASE="" BUILD_RPM_SPECFILE="/etc/gen_livepatch/build-rpm.spec" DEBUGINFO_LOCAL_PATH="" RPM_MACRO_FILE="" HAS_DIST_TAG=1 patches=() # args # 1. string to match # 2. array values match_string_in_array() { local string_to_match=$1 shift while (( $# )); do if [[ "$string_to_match" == "$1" ]]; then return 0 fi shift done return 1 } # parse the command line arguments and fill in variables parse_args() { # just print help message if no arguments if [ $# -eq 0 ]; then print_help 0 elif [[ $1 != -* ]]; then echo "A flag must be set before any other parameters" print_help 1 fi local flag="" flags=( "-s" "-p" "-v" "-o" "-h" "--help" "-k" "-n" "-R" "--export-debuginfo" "-d" "--rpm" "--rpm-version" "--rpm-release" "--rpm-desc" ) while (( "$#" )); do if [[ $1 = -* ]]; then flag=$1 # check to make sure number of args are correct if ! match_string_in_array $flag ${flags[@]} ; then error "Unknown option $flag" elif [[ $1 == -h || $1 == --help ]]; then print_help 0 elif [[ $1 == -R ]]; then NON_REPLACE_FLAG="-R" elif [[ $1 == --export-debuginfo ]]; then EXPORT_DEBUGINFO=1 elif [[ $1 == --rpm ]]; then PACKAGE_AS_RPM=1 elif [[ $2 == -* || -z $2 ]]; then error "$1 needs at least one argument" elif [[ $3 != -* && $flag != -p && -n $3 ]]; then error "$1 only takes one argument" fi else case "$flag" in -p) patches+=("$1") ;; -k) VERSION_RELEASE_FLAVOR=$1 ;; -o) OUTPUT_FOLDER=$1 ;; -n) LIVEPATCH_NAME=$1 ;; -d) DESC_FILE=$1 [ -f "$DESC_FILE" ] || error "Module description file does not exist" ;; -s) SRC_RPM_LOCAL_PATH=$1 [ -f "$SRC_RPM_LOCAL_PATH" ] || error "Unable to locate local src rpm at $SRC_RPM_LOCAL_PATH" ;; -v) DEBUGINFO_LOCAL_PATH=$1 [ -f "$DEBUGINFO_LOCAL_PATH" ] || error "Unable to locate local debuginfo rpm at $DEBUGINFO_LOCAL_PATH" ;; --rpm-version) RPM_VERSION=$1 ;; --rpm-release) RPM_RELEASE=$1 ;; --rpm-desc) RPM_DESCFILE=$1 [ -f "$RPM_DESCFILE" ] || error "RPM description file does not exist" ;; esac fi # shift to the next argument shift done if [ -z "$patches" ]; then error "Please input at least one patch file" fi if [ -z "$VERSION_RELEASE_FLAVOR" ]; then echo "No kernel version specified, building native patch" VERSION_RELEASE_FLAVOR=$(uname -r) fi if [ -z "$LIVEPATCH_NAME" ]; then echo "No output name specified, using default" # just use the first 20 characters of the patch file name local first_patch_name=$(cut -d '.' -f 1 <<< "$(basename "${patches[0]}")") local cur_datetime=$(date +%d%b%Y-%H_%M_%S) LIVEPATCH_NAME=${first_patch_name}-${cur_datetime}${LIVEPATCH_EXT} fi if [ -z "$OUTPUT_FOLDER" ]; then echo "Output folder not specified, using default." echo "Outputting livepatches to: $DEFAULT_OUTPUT_FOLDER" OUTPUT_FOLDER="$DEFAULT_OUTPUT_FOLDER" fi if [[ -z "$RPM_VERSION" && "$PACKAGE_AS_RPM" == 1 ]]; then echo "RPM version number not specified, setting to 1" RPM_VERSION=1 fi if [[ -z "$RPM_RELEASE" && "$PACKAGE_AS_RPM" == 1 ]]; then echo "RPM release not specified, setting to 1" RPM_RELEASE=1 fi # if building an rpm, and rpm description is not specified, use the kernel module desc file if it is set if [[ ! -z "$DESC_FILE" && "$PACKAGE_AS_RPM" == 1 && -z "$RPM_DESCFILE" ]]; then echo "Separate RPM description not specified, using module description" RPM_DESCFILE=$DESC_FILE fi # make sure output folder exists mkdir -p "$OUTPUT_FOLDER" # remove .ko if it's added, otherwise resulting kernel module will have wrong name if [[ "${LIVEPATCH_NAME##*.}" == "ko" ]]; then LIVEPATCH_NAME="${LIVEPATCH_NAME%.*}" fi is_rt "$VERSION_RELEASE_FLAVOR" get_kernel_flavor "$VERSION_RELEASE_FLAVOR" get_kernel_version "$VERSION_RELEASE_FLAVOR" get_kernel_release "$VERSION_RELEASE_FLAVOR" get_photon_tag "$VERSION_RELEASE_FLAVOR" KERNEL_VERSION_RELEASE_TAG=${KERNEL_VERSION}-${KERNEL_RELEASE}.${PH_TAG} KERNEL_RELEASE_TAG=${KERNEL_RELEASE}.${PH_TAG} # Add dist tag to rpm macros file to better handle rpmbuild of linux src rpm if [ "$(rpm -E %dist)" == "%dist" ]; then echo "%dist .$PH_TAG" >> "$HOME"/.rpmmacros || error "Failed to define %dist tag in $HOME/.rpmmacros" # record RPM macro file as whatever file we edited here, so we can reverse # this change at the end of the build RPM_MACRO_FILE="$HOME/.rpmmacros" HAS_DIST_TAG=0 fi } # takes in uname -r formatted string # ex) 4.19.247-2.ph3-aws get_kernel_flavor() { # check for rt kernel extension if [[ $IS_RT != 1 ]]; then KERNEL_FLAVOR=$(cut -d '-' -f 3 <<< "$1") if [ -z "$KERNEL_FLAVOR" ]; then KERNEL_FLAVOR="generic" fi else KERNEL_FLAVOR=rt fi } is_rt() { local rt_ext="$(cut -d '-' -f 4 <<< "$1")" if [[ -n $rt_ext ]]; then IS_RT=1 fi } # takes in uname -r formatted string # returns the release number get_kernel_release() { local field_num=2 if [[ $IS_RT == 1 ]]; then # rt kernel field_num=3 fi local release_tag=$(cut -d '-' -f $field_num <<< "$1") local release=$(cut -d '.' -f 1 <<< "$release_tag") KERNEL_RELEASE=$release } # takes in uname -r formatted string # gets kernel version only # ex) 5.10.118 get_kernel_version() { KERNEL_VERSION=$(cut -d '-' -f 1 <<< "$1") } # extracts .phX tag from # uname -r formatted string get_photon_tag() { local field_num=2 if [[ $IS_RT == 1 ]]; then # rt kernel field_num=3 fi local release_tag=$(cut -d '-' -f $field_num <<< "$1") local kernel_tag=$(cut -d '.' -f 2 <<< "$release_tag") PH_TAG=$kernel_tag } print_config() { echo -e "\nBuilding livepatch" echo -e "Linux Version: $KERNEL_VERSION_RELEASE_TAG" echo -e "Photon OS Version: $PHOTON_VERSION" echo -e "Photon OS Flavor: $KERNEL_FLAVOR" echo -e "Patch files: " for patch in "${patches[@]}"; do echo -e "\t $(basename "$patch")" done } # Copied from kpatch-build. Used to find gcc version that was used to compile an executable gcc_version_from_file() { readelf -p .comment "$1" | grep -m 1 -o 'GCC:.*' || error "Error with readelf" } # Copied from kpatch-build. Used to check if GCC versions match gcc_version_check() { local target="$1" local c="$BUILD_DIR/gcc_version_check.c" local o="$BUILD_DIR/gcc_version_check.o" local out gccver kgccver # gcc --version varies between distributions therefore extract version # by compiling a test file and compare it to vmlinux's version. echo 'int main(void) {return 0;}' > "$c" out="$("$GCC" -c -pg -ffunction-sections -o "$o" "$c" 2>&1)" if [[ $? != 0 ]]; then error "GCC compilation error" fi if [[ -n "$out" ]]; then echo "gcc >= 4.8 required for -pg -ffunction-settings" echo "gcc output: $out" error fi gccver="$(gcc_version_from_file "$o")" kgccver="$(gcc_version_from_file "$target")" rm -f "$c" "$o" # ensure gcc version matches that used to build the kernel if [[ "$gccver" != "$kgccver" ]]; then echo "gcc/kernel version mismatch" echo "gcc version: $gccver" echo "kernel version: $kgccver" echo "kpatch may have problems when building with the wrong gcc, exiting to be safe..." error fi } # prints error message and then exits error() { if [[ -z "$1" ]]; then echo "Error! Exiting." 1>&2 else echo "ERROR: $1" 1>&2 fi #clean up before exiting if [[ $GEN_LIVEPATCH_DEBUG != 1 ]]; then rm -rf $TEMP_DIR fi exit 1 } print_help() { echo -e "This script is designed to create a livepatch module for the specified Photon OS kernel version and flavor." echo -e "Arguments:" echo -e "\t -k: Specifies the kernel version. If not set, builds native livepatch" echo -e "\t -p: Patch file list. Need at least one patch file listed here" echo -e "\t -n: Output file name. Will be default if not specified." echo -e "\t -o: Output directory. Will be default if not specified." echo -e "\t -R: Don't set the replace flag in the livepatch module. Replace flag is set by default." echo -e "\t --export-debuginfo: Save debug files such as patched vmlinux, changed objs, etc." echo -e "\t -h/--help: Print help message" echo -e "\t -d: Use file contents as description field for livepatch module." echo -e "\t -s: Specify the location of a local copy of the Linux src rpm to use" echo -e "\t -v: Specify the location of a local copy of the Linux debuginfo rpm to use" echo -e "\t --rpm: Package the kernel module as an rpm" echo -e "\t --rpm-version: Specify the version number of the rpm" echo -e "\t --rpm-release: Specify the release number of the rpm" echo -e "\t --rpm-desc: Specify the description file for the rpm. If not set, it will be the same as the module." exit "$1" } install_kernel_dependencies() { local to_be_installed_pkgs=($(rpm -qpR "$1" | grep -vw rpmlib)) echo -e "The following packages must be installed:\n${to_be_installed_pkgs[*]}\n" tdnf install -qy "${to_be_installed_pkgs[@]}" || error "Error installing required packages" } parse_source_rpm() { echo -e "\nDownloading and/or processing source rpm" local src_rpm_url="https://packages.vmware.com/photon/${PHOTON_VERSION}/photon_srpms_${PHOTON_VERSION}_x86_64/$SRC_PKGNAME" # allow downloading/copying of source rpm from either local or custom urls. Just need these variables to be exported before # running to enable these options. if [ -n "${SRC_RPM_LOCAL_PATH}" ]; then cp "$SRC_RPM_LOCAL_PATH" "$SRC_PKGNAME" || error "Couldn't find local src rpm" elif [ -n "${SRC_RPM_REMOTE_URL}" ]; then curl "$SRC_RPM_REMOTE_URL" --output "$SRC_PKGNAME" &> /dev/null || error "Couldn't download remote src rpm" else curl "$src_rpm_url" --output "$SRC_PKGNAME" &> /dev/null || error "Couldn't download photon kernel source rpm" fi # set up temporary rpm build environment local rpmdir="%_topdir %(echo $PWD)/rpmbuild" mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} || error rpm -i "$SRC_PKGNAME" --define "$rpmdir" || error "Failed to extract spec file" local config_filename=$(awk '/^Source1:/{print $NF}' rpmbuild/SPECS/linux*.spec) if [[ "$config_filename" = "config_%{_arch}" ]]; then config_filename="config_x86_64" fi install_kernel_dependencies "$SRC_PKGNAME" rpmbuild -bp "rpmbuild/SPECS/$SPEC_FILENAME" --define "$rpmdir" &> /dev/null || error "Failed to extract kernel source and create linux source directory" echo "Copying source files to correct locations, if they exist" ls rpmbuild/BUILD/fips*canister* &> /dev/null && cp -rT rpmbuild/BUILD/fips*canister* "rpmbuild/BUILD/linux-$KERNEL_VERSION/crypto" cp rpmbuild/SOURCES/"$config_filename" rpmbuild/BUILD/linux-"$LINUX_VERSION"/.config || error "Failed to locate kernel config file" [[ -f rpmbuild/SOURCES/fips_canister-kallsyms ]] && cp rpmbuild/SOURCES/fips_canister-kallsyms "rpmbuild/BUILD/linux-$LINUX_VERSION/crypto" if [[ "$KERNEL_FLAVOR" == "esx" ]] || [[ "$KERNEL_FLAVOR" == "rt" ]]; then # change m to y for fips canister cp "rpmbuild/SOURCES/modify_kernel_configs.inc" "rpmbuild/BUILD/linux-$LINUX_VERSION" pushd "rpmbuild/BUILD/linux-$LINUX_VERSION" &> /dev/null || error source "modify_kernel_configs.inc" popd &> /dev/null || error fi # make sure vermagic gets the right tag, otherwise kpatch load may fail # basically just make sure vermagic in modinfo is the same as uname -r if [[ $KERNEL_FLAVOR == "generic" ]]; then sed -i s/^CONFIG_LOCALVERSION=\".*\"/CONFIG_LOCALVERSION=\"-$KERNEL_RELEASE_TAG\"/g rpmbuild/BUILD/linux-"$LINUX_VERSION"/.config else sed -i s/^CONFIG_LOCALVERSION=\".*\"/CONFIG_LOCALVERSION=\"-$KERNEL_RELEASE_TAG-$KERNEL_FLAVOR\"/g rpmbuild/BUILD/linux-"$LINUX_VERSION"/.config fi } parse_debuginfo_rpm() { echo -e "\nDownloading debug package and extracting vmlinux" if [[ -n "$DEBUGINFO_LOCAL_PATH" ]]; then cp "$DEBUGINFO_LOCAL_PATH" "$DEBUG_PKGNAME" else curl "https://packages.vmware.com/photon/$PHOTON_VERSION/photon_debuginfo_${PHOTON_VERSION}_x86_64/x86_64/$DEBUG_PKGNAME" --output "$DEBUG_PKGNAME" &> /dev/null || error fi local absolute_path=$(rpm -qlp "$DEBUG_PKGNAME" | grep "vmlinux-$LINUX_VERSION") # remove the first slash from the path VMLINUX_PATH=${absolute_path:1} # extract vmlinux rpm2cpio "$DEBUG_PKGNAME" | cpio -ivd "./$VMLINUX_PATH" &> /dev/null || error "Couldn't extract vmlinux from $DEBUG_PKGNAME" } # sets all the variables for file names # vmlinux path is set when we parse the debuginfo package set_filenames_and_paths() { #determine which photon version this is PHOTON_VERSION="${PH_TAG//[^0-9]/}".0 #determine which linux version this is LINUX_VERSION=$(cut -d '-' -f 1 <<< $KERNEL_VERSION_RELEASE_TAG) #set file paths based on kernel flavor if [[ "$KERNEL_FLAVOR" == "generic" ]]; then DEBUG_PKGNAME="linux-debuginfo-$KERNEL_VERSION_RELEASE_TAG.x86_64.rpm" SRC_PKGNAME="linux-$KERNEL_VERSION_RELEASE_TAG.src.rpm" SPEC_FILENAME="linux.spec" else DEBUG_PKGNAME="linux-$KERNEL_FLAVOR-debuginfo-$KERNEL_VERSION_RELEASE_TAG.x86_64.rpm" SRC_PKGNAME="linux-$KERNEL_FLAVOR-$KERNEL_VERSION_RELEASE_TAG.src.rpm" SPEC_FILENAME="linux-$KERNEL_FLAVOR.spec" fi } #executes the kpatch-build command to build the livepatch kpatch_build() { #only need -R flag for 3.0, since 3.0 doesn't support klp_replace #otherwise leave it as set by the user if [[ $PHOTON_VERSION == "3.0" ]]; then NON_REPLACE_FLAG="-R" fi local skip_cleanup="" if [[ $EXPORT_DEBUGINFO == 1 ]]; then skip_cleanup="--skip-cleanup" fi local description_file="" if [ -f "$DESC_FILE" ]; then description_file="-D $DESC_FILE" fi kpatch-build -v "$TEMP_DIR/$VMLINUX_PATH" \ -s "$TEMP_DIR/rpmbuild/BUILD/linux-$LINUX_VERSION" \ -c "$TEMP_DIR/rpmbuild/BUILD/linux-$LINUX_VERSION/.config" \ -j "$(nproc)" \ -n "$LIVEPATCH_NAME" \ -o "$OUTPUT_FOLDER" \ "${patches[@]}" \ $NON_REPLACE_FLAG \ $skip_cleanup \ $description_file \ || error "Building livepatch module failed." echo "Livepatch module successfully built" # save all of the debug info like vmlinux, changed objects, etc if asked to # vmlinux is the patched vmlinux, not original if [[ $EXPORT_DEBUGINFO == 1 ]]; then echo "Saving kpatch-build debug files to $DEBUGINFO_DIR" # clean out any old files if [[ -n "$DEBUFINFO_DIR" ]]; then rm -r "${DEBUGINFO_DIR:?}"/* &> /dev/null fi mkdir -p $DEBUGINFO_DIR/patched-debuginfo cp $SRC_DIR/linux-"$KERNEL_VERSION"/vmlinux* \ $SRC_DIR/linux-"$KERNEL_VERSION"/modules* \ $SRC_DIR/linux-"$KERNEL_VERSION"/Module.symvers \ $DEBUGINFO_DIR/patched-debuginfo cp -r "$KPATCH_BUILDDIR"/tmp "$DEBUGINFO_DIR"/kpatch-tmp fi } # function to package the required module inside of an rpm package_module_in_rpm() { echo -e "\nPreparing to build rpm" local linux_version [[ "$KERNEL_FLAVOR" == "generic" ]] && linux_version="$KERNEL_VERSION_RELEASE_TAG" || linux_version="${KERNEL_VERSION_RELEASE_TAG}-${KERNEL_FLAVOR}" pushd "$TEMP_DIR" > /dev/null 2>&1 || error # set up temporary rpm build environment local rpmdir="%_topdir %(echo $PWD)/rpmbuild" mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} || error popd > /dev/null 2>&1 || error cd "$STARTING_DIR" > /dev/null 2>&1 || error mv "$OUTPUT_FOLDER"/"$LIVEPATCH_NAME".ko "$TEMP_DIR"/rpmbuild/SOURCES || error # fill in needed info in spec file skeleton local spec_file="$TEMP_DIR/rpmbuild/SPECS/build-rpm.spec" cp "$BUILD_RPM_SPECFILE" "$spec_file" || error sed -i "s/@@VERSION@@/$RPM_VERSION/g" "$spec_file" || error "Filling in RPM vesion for spec file skeleton failed" sed -i "s/@@RELEASE@@/$RPM_RELEASE/g" "$spec_file" || error "Filling in RPM release for spec file skeleton failed" sed -i "s/@@LIVEPATCH_NAME@@/$LIVEPATCH_NAME/g" "$spec_file" || error "Filling in livepatch name for spec file skeleton failed" sed -i "s/@@LINUX_VERSION@@/$linux_version/g" "$spec_file" || error "Filling in linux version for spec file skeleton failed" if [ -f "$RPM_DESCFILE" ]; then sed -i "s/@@DESCRIPTION@@/$(cat "$RPM_DESCFILE")/g" "$spec_file" || error "Filling in description for spec file skeleton failed" else sed -i "s/@@DESCRIPTION@@/Livepatch module for Linux $linux_version\n/g" "$spec_file" || error "Filling in description for spec file skeleton failed" fi echo "Building rpm" rpmbuild -bb $spec_file --define "$rpmdir" > /dev/null 2>&1 || error "Packaging kernel module as rpm failed" # should only build for x86_64 arch but put a * there just in case cp "$TEMP_DIR"/rpmbuild/RPMS/*/"$LIVEPATCH_NAME"*.rpm "$OUTPUT_FOLDER" || error "Current dir: $STARTING_DIR. Failed to save RPM to $OUTPUT_FOLDER" echo "SUCCESS: Building rpm finished" } cleanup() { if [[ $GEN_LIVEPATCH_DEBUG != 1 ]]; then rm -rf $TEMP_DIR || error "Failed to delete temp dir: $TEMP_DIR" fi [[ "$HAS_DIST_TAG" -eq 0 ]] && sed -i "/%dist .$PH_TAG/d" "$RPM_MACRO_FILE" } #cleanup on exit trap cleanup EXIT SIGINT SIGTERM #make sure our working directory is clean before we start rm -rf $TEMP_DIR #parse command line arguments parse_args "$@" #set variables for all filenames/paths set_filenames_and_paths print_config mkdir -p $TEMP_DIR cd $TEMP_DIR || error #download, prep, kernel source parse_source_rpm #download and extract vmlinux parse_debuginfo_rpm #execute kpatch-build with all of the required args gcc_version_check "$VMLINUX_PATH" echo -e "\nAll sources ready, building livepatch module..." # cd back to starting directory so that patch file paths are correct cd "$STARTING_DIR" || error kpatch_build if [[ $PACKAGE_AS_RPM == 1 ]]; then package_module_in_rpm fi