313 lines
8.1 KiB
Bash
313 lines
8.1 KiB
Bash
#!/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
|