#!/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.1.10"

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 "replace <module>" "load patch module into the running kernel, replacing all other modules"
	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
}

find_module () {
	arg="$1"
	__find_module "${arg}"
}

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
}

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
	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
}

unload_disabled_modules() {
	for module in /sys/kernel/kpatch/patches/*; do
		if [[ $(cat $module/enabled) -eq 0 ]]; then
			unload_module $(basename $module) || die "failed to unload $module"
		fi
	done
}

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
	;;

"replace")
	[[ "$#" -ne 2 ]] && usage
	PATCH="$2"
	find_module "$PATCH" || die "can't find $PATCH"
	load_module "$MODULE" replace=1 || die "failed to load module $PATCH"
	unload_disabled_modules || die "failed to unload old modules"
	;;

"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"

	if lsinitrd -k $KVER &> /dev/null; then
		echo "rebuilding $KVER initramfs"
		dracut -f --kver $KVER || die "dracut failed"
	fi
	;;

"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

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

	echo "uninstalling $PATCH ($KVER)"
	rm -f $INSTALLDIR/$KVER/"$PATCH" || die "failed to uninstall module $PATCH"
	if lsinitrd -k $KVER &> /dev/null; then
		echo "rebuilding $KVER initramfs"
		dracut -f --kver $KVER || die "dracut failed"
	fi
	;;

"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
			echo "$(basename $module) ($(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
