569 lines
12 KiB
Bash
Executable File
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
|