#!/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 -l '^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() {
    unset PATCHES_FOUND
    for d in /sys/kernel/livepatch/*; do
	[ ! -d "$d" ] && continue
	PATCH_NAME=${d#/sys/kernel/livepatch/}
	PATCH_MOD=${PATCH_NAME}
	echo "${PATCH_MOD}"
	if [ -n "$VERBOSE" ]; then
	    klp_detailed_patch_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_detailed_patch_info() {
    REFCNT=$(cat "/sys/module/$1/refcnt")
    ACTIVE=$([[ "$REFCNT" -eq 0 ]]; echo $?)

    echo "active: ${ACTIVE}"

    # 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

    RPMNAME=$(rpm -qf "${MODPATH}" 2>/dev/null) || exit
    echo "RPM: ${RPMNAME}"
    REFS=($(rpm -q --changelog "${RPMNAME}" | \
	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
    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
}

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

Options:
	-h	   print this help
	-v	   more detailed output

Report bugs at https://bugzilla.suse.com/"
PKGVERSION="1.2-7.6.1"

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

shift `expr $OPTIND - 1`

if [ $# -ne 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 ;;
    patches) klp_patches ;;
    *) echo "Error: unknown command \`$1'"; exit 1 ;;
esac

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