#!/bin/bash

# Copyright (c) 2018  Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

VERSION="1.6"

# Warning! Be sure to download the latest version of this script from its primary source:
# https://access.redhat.com/security/vulnerabilities/l1tf
# DO NOT blindly trust any internet sources and NEVER do `curl something | bash`!

# This script is meant for simple detection of the vulnerability. Feel free to modify it for your
# environment or needs. For more advanced detection, consider Red Hat Insights:
# https://access.redhat.com/products/red-hat-insights#getstarted


ARTICLE="https://access.redhat.com/security/vulnerabilities/l1tf"
ARTICLE_PERF="https://access.redhat.com/security/vulnerabilities/l1tf-perf"


basic_args() {
    # Parses basic commandline arguments and sets basic environment.
    #
    # Args:
    #     parameters - an array of commandline arguments
    #
    # Side effects:
    #     Exits if --help parameters is used
    #     Sets COLOR constants and debug variable

    local parameters=( "$@" )

    RED="\\033[1;31m"
    GREEN="\\033[1;32m"
    BOLD="\\033[1m"
    RESET="\\033[0m"
    for parameter in "${parameters[@]}"; do
        if [[ "$parameter" == "-h" || "$parameter" == "--help" ]]; then
            echo "Usage: $( basename "$0" ) [-n | --no-colors] [-d | --debug]"
            exit 1
        elif [[ "$parameter" == "-n" || "$parameter" == "--no-colors" ]]; then
            RED=""
            GREEN=""
            BOLD=""
            RESET=""
        elif [[ "$parameter" == "-d" || "$parameter" == "--debug" ]]; then
            debug=true
        fi
    done
}


basic_reqs() {
    # Prints common disclaimer and checks basic requirements.
    #
    # Args:
    #     CVE - string printed in the disclaimer
    #
    # Side effects:
    #     Exits when 'rpm' command is not available

    local CVE="$1"

    # Disclaimer
    echo
    echo -e "$CVE Detection Script Ver. $VERSION"
    echo -e "${BOLD}This script is primarily designed to detect $CVE on supported"
    echo -e "Red Hat Enterprise Linux systems and kernel packages."
    echo -e "Result may be inaccurate for other RPM based systems.${RESET}"
    echo

    # RPM is required
    if ! command -v rpm &> /dev/null; then
        echo "'rpm' command is required, but not installed. Exiting."
        exit 1
    fi
}


read_array() {
    # Reads lines from stdin and saves them in a global array referenced by a name.
    # It is a poor man's readarray compatible with Bash 3.1.
    #
    # Args:
    #     array_name - name of the global array
    #
    # Side effects:
    #     Overwrites content of the array 'array_name' with lines from stdin
    local array_name="$1"

    local i=0
    while IFS= read -r line; do
        read -r "${array_name}[$(( i++ ))]" <<< "$line"
    done
}


check_supported_kernel() {
    # Checks if running kernel is supported.
    #
    # Args:
    #     running_kernel - kernel string as returned by 'uname -r'
    #
    # Side effects:
    #     Exits when running kernel is obviously not supported

    local running_kernel="$1"

    # Check supported platform
    if [[ "$running_kernel" != *".el"[5-8]* ]]; then
        echo -e "${RED}This script is meant to be used only on RHEL 5-8.${RESET}"
        exit 1
    fi
}


get_rhel() {
    # Gets RHEL number.
    #
    # Args:
    #     running_kernel - kernel string as returned by 'uname -r'
    #
    # Prints:
    #     RHEL number, e.g. '5', '6', '7', or '8'

    local running_kernel="$1"

    local rhel
    rhel=$( sed -r -n 's/^.*el([[:digit:]]).*$/\1/p' <<< "$running_kernel" )
    echo "$rhel"
}


