diff options
author | Helmut Grohne <helmut@subdivi.de> | 2025-04-06 07:59:25 +0200 |
---|---|---|
committer | Helmut Grohne <helmut@subdivi.de> | 2025-04-06 17:44:08 +0200 |
commit | 6f2a356ca10ab97ed257097996593d707eca9bd7 (patch) | |
tree | 4ba99a0bfa2c9f5967b47c17ab4e819756a0f787 /bin | |
parent | 1c6688cb9aeffdf2027003423d0ab792045952a8 (diff) | |
download | debvm-6f2a356ca10ab97ed257097996593d707eca9bd7.tar.gz |
add a new family of wrappers for EFI based images
debefivm-create is based on mmdebstrap-autopkgtest-build-qemu, which is
is co-authored with Johannes Schauer Marin Rodrigues. Also thanks to
Jochen Sprickerhof for suggesting the --rootsize option for use in
Debusine.
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/debefivm-create | 498 | ||||
-rwxr-xr-x | bin/debefivm-run | 401 | ||||
-rwxr-xr-x | bin/debefivm-ukify | 329 |
3 files changed, 1228 insertions, 0 deletions
diff --git a/bin/debefivm-create b/bin/debefivm-create new file mode 100755 index 0000000..6efc204 --- /dev/null +++ b/bin/debefivm-create @@ -0,0 +1,498 @@ +#!/bin/sh +# Copyright 2023 Johannes Schauer Marin Rodrigues <josch@debian.org> +# SPDX-FileCopyrightText: 2023-2025 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT + +# shellcheck disable=SC2016 # Intentional quoting technique + +: <<'POD2MAN' +=head1 NAME + +debefivm-create - Create an EFI-bootable VM image for various Debian releases and architectures + +=head1 SYNOPSIS + +B<debefivm-create> [B<-h> I<hostname>] [B<-k> F<sshkey>] [B<-o> F<output>] [B<-r> I<release>] [B<-s> <task>] [B<-z> I<size>] [B<--> I<mmdebstrap options>] + +=head1 DESCRIPTION + +Create an EFI-bootable disk image image booting into Debian. +This essentially is a thin wrapper around B<mmdebstrap> much like B<debvm-create>. +The resulting image contains a GPT partition table with an EFI system partition. +A Unified Kernel Image is constructed from the installed kernel and initrd and placed into said partition. +As a result, such an image may be booted with a variety of virtual machine emulators such as incus, libvirt or VirtualBox. +It may also be booted using B<debefivm-run>. +No user account is created and root can login without specifying a password. + +=head1 OPTIONS + +=over 8 + +=item B<-a> I<architecture>, B<--architecture>=I<architecture> + +Set the architecture of the virtual machine. +This option is forwarded to B<mmdebstrap> and multiple architectures may be supplied. +The first architecture is used to assemble the Unified Kernel Image. +It defaults to the native architecture of the installation. + +=item B<-h> I<hostname>, B<--hostname>=I<hostname> + +Set the hostname of the virtual machine. +By default, the hostname is B<testvm>. + +=item B<-k> F<sshkey>, B<--sshkey>=F<sshkey> + +Install the given ssh public key file into the virtual machine image for the root user. +This option also causes the ssh server to be installed. +By default, no key or server is installed. +To connect to the vm, pass a port number to B<debefivm-run> with the B<-s> option. + +=item B<-o> F<output>, B<--output>=F<output> + +Specify the file name of the resulting virtual machine image. +By default, it is written to F<vm.img>. + +=item B<-r> I<release>, B<--release>=I<release> + +Use the given Debian release. +By default, B<unstable> is being used. +If you pass a complete F<sources.list> that includes release names to B<mmdebstrap>, you may pass an empty string here. + +=item B<-s> I<task>, B<--skip>=I<task> + +Skip a particular task or feature. +The option may be specified multiple times or list multiple tasks to be skipped by separating them with a comma. +By default, no tasks are skipped. +The following tasks may be skipped. + +=over 4 + +=item B<autologin> + +Skips adding a the customize-autologin.sh to B<mmdebstrap> that configures +automatic root login on a serial console and also parses the C<TERM> kernel +cmdline and passes it as C<TERM> to B<agetty>. + +=item B<kernel> + +skips installing a linux kernel image. +This can be useful to install a kernel without a package. +If a kernel is installed via B<mmdebstrap> option C<--include>, automtatic kernel installation is automatically skipped. + +=item B<packagelists> + +reduces the package lists inside the image. +The B<available> database for B<dpkg> is not created. +The package lists used by B<apt> are deleted. +This generally produces a smaller image, but you need to run B<apt update> before installing packages and B<dpkg --set-selections> does not work. + +=item B<systemdnetwork> + +skips installing B<libnss-resolve> as well as automatic network configuration via B<systemd-networkd>. + +=item B<usrmerge> + +By default B<debefivm-create> adds a hook to enable merged-/usr without the B<usrmerge> package given a sufficiently recent Debian release. +Without the hook, dependencies will pull the B<usrmerge> package as needed, which may result in a larger installation. + +=back + +=item B<--rootsize>=I<size> + +This option may be used as an alternative to B<--imagesize> to determine the size of the output image. +It is also composed of an integer with optional unit. +Unlike B<--imagesize> it determines the size of the root filesystem partition and the resulting image will be slightly larger than the size given here. + +=item B<-z> I<size>, B<--imagesize>=I<size> + +Specify the total image size as an integer and optional unit (example: 10K is 10*1024). +Units are K,M,G,T (powers of 1024). +The disk image contains an EFI system partition and an ext4 root filesystem. +The EFI partition has a static size of about 128MB and the remaining space will be allocated to the root filesystem. +The default is 10 GB. + +=item B<--> I<mmdebstrap options> + +All options beyond a double dash are passed to B<mmdebstrap> after the suite and target specification. +This can be used to provide additional hooks for image customization. +You can also request additional packages to be installed into the image using B<mmdebstrap>'s B<--include> option. +Any positional arguments passed here will be treated as mirror specifications by B<mmdebstrap>. +The B<--architecture> option should not be given here as B<debefivm-create> needs to know its value to construct a Unified Kernel Image. + +=back + +=head1 EXAMPLES + +Create an image suitable for use with B<autopkgtest-virt-qemu> B<--efi>. + + debefivm-create ... -- --include=linux-image-generic,libpam-systemd,passwd,python3 --hook-dir=/usr/share/mmdebstrap/hooks/useradd --customize-hook=/usr/share/autopkgtest/setup-commands/setup-testbed + +=head1 SEE ALSO + + debefivm-run(1) debefivm-ukify(1) debvm-create(1) mmdebstrap(1) + +=cut +POD2MAN + +set -eu + +DEBARCH=$(dpkg --print-architecture) +EFILABEL=efisys +FAT_SIZE_KIB=$((126*1024)) +IMAGE=vm.img +SKIP=, +SSHKEY= +SUITE=unstable +ROOTLABEL=rootfs +TOT_SIZE_KIB=$((10*1024*1024)) +VMNAME=testvm + +SHARE_DIR="${0%/*}/../share" +BIN_DIR="${0%/*}/../bin/" + +# Disk layout: +# * GPT partition table in the front. Oversize to align. +GPT_FRONT_SIZE_KIB=1024 +# * FAT partition containing the EFI boot loader. Size configured. +FAT_OFFSET_KIB=$GPT_FRONT_SIZE_KIB +# * Main ext4 root partition. Compute as remaining size. +EXT4_OFFSET_KIB=$((FAT_OFFSET_KIB + FAT_SIZE_KIB)) +# EXT4_SIZE_KIB and TOT_SIZE_KIB depend on one another. +# * GPT backup table in the back. Oversize to align. +GPT_BACK_SIZE_KIB=1024 + +NON_EXT4_SIZE_KIB=$((EXT4_OFFSET_KIB + GPT_BACK_SIZE_KIB)) +EXT4_SIZE_KIB=$((TOT_SIZE_KIB - NON_EXT4_SIZE_KIB)) + +nth_arg() { + shift "$1" + printf "%s" "$1" +} + +die() { + echo "$*" >&2 + exit 1 +} +usage() { + die "usage: $0 [-a architecture] [-h hostname] [-k sskey] [-o output] [-r release] [-s task] [-z imagesize] [-- mmdebstrap options]" +} +usage_error() { + echo "error: $*" 1>&2 + usage +} + +opt_architecture() { + DEBARCH="$1" +} +opt_hostname() { + VMNAME=$1 +} +opt_output() { + IMAGE=$1 + if test "${IMAGE#-}" != "$IMAGE"; then + IMAGE="./$IMAGE" + fi +} +opt_release() { + SUITE="$1" +} +opt_rootsize() { + EXT4_SIZE_KIB="$(numfmt --to=none --to-unit=Ki --from=iec "$1")" || + die "failed to parse size '$1'" + TOT_SIZE_KIB=$((EXT4_SIZE_KIB + NON_EXT4_SIZE_KIB)) +} +opt_skip() { + SKIP="$SKIP$1," +} +opt_sshkey() { + SSHKEY=$1 +} +opt_imagesize() { + TOT_SIZE_KIB="$(numfmt --to=none --to-unit=Ki --from=iec "$1")" || + die "failed to parse size '$1'" + EXT4_SIZE_KIB=$((TOT_SIZE_KIB - NON_EXT4_SIZE_KIB)) +} + +while getopts :a:h:k:o:r:s:z:-: OPTCHAR; do + case "$OPTCHAR" in + a) opt_architecture "$OPTARG" ;; + h) opt_hostname "$OPTARG" ;; + k) opt_sshkey "$OPTARG" ;; + o) opt_output "$OPTARG" ;; + r) opt_release "$OPTARG" ;; + s) opt_skip "$OPTARG" ;; + z) opt_imagesize "$OPTARG" ;; + -) + case "$OPTARG" in + help) + usage + ;; + architecture|hostname|imagesize|output|release|rootsize|skip|sshkey) + test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG" + "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")" + OPTIND=$((OPTIND+1)) + ;; + architecture=*|hostname=*|imagesize=*|output=*|release=*|rootsize=*|size=*|skip=*|sshkey=*) + "opt_${OPTARG%%=*}" "${OPTARG#*=}" + ;; + *) + usage_error "unrecognized option --$OPTARG" + ;; + esac + ;; + :) + usage_error "missing argument for -$OPTARG" + ;; + '?') + usage_error "unrecognized option -$OPTARG" + ;; + *) + die "internal error while parsing command options, please report a bug" + ;; + esac +done +shift "$((OPTIND - 1))" + +if test -n "$SSHKEY" && ! test -f "$SSHKEY"; then + die "error: ssh keyfile '$SSHKEY' not found" +fi + +check_skip() { + case "$SKIP" in + *",$1,"*) return 0 ;; + *) return 1 ;; + esac +} + +test "$EXT4_SIZE_KIB" -lt "$((1024*64))" && + die "implausibly small disk size $TOT_SIZE_KIB" + +# In a multiarch setting, we cannot tell in advance which architecture will be +# supplying the Linux kernel image, but that's the architecture we need the EFI +# stub for. Hence fail early. +test "${DEBARCH%,*}" = "$DEBARCH" || + die "multiarch images are not yet supported" + +case "${DEBARCH%%,*}" in + amd64) + EFIARCH=x64 + ROOTGUID=4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709 + ;; + arm64) + EFIARCH=aa64 + ROOTGUID=B921B045-1DF0-41C3-AF44-4C6F280D3FAE + ;; + armhf) + EFIARCH=arm + ROOTGUID=69DAD710-2CE4-4E3C-B16C-21A1D49ABED3 + ;; + i386) + EFIARCH=ia32 + ROOTGUID=44479540-F297-41B2-9AF7-D131D5F0458A + ;; + riscv64) + EFIARCH=riscv64 + ROOTGUID=72EC70A6-CF74-40E6-BD49-4BDA08E8F224 + ;; + *) + die "unsupported architecture: $DEBARCH" + ;; +esac + +test -e "$IMAGE" && + die "output file $IMAGE already exists" + +if command -v ukify >/dev/null; then + UKIFY=ukify + UKIFY_VERBOSE= +else + UKIFY="${BIN_DIR}debefivm-ukify" + UKIFY_VERBOSE=--verbose +fi + +WORKDIR= + +cleanup() { + test -n "$WORKDIR" && rm -Rf "$WORKDIR" +} +cleanup_abort() { + cleanup + echo aborted >&2 + exit 2 +} +trap cleanup EXIT +trap cleanup_abort HUP INT TERM QUIT + +WORKDIR=$(mktemp -d) + +truncate --size="${TOT_SIZE_KIB}K" "$IMAGE" + +# Stuff most of our options before the user options to allow them to override +# ours. +# +# We skip replacing start-stop-daemon, because policy-rc.d nowadays works well +# enough and then we have to not restore it before calling mkfs.ext4. +set -- \ + --mode=root \ + --variant=apt \ + --architecture="$DEBARCH" \ + --skip=chroot/start-stop-daemon \ + "$@" + +if ! check_skip kernel; then + set -- "--customize-hook=$SHARE_DIR/customize-kernel.sh" "$@" +fi + +set -- \ + "--customize-hook=printf 'LABEL=%s / ext4 defaults 0 0' '$ROOTLABEL' >"'"$1/etc/fstab"' \ + "--include=?narrow(?exact-name(systemd-boot),?architecture(${DEBARCH%%,*}))" \ + '--include=systemd-sysv' \ + "$SUITE" \ + /dev/null \ + "$@" + +# set up a hostname +ETC_HOSTS_TEMPLATE='127.0.0.1 localhost\n127.0.1.1 %s\n::1 ip6-localhost ip6-loopback\n' +set -- \ + "--customize-hook=echo '$VMNAME' >"'"$1/etc/hostname"' \ + "--customize-hook=printf '$ETC_HOSTS_TEMPLATE' '$VMNAME' >"'"$1/etc/hosts"' \ + "$@" + +# allow password-less root login +set -- "--customize-hook=$SHARE_DIR/customize-delete-rootpw.sh" "$@" + +if ! check_skip systemdnetwork; then + # dhcp on all network interfaces, and add a dns resolver + set -- \ + "--customize-hook=$SHARE_DIR/customize-networkd.sh" \ + '--include=?not(?virtual)?exact-name(libnss-resolve)' \ + "--customize-hook=$SHARE_DIR/customize-resolved.sh" \ + "$@" +fi + +# add ssh key for root +if test -n "$SSHKEY"; then + set -- \ + --include=openssh-server \ + '--customize-hook=mkdir -m700 -p "$1/root/.ssh"' \ + "--customize-hook=upload $SSHKEY /root/.ssh/authorized_keys" \ + "$@" +fi + +if ! check_skip packagelists; then + set -- --skip=cleanup/apt/lists "$@" + set -- "--customize-hook=$SHARE_DIR/customize-dpkgavailable.sh" "$@" +fi + +if ! check_skip usrmerge; then + # Avoid the usrmerge package + set -- --hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr "$@" +fi + +if ! check_skip autologin; then + set -- \ + --include=login \ + "--customize-hook=$SHARE_DIR/customize-autologin.sh" "$@" +fi + +# By default, Debian mounts the EFI System Partition to /boot/efi. Keep that +# default. +BOOT_EFI_MOUNT_TEMPLATE='[Unit]\nDescription=EFI System Partition\n\n[Mount]\nWhat=LABEL=%s\nWhere=/boot/efi\nType=vfat\nOptions=umask=0077,rw,nodev,nosuid,noexec,nosymfollow\n\n[Install]\nWantedBy=local-fs.target\n' +set -- \ + '--customize-hook=mkdir "$1/boot/efi"' \ + "--customize-hook=printf '$BOOT_EFI_MOUNT_TEMPLATE' '$EFILABEL' >"'"$1/etc/systemd/system/boot-efi.mount"' \ + '--customize-hook=systemctl --root "$1" enable boot-efi.mount' \ + "$@" + +# Pass the remaining hooks last. These collect data from the generated tree and +# create the resulting filesystem. All user-supplied customizations should have +# run before these. +# +# We download the artifacts that end up being turned into the EFI. +EXT4_OPTIONS="offset=$((EXT4_OFFSET_KIB * 1024)),assume_storage_prezeroed=1" +set -- "$@" \ + "--customize-hook=download vmlinuz '$WORKDIR/kernel'" \ + "--customize-hook=download initrd.img '$WORKDIR/initrd'" \ + "--customize-hook=download '/usr/lib/systemd/boot/efi/linux$EFIARCH.efi.stub' '$WORKDIR/stub'" \ + '--customize-hook=mount --bind "$1" "$1/mnt"' \ + '--customize-hook=mount -t tmpfs tmpfs "$1/mnt/dev" -o mode=0755' \ + '--customize-hook=rm -f "$1/usr/sbin/policy-rc.d"' \ + '--customize-hook=/sbin/mkfs.ext4 -d "$1/mnt" -L '"'$ROOTLABEL' -E '$EXT4_OPTIONS' '$IMAGE' '${EXT4_SIZE_KIB}K'" \ + '--customize-hook=umount --lazy "$1/mnt"' + +set -- mmdebstrap "$@" + +# We need to write the resulting $IMAGE from inside the namespace used by +# mmdebstrap. If we were to have mmdebstrap --mode=unshare set up that +# namespace, we could end up being unable to write it if a leading directory +# component in $IMAGE were not not grant execute permission to any of the +# subuids mapped. This typically is the case when using PrivateTmp=yes. With +# that setting, there is a 0700 directory owned by the current user that +# happens to not be mapped by mmdebstrap. +# +# Instead, we take matters into our own hands and set up the namespaces outside +# mmdebstrap asking it to assume root. In addition to what mmdebstrap would do +# by itself, we also map the current user to 65536. The precise uid does not +# matter, but having it mapped allows CAP_DAC_OVERRIDE to be used for writing +# the image. +if test "$(id -u)" != 0; then + set -- \ + unshare \ + --user \ + --mount \ + --ipc \ + --pid \ + --uts \ + --fork \ + --kill-child=TERM \ + --map-user=65536 \ + --map-users=auto \ + --map-group=65536 \ + --map-groups=auto \ + --propagation=private \ + --mount-proc \ + --setuid=0 \ + --setgid=0 \ + "$@" +fi +echo "+ $*" >&2 +"$@" || die "mmdebstrap failed" + +CMDLINE="root=LABEL=$ROOTLABEL rw" +case "${DEBARCH%%,*}" in + amd64|i386) + CMDLINE="$CMDLINE console=ttyS0" + ;; +esac + +set -- "$UKIFY" \ + build \ + $UKIFY_VERBOSE \ + --efi-arch "$EFIARCH" \ + --linux "$WORKDIR/kernel" \ + --initrd "$WORKDIR/initrd" \ + --cmdline "$CMDLINE" \ + --stub "$WORKDIR/stub" \ + --output "$WORKDIR/efiimg" +echo "+ $*" >&2 +"$@" || + die "failed to generate UKI" + +rm -f "$WORKDIR/kernel" "$WORKDIR/initrd" "$WORKDIR/stub" + +truncate -s "${FAT_SIZE_KIB}K" "$WORKDIR/fat" +/sbin/mkfs.fat -n "$EFILABEL" -F 32 --invariant "$WORKDIR/fat" +mmd -i "$WORKDIR/fat" EFI EFI/BOOT +mcopy -i "$WORKDIR/fat" "$WORKDIR/efiimg" "::EFI/BOOT/boot$EFIARCH.efi" + +rm -f "$WORKDIR/efiimg" + +/sbin/sfdisk "$IMAGE" <<EOF +label: gpt +unit: sectors + +start=${FAT_OFFSET_KIB}KiB, size=${FAT_SIZE_KIB}KiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B +start=$((FAT_OFFSET_KIB + FAT_SIZE_KIB))KiB, type=$ROOTGUID +EOF + +dd if="$WORKDIR/fat" of="$IMAGE" conv=notrunc,sparse bs=1024 "seek=$FAT_OFFSET_KIB" status=none diff --git a/bin/debefivm-run b/bin/debefivm-run new file mode 100755 index 0000000..c93d7d2 --- /dev/null +++ b/bin/debefivm-run @@ -0,0 +1,401 @@ +#!/bin/sh +# SPDX-FileCopyrightText: 2024-2025 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT + +: <<'POD2MAN' +=head1 NAME + +debefivm-run - Run a virtual machine from an EFI-bootable raw disk image + +=head1 SYNOPSIS + +B<-debefivm-run> [B<-a>] I<architecture>] [B<-i> F<image>] [B<-s> I<sshport>] [B<--> I<qemu options>] + +=head1 DESCRIPTION + +B<debefivm-run> is essentially a thing wrapper around B<qemu> for running a virtual machine from an UEFI bootable raw disk image. +Such an image may be created using B<debefivm-create> or with another image creator, but its use is limited to architectures supporting EFI booting. + +=head1 OPTIONS + +=over 8 + +=item B<-a> I<architecture>, B<--architecture>=I<architecture> + +Override the Debian architecture of the provided image. +If the image uses architecture-specific type UUIDs for the root partition, the architecture can be detected. +Otherwise, the host's architecture is assumed. +The images created by B<debefivm-create> employ these UUIDs. +The value is used to determine the correct emulator binary as well as suitable EFI firmware. + +=item B<--efi-vars>=F<variablefile> + +EFI variables can be changed and retained across reboots of a virtual machine if a separate variable file is supplied. +The passed file is created from a template if absent. +If absent, a read-only variable store will be supplied to the virtual machine. + +=item B<-i> F<image>, B<--image>=F<image> + +This option specifies the location of the virtual machine image file. +By default F<vm.img> in the working directory is used. + +=item B<--netopt>=I<option> + +B<debefivm-run> sets up a user mode network by default. +It therefore passes a B<-netdev> option to B<qemu>. +Using this option, you can customize the value of that B<-netdev> option. +For instance, you can set up additional port forwards by passing e.g. C<--netopt hostfwd=:127.0.0.1:8080-:80>. +It can be used multiple times. + +=item B<--skip>=I<task> + +Skip a particular task or feature. +The option may be specified multiple times or list multiple tasks to be skipped by separating them with a comma. +By default, no tasks are skipped. +The following tasks may be skipped. + +=over 4 + +=item B<network> + +Do not configure a network card. +Use this if you want to configure network on your own. +This should also be passed in addition to passing C<-nic none> when you want to disable networking. + +=item B<rngdev> + +Do not pass a random number generator device. + +=back + +=item B<--transport>=I<transport> + +When B<debefivm-run> adds devices to B<qemu>, it has to select a transport and it most often guesses B<pci>. +When specifying a different machine such as B<-machine microvm>, a different transport such as B<device> may be needed. + +=item B<-s> I<sshport>, B<--sshport>=I<sshport> + +If given, B<qemu> is configured to pass connections to I<127.0.0.1:sshport> to port 22 of the virtual machine. +You can connect to your virtual machine without updating your known hosts like this: + + ssh -o NoHostAuthenticationForLocalhost=yes -p $sshport root@127.0.0.1 + +The option is a shorthand for C<--netopt hostfwd=tcp:127.0.0.1:sshport-:22>. + +=item B<--> I<qemu options> + +All options beyond a double dash are passed to B<qemu>. +This can be used to configure additional hardware components. +One possible use of this method is passing B<-snapshot> to avoid modifying the virtual machine image. + +=back + +=head1 SEE ALSO + + debefivm-create(1) debvm-run(1) qemu(1) + +=cut +POD2MAN + +set -u + +EFI_VARS= +GRAPHICAL= +IMAGE=vm.img +IMAGE_ARCH= +NATIVE_ARCH=$(dpkg --print-architecture) +NETOPTS= +SKIP=, +SSHPORT= +TRANSPORT= + +nth_arg() { + shift "$1" + printf "%s" "$1" +} + +die() { + echo "error: $*" >&2 + exit 1 +} +usage() { + die "usage: $0 [-a architecture] [-g] [-i image] [-s sshport] [-- qemu options]" +} +usage_error() { + echo "error: $*" 1>&2 + usage +} + +opt_architecture() { + IMAGE_ARCH=$1 +} +opt_efi_vars() { + EFI_VARS=$1 +} +opt_graphical() { + GRAPHICAL=1 +} +opt_help() { + usage +} +opt_image() { + IMAGE=$1 +} +opt_netopt() { + NETOPTS="$NETOPTS,$1" +} +opt_skip() { + SKIP="$SKIP$1," +} +opt_sshport() { + SSHPORT=$1 +} +opt_transport() { + TRANSPORT=$1 +} + +while getopts :a:gi:s:-: OPTCHAR; do + case "$OPTCHAR" in + a) opt_architecture "$OPTARG" ;; + g) opt_graphical ;; + i) opt_image "$OPTARG" ;; + s) opt_sshport "$OPTARG" ;; + -) + case "$OPTARG" in + graphical|help) + "opt_$OPTARG" + ;; + architecture|efi-vars|image|netopt|skip|sshport|transport) + test "$OPTIND" -gt "$#" && usage_error "missing argument for --$OPTARG" + "opt_$OPTARG" "$(nth_arg "$OPTIND" "$@")" + OPTIND=$((OPTIND+1)) + ;; + architecture=*|efi-vars=*|image=*|netopt=*|skip=*|sshport=*|transport=*) + "opt_${OPTARG%%=*}" "${OPTARG#*=}" + ;; + *) + usage_error "unrecognized option --$OPTARG" + ;; + esac + ;; + :) + usage_error "missing argument for -$OPTARG" + ;; + '?') + usage_error "unrecognized option -$OPTARG" + ;; + *) + die "internal error while parsing command options, please report a bug" + ;; + esac +done +shift "$((OPTIND - 1))" + +if test -n "$SSHPORT"; then + opt_netopt "hostfwd=tcp:127.0.0.1:$SSHPORT-:22" +fi + +test -f "$IMAGE" || die "image '$IMAGE' not found" +test -s "$IMAGE" || die "image '$IMAGE' is empty" + +check_skip() { + case "$SKIP" in + *",$1,"*) return 0 ;; + *) return 1 ;; + esac +} + +if test -z "$IMAGE_ARCH"; then + PARTITIONS=$(partx --show --noheadings --output type "$IMAGE") + + case "$PARTITIONS" in + *4f68bce3-e8cd-4db1-96e7-fbcaf984b709*) + IMAGE_ARCH=amd64 + ;; + *b921b045-1df0-41c3-af44-4c6f280d3fae*) + IMAGE_ARCH=arm64 + ;; + *72ec70a6-cf74-40e6-bd49-4bda08e8f224*) + IMAGE_ARCH=riscv64 + ;; + *69dad710-2ce4-4e3c-b16c-21a1d49abed3*) + IMAGE_ARCH=armhf + ;; + *44479540-f297-41b2-9af7-d131d5f0458a*) + IMAGE_ARCH=i386 + ;; + *) + echo "cannot detect image architecture from gpt, assuming $NATIVE_ARCH" >&2 + IMAGE_ARCH="$NATIVE_ARCH" + ;; + esac +fi + +# Translate IMAGE_ARCH (a Debian architecture) to a Debian CPU name. +# This utilizes the QEMU Debian package symlink mapping that ensures that +# calling qemu-system-${DEB_HOST_ARCH_CPU} will run the QEMU binary providing +# the correct emulator for that CPU. +IMAGEARCHCPU="$(dpkg-architecture --force --host-arch "$IMAGE_ARCH" --query DEB_HOST_ARCH_CPU)" +QEMU="qemu-system-$IMAGEARCHCPU" +CPU= +MACHINE= +MAX_SMP= + +case "$IMAGEARCHCPU" in + amd64) + QEMU=qemu-system-x86_64 + MACHINE="type=q35" + BIOSCODE='/usr/share/OVMF/OVMF_CODE_4M.fd' + BIOSDATA='/usr/share/OVMF/OVMF_VARS_4M.fd' + BIOSPACKAGE=ovmf + ;; + arm) + CPU=max + MACHINE="type=virt,highmem=off" + MAX_SMP=8 + BIOSCODE='/usr/share/AAVMF/AAVMF32_CODE.fd' + BIOSDATA='/usr/share/AAVMF/AAVMF32_VARS.fd' + BIOSPACKAGE=qemu-efi-arm + ;; + arm64) + CPU=max,pauth-impdef=on + MACHINE="type=virt,gic-version=max" + BIOSCODE='/usr/share/AAVMF/AAVMF_CODE.fd' + BIOSDATA='/usr/share/AAVMF/AAVMF_VARS.fd' + BIOSPACKAGE=qemu-efi-aarch64 + ;; + i386) + BIOSCODE='/usr/share/OVMF/OVMF32_CODE_4M.secboot.fd' + BIOSDATA='/usr/share/OVMF/OVMF32_VARS_4M.fd' + BIOSPACKAGE=ovfm-ia32 + ;; + riscv64) + MACHINE="type=virt" + BIOSCODE='/usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd' + BIOSDATA='/usr/share/qemu-efi-riscv64/RISCV_VIRT_VARS.fd' + BIOSPACKAGE=qemu-efi-riscv64 + ;; + *) + die "support for $IMAGE_ARCH is not implemented" + ;; +esac + +test -e "$BIOSCODE" || + die "cannot file firmware file $BIOSCODE. Is $BIOSPACKAGE installed?" +test -e "$BIOSDATA" || + die "cannot file firmware file $BIOSDATA. Is $BIOSPACKAGE installed?" + +# Assign the default late to allow both cli and arch-specific overrides. +: "${TRANSPORT:=pci}" + +comma_escape() { + # If a filename contains a comma, then that comma must be escaped by + # prefixing it with another comma or otherwise output filenames are + # able to inject options to qemu (and load the wrong file). + comma_escape_str="$1" + while test "${comma_escape_str%,*}" != "$comma_escape_str"; do + printf "%s,," "${comma_escape_str%%,*}" + comma_escape_str="${comma_escape_str#*,}" + done + printf "%s" "$comma_escape_str" +} + +EFI_VAR_DRIVE="if=pflash,format=raw,unit=1" +if test -n "$EFI_VARS"; then + if ! test -e "$EFI_VARS"; then + cp "$BIOSDATA" "$EFI_VARS" + fi + EFI_VAR_DRIVE="$EFI_VAR_DRIVE,read-only=off,file=$(comma_escape "$EFI_VARS")" +else + EFI_VAR_DRIVE="$EFI_VAR_DRIVE,read-only=on,file=$(comma_escape "$BIOSDATA")" +fi + +ENABLE_KVM=no +if test "$NATIVE_ARCH" = "$IMAGE_ARCH"; then + ENABLE_KVM=yes +fi +if test "$ENABLE_KVM" = yes; then + if ! command -v "$QEMU" >/dev/null 2>&1; then + # Fall back to kvm in case we badly guessed qemu. + QEMU=kvm + fi + MACHINE="${MACHINE:+$MACHINE,}accel=kvm:tcg" + # While kvm will fall back gracefully, only override CPU when we expect + # kvm to work. + if test -w /dev/kvm; then + CPU=max + # kvm: "max" will become "host", intended. + # tcg: "max" will actually work, "host" would not. + fi +fi + +if test -n "$MACHINE"; then + set -- -machine "$MACHINE" "$@" +fi + +set -- \ + -no-user-config \ + -nodefaults \ + -chardev stdio,id=console,mux=on,signal=off \ + -serial chardev:console \ + -name "debefivm-run $IMAGE" \ + -m 1G \ + -drive "if=pflash,format=raw,unit=0,read-only=on,file=$(comma_escape "$BIOSCODE")" \ + -drive "$EFI_VAR_DRIVE" \ + -drive "id=root,media=disk,format=raw,discard=unmap,file=$(comma_escape "$IMAGE"),if=none,cache=unsafe" \ + -device "virtio-blk-$TRANSPORT,drive=root,serial=root" \ + "$@" + +if test -n "$CPU"; then + set -- -cpu "$CPU" "$@" +fi + +NPROC=$(nproc) +if test "$NPROC" -gt 1; then + test -n "$MAX_SMP" && test "$NPROC" -gt "$MAX_SMP" && NPROC=$MAX_SMP + set -- -smp "$NPROC" "$@" +fi +if ! check_skip rngdev; then + set -- \ + -device "virtio-rng-$TRANSPORT,rng=rng0" \ + -object rng-random,filename=/dev/urandom,id=rng0 \ + "$@" +fi + +if test -z "$GRAPHICAL"; then + set -- -nographic "$@" +else + case "$KERNELARCH" in + amd64|i386) + set -- -vga virtio "$@" + ;; + *) + set -- \ + -device "virtio-gpu-gl-$TRANSPORT" \ + -display gtk,gl=on \ + "$@" + ;; + esac + set -- \ + -device "virtio-keyboard-$TRANSPORT" \ + -device "virtio-tablet-$TRANSPORT" \ + "$@" +fi + +DNSSEARCH=$(dnsdomainname) +if test -z "$DNSSEARCH"; then + DNSSEARCH=$(sed -n 's/^\s*search\s*\.\?//p;T;q' /etc/resolv.conf) +fi +if test -n "$DNSSEARCH"; then + NETOPTS=",domainname=$DNSSEARCH$NETOPTS" +fi + +if ! check_skip network; then + set -- \ + -netdev "user,id=net0$NETOPTS" \ + -device "virtio-net-$TRANSPORT,netdev=net0" \ + "$@" +fi + +echo "+ $QEMU $*" 1>&2 +exec "$QEMU" "$@" diff --git a/bin/debefivm-ukify b/bin/debefivm-ukify new file mode 100755 index 0000000..9267158 --- /dev/null +++ b/bin/debefivm-ukify @@ -0,0 +1,329 @@ +#!/bin/sh +# SPDX-FileCopyrightText: 2023-2025 Helmut Grohne <helmut@subdivi.de> +# SPDX-License-Identifier: MIT +# + +: <<'POD2MAN' +=head1 NAME + +debefivm-ukify - Create Unified Kernel Images for UEFI systems in a similar way to systemd's ukify + +=head1 SYNOPSIS + +B<debefivm-ukify> B<build> B<--linux>=F<vmlinuz> B<--output>=F<uki.efi> [options] + +=head1 DESCRIPTION + +B<debefivm-ukify> is a reimplementation of parts of B<systemd-ukify> and mainly meant for backports. +Please prefer B<systemd>'s implementation. +It only supports the B<build> subcommand, does not implement any functionality related to secure boot and lacks configuration file support. +Other than that, it can be used to assemble a linux kernel image, an initrd and an EFI stub image from B<systemd> into an EFI-bootable Unified Kernel Image. + +=head1 OPTIONS + +=over 8 + +=item B<--deb-arch>=I<debian_architecture> + +Set the architecture of the supplied and produced artifacts using Debian's architecture nomenclature. +See B<--efi-arch> for another supplying it using the EFI nomenclature instead. +This option is not available on the B<systemd> implementation. + +=item B<--devicetree>=F<devicetree_blob> + +Add a device tree file as a dtb section to the generated UKI. +There can be at most one device tree. + +=item B<--cmdline>=I<kernel_cmdline> + +Supply extra arguments to be appended to the Linux kernel command line during boot. +Can be given given at most once. +If the value starts with an B<@>, it is treated as a filename whose contents will be added. + +=item B<--efi-arch>=I<efi_architecture> + +Set the architecture of the supplied and produced artifacts using EFI's architecture nomenclature. +See B<--deb-arch> for another supplying it using the Debian nomenclature instead. + +=item B<--initrd>=F<initrd> + +Supply an initrd image to be embedded. +There can be at most one initrd. + +=item B<--linux>=F<vmlinuz> + +Supply a Linux kernel image to be embedded. +This option must be given exactly once. + +=item B<--os-release>=I<release_information> + +Set the os-release description of in the UKI. +Can be given given at most once. +If the value starts with an B<@>, it is treated as a filename whose contents will be added. + +=item B<--output>=<uki.efi> + +Set the name of the resulting UKI image. +This option must be given exactly once and the target file must not exist already. + +=item B<--section>=<name:value> + +Add an arbitrary ELF section to the resulting UKI. +If the value starts with an B<@>, it is treated as a filename whose contents will be added. +Most image components actually are such ELF sections. +For instance B<--os-release>, adds a section named B<osrel>. + +=item B<--stub>=F<efi_stub> + +Supply the location of B<systemd>'s EFI stub image. +Unlike the B<systemd> implementation, this option accepts a directory containing stubs named by architecture. +By default, stubs will be looked up in B</usr/lib/systemd/boot/efi>. + +=item B<--verbose> + +Print diagnostic information during image conversion. +This option is not available on the B<systemd> implementation. + +=back + +=head1 SEE ALSO + + ukify(1) + +=cut +POD2MAN + +set -eu + +die() { + echo "$*" >&2 + exit 1 +} +usage() { + die "usage: $0 [build] <--linux=|--initrd=|--stub=> [--cmdline=|--deb-arch=|--devicetree=|--efi-arch=|--os-release=|--output=|--section=|--verbose]" +} +usage_error() { + echo "error: $*" >&2 + usage +} + +DEBARCH= +BOOTSTUB=/usr/lib/systemd/boot/efi +OUTPUT= +VERBOSE= + +TDIR= +cleanup() { + if test -n "$TDIR"; then + rm -Rf "$TDIR" + fi +} +cleanup_abort() { + cleanup + echo aborted >&2 + exit 2 +} +trap cleanup EXIT +trap cleanup_abort HUP INT TERM QUIT + +# The TDIR will contain one file per section. It can be a symbolic link or +# regular file. The section name is the filename with a leading dot. +TDIR=$(mktemp --directory --tmpdir mini-ukify.XXXXXXXXXX) + +add_section_content() { + test -e "$TDIR/$1" && die "section '$1' already defined" + printf "%s" "$2" > "$TDIR/$1" +} +add_section_file() { + test -e "$TDIR/$1" && die "section '$1' already defined" + test -f "$2" || die "file '$2' does not exist" + if test "${2#/}" = "$2"; then + ln --symbolic --relative "$2" "$TDIR/$1" + else + ln --symbolic "$2" "$TDIR/$1" + fi +} +add_section_either() { + if test "${2#@}" = "$2"; then + add_section_content "$1" "$2" + else + add_section_file "$1" "${2#@}" + fi +} + +opt_cmdline() { + add_section_either cmdline "$1" +} +opt_deb_arch() { + case "$1" in + amd64) + EFIARCH=x64 + GNU_TYPE=x86_64-linux-gnu + ;; + arm64) + EFIARCH=aa64 + GNU_TYPE=aarch64-linux-gnu + ;; + armhf) + EFIARCH=arm + GNU_TYPE=arm-linux-gnueabihf + ;; + i386) + EFIARCH=ia32 + GNU_TYPE=i686-linux-gnu + ;; + riscv64) + EFIARCH=riscv64 + GNU_TYPE=riscv64-linux-gnu + ;; + *) + die "unsupported Debian architecture: $1" + ;; + esac + DEBARCH="$1" +} +opt_devicetree() { + add_section_file dtb "$1" +} +opt_efi_arch() { + case "$1" in + aa64) opt_deb_arch arm64 ;; + arm) opt_deb_arch armhf ;; + ia32) opt_deb_arch i386 ;; + x64) opt_deb_arch amd64 ;; + riscv64) opt_deb_arch "$1" ;; + *) + die "unsupported EFI architecture: $1" + ;; + esac +} +opt_initrd() { + add_section_file initrd "$1" +} +opt_linux() { + add_section_file linux "$1" + test -z "$OUTPUT" && OUTPUT="$1.unsigned.efi" +} +opt_os_release() { + add_section_either osrel "$1" +} +opt_output() { + OUTPUT="$1" +} +opt_section() { + case "$1" in + *[!a-z]*:*) + die "invalid section name '${1%%:*}'" + ;; + *:*) + ;; + *) + die "missing section name separated by colon in '$1'" + ;; + esac + add_section_either "${1%%:*}" "${1#*:}" +} +opt_stub() { + BOOTSTUB="$1" +} +opt_verbose() { + VERBOSE=yes +} +positional=1 +positional_1() { + test "$1" = "build" || usage_error "command not understood: '$1'" +} +positional_2() { + usage_error "too many positional arguments" +} + +while test "$#" -gt 0; do + case "$1" in + --cmdline=*|--deb-arch=*|--devicetree=*|--efi-arch=*|--initrd=*|--linux=*|--os-release=*|--output=*|--section=*|--stub=*) + optname="${1%%=*}" + optname="${optname#--}" + test "${optname#*-}" = "$optname" || + optname="${optname%%-*}_${optname#*-}" + "opt_$optname" "${1#*=}" + ;; + --cmdline|--deb-arch|--devicetree|--efi-arch|--initrd|--linux|--os-release|--output|--section|--stub) + test "$#" -ge 2 || usage_error "missing argument for $1" + optname="${1#--}" + test "${optname#*-}" = "$optname" || + optname="${optname%%-*}_${optname#*-}" + "opt_$optname" "$2" + shift + ;; + --verbose) + "opt_${1#--}" + ;; + --*) + usage_error "unrecognized option $1" + ;; + *) + "positional_$positional" "$1" + positional=$((positional + 1)) + ;; + esac + shift +done + +test "$positional" = 2 || usage_error "missing subcommand, only 'build' is supported" +test -z "$DEBARCH" && opt_deb_arch "$(dpkg --print-architecture)" +test -e "$TDIR/linux" || usage_error "missing --linux argument" +test -e "$BOOTSTUB" || die "stub image or directory '$BOOTSTUB' does not exist" +test -e "$OUTPUT" && die "output '$OUTPUT' already exists" + +test -d "$BOOTSTUB" && BOOTSTUB="$BOOTSTUB/linux$EFIARCH.efi.stub" +test -f "$BOOTSTUB" || die "efi boot stub $BOOTSTUB not found" + +GNU_PREFIX="$GNU_TYPE-" +test "$(dpkg-query --showformat='${db:Status-Status}' --show binutils-multiarch)" = installed && + GNU_PREFIX= + +# Compute the next multiple of $2 greater than or equal to $1. +align_size() { + echo "$((($1) + ($2) - 1 - (($1) + ($2) - 1) % ($2)))" +} +alignment=$("${GNU_PREFIX}objdump" --private-headers "$BOOTSTUB" | sed 's/^SectionAlignment\s\+\([0-9]\)/0x/;t;d') +test -z "$alignment" && + die "failed to discover the alignment of the efi stub" +test -n "$VERBOSE" && + echo "determined efi vma alignment as $alignment" + +# Discover the last section in terms of vma + size. We'll append sections +# beyond. +lastoffset=0 +# shellcheck disable=SC2034 # unused variables serve documentation +lastoffset="$("${GNU_PREFIX}objdump" --section-headers "$BOOTSTUB" \ + | while read -r idx name size vma lma fileoff algn behind; do + test -z "$behind" -a "${algn#"2**"}" != "$algn" || continue + offset=$((0x$vma + 0x$size)) + test "$offset" -gt "$lastoffset" || continue + lastoffset="$offset" + echo "$lastoffset" + done | tail -n1)" +lastoffset=$(align_size "$lastoffset" "$alignment") +test -n "$VERBOSE" && + echo "determined minimum efi vma offset as $lastoffset" + +# Compute the objdump invocation that constructs the UKI. Successively add +# sections and compute non-overlapping VMAs. +set -- \ + "${GNU_PREFIX}objcopy" \ + --enable-deterministic-archives +for sectionfile in "$TDIR/"*; do + section="${sectionfile##*/}" + size=$(stat -Lc%s "$sectionfile") + size=$(align_size "$size" "$alignment") + set -- "$@" \ + --add-section ".$section=$sectionfile" \ + --change-section-vma ".$section=$lastoffset" + lastoffset=$((lastoffset + size)) +done +set -- "$@" "$BOOTSTUB" "$OUTPUT" +if test -n "$VERBOSE"; then + ls -lA "$TDIR" + printf "%s\n" "$*" +fi +"$@" || die "failed to construct UKI" |