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
205 changes: 205 additions & 0 deletions cryptpilot-convert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,205 @@ if [[ -f /tmp/cryptpilot/global.toml ]]; then
dracut_common_args+=(--include /tmp/cryptpilot/global.toml /etc/cryptpilot/global.toml)
fi

# --- UKI section layout ------------------------------------------------------
#
# dracut 059 and older hardcode absolute section VMAs when they assemble a UKI
# (.osrel=0x20000, .cmdline=0x30000, .linux=0x2000000, .initrd=0x3000000).
# Those values assume the stub's ImageBase is 0. systemd >= v254 builds
# linuxx64.efi.stub with lld, whose ImageBase is large (e.g. 0x4ff70000), and
# objcopy writes RVA = VMA - ImageBase. The subtraction underflows and wraps
# modulo 2^32: 0x2000000 - 0x4ff70000 == 0xb2090000. The stub's own sections
# stay below RVA 0x1d000 while .linux/.initrd land ~2.8 GiB up, so SizeOfImage
# grows to ~2.93 GiB for ~0.12 GiB of real data. Firmware maps that whole range
# page by page (slow boot, memory pressure), and stricter firmware refuses the
# image with "Load error". Disk usage and the PCR4/PCR11 values are unaffected,
# which is what makes the bug easy to miss.
#
# --uki-stub-version pins a recent stub from the Arch archive, so on an image
# with an old dracut this combination is hit every time. Neither upstream way
# out works here: upgrading dracut or installing ukify means installing
# packages into the rootfs, and the conversion has to leave the rootfs
# byte-for-byte unchanged (that is why even the pinned stub is downloaded, used
# and removed again). So reassemble the UKI here: derive the first free VMA from
# the stub's own section table and re-add the payload sections at aligned
# absolute VMAs (>= ImageBase). This mirrors the dynamic offset algorithm that
# upstream dracut-ng only gained in 060; dracut 059 and older hardcode the
# offsets and produce the hole. See dracut#2431 and systemd#28419.
#
# As a bonus the layout then depends only on the stub, not on the dracut
# version inside the image, which is the layout stability --uki-stub-version is
# after for its PCR reference values.
#
# Known limitations of this implementation:
# - Sections are matched by name, which assumes every name is unique. dracut
# on x86_64 never emits the repeatable .dtbauto/.efifw sections, so this
# holds today.
# - A stub-owned .sbat is treated as replaced by dracut based on its size
# alone.
# - x86_64 only (linuxx64 stub, BOOTX64.EFI), like the rest of this script.

# Print one field of the PE optional header, hex, as objdump -p spells it.
pe_header_field() {
objdump -p "$1" 2>/dev/null | awk -v key="$2" '$1 == key { print $2; exit }'
}

# Print "<name> <size-hex> <vma-hex>" for every section, in section table order.
pe_sections() {
objdump -h "$1" 2>/dev/null | awk '$1 ~ /^[0-9]+$/ && NF == 7 { print $2, $3, $4 }'
}

# Size of one section, hex, empty if the section is absent.
pe_section_size() {
pe_sections "$1" | awk -v name="$2" '$1 == name { print $2; exit }'
}

# Round $1 up to the next multiple of $2, the way dracut does it (an already
# aligned value still gets one full alignment of slack).
pe_align_up() {
echo $(($1 + $2 - $1 % $2))
}

