#!/bin/bash # Check kernel live patching status # Libor Pechacek 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