#!/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="$(getconf _NPROCESSORS_ONLN)"
CACHEDIR="$HOME/.kpatch"
SRCDIR="$CACHEDIR/src"
OBJDIR="$CACHEDIR/obj"
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"
	unset KCFLAGS
}

clean_cache() {
	[[ -z $USERSRCDIR ]] && rm -rf "$SRCDIR"
	rm -rf "$OBJDIR" "$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
}

gcc_version_check() {
	# ensure gcc version matches that used to build the kernel
	local gccver=$(gcc --version |head -n1 |cut -d' ' -f3-)
	local kgccver=$(readelf -p .comment $VMLINUX |grep GCC: | tr -s ' ' | cut -d ' ' -f6-)
	if [[ $gccver != $kgccver ]]; then
		warn "gcc/kernel version mismatch"
		return 1
	fi

	# ensure gcc version is >= 4.8
	gccver=$(echo $gccver |cut -d'.' -f1,2)
	if [[ $gccver < 4.8 ]]; then
		warn "gcc >= 4.8 required"
		return 1
	fi

	return
}

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: $(basename $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 '-'.  Also truncate to 48 chars so the full name fits in the
# kernel's 56-byte module name array.
PATCHNAME=$(echo ${PATCHNAME//[^a-zA-Z0-9_-]/-} |cut -c 1-48)

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 ]] || [[ $DISTRO = debian ]]; then
	[[ -z $VMLINUX ]] && VMLINUX=/usr/lib/debug/boot/vmlinux-$ARCHVERSION

	if [[ $DISTRO = ubuntu ]]; then
		[[ -e "$VMLINUX" ]] || die "linux-image-$ARCHVERSION-dbgsym not installed"

        elif [[ $DISTRO = debian ]]; then
		[[ -e "$VMLINUX" ]] || die "linux-image-$ARCHVERSION-dbg not installed"
	fi

	export PATH=/usr/lib/ccache:$PATH
fi

gcc_version_check || die

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)" "$(rpm --eval %{_specdir})"/kernel.spec >> "$LOGFILE" 2>&1 ||
			die "rpmbuild -bp failed.  you may need to run 'yum-builddep kernel' first."

		clean_cache

		mv "$(rpm --eval %{_builddir})"/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 ]] || [[ $DISTRO = debian ]]; then

		echo "Debian/Ubuntu distribution detected"

		if [[ $DISTRO = ubuntu ]]; then

			# url may be changed for a different mirror
			url="http://us.archive.ubuntu.com/ubuntu/pool/main/l/linux"
			extension="bz2"
			sublevel="SUBLEVEL = 0"
			taroptions="xvjf"

		elif [[ $DISTRO = debian ]]; then

			# url may be changed for a different mirror
			url="http://ftp.debian.org/debian/pool/main/l/linux"
			extension="xz"
			sublevel="SUBLEVEL ="
			taroptions="xvf"
		fi

		# The linux-source packages are formatted like the following for:
		# ubuntu: linux-source-3.13.0_3.13.0-24.46_all.deb
		# debian: 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 $taroptions usr/src/linux-source-${ARCHVERSION%%-*}.tar.${extension} >> "$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}/" "$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
cp -LR "$DATADIR/patch" "$TEMPDIR" || die
export KCFLAGS="-I$DATADIR/patch -ffunction-sections -fdata-sections"

echo "Building original kernel"
make mrproper >> "$LOGFILE" 2>&1 || die
make "-j$CPUS" $TARGETS "O=$OBJDIR" >> "$LOGFILE" 2>&1 || 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"

mkdir "$TEMPDIR/patched"
for i in $(cat $TEMPDIR/changed_objs); do
	mkdir -p "$TEMPDIR/patched/$(dirname $i)"
	cp -f "$OBJDIR/$i" "$TEMPDIR/patched/$i" || die
done

echo "Rebuilding original objects"
patch -R -p1 < "$APPLIEDPATCHFILE" >> "$LOGFILE" 2>&1
rm -f "$APPLIEDPATCHFILE"
make "-j$CPUS" $TARGETS "O=$OBJDIR" >> "$LOGFILE" 2>&1 || die
mkdir "$TEMPDIR/orig"
for i in $(cat $TEMPDIR/changed_objs); do
	mkdir -p "$TEMPDIR/orig/$(dirname $i)"
	cp -f "$OBJDIR/$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"
