kernel-livepatch-tools/klp.sh

313 lines
8.1 KiB
Bash
Raw Normal View History

#!/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
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_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 klp_info_from_rpm() {
RPMNAME=$(klp_patch_rpm_name "$1")
[ -n "$RPMNAME" ] || exit
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
declare -p RPMNAME
declare -p CVES
declare -p BUGS_FATES
}
function klp_detailed_patch_info() {
REFCNT=$(cat "/sys/module/$1/refcnt")
ACTIVE=$([[ "$REFCNT" -eq 0 ]]; echo $?)
echo "active: ${ACTIVE}"
# collect info if we have it; first try the "cache" (bsc#1191344)
SRCVERSION=$(cat "/sys/module/$1/srcversion")
CACHE_FILE="/var/cache/livepatch/$1-$SRCVERSION"
if [ -e "$CACHE_FILE" ]; then
. "$CACHE_FILE"
else
KLP_INFO=$(klp_info_from_rpm $1)
echo "$KLP_INFO" > "$CACHE_FILE"
eval "$KLP_INFO"
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
}
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
ZYPPER_COMMAND="zypper -n in --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 ;;
store_patch_info)
SRCVERSION=$(cat "/sys/module/$2/srcversion")
klp_info_from_rpm $2 > "/var/cache/livepatch/$2-$SRCVERSION" ;;
patches) klp_patches all ;;
downgrade) klp_downgrade ;;
*) echo "Error: unknown command \`$1'"; exit 1 ;;
esac
# vim: ai sw=4 et sts=4 ft=sh