From e0b46d086ac18826979b03e67a4405275801e1a0 Mon Sep 17 00:00:00 2001 From: Jonathon Hall Date: Wed, 10 Jan 2024 17:04:21 -0500 Subject: [PATCH 1/6] functions: TRACE_FUNC and DEBUG_STACK Add TRACE_FUNC to trace the file, line, and name of the calling function. File and function names don't have to be duplicated in a TRACE statement with this (they tend to become inaccurate as functions are renamed and the TRACE statement is forgotten). Add DEBUG_STACK to dump the bash stack to debug output. Configure bash with --enable-debugger. Bash doesn't actually include the entire debugger, this is just some supporting variables for it. Evidently, BASH_SOURCE[n] is only set within a function if this is enabled. I couldn't find this indicated in any documentation, but it happened in practice. Compressed initrd size only increased by 2560 bytes for librem_mini_v2, I think that is fine. This also gives us BASH_ARGC/BASH_ARGV which might be useful for diagnostics. Signed-off-by: Jonathon Hall --- initrd/etc/functions | 22 ++++++++++++++++++++++ modules/bash | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/initrd/etc/functions b/initrd/etc/functions index 7e6a63515..7d8f2d26d 100755 --- a/initrd/etc/functions +++ b/initrd/etc/functions @@ -33,6 +33,28 @@ DO_WITH_DEBUG() { "$@" } +# Trace the current script and function. +TRACE_FUNC() { + # Index [1] for BASH_SOURCE and FUNCNAME give us the caller location. + # FUNCNAME is 'main' if called from a script outside any function. + # BASH_LINENO is offset by 1, it provides the line that the + # corresponding FUNCNAME was _called from_, so BASH_LINENO[0] is the + # location of the caller. + TRACE "${BASH_SOURCE[1]}(${BASH_LINENO[0]}): ${FUNCNAME[1]}" +} + +# Show the entire current call stack in debug output - useful if a catastrophic +# error or something very unexpected occurs, like totally invalid parameters. +DEBUG_STACK() { + local FRAMES + FRAMES="${#FUNCNAME[@]}" + DEBUG "call stack: ($((FRAMES-1)) frames)" + # Don't print DEBUG_STACK itself, start from 1 + for i in $(seq 1 "$((FRAMES-1))"); do + DEBUG "- $((i-1)) - ${BASH_SOURCE[$i]}(${BASH_LINENO[$((i-1))]}): ${FUNCNAME[$i]}" + done +} + pcrs() { if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then tpm2 pcrread sha256 diff --git a/modules/bash b/modules/bash index 327b5dd50..d42733866 100644 --- a/modules/bash +++ b/modules/bash @@ -7,6 +7,9 @@ bash_tar := bash-$(bash_version).tar.gz bash_url := https://ftpmirror.gnu.org/bash/$(bash_tar) bash_hash := 5bac17218d3911834520dad13cd1f85ab944e1c09ae1aba55906be1f8192f558 +# --enable-debugger: Enables BASH_SOURCE tracing through functions as well as +# BASH_ARGV/BASH_ARGC. (Otherwise BASH_SOURCE[0] is empty when calling a +# function, it's only set in top level script code.) bash_configure := CFLAGS="-g0 -Os" LDFLAGS="-s" ./configure \ $(CROSS_TOOLS) \ --host $(target) \ @@ -16,7 +19,7 @@ bash_configure := CFLAGS="-g0 -Os" LDFLAGS="-s" ./configure \ --mandir=/usr/share/man \ --without-bash-malloc \ --disable-coprocesses \ - --disable-debugger \ + --enable-debugger \ --disable-net-redirections \ --enable-single-help-strings \ --disable-nls \ From de1592e2f52ef08f4f0a6be40d3fc7f346973048 Mon Sep 17 00:00:00 2001 From: Jonathon Hall Date: Wed, 10 Jan 2024 17:08:23 -0500 Subject: [PATCH 2/6] lvm2: Support LVM2 thin provisioned volumes Support LVM2 thin-provisioned volumes. LVM2 wants the thin_check utility by default, but it has multiple dependencies we do not currently ship (boost, libexpat, others), so disable it. Signed-off-by: Jonathon Hall --- modules/lvm2 | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/lvm2 b/modules/lvm2 index 4bf9f172b..be1435a36 100644 --- a/modules/lvm2 +++ b/modules/lvm2 @@ -33,6 +33,7 @@ lvm2_configure := \ --disable-cache_check_needs_check \ --disable-thin_check_needs_check \ --with-cluster=none \ + --with-thin-check= \ # not sure why LIB_SUFFIX is not defined in the cross build lvm2_target := \ From 70d249ae4651753776798f429c050bf618038a03 Mon Sep 17 00:00:00 2001 From: Jonathon Hall Date: Wed, 10 Jan 2024 17:09:57 -0500 Subject: [PATCH 3/6] intird/bin/config-gui.sh: Clarify root hash menu item, minor cleanup Say the action to take in the menu (enable or disable) instead of just "Check root hashes at boot". Clean up some use of load_config_value, set_config, combine_configs. Get config values from the environment directly. set_user_config does set_config and combine_configs. Signed-off-by: Jonathon Hall --- initrd/bin/config-gui.sh | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/initrd/bin/config-gui.sh b/initrd/bin/config-gui.sh index 24a940ba0..f31838675 100755 --- a/initrd/bin/config-gui.sh +++ b/initrd/bin/config-gui.sh @@ -39,7 +39,7 @@ while true; do 'r' ' Clear GPG key(s) and reset all user settings' 'R' ' Change the root device for hashing' 'D' ' Change the root directories to hash' - 'B' ' Check root hashes at boot' + 'B' " $(get_config_display_action "$CONFIG_ROOT_CHECK_AT_BOOT") root check at boot" 'L' " $(get_config_display_action "$CONFIG_RESTRICTED_BOOT") Restricted Boot" ) @@ -255,10 +255,9 @@ while true; do --msgbox "The root directories to hash was successfully changed to:\n$NEW_CONFIG_ROOT_DIRLIST" 0 80 ;; "B" ) - CURRENT_OPTION="$(load_config_value CONFIG_ROOT_CHECK_AT_BOOT)" - if [ "$CURRENT_OPTION" != "y" ]; then + if [ "$CONFIG_ROOT_CHECK_AT_BOOT" != "y" ]; then # Root device and directories must be set to enable this - if [ -z "$(load_config_value CONFIG_ROOT_DEV)" ] || [ -z "$(load_config_value CONFIG_ROOT_DIRLIST)" ]; then + if [ -z "$CONFIG_ROOT_DEV" ] || [ -z "$CONFIG_ROOT_DIRLIST" ]; then whiptail $BG_COLOR_ERROR --title 'Root Check Not Configured' \ --msgbox "Set the root device and directories to hash before enabling this feature." 0 80 elif (whiptail --title 'Enable Root Hash Check at Boot?' \ @@ -267,8 +266,7 @@ while true; do \na minute or more to the boot time. \n\nDo you want to proceed?" 0 80) then - set_config /etc/config.user "CONFIG_ROOT_CHECK_AT_BOOT" "y" - combine_configs + set_user_config "CONFIG_ROOT_CHECK_AT_BOOT" "y" # check that root hash file exists if [ ! -f ${ROOT_HASH_FILE} ]; then @@ -288,8 +286,7 @@ while true; do --yesno "This will disable checking root hashes each time you boot. \n\nDo you want to proceed?" 0 80) then - set_config /etc/config.user "CONFIG_ROOT_CHECK_AT_BOOT" "n" - combine_configs + set_user_config "CONFIG_ROOT_CHECK_AT_BOOT" "n" whiptail --title 'Config change successful' \ --msgbox "The root device will not be checked at each boot." 0 80 From 80b57eb60dbaf3fc64ecf6723940aa3167d5c71e Mon Sep 17 00:00:00 2001 From: Jonathon Hall Date: Wed, 10 Jan 2024 17:13:44 -0500 Subject: [PATCH 4/6] initrd/bin/root-hashes-gui.sh: Qubes support, faster hash creation Don't spew the root hashes to the console when creating the hash file. This speeds up hash creation significantly. A basic Qubes install on a cheap (slow) SATA SSD reduced from about 1.5 minutes to just under 1 minute, and a PureOS install on a fast NVMe disk reduced from 2.5 minutes to 1 minute. Support opening LVM volume groups to find the root disk. If an LVM PV is found, its group is opened and the 'root' volume is used. There is no way to set the volume name in this iteration; this is the default name used by Qubes and probably common to many LVM OS installations. LUKS and LVM can be mixed. Tested LUKS (PureOS) and LUKS+LVM (Qubes). Always cd to "$ROOT_MOUNT" in a subshell, improves robustness of scripts (previously some functions only worked if they were called after another function had cd'd to "$ROOT_MOUNT"). Signed-off-by: Jonathon Hall --- initrd/bin/root-hashes-gui.sh | 269 +++++++++++++++++++++++++++++----- 1 file changed, 234 insertions(+), 35 deletions(-) diff --git a/initrd/bin/root-hashes-gui.sh b/initrd/bin/root-hashes-gui.sh index 94533bcb0..d772e02b6 100755 --- a/initrd/bin/root-hashes-gui.sh +++ b/initrd/bin/root-hashes-gui.sh @@ -32,7 +32,9 @@ update_root_checksums() { fi echo "+++ Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY " - cd $ROOT_MOUNT && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum | tee ${HASH_FILE} + # Intentional wordsplit + # shellcheck disable=SC2086 + (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum) >"${HASH_FILE}" # switch back to ro mode mount -o ro,remount /boot @@ -86,7 +88,7 @@ check_root_checksums() { fi echo "+++ Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY " - find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' | sort > /tmp/new_file_list + (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*') | sort > /tmp/new_file_list cut -d' ' -f3- ${HASH_FILE} | sort | diff -U0 - /tmp/new_file_list > /tmp/new_file_diff || new_files_found=y if [ "$new_files_found" == "y" ]; then grep -E -v '^[+-]{3}|[@]{2} ' /tmp/new_file_diff > /tmp/new_file_diff2 # strip any output that's not a file @@ -102,7 +104,7 @@ check_root_checksums() { fi echo "+++ Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this might take a while) " - if cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} > /tmp/hash_output 2>/dev/null; then + if (cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} > /tmp/hash_output 2>/dev/null); then echo "+++ Verified root hashes " valid_hash='y' unmount_root_device @@ -156,29 +158,234 @@ check_root_checksums() { fi fi } + +# Check if a device is an LVM2 PV, and if so print the VG name +find_lvm_vg_name() { + TRACE_FUNC + local DEVICE VG + DEVICE="$1" + + mkdir -p /tmp/root-hashes-gui + if ! lvm pvs "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/dev/null; then + DEBUG "Did not detect an LVM2 PV: $DEVICE" + return 1 + fi + + VG="$(tail -n +2 /tmp/root-hashes-gui/lvm_vg | awk '{print $2}')" + if [ -z "$VG" ]; then + DEBUG "Could not find LVM2 VG from lvm pvs output:" + DEBUG "$(cat /tmp/root-hashes-gui/lvm_vg)" + return 1 + fi + + echo "$VG" +} + +# Open an LVM volume group, then continue looking for more layers in the 'root' +# logical volume. +open_block_device_lvm() { + TRACE_FUNC + local VG="$1" + + if ! lvm vgchange -ay "$VG"; then + DEBUG "Can't open LVM VG: $VG" + return 1 + fi + + # Use the LV 'root'. This is the default name used by Qubes. There's no + # way to configure this at the moment. + if ! [ -e "/dev/mapper/$VG-root" ]; then + DEBUG "LVM volume group does not have 'root' logical volume" + return 1 + fi + + # Use the root LV now + open_block_device_layers "/dev/mapper/$VG-root" +} + +# Open a LUKS device, then continue looking for more layers. +open_block_device_luks() { + TRACE_FUNC + local DEVICE="$1" + local LUKSDEV + LUKSDEV="$(basename "$DEVICE")_crypt" + + # Open the LUKS device. This may prompt interactively for the passphrase, so + # hook it up to the console even if stdout/stdin have been redirected. + if ! cryptsetup open "$DEVICE" "$LUKSDEV"; then + DEBUG "Can't open LUKS volume: $DEVICE" + return 1 + fi + + open_block_device_layers "/dev/mapper/$LUKSDEV" +} + +# Open block device layers to access /root recursively. If another layer (LUKS +# or LVM) can be identified, open it and recurse into the new device. When all +# recognized layers are opened, print the final block device and exit +# successfully (open_root_device will try to mount it). +# +# This only fails if we can recognize another LUKS or LVM layer, but cannot open +# it. It succeeds otherwise, even if no layers are recognized, because we +# should try to mount the block device directly in that case. +open_block_device_layers() { + TRACE_FUNC + local DEVICE="$1" + local VG + + if ! [ -e "$DEVICE" ]; then + DEBUG "Block device doesn't exit: $DEVICE" + # This shouldn't really happen, we thought we opened the last layer + # successfully. The call stack reveals what LUKS/LVM2 layers have been + # opened so far. + DEBUG_STACK + return 1 + fi + + # Try to open a LUKS layer + if cryptsetup isLuks "$DEVICE" &>/dev/null; then + open_block_device_luks "$DEVICE" || return 1 + # Try to open an LVM layer + elif VG="$(find_lvm_vg_name "$DEVICE")"; then + open_block_device_lvm "$VG" || return 1 + else + # The given block device exists but is not any layer we understand. Stop + # opening layers and try to mount it. + echo "$DEVICE" + fi +} + +# Try to open a block device as /root. open_block_device_layers() is used to +# open LUKS and LVM layers before mounting the filesystem. +# +# This function does not clean up anything if it is unsuccessful. Use +# try_open_root_device() to also clean up when unsuccessful. +open_root_device_no_clean_up() { + TRACE_FUNC + local DEVICE="$1" + local FS_DEVICE + + # Open LUKS/LVM and get the name of the block device that should contain the + # filesystem. If there are no LUKS/LVM layers, FS_DEVICE is just DEVICE. + FS_DEVICE="$(open_block_device_layers "$DEVICE")" || return 1 + + # Mount the device + if ! mount -o ro "$FS_DEVICE" "$ROOT_MOUNT" &>/dev/null; then + DEBUG "Can't mount filesystem on $FS_DEVICE from $DEVICE" + return 1 + fi + + # The filesystem must have all of the directories configured. (Intentional + # word-split) + # shellcheck disable=SC2086 + if ! (cd "$ROOT_MOUNT" && ls -d $CONFIG_ROOT_DIRLIST &>/dev/null); then + DEBUG "Root filesystem on $DEVICE lacks one of the configured directories: $CONFIG_ROOT_DIRLIST" + return 1 + fi + + # Root is mounted now and the directories are present + return 0 +} + +# If an LVM VG is open, close any layers within it, then close the LVM VG. +close_block_device_lvm() { + TRACE_FUNC + local VG="$1" + + # We always use the LV 'root' currently + local LV="/dev/mapper/$VG-root" + if [ -e "$LV" ]; then + close_block_device_layers "$LV" + fi + + # The LVM VG might be open even if no 'root' LV exists, still try to close it. + lvm vgchange -an "$VG" || \ + DEBUG "Can't close LVM VG: $VG" +} + +# If a LUKS device is open, close any layers within the LUKS device, then close +# the LUKS device. +close_block_device_luks() { + TRACE_FUNC + local DEVICE="$1" + local LUKSDEV + LUKSDEV="$(basename "$DEVICE")_crypt" + + if [ -e "/dev/mapper/$LUKSDEV" ]; then + # Close inner layers before trying to close LUKS + close_block_device_layers "/dev/mapper/$LUKSDEV" + cryptsetup close "$LUKSDEV" || \ + DEBUG "Can't close LUKS volume: $LUKSDEV" + fi +} + +# Close the root device, including unmounting the filesystem and closing all +# layers. This can close a partially-opened device if an error occurs. +close_block_device_layers() { + TRACE_FUNC + local DEVICE="$1" + local VG + + if ! [ -e "$DEVICE" ]; then + DEBUG "Block device doesn't exit: $DEVICE" + # Like in open_root_device(), this shouldn't really happen, show the layers + # up to this point via the call stack. + DEBUG_STACK + return 1 + fi + + if cryptsetup isLuks "$DEVICE"; then + close_block_device_luks "$DEVICE" + elif VG="$(find_lvm_vg_name "$DEVICE")"; then + close_block_device_lvm "$VG" + fi + # Otherwise, we've handled all the layers we understood, there's nothing left + # to do. +} + +# Try to open the root device, and clean up if unsuccessful. +open_root_device() { + TRACE_FUNC + if ! open_root_device_no_clean_up "$1"; then + unmount_root_device + return 1 + fi + + return 0 +} + +# Close the root device, including unmounting the filesystem and closing all +# layers. This can close a partially-opened device if an error occurs. This +# never fails, if an error occurs it still tries to close anything it can. +close_root_device() { + TRACE_FUNC + local DEVICE="$1" + + # Unmount the filesystem if it is mounted. If it is not mounted, ignore the + # failure. If it is mounted but can't be unmounted, this will fail and we + # will fail to close any LUKS/LVM layers too. + umount "$ROOT_MOUNT" &>/dev/null || true + + close_block_device_layers "$DEVICE" || true +} + # detect and set /root device # mount /root if successful detect_root_device() { + TRACE_FUNC + echo "+++ Detecting root device " if [ ! -e $ROOT_MOUNT ]; then mkdir -p $ROOT_MOUNT fi - # unmount $ROOT_MOUNT to be safe - cd / && umount $ROOT_MOUNT 2>/dev/null + # Ensure nothing is opened/mounted + unmount_root_device # check $CONFIG_ROOT_DEV if set/valid - if [ -e "$CONFIG_ROOT_DEV" ]; then - if cryptsetup isLuks $CONFIG_ROOT_DEV >/dev/null 2>&1; then - if cryptsetup open $CONFIG_ROOT_DEV rootdisk; then - if mount -o ro /dev/mapper/rootdisk $ROOT_MOUNT >/dev/null 2>&1; then - if cd $ROOT_MOUNT && ls -d $CONFIG_ROOT_DIRLIST >/dev/null 2>&1; then # CONFIG_ROOT_DEV is valid device and contains an installed OS - return 0 - fi - fi - fi - fi + if [ -e "$CONFIG_ROOT_DEV" ] && open_root_device "$CONFIG_ROOT_DEV"; then + return 0 fi # generate list of possible boot devices @@ -186,7 +393,7 @@ detect_root_device() # filter out extraneous options > /tmp_root_device_list - for i in `cat /tmp/disklist`; do + while IFS= read -r -u 10 i; do # remove block device from list if numeric partitions exist DEV_NUM_PARTITIONS=$((`ls -1 $i* | wc -l`-1)) if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then @@ -194,33 +401,25 @@ detect_root_device() else ls $i* | tail -${DEV_NUM_PARTITIONS} >> /tmp_root_device_list fi - done - - # iterate thru possible options and check for LUKS - for i in `cat /tmp_root_device_list`; do - if cryptsetup isLuks $i >/dev/null 2>&1; then - if cryptsetup open $i rootdisk; then - if mount -o ro /dev/mapper/rootdisk $ROOT_MOUNT >/dev/null 2>&1; then - if cd $ROOT_MOUNT && ls -d $CONFIG_ROOT_DIRLIST >/dev/null 2>&1; then - # CONFIG_ROOT_DEV is valid device and contains an installed OS - CONFIG_ROOT_DEV="$i" - return 0 - fi - fi - fi + done 10/dev/null - cryptsetup close rootdisk + [ -e "$CONFIG_ROOT_DEV" ] && close_root_device "$CONFIG_ROOT_DEV" } checkonly="n" From ae29ddbc78cb7004bb27a57c986dd949da24ec1f Mon Sep 17 00:00:00 2001 From: Jonathon Hall Date: Wed, 17 Jan 2024 16:29:50 -0500 Subject: [PATCH 5/6] initrd/bin/root-hashes-gui.sh: Remove debug statement for non-LVM-PV This statement was confusing and should be clear from tracing anyway. Signed-off-by: Jonathon Hall --- initrd/bin/root-hashes-gui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/initrd/bin/root-hashes-gui.sh b/initrd/bin/root-hashes-gui.sh index d772e02b6..1e55018f1 100755 --- a/initrd/bin/root-hashes-gui.sh +++ b/initrd/bin/root-hashes-gui.sh @@ -167,7 +167,7 @@ find_lvm_vg_name() { mkdir -p /tmp/root-hashes-gui if ! lvm pvs "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/dev/null; then - DEBUG "Did not detect an LVM2 PV: $DEVICE" + # It's not an LVM PV return 1 fi From 84040176faa142438608f42c71109bbdc8b0379b Mon Sep 17 00:00:00 2001 From: Jonathon Hall Date: Wed, 17 Jan 2024 16:30:31 -0500 Subject: [PATCH 6/6] modules/bash: Enable readline Restores autocomplete and makes bash more usable as an interactive shell. Added 106 KB to compressed initrd (checked librem_14). Signed-off-by: Jonathon Hall --- modules/bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bash b/modules/bash index d42733866..e29fed726 100644 --- a/modules/bash +++ b/modules/bash @@ -23,7 +23,7 @@ bash_configure := CFLAGS="-g0 -Os" LDFLAGS="-s" ./configure \ --disable-net-redirections \ --enable-single-help-strings \ --disable-nls \ - --disable-readline \ + --enable-readline \ bash_target := $(MAKE_JOBS) \ && $(MAKE) -C $(build)/$(bash_dir) \