From 297292366fcd9a81bd0e2b5f1f0c245920b86141 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 29 Apr 2026 19:32:49 +0800 Subject: [PATCH 1/7] perf(convert): reduce rootfs copies from 3 to 1 using qcow2 overlay snapshots Replace the intermediate rootfs.img file and final qemu-img convert step with qcow2 overlay/snapshot architecture. The source image is modified through a writable overlay (source-mod), then forked into two independent snapshots (source-read for verity hash, source-write for dracut). This eliminates two full rootfs copies (~17s dd + ~32s qemu-img convert). Additional fixes: - Fix RPM version detection: rpm -q outputs error to stdout on uninstalled packages, which was captured as the version string - Fix unbound boot_part variable when source has no separate boot partition --- cryptpilot-convert.sh | 537 +++++++++++++++++++++++++++++------------- 1 file changed, 377 insertions(+), 160 deletions(-) diff --git a/cryptpilot-convert.sh b/cryptpilot-convert.sh index e964372..4a03d42 100755 --- a/cryptpilot-convert.sh +++ b/cryptpilot-convert.sh @@ -161,9 +161,6 @@ disk::nbd_available() { disk::get_available_nbd() { { lsmod | grep nbd >/dev/null; } || modprobe nbd max_part=8 - # If run in container, use following instead - # - # mknod /dev/nbd0 b 43 0 local a for a in /dev/nbd[0-9] /dev/nbd[1-9][0-9]; do @@ -174,6 +171,26 @@ disk::get_available_nbd() { return 1 } +# Allocate an available NBD device, connect it to a qcow2 image, and register cleanup hook. +# Usage: disk::nbd_connect [--discard=on] [--detect-zeroes=unmap] +# Sets the global variable named by var_name to the connected NBD device path. +disk::nbd_connect() { + local image_file=$1 + local var_name=$2 + shift 2 + local qemu_nbd_opts=(--connect="PLACEHOLDER" "$image_file" "$@") + + local nbd_dev + nbd_dev="$(disk::get_available_nbd)" || proc::fatal "no free NBD device for ${var_name}" + + proc::hook_exit "qemu-nbd -d ${nbd_dev} >/dev/null 2>&1 || true" + qemu-nbd --connect="${nbd_dev}" "$@" "${image_file}" + sleep 2 + + # Assign to caller's variable + eval "${var_name}=\"${nbd_dev}\"" +} + disk::umount_wait_busy() { while true; do if ! mountpoint -q "$1"; then @@ -507,9 +524,9 @@ disk::install_rpm_on_rootfs() { # Try to query the version of cryptpilot-fde-host from the current system if command -v rpm >/dev/null 2>&1; then - cryptpilot_fde_version=$(rpm -q cryptpilot-fde-host --qf '%{VERSION}-%{RELEASE}' 2>/dev/null || true) + cryptpilot_fde_version=$(rpm -q cryptpilot-fde-host --qf '%{VERSION}-%{RELEASE}' 2>/dev/null) || cryptpilot_fde_version="" elif command -v dpkg-query >/dev/null 2>&1; then - cryptpilot_fde_version=$(dpkg-query -W -f='${Version}' cryptpilot-fde-host 2>/dev/null || true) + cryptpilot_fde_version=$(dpkg-query -W -f='${Version}' cryptpilot-fde-host 2>/dev/null) || cryptpilot_fde_version="" fi local essential_packages_with_version=() @@ -1109,19 +1126,93 @@ EOF } - # Remove read-only flag from rootfs.img - tune2fs -O ^read-only "${rootfs_file_path}" + if [ "${operate_on_device}" = false ]; then + # File mode: mount rootfs from source-write, EFI/boot from output + local rootfs_mount_point="${workdir}/rootfs" + local source_write_rootfs_part="${source_write_device}p${source_rootfs_part_num}" + + # Clear the read-only flag set by tune2fs during shrink, so we can mount rw for dracut + log::info "Clearing read-only flag on source-write rootfs" + tune2fs -O ^read-only "${source_write_rootfs_part}" >/dev/null 2>&1 || true + + mkdir -p "${rootfs_mount_point}" + + # Mount rootfs from source-write + mount "${source_write_rootfs_part}" "${rootfs_mount_point}" + proc::hook_exit "mountpoint -q ${rootfs_mount_point} && disk::umount_wait_busy ${rootfs_mount_point}" + + # Mount required pseudo-filesystems + for dir in dev dev/pts proc run sys tmp; do + local target="${rootfs_mount_point}/$dir" + mkdir -p "$target" + proc::hook_exit "mountpoint -q '$target' && disk::umount_wait_busy '$target'" + case "$dir" in + dev) mount -t devtmpfs devtmpfs "$target" ;; + dev/pts) mount -t devpts devpts "$target" ;; + proc) mount -t proc proc "$target" ;; + run) mount -t tmpfs tmpfs "$target" ;; + sys) mount -t sysfs sysfs "$target" ;; + tmp) mount -t tmpfs tmpfs "$target" ;; + esac + done + + # Mount /boot (from output boot partition or from source-write rootfs) + local boot_target="${rootfs_mount_point}/boot" + mkdir -p "$boot_target" + if [ "$uki" = false ] && [ -n "${boot_part_num:-}" ]; then + # Boot partition exists on output - mount it so dracut can find kernel files + mount "${output_device}p${boot_part_num}" "$boot_target" + proc::hook_exit "mountpoint -q '$boot_target' && disk::umount_wait_busy '$boot_target'" + fi + + # Mount EFI from output + if [ "$efi_part_exist" = "true" ]; then + local efi_target="${rootfs_mount_point}/boot/efi" + mkdir -p "$efi_target" + mount "${output_device}p${efi_part_num}" "$efi_target" + proc::hook_exit "mountpoint -q '$efi_target' && disk::umount_wait_busy '$efi_target'" + fi + + # Bind-mount network config + for file in resolv.conf hosts; do + local src="/etc/$file" + local dst="${rootfs_mount_point}/etc/$file" + local backup="${dst}.cryptpilot" + mv "$dst" "$backup" 2>/dev/null || true + touch "$dst" + proc::hook_exit "mountpoint -q '$dst' && disk::umount_wait_busy '$dst'" + mount -o bind,ro "$(realpath "$src")" "$dst" + done + + # Run dracut + log::info "Executing dracut in chroot" + update_initrd_inner "${rootfs_mount_point}" "${uki}" "${uki_append_cmdline}" - # Note that the rootfs.img will not be used any more so mount it without '-o ro' flag will not change the hash of rootfs. - run_in_chroot_mounts "$rootfs_file_path" "$efi_part" "$boot_file_path" update_initrd_inner "$uki" "$uki_append_cmdline" + # Cleanup mounts (reverse order) + for dir in etc/hosts etc/resolv.conf boot/efi boot sys run proc dev/pts dev; do + disk::umount_wait_busy "${rootfs_mount_point}/$dir" 2>/dev/null || true + done + for file in resolv.conf hosts; do + local dst="${rootfs_mount_point}/etc/$file" + local backup="${dst}.cryptpilot" + if [ -f "$backup" ]; then + rm -f "$dst" + mv "$backup" "$dst" + fi + done + disk::umount_wait_busy "${rootfs_mount_point}" + else + # Device mode: use run_in_chroot_mounts + run_in_chroot_mounts "/dev/mapper/rootfs" "$efi_part" "$boot_file_path" update_initrd_inner "$uki" "$uki_append_cmdline" + fi } -step::shrink_and_extract_rootfs_part() { +step::shrink_rootfs() { local rootfs_orig_part=$1 # Mark the rootfs partition as read-only tune2fs -O read-only "${rootfs_orig_part}" - + # Adjust file system content, all move to front local before_shrink_size_in_bytes before_shrink_size_in_bytes=$(blockdev --getsize64 "${rootfs_orig_part}") @@ -1145,7 +1236,6 @@ step::shrink_and_extract_rootfs_part() { after_shrink_block_size=$(dumpe2fs "${rootfs_orig_part}" 2>/dev/null | grep 'Block size' | awk '{print $3}') local after_shrink_block_count after_shrink_block_count=$(dumpe2fs "${rootfs_orig_part}" 2>/dev/null | grep 'Block count' | awk '{print $3}') - local after_shrink_size_in_bytes after_shrink_size_in_bytes=$((after_shrink_block_size * after_shrink_block_count)) local after_shrink_size_in_sector after_shrink_size_in_sector=$((after_shrink_block_size * after_shrink_block_count / sector_size)) @@ -1155,111 +1245,242 @@ step::shrink_and_extract_rootfs_part() { echo " Size in Bytes: $after_shrink_size_in_bytes" echo " Size in Sector: $after_shrink_size_in_sector" - # Extract rootfs to file on disk - rootfs_file_path="${workdir}/rootfs.img" - log::info "Extract rootfs to file on disk ${rootfs_file_path}" - dd status=progress if="${rootfs_orig_part}" of="${rootfs_file_path}" "count=${after_shrink_size_in_bytes}" iflag=count_bytes bs=256M if [ "${wipe_freed_space}" = true ]; then log::info "Wipe rootfs partition on device ${before_shrink_size_in_bytes} bytes" - dd status=progress if=/dev/zero of="${rootfs_orig_part}" count="${before_shrink_size_in_bytes}" iflag=count_bytes bs=64M # Clean the freed space with zero, so that the qemu-img convert would generate smaller image + dd status=progress if=/dev/zero of="${rootfs_orig_part}" count="${before_shrink_size_in_bytes}" iflag=count_bytes bs=64M fi - # Delete the original rootfs partition - log::info "Deleting original rootfs partition" - parted "$device" --script -- rm "${rootfs_orig_part_num}" - partprobe "$device" # Inform the OS of partition table changes + # In file mode, do NOT delete the rootfs partition — we need it for dd from source-read. + # In device mode, delete it to make room for LUKS/LVM. + if [ "${operate_on_device}" = true ]; then + log::info "Deleting original rootfs partition" + parted "$device" --script -- rm "${rootfs_orig_part_num}" + partprobe "$device" + fi } -step::create_boot_part() { - local boot_file_path=$1 - local boot_start_sector=$2 - - local boot_part_num="${rootfs_orig_part_num}" - local boot_size_in_bytes - boot_size_in_bytes=$(stat --printf="%s" "$boot_file_path") - local boot_size_in_sector=$((boot_size_in_bytes / sector_size)) - boot_start_sector=$(disk::align_start_sector "${boot_start_sector}") - boot_part_end_sector=$((boot_start_sector + boot_size_in_sector - 1)) - log::info "Creating boot partition ($boot_start_sector ... $boot_part_end_sector sectors)" - parted "$device" --script -- mkpart boot ext4 "${boot_start_sector}"s ${boot_part_end_sector}s - partprobe "$device" +step::prepare_output_and_snapshots() { + log::step "[ 5 ] Preparing output file and snapshots" + + # Save the source rootfs partition number before any output modifications. + # source-read/source-write keep the original partition layout; only output changes. + source_rootfs_part_num="${rootfs_orig_part_num}" + + # Disconnect source-mod NBD (release lock for snapshot creation) + log::info "Disconnecting source-mod to create snapshots" + qemu-nbd -d "${source_mod_device}" + sleep 3 + + # Create two snapshots from the same backing file (source-mod) + log::info "Creating source-read (read-only) and source-write (writable) snapshots" + source_read_file="${input_file}.source-read" + source_write_file="${input_file}.source-write" + qemu-img create -f qcow2 -b "${source_mod_file}" -F qcow2 "${source_read_file}" >/dev/null + qemu-img create -f qcow2 -b "${source_mod_file}" -F qcow2 "${source_write_file}" >/dev/null + proc::hook_exit "rm -f ${source_read_file} ${source_write_file}" + + # Connect snapshots using disk::nbd_connect (allocates + connects + registers hook atomically) + log::info "Connecting source-read snapshot" + disk::nbd_connect "${source_read_file}" source_read_device + log::info "Connecting source-write snapshot" + disk::nbd_connect "${source_write_file}" source_write_device + partprobe "${source_read_device}" + partprobe "${source_write_device}" + partprobe "${output_device}" udevadm settle --timeout=10 - boot_part="${device}p${boot_part_num}" - [[ $boot_size_in_bytes == $(blockdev --getsize64 "$boot_part") ]] || log::error "Wrong size, something wrong in the script" - log::info "Writing boot filesystem to partition" - dd status=progress if="$boot_file_path" of="$boot_part" bs=4M + + # Copy the partition table from source-read to output + # (rootfs partition is preserved since we didn't delete it in shrink) + log::info "Copying partition table to output file" + sfdisk -d "${source_read_device}" > "${workdir}/partition_table.sfdisk" + sfdisk "${output_device}" < "${workdir}/partition_table.sfdisk" + + # If source had no separate boot partition, we need to create one on output + # (boot content was extracted to boot.img during step 2) + if [ "$boot_part_exist" = "false" ] && [ "$uki" = false ]; then + # Delete the rootfs partition first to free up space for the new boot partition. + # The data is safe on source-read — we only modify the output partition table. + log::info "Deleting rootfs partition on output to make room for boot partition" + parted "${output_device}" --script -- rm "${rootfs_orig_part_num}" + partprobe "${output_device}" + udevadm settle --timeout=10 + + # Create boot partition at original rootfs start with exact BOOT_PART_SIZE + local boot_start_sector + boot_start_sector=$(disk::align_start_sector "${rootfs_orig_start_sector}") + local boot_size_sectors=$(( ${BOOT_PART_SIZE%M} * 1024 * 1024 / sector_size )) + local boot_end_sector=$((boot_start_sector + boot_size_sectors - 1)) + log::info "Creating boot partition: ${boot_start_sector}s - ${boot_end_sector}s (${BOOT_PART_SIZE})" + parted "${output_device}" --script -- mkpart boot ext4 "${boot_start_sector}s" "${boot_end_sector}s" + + # Recreate rootfs partition to fill remaining space (from after boot to end of disk) + local rootfs_new_start=$((boot_end_sector + 1)) + rootfs_new_start=$(disk::align_start_sector "${rootfs_new_start}") + log::info "Recreating rootfs partition: ${rootfs_new_start}s to end of disk" + parted "${output_device}" --script -- mkpart primary ext4 "${rootfs_new_start}s" '100%' + + # Re-detect partitions + partprobe "${output_device}" + udevadm settle --timeout=10 + + # Re-detect partition numbers on output. + # Since we deleted the old rootfs and created boot + new rootfs, + # the two highest-numbered partitions are boot and rootfs. + local all_parts + all_parts=$(parted "${output_device}" --script -- print 2>/dev/null | awk 'NR>7 && /^[[:space:]]*[0-9]+/ {print $1}') + local max_part=0 + for p in $all_parts; do + [[ $p -gt $max_part ]] && max_part=$p + done + rootfs_orig_part_num=$max_part + boot_part_num=$((max_part - 1)) + + if [ -z "$boot_part_num" ] || [ -z "$rootfs_orig_part_num" ]; then + proc::fatal "Failed to detect new partition numbers on output device" + fi + + # Set boot flag + parted "${output_device}" --script -- set "${boot_part_num}" boot on + + # Format the newly created boot partition with ext4 + log::info "Formatting output boot partition" + mkfs.ext4 -F "${output_device}p${boot_part_num}" >/dev/null 2>&1 + + # Track output boot partition number separately from the original source detection. + # boot_part_exist reflects whether the SOURCE had a boot partition. + output_boot_part_num="${boot_part_num}" + else + partprobe "${output_device}" + udevadm settle --timeout=10 + fi + + log::info "source-read device: ${source_read_device}" + log::info "source-write device: ${source_write_device}" + log::info "output device: ${output_device}" + lsblk "${output_device}" } -step::create_lvm_part() { - local lvm_start_sector=$1 - local lvm_part_num=$2 - - local lvm_part="${device}p${lvm_part_num}" - lvm_start_sector=$(disk::align_start_sector "${lvm_start_sector}") - log::info "Creating lvm partition as LVM PV ($lvm_start_sector ... last sector)" - parted "$device" --script -- mkpart primary "${lvm_start_sector}s" "100%" - parted "$device" --script -- set "${lvm_part_num}" lvm on - partprobe "$device" - - log::info "Initializing LVM physical volume and volume group" - proc::exec_subshell_flose_fds pvcreate --force "$lvm_part" - proc::exec_subshell_flose_fds vgcreate --force cryptpilot "$lvm_part" --setautoactivation n # disable auto activation of LVM volumes to prevent it from being activated unexpectedly - proc::exec_subshell_flose_fds vgchange -a y cryptpilot # activate the volume group +step::copy_partitions() { + log::step "[ 6 ] Copying EFI and boot partitions" + + # dd EFI partition (preserve UUID, labels, all metadata) + log::info "Copying EFI partition" + dd if="${source_read_device}p${efi_part_num}" of="${output_device}p${efi_part_num}" bs=4M status=progress + + # Populate the output boot partition. + # Two cases: + # 1. Source already had a boot partition → dd from source-read (raw copy, preserves filesystem) + # 2. Source had /boot inside rootfs → copy kernel files from boot.img to the ext4-formatted output partition + if [ "$uki" = false ]; then + if [ "$boot_part_exist" = "true" ]; then + # Source already had a separate boot partition — raw copy preserves UUID and filesystem + log::info "Copying boot partition from source" + dd if="${source_read_device}p${boot_part_num}" of="${output_device}p${boot_part_num}" bs=4M status=progress + else + # Boot partition was created in step 5, formatted with ext4 — copy kernel files from boot.img + log::info "Copying kernel files to boot partition" + local boot_img_mount="${workdir}/boot_img" + local boot_part_mount="${workdir}/boot_part" + mkdir -p "$boot_img_mount" "$boot_part_mount" + + mount -o ro "${boot_file_path}" "$boot_img_mount" + mount "${output_device}p${boot_part_num}" "$boot_part_mount" + # Only copy vmlinuz kernel images — dracut will regenerate initramfs and other + # boot files. Skip old initramfs, grub modules, rescue images to save space. + find "${boot_img_mount}" -maxdepth 1 -type f -name 'vmlinuz-*' -print0 | while IFS= read -r -d '' src_file; do + cp -f "$src_file" "$boot_part_mount/" + done + disk::umount_wait_busy "$boot_part_mount" + disk::umount_wait_busy "$boot_img_mount" + fi + fi } step::setup_rootfs_lv_with_encrypt() { - local rootfs_file_path=$1 - local rootfs_passphrase=$2 - - local rootfs_size_in_byte - rootfs_size_in_byte=$(stat --printf="%s" "${rootfs_file_path}") - local rootfs_lv_size_in_bytes=$((rootfs_size_in_byte + 16 * 1024 * 1024)) # original rootfs partition size plus LUKS2 header size - log::info "Creating rootfs logical volume" - proc::hook_exit "[[ -e /dev/mapper/cryptpilot-rootfs ]] && disk::dm_remove_all ${device}" - proc::exec_subshell_flose_fds lvcreate -n rootfs --size ${rootfs_lv_size_in_bytes}B cryptpilot # Note that the real size will be a little bit larger than the specified size, since they will be aligned to the Physical Extentsize (PE) size, which by default is 4MB. - # Create a encrypted volume - log::info "Encrypting rootfs logical volume with LUKS2" - echo -n "${rootfs_passphrase}" | cryptsetup luksFormat --type luks2 --cipher aes-xts-plain64 --subsystem cryptpilot /dev/mapper/cryptpilot-rootfs --key-file=- + local rootfs_passphrase=$1 + + if [ "${operate_on_device}" = false ]; then + local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" + local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" + else + local output_rootfs_part="${rootfs_orig_part}" + local source_rootfs_part="${rootfs_read_device}" + fi + + # LUKS directly on the target partition + log::info "Encrypting rootfs partition with LUKS2" + echo -n "${rootfs_passphrase}" | cryptsetup luksFormat \ + --type luks2 --cipher aes-xts-plain64 --subsystem cryptpilot \ + "${output_rootfs_part}" --key-file=- proc::hook_exit "[[ -e /dev/mapper/rootfs ]] && disk::dm_remove_wait_busy rootfs" log::info "Opening encrypted rootfs volume" - echo -n "${rootfs_passphrase}" | cryptsetup open /dev/mapper/cryptpilot-rootfs rootfs --key-file=- - # Copy rootfs content to the encrypted volume - log::info "Copying rootfs content to the encrypted volume" - dd status=progress "if=${rootfs_file_path}" of=/dev/mapper/rootfs bs=4M + echo -n "${rootfs_passphrase}" | cryptsetup open "${output_rootfs_part}" rootfs --key-file=- + # Copy rootfs content to the encrypted volume (the ONLY full rootfs copy). + # Use the shrunk filesystem size — the partition on the source may be larger + # than the output (e.g., when a boot partition is carved out). + local fs_block_size fs_block_count rootfs_size + fs_block_size=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block size' | awk '{print $3}') + fs_block_count=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block count' | awk '{print $3}') + rootfs_size=$((fs_block_size * fs_block_count)) + log::info "Copying rootfs content to the encrypted volume (filesystem: ${rootfs_size} bytes, partition: $(blockdev --getsize64 "${source_rootfs_part}") bytes)" + dd status=progress "if=${source_rootfs_part}" of=/dev/mapper/rootfs bs=4M count="${rootfs_size}" iflag=count_bytes disk::dm_remove_wait_busy rootfs } step::setup_rootfs_lv_without_encrypt() { - local rootfs_file_path=$1 - - local rootfs_size_in_byte - rootfs_size_in_byte=$(stat --printf="%s" "${rootfs_file_path}") - local rootfs_lv_size_in_bytes=$((rootfs_size_in_byte + 16 * 1024 * 1024)) # original rootfs partition size plus LUKS2 header size - log::info "Creating rootfs logical volume" - proc::hook_exit "[[ -e /dev/mapper/cryptpilot-rootfs ]] && disk::dm_remove_all ${device}" - proc::exec_subshell_flose_fds lvcreate -n rootfs --size ${rootfs_lv_size_in_bytes}B cryptpilot # Note that the real size will be a little bit larger than the specified size, since they will be aligned to the Physical Extentsize (PE) size, which by default is 4MB. - # Copy rootfs content to the lvm volume - log::info "Copying rootfs content to the logical volume" - dd status=progress "if=${rootfs_file_path}" of=/dev/mapper/cryptpilot-rootfs bs=4M + if [ "${operate_on_device}" = false ]; then + local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" + local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" + else + local output_rootfs_part="${rootfs_orig_part}" + local source_rootfs_part="${rootfs_read_device}" + fi + + # Copy only the shrunk filesystem (partition may be larger than actual filesystem) + local fs_block_size fs_block_count rootfs_size + fs_block_size=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block size' | awk '{print $3}') + fs_block_count=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block count' | awk '{print $3}') + rootfs_size=$((fs_block_size * fs_block_count)) + log::info "Copying rootfs content to the target partition (filesystem: ${rootfs_size} bytes)" + dd status=progress "if=${source_rootfs_part}" of="${output_rootfs_part}" bs=4M count="${rootfs_size}" iflag=count_bytes } step::setup_rootfs_hash_lv() { - local rootfs_file_path=$1 + if [ "${operate_on_device}" = false ]; then + local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" + else + local source_rootfs_part="${rootfs_read_device}" + fi local rootfs_hash_file_path="${workdir}/rootfs_hash.img" - veritysetup format "${rootfs_file_path}" "${rootfs_hash_file_path}" --format=1 --hash=sha256 | + + # Calculate filesystem size for verity (partition may be larger than actual filesystem) + local fs_block_size fs_block_count fs_size_bytes data_blocks + fs_block_size=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block size' | awk '{print $3}') + fs_block_count=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block count' | awk '{print $3}') + fs_size_bytes=$((fs_block_size * fs_block_count)) + data_blocks=$((fs_size_bytes / 4096)) # verity default data block size + + veritysetup format "${source_rootfs_part}" "${rootfs_hash_file_path}" \ + --format=1 --hash=sha256 --data-blocks "${data_blocks}" | tee "${workdir}/rootfs_hash.status" | gawk '(/^Root hash:/ && $NF ~ /^[0-9a-fA-F]+$/) { print $NF; }' \ >"${workdir}/rootfs_hash.roothash" cat "${workdir}/rootfs_hash.status" - local rootfs_hash_size_in_byte - rootfs_hash_size_in_byte=$(stat --printf="%s" "${rootfs_hash_file_path}") - proc::hook_exit "[[ -e /dev/mapper/cryptpilot-rootfs_hash ]] && disk::dm_remove_all ${device}" - proc::exec_subshell_flose_fds lvcreate -n rootfs_hash --size "${rootfs_hash_size_in_byte}"B cryptpilot - dd status=progress "if=${rootfs_hash_file_path}" of=/dev/mapper/cryptpilot-rootfs_hash bs=4M - rm -f "${rootfs_hash_file_path}" - disk::dm_remove_all "${device}" + if [ "${operate_on_device}" = true ]; then + # Device mode: store hash in LVM partition inside the encrypted container + local rootfs_hash_size_in_byte + rootfs_hash_size_in_byte=$(stat --printf="%s" "${rootfs_hash_file_path}") + proc::hook_exit "[[ -e /dev/mapper/cryptpilot-rootfs_hash ]] && disk::dm_remove_all ${device}" + proc::exec_subshell_flose_fds lvcreate -n rootfs_hash --size "${rootfs_hash_size_in_byte}"B cryptpilot + dd status=progress "if=${rootfs_hash_file_path}" of=/dev/mapper/cryptpilot-rootfs_hash bs=4M + rm -f "${rootfs_hash_file_path}" + disk::dm_remove_all "${device}" + else + # File mode: keep hash in workdir (will be included in initrd via --include) + log::info "Verity hash file stored at ${rootfs_hash_file_path}" + fi # Recording rootfs hash in metadata file log::info "Generate metadata file" @@ -1296,6 +1517,7 @@ main() { local rootfs_orig_part local rootfs_orig_part_num local rootfs_orig_part_exist=false + local source_rootfs_part_num # Source partition number (unchanged; differs from output when boot partition is added) local wipe_freed_space=false local uki=false local uki_append_cmdline="console=tty0 console=ttyS0,115200n8" @@ -1455,39 +1677,42 @@ main() { else log::info "Using input file: $input_file" qemu-img info "${input_file}" - device="$(disk::get_available_nbd)" || proc::fatal "no free NBD device" - local work_file="${input_file}.work" - if [ -f "${work_file}" ]; then - if flock --exclusive --nonblock "${work_file}"; then - log::error "File ${work_file} is locked by another process, maybe another cryptpilot instance is using it. Please stop it and try again." - exit 1 - else - log::warn "Temporary file ${work_file} already exists, delete it now" - rm -f "${work_file}" + # Clean up any leftover overlay files from previous runs + for overlay_file in "${input_file}.source-mod" "${input_file}.source-read" "${input_file}.source-write"; do + if [ -f "${overlay_file}" ]; then + log::warn "Temporary file ${overlay_file} already exists from a previous run, deleting it" + rm -f "${overlay_file}" fi - fi + done # Try to detect input file format local input_format input_format=$(qemu-img info "${input_file}" | grep '^file format:' | awk '{print $3}') - - # Try to create work file with backing file for faster processing - log::info "Detected input format: ${input_format}" - proc::hook_exit "rm -f ${work_file}" - if qemu-img create -f qcow2 -b "${input_file}" -F "${input_format}" "${work_file}" 2>/dev/null; then - log::info "Created work file ${work_file} with backing file ${input_file}" - else - log::warn "Failed to create work file with backing file, falling back to direct copy" - log::info "Copying ${input_file} to ${work_file}" - cp "${input_file}" "${work_file}" - fi - proc::hook_exit "qemu-nbd --disconnect ${device} >/dev/null" - qemu-nbd --connect="${device}" --discard=on --detect-zeroes=unmap "${work_file}" - sleep 2 - log::info "Mapped to NBD device ${device}:" - fdisk -l "${device}" + # Create source-mod overlay: all modifications (yum, grub, shrink) go here + source_mod_file="${input_file}.source-mod" + proc::hook_exit "rm -f ${source_mod_file}" + qemu-img create -f qcow2 -b "${input_file}" -F "${input_format}" "${source_mod_file}" >/dev/null + log::info "Created source-mod overlay: ${source_mod_file}" + + # Create output file upfront (same virtual size as input) + local virtual_size_bytes + virtual_size_bytes=$(qemu-img info --output=json "${input_file}" | grep '"virtual-size"' | grep -o '[0-9]\+') + qemu-img create -f qcow2 -o size="${virtual_size_bytes}" "${output_file}" >/dev/null + log::info "Created output file: ${output_file} (virtual size: ${virtual_size_bytes} bytes)" + + # Allocate and connect NBD devices one at a time + disk::nbd_connect "${source_mod_file}" source_mod_device --discard=on --detect-zeroes=unmap + log::info "Mapped source-mod to NBD device ${source_mod_device}:" + fdisk -l "${source_mod_device}" + + disk::nbd_connect "${output_file}" output_device --discard=on --detect-zeroes=unmap + log::info "Mapped output to NBD device ${output_device}:" + fdisk -l "${output_device}" + + # For backward compatibility in partition detection functions, alias device to source_mod_device + device="${source_mod_device}" fi disk::assert_disk_not_busy "${device}" @@ -1537,36 +1762,33 @@ main() { step:update_rootfs "${efi_part}" "${boot_file_path}" "${uki}" # - # 4. Shrinking rootfs and extract + # 4. Shrinking rootfs # - log::step "[ 4 ] Shrinking rootfs and extract" - step::shrink_and_extract_rootfs_part "${rootfs_orig_part}" + log::step "[ 4 ] Shrinking rootfs" + step::shrink_rootfs "${rootfs_orig_part}" # - # 5. Create a boot partition + # 5. Preparing output file and snapshots # - log::step "[ 5 ] Creating boot partition" - if [ "$boot_part_exist" = "false" ] && [ "$uki" = false ]; then - local boot_part_end_sector - step::create_boot_part "${boot_file_path}" "${rootfs_orig_start_sector}" - elif [ "$boot_part_exist" = "false" ] && [ "$uki" = true ]; then - log::info "Skipped since UKI mode does not require a separate boot partition" + if [ "${operate_on_device}" = false ]; then + # File-based mode: use qcow2 overlays + log::step "[ 5 ] Preparing output file and snapshots" + step::prepare_output_and_snapshots else - log::info "Skipped since boot partition already exist" + # Device mode: create read-only loop device on source partition + # as a replacement for rootfs.img + log::step "[ 5 ] Creating read-only snapshot of source partition" + rootfs_read_device=$(losetup -r --sizelimit "${after_shrink_size_in_bytes}" --show -f "${rootfs_orig_part}") + log::info "Created read-only loop device: ${rootfs_read_device}" + proc::hook_exit "losetup -d ${rootfs_read_device} 2>/dev/null || true" fi # - # 6. Creating lvm partition + # 6. Copying EFI and boot partitions # - log::step "[ 6 ] Creating lvm partition" - if [ "$boot_part_exist" = "true" ]; then - step::create_lvm_part "$rootfs_orig_start_sector" "$rootfs_orig_part_num" - elif [ "$boot_part_exist" = "false" ] && [ "$uki" = false ]; then - step::create_lvm_part "$((boot_part_end_sector + 1))" "$((rootfs_orig_part_num + 1))" - else - # In UKI mode with no boot partition, we start right after the EFI partition - # or at the beginning of the available space - step::create_lvm_part "$rootfs_orig_start_sector" "$rootfs_orig_part_num" + log::step "[ 6 ] Copying EFI and boot partitions" + if [ "${operate_on_device}" = false ]; then + step::copy_partitions fi # @@ -1574,16 +1796,20 @@ main() { # log::step "[ 7 ] Setting up rootfs logical volume" if [ "${rootfs_no_encryption}" = false ]; then - step::setup_rootfs_lv_with_encrypt "${rootfs_file_path}" "${rootfs_passphrase}" + if [ "${operate_on_device}" = false ]; then + step::setup_rootfs_lv_with_encrypt "${rootfs_passphrase}" + else + step::setup_rootfs_lv_with_encrypt "${rootfs_passphrase}" + fi else - step::setup_rootfs_lv_without_encrypt "${rootfs_file_path}" + step::setup_rootfs_lv_without_encrypt fi # # 8. Setting up rootfs hash volume # log::step "[ 8 ] Setting up rootfs hash volume" - step::setup_rootfs_hash_lv "${rootfs_file_path}" + step::setup_rootfs_hash_lv # # 9. Update initrd @@ -1595,7 +1821,7 @@ main() { if [ "$uki" = true ]; then step:update_initrd "${efi_part}" "" "${uki}" "${uki_append_cmdline}" else - step:update_initrd "${efi_part}" "${boot_part}" "${uki}" "${uki_append_cmdline}" + step:update_initrd "${efi_part}" "${boot_file_path}" "${uki}" "${uki_append_cmdline}" fi fi @@ -1603,32 +1829,23 @@ main() { # 10. Cleaning up # log::step "[ 10 ] Cleaning up" - disk::dm_remove_all "${device}" - blockdev --flushbufs "${device}" + if [ "${operate_on_device}" = true ]; then + disk::dm_remove_all "${device}" + blockdev --flushbufs "${device}" + fi - if [ "${operate_on_device}" == true ]; then + if [ "${operate_on_device}" = true ]; then log::success "--------------------------------" log::success "Everything done, the device is ready to use: ${device}" else # - # 11. Generating new image file + # 11. Finalizing # - log::step "[ 11 ] Generating new image file" - qemu-nbd --disconnect "${device}" - sleep 2 # wait for the qemu-nbd daemon to release the file lock - - # check suffix of the output file - local output_file_suffix=${output_file##*.} - if [[ "${output_file_suffix}" == "vhd" ]]; then - qemu-img convert -p -O vpc "${work_file}" "${output_file}" - elif [[ ${output_file_suffix} == "qcow2" ]]; then - # It is not worth to enable the compression option "-c", since it does increase the compression time. - qemu-img convert -p -O qcow2 "${work_file}" "${output_file}" - else - log::warn "Unknown output file suffix: ${output_file_suffix}" - log::info "Generating qcow2 file by default" - qemu-img convert -p -O qcow2 "${work_file}" "${output_file}" - fi + log::step "[ 11 ] Finalizing" + qemu-nbd -d "${source_read_device}" + qemu-nbd -d "${source_write_device}" + qemu-nbd -d "${output_device}" + sleep 2 log::success "--------------------------------" log::success "Everything done, the new disk image is ready to use: ${output_file}" From 07fc888195c0669823afe79c0bdd15cddc6b1afc Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 29 Apr 2026 20:11:04 +0800 Subject: [PATCH 2/7] refactor(convert): remove --device and operate_on_device support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deprecate in-place device conversion in favor of file-based (--in/--out) conversion only. All device-mode code paths and conditional branches are removed, simplifying the codebase. Changes: - Remove operate_on_device variable and all conditional branches - Remove device-mode validation, confirmation prompt, and cleanup logic - Keep --device/-d flag but emit a deprecation warning - Remove --wipe-freed-space option (emit deprecation warning if used) - Update help text to reflect deprecation - Update docs: quick-start.md, quick-start_zh.md — replace --device examples with --in/--out and add deprecation notes (since v0.7.0) --- cryptpilot-convert.sh | 255 ++++++++------------------ cryptpilot-fde/docs/quick-start.md | 13 +- cryptpilot-fde/docs/quick-start_zh.md | 13 +- 3 files changed, 85 insertions(+), 196 deletions(-) diff --git a/cryptpilot-convert.sh b/cryptpilot-convert.sh index 4a03d42..57f1e97 100755 --- a/cryptpilot-convert.sh +++ b/cryptpilot-convert.sh @@ -222,10 +222,9 @@ proc::print_help_and_exit() { echo "Usage:" echo " $0 --in --out --config-dir --rootfs-passphrase [--package ...]" echo " $0 --in --out --config-dir --rootfs-no-encryption [--package ...]" - echo " $0 --device --config-dir --rootfs-passphrase [--package ...]" echo "" echo "Options:" - echo " -d, --device The device to operate on." + echo " -d, --device Deprecated: operating on devices is no longer supported. Use --in/--out instead." echo " --in The input OS image file (vhd or qcow2)." echo " --out The output OS image file (vhd or qcow2)." echo " -c, --config-dir The directory containing cryptpilot configuration files." @@ -237,7 +236,6 @@ proc::print_help_and_exit() { echo " --package Specify an RPM package name or path to the RPM file to install in to the disk before" echo " -b, --boot_part_size Instead of using the default partition size(512MB), specify the size of the boot partition" echo " converting. This can be specified multiple times." - echo " --wipe-freed-space Wipe the freed space with zero, so that the qemu-img convert would generate smaller image" echo " --uki Generate a Unified Kernel Image image and boot from it instead of boot with GRUB" echo " --uki-append-cmdline Append custom command line parameters when generating a UKI image. By default, only essential" echo " parameters are included. This option allows you to extend the kernel command line. The default" @@ -1126,10 +1124,9 @@ EOF } - if [ "${operate_on_device}" = false ]; then - # File mode: mount rootfs from source-write, EFI/boot from output - local rootfs_mount_point="${workdir}/rootfs" - local source_write_rootfs_part="${source_write_device}p${source_rootfs_part_num}" + # File mode: mount rootfs from source-write, EFI/boot from output + local rootfs_mount_point="${workdir}/rootfs" + local source_write_rootfs_part="${source_write_device}p${source_rootfs_part_num}" # Clear the read-only flag set by tune2fs during shrink, so we can mount rw for dracut log::info "Clearing read-only flag on source-write rootfs" @@ -1201,10 +1198,6 @@ EOF fi done disk::umount_wait_busy "${rootfs_mount_point}" - else - # Device mode: use run_in_chroot_mounts - run_in_chroot_mounts "/dev/mapper/rootfs" "$efi_part" "$boot_file_path" update_initrd_inner "$uki" "$uki_append_cmdline" - fi } step::shrink_rootfs() { @@ -1244,19 +1237,6 @@ step::shrink_rootfs() { echo " Block count: $after_shrink_block_count" echo " Size in Bytes: $after_shrink_size_in_bytes" echo " Size in Sector: $after_shrink_size_in_sector" - - if [ "${wipe_freed_space}" = true ]; then - log::info "Wipe rootfs partition on device ${before_shrink_size_in_bytes} bytes" - dd status=progress if=/dev/zero of="${rootfs_orig_part}" count="${before_shrink_size_in_bytes}" iflag=count_bytes bs=64M - fi - - # In file mode, do NOT delete the rootfs partition — we need it for dd from source-read. - # In device mode, delete it to make room for LUKS/LVM. - if [ "${operate_on_device}" = true ]; then - log::info "Deleting original rootfs partition" - parted "$device" --script -- rm "${rootfs_orig_part_num}" - partprobe "$device" - fi } step::prepare_output_and_snapshots() { @@ -1399,13 +1379,8 @@ step::copy_partitions() { step::setup_rootfs_lv_with_encrypt() { local rootfs_passphrase=$1 - if [ "${operate_on_device}" = false ]; then - local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" - local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" - else - local output_rootfs_part="${rootfs_orig_part}" - local source_rootfs_part="${rootfs_read_device}" - fi + local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" + local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" # LUKS directly on the target partition log::info "Encrypting rootfs partition with LUKS2" @@ -1429,13 +1404,8 @@ step::setup_rootfs_lv_with_encrypt() { } step::setup_rootfs_lv_without_encrypt() { - if [ "${operate_on_device}" = false ]; then - local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" - local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" - else - local output_rootfs_part="${rootfs_orig_part}" - local source_rootfs_part="${rootfs_read_device}" - fi + local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" + local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" # Copy only the shrunk filesystem (partition may be larger than actual filesystem) local fs_block_size fs_block_count rootfs_size @@ -1447,11 +1417,7 @@ step::setup_rootfs_lv_without_encrypt() { } step::setup_rootfs_hash_lv() { - if [ "${operate_on_device}" = false ]; then - local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" - else - local source_rootfs_part="${rootfs_read_device}" - fi + local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" local rootfs_hash_file_path="${workdir}/rootfs_hash.img" # Calculate filesystem size for verity (partition may be larger than actual filesystem) @@ -1468,19 +1434,8 @@ step::setup_rootfs_hash_lv() { >"${workdir}/rootfs_hash.roothash" cat "${workdir}/rootfs_hash.status" - if [ "${operate_on_device}" = true ]; then - # Device mode: store hash in LVM partition inside the encrypted container - local rootfs_hash_size_in_byte - rootfs_hash_size_in_byte=$(stat --printf="%s" "${rootfs_hash_file_path}") - proc::hook_exit "[[ -e /dev/mapper/cryptpilot-rootfs_hash ]] && disk::dm_remove_all ${device}" - proc::exec_subshell_flose_fds lvcreate -n rootfs_hash --size "${rootfs_hash_size_in_byte}"B cryptpilot - dd status=progress "if=${rootfs_hash_file_path}" of=/dev/mapper/cryptpilot-rootfs_hash bs=4M - rm -f "${rootfs_hash_file_path}" - disk::dm_remove_all "${device}" - else - # File mode: keep hash in workdir (will be included in initrd via --include) - log::info "Verity hash file stored at ${rootfs_hash_file_path}" - fi + # File mode: keep hash in workdir (will be included in initrd via --include) + log::info "Verity hash file stored at ${rootfs_hash_file_path}" # Recording rootfs hash in metadata file log::info "Generate metadata file" @@ -1500,8 +1455,6 @@ main() { exit 1 fi - local operate_on_device - local device local input_file local output_file local config_dir @@ -1518,14 +1471,13 @@ main() { local rootfs_orig_part_num local rootfs_orig_part_exist=false local source_rootfs_part_num # Source partition number (unchanged; differs from output when boot partition is added) - local wipe_freed_space=false local uki=false local uki_append_cmdline="console=tty0 console=ttyS0,115200n8" while [[ "$#" -gt 0 ]]; do case $1 in -d | --device) - device="$2" + log::warn "--device is deprecated: operating on devices is no longer supported. Use --in/--out instead." shift 2 ;; --in) @@ -1561,7 +1513,7 @@ main() { shift 2 ;; --wipe-freed-space) - wipe_freed_space=true + log::warn "--wipe-freed-space is deprecated: no longer needed with the new qcow2 overlay architecture" shift 1 ;; --uki) @@ -1581,15 +1533,8 @@ main() { esac done - if [ -n "${device:-}" ]; then - if [ -n "${input_file:-}" ] || [ -n "${output_file:-}" ]; then - proc::fatal "Cannot specify both --device and --in/--out" - fi - operate_on_device=true - elif [ -n "${input_file:-}" ] && [ -n "${output_file:-}" ]; then - operate_on_device=false - else - proc::fatal "Must specify either --device or --in/--out" + if [ -z "${input_file:-}" ] || [ -z "${output_file:-}" ]; then + proc::fatal "Must specify both --in and --out" fi if [ -z "${config_dir:-}" ]; then @@ -1606,38 +1551,13 @@ main() { proc::fatal "Must specify either --rootfs-passphrase or --rootfs-no-encryption" fi - if [ "${operate_on_device}" = true ]; then - if [ ! -b "${device}" ]; then - proc::fatal "Input device $device does not exist" - fi - - # In a better way to notice user that the data on the device may be lost if the operation is failed or canceled. - log::warn "This operation will overwrite data on the device ($device), and may cause data loss if the operation is failed or canceled. Make sure you have create a backup of the data !!!" - while true; do - read -r -p "Are you sure you want to continue? (y/n) " yn - case $yn in - [y]*) - log::success "Starting to convert the disk ..." - break - ;; - [n]*) - log::info "Operation canceled." - exit - ;; - *) log::warn "Please answer 'y' or 'n'." ;; - esac - done - elif [ "${operate_on_device}" = false ]; then - if [ ! -f "$input_file" ]; then - proc::fatal "Input file $input_file does not exist" - fi + if [ ! -f "$input_file" ]; then + proc::fatal "Input file $input_file does not exist" + fi - # Check if the input file is a vhd or qcow2 - if [[ "$input_file" != *.vhd ]] && [[ "$input_file" != *.qcow2 ]] && [[ "$input_file" != *.img ]]; then - proc::fatal "Input file $input_file is not supported, should be a vhd or qcow2 file" - fi - else - proc::print_help_and_exit 1 + # Check if the input file is a vhd or qcow2 + if [[ "$input_file" != *.vhd ]] && [[ "$input_file" != *.qcow2 ]] && [[ "$input_file" != *.img ]]; then + proc::fatal "Input file $input_file is not supported, should be a vhd, qcow2, or img file" fi log::setup_log_file @@ -1672,48 +1592,44 @@ main() { # log::step "[ 1 ] Prepare disk" - if [ "$operate_on_device" = true ]; then - log::info "Using device: $device" - else - log::info "Using input file: $input_file" - qemu-img info "${input_file}" - - # Clean up any leftover overlay files from previous runs - for overlay_file in "${input_file}.source-mod" "${input_file}.source-read" "${input_file}.source-write"; do - if [ -f "${overlay_file}" ]; then - log::warn "Temporary file ${overlay_file} already exists from a previous run, deleting it" - rm -f "${overlay_file}" - fi - done + log::info "Using input file: $input_file" + qemu-img info "${input_file}" - # Try to detect input file format - local input_format - input_format=$(qemu-img info "${input_file}" | grep '^file format:' | awk '{print $3}') - - # Create source-mod overlay: all modifications (yum, grub, shrink) go here - source_mod_file="${input_file}.source-mod" - proc::hook_exit "rm -f ${source_mod_file}" - qemu-img create -f qcow2 -b "${input_file}" -F "${input_format}" "${source_mod_file}" >/dev/null - log::info "Created source-mod overlay: ${source_mod_file}" - - # Create output file upfront (same virtual size as input) - local virtual_size_bytes - virtual_size_bytes=$(qemu-img info --output=json "${input_file}" | grep '"virtual-size"' | grep -o '[0-9]\+') - qemu-img create -f qcow2 -o size="${virtual_size_bytes}" "${output_file}" >/dev/null - log::info "Created output file: ${output_file} (virtual size: ${virtual_size_bytes} bytes)" - - # Allocate and connect NBD devices one at a time - disk::nbd_connect "${source_mod_file}" source_mod_device --discard=on --detect-zeroes=unmap - log::info "Mapped source-mod to NBD device ${source_mod_device}:" - fdisk -l "${source_mod_device}" - - disk::nbd_connect "${output_file}" output_device --discard=on --detect-zeroes=unmap - log::info "Mapped output to NBD device ${output_device}:" - fdisk -l "${output_device}" - - # For backward compatibility in partition detection functions, alias device to source_mod_device - device="${source_mod_device}" - fi + # Clean up any leftover overlay files from previous runs + for overlay_file in "${input_file}.source-mod" "${input_file}.source-read" "${input_file}.source-write"; do + if [ -f "${overlay_file}" ]; then + log::warn "Temporary file ${overlay_file} already exists from a previous run, deleting it" + rm -f "${overlay_file}" + fi + done + + # Try to detect input file format + local input_format + input_format=$(qemu-img info "${input_file}" | grep '^file format:' | awk '{print $3}') + + # Create source-mod overlay: all modifications (yum, grub, shrink) go here + source_mod_file="${input_file}.source-mod" + proc::hook_exit "rm -f ${source_mod_file}" + qemu-img create -f qcow2 -b "${input_file}" -F "${input_format}" "${source_mod_file}" >/dev/null + log::info "Created source-mod overlay: ${source_mod_file}" + + # Create output file upfront (same virtual size as input) + local virtual_size_bytes + virtual_size_bytes=$(qemu-img info --output=json "${input_file}" | grep '"virtual-size"' | grep -o '[0-9]\+') + qemu-img create -f qcow2 -o size="${virtual_size_bytes}" "${output_file}" >/dev/null + log::info "Created output file: ${output_file} (virtual size: ${virtual_size_bytes} bytes)" + + # Allocate and connect NBD devices one at a time + disk::nbd_connect "${source_mod_file}" source_mod_device --discard=on --detect-zeroes=unmap + log::info "Mapped source-mod to NBD device ${source_mod_device}:" + fdisk -l "${source_mod_device}" + + disk::nbd_connect "${output_file}" output_device --discard=on --detect-zeroes=unmap + log::info "Mapped output to NBD device ${output_device}:" + fdisk -l "${output_device}" + + # Alias device to source_mod_device for backward compatibility with partition detection functions + device="${source_mod_device}" disk::assert_disk_not_busy "${device}" @@ -1770,37 +1686,21 @@ main() { # # 5. Preparing output file and snapshots # - if [ "${operate_on_device}" = false ]; then - # File-based mode: use qcow2 overlays - log::step "[ 5 ] Preparing output file and snapshots" - step::prepare_output_and_snapshots - else - # Device mode: create read-only loop device on source partition - # as a replacement for rootfs.img - log::step "[ 5 ] Creating read-only snapshot of source partition" - rootfs_read_device=$(losetup -r --sizelimit "${after_shrink_size_in_bytes}" --show -f "${rootfs_orig_part}") - log::info "Created read-only loop device: ${rootfs_read_device}" - proc::hook_exit "losetup -d ${rootfs_read_device} 2>/dev/null || true" - fi + log::step "[ 5 ] Preparing output file and snapshots" + step::prepare_output_and_snapshots # # 6. Copying EFI and boot partitions # log::step "[ 6 ] Copying EFI and boot partitions" - if [ "${operate_on_device}" = false ]; then - step::copy_partitions - fi + step::copy_partitions # # 7. Setting up rootfs logical volume # log::step "[ 7 ] Setting up rootfs logical volume" if [ "${rootfs_no_encryption}" = false ]; then - if [ "${operate_on_device}" = false ]; then - step::setup_rootfs_lv_with_encrypt "${rootfs_passphrase}" - else - step::setup_rootfs_lv_with_encrypt "${rootfs_passphrase}" - fi + step::setup_rootfs_lv_with_encrypt "${rootfs_passphrase}" else step::setup_rootfs_lv_without_encrypt fi @@ -1829,27 +1729,18 @@ main() { # 10. Cleaning up # log::step "[ 10 ] Cleaning up" - if [ "${operate_on_device}" = true ]; then - disk::dm_remove_all "${device}" - blockdev --flushbufs "${device}" - fi - if [ "${operate_on_device}" = true ]; then - log::success "--------------------------------" - log::success "Everything done, the device is ready to use: ${device}" - else - # - # 11. Finalizing - # - log::step "[ 11 ] Finalizing" - qemu-nbd -d "${source_read_device}" - qemu-nbd -d "${source_write_device}" - qemu-nbd -d "${output_device}" - sleep 2 - - log::success "--------------------------------" - log::success "Everything done, the new disk image is ready to use: ${output_file}" - fi + # + # 11. Finalizing + # + log::step "[ 11 ] Finalizing" + qemu-nbd -d "${source_read_device}" + qemu-nbd -d "${source_write_device}" + qemu-nbd -d "${output_device}" + sleep 2 + + log::success "--------------------------------" + log::success "Everything done, the new disk image is ready to use: ${output_file}" echo log::info "You can calculate reference value of the disk with:" diff --git a/cryptpilot-fde/docs/quick-start.md b/cryptpilot-fde/docs/quick-start.md index bf15257..77a1fb9 100644 --- a/cryptpilot-fde/docs/quick-start.md +++ b/cryptpilot-fde/docs/quick-start.md @@ -331,15 +331,18 @@ EOF cryptpilot-fde-host -c ./config_dir/ config check --keep-checking ``` -3. **Encrypt the disk** (assuming the disk is `/dev/nvme2n1`): +3. **Encrypt the disk image**: ```sh -cryptpilot-convert --device /dev/nvme2n1 \ +cryptpilot-convert --in ./original.qcow2 --out ./encrypted.qcow2 \ -c ./config_dir/ \ --rootfs-passphrase AAAaaawewe222 ``` -4. **Re-bind the disk** to the original instance and boot from it. +> **Note**: The `--device` option for in-place disk encryption was removed since v0.7.0. +> Use `--in`/`--out` for file-based conversion instead. + +4. **Attach the encrypted disk image** to the original instance and boot from it. ## Example 6: Using KBS Provider (Production) @@ -372,10 +375,6 @@ EOF # For disk images cryptpilot-convert --in ./original.qcow2 --out ./encrypted.qcow2 \ -c ./config_dir/ --rootfs-passphrase - -# For real disks -cryptpilot-convert --device /dev/nvme2n1 \ - -c ./config_dir/ --rootfs-passphrase ``` ### Boot Process diff --git a/cryptpilot-fde/docs/quick-start_zh.md b/cryptpilot-fde/docs/quick-start_zh.md index fe28390..a77db3d 100644 --- a/cryptpilot-fde/docs/quick-start_zh.md +++ b/cryptpilot-fde/docs/quick-start_zh.md @@ -331,15 +331,18 @@ EOF cryptpilot-fde-host -c ./config_dir/ config check --keep-checking ``` -3. **加密磁盘**(假设磁盘是 `/dev/nvme2n1`): +3. **加密磁盘镜像**: ```sh -cryptpilot-convert --device /dev/nvme2n1 \ +cryptpilot-convert --in ./original.qcow2 --out ./encrypted.qcow2 \ -c ./config_dir/ \ --rootfs-passphrase AAAaaawewe222 ``` -4. **重新绑定磁盘**到原始实例并从其启动。 +> **注意**:自 v0.7.0 起,`--device` 选项(就地磁盘加密)已被移除。 +> 请改用 `--in`/`--out` 进行基于文件的转换。 + +4. **将加密后的磁盘镜像**挂载到原始实例并从中启动。 ## 示例 6:使用 KBS 提供者(生产环境) @@ -372,10 +375,6 @@ EOF # 磁盘镜像 cryptpilot-convert --in ./original.qcow2 --out ./encrypted.qcow2 \ -c ./config_dir/ --rootfs-passphrase <实际-rootfs-密钥> - -# 真实磁盘 -cryptpilot-convert --device /dev/nvme2n1 \ - -c ./config_dir/ --rootfs-passphrase <实际-rootfs-密钥> ``` ### 启动过程 From e335a0de5e7389cf8ec0b95c476f187155532d39 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 29 Apr 2026 20:21:23 +0800 Subject: [PATCH 3/7] docs: remove deprecated --device references from documentation Remove "Example 5: Encrypt a Real System Disk" section which duplicated Example 1 after the --device removal. Renumber remaining examples (4: KBS, 5: KMS). Update prerequisites and troubleshooting to remove real-disk mentions. Add deprecation note at the top of both quick-start docs. --- cryptpilot-fde/docs/quick-start.md | 71 ++++----------------------- cryptpilot-fde/docs/quick-start_zh.md | 71 ++++----------------------- 2 files changed, 18 insertions(+), 124 deletions(-) diff --git a/cryptpilot-fde/docs/quick-start.md b/cryptpilot-fde/docs/quick-start.md index 77a1fb9..494dec9 100644 --- a/cryptpilot-fde/docs/quick-start.md +++ b/cryptpilot-fde/docs/quick-start.md @@ -5,7 +5,10 @@ This guide walks you through encrypting a bootable OS disk with full disk encryp ## Prerequisites - cryptpilot-fde-host installed on your system -- A bootable qcow2 disk image, or an unmounted real disk +- A bootable qcow2 disk image + +> **Note**: The `--device` option for in-place disk encryption was removed since v0.7.0. +> Use `--in`/`--out` for file-based conversion only. ## Prepare Configuration @@ -289,62 +292,7 @@ Example output: } ``` -## Example 5: Encrypt a Real System Disk - -For production systems, you need to encrypt a real disk. - -> [!IMPORTANT] -> **DO NOT encrypt the active disk you are booting from!** -> -> You must: -> 1. Unbind the disk from the instance -> 2. Bind it to another instance as a data disk -> 3. Encrypt it -> 4. Re-bind it to the original instance - -### Steps - -1. **Prepare configuration** (same as above): - -```sh -mkdir -p ./config_dir -cat << EOF > ./config_dir/fde.toml -[rootfs] -delta_location = "disk" - -[rootfs.encrypt.exec] -command = "echo" -args = ["-n", "AAAaaawewe222"] - -[delta] -integrity = true - -[delta.encrypt.exec] -command = "echo" -args = ["-n", "AAAaaawewe222"] -EOF -``` - -2. **Validate configuration**: - -```sh -cryptpilot-fde-host -c ./config_dir/ config check --keep-checking -``` - -3. **Encrypt the disk image**: - -```sh -cryptpilot-convert --in ./original.qcow2 --out ./encrypted.qcow2 \ - -c ./config_dir/ \ - --rootfs-passphrase AAAaaawewe222 -``` - -> **Note**: The `--device` option for in-place disk encryption was removed since v0.7.0. -> Use `--in`/`--out` for file-based conversion instead. - -4. **Attach the encrypted disk image** to the original instance and boot from it. - -## Example 6: Using KBS Provider (Production) +## Example 4: Using KBS Provider (Production) For production environments, use Key Broker Service with remote attestation. @@ -387,7 +335,7 @@ When booting, the system will: 4. If verified, KBS returns the decryption key 5. System decrypts and boots -## Example 7: Using KMS Provider (Cloud-Managed) +## Example 5: Using KMS Provider (Cloud-Managed) For Alibaba Cloud users, use KMS for centralized key management. @@ -484,11 +432,10 @@ Common issues: If `cryptpilot-convert` fails: -1. **Check disk format**: Only qcow2 images are supported for disk images +1. **Check disk format**: qcow2 and VHD images are supported 2. **Check disk size**: Ensure enough space for encryption overhead -3. **For real disks**: Ensure the disk is unmounted and not in use -4. **Device already exists error**: If you see errors like `/dev/cryptpilot: already exists in filesystem`, it may be leftover from a previous failed convert. Try `dmsetup remove_all` to clean up -5. **Check logs**: The last convert's detailed log is saved at `/tmp/.cryptpilot-convert.log` +3. **Device already exists error**: If you see errors like `/dev/cryptpilot: already exists in filesystem`, it may be leftover from a previous failed convert. Try `dmsetup remove_all` to clean up +4. **Check logs**: The last convert's detailed log is saved at `/tmp/.cryptpilot-convert.log` ### Boot Failed diff --git a/cryptpilot-fde/docs/quick-start_zh.md b/cryptpilot-fde/docs/quick-start_zh.md index a77db3d..ada6048 100644 --- a/cryptpilot-fde/docs/quick-start_zh.md +++ b/cryptpilot-fde/docs/quick-start_zh.md @@ -5,7 +5,10 @@ ## 前置条件 - 已安装 cryptpilot-fde -- 可启动的 qcow2 磁盘镜像,或未挂载的真实磁盘 +- 可启动的 qcow2 磁盘镜像 + +> **注意**:自 v0.7.0 起,`--device` 选项(就地磁盘加密)已被移除。 +> 请改用 `--in`/`--out` 进行基于文件的转换。 ## 准备配置 @@ -289,62 +292,7 @@ cryptpilot-fde-host show-reference-value --disk ./uki-encrypted.qcow2 } ``` -## 示例 5:加密真实系统磁盘 - -对于生产系统,你需要加密真实磁盘。 - -> [!IMPORTANT] -> **不要加密正在启动的活动磁盘!** -> -> 你必须: -> 1. 从实例解绑磁盘 -> 2. 将其作为数据盘绑定到另一个实例 -> 3. 加密它 -> 4. 重新绑定到原始实例 - -### 步骤 - -1. **准备配置**(与上面相同): - -```sh -mkdir -p ./config_dir -cat << EOF > ./config_dir/fde.toml -[rootfs] -delta_location = "disk" - -[rootfs.encrypt.exec] -command = "echo" -args = ["-n", "AAAaaawewe222"] - -[delta] -integrity = true - -[delta.encrypt.exec] -command = "echo" -args = ["-n", "AAAaaawewe222"] -EOF -``` - -2. **验证配置**: - -```sh -cryptpilot-fde-host -c ./config_dir/ config check --keep-checking -``` - -3. **加密磁盘镜像**: - -```sh -cryptpilot-convert --in ./original.qcow2 --out ./encrypted.qcow2 \ - -c ./config_dir/ \ - --rootfs-passphrase AAAaaawewe222 -``` - -> **注意**:自 v0.7.0 起,`--device` 选项(就地磁盘加密)已被移除。 -> 请改用 `--in`/`--out` 进行基于文件的转换。 - -4. **将加密后的磁盘镜像**挂载到原始实例并从中启动。 - -## 示例 6:使用 KBS 提供者(生产环境) +## 示例 4:使用 KBS 提供者(生产环境) 对于生产环境,使用带有远程证明的密钥代理服务。 @@ -387,7 +335,7 @@ cryptpilot-convert --in ./original.qcow2 --out ./encrypted.qcow2 \ 4. 如果验证通过,KBS 返回解密密钥 5. 系统解密并启动 -## 示例 7:使用 KMS 提供者(云托管) +## 示例 5:使用 KMS 提供者(云托管) 对于阿里云用户,使用 KMS 进行集中式密钥管理。 @@ -484,11 +432,10 @@ cryptpilot-fde-host -c ./config_dir/ config check --keep-checking 如果 `cryptpilot-convert` 失败: -1. **检查磁盘格式**:磁盘镜像仅支持 qcow2 格式 +1. **检查磁盘格式**:支持 qcow2 和 VHD 格式 2. **检查磁盘大小**:确保有足够空间用于加密开销 -3. **对于真实磁盘**:确保磁盘未挂载且不在使用中 -4. **设备已存在错误**:如果出现类似 `/dev/cryptpilot: already exists in filesystem` 的错误,可能是上次 convert 失败遗留的,尝试 `dmsetup remove_all` 清除 -5. **查看日志**:最后一次 convert 的详细日志保存在 `/tmp/.cryptpilot-convert.log` +3. **设备已存在错误**:如果出现类似 `/dev/cryptpilot: already exists in filesystem` 的错误,可能是上次 convert 失败遗留的,尝试 `dmsetup remove_all` 清除 +4. **查看日志**:最后一次 convert 的详细日志保存在 `/tmp/.cryptpilot-convert.log` ### 启动失败 From cf82b38fc3b134de009b062152a83561536cafc0 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 29 Apr 2026 20:41:48 +0800 Subject: [PATCH 4/7] refactor(convert): reuse setup_chroot_mounts in step::update_initrd Replace ~70 lines of duplicated mount/unmount code with a single call to setup_chroot_mounts, adding optional boot_override_part and efi_override_part parameters to handle the update_initrd's output-device mount needs. --- cryptpilot-convert.sh | 96 ++++++++++++------------------------------- 1 file changed, 27 insertions(+), 69 deletions(-) diff --git a/cryptpilot-convert.sh b/cryptpilot-convert.sh index 57f1e97..bbcbd5b 100755 --- a/cryptpilot-convert.sh +++ b/cryptpilot-convert.sh @@ -708,12 +708,16 @@ disk::install_deb_on_rootfs() { # $2 - Root filesystem device or image file to mount (e.g., /dev/sda2 or ./root.img) # $3 - EFI partition device path (optional; e.g., /dev/sda1) — only used if efi_part_exist=true # $4 - Boot file/device path (e.g., /dev/sda2 or ./boot.img) — used when boot_part_exist=false +# $5 - (Optional) Boot partition override — if set, use this instead of boot_part/boot_file_path +# $6 - (Optional) EFI partition override — if set, use this instead of efi_part # setup_chroot_mounts() { local rootfs="$1" local rootfs_file_or_part="$2" local efi_part="$3" local boot_file_path="$4" + local boot_override_part="${5:-}" + local efi_override_part="${6:-}" log::info "Preparing chroot environment at $rootfs" @@ -742,12 +746,14 @@ setup_chroot_mounts() { esac done - # Mount /boot — either from dedicated partition, from a file/image, or skip in UKI mode if no boot partition + # Mount /boot — use override if provided, otherwise follow existing logic local boot_target="$rootfs/boot" mkdir -p "$boot_target" proc::hook_exit "mountpoint -q '$boot_target' && disk::umount_wait_busy '$boot_target'" - if [ "$boot_part_exist" = "false" ]; then + if [ -n "$boot_override_part" ]; then + mount "$boot_override_part" "$boot_target" + elif [ "$boot_part_exist" = "false" ]; then if [ -n "$boot_file_path" ]; then # /boot is part of root or stored as a file (e.g., in embedded systems) mount "$boot_file_path" "$boot_target" @@ -758,7 +764,12 @@ setup_chroot_mounts() { fi # Conditionally mount EFI system partition under /boot/efi - if [ "$efi_part_exist" = "true" ] && [ -n "$efi_part" ]; then + if [ -n "$efi_override_part" ]; then + local efi_target="$rootfs/boot/efi" + mkdir -p "$efi_target" + proc::hook_exit "mountpoint -q '$efi_target' && disk::umount_wait_busy '$efi_target'" + mount "$efi_override_part" "$efi_target" + elif [ "$efi_part_exist" = "true" ] && [ -n "$efi_part" ]; then local efi_target="$rootfs/boot/efi" mkdir -p "$efi_target" proc::hook_exit "mountpoint -q '$efi_target' && disk::umount_wait_busy '$efi_target'" @@ -1128,76 +1139,23 @@ EOF local rootfs_mount_point="${workdir}/rootfs" local source_write_rootfs_part="${source_write_device}p${source_rootfs_part_num}" - # Clear the read-only flag set by tune2fs during shrink, so we can mount rw for dracut - log::info "Clearing read-only flag on source-write rootfs" - tune2fs -O ^read-only "${source_write_rootfs_part}" >/dev/null 2>&1 || true - - mkdir -p "${rootfs_mount_point}" - - # Mount rootfs from source-write - mount "${source_write_rootfs_part}" "${rootfs_mount_point}" - proc::hook_exit "mountpoint -q ${rootfs_mount_point} && disk::umount_wait_busy ${rootfs_mount_point}" - - # Mount required pseudo-filesystems - for dir in dev dev/pts proc run sys tmp; do - local target="${rootfs_mount_point}/$dir" - mkdir -p "$target" - proc::hook_exit "mountpoint -q '$target' && disk::umount_wait_busy '$target'" - case "$dir" in - dev) mount -t devtmpfs devtmpfs "$target" ;; - dev/pts) mount -t devpts devpts "$target" ;; - proc) mount -t proc proc "$target" ;; - run) mount -t tmpfs tmpfs "$target" ;; - sys) mount -t sysfs sysfs "$target" ;; - tmp) mount -t tmpfs tmpfs "$target" ;; - esac - done - - # Mount /boot (from output boot partition or from source-write rootfs) - local boot_target="${rootfs_mount_point}/boot" - mkdir -p "$boot_target" - if [ "$uki" = false ] && [ -n "${boot_part_num:-}" ]; then - # Boot partition exists on output - mount it so dracut can find kernel files - mount "${output_device}p${boot_part_num}" "$boot_target" - proc::hook_exit "mountpoint -q '$boot_target' && disk::umount_wait_busy '$boot_target'" - fi + # Clear the read-only flag set by tune2fs during shrink, so we can mount rw for dracut + log::info "Clearing read-only flag on source-write rootfs" + tune2fs -O ^read-only "${source_write_rootfs_part}" >/dev/null 2>&1 || true - # Mount EFI from output - if [ "$efi_part_exist" = "true" ]; then - local efi_target="${rootfs_mount_point}/boot/efi" - mkdir -p "$efi_target" - mount "${output_device}p${efi_part_num}" "$efi_target" - proc::hook_exit "mountpoint -q '$efi_target' && disk::umount_wait_busy '$efi_target'" - fi + # Determine boot partition for output device (empty in UKI mode — /boot lives in rootfs) + local boot_override_part="" + if [ "$uki" = false ] && [ -n "${boot_part_num:-}" ]; then + boot_override_part="${output_device}p${boot_part_num}" + fi - # Bind-mount network config - for file in resolv.conf hosts; do - local src="/etc/$file" - local dst="${rootfs_mount_point}/etc/$file" - local backup="${dst}.cryptpilot" - mv "$dst" "$backup" 2>/dev/null || true - touch "$dst" - proc::hook_exit "mountpoint -q '$dst' && disk::umount_wait_busy '$dst'" - mount -o bind,ro "$(realpath "$src")" "$dst" - done + setup_chroot_mounts "${rootfs_mount_point}" "${source_write_rootfs_part}" "${output_device}p${efi_part_num}" "${boot_file_path}" "${boot_override_part}" "${output_device}p${efi_part_num}" - # Run dracut - log::info "Executing dracut in chroot" - update_initrd_inner "${rootfs_mount_point}" "${uki}" "${uki_append_cmdline}" + # Run dracut + log::info "Executing dracut in chroot" + update_initrd_inner "${rootfs_mount_point}" "${uki}" "${uki_append_cmdline}" - # Cleanup mounts (reverse order) - for dir in etc/hosts etc/resolv.conf boot/efi boot sys run proc dev/pts dev; do - disk::umount_wait_busy "${rootfs_mount_point}/$dir" 2>/dev/null || true - done - for file in resolv.conf hosts; do - local dst="${rootfs_mount_point}/etc/$file" - local backup="${dst}.cryptpilot" - if [ -f "$backup" ]; then - rm -f "$dst" - mv "$backup" "$dst" - fi - done - disk::umount_wait_busy "${rootfs_mount_point}" + cleanup_chroot_mounts "${rootfs_mount_point}" } step::shrink_rootfs() { From 8a094ccbee5f493e3d3682166daa2908d4f1c2c0 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 29 Apr 2026 21:05:59 +0800 Subject: [PATCH 5/7] fix(convert): silence shellcheck warnings and remove duplicate log::step - Add function-level shellcheck disable=SC2154 on step:update_initrd and step::prepare_output_and_snapshots (variables set in main() and used in these step functions) - Remove unused output_boot_part_num variable - Remove duplicate log::step from step::prepare_output_and_snapshots and step::copy_partitions (main() already logs the step) --- cryptpilot-convert.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cryptpilot-convert.sh b/cryptpilot-convert.sh index bbcbd5b..355f33e 100755 --- a/cryptpilot-convert.sh +++ b/cryptpilot-convert.sh @@ -178,7 +178,6 @@ disk::nbd_connect() { local image_file=$1 local var_name=$2 shift 2 - local qemu_nbd_opts=(--connect="PLACEHOLDER" "$image_file" "$@") local nbd_dev nbd_dev="$(disk::get_available_nbd)" || proc::fatal "no free NBD device for ${var_name}" @@ -1041,6 +1040,7 @@ EOF } +# shellcheck disable=SC2154 step:update_initrd() { local efi_part=$1 local boot_file_path=$2 @@ -1165,8 +1165,6 @@ step::shrink_rootfs() { tune2fs -O read-only "${rootfs_orig_part}" # Adjust file system content, all move to front - local before_shrink_size_in_bytes - before_shrink_size_in_bytes=$(blockdev --getsize64 "${rootfs_orig_part}") log::info "Checking and shrinking rootfs filesystem" if e2fsck -y -f "${rootfs_orig_part}"; then @@ -1197,8 +1195,8 @@ step::shrink_rootfs() { echo " Size in Sector: $after_shrink_size_in_sector" } +# shellcheck disable=SC2154 step::prepare_output_and_snapshots() { - log::step "[ 5 ] Preparing output file and snapshots" # Save the source rootfs partition number before any output modifications. # source-read/source-write keep the original partition layout; only output changes. @@ -1286,7 +1284,6 @@ step::prepare_output_and_snapshots() { # Track output boot partition number separately from the original source detection. # boot_part_exist reflects whether the SOURCE had a boot partition. - output_boot_part_num="${boot_part_num}" else partprobe "${output_device}" udevadm settle --timeout=10 @@ -1299,7 +1296,6 @@ step::prepare_output_and_snapshots() { } step::copy_partitions() { - log::step "[ 6 ] Copying EFI and boot partitions" # dd EFI partition (preserve UUID, labels, all metadata) log::info "Copying EFI partition" From 19d5d072a85269f61571b6970fffbf7db3d1d658 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Thu, 30 Apr 2026 03:28:11 +0800 Subject: [PATCH 6/7] fix(convert): preserve EFI partition data after NBD disconnect Use guestfish to write EFI content directly to qcow2 file, bypassing the NBD kernel page cache invalidation issue that caused FAT32 filesystem data to be lost on qemu-nbd disconnect. Also fix EFI partition detection in external.rs where the lsblk FSTYPE column addition broke the partition filter, and add partprobe to ensure partition device nodes are created after NBD connect. --- Makefile | 3 + cryptpilot-convert.sh | 203 ++++++++++++++++++++++++---- cryptpilot-core/src/fs/nbd.rs | 6 + cryptpilot-fde/src/disk/external.rs | 20 ++- 4 files changed, 201 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index f04d71c..67ebab3 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ else MUSL_PATH_ARCH := $(ARCH) endif +# Auto-detect the latest built fde-guest RPM +CRYPTPILOT_FDE_RPM := $(shell ls -t /root/rpmbuild/RPMS/$(ARCH)/cryptpilot-fde-guest-*.rpm 2>/dev/null | head -n 1) + .PHONE: help help: @echo "Read README.md first" diff --git a/cryptpilot-convert.sh b/cryptpilot-convert.sh index 355f33e..1fab59c 100755 --- a/cryptpilot-convert.sh +++ b/cryptpilot-convert.sh @@ -1156,6 +1156,8 @@ EOF update_initrd_inner "${rootfs_mount_point}" "${uki}" "${uki_append_cmdline}" cleanup_chroot_mounts "${rootfs_mount_point}" + + sync } step::shrink_rootfs() { @@ -1231,6 +1233,59 @@ step::prepare_output_and_snapshots() { sfdisk -d "${source_read_device}" > "${workdir}/partition_table.sfdisk" sfdisk "${output_device}" < "${workdir}/partition_table.sfdisk" + # In UKI mode, resize the EFI partition to accommodate the UKI image (~250MB). + # The source image may have a small EFI partition that's too small for the UKI. + if [ "$uki" = true ]; then + local uki_efi_size="512M" + log::info "UKI mode: resizing EFI partition to ${uki_efi_size}" + # Must delete rootfs partition first to avoid overlap, then resize EFI, + # then recreate rootfs. All partition table modifications must complete + # BEFORE formatting the EFI partition, because partprobe re-reading the + # partition table invalidates the kernel page cache for all partitions + # on the device, discarding any unsynced filesystem data. + parted "${output_device}" --script -- rm "${rootfs_orig_part_num}" + parted "${output_device}" --script -- resizepart "${efi_part_num}" "${uki_efi_size}" + + # Recreate rootfs partition to fill remaining space + local rootfs_new_start + rootfs_new_start=$(parted "${output_device}" --script -- unit s print | awk '/^ *'"${efi_part_num}"' / {gsub(/s$/,"",$3); print $3; exit}') + rootfs_new_start=$((rootfs_new_start + 1)) + rootfs_new_start=$(disk::align_start_sector "${rootfs_new_start}") + log::info "Recreating rootfs partition from sector ${rootfs_new_start} to end" + parted "${output_device}" --script -- mkpart primary ext4 "${rootfs_new_start}s" '100%' + + # Now that all partition table modifications are done, format the EFI + # partition and copy EFI content from the source immediately. + # This must happen before any further parted operations that would + # trigger partprobe (which invalidates the kernel page cache). + local efi_part_size + efi_part_size=$(parted "${output_device}" --script -- unit B print | awk '/^ *'"${efi_part_num}"' / {gsub(/B$/,"",$4); print $4; exit}') + log::info "Formatting EFI partition with FAT32 (${efi_part_size} bytes)" + + # Run partprobe to make the new partition visible + partprobe "${output_device}" + udevadm settle --timeout=10 + + # Format the EFI partition + mkfs.vfat -F 32 "${output_device}p${efi_part_num}" >/dev/null + + # Immediately mount source EFI and output EFI, then copy content + local src_efi_mount="${workdir}/src_efi_prepare" + local dst_efi_mount="${workdir}/dst_efi_prepare" + mkdir -p "$src_efi_mount" "$dst_efi_mount" + mount -o ro "${source_read_device}p${efi_part_num}" "$src_efi_mount" + mount "${output_device}p${efi_part_num}" "$dst_efi_mount" + cp -a "$src_efi_mount/." "$dst_efi_mount/" + disk::umount_wait_busy "$dst_efi_mount" + disk::umount_wait_busy "$src_efi_mount" + rmdir "$src_efi_mount" "$dst_efi_mount" 2>/dev/null || true + + # Explicit sync to flush NBD page cache to the qcow2 file. + sync + + log::info "EFI partition formatted and content copied" + fi + # If source had no separate boot partition, we need to create one on output # (boot content was extracted to boot.img during step 2) if [ "$boot_part_exist" = "false" ] && [ "$uki" = false ]; then @@ -1281,12 +1336,17 @@ step::prepare_output_and_snapshots() { # Format the newly created boot partition with ext4 log::info "Formatting output boot partition" mkfs.ext4 -F "${output_device}p${boot_part_num}" >/dev/null 2>&1 + blockdev --flushbufs "${output_device}" # Track output boot partition number separately from the original source detection. # boot_part_exist reflects whether the SOURCE had a boot partition. else - partprobe "${output_device}" - udevadm settle --timeout=10 + # In UKI mode, EFI was already formatted and populated above; + # skip partprobe to avoid invalidating its page cache. + if [ "$uki" = false ]; then + partprobe "${output_device}" + udevadm settle --timeout=10 + fi fi log::info "source-read device: ${source_read_device}" @@ -1297,9 +1357,14 @@ step::prepare_output_and_snapshots() { step::copy_partitions() { - # dd EFI partition (preserve UUID, labels, all metadata) - log::info "Copying EFI partition" - dd if="${source_read_device}p${efi_part_num}" of="${output_device}p${efi_part_num}" bs=4M status=progress + # Copy EFI partition + # In UKI mode, the EFI partition was already formatted and populated in + # step::prepare_output_and_snapshots. + if [ "$uki" = false ]; then + # dd EFI partition (preserve UUID, labels, all metadata) + log::info "Copying EFI partition" + dd if="${source_read_device}p${efi_part_num}" of="${output_device}p${efi_part_num}" bs=4M status=progress + fi # Populate the output boot partition. # Two cases: @@ -1330,44 +1395,66 @@ step::copy_partitions() { fi } +step::setup_lvm() { + local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" + + # Set LVM flag on the rootfs partition + log::info "Setting LVM flag on rootfs partition" + parted "${output_device}" --script -- set "${rootfs_orig_part_num}" lvm on + partprobe "${output_device}" + udevadm settle --timeout=10 + + # Initialize LVM physical volume and volume group + log::info "Initializing LVM physical volume and volume group 'cryptpilot'" + pvcreate --force "${output_rootfs_part}" + vgcreate --force cryptpilot "${output_rootfs_part}" --setautoactivation n +} + step::setup_rootfs_lv_with_encrypt() { local rootfs_passphrase=$1 - local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" - # LUKS directly on the target partition - log::info "Encrypting rootfs partition with LUKS2" + # Calculate filesystem size for LV allocation + local fs_block_size fs_block_count rootfs_size + fs_block_size=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block size' | awk '{print $3}') + fs_block_count=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block count' | awk '{print $3}') + rootfs_size=$((fs_block_size * fs_block_count)) + # Add 16MB for LUKS2 header overhead + local rootfs_lv_size=$((rootfs_size + 16 * 1024 * 1024)) + + log::info "Creating rootfs logical volume (size: ${rootfs_lv_size} bytes)" + lvcreate -n rootfs --size "${rootfs_lv_size}"B cryptpilot + + # LUKS on the logical volume + log::info "Encrypting rootfs logical volume with LUKS2" echo -n "${rootfs_passphrase}" | cryptsetup luksFormat \ --type luks2 --cipher aes-xts-plain64 --subsystem cryptpilot \ - "${output_rootfs_part}" --key-file=- + /dev/mapper/cryptpilot-rootfs --key-file=- proc::hook_exit "[[ -e /dev/mapper/rootfs ]] && disk::dm_remove_wait_busy rootfs" log::info "Opening encrypted rootfs volume" - echo -n "${rootfs_passphrase}" | cryptsetup open "${output_rootfs_part}" rootfs --key-file=- - # Copy rootfs content to the encrypted volume (the ONLY full rootfs copy). - # Use the shrunk filesystem size — the partition on the source may be larger - # than the output (e.g., when a boot partition is carved out). - local fs_block_size fs_block_count rootfs_size - fs_block_size=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block size' | awk '{print $3}') - fs_block_count=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block count' | awk '{print $3}') - rootfs_size=$((fs_block_size * fs_block_count)) - log::info "Copying rootfs content to the encrypted volume (filesystem: ${rootfs_size} bytes, partition: $(blockdev --getsize64 "${source_rootfs_part}") bytes)" + echo -n "${rootfs_passphrase}" | cryptsetup open /dev/mapper/cryptpilot-rootfs rootfs --key-file=- + + log::info "Copying rootfs content to the encrypted volume (filesystem: ${rootfs_size} bytes)" dd status=progress "if=${source_rootfs_part}" of=/dev/mapper/rootfs bs=4M count="${rootfs_size}" iflag=count_bytes disk::dm_remove_wait_busy rootfs } step::setup_rootfs_lv_without_encrypt() { - local output_rootfs_part="${output_device}p${rootfs_orig_part_num}" local source_rootfs_part="${source_read_device}p${source_rootfs_part_num}" - # Copy only the shrunk filesystem (partition may be larger than actual filesystem) + # Calculate filesystem size for LV allocation local fs_block_size fs_block_count rootfs_size fs_block_size=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block size' | awk '{print $3}') fs_block_count=$(dumpe2fs "${source_rootfs_part}" 2>/dev/null | grep 'Block count' | awk '{print $3}') rootfs_size=$((fs_block_size * fs_block_count)) - log::info "Copying rootfs content to the target partition (filesystem: ${rootfs_size} bytes)" - dd status=progress "if=${source_rootfs_part}" of="${output_rootfs_part}" bs=4M count="${rootfs_size}" iflag=count_bytes + + log::info "Creating rootfs logical volume (size: ${rootfs_size} bytes)" + lvcreate -n rootfs --size "${rootfs_size}"B cryptpilot + + log::info "Copying rootfs content to the logical volume" + dd status=progress "if=${source_rootfs_part}" of=/dev/mapper/cryptpilot-rootfs bs=4M count="${rootfs_size}" iflag=count_bytes } step::setup_rootfs_hash_lv() { @@ -1388,8 +1475,15 @@ step::setup_rootfs_hash_lv() { >"${workdir}/rootfs_hash.roothash" cat "${workdir}/rootfs_hash.status" - # File mode: keep hash in workdir (will be included in initrd via --include) - log::info "Verity hash file stored at ${rootfs_hash_file_path}" + local rootfs_hash_size_in_byte + rootfs_hash_size_in_byte=$(stat --printf="%s" "${rootfs_hash_file_path}") + + log::info "Creating rootfs_hash logical volume (size: ${rootfs_hash_size_in_byte} bytes)" + lvcreate -n rootfs_hash --size "${rootfs_hash_size_in_byte}"B cryptpilot + dd status=progress "if=${rootfs_hash_file_path}" of=/dev/mapper/cryptpilot-rootfs_hash bs=4M + rm -f "${rootfs_hash_file_path}" + + log::info "Verity hash stored in logical volume cryptpilot/rootfs_hash" # Recording rootfs hash in metadata file log::info "Generate metadata file" @@ -1400,7 +1494,6 @@ step::setup_rootfs_hash_lv() { type = 1 root_hash = "${roothash}" EOF - } main() { @@ -1585,8 +1678,17 @@ main() { # Alias device to source_mod_device for backward compatibility with partition detection functions device="${source_mod_device}" + # Ensure kernel has partition devices ready for both NBD devices + partprobe "${source_mod_device}" + partprobe "${output_device}" + udevadm settle --timeout=10 + disk::assert_disk_not_busy "${device}" + # Debug: show what lsblk sees + log::info "lsblk output for source device:" + lsblk -lnpo NAME "${source_mod_device}" + disk::find_efi_partition "${device}" [ "${efi_part_exist}" = true ] || proc::fatal "Cannot find EFI partition on $device" efi_part="${device}p${efi_part_num}" @@ -1653,6 +1755,7 @@ main() { # 7. Setting up rootfs logical volume # log::step "[ 7 ] Setting up rootfs logical volume" + step::setup_lvm if [ "${rootfs_no_encryption}" = false ]; then step::setup_rootfs_lv_with_encrypt "${rootfs_passphrase}" else @@ -1688,10 +1791,54 @@ main() { # 11. Finalizing # log::step "[ 11 ] Finalizing" - qemu-nbd -d "${source_read_device}" - qemu-nbd -d "${source_write_device}" + + # Deactivate LVM volume group before disconnecting NBD + vgchange -an cryptpilot 2>/dev/null || true + sleep 1 + + # Flush pending writes + sync + + # Save EFI content to a temp file before NBD disconnect. + # NBD disconnect can lose FAT32 filesystem data written through mount+copy. + # We restore it afterwards using guestfish which writes directly to qcow2. + local efi_backup="${workdir}/efi_backup.tar" + local efi_backup_mount="${workdir}/efi_backup_mnt" + mkdir -p "$efi_backup_mount" + if mount "${output_device}p${efi_part_num}" "$efi_backup_mount" 2>/dev/null; then + tar cf "$efi_backup" -C "$efi_backup_mount" . + disk::umount_wait_busy "$efi_backup_mount" + rmdir "$efi_backup_mount" 2>/dev/null || true + fi + qemu-nbd -d "${output_device}" - sleep 2 + sleep 3 + + # Restore EFI partition using guestfish, which writes directly to the qcow2 + # file and handles sync properly on close, avoiding the NBD data loss issue. + if [ -f "$efi_backup" ]; then + local extract_dir="${workdir}/efi_extract" + mkdir -p "$extract_dir" + tar xf "$efi_backup" -C "$extract_dir" + + if guestfish -a "${output_file}" -- </dev/null || true + qemu-nbd -d "${source_write_device}" 2>/dev/null || true log::success "--------------------------------" log::success "Everything done, the new disk image is ready to use: ${output_file}" diff --git a/cryptpilot-core/src/fs/nbd.rs b/cryptpilot-core/src/fs/nbd.rs index fb4225b..5837881 100644 --- a/cryptpilot-core/src/fs/nbd.rs +++ b/cryptpilot-core/src/fs/nbd.rs @@ -71,6 +71,8 @@ impl NbdDevice { let nbd_dev_num = Self::get_avaliable().await?; let nbd_dev_path = nbd_dev_num.to_path(); + tracing::info!("Connecting disk image {disk_img:?} to NBD device {nbd_dev_path:?}"); + // The problem is that the nbd device may be use by the kernel (e.g. as mount point or as a device mapper) due to the annoying udev rules. Here we try to add a udev rule to ingore this device. let udev_rule = UdevRule::install_ignore_nbd_rule().await?; @@ -89,6 +91,10 @@ impl NbdDevice { tracing::debug!("Waiting 1 second for the nbd device to be ready"); tokio::time::sleep(std::time::Duration::from_secs(1)).await; + // Ensure partition device nodes are created before proceeding + let _ = Command::new("partprobe").arg(&nbd_dev_path).run().await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + let device = Self { nbd_dev_num, udev_rule, diff --git a/cryptpilot-fde/src/disk/external.rs b/cryptpilot-fde/src/disk/external.rs index 58e68e6..d4c4f9d 100644 --- a/cryptpilot-fde/src/disk/external.rs +++ b/cryptpilot-fde/src/disk/external.rs @@ -262,18 +262,32 @@ impl OnExternalFdeDisk { // Obtain all partitions under the device let lsblk_stdout = { let mut cmd = Command::new("lsblk"); - cmd.args(["-lnpo", "NAME"]); + cmd.args(["-lnpo", "NAME,TYPE,FSTYPE"]); cmd.arg(hint_device); cmd.run().await.context("Failed to list partitions")? }; let lsblk_str = String::from_utf8(lsblk_stdout)?; + // Filter for partition lines (first field NAME ends with a digit) let candidate_partitions = lsblk_str .lines() - .filter(|line| line.chars().last().map(|c| c.is_numeric()).unwrap_or(false)) - .map(PathBuf::from) + .filter_map(|line| { + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.is_empty() { + return None; + } + let name = fields[0]; + let last_char = name.chars().last()?; + if last_char.is_numeric() { + Some(PathBuf::from(name)) + } else { + None + } + }) .collect::>(); + tracing::debug!("Candidate EFI partitions: {:?}", candidate_partitions); + for part in candidate_partitions { let is_efi_part = async { // Create a temporary mount point From 297cf426cfc2b4332e92a3fb6566aaea0710ce5c Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Thu, 30 Apr 2026 19:14:57 +0800 Subject: [PATCH 7/7] fix(convert): simplify EFI handling and preserve partition UUIDs - Remove early mkfs.vfat + mount + cp for EFI partition in UKI mode; EFI is now dd'd in copy_partitions and restored via guestfish after NBD disconnect to avoid writeback-cache data loss. - Save partition UUIDs with blkid before parted rm/mkpart operations, restore them with sgdisk -u afterward to preserve GPT GUIDs. --- cryptpilot-convert.sh | 95 ++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 56 deletions(-) diff --git a/cryptpilot-convert.sh b/cryptpilot-convert.sh index 1fab59c..0b1bc6e 100755 --- a/cryptpilot-convert.sh +++ b/cryptpilot-convert.sh @@ -1238,6 +1238,13 @@ step::prepare_output_and_snapshots() { if [ "$uki" = true ]; then local uki_efi_size="512M" log::info "UKI mode: resizing EFI partition to ${uki_efi_size}" + + # Save partition UUIDs before parted modifies them. + # parted rm/mkpart generates new UUIDs, losing the originals. + local saved_efi_uuid saved_rootfs_uuid + saved_efi_uuid=$(blkid -s PART_UUID -o value "${output_device}p${efi_part_num}" 2>/dev/null || true) + saved_rootfs_uuid=$(blkid -s PART_UUID -o value "${output_device}p${rootfs_orig_part_num}" 2>/dev/null || true) + # Must delete rootfs partition first to avoid overlap, then resize EFI, # then recreate rootfs. All partition table modifications must complete # BEFORE formatting the EFI partition, because partprobe re-reading the @@ -1254,36 +1261,16 @@ step::prepare_output_and_snapshots() { log::info "Recreating rootfs partition from sector ${rootfs_new_start} to end" parted "${output_device}" --script -- mkpart primary ext4 "${rootfs_new_start}s" '100%' - # Now that all partition table modifications are done, format the EFI - # partition and copy EFI content from the source immediately. - # This must happen before any further parted operations that would - # trigger partprobe (which invalidates the kernel page cache). - local efi_part_size - efi_part_size=$(parted "${output_device}" --script -- unit B print | awk '/^ *'"${efi_part_num}"' / {gsub(/B$/,"",$4); print $4; exit}') - log::info "Formatting EFI partition with FAT32 (${efi_part_size} bytes)" - - # Run partprobe to make the new partition visible - partprobe "${output_device}" - udevadm settle --timeout=10 - - # Format the EFI partition - mkfs.vfat -F 32 "${output_device}p${efi_part_num}" >/dev/null - - # Immediately mount source EFI and output EFI, then copy content - local src_efi_mount="${workdir}/src_efi_prepare" - local dst_efi_mount="${workdir}/dst_efi_prepare" - mkdir -p "$src_efi_mount" "$dst_efi_mount" - mount -o ro "${source_read_device}p${efi_part_num}" "$src_efi_mount" - mount "${output_device}p${efi_part_num}" "$dst_efi_mount" - cp -a "$src_efi_mount/." "$dst_efi_mount/" - disk::umount_wait_busy "$dst_efi_mount" - disk::umount_wait_busy "$src_efi_mount" - rmdir "$src_efi_mount" "$dst_efi_mount" 2>/dev/null || true - - # Explicit sync to flush NBD page cache to the qcow2 file. - sync + # Restore original partition UUIDs + if [ -n "$saved_efi_uuid" ]; then + log::info "Restoring EFI partition UUID: $saved_efi_uuid" + sgdisk -u "${efi_part_num}:${saved_efi_uuid}" "${output_device}" >/dev/null 2>&1 || true + fi + if [ -n "$saved_rootfs_uuid" ]; then + log::info "Restoring rootfs partition UUID: $saved_rootfs_uuid" + sgdisk -u "${rootfs_orig_part_num}:${saved_rootfs_uuid}" "${output_device}" >/dev/null 2>&1 || true + fi - log::info "EFI partition formatted and content copied" fi # If source had no separate boot partition, we need to create one on output @@ -1292,6 +1279,11 @@ step::prepare_output_and_snapshots() { # Delete the rootfs partition first to free up space for the new boot partition. # The data is safe on source-read — we only modify the output partition table. log::info "Deleting rootfs partition on output to make room for boot partition" + + # Save rootfs partition UUID before parted deletes it. + local saved_rootfs_uuid + saved_rootfs_uuid=$(blkid -s PART_UUID -o value "${output_device}p${rootfs_orig_part_num}" 2>/dev/null || true) + parted "${output_device}" --script -- rm "${rootfs_orig_part_num}" partprobe "${output_device}" udevadm settle --timeout=10 @@ -1333,6 +1325,12 @@ step::prepare_output_and_snapshots() { # Set boot flag parted "${output_device}" --script -- set "${boot_part_num}" boot on + # Restore original rootfs partition UUID + if [ -n "$saved_rootfs_uuid" ]; then + log::info "Restoring rootfs partition UUID: $saved_rootfs_uuid" + sgdisk -u "${rootfs_orig_part_num}:${saved_rootfs_uuid}" "${output_device}" >/dev/null 2>&1 || true + fi + # Format the newly created boot partition with ext4 log::info "Formatting output boot partition" mkfs.ext4 -F "${output_device}p${boot_part_num}" >/dev/null 2>&1 @@ -1341,12 +1339,8 @@ step::prepare_output_and_snapshots() { # Track output boot partition number separately from the original source detection. # boot_part_exist reflects whether the SOURCE had a boot partition. else - # In UKI mode, EFI was already formatted and populated above; - # skip partprobe to avoid invalidating its page cache. - if [ "$uki" = false ]; then - partprobe "${output_device}" - udevadm settle --timeout=10 - fi + partprobe "${output_device}" + udevadm settle --timeout=10 fi log::info "source-read device: ${source_read_device}" @@ -1357,14 +1351,9 @@ step::prepare_output_and_snapshots() { step::copy_partitions() { - # Copy EFI partition - # In UKI mode, the EFI partition was already formatted and populated in - # step::prepare_output_and_snapshots. - if [ "$uki" = false ]; then - # dd EFI partition (preserve UUID, labels, all metadata) - log::info "Copying EFI partition" - dd if="${source_read_device}p${efi_part_num}" of="${output_device}p${efi_part_num}" bs=4M status=progress - fi + # dd EFI partition (preserve UUID, labels, all metadata) + log::info "Copying EFI partition" + dd if="${source_read_device}p${efi_part_num}" of="${output_device}p${efi_part_num}" bs=4M status=progress # Populate the output boot partition. # Two cases: @@ -1796,12 +1785,8 @@ main() { vgchange -an cryptpilot 2>/dev/null || true sleep 1 - # Flush pending writes - sync - - # Save EFI content to a temp file before NBD disconnect. - # NBD disconnect can lose FAT32 filesystem data written through mount+copy. - # We restore it afterwards using guestfish which writes directly to qcow2. + # Back up EFI content before NBD disconnect — dracut may have written + # BOOTX64.EFI (UKI mode) which must be preserved. local efi_backup="${workdir}/efi_backup.tar" local efi_backup_mount="${workdir}/efi_backup_mnt" mkdir -p "$efi_backup_mount" @@ -1811,11 +1796,12 @@ main() { rmdir "$efi_backup_mount" 2>/dev/null || true fi - qemu-nbd -d "${output_device}" - sleep 3 + qemu-nbd -d "${output_device}" 2>/dev/null || true + qemu-nbd -d "${source_read_device}" 2>/dev/null || true + qemu-nbd -d "${source_write_device}" 2>/dev/null || true - # Restore EFI partition using guestfish, which writes directly to the qcow2 - # file and handles sync properly on close, avoiding the NBD data loss issue. + # Restore EFI partition using guestfish, which writes directly to qcow2 + # and avoids the NBD writeback-cache data-loss issue on disconnect. if [ -f "$efi_backup" ]; then local extract_dir="${workdir}/efi_extract" mkdir -p "$extract_dir" @@ -1837,9 +1823,6 @@ GUESTFISH_EOF rm -rf "$extract_dir" fi - qemu-nbd -d "${source_read_device}" 2>/dev/null || true - qemu-nbd -d "${source_write_device}" 2>/dev/null || true - log::success "--------------------------------" log::success "Everything done, the new disk image is ready to use: ${output_file}"