Blob jail: Add zstd-decompress, decompress more complex archives

Debian 12's initrd by default now consists of an uncompressed cpio
archive containing microcode, followed by a zstd-compressed cpio
archive.  inject_firmware.sh only supported gzip-compressed cpio, so it
could not extract /init from this archive.

Add zstd-decompress to decompress zstd streams (uncompressed size is
about 180 KB).

Add unpack_initramfs.sh which is able to decompress uncompressed, gzip,
or zstd archives, with multiple segments, much like the Linux kernel
itself does.

Use unpack_initramfs.sh to extract /init for blob jail.

Don't compress the new archive segment containing firmware and the
updated /init.

Signed-off-by: Jonathon Hall <jonathon.hall@puri.sm>
This commit is contained in:
Jonathon Hall 2023-06-20 15:29:10 -04:00
parent 3e6eac9ffd
commit 1bf8331ffb
No known key found for this signature in database
GPG Key ID: 1E9C3CA91AE25114
5 changed files with 143 additions and 5 deletions

View File

@ -500,6 +500,7 @@ bin_modules-$(CONFIG_TPM2_TOOLS) += tpm2-tools
bin_modules-$(CONFIG_BASH) += bash
bin_modules-$(CONFIG_POWERPC_UTILS) += powerpc-utils
bin_modules-$(CONFIG_IOPORT) += ioport
bin_modules-$(CONFIG_ZSTD) += zstd
$(foreach m, $(bin_modules-y), \
$(call map,initrd_bin_add,$(call bins,$m)) \

View File

@ -20,6 +20,8 @@ CONFIG_PCIUTILS=y
CONFIG_POPT=y
CONFIG_QRENCODE=y
CONFIG_TPMTOTP=y
# zstd-decompress - for blob jail, needed to extract /init from zstd cpio archive
CONFIG_ZSTD=y
CONFIG_CAIRO=y
CONFIG_FBWHIPTAIL=y

View File

@ -36,7 +36,8 @@ ORIG_INITRD="$1"
INITRD_ROOT="/tmp/inject_firmware_initrd_root"
rm -rf "$INITRD_ROOT" || true
mkdir "$INITRD_ROOT"
gunzip <"$ORIG_INITRD" | (cd "$INITRD_ROOT"; cpio -i init 2>/dev/null)
# 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
@ -50,6 +51,7 @@ 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
@ -79,11 +81,14 @@ 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, the last gzip blob is often not
# padded. (If it is not gzip-compressed, we would already have failed above.)
# 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
(cd "$INITRD_ROOT"; find . | cpio -o -H newc) | gzip >>"$FW_INITRD"
# 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"

110
initrd/bin/unpack_initramfs.sh Executable file
View File

@ -0,0 +1,110 @@
#! /bin/bash
set -e -o pipefail
. /etc/functions
# Unpack a Linux initramfs archive.
#
# In general, the initramfs archive is one or more cpio archives, optionally
# compressed, concatenated together. Uncompressed and compressed segments can
# exist in the same file. Zero bytes between segments are skipped. To properly
# unpack such an archive, all segments must be unpacked.
#
# This script unpacks such an archive, but with a limitation that once a
# compressed segment is reached, no more segments can be read. This works for
# common initrds on x86, where the microcode must be stored in an initial
# uncompressed segment, followed by the "real" initramfs content which is
# usually in one compressed segment.
#
# The limitation comes from gunzip/unzstd, there's no way to prevent them from
# consuming trailing data or tell us the member/frame length. The script
# succeeds with whatever was extracted, since this is used to extract particular
# files and boot can proceed as long as those files were found.
INITRAMFS_ARCHIVE="$1"
DEST_DIR="$2"
shift
shift
# rest of args go to cpio, can specify filename patterns
CPIO_ARGS=("$@")
# Consume zero bytes, the first nonzero byte read (if any) is repeated on stdout
consume_zeros() {
next_byte='00'
while [ "$next_byte" = "00" ]; do
# if we reach EOF, next_byte becomes empty (dd does not fail)
next_byte="$(dd bs=1 count=1 status=none | xxd -p | tr -d ' ')"
done
# if we finished due to nonzero byte (not EOF), then carry that byte
if [ -n "$next_byte" ]; then
echo -n "$next_byte" | xxd -p -r
fi
}
unpack_cpio() {
(cd "$dest_dir"; cpio -i "${CPIO_ARGS[@]}" 2>/dev/null)
}
# unpack the first segment of an archive, then write the rest to another file
unpack_first_segment() {
unpack_archive="$1"
dest_dir="$2"
rest_archive="$3"
mkdir -p "$dest_dir"
# peek the beginning of the file to determine what type of content is next
magic="$(dd if="$unpack_archive" bs=6 count=1 status=none | xxd -p)"
# read this segment of the archive, then write the rest to the next file
(
# Magic values correspond to Linux init/initramfs.c (zero, cpio) and
# lib/decompress.c (gzip)
case "$magic" in
00*)
# Skip zero bytes and copy the first nonzero byte
consume_zeros
# Copy the remaining data
cat
;;
303730373031*|303730373032*) # plain cpio
# Unpack the plain cpio, this stops reading after the trailer
unpack_cpio
# Copy the remaining data
cat
;;
1f8b*|1f9e*) # gzip
# gunzip won't stop when reaching the end of the gzipped member,
# so we can't read another segment after this. We can't
# reasonably determine the member length either, this requires
# walking all the compressed blocks.
gunzip | unpack_cpio
;;
28b5*) # zstd
# Like gunzip, this will not stop when reaching the end of the
# frame, and determining the frame length requires walking all
# of its blocks.
(zstd-decompress -d || true) | unpack_cpio
;;
*) # unknown
die "Can't decompress initramfs archive, unknown type: $magic"
;;
esac
) <"$unpack_archive" >"$rest_archive"
orig_size="$(stat -c %s "$unpack_archive")"
rest_size="$(stat -c %s "$rest_archive")"
DEBUG "archive segment $magic: $((orig_size - rest_size)) bytes"
}
DEBUG "Unpacking $INITRAMFS_ARCHIVE to $DEST_DIR"
next_archive="$INITRAMFS_ARCHIVE"
rest_archive="/tmp/unpack_initramfs_rest"
# Break when there is no remaining data
while [ -s "$next_archive" ]; do
unpack_first_segment "$next_archive" "$DEST_DIR" "$rest_archive"
next_archive="/tmp/unpack_initramfs_next"
mv "$rest_archive" "$next_archive"
done

20
modules/zstd Normal file
View File

@ -0,0 +1,20 @@
modules-$(CONFIG_ZSTD) += zstd
zstd_version := 1.5.5
zstd_dir := zstd-$(zstd_version)
zstd_tar := zstd-$(zstd_version).tar.gz
zstd_url := https://github.com/facebook/zstd/releases/download/v$(zstd_version)/$(zstd_tar)
zstd_hash := 9c4396cc829cfae319a6e2615202e82aad41372073482fce286fac78646d3ee4
zstd_configure := true
# Only the decompressor is built and installed, to be able to read zstd-compressed
# initramfs archives.
zstd_target := \
$(MAKE_JOBS) $(CROSS_TOOLS) -C programs CFLAGS="-g0 -Os" \
HAVE_ZLIB=0 \
HAVE_LZMA=0 \
HAVE_LZ4=0 \
zstd-decompress
zstd_output := programs/zstd-decompress