#!/bin/bash
#
# kpatch hot patch module management script
#
# Copyright (C) 2014 Seth Jennings <sjenning@redhat.com>
# Copyright (C) 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 is the kpatch user script that manages installing, loading, and
# displaying information about kernel patch modules installed on the system.

INSTALLDIR=/var/lib/kpatch
SCRIPTDIR="$(readlink -f $(dirname $(type -p $0)))"
VERSION="0.3.2"

usage_cmd() {
	printf '   %-20s\n      %s\n' "$1" "$2" >&2
}

usage () {
	# ATTENTION ATTENTION ATTENTION ATTENTION ATTENTION ATTENTION
	# When changing this, please also update the man page.  Thanks!
	echo "usage: kpatch <command> [<args>]" >&2
	echo >&2
	echo "Valid commands:" >&2
	usage_cmd "install [-k|--kernel-version=<kernel version>] <module>" "install patch module to the initrd to be loaded at boot"
	usage_cmd "uninstall [-k|--kernel-version=<kernel version>] <module>" "uninstall patch module from the initrd"
	echo >&2
	usage_cmd "load --all" "load all installed patch modules into the running kernel"
	usage_cmd "load <module>" "load patch module into the running kernel"
	usage_cmd "unload --all" "unload all patch modules from the running kernel"
	usage_cmd "unload <module>" "unload patch module from the running kernel"
	echo >&2
	usage_cmd "info <module>" "show information about a patch module"
	echo >&2
	usage_cmd "list" "list installed patch modules"
	echo >&2
	usage_cmd "version" "display the kpatch version"
	exit 1
}

warn() {
	echo "kpatch: $@" >&2
}

die() {
	warn "$@"
	exit 1
}

__find_module () {
	MODULE="$1"
	[[ -f "$MODULE" ]] && return

	MODULE=$INSTALLDIR/$(uname -r)/"$1"
	[[ -f "$MODULE" ]] && return

	return 1
}

mod_name () {
	MODNAME="$(basename $1)"
	MODNAME="${MODNAME%.ko}"
	MODNAME="${MODNAME//-/_}"
}

find_module () {
	arg="$1"
	if [[ "$arg" =~ \.ko ]]; then
		__find_module "$arg" || return 1
		mod_name "$MODULE"
		return
	else
		for i in $INSTALLDIR/$(uname -r)/*; do
			mod_name "$i"
			if [[ $MODNAME == $arg ]]; then
				MODULE="$i"
				return
			fi
		done
	fi

	return 1
}

find_core_module() {
	COREMOD="$SCRIPTDIR"/../kmod/core/kpatch.ko
	[[ -f "$COREMOD" ]] && return

	COREMOD="/usr/local/lib/kpatch/$(uname -r)/kpatch.ko"
	[[ -f "$COREMOD" ]] && return

	COREMOD="/usr/lib/kpatch/$(uname -r)/kpatch.ko"
	[[ -f "$COREMOD" ]] && return

	COREMOD="/usr/local/lib/modules/$(uname -r)/extra/kpatch/kpatch.ko"
	[[ -f "$COREMOD" ]] && return

	COREMOD="/usr/lib/modules/$(uname -r)/extra/kpatch/kpatch.ko"
	[[ -f "$COREMOD" ]] && return

	return 1
}

core_module_loaded () {
	grep -q "T kpatch_register" /proc/kallsyms
}

get_module_name () {
	echo $(readelf -p .gnu.linkonce.this_module $1 | grep '\[.*\]' | awk '{print $3}')
}

verify_module_checksum () {
    modname=$(get_module_name $1)
    [[ -z $modname ]] && return 1

    checksum=$(readelf -p .kpatch.checksum $1 | grep '\[.*\]' | awk '{print $3}')
    [[ -z $checksum ]] && return 1

    sysfs_checksum=$(cat /sys/kernel/kpatch/patches/${modname}/checksum)
    [[ $checksum == $sysfs_checksum ]] || return 1
}

load_module () {
	if ! core_module_loaded; then
		if modprobe -q kpatch; then
			echo "loaded core module"
		else
			find_core_module || die "can't find core module"
			echo "loading core module: $COREMOD"
			insmod "$COREMOD" || die "failed to load core module"
		fi
	fi

	modname=$(get_module_name $1)
	moddir=/sys/kernel/kpatch/patches/$modname
	if [[ -d $moddir ]] ; then
		if [[ $(cat "${moddir}/enabled") -eq 0 ]]; then
			if verify_module_checksum $1; then # same checksum
				echo "module already loaded, re-enabling"
				echo 1 > ${moddir}/enabled || die "failed to re-enable module $modname"
				return
			else
				die "error: cannot re-enable patch module $modname, cannot verify checksum match"
			fi
		else
			die "error: module named $modname already loaded and enabled"
		fi
	fi

	echo "loading patch module: $1"
	insmod "$1" "$2"
}

unload_module () {
	PATCH="${1//-/_}"
	PATCH="${PATCH%.ko}"
	ENABLED=/sys/kernel/kpatch/patches/"$PATCH"/enabled
	[[ -e "$ENABLED" ]] || die "patch module $1 is not loaded"
	if [[ $(cat "$ENABLED") -eq 1 ]]; then
		echo "disabling patch module: $PATCH"
		echo 0 > $ENABLED || die "can't disable $PATCH"
	fi

	echo "unloading patch module: $PATCH"
	# ignore any error here because rmmod can fail if the module used
	# KPATCH_FORCE_UNSAFE.
	rmmod $PATCH 2> /dev/null || return 0
}

get_module_version() {
	MODVER=$(modinfo -F vermagic "$1") || return 1
	MODVER=${MODVER/ */}
}

