#!/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 ] [-r ] [-o ] [-e] [-d] [bootenv|zbm-efi] Review or update kernel command line (KCL) associated with the ZFS boot environment or or the ZFSBootMenu EFI executable . 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 : Append an argument -r : Remove an argument -o : Write output to filesystem or executable -e: Open the KCL for editing in \$EDITOR -d: Remove entire command line EOF } check_zfs_fs() { zfs list -o name -H "$1" >/dev/null 2>&1 } 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 } # Assemble 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 [ "${input}" = "-" ] || [ -f "${input}" ]; then # When input is stdin or a file, assume it is 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}" elif ! cp "${input}" "${TEMP_EFI}"; then zerror "failed to copy EFI executable '${input}' to workspace" exit 1 fi kcl_reader=( "raw_kcl_efi" "${TEMP_EFI}" ) kcl_writer=( "save_kcl_efi" "${TEMP_EFI}" ) elif ! check_zfs_fs "${input}"; then zerror "KCL source is not a ZFS filesystem or EFI executable" exit 1 elif [ "${output}" != "${input}" ] && ! check_zfs_fs "${output}"; then zerror "KCL destination is not a ZFS filesystem" exit 1 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