#!/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"
TEMPDIR=
STRIPCMD="strip -d --keep-file-symbols"
APPLIEDPATCHFILE="applied-patch"
DEBUG=0

die() {
	if [[ -z $1 ]]; then
		echo "ERROR: kpatch build failed. Check $LOGFILE for more details." >&2
	else
		echo "ERROR: $1" >&2
	fi
	exit 1
}

cleanup() {
	[[ "$DEBUG" -eq 0 ]] && rm -rf "$TEMPDIR"
	if [[ -e "$SRCDIR/$APPLIEDPATCHFILE" ]]; then
		patch -p1 -R -d "$SRCDIR" < "$SRCDIR/$APPLIEDPATCHFILE" &> /dev/null
		rm -f "$SRCDIR/$APPLIEDPATCHFILE"
	fi
}

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
}

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 "		-d, --debug	Keep scratch files in /tmp" >&2
}

options=$(getopt -o hr:s:c:d -l "help,sourcerpm:,sourcedir:,config:,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"
		;;
	-d|--debug)
		echo "DEBUG mode enabled"
		DEBUG=1
		;;
	--)
		if [[ -z "$2" ]]; then
			echo "ERROR: no patch file specified" >&2
			usage
			exit 1
		fi
		PATCHFILE=$(readlink -f "$2")
		[[ ! -f "$PATCHFILE" ]] && die "patch file $PATCHFILE not found"
		break
		;;
	esac
	shift
done

SRCDIR="$CACHEDIR/$ARCHVERSION/src"
OBJDIR="$CACHEDIR/$ARCHVERSION/obj"
OBJDIR2="$CACHEDIR/$ARCHVERSION/obj2"

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_-]/-}

TEMPDIR="$(mktemp -d /tmp/kpatch-build-XXXXXX)" || die "mktemp failed"

trap cleanup EXIT INT TERM

find_dirs || die "can't find supporting tools"

[[ -e "$SYMVERSFILE" ]] || die "can't find core module Module.symvers"

[[ -z $VMLINUX ]] && VMLINUX=/usr/lib/debug/lib/modules/${ARCHVERSION}/vmlinux
[[ -e "$VMLINUX" ]] || die "kernel-debuginfo not installed"

if [[ -n "$USERSRCDIR" ]]; then
	SRCDIR="$CACHEDIR/src"
	OBJDIR="$CACHEDIR/obj"
	OBJDIR2="$CACHEDIR/obj2"

	rm -rf "$CACHEDIR"
	mkdir -p "$CACHEDIR"
	mkdir -p "$OBJDIR" "$OBJDIR2"

	if [[ -n "$CONFIGFILE" ]]; then
		cp "$CONFIGFILE" "$OBJDIR/.config" || die "config file is missing"
	else
		cp "$USERSRCDIR/.config" "$OBJDIR" || die "source dir is missing a .config file"
	fi

	echo "Copying source to $SRCDIR"
	cp -a "$USERSRCDIR" "$SRCDIR" || die "copy failed"

elif [[ -d "$SRCDIR" ]]; then
	echo "Using cache at $SRCDIR"

else
	source /etc/os-release
	if [[ $ID = fedora ]] || [[ $ID = 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."
		rm -rf "$CACHEDIR"
		mkdir -p "$OBJDIR"
		mv "$HOME"/rpmbuild/BUILD/kernel-*/linux-"$ARCHVERSION" "$SRCDIR" >> "$LOGFILE" 2>&1 || die

		cp "$SRCDIR/.config" "$OBJDIR" || die
		echo "-${ARCHVERSION##*-}" > "$SRCDIR/localversion" || die

	elif [[ $ID = ubuntu ]]; then

		echo "Debian/Ubuntu distribution detected"

		cd $TEMPDIR
		echo "Downloading and unpacking kernel source for $ARCHVERSION"
		apt-get source linux || die "'apt-get source linux' failed. you may need to run 'apt-get install dpkg-dev'"
		mkdir -p "$OBJDIR"
		mv linux-${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

	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" vmlinux "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" vmlinux "O=$OBJDIR" 2>&1 | tee -a "$TEMPDIR/patched_build.log" >> "$LOGFILE"
[[ "${PIPESTATUS[0]}" -eq 0 ]] || die

echo "Detecting changed objects"
grep CC "$TEMPDIR/patched_build.log" | grep -v -e init/version.o -e scripts/mod/devicetable-offsets.s -e scripts/mod/file2alias.o | awk '{print $2}' > "$TEMPDIR/changed_objs"
[[ ! -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
	$STRIPCMD "$OBJDIR2/$i" >> "$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
	$STRIPCMD -d "$OBJDIR2/$i" >> "$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
for i in $FILES; do
	mkdir -p "output/$(dirname $i)"
	"$TOOLSDIR"/create-diff-object "orig/$i" "patched/$i" "$VMLINUX" "output/$i" 2>&1 |tee -a "$LOGFILE"
	[[ "${PIPESTATUS[0]}" -eq 0 ]] || die
done

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
$STRIPCMD "kpatch-$PATCHNAME.ko" >> "$LOGFILE" 2>&1 || die

cp -f "$TEMPDIR/patch/kpatch-$PATCHNAME.ko" "$BASE" || die

[[ "$DEBUG" -eq 0 ]] && rm -f "$LOGFILE"

echo "SUCCESS"
