kpatch/kpatch-build/kpatch-build
2014-07-01 10:55:43 -04:00

481 lines
15 KiB
Bash
Executable File

#!/bin/bash
#
# kpatch build script
#
# Copyright (C) 2014 Seth Jennings <sjenning@redhat.com>
# Copyright (C) 2013,2014 Josh Poimboeuf <jpoimboe@redhat.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA,
# 02110-1301, USA.
# This script takes a patch based on the version of the kernel
# currently running and creates a kernel module that will
# replace modified functions in the kernel such that the
# patched code takes effect.
# This script currently only works on Fedora and will need to be adapted to
# work on other distros.
# This script:
# - Downloads the kernel src rpm for the currently running kernel
# - Unpacks and prepares the src rpm for building
# - Builds the base kernel (vmlinux)
# - Builds the patched kernel and monitors changed objects
# - Builds the patched objects with gcc flags -f[function|data]-sections
# - Runs kpatch tools to create and link the patch kernel module
BASE="$PWD"
LOGFILE="/tmp/kpatch-build-$(date +%s).log"
SCRIPTDIR="$(readlink -f $(dirname $(type -p $0)))"
ARCHVERSION="$(uname -r)"
CPUS="$(grep -c ^processor /proc/cpuinfo)"
CACHEDIR="$HOME/.kpatch"
SRCDIR="$CACHEDIR/src"
OBJDIR="$CACHEDIR/obj"
OBJDIR2="$CACHEDIR/obj2"
VERSIONFILE="$CACHEDIR/version"
TEMPDIR=
APPLIEDPATCHFILE="kpatch.patch"
DEBUG=0
warn() {
echo "ERROR: $1" >&2
}
die() {
if [[ -z $1 ]]; then
warn "kpatch build failed. Check $LOGFILE for more details."
else
warn "$1"
fi
exit 1
}
cleanup() {
if [[ -e "$SRCDIR/$APPLIEDPATCHFILE" ]]; then
patch -p1 -R -d "$SRCDIR" < "$SRCDIR/$APPLIEDPATCHFILE" &> /dev/null
rm -f "$SRCDIR/$APPLIEDPATCHFILE"
fi
if [[ -n $USERSRCDIR ]]; then
# restore original .config and vmlinux since they were removed
# with mrproper
[[ -e $TEMPDIR/vmlinux ]] && cp -f $TEMPDIR/vmlinux $USERSRCDIR
[[ -e $TEMPDIR/.config ]] && cp -f $TEMPDIR/.config $USERSRCDIR
fi
[[ "$DEBUG" -eq 0 ]] && rm -rf "$TEMPDIR"
}
clean_cache() {
[[ -z $USERSRCDIR ]] && rm -rf "$SRCDIR"
rm -rf "$OBJDIR" "$OBJDIR2" "$VERSIONFILE"
mkdir -p "$OBJDIR"
}
find_dirs() {
if [[ -e "$SCRIPTDIR/create-diff-object" ]]; then
# git repo
TOOLSDIR="$SCRIPTDIR"
DATADIR="$(readlink -f $SCRIPTDIR/../kmod)"
SYMVERSFILE="$DATADIR/core/Module.symvers"
return
elif [[ -e "$SCRIPTDIR/../libexec/kpatch/create-diff-object" ]]; then
# installation path
TOOLSDIR="$(readlink -f $SCRIPTDIR/../libexec/kpatch)"
DATADIR="$(readlink -f $SCRIPTDIR/../share/kpatch)"
SYMVERSFILE="$(readlink -f $SCRIPTDIR/../lib/modules/$(uname -r)/extra/kpatch/Module.symvers)"
return
fi
return 1
}
find_parent_obj() {
dir=$(dirname $1)
file=$(basename $1)
grepname=${1%.o}
grepname=$grepname\\\.o
if [[ $DEEP_FIND -eq 1 ]]; then
parent=$(find . -name ".*.cmd" | xargs grep -l $grepname | grep -v $dir/.${file}.cmd)
num=$(find . -name ".*.cmd" | xargs grep -l $grepname | grep -v $dir/.${file}.cmd | wc -l)
else
parent=$(grep -l $grepname $dir/.*.cmd | grep -v $dir/.${file}.cmd)
num=$(grep -l $grepname $dir/.*.cmd | grep -v $dir/.${file}.cmd | wc -l)
fi
[[ $num -eq 0 ]] && PARENT="" && return
[[ $num -gt 1 ]] && die "two parent matches for $1"
dir=$(dirname $parent)
PARENT=$(basename $parent)
PARENT=${PARENT#.}
PARENT=${PARENT%.cmd}
PARENT=$dir/$PARENT
[[ ! -e $PARENT ]] && die "ERROR: can't find parent $PARENT for $1"
}
find_kobj() {
arg=$1
KOBJFILE=$arg
dir=$(dirname $KOBJFILE)
file=$(basename $KOBJFILE)
DEEP_FIND=0
while true; do
find_parent_obj $KOBJFILE
if [[ -z $PARENT ]]; then
[[ $KOBJFILE = *built-in.o ]] && KOBJFILE=vmlinux && return
[[ $KOBJFILE = *.ko ]] && return
if [[ $DEEP_FIND -eq 0 ]]; then
DEEP_FIND=1
continue;
fi
die "invalid ancestor $KOBJFILE for $arg"
fi
KOBJFILE=$PARENT
done
}
usage() {
echo "usage: $0 [options] <patch file>" >&2
echo " -h, --help Show this help message" >&2
echo " -r, --sourcerpm Specify kernel source RPM" >&2
echo " -s, --sourcedir Specify kernel source directory" >&2
echo " -c, --config Specify kernel config file" >&2
echo " -v, --vmlinux Specify original vmlinux" >&2
echo " -t, --target Specify custom kernel build targets" >&2
echo " -d, --debug Keep scratch files in /tmp" >&2
}
options=$(getopt -o hr:s:c:v:t:d -l "help,sourcerpm:,sourcedir:,config:,vmlinux:,target:,debug" -- "$@") || die "getopt failed"
eval set -- "$options"
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-r|--sourcerpm)
SRCRPM=$(readlink -f "$2")
shift
[[ ! -f "$SRCRPM" ]] && die "source rpm $SRCRPM not found"
rpmname=$(basename "$SRCRPM")
ARCHVERSION=${rpmname%.src.rpm}.$(uname -m)
ARCHVERSION=${ARCHVERSION#kernel-}
;;
-s|--sourcedir)
USERSRCDIR=$(readlink -f "$2")
shift
[[ ! -d "$USERSRCDIR" ]] && die "source dir $USERSRCDIR not found"
;;
-c|--config)
CONFIGFILE=$(readlink -f "$2")
shift
[[ ! -f "$CONFIGFILE" ]] && die "config file $CONFIGFILE not found"
;;
-v|--vmlinux)
VMLINUX=$(readlink -f "$2")
shift
[[ ! -f "$VMLINUX" ]] && die "vmlinux file $VMLINUX not found"
;;
-t|--target)
TARGETS="$TARGETS $2"
shift
;;
-d|--debug)
echo "DEBUG mode enabled"
DEBUG=1
set -o xtrace
;;
--)
if [[ -z "$2" ]]; then
warn "no patch file specified"
usage
exit 1
fi
PATCHFILE=$(readlink -f "$2")
[[ ! -f "$PATCHFILE" ]] && die "patch file $PATCHFILE not found"
break
;;
esac
shift
done
TEMPDIR="$(mktemp -d /tmp/kpatch-build-XXXXXX)" || die "mktemp failed"
trap cleanup EXIT INT TERM
if [[ -n $USERSRCDIR ]]; then
# save .config and vmlinux since they'll get removed with mrproper so
# we can restore them later and be able to run kpatch-build multiple
# times on the same sourcedir
[[ -z $CONFIGFILE ]] && CONFIGFILE="$USERSRCDIR"/.config
[[ ! -e "$CONFIGFILE" ]] && die "can't find config file"
[[ "$CONFIGFILE" = "$USERSRCDIR"/.config ]] && cp -f "$CONFIGFILE" $TEMPDIR
[[ -z $VMLINUX ]] && VMLINUX="$USERSRCDIR"/vmlinux
[[ ! -e "$VMLINUX" ]] && die "can't find vmlinux"
[[ "$VMLINUX" = "$USERSRCDIR"/vmlinux ]] && cp -f "$VMLINUX" $TEMPDIR/vmlinux && VMLINUX=$TEMPDIR/vmlinux
fi
[[ -z $TARGETS ]] && TARGETS="vmlinux modules"
PATCHNAME="$(basename $PATCHFILE)"
if [[ "$PATCHNAME" =~ \.patch ]] || [[ "$PATCHNAME" =~ \.diff ]]; then
PATCHNAME="${PATCHNAME%.*}"
fi
# Only allow alphanumerics and '_' and '-' in the module name.
# Everything else is replaced with '-'.
PATCHNAME=${PATCHNAME//[^a-zA-Z0-9_-]/-}
find_dirs || die "can't find supporting tools"
[[ -e "$SYMVERSFILE" ]] || die "can't find core module Module.symvers"
source /etc/os-release
DISTRO=$ID
if [[ $DISTRO = fedora ]] || [[ $DISTRO = rhel ]]; then
[[ -z $VMLINUX ]] && VMLINUX=/usr/lib/debug/lib/modules/$ARCHVERSION/vmlinux
[[ -e "$VMLINUX" ]] || die "kernel-debuginfo-$ARCHVERSION not installed"
export PATH=/usr/lib64/ccache:$PATH
elif [[ $DISTRO = ubuntu ]]; then
[[ -z $VMLINUX ]] && VMLINUX=/usr/lib/debug/boot/vmlinux-$ARCHVERSION
[[ -e "$VMLINUX" ]] || die "linux-image-$ARCHVERSION-dbgsym not installed"
export PATH=/usr/lib/ccache:$PATH
elif [[ $DISTRO = debian ]]; then
[[ -z $VMLINUX ]] && VMLINUX=/usr/lib/debug/lib/modules/$ARCHVERSION/vmlinux
[[ -e "$VMLINUX" ]] || die "linux-image-$ARCHVERSION-dbg not installed"
export PATH=/usr/lib/ccache:$PATH
fi
if [[ -n "$USERSRCDIR" ]]; then
echo "Using source directory at $USERSRCDIR"
SRCDIR="$USERSRCDIR"
clean_cache
cp -f "$CONFIGFILE" "$OBJDIR/.config"
echo "Detecting kernel version"
cd "$SRCDIR"
make mrproper >> "$LOGFILE" 2>&1 || die "make mrproper failed"
make O="$OBJDIR" prepare >> "$LOGFILE" 2>&1 || die "make prepare failed"
ARCHVERSION=$(make O="$OBJDIR" kernelrelease) || die "make kernelrelease failed"
elif [[ -e "$SRCDIR" ]] && [[ -e "$VERSIONFILE" ]] && [[ $(cat "$VERSIONFILE") = $ARCHVERSION ]]; then
echo "Using cache at $SRCDIR"
else
if [[ $DISTRO = fedora ]] || [[ $DISTRO = rhel ]]; then
echo "Fedora/Red Hat distribution detected"
rpm -q --quiet rpmdevtools || die "rpmdevtools not installed"
if [[ -z "$SRCRPM" ]]; then
rpm -q --quiet yum-utils || die "yum-utils not installed"
echo "Downloading kernel source for $ARCHVERSION"
yumdownloader --source --destdir "$TEMPDIR" "kernel-$ARCHVERSION" >> "$LOGFILE" 2>&1 || die
SRCRPM="$TEMPDIR/kernel-${ARCHVERSION%*.*}.src.rpm"
fi
echo "Unpacking kernel source"
rpmdev-setuptree >> "$LOGFILE" 2>&1 || die
rpm -ivh "$SRCRPM" >> "$LOGFILE" 2>&1 || die
rpmbuild -bp "--target=$(uname -m)" "$HOME/rpmbuild/SPECS/kernel.spec" >> "$LOGFILE" 2>&1 ||
die "rpmbuild -bp failed. you may need to run 'yum-builddep kernel' first."
clean_cache
mv "$HOME"/rpmbuild/BUILD/kernel-*/linux-"$ARCHVERSION" "$SRCDIR" >> "$LOGFILE" 2>&1 || die
cp "$SRCDIR/.config" "$OBJDIR" || die
echo "-${ARCHVERSION##*-}" > "$SRCDIR/localversion" || die
echo $ARCHVERSION > "$VERSIONFILE" || die
elif [[ $DISTRO = ubuntu ]]; then
echo "Ubuntu distribution detected"
# url may be changed for a different mirror
url="http://us.archive.ubuntu.com/ubuntu/pool/main/l/linux"
# The linux-source packages are formatted like the following: linux-source-3.13.0_3.13.0-24.46_all.deb
pkgver="${ARCHVERSION%%-*}_$(dpkg-query -W -f='${Version}' linux-image-$(uname -r))"
pkgname="linux-source-${pkgver}_all"
cd $TEMPDIR
echo "Downloading the kernel source for $ARCHVERSION"
# Download source deb pkg
(wget "$url/${pkgname}.deb" 2>&1) >> "$LOGFILE" || die "wget: Could not fetch $url/${pkgname}.deb"
# Unpack
echo "Unpacking kernel source"
dpkg -x ${pkgname}.deb $TEMPDIR >> "$LOGFILE" || die "dpkg: Could not extract ${pkgname}.deb"
# extract and move to SRCDIR
tar xvjf usr/src/linux-source-${ARCHVERSION%%-*}.tar.bz2 >> "$LOGFILE" || die "tar: Failed to extract kernel source package"
clean_cache
mv linux-source-${ARCHVERSION%%-*} "$SRCDIR" || die
cp "/boot/config-${ARCHVERSION}" "$OBJDIR/.config" || die
echo "-${ARCHVERSION#*-}" > "$SRCDIR/localversion" || die
# for some reason the Ubuntu kernel versions don't follow the
# upstream SUBLEVEL; they are always at SUBLEVEL 0
sed -i "s/^SUBLEVEL.*/SUBLEVEL = 0/" "$SRCDIR/Makefile" || die
echo $ARCHVERSION > "$VERSIONFILE" || die
elif [[ $DISTRO = debian ]]; then
echo "Debian distribution detected"
# url may be changed for a different mirror
url="http://ftp.debian.org/debian/pool/main/l/linux"
# The linux-source packages are formatted like the following: linux-source-3.14_3.14.7-1_all.deb
pkgver="${ARCHVERSION%%-*}_$(dpkg-query -W -f='${Version}' linux-image-$(uname -r))"
pkgname="linux-source-${pkgver}_all"
cd $TEMPDIR
echo "Downloading the kernel source for $ARCHVERSION"
# Download source deb pkg
(wget "$url/${pkgname}.deb" 2>&1) >> "$LOGFILE" || die "wget: Could not fetch $url/${pkgname}.deb"
# Unpack
echo "Unpacking kernel source"
dpkg -x ${pkgname}.deb $TEMPDIR >> "$LOGFILE" || die "dpkg: Could not extract ${pkgname}.deb"
# extract and move to SRCDIR
tar xvf usr/src/linux-source-${ARCHVERSION%%-*}.tar.xz >> "$LOGFILE" || die "tar: Failed to extract kernel source package"
clean_cache
mv linux-source-${ARCHVERSION%%-*} "$SRCDIR" || die
cp "/boot/config-${ARCHVERSION}" "$OBJDIR/.config" || die
echo "-${ARCHVERSION#*-}" > "$SRCDIR/localversion" || die
# for some reason the Debian kernel versions don't follow the
# upstream SUBLEVEL; they are always at SUBLEVEL 0
sed -i "s/^SUBLEVEL.*/SUBLEVEL = 0/" "$SRCDIR/Makefile" || die
echo $ARCHVERSION > "$VERSIONFILE" || die
else
die "Unsupported distribution"
fi
fi
echo "Testing patch file"
cd "$SRCDIR" || die
patch -N -p1 --dry-run < "$PATCHFILE" || die "source patch file failed to apply"
cp "$PATCHFILE" "$APPLIEDPATCHFILE" || die
echo "Building original kernel"
make mrproper >> "$LOGFILE" 2>&1 || die
make "-j$CPUS" $TARGETS "O=$OBJDIR" >> "$LOGFILE" 2>&1 || die
cp -LR "$DATADIR/patch" "$TEMPDIR" || die
echo "Building patched kernel"
patch -N -p1 < "$APPLIEDPATCHFILE" >> "$LOGFILE" 2>&1 || die
make "-j$CPUS" $TARGETS "O=$OBJDIR" 2>&1 | tee -a "$TEMPDIR/patched_build.log" >> "$LOGFILE"
[[ "${PIPESTATUS[0]}" -eq 0 ]] || die
echo "Detecting changed objects"
while read line; do
[[ "$line" =~ CC ]] || continue
eval set -- "$line"
case $2 in
init/version.o) continue ;;
scripts/mod/devicetable-offsets.s) continue ;;
scripts/mod/file2alias.o) continue ;;
*.mod.o) continue ;;
arch/x86/kernel/asm-offsets.s) die "a struct definition change was detected" ;;
\[M\]) obj=$3 ;;
*) obj=$2 ;;
esac
echo $obj >> $TEMPDIR/changed_objs
done < "$TEMPDIR/patched_build.log"
[[ ! -s "$TEMPDIR/changed_objs" ]] && die "no changed objects were detected"
echo "Rebuilding changed objects"
rm -rf "$OBJDIR2"
mkdir -p "$OBJDIR2"
cp "$OBJDIR/.config" "$OBJDIR2" || die
mkdir "$TEMPDIR/patched"
for i in $(cat $TEMPDIR/changed_objs); do
KCFLAGS="-ffunction-sections -fdata-sections" make "$i" "O=$OBJDIR2" >> "$LOGFILE" 2>&1 || die
mkdir -p "$TEMPDIR/patched/$(dirname $i)"
cp -f "$OBJDIR2/$i" "$TEMPDIR/patched/$i" || die
done
patch -R -p1 < "$APPLIEDPATCHFILE" >> "$LOGFILE" 2>&1
rm -f "$APPLIEDPATCHFILE"
mkdir "$TEMPDIR/orig"
for i in $(cat $TEMPDIR/changed_objs); do
rm -f "$i"
KCFLAGS="-ffunction-sections -fdata-sections" make "$i" "O=$OBJDIR2" >> "$LOGFILE" 2>&1 || die
mkdir -p "$TEMPDIR/orig/$(dirname $i)"
cp -f "$OBJDIR2/$i" "$TEMPDIR/orig/$i" || die
done
echo "Extracting new and modified ELF sections"
cd "$TEMPDIR/orig"
FILES="$(find * -type f)"
cd "$TEMPDIR"
mkdir output
declare -A objnames
for i in $FILES; do
mkdir -p "output/$(dirname $i)"
cd "$OBJDIR"
find_kobj $i
objnames[$KOBJFILE]=1
if [[ $KOBJFILE = vmlinux ]]; then
KOBJFILE=$VMLINUX
else
KOBJFILE="$(readlink -f $KOBJFILE)"
fi
cd $TEMPDIR
debugopt=
[[ $DEBUG -eq 1 ]] && debugopt=-d
"$TOOLSDIR"/create-diff-object $debugopt "orig/$i" "patched/$i" "$KOBJFILE" "output/$i" 2>&1 |tee -a "$LOGFILE"
rc="${PIPESTATUS[0]}"
if [[ $rc = 139 ]]; then
warn "create-diff-object SIGSEGV"
if ls core* &> /dev/null; then
cp core* /tmp
warn "core file at /tmp/$(ls core*)"
else
warn "no core file found, run 'ulimit -c unlimited' and try to recreate"
fi
exit 1
fi
[[ $rc -eq 0 ]] || die
done
echo -n "Patched objects:"
for i in "${!objnames[@]}"; do echo -n " $(basename $i)"; done
echo
echo "Building patch module: kpatch-$PATCHNAME.ko"
cp "$OBJDIR/.config" "$SRCDIR"
cd "$SRCDIR"
make prepare >> "$LOGFILE" 2>&1 || die
cd "$TEMPDIR/output"
ld -r -o ../patch/output.o $FILES >> "$LOGFILE" 2>&1 || die
cd "$TEMPDIR/patch"
KPATCH_BUILD="$SRCDIR" KPATCH_NAME="$PATCHNAME" KBUILD_EXTRA_SYMBOLS="$SYMVERSFILE" make "O=$OBJDIR" >> "$LOGFILE" 2>&1 || die
cp -f "$TEMPDIR/patch/kpatch-$PATCHNAME.ko" "$BASE" || die
[[ "$DEBUG" -eq 0 ]] && rm -f "$LOGFILE"
echo "SUCCESS"