#!/bin/bash

# Check kernel live patching status
# Libor Pechacek <lpechacek@suse.com>

unset VERBOSE

function klp_in_progress() {
    for p in /sys/kernel/livepatch/*; do
	    [ 0$(cat "$p/transition" 2>/dev/null) -ne 0 ] && return 0
    done
    return 1
}

function klp_dump_blocking_threads() {
    if [[ $EUID -ne 0 ]]; then
       echo "Warning: running as non-root user, display will be limited" >&2
    fi

    unset PIDS

    TRANSITIONING_PATCH="$(grep -ls '^1$' /sys/kernel/livepatch/*/transition | head -n1)"

    if [ -n "$TRANSITIONING_PATCH" ]; then
	TRANSITION_DIRECTION=$(cat "${TRANSITIONING_PATCH/%\/transition/\/enabled}")

	for DIR in /proc/[0-9]*/task/[0-9]*; do
	    PATCH_STATE=$(cat $DIR/patch_state 2>/dev/null)
	    if [ -n "$PATCH_STATE" ] && [ "$PATCH_STATE" -ge 0 \
		-a "$PATCH_STATE" -ne "$TRANSITION_DIRECTION" ]; then
		PID=${DIR#/proc/}
		PID=${PID%/task/*}
		TID=${DIR#*/task/}
		if [ -n "$VERBOSE" ]; then
		    COMM="$(cat $DIR/cmdline 2>/dev/null | tr \\0 \ )"
		    # fallback to the command name, for example for kernel threads
		    [ -z "$COMM" ] && COMM="[$(cat $DIR/comm 2>/dev/null | tr \\0 \ )]"
		    if [ ${VERBOSE:-0} -gt 1 ]; then
			STACK=$(cat $DIR/stack 2>/dev/null | sed 's/^/  /')
		    fi
		    # don't write out anything in case the process has exited
		    if [ -e "$DIR" ]; then
			    echo "$PID $TID $COMM"
			    [ ${VERBOSE:-0} -gt 1 ] && echo "$STACK"
		    fi
		else
		    echo $PID $TID
		fi
		PIDS="$PIDS $PID"
	    fi
	done
    fi

    if [ -z "$PIDS" -a -n "$VERBOSE" ]; then
	echo "no threads with klp_in_progress set"
    fi
}

function klp_status() {
    if klp_in_progress ; then
	echo "in_progress"
    else
	echo "ready"
    fi
}

function klp_check() {
    if klp_in_progress ; then
	echo "Following processes have not finished a previous kernel live patching yet:"
	VERBOSE=2 klp_dump_blocking_threads
	return 1
    fi
}