# Rebuild $2 (a dracut-generated UKI) on top of a pristine copy of the stub $1,
# placing the payload sections right after the stub's own ones.
uki_reassemble() {
local stub=$1
local uki=$2
local align image_base offs=0 end
local name size vma s
local stub_sections uki_sections payload="" replaced=""
local dumpdir build_stub dump_args=() add_args=()
local objcopy_help
local size_of_image file_size

if ! command -v objdump > /dev/null 2>&1; then
echo "ERROR: objdump is needed to lay out the UKI, install binutils" >&2
return 1
fi

align=$(pe_header_field "$stub" SectionAlignment)
image_base=$(pe_header_field "$stub" ImageBase)
if [ -z "$align" ] || [ -z "$image_base" ]; then
echo "ERROR: cannot read the PE header of the efi stub $stub" >&2
return 1
fi
align=$((16#$align))
image_base=$((16#$image_base))
if [ "$align" -le 0 ]; then
echo "ERROR: bogus SectionAlignment in the efi stub $stub" >&2
return 1
fi

# First free VMA behind the stub's own sections. objdump reports absolute
# VMAs, so every offset derived from it stays >= ImageBase and objcopy's
# RVA = VMA - ImageBase can no longer underflow.
while read -r name size vma; do
end=$((16#$size + 16#$vma))
if [ "$end" -gt "$offs" ]; then
offs=$end
fi
done < <(pe_sections "$stub")
if [ "$offs" -lt "$image_base" ]; then
offs=$image_base
fi
offs=$(pe_align_up "$offs" "$align")

# Sections to carry over from the generated UKI, in a fixed order so the
# result only depends on the stub. A section the stub already owns is only
# carried over if dracut replaced it (newer dracut merges .sbat that way).
stub_sections=" $(pe_sections "$stub" | awk '{ print $1 }' | tr '\n' ' ')"
uki_sections=" $(pe_sections "$uki" | awk '{ print $1 }' | tr '\n' ' ')"
for s in .osrel .cmdline .uname .splash .dtb .sbat .linux .initrd; do
case "$uki_sections" in
*" $s "*) ;;
*) continue ;;
esac
case "$stub_sections" in
*" $s "*)
if [ "$(pe_section_size "$uki" "$s")" = "$(pe_section_size "$stub" "$s")" ]; then
continue
fi
replaced="${replaced} ${s}"
;;
esac
payload="${payload} ${s}"
done
# Anything else dracut added goes last, so nothing is silently dropped.
for s in $uki_sections; do
case "$stub_sections" in
*" $s "*) continue ;;
esac
case " ${payload} " in
*" $s "*) continue ;;
esac
echo "WARNING: unexpected section $s in the generated UKI, appending it last" >&2
payload="${payload} ${s}"
done
if [ -z "$payload" ]; then
echo "ERROR: the generated UKI carries no payload section on top of $stub" >&2
return 1
fi

dumpdir=$(mktemp -d /tmp/cryptpilot-uki-XXXXXX)
for s in $payload; do
dump_args+=(--dump-section "${s}=${dumpdir}/${s}")
done
# In place, so objcopy's scratch file stays in /tmp next to the UKI.
if ! objcopy "${dump_args[@]}" "$uki"; then
rm -rf "$dumpdir"
echo "ERROR: failed to dump the payload sections of the generated UKI" >&2
return 1
fi

for s in $payload; do
size=$(stat -Lc%s "${dumpdir}/${s}")
if [ "$size" -le 0 ]; then
rm -rf "$dumpdir"
echo "ERROR: section $s of the generated UKI is empty" >&2
return 1
fi
add_args+=(--add-section "${s}=${dumpdir}/${s}" --change-section-vma "${s}=$(printf '0x%x' "$offs")")
offs=$(pe_align_up $((offs + size)) "$align")
done
# Keep the output ImageBase identical to the stub's, otherwise the RVAs
# would be computed against a different base again. Only PE-aware binutils
# know this option.
objcopy_help=$(objcopy --help 2>/dev/null || true)
case "$objcopy_help" in
*--image-base*) add_args+=(--image-base="$(printf '0x%x' "$image_base")") ;;
esac

build_stub="${dumpdir}/stub.efi"
cp "$stub" "$build_stub"
for s in $replaced; do
# Separate pass: removing and adding the same section name in one
# objcopy run is ambiguous.
if ! objcopy --remove-section "$s" "$build_stub"; then
rm -rf "$dumpdir"
echo "ERROR: failed to drop section $s from the efi stub copy" >&2
return 1
fi
done

if ! objcopy "${add_args[@]}" "$build_stub" "${uki}.relayout"; then
rm -rf "$dumpdir" "${uki}.relayout"
echo "ERROR: failed to reassemble the UKI from the efi stub" >&2
return 1
fi
mv "${uki}.relayout" "$uki"
rm -rf "$dumpdir"

size_of_image=$(pe_header_field "$uki" SizeOfImage)
size_of_image=$((16#${size_of_image:-0}))
file_size=$(stat -Lc%s "$uki")
echo "UKI sections:${payload}, SizeOfImage=${size_of_image} bytes, file size=${file_size} bytes"
# A sane UKI maps just a bit more than it stores. A big gap means the RVAs
# wrapped again, and such an image boots slowly or not at all.
if [ "$size_of_image" -gt $((file_size + 64 * 1024 * 1024)) ]; then
echo "ERROR: the UKI still has a $(((size_of_image - file_size) / 1024 / 1024)) MiB hole (SizeOfImage=${size_of_image}, file size=${file_size})" >&2
return 1
fi
}

if [ "${uki:-false}" = true ]; then
# dracut --uefi needs the systemd UEFI stub (linuxx64.efi.stub) at
# /usr/lib/systemd/boot/efi/ to assemble a UKI. Two sourcing modes,
Expand Down Expand Up @@ -1283,6 +1482,12 @@ if [ "${uki:-false}" = true ]; then
FINAL_UKI_FILE="/boot/efi/EFI/BOOT/BOOTX64.EFI"
dracut "${dracut_args[@]}" "$TMP_UKI_FILE"

# dracut may have scattered the payload sections far above the stub, see the
# comment at uki_reassemble. Rebuild the layout from the stub itself before
# touching anything else, so the following passes work on a compact image.
echo "Fixing UKI section layout"
uki_reassemble /usr/lib/systemd/boot/efi/linuxx64.efi.stub "$TMP_UKI_FILE"

echo "Patching cmdline in UKI"
# The generated cmdline will have a leading space, remove it
objcopy --dump-section .cmdline="/tmp/cmdline_full.bin" "$TMP_UKI_FILE"
Expand Down
2 changes: 2 additions & 0 deletions cryptpilot-fde/docs/boot.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ UKI generation uses dracut's `--uefi` parameter. The default kernel command line
- `--uki-stub-version distro` (default): installs `systemd-boot-unsigned` from the distro's own repo. The version floats with the distro, so PCR reference values are only stable as long as the distro package does not change.
- `--uki-stub-version <version>`: downloads a pinned, unsigned stub from the public [Arch Linux Archive](https://archive.archlinux.org/packages/s/systemd/). The value is a version prefix resolved to the highest matching package — `261` selects the latest `261.x`, `261.2-1` selects that exact package (fully pinned). The stub is downloaded, used to build the UKI, then removed, so the converted image is left unchanged. The resolved exact version and stub SHA-256 are logged, which lets you fix an exact value after verifying once.

**Section layout**: after dracut has assembled the UKI, the conversion script rebuilds it, placing `.osrel`/`.cmdline`/`.linux`/`.initrd` directly behind the stub's own sections at aligned absolute VMAs. dracut 059 and older hardcode VMAs that assume the stub's `ImageBase` is 0 (upstream dracut-ng computes them dynamically since 060). Stubs from systemd v254 and later have a large `ImageBase`, so those VMAs wrap around and push `.linux`/`.initrd` about 2.8 GiB up: `SizeOfImage` grows to ~2.93 GiB for ~0.12 GiB of data, which slows boot down and makes stricter firmware fail with `Load error` (see dracut#2431 and systemd#28419). Rebuilding avoids that without touching the rootfs, and makes the layout a function of the pinned stub alone instead of the dracut version inside the image. The final `SizeOfImage` and file size are logged, and the conversion aborts if a hole remains.

### 3.3 Mode Comparison

| Feature | GRUB Mode | UKI Mode |
Expand Down
2 changes: 2 additions & 0 deletions cryptpilot-fde/docs/boot_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ UKI生成使用dracut的`--uefi`参数,默认内核命令行为`console=tty0 c
- `--uki-stub-version distro`(默认):从发行版自身仓库安装 `systemd-boot-unsigned`。版本随发行版浮动,因此 PCR 参考值仅在发行版包不变时才稳定。
- `--uki-stub-version <version>`:从公开的 [Arch Linux Archive](https://archive.archlinux.org/packages/s/systemd/) 下载一个钉死的 unsigned stub。取值为版本前缀,解析为最高匹配的包——`261` 选最新 `261.x`,`261.2-1` 选该精确包(完全钉死)。stub 下载后用于构建 UKI,随后删除,故转换后的镜像保持不变。解析到的精确版本与 stub 的 SHA-256 会被记录到日志,便于你验证一次后把精确值固化下来。

**节区布局**:dracut 组装出 UKI 后,转换脚本会重新组装一遍,把 `.osrel`/`.cmdline`/`.linux`/`.initrd` 按对齐后的绝对 VMA 紧挨在 stub 自身节区之后。059 及更早版本的 dracut 硬编码 VMA,且假设 stub 的 `ImageBase` 为 0(上游 dracut-ng 自 060 起改为动态计算);而 systemd v254 及以后的 stub 的 `ImageBase` 很大,这些 VMA 会回卷,把 `.linux`/`.initrd` 推到约 2.8 GiB 处:真实数据约 0.12 GiB,`SizeOfImage` 却涨到约 2.93 GiB,导致启动变慢,严格的固件还会直接报 `Load error`(见 dracut#2431 与 systemd#28419)。重新组装无需改动 rootfs 即可绕开该问题,并使布局只取决于钉死的 stub,而不再取决于镜像内的 dracut 版本。最终的 `SizeOfImage` 与文件大小会记录到日志,若仍存在空洞则转换直接失败。

### 3.3 模式对比

| 特性 | GRUB模式 | UKI模式 |
Expand Down
Loading