heads/initrd/bin/inject_firmware.sh
Jonathon Hall af5eb2edf9
Blob jail: Make device firmware available during initrd
Some device firmware, such as the graphics microcontroller, is needed
during the initrd - i915 is often loaded in the initrd, and this is the
only chance to load GuC firmware.

Device firmware must still be available after the real root is mounted
too, so update the custom firmware path in the kernel when the firmware
is moved to /run.

Signed-off-by: Jonathon Hall <jonathon.hall@puri.sm>
2023-09-29 15:36:31 -04:00

105 lines
4.1 KiB
Bash
Executable File

#!/bin/bash
# If blob jail is enabled, copy initrd and inject firmware.
# Prints new initrd path (in memory) if firmware was injected.
#
# This does not alter the initrd on disk:
# * Signatures are not invalidated
# * If the injection fails for any reason, we just proceed with the original
# initrd (lacking firmware, but still booting).
# * If, somehow, this injection malfunctions (without failing outright) and
# prevents a boot, the user can work around it just by disabling blob jail.
# We do not risk ruining the real initrd.
#
# The injection has some requirements on the initrd that are all true for
# Debian:
# * initrd must be a gzipped cpio (Linux supports other compression methods)
# * /init must be a shell script (so we can inject a command to copy firmware)
# * There must be an 'exec run-init ... ${rootmnt} ...' line that moves the
# real root to / and invokes init
#
# If the injection can't be performed, boot will continue with no firmware.
set -e -o pipefail
. /tmp/config
. /etc/functions
if [ "$(load_config_value CONFIG_USE_BLOB_JAIL)" != "y" ]; then
# Blob jail not active, nothing to do
exit 0
fi
ORIG_INITRD="$1"
# Extract the init script from the initrd
INITRD_ROOT="/tmp/inject_firmware_initrd_root"
rm -rf "$INITRD_ROOT" || true
mkdir "$INITRD_ROOT"
# Unpack just 'init' from the original initrd
unpack_initramfs.sh "$ORIG_INITRD" "$INITRD_ROOT" init
# Copy the firmware into the initrd
for f in $(cbfs -l | grep firmware); do
mkdir -p "$INITRD_ROOT/$(dirname "$f")"
cbfs -r "$f" > "$INITRD_ROOT/$f"
if [[ "$f" == *.lzma ]]; then
lzma -d "$INITRD_ROOT/$f"
fi
done
# awk will happily pass through a binary file, so look for the match we want
# before modifying init to ensure it's a shell script and not an ELF, etc.
if ! grep -E -q '^exec run-init .*\$\{rootmnt\}' "$INITRD_ROOT/init"; then
WARN "Can't apply firmware blob jail, unknown init script"
exit 0
fi
# In general, firmware files must be available _both_ during the initrd _and_
# once root is moved to /. Firmware loading may happen in either phase (e.g.
# i915 GUC firmware is usually loaded in the initrd because i915 is used there,
# but Wi-Fi/BT modules typically are not in the initrd, they're loaded later).
#
# We want to place the firmware after boot in /run, since this is a tmpfs mount
# - it works even if the root filesystem is read-only and does not persist
# anything. But we cannot place it there for the initrd, since the initrd also
# mounts a tmpfs on /run. We can only specify one custom firmware path, but we
# can change it at runtime.
#
# So during the initrd, the firmware is in /firmware, and we provide that path
# on the kernel command line. Just before invoking the real init (after root is
# mounted), we copy it to /run/firmware and also change the firmware path.
#
# Debian's init script ends with an "exec run-init ..." (followed by a few lines
# to print a message in case it fails). At that point, root is mounted, and
# run-init will move it to / and then exec init. We can insert the firmware
# actions just before that, so we don't have to know anything about how root was
# mounted.
#
# The root path is in ${rootmnt}, which should appear in the run-init command.
# If it doesn't, then we don't understand the init script.
AWK_INSERT_CP='
BEGIN{inserted=0}
/^exec run-init .*\$\{rootmnt\}/ && inserted==0 {
print "cp -r /firmware ${rootmnt}/run/firmware"
print "echo -n /run/firmware >${rootmnt}/sys/module/firmware_class/parameters/path"
inserted=1
}
{print $0}'
awk -e "$AWK_INSERT_CP" "$INITRD_ROOT/init" >"$INITRD_ROOT/init_fw"
mv "$INITRD_ROOT/init_fw" "$INITRD_ROOT/init"
chmod a+x "$INITRD_ROOT/init"
# Pad the original initrd to 512 byte blocks. Uncompressed cpio contents must
# be 4-byte aligned, and anecdotally gzip frames might not be padded by dracut.
# Linux ignores zeros between archive segments, so any extra padding is not
# harmful.
FW_INITRD="/tmp/inject_firmware_initrd.cpio.gz"
dd if="$ORIG_INITRD" of="$FW_INITRD" bs=512 conv=sync status=none
# Pack up the new contents and append to the initrd. Don't spend time
# compressing this.
(cd "$INITRD_ROOT"; find . | cpio -o -H newc) >>"$FW_INITRD"
# Use this initrd
echo "$FW_INITRD"