#! /bin/bash set -e -o pipefail . /etc/functions TRACE_FUNC # 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() { TRACE_FUNC 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() { TRACE_FUNC ( 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() { TRACE_FUNC 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*) DEBUG "archive segment $magic: uncompressed cpio" # Skip zero bytes and copy the first nonzero byte consume_zeros # Copy the remaining data cat ;; 303730373031* | 303730373032*) # plain cpio DEBUG "archive segment $magic: plain cpio" # Unpack the plain cpio, this stops reading after the trailer unpack_cpio # Copy the remaining data cat ;; 1f8b* | 1f9e*) # gzip DEBUG "archive segment $magic: 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 ;; fd37*) # xz DEBUG "archive segment $magic: xz" unxz | unpack_cpio ;; 28b5*) # zstd DEBUG "archive segment $magic: 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" # The following are magic values for other compression formats # but not added because not tested. # TODO: open an issue for unsupported magic number reported on die. # #425a*) # bzip2 # DEBUG "archive segment $magic: bzip2" # bunzip2 | unpack_cpio #;; #5d00*) # lzma # DEBUG "archive segment $magic: lzma" # unlzma | unpack_cpio #;; #894c*) # lzo # DEBUG "archive segment $magic: lzo" # lzop -d | unpack_cpio #;; #0221*) # lz4 # DEBUG "archive segment $magic: lz4" # lz4 -d | unpack_cpio # ;; ;; 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