Files
s390-tools/scripts/cpictl
Shalini Chellathurai Saroja ce9c518b97 cpi: Disable CPI for SEL guests by default
The cpictl utility sends control-program identification data
from protected virtualization guests to hosts by default.
This behaviour leaks the below potentially sensitive
information to untrusted hosts.
- system_type
- system_level
- sysplex_name
- system_name

To prevent this behaviour, enhance the cpictl utility to stop
setting CPI information on protected virtualization guests by
default. If the user chooses to set the CPI information, it
could be set by one of the below options
- use the command line option --permit-cpi
- set the environment variable CPI_PERMIT_ON_PVGUEST to 1 to
control the CPI service behaviour during boot

Signed-off-by: Hendrik Brueckner <brueckner@linux.ibm.com>
Signed-off-by: Shalini Chellathurai Saroja <shalini@linux.ibm.com>
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Reviewed-by: Peter Oberparleiter <oberpar@linux.ibm.com>
Reviewed-by: Hendrik Brueckner <brueckner@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2025-07-29 18:23:49 +02:00

552 lines
13 KiB
Bash
Executable File

#!/bin/bash
#
# cpictl - Configure the Control-Program-Information (CPI) settings
#
# This is an internal helper script that is used by the "cpi.service"
# systemd unit and the "90-cpi.rules" udev rule.
#
# The bash shell is really needed. Other shells have different ideas of how
# bitwise operators work.
#
# Copyright 2017 IBM Corp.
#
# s390-tools is free software; you can redistribute it and/or modify
# it under the terms of the MIT license. See LICENSE for details.
#
readonly CPI_LOCK="/var/lock/cpictl.lock"
readonly PRG="${0##*/}"
readonly SYSTEM_LEVEL_PATH="/sys/firmware/cpi/system_level"
readonly SYSTEM_TYPE_PATH="/sys/firmware/cpi/system_type"
readonly SYSTEM_NAME_PATH="/sys/firmware/cpi/system_name"
readonly SYSPLEX_NAME_PATH="/sys/firmware/cpi/sysplex_name"
readonly CPI_SET="/sys/firmware/cpi/set"
# Location of os-release file - can be specified externally for testing purpose
readonly OS_RELEASE=${CPI_OS_RELEASE:-"/etc/os-release"}
declare LEVEL
declare TYPE
declare NAME
declare SYSPLEX
declare PV_GUEST
declare -i CPI_PERMIT="$CPI_PERMIT_ON_PVGUEST"
declare -i DRYRUN=0
# Exit codes
readonly EXIT_SUCCESS=0
readonly EXIT_FAILURE=1
readonly EXIT_ARG_TOO_LONG=3
readonly EXIT_INVALID_CHARS=4
readonly EXIT_INVALID_ARGS=5
readonly EXIT_NO_PERMIT_CPI=6
# Distro-IDs as supported by SE/HMC firmware
readonly DISTRO_GENERIC=0
readonly DISTRO_RHEL=1
readonly DISTRO_SLES=2
readonly DISTRO_UBUNTU=3
readonly DISTRO_FEDORA=4
readonly DISTRO_OPENSUSE=5
readonly DISTRO_DEBIAN=6
readonly DISTRO_RHCOS=7
print_help_and_exit()
{
cat <<EndHelp
Usage: $PRG [OPTIONS]
Configure the Control-Program-Information (CPI) settings.
-b, --set-bit BIT Set and commit the bit BIT in the flags
-e, --environment Set and commit the system type, level, name and
sysplex name with values taken from environment
variables
-h, --help Print this help, then exit
-L, --level LEVEL Set and commit OS level to LEVEL. Format: 0x<level> or
[[[flags:]distro_id:distro_version:]kernel_version]
-N, --name SYSTEM Set and commit the system name to SYSTEM
-S, --sysplex SYSPLEX Set and commit the sysplex name to SYSPLEX
-T, --type TYPE Set and commit OS type to TYPE
-v, --version Print version information, then exit
--permit-cpi Permit to send Control-Program Identification data of
protected virtualization guest to the host (must be
specified before any commit option). See also the
important note.
--commit Ignore all other options and commit any uncommitted
values
--dry-run Do not actually set or commit anything, but show what
would be done
--show Ignore all other options and show current (possibly
uncommitted) values
Environment variables used for the --defaults option:
CPI_SYSTEM_TYPE, CPI_SYSTEM_LEVEL, CPI_SYSTEM_NAME, CPI_SYSPLEX_NAME,
CPI_PERMIT_ON_PVGUEST (See also the important note.)
Important: Set CPI_PERMIT_ON_PVGUEST=1 or use --permit_cpi option only if you
trust the host system. Enabling these options allows the host to receive
potentially sensitive Control-Program Identification (CPI) data from the
protected virtualization guest, including:
- system_type
- system_level
- sysplex_name
- system_name
Available bits for the --set-bit option:
kvm: Indicate that system is a KVM host
The maximum length for system type, system name, and sysplex name is 8
characters. The allowed characters are: [A-Z0-9 @#$]
This tool should be called as root.
EndHelp
exit $EXIT_SUCCESS
}
print_version_and_exit()
{
cat <<EndVersion
$PRG: Configure the CPI settings version %S390_TOOLS_VERSION%
Copyright IBM Corp. 2017
EndVersion
exit $EXIT_SUCCESS
}
cpi_show()
{
cat <<-EndShow
System type: $(cat "$SYSTEM_TYPE_PATH")
System level: $(cat "$SYSTEM_LEVEL_PATH")
System name: $(cat "$SYSTEM_NAME_PATH")
Sysplex name: $(cat "$SYSPLEX_NAME_PATH")
EndShow
}
print_parse_error_and_exit()
{
echo "Try '$PRG --help' for more information." >&2
exit $EXIT_FAILURE
}
fail_with()
{
echo "$1" >&2
echo "Try '$PRG --help' for more information." >&2
exit ${2:-$EXIT_FAILURE}
}
cpi_commit()
{
# Commit Control-Program Identification changes on protected
# virtualization guests only if it is permitted by the guest. This
# prevents leakage of potentially sensitive information to untrusted
# hosts.
if [[ -f "/sys/firmware/uv/prot_virt_guest" ]]; then
read -r PV_GUEST < "/sys/firmware/uv/prot_virt_guest"
if [[ "$PV_GUEST" -eq 1 ]]; then
if [[ -z "$CPI_PERMIT" ]] || [[ "$CPI_PERMIT" -ne 1 ]]; then
echo "Sending CPI data from secure execution Linux guests is disabled. Use --permit-cpi to enable CPI data." >&2
exit "$EXIT_NO_PERMIT_CPI"
fi
fi
fi
echo 1 > "$CPI_SET" 2> /dev/null
}
do_length_check()
{
[ ${#1} -gt 8 ] &&
fail_with "$PRG: Specified $2 too long. The maximum length is 8 characters." $EXIT_ARG_TOO_LONG
}
do_character_check()
{
echo "$1" | grep -q -E '^[a-zA-Z0-9@#$ ]*$' ||
fail_with "$PRG: Invalid characters in $2. Valid characters are: A-Z0-9 @#$" $EXIT_INVALID_CHARS
}
cpi_set_bit()
{
LEVEL=$(printf '0x%x' $((LEVEL | (1 << (63 - $1)) )) )
}
#
# split_version - Split generic version string into array of sub-versions
#
# @version: Version string
# @delim: Characters that delimit sub-versions in version string
# @num: Number of sub-versions
#
# Print @num sub-versions of @version where each sub-version is delimited by
# any of the characters in @delim. Print 0 in place of non-decimal or missing
# sub-versions.
#
# Examples:
# version=10 delim=. num=3 => 10 0 0
# version=4.8 delim=. num=2 => 4 8
#
split_version()
{
local version="$1" delim="$2" num="$3"
local list i subver
IFS="$delim" read -r -a list <<< "$version"
for (( i=0; i<num; i++ )) ; do
subver="${list[i]:-0}"
# Handle non-number sub-versions
[[ "$subver" =~ ^[0-9]+$ ]] || subver=0
# Force decimal interpretation in case of leading zeroes
subver=$(( 10#$subver ))
printf "%s " "$subver"
done
}
#
# split_kver - Split Linux kernel version string into array of sub-versions
#
# @version: Linux kernel version string
# @num: Number of sub-versions
#
# Print @num sub-versions of the specified kernel @version. Print 0 in place
# of non-decimal or missing sub-versions.
#
# Examples:
# version=2.4-13 num=4 => 2 4 0 13
# version=3.0.93_3.0.101-0.8.2_0.8.1 num=6 => 3 0 93 3 0 101
# version=4.12.14-lp150.11.4 num=5 => 4 12 14 0 11
#
split_kver()
{
local version="$1" num="$2"
local main extra
# Separate extra version to handle short main version (e.g. 2.4-13)
IFS="-_" read -r main extra <<< "$version"
split_version "$main" "." $(( num > 3 ? 3 : num ))
[[ "$num" -gt 3 ]] && split_version "$extra" ".-_" $(( num - 3 ))
}
#
# bytes_to_word - Convert byte array to hexadecimal word
#
# @bytes: List of numbers representing byte values
#
# Print a big-endian hexadecimal representation of the word that results from
# combining the specified byte values.
#
bytes_to_word()
{
printf "0x"
printf "%02x" "$@"
}
#
# get_system_level - Print system level word for specified distribution version
#
# @distro: Distro ID (ID value from /etc/os-release)
# @ver_str: Distro version string (VERSION_ID from /etc/os-release)
# @kver_str: Kernel version string (output of 'uname -r')
# @flags: Optional statistics flags
#
# Print a 64 bit hexadecimal system level in a format as understood by firmware.
#
# The format is 0xabccddeeeeffgghh, where
# - a=statistics flags
# - b=distro id
# - c=distro major version
# - d=distro minor version(s)
# - e=kernel sublevel 2
# - f=kernel version
# - g=kernel patchlevel
# - h=kernel sublevel 1
#
get_system_level()
{
local distro="$1" ver_str="$2" kver_str="$3" flags="${4:-0}"
local distro_id d_major d_minor d_minor2
local k_ver k_patchlvl k_sublvl k_sublvl2 bytes=()
# Extract list of sub-version numbers from version strings
read -r d_major d_minor d_minor2 <<< "$(split_version "$ver_str" "._-" 3)"
read -r k_ver k_patchlvl k_sublvl k_sublvl2 <<< "$(split_kver "$kver_str" 4)"
# Handle excessive sublevel numbers consistently
if [[ "$k_sublvl" -gt 255 ]] ; then
k_sublvl=0
fi
if [[ "$k_sublvl2" -gt 65535 ]] ; then
k_sublvl2=0
fi
# Apply distro-specific logic
case "$distro" in
"rhel")
distro_id=$DISTRO_RHEL
;;
"sles")
distro_id=$DISTRO_SLES
;;
"ubuntu")
distro_id=$DISTRO_UBUNTU
# Encode minor and update version numbers in minor field
(( d_minor=((d_minor & 0xf) * 0x10) + (d_minor2 & 0xf) ))
;;
"fedora")
distro_id=$DISTRO_FEDORA
;;
"opensuse-leap")
distro_id=$DISTRO_OPENSUSE
;;
"debian")
distro_id=$DISTRO_DEBIAN
;;
"rhcos")
distro_id=$DISTRO_RHCOS
;;
*)
distro_id=$DISTRO_GENERIC
# Reset unsupported fields
d_major=0
d_minor=0
k_sublvl2=0
;;
esac
# Assemble byte data
(( bytes[0] = (flags & 0xf) * 0x10 + distro_id ))
(( bytes[1] = d_major ))
(( bytes[2] = d_minor ))
(( bytes[3] = (k_sublvl2 / 256) & 0xff ))
(( bytes[4] = k_sublvl2 & 0xff ))
(( bytes[5] = k_ver ))
(( bytes[6] = k_patchlvl ))
(( bytes[7] = k_sublvl ))
# Print as single hex word
bytes_to_word "${bytes[@]}"
}
get_distro()
{
local line ID="linux" VERSION_ID="0" VERSION="" update
[[ ! -e "$OS_RELEASE" ]] && return
# Only import required variables
while read -r line ; do
if [[ "$line" =~ ^ID= ]] || [[ "$line" =~ ^VERSION_ID= ]] ||
[[ "$line" =~ ^VERSION= ]] ; then
eval "$line"
fi
done <"$OS_RELEASE"
if [[ "$ID" == "ubuntu" ]] ; then
# Extract update version number only found in VERSION, e.g.
# VERSION_ID="18.04" VERSION="18.04.5 LTS"
update="${VERSION/*$VERSION_ID/}"
update="${update%% *}"
VERSION_ID="$VERSION_ID$update"
fi
echo "$ID:$VERSION_ID"
}
cpi_set_oslevel()
{
local level="${1:-}"
local flags list distro_id distro_ver kver id ver
if [[ "$level" =~ ^0x ]] && ! [[ "$level" =~ : ]] ; then
# Format: level=0x<hex>
printf -v LEVEL "0x%016x" "$level" 2>/dev/null ||
fail_with "$PRG: Invalid hexadecimal number in $level" \
$EXIT_INVALID_CHARS
return
fi
# Format: level=[[[flags:]distro_id:distro_ver:]kver]
IFS=":" read -r -a list <<< "$level:"
kver="${list[*]: -1: 1}"
distro_ver="${list[*]: -2: 1}"
distro_id="${list[*]: -3: 1}"
flags="${list[*]: -4: 1}"
if [[ -z "$kver" ]] ; then
# Use version of currently running kernel
kver="$(uname -r)"
fi
if [[ -z "$distro_ver" ]] || [[ -z "$distro_id" ]] ; then
# Use distro ID and version from os-release file
IFS=":" read -r id ver <<< "$(get_distro)"
distro_id=${distro_id:-$id}
distro_ver=${distro_ver:-$ver}
fi
if [[ -z "$flags" ]] ; then
# Keep statistics flags from current system level
flags=$(( (LEVEL >> 60) & 0xf ))
fi
LEVEL=$(get_system_level "$distro_id" "$distro_ver" "$kver" "$flags")
}
cpi_set_type()
{
TYPE="$1"
do_length_check "$TYPE" "system type"
do_character_check "$TYPE" "system type"
}
cpi_set_sysplex()
{
SYSPLEX="$1"
do_length_check "$SYSPLEX" "sysplex name"
do_character_check "$SYSPLEX" "sysplex name"
}
cpi_set_name()
{
NAME="$1"
do_length_check "$NAME" "system name"
do_character_check "$NAME" "system name"
}
# cpictl starts here
if [ $# -le 0 ]; then
echo "$PRG: No parameters specified"
print_parse_error_and_exit
fi
opts=$(getopt -o b:ehL:N:S:T:v -l set-bit:,environment,help,level:,name:,sysplex:,type:,commit,dry-run,permit-cpi,show,version -n "$PRG" -- "$@")
if [ $? -ne 0 ]; then
print_parse_error_and_exit
fi
# This guarantees that only one instance will be running, and will serialize
# the execution of multiple instances
[ -e "$CPI_LOCK" -a ! -w "$CPI_LOCK" ] &&
fail_with "$PRG: Cannot access lock file: $CPI_LOCK"
[ ! -w "${CPI_LOCK%/*}" ] &&
fail_with "$PRG: Cannot access lock file: $CPI_LOCK"
exec 9<> "$CPI_LOCK"
flock -x 9
# Get current values from sys/firmware
read LEVEL < "$SYSTEM_LEVEL_PATH"
read TYPE < "$SYSTEM_TYPE_PATH"
read NAME < "$SYSTEM_NAME_PATH"
read SYSPLEX < "$SYSPLEX_NAME_PATH"
# Parse command line options: Use eval to remove getopt quotes
eval set -- $opts
while [ -n $1 ]; do
case "$1" in
--help|-h)
print_help_and_exit
;;
--version|-v)
print_version_and_exit
;;
-b|--set-bit)
case "$2" in
kvm)
cpi_set_bit 0
;;
*)
fail_with "$PRG: Unknown bit \"$2\" for the $1 option"
;;
esac
shift 2
;;
-L|--level)
cpi_set_oslevel "$2"
shift 2
;;
-e|--environment)
cpi_set_type "$CPI_SYSTEM_TYPE"
cpi_set_name "$CPI_SYSTEM_NAME"
cpi_set_oslevel "$CPI_SYSTEM_LEVEL"
cpi_set_sysplex "$CPI_SYSPLEX_NAME"
shift
;;
-T|--type)
cpi_set_type "$2"
shift 2
;;
-S|--sysplex)
cpi_set_sysplex "$2"
shift 2
;;
-N|--name)
cpi_set_name "$2"
shift 2
;;
--show)
cpi_show
exit $EXIT_SUCCESS
;;
--permit-cpi)
CPI_PERMIT=1
shift
;;
--commit)
cpi_commit
exit $EXIT_SUCCESS
;;
--dry-run)
DRYRUN=1
shift
;;
--)
shift
break
;;
*)
break;
;;
esac
done
# Unparsed options are not supported
if [ $# -ne 0 ]; then
fail_with "$PRG: Invalid command-line option: $*" $EXIT_INVALID_ARGS
fi
# Print settings for --dry-run or commit them to sysfs otherwise
if [ $DRYRUN -eq 1 ]; then
cat <<-EndDryrun
System type: $TYPE
System level: $LEVEL
System name: $NAME
Sysplex name: $SYSPLEX
EndDryrun
else
echo "$LEVEL" > "$SYSTEM_LEVEL_PATH"
echo "$TYPE" > "$SYSTEM_TYPE_PATH"
echo "$NAME" > "$SYSTEM_NAME_PATH"
echo "$SYSPLEX" > "$SYSPLEX_NAME_PATH"
cpi_commit
fi
exit $EXIT_SUCCESS