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
33 changes: 30 additions & 3 deletions .github/workflows/build-rpm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,29 @@ jobs:
- name: alinux4
test_image_tag: "alinux4-20260710"
runtime_image: "alibaba-cloud-linux-4-registry.cn-hangzhou.cr.aliyuncs.com/alinux4/alinux4:latest"
# Pinned-stub 258 + RAM matrix on the uki/noenc/disk combo, per distro.
# The 2:4G entry surfaces the SizeOfImage hole on a real pinned stub;
# the reassembly fix should make it boot. Baseline rows carry neither
# var, so the Makefile emits neither flag there (single boot, distro stub).
include:
- bootloader: uki
rootfs_enc: noenc
delta_location: disk
distro:
name: alinux3
test_image_tag: "alinux3-20251030"
runtime_image: "alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/alinux3:latest"
uki_stub_version: "258"
boot_matrix: "2:4G 4:8G 4:16G"
- bootloader: uki
rootfs_enc: noenc
delta_location: disk
distro:
name: alinux4
test_image_tag: "alinux4-20260710"
runtime_image: "alibaba-cloud-linux-4-registry.cn-hangzhou.cr.aliyuncs.com/alinux4/alinux4:latest"
uki_stub_version: "258"
boot_matrix: "2:4G 4:8G 4:16G"
runs-on: ubuntu-latest
needs: build
env:
Expand Down Expand Up @@ -252,7 +275,7 @@ jobs:
set -e
sed -i -E 's|https?://mirrors.cloud.aliyuncs.com/|https://mirrors.aliyun.com/|g' /etc/yum.repos.d/*.repo
yum update -y
yum install -y git make
yum install -y git make zstd
"

- name: Download artifacts
Expand Down Expand Up @@ -288,9 +311,13 @@ jobs:
path: repo

- name: Run convert test
env:
# Empty for baseline rows; make imports these from the environment.
BOOT_MATRIX: ${{ matrix.boot_matrix }}
UKI_STUB_VERSION: ${{ matrix.uki_stub_version }}
run: |
docker exec -w /workspace/repo test-container bash -c "
make run-convert-test-case BOOTLOADER=${{ matrix.bootloader }} ROOTFS_ENC=${{ matrix.rootfs_enc }} DELTA_LOCATION=${{ matrix.delta_location }} INPUT_IMAGE=/workspace/test-images/test-image.qcow2 CRYPTPILOT_FDE_RPM=${{ steps.install-rpm.outputs.cryptpilot_fde_rpm_container }} QEMU_RAM=4G
docker exec -w /workspace/repo --env BOOT_MATRIX --env UKI_STUB_VERSION test-container bash -c "
make run-convert-test-case BOOTLOADER=${{ matrix.bootloader }} ROOTFS_ENC=${{ matrix.rootfs_enc }} DELTA_LOCATION=${{ matrix.delta_location }} INPUT_IMAGE=/workspace/test-images/test-image.qcow2 CRYPTPILOT_FDE_RPM=${{ steps.install-rpm.outputs.cryptpilot_fde_rpm_container }}
"

- name: Print convert diagnostic log
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ install-convert-test-depend:

.PHONY: run-convert-test-case
run-convert-test-case: install-convert-test-depend
bash tests/test-convert.sh --rpm $(CRYPTPILOT_FDE_RPM) --bootloader $(BOOTLOADER) --rootfs-$(ROOTFS_ENC) --delta-location $(DELTA_LOCATION) $(if $(INPUT_IMAGE),--input $(INPUT_IMAGE),) $(if $(QEMU_RAM),--ram $(QEMU_RAM),)
bash tests/test-convert.sh --rpm $(CRYPTPILOT_FDE_RPM) --bootloader $(BOOTLOADER) --rootfs-$(ROOTFS_ENC) --delta-location $(DELTA_LOCATION) $(if $(INPUT_IMAGE),--input $(INPUT_IMAGE),) $(if $(BOOT_MATRIX),--boot-matrix "$(BOOT_MATRIX)",) $(if $(UKI_STUB_VERSION),--uki-stub-version $(UKI_STUB_VERSION),)

.PHONE: shellcheck
shellcheck:
Expand Down
149 changes: 139 additions & 10 deletions cryptpilot-convert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,93 @@ pe_align_up() {
echo $(($1 + $2 - $1 % $2))
}

# Patch a Linux kernel bzImage's PE/COFF OptionalHeader.ImageBase to 0, in
# place. systemd's UKI stub (linuxx64.efi.stub) >= 258 and < 260 computes each
# inner kernel section's load offset as (section.VirtualAddress - ImageBase)
# and rejects the image when that underflows, printing
# "Section would write outside of memory" (systemd src/boot/linux.c, issue
# #40342). Kernels v5.7..v6.6 are built with ImageBase = CONFIG_PHYSICAL_START
# (default 0x1000000) while their section RVAs (~0x4000) are below it, so a
# pinned 258/259 stub cannot boot them (e.g. alinux3 5.10); systemd fixed it in
# >= 260 (PR #40429) and mainline kernel reverted to ImageBase=0 in v6.7.
#
# Setting ImageBase to 0 makes the buggy stub's math yield the correct RVA. It
# is safe per the PE spec: ImageBase is the *preferred* load address, not used
# by the position-independent kernel at runtime (the stub loads it into its own
# base-0 buffer regardless). Caveat: changes the kernel hash -> re-sign and
# re-measure. No-op (returns 0) if the file is not a valid PE kernel or
# ImageBase is already 0; returns 1 only if it looks like a PE but cannot be
# patched (truncated headers, unknown optional-header magic, write/verify
# failure).
patch_kernel_image_base_zero() {
local file=$1
[ -n "${file:-}" ] || { echo 'ERROR: patch_kernel_image_base_zero: no file given' >&2; return 1; }
[ -f "$file" ] || { echo "ERROR: kernel file not found: $file" >&2; return 1; }
command -v python3 >/dev/null 2>&1 || { echo "ERROR: python3 required to patch kernel ImageBase; cannot patch ${file##*/}" >&2; return 1; }

