diff --git a/Makefile b/Makefile index f6567227..60b8f218 100644 --- a/Makefile +++ b/Makefile @@ -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)) \ diff --git a/boards/librem_mini_v2/librem_mini_v2.config b/boards/librem_mini_v2/librem_mini_v2.config index 9f8afde8..04c8cfd0 100644 --- a/boards/librem_mini_v2/librem_mini_v2.config +++ b/boards/librem_mini_v2/librem_mini_v2.config @@ -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 diff --git a/initrd/bin/inject_firmware.sh b/initrd/bin/inject_firmware.sh index 87ea0a88..452e931f 100755 --- a/initrd/bin/inject_firmware.sh +++ b/initrd/bin/inject_firmware.sh @@ -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" diff --git a/initrd/bin/unpack_initramfs.sh b/initrd/bin/unpack_initramfs.sh new file mode 100755 index 00000000..8c1a5cd4 --- /dev/null +++ b/initrd/bin/unpack_initramfs.sh @@ -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 diff --git a/modules/zstd b/modules/zstd new file mode 100644 index 00000000..3d7bd4b3 --- /dev/null +++ b/modules/zstd @@ -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