check_affected_cpu() {
    # Checks for affected CPU vendor/architecture.
    #
    # Prints:
    #     CPU vendor, e.g. 'Intel', 'AMD', 'POWER', or 'Other'
    #
    # Returns:
    #     1 if affected CPU found, otherwise 0
    #
    # Notes:
    #     MOCK_CPU_INFO_PATH can be used to mock /proc/cpuinfo file

    local cpuinfo=${MOCK_CPU_INFO_PATH:-/proc/cpuinfo}

    if grep --quiet "GenuineIntel" "$cpuinfo"; then
        echo "Intel"
        return 1
    fi

    if grep --quiet "AuthenticAMD" "$cpuinfo"; then
        echo "AMD"
        return 0
    fi
    if grep --quiet "POWER" "$cpuinfo"; then
        echo "POWER"
        return 0
    fi

    echo "Other"
    return 0
}


check_updated_kernel() {
    # Checks the kernel was updated to fixed one.
    #
    # Returns:
    #     1 if using the updated kernel, otherwise 0
    #
    # Notes:
    #     MOCK_VULN_FILE_PATH can be used to mock /sys/devices/system/cpu/vulnerabilities/l1tf file

    local vuln_file=${MOCK_VULN_FILE_PATH:-/sys/devices/system/cpu/vulnerabilities/l1tf}

    # Updated kernel introduces new vulnerability file in sysfs interface:
    #   /sys/devices/system/cpu/vulnerabilities/l1tf
    # And interface for disabling SMT.
    #   /sys/devices/system/cpu/smt
    #   /sys/devices/system/cpu/smt/active
    #   /sys/devices/system/cpu/smt/control
    # SMT interface is not available on RHEL-5.
    if [[ -f "${vuln_file}" ]]; then
        # Available - vulnerability file.
        return 1
    fi

    return 0  # Kernel not updated.
}


check_updated_microcode() {
    # Checks if microcode was updated and properly loaded. Updated kernel
    # with updated microcode introduces cpu flag 'flush_l1d'.
    #
    # Returns:
    #     1 if microcode is updated, otherwise 0
    #
    # Notes:
    #     MOCK_CPU_INFO_PATH can be used to mock /proc/cpuinfo file

    local cpuinfo=${MOCK_CPU_INFO_PATH:-/proc/cpuinfo}
    local cpuflags
    local flags

    cpuflags=()
    read_array cpuflags <<< "$( sed -rn 's/flags\s+: (.*)/\1/p' "$cpuinfo" )"

    for flags in "${cpuflags[@]}"; do
        # Any CPU that does not have the flag present.
        if grep --quiet -v 'flush_l1d' <<< "$flags" ; then
            return 0  # Microcode not updated.
        fi
    done

    # The flag 'flush_l1d' is present for all CPUs. Microcode was updated.
    return 1
}


check_mitigation_active() {
    # Check the mitigation type in the vulnerability file.
    #
    # Prints:
    #     Value from vulnerability file, e.g. 'Mitigation: PTE Inversion', 'Vulnerable',
    #     'Mitigation: PTE Inversion; VMX: SMT vulnerable, L1D conditional cache flushes',
    #     or 'Not affected'
    #
    # Returns:
    #     1 if mitigation active, otherwise 0
    #
    # Notes:
    #     MOCK_VULN_FILE_PATH can be used to mock /sys/devices/system/cpu/vulnerabilities/l1tf file

    local vuln_file=${MOCK_VULN_FILE_PATH:-/sys/devices/system/cpu/vulnerabilities/l1tf}
    local status

    # Is cpu/vulnerability files present?
    if [[ -r "${vuln_file}" ]]; then
        # Read status from vulnerability file.
        status=$( <"$vuln_file" )
        # With command 'l1tf=off' file contains this value on rhel-7 and
        # the mitigation is not active.
        if grep --quiet 'L1D vulnerable' <<< "$status"; then
            echo "$status"
            return 0  # Mitigation not active.
        fi
        # The status should not be 'Vulnerable'.
        if grep --quiet -v 'Vulnerable' <<< "$status"; then
            echo "$status"
            return 1  # Mitigation active.
        fi
    fi

    return 0  # Mitigation not active.
}