unset MODULE
[[ "$#" -lt 1 ]] && usage
case "$1" in
"load")
	[[ "$#" -ne 2 ]] && usage
	case "$2" in
	"--all")
		for i in "$INSTALLDIR"/$(uname -r)/*.ko; do
			[[ -e "$i" ]] || continue
			load_module "$i" || die "failed to load module $i"
		done
		;;
	*)
		PATCH="$2"
		find_module "$PATCH" || die "can't find $PATCH"
		load_module "$MODULE" || die "failed to load module $PATCH"
		;;
	esac
	;;

"unload")
	[[ "$#" -ne 2 ]] && usage
	case "$2" in
	"--all")
		for module in /sys/kernel/kpatch/patches/*; do
			[[ -e $module ]] || continue
			unload_module $(basename $module) || die "failed to unload module $module"
		done
		;;
	*)
		unload_module "$(basename $2)" || die "failed to unload module $2"
		;;
	esac
	;;

"install")
	KVER=$(uname -r)
	shift
	options=$(getopt -o k: -l "kernel-version:" -- "$@") || die "getopt failed"
	eval set -- "$options"
	while [[ $# -gt 0 ]]; do
		case "$1" in
		-k|--kernel-version)
			KVER=$2
			shift
			;;
		--)
			[[ -z "$2" ]] && die "no module file specified"
			PATCH="$2"
			;;
		esac
		shift
	done

	[[ ! -e "$PATCH" ]] && die "$PATCH doesn't exist"
	[[ ${PATCH: -3} == ".ko" ]] || die "$PATCH isn't a .ko file"

	get_module_version "$PATCH" || die "modinfo failed"
	[[ $KVER != $MODVER ]] && die "invalid module version $MODVER for kernel $KVER"

	[[ -e $INSTALLDIR/$KVER/$(basename "$PATCH") ]] && die "$PATCH is already installed"

	echo "installing $PATCH ($KVER)"
	mkdir -p $INSTALLDIR/$KVER || die "failed to create install directory"
	cp -f "$PATCH" $INSTALLDIR/$KVER || die "failed to install module $PATCH"
	systemctl enable kpatch.service
	;;

"uninstall")
	KVER=$(uname -r)
	shift
	options=$(getopt -o k: -l "kernel-version:" -- "$@") || die "getopt failed"
	eval set -- "$options"
	while [[ $# -gt 0 ]]; do
		case "$1" in
		-k|--kernel-version)
			KVER=$2
			shift
			;;
		--)
			[[ -z "$2" ]] && die "no module file specified"
			PATCH="$2"
			[[ "$PATCH" != $(basename "$PATCH") ]] && die "please supply patch module name without path"
			;;
		esac
		shift
	done

	MODULE=$INSTALLDIR/$KVER/"$PATCH"
	if [[ ! -f "$MODULE" ]]; then
		mod_name "$PATCH"
		PATCHNAME=$MODNAME
		for i in $INSTALLDIR/$KVER/*; do
			mod_name "$i"
			if [[ $MODNAME == $PATCHNAME ]]; then
				MODULE="$i"
				break
			fi
		done
	fi

	[[ ! -e $MODULE ]] && die "$PATCH is not installed for kernel $KVER"
	

	echo "uninstalling $PATCH ($KVER)"
	rm -f $MODULE || die "failed to uninstall module $PATCH"
	;;

"list")
	[[ "$#" -ne 1 ]] && usage
	echo "Loaded patch modules:"
	for module in /sys/kernel/kpatch/patches/*; do
		if [[ -e $module ]] && [[ $(cat $module/enabled) -eq 1 ]]; then
			echo $(basename "$module")
		fi
	done
	echo ""
	echo "Installed patch modules:"
	for kdir in $INSTALLDIR/*; do
		[[ -e "$kdir" ]] || continue
		for module in $kdir/*; do
			[[ -e "$module" ]] || continue
			mod_name "$module"
			echo "$MODNAME ($(basename $kdir))"
		done
	done
	;;

"info")
	[[ "$#" -ne 2 ]] && usage
	PATCH="$2"
	find_module "$PATCH" || die "can't find $PATCH"
	echo "Patch information for $PATCH:"
	modinfo "$MODULE" || die "failed to get info for module $PATCH"
	;;

"help"|"-h"|"--help")
	usage
	;;

"version"|"-v"|"--version")
	echo "$VERSION"
	;;

*)
	echo "subcommand $1 not recognized"
	usage
	;;
esac
