#!/bin/bash

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

unset VERBOSE

function kgr_in_progress() {
    [ "$(cat /sys/kernel/kgraft/in_progress)" -ne 0 ]
}

function kgr_poke_processes() {
    if [[ $EUID -ne 0 ]]; then
       echo "Warning: running as non-root user, only this user's processes will be poked" >&2
    fi

    for PROC in /proc/[0-9]*; do
	if [ 0$(cat $PROC/kgr_in_progress 2>/dev/null) -ne 0 ]; then
	    PID=$(echo $PROC | cut -d/ -f3)
	    if [ -n "$VERBOSE" ]; then
		echo "sending $PID STOP/CONT"
	    fi
	    kill -STOP $PID
	    # give kernel time to distribute the signal to all threads
	    sleep .1
	    kill -CONT $PID
	fi
    done
}

function kgr_dump_blocking_processes() {
    local OPTIND
    local THREADS

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

    while getopts t opt; do
	case $opt in
	t) if [ ! -e /proc/1/task/1/kgr_in_progress ]; then
		echo "Warning: this kernel does not provide per-thread kGraft progress flag" >&2
		return
	   fi
           THREADS='yes' ;;
	*) echo "Warning: $FUNCNAME: ignoring unknown option \'$opt\'" >&2 ;;
	esac
    done

    unset PIDS

    for PROC in /proc/[0-9]*${THREADS:+/task/[0-9]*}; do
	if [ 0$(cat $PROC/kgr_in_progress 2>/dev/null) -ne 0 ]; then
	    DIR=${PROC%/kgr_in_progress}
	    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 ${THREADS:+$TID }$COMM"
			[ ${VERBOSE:-0} -gt 1 ] && echo "$STACK"
		fi
	    else
		echo $PID ${THREADS:+$TID}
	    fi
	    PIDS="$PIDS $PID"
	fi
    done
    if [ -z "$PIDS" -a -n "$VERBOSE" ]; then
	echo "no processes with kgr_in_progress set"
    fi
}

function kgr_status() {
    if kgr_in_progress ; then
	echo "in_progress"
    else
	echo "ready"
    fi
}

function kgr_check() {
    if kgr_in_progress ; then
	echo "Following processes have not finished a previous kGraft patch yet:"
	VERBOSE=2 kgr_dump_blocking_processes -t
	return 1
    fi
}

function kgr_patches() {
    unset PATCHES_FOUND
    for d in /sys/kernel/kgraft/*; do
	[ ! -d "$d" ] && continue
	PATCH_NAME=${d#/sys/kernel/kgraft/}
	PATCH_MOD=$(readlink $d/owner)
	PATCH_MOD=${PATCH_MOD#../../../module/}
	echo "${PATCH_MOD}"
	if [ -n "$VERBOSE" ]; then
	    kgr_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 kgr_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 '/^[[:space:]]*\(Fixes\|KLP\):[[:space:]]*[[:xdigit:]]\{12\}/d;
             s/^[[:space:]]*\(Fixes\|KLP\):[[:space:]]*\(.*\)/\2/;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 kGraft 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 and manipulate kGraft patching status.

Commands:
	status:    display the overall status of kGraft patching
	patches:   display the list of loaded patches
	blocking:  list processes that are preventing kGraft patching
		   from finishing
        blocking_threads: list execution threads that are preventing kGraft
                   patching from finishing
	poke:      move forward with the kGraft patching by sending
		   STOP and CONT signal to the pending processes

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

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

while getopts vh-: opt
do
    case $opt$OPTARG in
    -help|h)
	exec echo "$USAGE" ;;
    -version)
	exec echo "kgr $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) kgr_dump_blocking_processes ;;
    blocking_threads) kgr_dump_blocking_processes -t ;;
    poke) kgr_poke_processes ;;
    status) kgr_status ;;
    check) kgr_check ;;
    patches) kgr_patches ;;
    *) echo "Error: unknown command \`$1'"; exit 1 ;;
esac

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