check_smt_enabled() {
    # Check if hyper-threading (SMT) is enabled.
    # 'lscpu' is not available on rhel-5 and cannot be used for detection.
    #
    # Returns:
    #     1 if smt enabled, otherwise 0
    #
    # Notes:
    #     MOCK_CPU_DIRS_PATH can be used to mock files in /sys/devices/system/cpu.

    local sysfs_cpu_dirs=${MOCK_CPU_DIRS_PATH:-/sys/devices/system/cpu}
    local cpu_number
    local main_thread_number
    local siblings_list_file

    # At least one SMT CPU thread is enabled.
    for cpu_directory in "${sysfs_cpu_dirs}"/cpu[0-9]*; do
        cpu_number="${cpu_directory##*cpu}"
        siblings_list_file="$cpu_directory/topology/thread_siblings_list"
        # Example output with disabled SMT: '0'
        # Example output with enabled SMT: '0-1'
        if [[ -r "${siblings_list_file}" ]]; then
            main_thread_number=$( < "$siblings_list_file" )
            if (( cpu_number != main_thread_number )); then
                return 1  # SMT enabled
            fi
        fi
    done

    return 0  # SMT disabled
}


check_dmesg() {
    # Checks if dmesg log or dmesg command contains given string:
    # "System has more than MAX_PA/2 memory. L1TF mitigation not effective."
    #
    # Returns:
    #     1 if given string present, otherwise 0
    #
    # Notes:
    #     MOCK_DMESG_PATH can be used to mock /var/log/dmesg file

    local dmesg_log=${MOCK_DMESG_PATH:-/var/log/dmesg}
    local dmesg_cmd
    dmesg_cmd=$( dmesg )
    local max_pa_half="System has more than MAX_PA/2 memory. L1TF mitigation not effective."

    if grep --quiet "$max_pa_half" "$dmesg_log"; then
        return 1
    fi
    if grep --quiet "$max_pa_half" <<< "$dmesg_cmd"; then
        return 1
    fi

    return 0
}


require_root() {
    # Checks if user is root.
    #
    # Side effects:
    #     Exits when user is not root.
    #
    # Notes:
    #     MOCK_EUID can be used to mock EUID variable

    local euid=${MOCK_EUID:-$EUID}

    # Am I root?
    if (( euid != 0 )); then
        echo "This script must run with elevated privileges (e.g. as root)"
        exit 1
    fi
}


get_virtualization() {
    # Gets virtualization type.
    #
    # Prints:
    #     Virtualization type, "None", or "virt-what not available"

    local virt
    if command -v virt-what &> /dev/null; then
        virt=$( virt-what 2>&1 | tr '\n' ' ' )
        if [[ "$virt" ]]; then
            echo "$virt"
        else
            echo "None"
        fi
    else
        echo "virt-what not available"
    fi
}


