Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 137 additions & 20 deletions initrd/bin/root-hashes-gui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,29 @@ set -e -o pipefail
CONFIG_ROOT_DIRLIST="bin boot lib sbin usr"
HASH_FILE="/boot/kexec_root_hashes.txt"
ROOT_MOUNT="/root"
ROOT_DETECT_UNSUPPORTED_REASON=""
ROOT_SUPPORTED_LAYOUT_MSG="Filesystem support in this build:\n- ext4 (ext2/ext3 compatible)\n- xfs\n\nSupported root layouts:\n- LUKS + ext4/ext3/ext2 or xfs\n- LUKS+LVM + ext4/ext3/ext2 or xfs\n\nNot supported:\n- btrfs"

. /etc/functions
. /etc/gui_functions
. /tmp/config

export CONFIG_ROOT_DIRLIST_PRETTY=$(echo $CONFIG_ROOT_DIRLIST | sed -e 's/^/\//;s/ / \//g')

show_unsupported_root_layout_and_die() {
local ACTION="$1"

whiptail_error --title 'ERROR: Unsupported Root Layout' \
--msgbox "$ROOT_DETECT_UNSUPPORTED_REASON\n\n$ROOT_SUPPORTED_LAYOUT_MSG\n\nTry a supported root layout,\nor do not use root hashing,\nthen rerun $ACTION." 0 80
die "$ROOT_DETECT_UNSUPPORTED_REASON"
}