python3 - "$file" <<'PYEOF'
import sys, struct, os
path = sys.argv[1]
name = os.path.basename(path)

def note(m): print('NOTE: %s: %s' % (name, m))
def err(m): print('ERROR: %s: %s' % (name, m), file=sys.stderr)

try:
size = os.path.getsize(path)
if size < 0x40:
note('too small to be a PE kernel; skip ImageBase patch')
sys.exit(0)
with open(path, 'r+b') as f:
head = f.read(min(size, 4096))
if head[0:2] != b'MZ':
note('no MZ DOS magic; skip ImageBase patch')
sys.exit(0)
if len(head) < 0x40:
err('truncated DOS header; cannot patch ImageBase')
sys.exit(1)
e_lfanew = struct.unpack_from('<I', head, 0x3c)[0]
if e_lfanew + 24 > len(head) or head[e_lfanew:e_lfanew+4] != b'PE\x00\x00':
err('bad PE signature; cannot patch ImageBase')
sys.exit(1)
opt_off = e_lfanew + 4 + 20
if opt_off + 28 > len(head):
err('truncated PE optional header; cannot patch ImageBase')
sys.exit(1)
magic = struct.unpack_from('<H', head, opt_off)[0]
if magic == 0x20b:
ib_off, fmt, w = opt_off + 24, '<Q', 8
elif magic == 0x10b:
ib_off, fmt, w = opt_off + 28, '<I', 4
else:
err('unknown PE optional-header magic 0x%x; cannot patch ImageBase' % magic)
sys.exit(1)
if ib_off + w > len(head):
err('truncated PE headers; cannot patch ImageBase')
sys.exit(1)
cur = struct.unpack_from(fmt, head, ib_off)[0]
if cur == 0:
note('ImageBase already 0; no patch needed')
sys.exit(0)
f.seek(ib_off)
f.write(struct.pack(fmt, 0))
f.flush()
os.fsync(f.fileno())
f.seek(ib_off)
chk = struct.unpack_from(fmt, f.read(w), 0)[0]
if chk != 0:
err('ImageBase patch verify failed (still 0x%x)' % chk)
sys.exit(1)
print('PATCHED %s: PE ImageBase 0x%x -> 0x0 (file offset 0x%x, %d bytes)' % (name, cur, ib_off, w))
sys.exit(0)
except SystemExit:
raise
except Exception as e:
err('ImageBase patch failed: %s' % e)
sys.exit(1)
PYEOF
}

