#!/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.3-pre (git revision f80c2cf)"

LOGFILE=/var/log/kpatch.log

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 be loaded at boot"
	usage_cmd "uninstall [-k|--kernel-version=<kernel version>] {--all|<module>}" "uninstall patch module(s)"
	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" "show information about the 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
}

# log_event
# 	event_name: string, no spaces
#	patch_module: string, no spaces
# 	result: string, no spaces
# 	message: string, optional, may contain spaces
log_event() {
	event="$1"
	patchmod="$2"
	result="$3"
	message="$4"

	sep=""
	[[ -n "$message" ]] && sep=" "

	datetime=$(date --iso-8601=seconds)
	kernel=$(uname -r)
	echo "$datetime $event $patchmod $kernel $result$sep$message" >> "$LOGFILE"
}

log_success() {
	event="$1"
	patchmod="$2"
	log_event "$event" "$patchmod" "SUCCESS" ""
}

log_failure() {
	event="$1"
	patchmod="$2"
	message="$3"
	log_event "$event" "$patchmod" "FAIL" "$message"
}

__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 () {
	event="LOAD"
	[[ t"$2" == "treplace=1" ]] && event="REPLACE"
	modname=$(get_module_name $1)

	if ! core_module_loaded; then
		if modprobe -q kpatch; then
			echo "loaded core module"
		else
			if ! find_core_module; then
				log_failure $event $modname "Failed to load $1: can't find core module."
				die "can't find core module"
			fi
			echo "loading core module: $COREMOD"
			if ! insmod "$COREMOD"; then
				log_failure $event $modname "Failed to load $1: failed to load core module."
				die "failed to load core module"
			fi
		fi
	fi

	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
				if [[ $? -ne 0 ]]; then
					log_failure $event $modname "Failed to load $1: failed to re-enable module."
					die "failed to re-enable module $modname"
				fi
				return
			else
				log_failure $event $modname "Failed to load $1: failed to re-enable module, cannot verify checksum match."
				die "error: cannot re-enable patch module $modname, cannot verify checksum match"
			fi
		else
			log_failure $event $modname "Failed to load $1: the module is already loaded and enabled."
			die "error: module named $modname already loaded and enabled"
		fi
	fi

	echo "loading patch module: $1"
	if insmod "$1" "$2"; then
		log_success $event $modname
		return 0
	else
		log_failure $event $modname "Failed to load $1."
		return 1
	fi
}

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

	echo "unloading patch module: $PATCH"
	# ignore any error here because rmmod can fail if the module used
	# KPATCH_FORCE_UNSAFE.
	if rmmod $PATCH 2> /dev/null; then
		log_success $event $PATCH
	else
		log_failure $event $PATCH "Failed to unload $PATCH."
	fi

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

print_newest_patch_name() {
	for mod in /sys/kernel/kpatch/patches/*; do echo $(basename "$mod"); done \
		| grep kpatch_cumulative | sort -V -r | head -n 1
}

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"
	systemctl enable kpatch.service
	;;

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

	if [[ -n "$ALL" ]]; then
		for pp in $INSTALLDIR/$KVER/*.ko; do
			echo "uninstalling $(basename $pp) ($KVER)"
			rm -f $pp
		done
	else
		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"
	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
			mod_name "$module"
			echo "$MODNAME ($(basename $kdir))"
		done
	done
	;;

"info")
	[[ "$#" -ne 1 ]] && usage

	PATCH=$(print_newest_patch_name)
	if [[ -z "$PATCH" ]]; then
		NR_PATCHES=$(ls -ld /sys/kernel/kpatch/patches/kpatch* 2>/dev/null | wc -l)
		echo "Loaded patches: $NR_PATCHES."
		echo "'kpatch list' may provide more details."
		exit 1
	fi

	if find_module "$PATCH"; then
		echo "Patch module: $PATCH"
		echo -n "File: "
		modinfo -F "filename" "$MODULE" || die "failed to get info for module $PATCH"
	else
		echo "Patch module $PATCH is loaded but not installed."
	fi

	VZVER=$(uname -r | sed -e 's/^.*vz7\.//')
	VERREL_RAW=$(echo $PATCH | sed 's/^kpatch_cumulative_//;s/_/\./g')
	PATCHVER=$(echo $VERREL_RAW | sed 's/\.r.*$//')
	VERREL=$(echo $VERREL_RAW | sed 's/\.r/-/')

	echo "Version: $PATCHVER"

	PKG_NAME=
	if rpm -qi "readykernel-patch-$VZVER" > /dev/null 2>&1; then
		PKG_NAME="readykernel-patch-$VZVER"
	else
		if rpm -qi "kpatch-patch-$VZVER" > /dev/null 2>&1; then
			PKG_NAME="kpatch-patch-$VZVER"
		fi
	fi
	if [[ -n "$PKG_NAME" ]]; then
		NVR=$(rpm -q --qf '%{NAME}-%{VERSION}-%{RELEASE}\n' "$PKG_NAME")
		echo "Installed package: $NVR"
	fi

	echo
	INFO_FILE=/usr/share/readykernel-patch-$(uname -r)/info-$VERREL.txt
	if [[ -f "$INFO_FILE" ]]; then
		cat "$INFO_FILE"
	else
		INFO_FILE=/usr/share/kpatch-patch-$(uname -r)/info-$VERREL.txt
		[[ -f "$INFO_FILE" ]] && cat "$INFO_FILE"
	fi
	;;

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

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

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