if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    require_root  # Needed for virt-what and reading debugfs
    basic_args "$@"
    basic_reqs "CVE-2018-3620"
    running_kernel=$( uname -r )
    check_supported_kernel "$running_kernel"

    rhel=$( get_rhel "$running_kernel" )
    if [[ "$rhel" == "5" ]]; then
        export PATH='/sbin':$PATH
    fi

    # Checks
    vendor=$( check_affected_cpu )
    # Store result as a flag, 1 as True, 0 as False
    # shellcheck disable=SC2181
    affected_cpu=$(( $? ))  # store 1 as True, 0 as False
    check_updated_kernel
    # Store result as a flag, 1 as True, 0 as False
    # shellcheck disable=SC2181
    kernel_updated=$(( $? ))
    check_updated_microcode
    # Store result as a flag, 1 as True, 0 as False
    # shellcheck disable=SC2181
    microcode_updated=$(( $? ))
    mitigation=$( check_mitigation_active )
    # Store result as a flag, 1 as True, 0 as False
    # shellcheck disable=SC2181
    mitigation_active=$(( $? ))
    check_smt_enabled
    # Store result as a flag, 1 as True, 0 as False
    # shellcheck disable=SC2181
    smt_enabled=$(( $? ))
    virtualization=$( get_virtualization )
    # System is vulnerable when it has affected CPU and at least one
    # of required mitigation steps is not applied.
    vulnerable=$(( affected_cpu && (!kernel_updated || !mitigation_active) ))
    check_dmesg
    # Store result as a flag, 1 as True, 0 as False
    # shellcheck disable=SC2181
    dmesg_too_much_memory=$(( $? ))

    # Debug prints
    if [[ "$debug" ]]; then
        variables=( running_kernel rhel vendor affected_cpu kernel_updated
                    microcode_updated mitigation_active mitigation smt_enabled
                    virtualization vulnerable dmesg_too_much_memory
                    )
        for variable in "${variables[@]}"; do
            echo "$variable = *${!variable}*"
        done
        echo
    fi

    # Output
    echo -e "CPU vendor: ${BOLD}$vendor${RESET}"
    echo -e "Running kernel: ${BOLD}$running_kernel${RESET}"
    echo -e "Virtualization: ${BOLD}$virtualization${RESET}"
    if (( smt_enabled )); then
        echo -e "SMT status: ${BOLD}On${RESET}"
    else
        echo -e "SMT status: ${BOLD}Off${RESET}"
    fi
    if (( mitigation_active )); then
        echo -e "Mitigation: ${BOLD}$mitigation${RESET}"
    fi
    echo

    # Results
    if (( vulnerable )); then
        echo -e "This system is ${RED}vulnerable${RESET} for the following reasons:"
        if (( ! kernel_updated )); then
            echo -e "* Kernel is not updated"
        fi
        if (( ! mitigation_active )); then
            echo -e "* Mitigation is not active"
        fi
        if (( dmesg_too_much_memory )); then
            echo -e "* The system has more than MAX_PA/2 memory"
        fi
        echo

        echo "Red Hat recommends that you:"
        if (( ! kernel_updated )); then
            echo -e "* Update your kernel and reboot the system."
            echo -e "* Optionally ask your HW vendor for CPU microcode update if available."
        fi
        if (( kernel_updated && ! mitigation_active )); then
            echo -e "* Activate the mitigation provided by the kernel"
        fi
        if (( dmesg_too_much_memory )); then
            echo -e "* Boot the kernel with the half of maximum possible memory parameter."
        fi
        echo
        result=1
    else
        if (( ! affected_cpu )); then
            echo -e "This system is ${GREEN}not vulnerable${RESET}, because your CPU vendor is not affected."
            echo
        else
            echo -e "This system is ${GREEN}not vulnerable${RESET}, because it has correct mitigation applied."
            echo
        fi
        result=0
    fi

    # Final notes
    if (( affected_cpu && ! microcode_updated )); then
        echo -e "${BOLD}Note about microcode update${RESET}"
        echo -e "Customers desiring performance improvement will need to ask their HW vendor for CPU microcode update"
        echo -e "in order to use the mitigation implemented in the microcode."
        echo
    fi

    if (( affected_cpu && smt_enabled )); then
        echo -e "${BOLD}Note about Hyper-Threading (SMT)${RESET}"
        echo -e "It is not possible to provide complete mitigation while SMT is enabled."
        echo -e "Customers desiring to completely mitigate this issue will need to consider disabling SMT."
        echo -e "For details how to disable SMT see:"
        echo -e "https://access.redhat.com/solutions/352663"
        echo -e "For details about performance considerations for L1 Terminal Fault see:"
        echo -e "https://access.redhat.com/security/vulnerabilities/L1TF-perf"
        echo
    fi

    if [[ "$virtualization" != "None" ]]; then
        echo -e "${BOLD}Note about virtualization${RESET}"
        echo -e "${BOLD}CVE-2018-3646${RESET} is the CVE identifier assigned to the virtualization escape aspect of the"
        echo -e "flaw. In virtualized environment, there are more steps to mitigate the issue, including:"
        echo -e "* Host needs to have updated kernel and CPU microcode"
        echo -e "* Host needs to have updated virtualization software"
        echo -e "* Guest needs to have updated kernel"
        echo -e "* Hypervisor needs to propagate new CPU features correctly"
        echo -e "For more details about mitigations in virtualized environment see the vulnerability article."
        echo
    fi

    echo -e "For more information about the vulnerability see:"
    echo -e "$ARTICLE"
    echo
    echo -e "For more information about possible performance impact see:"
    echo -e "$ARTICLE_PERF"

    exit "$result"
fi