# 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() {
Expand All @@ -1305,14 +1392,18 @@ uki_reassemble() {
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

# Release the scratch dir and any half-written relayout on every exit path
# (including set -e aborts from the cp/mv/objcopy below), so a failure
# never leaks /tmp/cryptpilot-uki-XXXXXX across runs.
trap '[ -n "$dumpdir" ] && rm -rf "$dumpdir" "${uki}.relayout"' RETURN

align=$(pe_header_field "$stub" SectionAlignment)
image_base=$(pe_header_field "$stub" ImageBase)
if [ -z "$align" ] || [ -z "$image_base" ]; then
Expand Down Expand Up @@ -1353,6 +1444,11 @@ uki_reassemble() {
case "$stub_sections" in
*" $s "*)
if [ "$(pe_section_size "$uki" "$s")" = "$(pe_section_size "$stub" "$s")" ]; then
# SBAT is a revocation list; flag it so operators can audit
# which version is kept when both sides carry the same size.
if [ "$s" = ".sbat" ]; then
echo "NOTE: stub and dracut both ship .sbat with the same size; keeping the stub's, dropping dracut's" >&2
fi
continue
fi
replaced="${replaced} ${s}"
Expand Down Expand Up @@ -1397,16 +1493,18 @@ uki_reassemble() {
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
# The final objcopy copies build_stub (a byte copy of the stub) to the
# output, so it inherits the stub's ImageBase from the PE optional header.
# Pinning --image-base to the same value is a no-op (verified on binutils
# 2.35: outputs are byte-identical with and without it), and on binutils
# that lack the flag it cannot be passed at all. Rely on preservation; the
# post-reassembly SizeOfImage sanity check below catches any regression.

build_stub="${dumpdir}/stub.efi"
cp "$stub" "$build_stub"
if ! cp "$stub" "$build_stub"; then
echo "ERROR: failed to copy the efi stub $stub to $build_stub" >&2
return 1
fi
for s in $replaced; do
# Separate pass: removing and adding the same section name in one
# objcopy run is ambiguous.
Expand All @@ -1422,7 +1520,10 @@ uki_reassemble() {
echo "ERROR: failed to reassemble the UKI from the efi stub" >&2
return 1
fi
mv "${uki}.relayout" "$uki"
if ! mv "${uki}.relayout" "$uki"; then
echo "ERROR: failed to move the relayouted UKI ${uki}.relayout into place" >&2
return 1
fi
rm -rf "$dumpdir"

size_of_image=$(pe_header_field "$uki" SizeOfImage)
Expand All @@ -1438,6 +1539,14 @@ uki_reassemble() {
}

if [ "${uki:-false}" = true ]; then
# objcopy/objdump are needed to patch the cmdline and to fix the section
# layout below. Fail before dracut spends minutes building the UKI.
for tool in objcopy objdump; do
if ! command -v "$tool" > /dev/null 2>&1; then
echo "ERROR: $tool is needed to build the UKI, install binutils" >&2
exit 1
fi
done
# dracut --uefi needs the systemd UEFI stub (linuxx64.efi.stub) at
# /usr/lib/systemd/boot/efi/ to assemble a UKI. Two sourcing modes,
# selected by uki_stub_version (the literal "distro" sentinel must match
Expand Down Expand Up @@ -1488,6 +1597,26 @@ if [ "${uki:-false}" = true ]; then
echo "Fixing UKI section layout"
uki_reassemble /usr/lib/systemd/boot/efi/linuxx64.efi.stub "$TMP_UKI_FILE"

# Workaround for systemd stub 258/259 + kernels whose PE ImageBase is
# non-zero (v5.7..v6.6, e.g. alinux3 5.10): the stub rejects them with
# "Section would write outside of memory" (systemd #40342, fixed in >= 260
# by PR #40429). Patch ONLY the kernel embedded in the UKI's .linux section
# to ImageBase=0 (objcopy dump/update-section, mirroring the cmdline patch
# below); /boot/vmlinuz is never touched. No-op if ImageBase is already 0
# (>= v6.7 kernels / alinux4). Only for pinned stubs in the buggy range.
if [ "${uki:-false}" = true ] && [ "${uki_stub_version:-distro}" != "distro" ]; then
_stub_major=""
case "${uki_stub_version}" in
[0-9]*) _stub_major=${uki_stub_version%%[!0-9]*} ;;
esac
if [ -n "${_stub_major}" ] && [ "${_stub_major}" -lt 260 ]; then
objcopy --dump-section .linux="/tmp/.linux.bin" "$TMP_UKI_FILE"
patch_kernel_image_base_zero /tmp/.linux.bin || \
echo "WARNING: kernel ImageBase patch failed (boot may fail with 'Load Error' on stub < 260)" >&2
objcopy --update-section .linux="/tmp/.linux.bin" "$TMP_UKI_FILE"
fi
fi

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
Loading
Loading