zfsbootmenu/bin/zbm-kcl

569 lines
12 KiB
Bash
Executable File

#!/bin/bash
# vim: softtabstop=2 shiftwidth=2 expandtab
cleanup() {
if [ -f "${TEMP_EFI}" ]; then
if [ -z "${PRESERVE_EFI}" ]; then
rm "${TEMP_EFI}"
else
echo "NOTICE: saving temporary EFI output ${TEMP_EFI}"
fi
fi
unset TEMP_EFI PRESERVE_EFI
[ -f "${TEMP_KCL}" ] && rm "${TEMP_KCL}"
unset TEMP_KCL
}
usage() {
cat <<-EOF
USAGE: $0 [-a <arg>] [-r <arg>] [-o <out>] [-e] [-d] [bootenv|zbm-efi]
Review or update kernel command line (KCL) associated with the ZFS boot
environment or <bootenv> or the ZFSBootMenu EFI executable <zbm-efi>.
When the boot environment or EFI executable is unspecified, the current
root filesystem will be used by default. If "-" is passed, stdin will
be read as an EFI executable.
ARGUMENTS
-a <arg>: Append an argument
-r <arg>: Remove an argument
-o <out>: Write output to <out> filesystem or executable
-e: Open the KCL for editing in \$EDITOR
-d: Remove entire command line
EOF
}
zerror() {
echo "ERROR: $*" >&2
}
## BEGIN: zfsbootmenu-kcl.sh library functions
## These are duplicated here to avoid a dependency in this helper script.
## If you make improvements here, please add them back to the library too.
kcl_tokenize() {
awk '
BEGIN {
strt = 1;
quot = 0;
}
{
for (i=1; i <= NF; i++) {
# If an odd number of quotes are in this field, toggle quoting
if ( gsub(/"/, "\"", $(i)) % 2 == 1 ) {
quot = (quot + 1) % 2;
}
# Print a space if this is not the start of a line
if (strt == 0) {
printf " ";
}
printf "%s", $(i);
if (quot == 0) {
strt = 1;
printf "\n";
} else {
strt = 0;
}
}
}
'
}
kcl_suppress() {
local arg rem sup
while read -r arg; do
# Check match against all exclusions
sup=0
for rem in "$@"; do
# Arguments match entirely or up to first equal
if [[ "${arg}" == "${rem}" || "${arg%%=*}" == "${rem}" ]]; then
sup=1
break
fi
done
# Echo argument if it was not suppressed
[ "${sup}" -ne 1 ] && echo "${arg}"
done
}
kcl_assemble() {
awk '
BEGIN{ strt = 1; }
{
if (strt == 0) {
printf " ";
}
printf "%s", $0;
strt = 0;
}
'
}
## END: zfsbootmenu-kcl.sh library functions
# Append KCL arguments to the tokenized input stream, tokenizing each in turn.
kcl_append() {
local arg
# Carry forward input KCL
cat
for arg in "$@"; do
[ -n "${arg}" ] || continue
kcl_tokenize <<< "${arg}"
done
}
strip_kcl() {
awk '
BEGIN { first = 1; }
/^$/ { exit; }
{
gsub("[[:space:]]*#.*", "");
if (length == 0) next;
if (first == 1) {
first = 0;
} else {
printf " ";
}
printf "%s", $0;
}
'
}
delete() {
local bootenv="${1}"
if ! zfs list -o name -H "${bootenv}" >/dev/null 2>&1; then
zerror "no boot environment specified"
usage
return 1
fi
if ! zfs inherit org.zfsbootmenu:commandline "${bootenv}"; then
zerror "failed to remove KCL from ${bootenv}"
return 1
fi
}
raw_kcl_zfs() {
local bootenv="${1}"
kcl="$(zfs get -H -o value org.zfsbootmenu:commandline "${bootenv}")" || return 1
[ "${kcl}" = "-" ] && return
echo "${kcl}"
}
raw_kcl_efi() {
local efi kclfile
efi="${1}"
if [ ! -r "${efi}" ]; then
zerror "the EFI file cannot be read"
return 1
fi
if ! kclfile="$(mktemp)"; then
zerror "failed to create temporary file"
return 1
fi
trap 'rm -f "${kclfile}"; trap - RETURN' RETURN
if ! objout="$(objcopy --dump-section .cmdline="${kclfile}" "${efi}" 2>&1)"; then
zerror "failed to dump EFI cmdline: ${objout}"
return 1
fi
cat "${kclfile}"
}
save_kcl_zfs() {
local kcl bootenv
bootenv="${1}"
kcl="$(cat)"
if [ -n "${kcl}" ]; then
if ! zfs set org.zfsbootmenu:commandline="${kcl}" "${bootenv}"; then
zerror "failed to set KCL property on ${bootenv}"
return 1
fi
elif ! zfs inherit org.zfsbootmenu:commandline "${bootenv}"; then
zerror "failed to clear KCL property on ${bootenv}"
return 1
fi
return 0
}
find_cmdline_gap() {
local file
file="${1}"
if [ ! -r "${file}" ]; then
zerror "unable to read object file '${file}'"
return 1
fi
if ! OBJDATA="$(objdump -h -w "$1")"; then
zerror "failed to parse object file; is objdump available?"
return 1
fi
local ready idx name size vma rem offsets cmdoff
offsets=( )
cmdoff=
ready=
# shellcheck disable=SC2034
while read -r idx name size vma rem; do
# Object header table begins with header labeling columns
if [ "${idx,,}" = "idx" ] && [ "${vma,,}" = "vma" ]; then
ready="yes"
continue
fi
# Ignore all lines until the header line has been encountered
[ -n "${ready}" ] || continue
# Make sure the index field is integral
[ "${idx}" -eq "${idx}" ] >/dev/null 2>&1 || continue
# Validate the VMA field, which should be hex
vma="${vma,,}"
# Field should not start with 0x, but tolerate it anyway
vma="${vma#0x}"
if ! vma="$(( "0x${vma}" ))"; then
zerror "invalid VMA for section '${name}'"
return 1
fi
if [ "${name,,}" = ".cmdline" ]; then
cmdoff="${vma}"
else
offsets+=( "${vma}" )
fi
done <<< "${OBJDATA}"
if [ -z "${cmdoff}" ]; then
zerror "file '${file}' contains no .cmdline section"
return 1
fi
local gap mingap
gap=
mingap=
for vma in "${offsets[@]}"; do
[ "${vma}" -gt "${cmdoff}" ] >/dev/null 2>&1 || continue;
gap="$(( vma - cmdoff ))"
if [ -z "${mingap}" ] || [ "${gap}" -lt "${mingap}" ]; then
mingap="${gap}"
fi
done
if [ -z "${mingap}" ]; then
zerror "unable to determine .cmdline gap size"
return 1
fi
printf "%X %X\n" "${cmdoff}" "${mingap}"
return 0
}
save_kcl_efi() {
local kcl efi kclfile kcloff kclgap kclsize
efi="${1}"
# Find offset and space available for the cmdline to replace;
# this seems to be 4 kB with the x86_64 stub loader alignment
if ! kclsize="$(find_cmdline_gap "${efi}")"; then
zerror "failed to determine offset data for KCL"
return 1
fi
read -r kcloff kclgap <<< "${kclsize}"
if [ -z "${kcloff}" ] || [ -z "${kclgap}" ]; then
zerror "offset data for KCL appears invalid; aborting"
return 1
fi
if ! kclfile="$(mktemp)"; then
zerror "failed to save temporary KCL"
return 1
fi
trap 'rm -f "${kclfile}"; trap - RETURN' RETURN
# Dracut adds a leading space; is this necessary?
echo -n " " > "${kclfile}"
cat > "${kclfile}"
# Dracut also adds a null terminator
echo -ne "\x00" >> "${kclfile}"
if ! kclsize="$(stat -c %s "${kclfile}")"; then
zerror "failed to determine new KCL size; is stat available?"
return 1
fi
if ! [ "${kclsize}" -le "$(( "0x${kclgap}" ))" ] >/dev/null 2>&1; then
zerror "new KCL size exceeds space available in EFI file '${efi}'; aborting"
return 1
fi
if ! objout="$(objcopy --remove-section .cmdline "${efi}" 2>&1)"; then
zerror "failed to clear existing KCL from EFI executable"
return 1
fi
local objargs
objargs=( --add-section ".cmdline=${kclfile}"
--change-section-vma ".cmdline=0x${kcloff}" )
if ! objout="$(objcopy "${objargs[@]}" "${efi}" 2>&1)"; then
zerror "failed to write new KCL to EFI executable"
return 1
fi
return 0
}
# Assmeble and allow the user to edit an already tokenized KCL and, if it has
# changed, tokenize the edited version and save back to the input file.
#
# ARGUMENTS
# arg0: pre-existing tokenized KCL
#
# RETURNS
# 0 on successful edit
# 1 on error
edit() {
local kclsrc kclfile
kclsrc="${1}"
if ! command -v "${EDITOR:=vi}" >/dev/null 2>&1; then
zerror "define \$EDITOR to edit"
return 1
fi
if ! [ -r "${kclsrc}" ]; then
zerror "unable to read KCL"
return 1
fi
if ! kclfile="$(mktemp)"; then
zerror "failed to create temporary file"
return 1
fi
trap 'rm -f "${kclfile}"; trap - RETURN' RETURN
kcl_assemble < "${kclsrc}" > "${kclfile}"
cat >> "${kclfile}" <<-EOF
# KCL processing ends with the first line that contains no text.
# Anything starting with # is considered a comment and will be ignored.
# Multiple lines will be concatenated with spaces to form a single line.
EOF
if ! "${EDITOR}" "${kclfile}"; then
zerror "failed to edit KCL"
return 1
fi
if ! strip_kcl < "${kclfile}" | kcl_tokenize > "${kclsrc}"; then
zerror "failed to save edited KCL"
return 1
fi
return 0
}
getbootenv() {
local dev mntpt fs opts
# shellcheck disable=SC2034
while read -r dev mntpt fs opts; do
[ "${mntpt}" = "/" ] || continue
if [ "${fs}" != "zfs" ]; then
return 1
fi
echo "${dev}"
return 0
done < /proc/mounts
}
delkcl=
output=
editkcl=
appends=()
removes=()
while getopts "ha:r:o:ed" opt; do
case "${opt}" in
a)
appends+=( "${OPTARG}" )
;;
r)
removes+=( "${OPTARG}" )
;;
o)
output="${OPTARG}"
;;
e)
editkcl="yes"
;;
d)
delkcl="yes"
;;
h)
usage
exit
;;
*)
usage
exit 1
;;
esac
done
shift $((OPTIND-1))
input="${1}"
if [ -z "${input}" ]; then
if ! input="$(getbootenv)"; then
zerror "root does not appear to be ZFS" >&2
exit 1
fi
fi
if [ -z "${output}" ]; then
output="${input}"
fi
modify=
if (( ${#removes[@]} != 0 || ${#appends[@]} != 0 )); then
modify="yes"
elif [ "${editkcl}" = "yes" ]; then
modify="yes"
elif [ "${delkcl}" = "yes" ]; then
modify="yes"
fi
# Make sure to clean up temporary files on exit
unset TEMP_EFI PRESERVE_EFI TEMP_KCL
trap cleanup EXIT INT TERM
efi_mode=
kcl_reader=( "raw_kcl_zfs" "${input}" )
kcl_writer=( "save_kcl_zfs" "${output}" )
if ! zfs list -o name -H "${input}" >/dev/null 2>&1; then
# When input is not a ZFS filesystem, treat it as an EFI executable
efi_mode=yes
if ! TEMP_EFI="$(mktemp)"; then
zerror "unable to create temporary file for EFI executable edits"
exit 1
fi
if [ "${input}" = "-" ]; then
if [ "${editkcl}" = "yes" ]; then
zerror "cannot edit KCL in streaming mode"
exit 1
fi
if ! cat > "${TEMP_EFI}"; then
zerror "failed to save EFI executable from stdin"
exit 1
fi
input="${TEMP_EFI}"
else
if [ ! -r "${input}" ]; then
zerror "KCL source is not a ZFS filesystem or EFI executable"
exit 1
fi
if ! cp "${input}" "${TEMP_EFI}"; then
zerror "failed to copy EFI executable '${input}' to workspace"
exit 1
fi
fi
kcl_reader=( "raw_kcl_efi" "${TEMP_EFI}" )
kcl_writer=( "save_kcl_efi" "${TEMP_EFI}" )
elif [ "${output}" != "${input}" ]; then
if ! zfs list -o name -H "${output}" >/dev/null 2>&1; then
zerror "KCL destination is not a ZFS filesystem"
exit 1
fi
fi
if ! TEMP_KCL="$(mktemp)"; then
zerror "failed to create temporary KCL file"
exit 1
fi
# In "delete" mode, just "read" an empty string for the input kcl
if [ "${delkcl}" = "yes" ]; then
kcl_reader=( "true" )
fi
# Read, tokenize and modify the input KCL
if ! "${kcl_reader[@]}" | kcl_tokenize | \
kcl_suppress "${removes[@]}" | \
kcl_append "${appends[@]}" > "${TEMP_KCL}"; then
zerror "failed to parse input KCL"
exit 1
fi
# If the intent is not to modify, just print the existing KCL
if [ "${modify}" != "yes" ]; then
kcl_assemble < "${TEMP_KCL}"
echo ""
exit 0
fi
# Allow the user to edit if appropriate
if [ "${editkcl}" = "yes" ] && ! edit "${TEMP_KCL}"; then
exit 1
fi
# Save the working KCL back to the output
if ! kcl_assemble < "${TEMP_KCL}" | "${kcl_writer[@]}"; then
zerror "failed to save KCL changes"
exit 1
fi
# In EFI mode, try to copy the temporary EFI to the output
if [ "${efi_mode}" = "yes" ]; then
if [ "${output}" = "-" ]; then
cat "${TEMP_EFI}"
elif ! cp "${TEMP_EFI}" "${output}"; then
zerror "failed to write EFI executable '${output}'; saved as '${TEMP_EFI}'"
PRESERVE_EFI="yes"
exit 1
fi
fi