function klp_patches() {
    local TYPE="$1"

    unset PATCHES_FOUND
    for d in /sys/kernel/livepatch/*; do
	[ ! -d "$d" ] && continue

	if [ "$TYPE" = "active" ] ; then
	    PATCH_ENABLED=$(cat "$d/enabled" 2>/dev/null)
	    [ "$PATCH_ENABLED" -ne 1 ] && continue
	fi

	PATCH_NAME=${d#/sys/kernel/livepatch/}
	PATCH_MOD=${PATCH_NAME}
	echo "${PATCH_MOD}"
	if [ -n "$VERBOSE" ]; then
	    dump_klp_info "${PATCH_MOD}" | sed 's/^/    /'
	    echo
	fi
	PATCHES_FOUND=1
    done
    if [ -z "$PATCHES_FOUND" -a -n "$VERBOSE" ]; then
	echo "no patch"
    fi
}

function klp_patch_rpm_name() {
    # srcversion is the link between loaded kernel module and its RPM
    SRCVERSION=$(cat "/sys/module/$1/srcversion")

    # exit when the module cannot be tracked down
    MODPATH=$(/usr/sbin/modinfo -n "$1" 2>/dev/null) || exit
    MODSRCVERSION=$(/usr/sbin/modinfo -F srcversion "$1")

    if [ "$SRCVERSION" != "$MODSRCVERSION" ]; then
	echo "Warning: patch module srcversion does not match the on-disk checksum:" \
	     "$1 ($SRCVERSION/$MODSRCVERSION)" >&2
        exit 1
    fi

    echo $(rpm -qf "${MODPATH}" 2>/dev/null)
}

function declare_cves_bugs_fates() {
    # Parse output from a command defined by function parameters.
    REFS=($($* | \
	sed 's/^[[:space:]]*KLP:[[:space:]]*\(.*\)/\1/;t b;d;:b s/[[:space:]]/\n/g' | \
	sort -ru))
    declare -a CVES
    declare -a BUGS_FATES
    for REF in "${REFS[@]}"; do
	if [ ${REF:0:3} = 'CVE' ]; then
	    CVES+=($REF)
	else
	    BUGS_FATES+=($REF)
	fi
    done

    declare -p CVES
    declare -p BUGS_FATES
}

function klp_info_from_rpm() {

    RPMNAME=$(klp_patch_rpm_name "$1")
    [ -n "$RPMNAME" ] || exit

    declare -p RPMNAME
    declare_cves_bugs_fates "rpm" "-q" "--changelog" "${RPMNAME}"
}

function klp_info_from_rpm_changes() {
    CHANGES="$1"
    RPMNAME="$2"

    declare -p RPMNAME
    declare_cves_bugs_fates "cat" "$CHANGES"
}

function create_klp_info_cache() {
    MODNAME="$1"
    MODSRCVERSION=$(/usr/sbin/modinfo -F srcversion "$1")

    # The cache is necessary on transactional systems where the currently
    # running system is not able to access metadata of newly installed packages.
    # Also it might speedup "klp -v patches" on non-transactional systems
    # when a pre-generated info file is not packaged.
    if ! is_transactional_system &&
       [ -r "/usr/share/livepatch/info/${MODNAME}-${MODSRCVERSION}" ] ; then
	return 0
    fi

    if [ ! -d /var/cache/livepatch ] ; then
	if [ -x /usr/bin/systemd-tmpfiles ] ; then
	    /usr/bin/systemd-tmpfiles --create livepatch.conf >/dev/null 2>&1
	fi
	if [ ! -d /var/cache/livepatch ] ; then
	    echo "Warning: Can't cache klp metadata. The directory does not exist and can't be created: /var/cache/livepatch"
	    return 0
	fi
    fi

    # Pre-built klp info is provided by recent packages (PED-14811)
    if test -r /usr/share/livepatch/info/${MODNAME}-${MODSRCVERSION} ; then
	cp /usr/share/livepatch/info/${MODNAME}-${MODSRCVERSION} /var/cache/livepatch/
    else
	klp_info_from_rpm ${MODNAME} > "/var/cache/livepatch/${MODNAME}-${MODSRCVERSION}"
    fi
}

function dump_klp_info() {
    MODNAME="$1"
    REFCNT=$(cat "/sys/module/$MODNAME/refcnt")
    ACTIVE=$([[ "$REFCNT" -eq 0 ]]; echo $?)

    echo "active: ${ACTIVE}"

    # collect info from:
    #   + pre-generated info file (PED-14811)
    #   + /var/cache (bsc#1191344)
    #   + rpm log
    SRCVERSION=$(cat "/sys/module/$MODNAME/srcversion")
    INFO_FILE="/usr/share/livepatch/info/$MODNAME-$SRCVERSION"
    CACHE_FILE="/var/cache/livepatch/$MODNAME-$SRCVERSION"
    if [ -e "$INFO_FILE" ]; then
	. "$INFO_FILE"
    else
	if [ ! -r "$CACHE_FILE" ]; then
	    create_klp_info_cache "$MODNAME"
	fi
	if [ -e "$CACHE_FILE" ]; then
	    . "$CACHE_FILE"
	else
	    KLP_INFO=$(klp_info_from_rpm $MODNAME)
	    eval "$KLP_INFO"
	fi
    fi

    [ -n "$RPMNAME" ] || exit
    echo "RPM: ${RPMNAME}"
    echo -n "CVE: "
    if [ ${#CVES[*]} -gt 0 ]; then
	echo ${CVES[*]}
    else
        echo -n "(none"
        [ ${#BUGS_FATES[*]} -eq 0 ] && echo -n " - this is an initial kernel live patch"
        echo ")"
    fi
    echo -n "bug fixes and enhancements: "
    if [ ${#BUGS_FATES[*]} -gt 0 ]; then
	echo ${BUGS_FATES[*]}
    else
	echo "(none)"
    fi

    if [ ${VERBOSE:-0} -gt 1 ]; then
	SHORT_RPMNAME=$(rpm -q --qf "%{name}" "$RPMNAME" 2>/dev/null)

	echo -n "Update status: "
	if zypper -qn --no-refresh up -D "$SHORT_RPMNAME" 2>/dev/null | fgrep -q "package to upgrade"; then
	    echo "newer version is available"
	else
	    echo "up to date"
	fi

	EXP_DATE=$(grep "^$SHORT_RPMNAME," /usr/share/lifecycle/data/sle-module-live-patching.lifecycle 2>/dev/null \
	    | cut -d, -f3)

	echo -n "Patches issued until: "
	if [ -n "$EXP_DATE" ]; then
	    echo "$EXP_DATE"
	else
	    echo "to be announced"
	fi
    fi
}

# This check is inspired by zypper source code
function is_transactional_system() {
    findmnt -O "ro" -t "btrfs" "/" >/dev/null 2>&1 || return 1
    which "transactional-update" >/dev/null 2>&1 || return 1
    return 0
}

function klp_downgrade()
{
    VERBOSE_ORIG="$VERBOSE"
    unset VERBOSE

    ACTIVE_PATCHES=$(klp_patches active)
    ACTIVE_PATCHES_NUM=$(echo $ACTIVE_PATCHES | wc -w)
    if [ "$ACTIVE_PATCHES_NUM" -eq 0 ] ; then
	echo "Error: cannot determine livepatch for downgrade. No active livepatch." >&2
	exit 1
    fi
    if [ "$ACTIVE_PATCHES_NUM" -gt 1 ] ; then
	echo "Error: cannot determine livepatch for downgrade. Too many active livepatches: $ACTIVE_PATCHES" >&2
	exit 1
    fi

    PATCH="$ACTIVE_PATCHES"
    RPM_FULL_NAME=$(klp_patch_rpm_name "$PATCH")
    if [ -z "$RPM_FULL_NAME" ]; then
	echo "Error: cannot determine RPM package for $PATCH" >&2
	exit 1
    fi

    RPM_INFO=$(rpm -q --qf '%{name};%{version}' "$RPM_FULL_NAME")
    RPM_VERSION=${RPM_INFO#*;}
    RPM_NAME=${RPM_INFO%;*}
    if [ "$RPM_VERSION" -le 1 ]; then
	echo "Error: $RPM_FULL_NAME is the initial kernel live patch and cannot be downgraded."
	exit 1
    fi

    PREV_RPM_VERSION=$(($RPM_VERSION-1))
    while [ "$PREV_RPM_VERSION" -gt 0 ] ; do
	zypper -n se -x "$RPM_NAME-$PREV_RPM_VERSION" >/dev/null 2>&1
	[ "$?" -eq 0 ] && break
	PREV_RPM_VERSION=$(($PREV_RPM_VERSION-1))
    done

    if [ "$PREV_RPM_VERSION" -le 0 ] ; then
	echo "Error: cannot find package with lower version. The currently loaded livepatch is from the package: "$RPM_NAME" = "$RPM_VERSION"" >&2
	exit 1
    fi

    if is_transactional_system ; then
	ZYPPER_COMMAND="transactional-update pkg in -n"
    else
	ZYPPER_COMMAND="zypper -n in"
    fi
    ZYPPER_COMMAND="$ZYPPER_COMMAND --oldpackage $RPM_NAME = $PREV_RPM_VERSION"
    echo "KLP tool will replace the current kernel live patch with its previous version."
    echo "The command for downgrade is: $ZYPPER_COMMAND"
    if [ -z "$NON_INTERACTIVE" ]; then
	read -p "Continue? (y/N) " -n 1 -r
	echo
    else
	REPLY=Y
    fi
    if [[ $REPLY =~ ^[Yy]$ ]]; then
	eval $ZYPPER_COMMAND
	exit_val="$?"
	[ "$exit_val" -ne 0 ] && exit $exit_val
    fi

    VERBOSE="$VERBOSE_ORIG"
}

USAGE="Usage: $0 [-h][-v] COMMAND
Query kernel live patching status.

Commands:
	status:    display the overall status of kernel live patching
	patches:   display the list of loaded patches
	blocking:  list execution threads that are preventing kernel
                   live patching from finishing
        downgrade: revert the current live patch by installing
                   the previous one

Options:
	-h	   print this help
	-n	   non-interactive mode
	-v	   more detailed output

Report bugs at https://bugzilla.suse.com/"
PKGVERSION="@@VERSION@@"

while getopts hnv-: opt
do
    case $opt$OPTARG in
    -help|h)
	exec echo "$USAGE" ;;
    -non-interactive|n)
	NON_INTERACTIVE=1 ;;
    -version)
	exec echo "klp $PKGVERSION" ;;
    -verbose|v) VERBOSE=$((${VERBOSE:-0} + 1)) ;;
    *)
	echo "$0: try '$0 --help'" >&2; exit 1 ;;
    esac
done

shift `expr $OPTIND - 1`

if [ $# -lt 1 ]; then
    echo -e "Error: no command provided\n" >&2
    echo "$USAGE"
    exit 1
fi

case $1 in
    blocking) klp_dump_blocking_threads ;;
    status) klp_status ;;
    check) klp_check ;;
    cache_klp_info)
	if [ $# -ne 2 ]; then
	    echo "Usage: $0 $1 <klp_module_name>"
	    exit 1
	fi
	create_klp_info_cache "$2"
	;;
    rpm_changes_to_klp_info)
	if [ $# -ne 4 ]; then
	    echo "Usage: $0 $1 <package.changes> <full_package_name> <output>"
	    exit 1
	fi
	klp_info_from_rpm_changes "$2" "$3" >"$4"
	;;
    patches) klp_patches all ;;
    downgrade) klp_downgrade ;;
    *) echo "Error: unknown command \`$1'"; exit 1 ;;
esac

# vim: ai sw=4 et sts=4 ft=sh