update_root_checksums() {
TRACE_FUNC
if ! detect_root_device; then
if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then
show_unsupported_root_layout_and_die "root hash update"
fi
whiptail_error --title 'ERROR: No Valid Root Disk Found' \
--msgbox "No Valid Root Disk Found" 0 80
die "No Valid Root Disk Found"
Expand All @@ -31,6 +45,7 @@ update_root_checksums() {
mount -o rw,remount /boot
fi

DEBUG "calculating hashes for $CONFIG_ROOT_DIRLIST_PRETTY on $ROOT_MOUNT"
echo "+++ Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY "
# Intentional wordsplit
# shellcheck disable=SC2086
Expand All @@ -47,7 +62,12 @@ update_root_checksums() {
unmount_root_device
}
check_root_checksums() {
TRACE_FUNC
DEBUG "verifying existing hash file for $CONFIG_ROOT_DIRLIST_PRETTY"
if ! detect_root_device; then
if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then
show_unsupported_root_layout_and_die "root hash verification"
fi
whiptail_error --title 'ERROR: No Valid Root Disk Found' \
--msgbox "No Valid Root Disk Found" 0 80
die "No Valid Root Disk Found"
Expand All @@ -74,6 +94,7 @@ check_root_checksums() {
update_root_checksums
return 0
else
DEBUG "Root hash file not created (user declined)"
exit 1
fi
fi
Expand Down Expand Up @@ -124,6 +145,7 @@ check_root_checksums() {

return 0
else
DEBUG "Signatures not updated (user declined after new-files warning)"
return 1
fi
fi
Expand Down Expand Up @@ -154,6 +176,7 @@ check_root_checksums() {
update_root_checksums
return 0
else
DEBUG "Signatures not updated (user declined after hash-check failure)"
return 1
fi
fi
Expand All @@ -164,21 +187,69 @@ check_root_checksums() {
open_block_device_lvm() {
TRACE_FUNC
local VG="$1"
local LV MAPPER_VG MAPPER_LV name lvpath FIRST_LV_PREFERRED FIRST_LV_FALLBACK

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"
# Prefer an LV named 'root' (used by Qubes), but fall back to any LV
# in the VG. This ensures Ubuntu-style names (e.g. ubuntu-vg/ubuntu-root)
# also work.
LV="/dev/$VG/root"
if ! [ -e "$LV" ]; then
MAPPER_VG="${VG//-/--}"
LV="/dev/mapper/${MAPPER_VG}-root"
fi
if ! [ -e "$LV" ]; then
FIRST_LV_PREFERRED=""
FIRST_LV_FALLBACK=""
DEBUG "LVM VG $VG has no 'root' LV, enumerating all LVs"
# list LV names and prefer root-like names
for name in $(lvm lvs --noheadings -o lv_name --separator ' ' "$VG" 2>/dev/null); do
# thin pool/metadata and swap-like LVs are not root filesystems
case "$name" in
*pool*|*tmeta*|*tdata*|*tpool*|swap*)
DEBUG "skipping LV name $name (not a root LV candidate)"
continue
;;
esac

lvpath="/dev/$VG/$name"
if ! [ -e "$lvpath" ]; then
MAPPER_LV="${name//-/--}"
lvpath="/dev/mapper/${VG//-/--}-${MAPPER_LV}"
fi
if [ -e "$lvpath" ]; then
case "$name" in
root|dom0|dom0-root|qubes_dom0|qubes_dom0-root|*dom0*root*|*root*)
[ -n "$FIRST_LV_PREFERRED" ] || FIRST_LV_PREFERRED="$lvpath"
DEBUG "preferred LV candidate $lvpath (name $name)"
;;
*)
[ -n "$FIRST_LV_FALLBACK" ] || FIRST_LV_FALLBACK="$lvpath"
;;
esac
fi
done

if [ -n "$FIRST_LV_PREFERRED" ]; then
DEBUG "selecting preferred LV $FIRST_LV_PREFERRED in VG $VG"
LV="$FIRST_LV_PREFERRED"
elif [ -n "$FIRST_LV_FALLBACK" ]; then
DEBUG "falling back to first mountable LV $FIRST_LV_FALLBACK in VG $VG"
LV="$FIRST_LV_FALLBACK"
else
LV=""
fi
fi
if ! [ -e "$LV" ]; then
DEBUG "no usable LV found in VG $VG"
return 1
fi

# Use the root LV now
open_block_device_layers "/dev/mapper/$VG-root"
# Use selected LV
open_block_device_layers "$LV"
}

# Open a LUKS device, then continue looking for more layers.
Expand All @@ -195,6 +266,15 @@ open_block_device_luks() {
return 1
fi

# Inform LVM about any new physical volume inside this decrypted container.
# Some distributions (Fedora) require a vgscan before LVM will create nodes
# under /dev/mapper, otherwise our later search won't see the logical
# volumes. This is harmless on systems without lvm installed.
if command -v lvm >/dev/null 2>&1; then
DEBUG "running vgscan to populate /dev/mapper after unlocking LUKS"
lvm vgscan --mknodes >/dev/null 2>&1 || true
fi

open_block_device_layers "/dev/mapper/$LUKSDEV"
}

Expand Down Expand Up @@ -241,14 +321,28 @@ open_block_device_layers() {
open_root_device_no_clean_up() {
TRACE_FUNC
local DEVICE="$1"
local FS_DEVICE
local FS_DEVICE BLKID_OUT

# 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

# Keep detection minimal for initrd: only require blkid to return some
# metadata before mount probing. TYPE is often unavailable in this initrd.
BLKID_OUT="$(blkid "$FS_DEVICE" 2>/dev/null || true)"
DEBUG "blkid output for $FS_DEVICE: $BLKID_OUT"

# If blkid reports nothing at all, this is likely not a filesystem-bearing
# partition. Skip mount probing to avoid noisy kernel probe logs.
if [ -z "$BLKID_OUT" ]; then
ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer with no recognizable filesystem metadata."
DEBUG "Skipping $FS_DEVICE: blkid returned no filesystem metadata"
return 1
fi

# Mount the device
if ! mount -o ro "$FS_DEVICE" "$ROOT_MOUNT" &>/dev/null; then
ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer on $FS_DEVICE but it could not be mounted as root by this root-hash flow."
DEBUG "Can't mount filesystem on $FS_DEVICE from $DEVICE"
return 1
fi
Expand All @@ -269,14 +363,8 @@ open_root_device_no_clean_up() {
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.
# Deactivate the VG directly. This avoids recursive LV close probing noise
# for LV paths that are not PVs and matches the minimal initrd workflow.
lvm vgchange -an "$VG" || \
DEBUG "Can't close LVM VG: $VG"
}
Expand Down Expand Up @@ -325,7 +413,7 @@ close_block_device_layers() {
open_root_device() {
TRACE_FUNC
if ! open_root_device_no_clean_up "$1"; then
unmount_root_device
close_root_device "$1"
return 1
fi

Expand Down Expand Up @@ -360,37 +448,66 @@ detect_root_device()
fi
# Ensure nothing is opened/mounted
unmount_root_device
ROOT_DETECT_UNSUPPORTED_REASON=""

# check $CONFIG_ROOT_DEV if set/valid
if [ -e "$CONFIG_ROOT_DEV" ] && open_root_device "$CONFIG_ROOT_DEV"; then
# run open_root_device with fd10 closed so external tools don't inherit it
if [ -e "$CONFIG_ROOT_DEV" ] && open_root_device "$CONFIG_ROOT_DEV" 10<&-; then
return 0
fi

# generate list of possible boot devices
fdisk -l 2>/dev/null | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" > /tmp/disklist
DEBUG "detect_root_device: initial disklist=$(cat /tmp/disklist | tr '\n' ' ')"

# filter out extraneous options
> /tmp_root_device_list
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))
DEBUG "detect_root_device: candidate $i has $DEV_NUM_PARTITIONS numeric partitions"
if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then
echo $i >> /tmp_root_device_list
else
ls $i* | tail -${DEV_NUM_PARTITIONS} >> /tmp_root_device_list
fi
done 10</tmp/disklist

# log the list after filtering
DEBUG "detect_root_device: filtered candidates=$(cat /tmp_root_device_list | tr '\n' ' ')"

# iterate through possible options
while IFS= read -r -u 10 i; do
if open_root_device "$i"; then
# CONFIG_ROOT_DEV is valid device and contains an installed OS
DEBUG "detect_root_device: trying candidate $i"
# close fd10 for the called command so it isn't inherited by tools like
# lvm, which otherwise complain about a leaked descriptor.
if open_root_device "$i" 10<&-; then
DEBUG "detect_root_device: candidate $i succeeded"
CONFIG_ROOT_DEV="$i"
return 0
else
DEBUG "detect_root_device: candidate $i failed"
fi
done 10</tmp_root_device_list

# failed to find root on physical partitions; try any mapped devices
for m in /dev/mapper/*; do
# skip non-existent or non-block devices such as the control node
[ -e "$m" ] || continue
[ -b "$m" ] || continue

DEBUG "detect_root_device: trying mapper device $m as potential root"
if open_root_device "$m"; then
CONFIG_ROOT_DEV="$m"
DEBUG "detect_root_device: mapper device $m appears to contain root files"
return 0
fi
done

# no valid root device found
if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then
DEBUG "$ROOT_DETECT_UNSUPPORTED_REASON"
fi
echo "Unable to locate $ROOT_MOUNT files on any mounted disk"
return 1
}
Expand Down
53 changes: 48 additions & 5 deletions initrd/etc/functions
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
#!/bin/bash

# maintain a cross-script trace stack. When a script sources /etc/functions
# this appends the script name/line to TRACE_STACK; the variable is exported so
# it survives into children invoked with exec. TRACE_FUNC will prepend this
# stack to the normal function call stack, giving a full picture from init to
# the current point (even across multiple scripts).
# Only add the current script once to avoid repetition when the same script
# sources this file multiple times or invokes TRACE_FUNC repeatedly.
case "${TRACE_STACK}" in
*"main($0:"*)
;;
*)
TRACE_STACK="${TRACE_STACK:+$TRACE_STACK -> }main($0:0)"
export TRACE_STACK
;;
esac
Comment on lines +10 to +17
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TRACE_STACK header comment says it appends the script name/line, but the code always records ":0" (TRACE_STACK="...main($0:0)"). Consider capturing the actual source location (e.g., using BASH_SOURCE[1] and BASH_LINENO[0]) so traces are accurate and match the documented format.

Suggested change
case "${TRACE_STACK}" in
*"main($0:"*)
;;
*)
TRACE_STACK="${TRACE_STACK:+$TRACE_STACK -> }main($0:0)"
export TRACE_STACK
;;
esac
# Determine the calling script and line, if available.
if [ "${#BASH_SOURCE[@]}" -ge 2 ]; then
_TRACE_SRC="${BASH_SOURCE[1]}"
else
_TRACE_SRC="$0"
fi
if [ "${#BASH_LINENO[@]}" -ge 1 ]; then
_TRACE_LINE="${BASH_LINENO[0]}"
else
_TRACE_LINE=0
fi
_TRACE_ENTRY="main(${_TRACE_SRC}:${_TRACE_LINE})"
case "${TRACE_STACK}" in
*"${_TRACE_ENTRY}"*)
;;
*)
TRACE_STACK="${TRACE_STACK:+$TRACE_STACK -> }${_TRACE_ENTRY}"
export TRACE_STACK
;;
esac
unset _TRACE_SRC _TRACE_LINE _TRACE_ENTRY

Copilot uses AI. Check for mistakes.

# ------- Start of functions coming from /etc/ash_functions

die() {
Expand Down Expand Up @@ -680,8 +696,12 @@ TRACE_FUNC() {
# Append the direct caller (without extra " -> " at the end)
stack_trace+="${FUNCNAME[1]}(${BASH_SOURCE[1]}:${BASH_LINENO[0]})"

# Print the final trace output
TRACE "${stack_trace}"
# Print the final trace output, including any inherited script-level stack
if [ -n "$TRACE_STACK" ]; then
TRACE "$TRACE_STACK -> $stack_trace"
else
TRACE "${stack_trace}"
fi
}

# Show the entire current call stack in debug output - useful if a catastrophic
Expand Down Expand Up @@ -1277,12 +1297,35 @@ verify_checksums() {
# Check if a device is an LVM2 PV, and if so print the VG name
find_lvm_vg_name() {
TRACE_FUNC
local DEVICE VG
local DEVICE VG part
DEVICE="$1"

# closing fd10 should be handled by callers (detect_root_device now
# closes it for commands before invoking us). leaving this here can
# interfere with future uses of fd10 elsewhere in the same shell.
# (Note: previous versions contained a hack to close it here; see
# commit 700ed0c141.)

mkdir -p /tmp/root-hashes-gui
if ! lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/dev/null; then
# It's not an LVM PV
# Try to query whether DEVICE is an LVM physical volume. On systems
# without LVM the command may not exist; treat that like "not a PV".
if ! lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then
# It's not an LVM PV, or lvm failed entirely. Log stderr for debugging.
DEBUG "lvm pvs failed for $DEVICE, stderr:" "$(cat /tmp/root-hashes-gui/lvm_err)"
Comment on lines 1309 to +1314
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_lvm_vg_name() writes its scratch output under /tmp/root-hashes-gui/* even though it lives in the shared /etc/functions library. This creates a hard coupling to the root-hashes flow and can also clobber data if another caller uses the same path. Consider using a more generic per-function temp dir (or mktemp) so /etc/functions utilities remain reusable.

Copilot uses AI. Check for mistakes.
# try any children shown by lsblk (handles LUKS containers with
# internal partitions such as dm-0, dm-1 etc).
if command -v lsblk >/dev/null 2>&1; then
DEBUG "find_lvm_vg_name: lsblk children of $DEVICE"
for part in $(lsblk -np -l -o NAME "$DEVICE" | tail -n +2); do
[ -b "$part" ] || continue
DEBUG "find_lvm_vg_name: testing child $part"
if lvm pvs --noheadings -o vg_name "$part" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then
VG="$(awk 'NF {print $1; exit}' /tmp/root-hashes-gui/lvm_vg)"
[ -n "$VG" ] && { echo "$VG"; return 0; }
fi
done
fi
DEBUG "find_lvm_vg_name: $DEVICE is not an LVM PV"
return 1
fi

Expand Down