From a797b423d21e2231b8c55287ac9c46773b8816a3 Mon Sep 17 00:00:00 2001 From: Ben Hutchings Date: Mon, 31 Mar 2025 01:49:05 +0200 Subject: [PATCH 1/3] unmkinitramfs: Rewrite in C to make it acceptably fast Parsing uncompressed cpio archives in this shell script requires running multiple processes for each archive member. This was not too bad when there were usually only a few microcode blobs in an uncompressed archive, but now that we put compressed kernel modules in an uncompressed archive, it became very slow indeed. Rewrite it in C so that it's acceptably fast. This should have no functional changes, except that it adds support for an initramfs with no compressed part. Fixes: 81fd41f72dd8 ("Put compressed kernel modules and firmware in an uncompressed cpio") Signed-off-by: Ben Hutchings --- .gitignore | 1 + Makefile | 7 + unmkinitramfs | 219 --------------- unmkinitramfs.8 | 8 +- unmkinitramfs.c | 698 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 710 insertions(+), 223 deletions(-) create mode 100644 Makefile delete mode 100755 unmkinitramfs create mode 100644 unmkinitramfs.c diff --git a/.gitignore b/.gitignore index 42eda44..e35d40f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ .#* +/unmkinitramfs diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d82d47c --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +CFLAGS += -Wall -Wextra -O2 + +all: unmkinitramfs +clean: + rm -f unmkinitramfs + +.PHONY: all clean diff --git a/unmkinitramfs b/unmkinitramfs deleted file mode 100755 index 9bce38a..0000000 --- a/unmkinitramfs +++ /dev/null @@ -1,219 +0,0 @@ -#!/bin/sh - -set -eu - -usage() -{ - cat << EOF - -Usage: unmkinitramfs [-v] initramfs-file directory - -Options: - -v Display verbose messages about extraction - -See unmkinitramfs(8) for further details. - -EOF -} - -usage_error() -{ - usage >&2 - exit 2 -} - -# Extract a compressed cpio archive -xcpio() -{ - archive_uncomp="$1" - archive="$2" - dir="$3" - shift 3 - - { - cat "$archive_uncomp" - if gzip -t "$archive" >/dev/null 2>&1 ; then - gzip -c -d "$archive" - elif zstd -q -c -t "$archive" >/dev/null 2>&1 ; then - zstd -q -c -d "$archive" - elif xzcat -t "$archive" >/dev/null 2>&1 ; then - xzcat "$archive" - elif lz4cat -t < "$archive" >/dev/null 2>&1 ; then - lz4cat "$archive" - elif bzip2 -t "$archive" >/dev/null 2>&1 ; then - bzip2 -c -d "$archive" - elif lzop -t "$archive" >/dev/null 2>&1 ; then - lzop -c -d "$archive" - # Ignoring other data, which may be garbage at the end of the file - fi - } | ( - if [ -n "$dir" ]; then - mkdir -p -- "$dir" - cd -- "$dir" - fi - cpio "$@" - ) -} - -# Read bytes out of a file, checking that they are valid hex digits -readhex() -{ - dd < "$1" bs=1 skip="$2" count="$3" 2> /dev/null | \ - LANG=C grep -E "^[0-9A-Fa-f]{$3}\$" -} - -# Check for a zero byte in a file -checkzero() -{ - dd < "$1" bs=1 skip="$2" count=1 2> /dev/null | \ - LANG=C grep -q -z '^$' -} - -# Split an initramfs into archives and run cpio/xcpio to extract them -splitinitramfs() -{ - initramfs="$1" - dir="$2" - shift 2 - - # Ensure this exists so we can use it unconditionally later - touch "$tempdir/main-uncomp.cpio" - - count=0 - start=0 - while true; do - # There may be prepended uncompressed archives. cpio - # won't tell us the true size of these so we have to - # parse the headers and padding ourselves. This is - # very roughly based on linux/lib/earlycpio.c - end=$start - while true; do - headoff=$end - magic="$(readhex "$initramfs" $end 6)" || break - test "$magic" = 070701 || test "$magic" = 070702 || break - namesize=$((0x$(readhex "$initramfs" $((end + 94)) 8))) - filesize=$((0x$(readhex "$initramfs" $((end + 54)) 8))) - nameoff=$((end + 110)) - end=$(((nameoff + namesize + 3) & ~3)) - end=$(((end + filesize + 3) & ~3)) - - # Check for EOF marker. Note that namesize - # includes a null terminator. - if [ $namesize = 11 ] \ - && name="$(dd if="$initramfs" bs=1 skip=$nameoff count=$((namesize - 1)) 2> /dev/null)" \ - && [ "$name" = 'TRAILER!!!' ]; then - # There might be more zero padding - # before the next archive, so read - # through all of it. - while checkzero "$initramfs" $end; do - end=$((end + 4)) - done - break - fi - done - - if [ $end -eq $start ]; then - break - fi - - # Check whether this should be treated as an "early" - # or "main" initramfs. Currently all filenames the - # kernel looks for in an early initramfs begin with - # kernel/ subdirectory, but we should never create - # this in the main initramfs. - if dd < "$initramfs" skip=$start count=$((end - start)) \ - iflag=skip_bytes,count_bytes 2> /dev/null | - cpio -i --list 2> /dev/null | - grep -q ^kernel/; then - # Extract to early, early2, ... subdirectories - count=$((count + 1)) - if [ $count -eq 1 ]; then - subdir=early - else - subdir=early$count - fi - dd < "$initramfs" skip=$start count=$((end - start)) \ - iflag=skip_bytes,count_bytes 2> /dev/null | - ( - if [ -n "$dir" ]; then - mkdir -p -- "$dir/$subdir" - cd -- "$dir/$subdir" - fi - cpio -i "$@" - ) - else - # Append to main-uncomp.cpio, excluding the - # trailer so cpio won't stop before the - # (de)compressed part. - dd < "$initramfs" skip=$start \ - count=$((headoff - start)) \ - iflag=skip_bytes,count_bytes \ - >> "$tempdir/main-uncomp.cpio" 2> /dev/null - fi - - start=$end - done - - # Split out final archive if necessary - if [ "$end" -gt 0 ]; then - subarchive="$tempdir/main-comp.cpio" - dd < "$initramfs" skip="$end" iflag=skip_bytes 2> /dev/null \ - > "$subarchive" - else - subarchive="$initramfs" - fi - - # If we found an early initramfs, extract main initramfs to - # main subdirectory. Otherwise don't use a subdirectory (for - # backward compatibility). - if [ "$count" -gt 0 ]; then - subdir=main - else - subdir=. - fi - - xcpio "$tempdir/main-uncomp.cpio" "$subarchive" \ - "${dir:+$dir/$subdir}" -i "$@" -} - -OPTIONS=$(getopt -o hv --long help,list,verbose -n "$0" -- "$@") || usage_error - -cpio_opts="--preserve-modification-time --no-absolute-filenames --quiet" -expected_args=2 -eval set -- "$OPTIONS" - -while true; do - case "$1" in - -h|--help) - usage - exit 0 - ;; - --list) - # For lsinitramfs - cpio_opts="${cpio_opts:+${cpio_opts} --list}" - expected_args=1 - shift - ;; - -v|--verbose) - cpio_opts="${cpio_opts:+${cpio_opts} --verbose}" - shift - ;; - --) - shift - break - ;; - *) - echo "Internal error!" >&2 - exit 1 - esac -done - -if [ $# -ne $expected_args ]; then - usage_error -fi - -tempdir="$(mktemp -d "${TMPDIR:-/var/tmp}/unmkinitramfs_XXXXXX")" -trap 'rm -rf "$tempdir"' EXIT - -# shellcheck disable=SC2086 -splitinitramfs "$1" "${2:-}" $cpio_opts diff --git a/unmkinitramfs.8 b/unmkinitramfs.8 index d0965af..a53ba4d 100644 --- a/unmkinitramfs.8 +++ b/unmkinitramfs.8 @@ -18,8 +18,8 @@ command extracts the content of a given initramfs image using and the appropriate decompressor command. The initramfs image may be a single compressed cpio archive, or the -concatenation of any number of uncompressed cpio archives followed by -a compressed cpio archive. +concatenation of any number of uncompressed cpio archives optionally +followed by a compressed cpio archive. When the initramfs image includes one or more "early initramfs" archives, that is uncompressed archives with microcode or other files @@ -51,8 +51,8 @@ Extract initramfs content of current running kernel: .SH BUGS .BR unmkinitramfs -does not support initramfs images with more or less than one -compressed cpio archive. +does not support initramfs images with more than one compressed cpio +archive. .SH AUTHOR The initramfs-tools are written by Maximilian Attems diff --git a/unmkinitramfs.c b/unmkinitramfs.c new file mode 100644 index 0000000..d69fd7a --- /dev/null +++ b/unmkinitramfs.c @@ -0,0 +1,698 @@ +/* unmkinitramfs: Unpack an initramfs */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* + * The "new" cpio header supported by the kernel. All fields after + * magic hold 32-bit values in hexadecimal. This is immediately + * followed by the name, then contents. + */ +struct cpio_new { + char c_magic[6]; + char c_ino[8]; + char c_mode[8]; + char c_uid[8]; + char c_gid[8]; + char c_nlink[8]; + char c_mtime[8]; + char c_filesize[8]; + char c_dev_maj[8]; + char c_dev_min[8]; + char c_rdev_maj[8]; + char c_rdev_min[8]; + char c_namesize[8]; + char c_chksum[8]; +} __attribute__((packed)); + +#define CPIO_NEW_MAGIC "070701" +#define CPIO_NEW_CRC_MAGIC "070702" +#define CPIO_MAGIC_LEN 6 + +/* Name of the last entry in a cpio archive */ +#define CPIO_NAME_TRAILER "TRAILER!!!" + +struct cpio_entry { + const char * name; + off_t start, end; +}; + +struct cpio_handle { + FILE * file; + const char * name; + off_t next_off; + char name_buf[PATH_MAX]; +}; + +struct cpio_proc { + int pid; + int pipe; +}; + +enum format { + FORMAT_CPIO_NEW, + FORMAT_GZIP, + FORMAT_BZIP2, + FORMAT_XZ, + FORMAT_LZO, + FORMAT_LZ4, + FORMAT_ZSTD, +}; + +#define MAX_MAGIC_LEN 12 + +struct magic_entry { + unsigned int magic_len; + unsigned char magic[MAX_MAGIC_LEN]; + enum format format; +}; + +#define MAGIC_ENTRY(magic, format) { sizeof(magic) - 1, magic, format } + +static const struct magic_entry magic_table[] = { + MAGIC_ENTRY(CPIO_NEW_MAGIC, FORMAT_CPIO_NEW), + MAGIC_ENTRY(CPIO_NEW_CRC_MAGIC, FORMAT_CPIO_NEW), + MAGIC_ENTRY("\x1f\x8b\x08", FORMAT_GZIP), + MAGIC_ENTRY("BZh", FORMAT_BZIP2), + MAGIC_ENTRY("\xfd""7zXZ\0", FORMAT_XZ), + MAGIC_ENTRY("\x89LZO\0\r\n\x1a\n", FORMAT_LZO), + /* lz4 "legacy" format, the only version that the kernel supports */ + MAGIC_ENTRY("\x02\x21\x4c\x18", FORMAT_LZ4), + MAGIC_ENTRY("\x28\xb5\x2f\xfd", FORMAT_ZSTD), + { 0 } +}; + +static const char *const decomp_table[][2] = { + [FORMAT_GZIP] = { "gzip", "-cd" }, + [FORMAT_BZIP2] = { "bzip2", "-cd" }, + [FORMAT_XZ] = { "xzcat" }, + [FORMAT_LZO] = { "lzop", "-cd" }, + [FORMAT_LZ4] = { "lz4cat" }, + [FORMAT_ZSTD] = { "zstd", "-cdq" }, +}; + +/* mkdir() but return success if name already exists as directory */ +static bool mkdir_allow_exist(const char *name, mode_t mode) +{ + struct stat st; + int orig_errno; + + if (mkdir(name, mode) == 0) + return true; + + orig_errno = errno; + if (orig_errno == EEXIST && stat(name, &st) == 0 && S_ISDIR(st.st_mode)) + return true; + + errno = orig_errno; + return false; +} + +/* write() with loop in case of partial writes */ +static bool write_all(int fd, const void *buf, size_t len) +{ + size_t pos; + ssize_t ret; + + pos = 0; + do { + ret = write(fd, (const char *)buf + pos, len - pos); + if (ret < 0) + return false; + pos += ret; + } while (pos < len); + + return true; +} + +/* + * Warn about failure of fread. This may be due to a file error + * reported in errno, or EOF which is not. + */ +static void warn_after_fread_failure(FILE *file, const char *name) +{ + if (ferror(file)) + warn("%s", name); + else + warnx("%s: unexpected EOF", name); +} + +/* + * Parse one of the hexadecimal fields. Don't use strtoul() because + * it requires null-termination. + */ +static bool cpio_parse_hex(const char *field, uint32_t *value_p) +{ + const char digits[] = "0123456789ABCDEF", *p; + uint32_t value = 0; + unsigned int i; + bool found_digit = false; + + /* Skip leading spaces */ + for (i = 0; i < 8 && field[i] == ' '; ++i) + ; + + /* Parse digits up to end of field or null */ + for (; i < 8 && field[i] != 0; ++i) { + p = strchr(digits, field[i]); + if (!p) + return false; + value = (value << 4) | (p - digits); + found_digit = true; + } + + *value_p = value; + return found_digit; +} + +/* Align offset of file contents or header */ +static off_t cpio_align(off_t off) +{ + return (off + 3) & ~3; +} + +static struct cpio_handle *cpio_open(FILE *file, const char *name) +{ + struct cpio_handle *cpio; + + cpio = calloc(1, sizeof(*cpio)); + if (!cpio) + return NULL; + + cpio->file = file; + cpio->name = name; + cpio->next_off = ftell(file); + return cpio; +} + +/* + * Read next cpio header and name. + * Return: + * -1 on error + * 0 if entry is trailer + * 1 if entry is anything else + */ +static int cpio_get_next(struct cpio_handle *cpio, struct cpio_entry *entry) +{ + struct cpio_new header; + uint32_t file_size, name_size; + + if (fseek(cpio->file, cpio->next_off, SEEK_SET) < 0 || + fread(&header, sizeof(header), 1, cpio->file) != 1) { + warn_after_fread_failure(cpio->file, cpio->name); + return -1; + } + + if ((memcmp(header.c_magic, CPIO_NEW_MAGIC, CPIO_MAGIC_LEN) != 0 && + memcmp(header.c_magic, CPIO_NEW_CRC_MAGIC, CPIO_MAGIC_LEN) != 0) || + !cpio_parse_hex(header.c_filesize, &file_size) || + !cpio_parse_hex(header.c_namesize, &name_size)) { + warnx("%s: cpio archive has invalid header", cpio->name); + return -1; + } + + entry->name = cpio->name_buf; + entry->start = cpio->next_off; + + /* Calculate offset of the next header */ + cpio->next_off = cpio_align( + cpio_align(cpio->next_off + sizeof(header) + name_size) + + file_size); + entry->end = cpio->next_off; + + if (name_size > sizeof(cpio->name_buf)) { + warnx("%s: cpio member name is too long", cpio->name); + return -1; + } + + if (fread(cpio->name_buf, name_size, 1, cpio->file) != 1) { + warn_after_fread_failure(cpio->file, cpio->name); + return -1; + } + + if (name_size == 0 || cpio->name_buf[name_size - 1] != 0) { + warnx("%s: cpio member name is invalid", cpio->name); + return -1; + } + + return strcmp(entry->name, CPIO_NAME_TRAILER) != 0; +} + +static void cpio_close(struct cpio_handle *cpio) +{ + free(cpio); +} + +static bool detect_early_initramfs(FILE *in_file, const char *in_filename) +{ + struct cpio_handle *cpio; + struct cpio_entry entry; + bool ret = false; + off_t start = ftell(in_file); + + cpio = cpio_open(in_file, in_filename); + if (!cpio) + return false; + + while (cpio_get_next(cpio, &entry) > 0) { + if (strncmp(entry.name, "kernel/", 7) == 0) { + ret = true; + break; + } + } + + cpio_close(cpio); + + fseek(in_file, start, SEEK_SET); + return ret; +} + +static bool copy_to_pipe(FILE *in_file, const char *in_filename, + off_t start, off_t end, int out_pipe) +{ + char buf[0x10000]; + off_t in_pos; + size_t want_len, read_len; + + /* Set input position */ + fseek(in_file, start, SEEK_SET); + in_pos = start; + + while (in_pos < end) { + /* How much do we want to copy? */ + want_len = sizeof(buf); + if ((ssize_t)want_len > end - in_pos) + want_len = end - in_pos; + + /* Read to buffer; update input position */ + read_len = fread(buf, 1, want_len, in_file); + if (!read_len) { + warn_after_fread_failure(in_file, in_filename); + return false; + } + in_pos += read_len; + + /* Write to pipe */ + if (!write_all(out_pipe, buf, read_len)) { + warn("pipe write"); + return false; + } + } + + return true; +} + +static bool handle_uncompressed(FILE *in_file, const char *in_filename, + int out_pipe) +{ + struct cpio_handle *cpio; + struct cpio_entry entry; + uint32_t pad; + int ret; + + cpio = cpio_open(in_file, in_filename); + if (!cpio) + return false; + + while ((ret = cpio_get_next(cpio, &entry)) > 0) { + if (!copy_to_pipe(in_file, in_filename, entry.start, entry.end, + out_pipe)) { + ret = -1; + break; + } + } + + cpio_close(cpio); + + if (ret < 0) + return false; + + /* Skip trailer and any zero padidng */ + fseek(in_file, entry.end, SEEK_SET); + while (fread(&pad, sizeof(pad), 1, in_file)) { + if (pad != 0) { + fseek(in_file, -sizeof(pad), SEEK_CUR); + break; + } + } + + return true; +} + +static bool handle_compressed(FILE *in_file, enum format format, int out_pipe) +{ + const char *const *argv = decomp_table[format]; + int in_fd = fileno(in_file); + off_t in_pos = ftell(in_file); + int pid, wstatus; + + pid = fork(); + if (pid < 0) + return false; + + /* Child */ + if (pid == 0) { + /* + * Make in_file stdin. Reset the position of the file + * descriptor because stdio will have read-ahead from + * the position it reported. + */ + dup2(in_fd, 0); + close(in_fd); + lseek(0, in_pos, SEEK_SET); + + /* Make out_pipe stdout */ + dup2(out_pipe, 1); + close(out_pipe); + + execlp(argv[0], argv[0], argv[1], NULL); + _exit(127); + } + + /* Parent: wait for child */ + if (waitpid(pid, &wstatus, 0) != pid || + !WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { + warnx("%s failed", argv[0]); + return false; + } + return true; +} + +static bool write_trailer(int out_pipe) +{ + struct { + struct cpio_new header; + char name[sizeof(CPIO_NAME_TRAILER)]; + char pad[-(sizeof(struct cpio_new) + sizeof(CPIO_NAME_TRAILER)) + & 3]; + } __attribute__((packed)) trailer; + char name_size[8 + 1]; + + static_assert((sizeof(trailer) & 3) == 0, "pad miscalculated"); + + memset(&trailer.header, '0', sizeof(trailer.header)); + memcpy(trailer.header.c_magic, CPIO_NEW_MAGIC, CPIO_MAGIC_LEN); + sprintf(name_size, "%08zX", sizeof(CPIO_NAME_TRAILER)); + memcpy(trailer.header.c_namesize, name_size, + sizeof(trailer.header.c_namesize)); + + strcpy(trailer.name, CPIO_NAME_TRAILER); + + memset(&trailer.pad, 0, sizeof(trailer.pad)); + + if (!write_all(out_pipe, &trailer, sizeof(trailer))) { + warn("pipe write"); + return false; + } + + return true; +} + +static bool spawn_cpio(int optc, const char **optv, struct cpio_proc *proc) +{ + const char *argv[10]; + int pipe_fds[2], pid; + size_t argc; + + /* Combine base cpio command with extra options */ + argc = 0; + argv[argc++] = "cpio"; + argv[argc++] = "-i"; + argv[argc++] = "--preserve-modification-time"; + argv[argc++] = "--no-absolute-filenames"; + argv[argc++] = "--quiet"; + assert(argc + optc < sizeof(argv) / sizeof(argv[0])); + while (optc--) + argv[argc++] = *optv++; + argv[argc] = NULL; + + if (pipe(pipe_fds)) { + warn("pipe"); + return false; + } + pid = fork(); + if (pid < 0) { + warn("fork"); + return false; + } + + /* Child */ + if (pid == 0) { + /* + * Close write end of the pipe; make the read end + * stdout. + */ + close(pipe_fds[1]); + dup2(pipe_fds[0], 0); + close(pipe_fds[0]); + + execvp("cpio", (char **)argv); + _exit(127); + } + + /* + * Parent: close read end of the pipe; return child pid and + * write end of pipe. + */ + close(pipe_fds[0]); + proc->pid = pid; + proc->pipe = pipe_fds[1]; + return true; +} + +static bool end_cpio(const struct cpio_proc *proc, bool ok) +{ + int wstatus; + + close(proc->pipe); + + if (ok) { + if (waitpid(proc->pid, &wstatus, 0) != proc->pid || + !WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) { + warnx("cpio failed"); + return false; + } + } else { + kill(proc->pid, SIGTERM); + } + + return true; +} + +static const struct option long_opts[] = { + { "help", no_argument, NULL, 'h' }, + { "list", no_argument, NULL, 'l' }, + { "verbose", no_argument, NULL, 'v' }, + { NULL } +}; + +static void usage(FILE *stream) +{ + fprintf(stream, "\ +\n\ +Usage: unmkinitramfs [-v|--verbose] INITRAMFS-FILE DIRECTORY\n\ +\n\ +Options:\n\ + -v | --verbose Display verbose messages about extraction\n\ +\n\ +See unmkinitramfs(8) for further details.\n\ +\n" + ); +} + +int main(int argc, char **argv) +{ + int opt; + bool do_list = false; + bool verbose = false; + const char *in_filename; + FILE *in_file; + const char *out_dirname = NULL; + char *out_subdirname = NULL; + const char *cpio_optv[3]; + int cpio_optc; + struct cpio_proc cpio_proc = { 0 }; + unsigned int early_count = 0; + bool ok; + + /* Parse options */ + opterr = 0; + while ((opt = getopt_long(argc, argv, "hv", long_opts, NULL)) >= 0) { + switch (opt) { + case '?': + usage(stderr); + return 2; + case 'h': + usage(stdout); + return 0; + case 'l': + do_list = true; + break; + case 'v': + verbose = true; + break; + } + } + + /* Check number of non-option arguments */ + if (argc - optind != (do_list ? 1 : 2)) { + usage(stderr); + return 2; + } + + /* Set up input file and output directory */ + in_filename = argv[optind]; + in_file = fopen(in_filename, "rb"); + if (!in_file) + err(1, "%s", in_filename); + if (!do_list) { + out_dirname = argv[optind + 1]; + if (!mkdir_allow_exist(out_dirname, 0777)) + err(1, "%s", out_dirname); + out_subdirname = malloc(strlen(out_dirname) + 20); + if (!out_subdirname) + err(1, "malloc"); + } + + /* Set up extra options for cpio */ + cpio_optc = 0; + if (do_list) { + cpio_optv[cpio_optc++] = "--list"; + } else { + cpio_optv[cpio_optc++] = "-D"; + cpio_optv[cpio_optc++] = out_subdirname; + } + if (verbose) + cpio_optv[cpio_optc++] = "-v"; + + /* Iterate over archives within the initramfs */ + for (;;) { + unsigned char magic_buf[MAX_MAGIC_LEN]; + size_t read_len; + const struct magic_entry *me; + + /* Peek at first bytes of next archive; handle EOF */ + read_len = fread(magic_buf, 1, sizeof(magic_buf), in_file); + if (read_len == 0) { + /* + * EOF with no compresed archive. Add back a + * trailer to keep cpio happy. + */ + if (ferror(in_file)) { + warn("%s", in_filename); + ok = false; + } + if (cpio_proc.pid && !write_trailer(cpio_proc.pipe)) + ok = false; + break; + } + fseek(in_file, -(long)read_len, SEEK_CUR); + + /* Identify format */ + for (me = magic_table; me->magic_len; ++me) + if (read_len >= me->magic_len && + memcmp(magic_buf, me->magic, me->magic_len) == 0) + break; + if (me->magic_len == 0) { + warnx("%s: unrecognised compression or corrupted file", + in_filename); + ok = false; + break; + } + + /* + * Extract the archive to an "early" or "early" + * subdirectory if: + * - We have not already started the main cpio process + * - We are not listing + * - This looks like an early initramfs (uncompressed + * and contains files under kernel/) + */ + if (!cpio_proc.pid && !do_list && + me->format == FORMAT_CPIO_NEW && + detect_early_initramfs(in_file, in_filename)) { + struct cpio_proc early_cpio_proc; + + if (++early_count == 1) + sprintf(out_subdirname, "%s/early", + out_dirname); + else + sprintf(out_subdirname, "%s/early%u", + out_dirname, early_count); + if (!mkdir_allow_exist(out_subdirname, 0777)) { + warn("%s", out_subdirname); + ok = false; + break; + } + if (!spawn_cpio(cpio_optc, cpio_optv, + &early_cpio_proc)) { + ok = false; + break; + } + ok = handle_uncompressed(in_file, in_filename, + early_cpio_proc.pipe) + && write_trailer(early_cpio_proc.pipe); + if (!end_cpio(&early_cpio_proc, ok)) + ok = false; + if (!ok) + break; + } else { + /* + * Otherwise, extract to either the base + * output directory or a "main" subdirectory, + * depending on whether we already created + * subdirectories. + */ + if (!cpio_proc.pid) { + if (do_list) { + ; + } else if (early_count) { + sprintf(out_subdirname, "%s/main", + out_dirname); + if (!mkdir_allow_exist(out_subdirname, + 0777)) { + warn("%s", out_subdirname); + ok = false; + break; + } + } else { + strcpy(out_subdirname, out_dirname); + } + if (!spawn_cpio(cpio_optc, cpio_optv, + &cpio_proc)) { + ok = false; + break; + } + } + if (me->format == FORMAT_CPIO_NEW) { + ok = handle_uncompressed(in_file, in_filename, + cpio_proc.pipe); + if (!ok) + break; + } else { + ok = handle_compressed(in_file, me->format, + cpio_proc.pipe); + break; + } + } + } + + fclose(in_file); + + if (cpio_proc.pid && !end_cpio(&cpio_proc, ok)) + ok = false; + + return !ok; +} From 9e37d8b38da71e7a02c9e747fffb833266d9e6e4 Mon Sep 17 00:00:00 2001 From: Ben Hutchings Date: Mon, 7 Apr 2025 01:47:41 +0200 Subject: [PATCH 2/3] Move unmkinitramfs to new initramfs-tools-bin package Since unmkinitramfs is now native code, it needs to be built as part of an arch:any package. Introduce initramfs-tools-bin and move it there. CI: Remove the variables that disable arch:any builds. Signed-off-by: Ben Hutchings --- debian/.gitignore | 1 + debian/control | 10 ++++++++++ debian/initramfs-tools-bin.install | 1 + debian/initramfs-tools-bin.manpages | 1 + debian/initramfs-tools-core.install | 1 - debian/initramfs-tools-core.manpages | 1 - debian/rules | 1 + debian/salsa-ci.yml | 5 ----- 8 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 debian/initramfs-tools-bin.install create mode 100644 debian/initramfs-tools-bin.manpages diff --git a/debian/.gitignore b/debian/.gitignore index bc79963..bf015a2 100644 --- a/debian/.gitignore +++ b/debian/.gitignore @@ -4,4 +4,5 @@ /.debhelper /files /initramfs-tools/ +/initramfs-tools-bin/ /initramfs-tools-core/ diff --git a/debian/control b/debian/control index fe3553e..433e90a 100644 --- a/debian/control +++ b/debian/control @@ -30,6 +30,7 @@ Recommends: zstd, ${busybox:Recommends} Depends: coreutils (>= 8.24), cpio (>= 2.12), dracut-install, + initramfs-tools-bin (>= ${source:Version}), klibc-utils (>= 2.0.4-8~), kmod, logsave | e2fsprogs (<< 1.45.3-1~), @@ -42,3 +43,12 @@ Description: generic modular initramfs generator (core tools) create a bootable initramfs for a Linux kernel. The initramfs should be loaded along with the kernel and is then responsible for mounting the root filesystem and starting the main init system. + +Package: initramfs-tools-bin +Architecture: any +Multi-Arch: foreign +Depends: ${misc:Depends}, ${shlibs:Depends} +Replaces: initramfs-tools-core (<< 0.148~) +Description: generic modular initramfs generator (binary tools) + This package contains the unmkinitramfs program that can be used to + unpack an initramfs image. diff --git a/debian/initramfs-tools-bin.install b/debian/initramfs-tools-bin.install new file mode 100644 index 0000000..7b059a1 --- /dev/null +++ b/debian/initramfs-tools-bin.install @@ -0,0 +1 @@ +unmkinitramfs usr/bin diff --git a/debian/initramfs-tools-bin.manpages b/debian/initramfs-tools-bin.manpages new file mode 100644 index 0000000..24305c3 --- /dev/null +++ b/debian/initramfs-tools-bin.manpages @@ -0,0 +1 @@ +unmkinitramfs.8 diff --git a/debian/initramfs-tools-core.install b/debian/initramfs-tools-core.install index 0e433d3..119d8bb 100644 --- a/debian/initramfs-tools-core.install +++ b/debian/initramfs-tools-core.install @@ -5,4 +5,3 @@ hooks usr/share/initramfs-tools init usr/share/initramfs-tools lsinitramfs usr/bin scripts usr/share/initramfs-tools -unmkinitramfs usr/bin diff --git a/debian/initramfs-tools-core.manpages b/debian/initramfs-tools-core.manpages index 8db13b9..d03c678 100644 --- a/debian/initramfs-tools-core.manpages +++ b/debian/initramfs-tools-core.manpages @@ -2,4 +2,3 @@ initramfs-tools.7 initramfs.conf.5 lsinitramfs.8 mkinitramfs.8 -unmkinitramfs.8 diff --git a/debian/rules b/debian/rules index 3775a55..0cf8a09 100755 --- a/debian/rules +++ b/debian/rules @@ -14,6 +14,7 @@ override_dh_gencontrol: dh_gencontrol override_dh_install: + install -d debian/initramfs-tools-core/usr/sbin sed -e 's,@BUSYBOX_PACKAGES@,$(wordlist 2,100,$(BUSYBOX_PACKAGES:%=or %)),;s,@VERSION@,$(DEB_VERSION),' \ mkinitramfs > debian/initramfs-tools-core/usr/sbin/mkinitramfs chmod 755 debian/initramfs-tools-core/usr/sbin/mkinitramfs diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml index 3286aaf..23afe3e 100644 --- a/debian/salsa-ci.yml +++ b/debian/salsa-ci.yml @@ -4,11 +4,6 @@ include: variables: RELEASE: 'unstable' - # We only build arch:all packages - SALSA_CI_DISABLE_BLHC: 'true' - SALSA_CI_DISABLE_BUILD_PACKAGE_I386: 'true' - SALSA_CI_DISABLE_BUILD_PACKAGE_ANY: 'true' - SALSA_CI_DISABLE_CROSSBUILD_ARM64: 'true' shellcheck: stage: test From 49f3bbadcebb4eb3436341ca00727a183d100a35 Mon Sep 17 00:00:00 2001 From: Ben Hutchings Date: Tue, 22 Apr 2025 02:29:40 +0200 Subject: [PATCH 3/3] test: Add autopkgtest case for unmkinitramfs Add a test that exercises unmkinitramfs with: - 0-2 early cpio archives - 0-2 main cpio archives - All supported compressors, including none, for the last main archive and verifies that the output is as expected. Signed-off-by: Ben Hutchings --- debian/tests/control | 4 ++ debian/tests/unmkinitramfs | 127 +++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100755 debian/tests/unmkinitramfs diff --git a/debian/tests/control b/debian/tests/control index c69689e..ab81f98 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -2,6 +2,10 @@ Tests: copy-file Restrictions: needs-root, breaks-testbed, skip-not-installable, superficial, allow-stderr Depends: linux-image-generic, zstd, @ +Tests: unmkinitramfs +Restrictions: superficial +Depends: bzip2, xz-utils, lzop, lz4, zstd, @ + Tests: qemu-klibc Architecture: amd64 armhf s390x Restrictions: needs-root, breaks-testbed diff --git a/debian/tests/unmkinitramfs b/debian/tests/unmkinitramfs new file mode 100755 index 0000000..af042af --- /dev/null +++ b/debian/tests/unmkinitramfs @@ -0,0 +1,127 @@ +#!/bin/sh +set -eu + +make_one_archive() { + local type="$1" + local i="$2" + local dir_name + local j + + case "$type" in + early) + dir_name=kernel + ;; + main) + dir_name="dir$i" + ;; + *) + echo >&2 "E: Bad archive type $type" + exit 1 + ;; + esac + + mkdir -p "$AUTOPKGTEST_TMP/input/$type$i/$dir_name" + for j in $(seq 0 9); do + size=$((j * 987 + i * 654 + 321)) + dd if=/dev/urandom of="$AUTOPKGTEST_TMP/input/$type$i/$dir_name/file$j" \ + bs="$size" count=1 status=none + done + ( + cd "$AUTOPKGTEST_TMP/input/$type$i" + find . | cpio -H newc -o --quiet > ../"$type$i.cpio" + ) +} + +# Create input files and archives +mkdir "$AUTOPKGTEST_TMP/input" +for in_type in early main; do + for i in 0 1 2; do + make_one_archive "$in_type" "$i" + done +done + +# Dummy compressor; wrapper for cat that ignores its argument (-c) +no_compressor() { + # shellcheck disable=SC2317 + cat +} + +rc=0 + +# Test up to 2 early and 2 main cpio archives, and all supported +# compressors (including none) for the last main archive +for n_early in 0 1 2; do + for n_main in 0 1 2; do + # There must be at least 1 archive + if [ $((n_early + n_main)) -eq 0 ]; then + continue + fi + + for compressor in no_compressor gzip bzip2 xz lzop 'lz4 -l' zstd; do + # If last archive is early, it can't be compressed + if [ $n_main -eq 0 ] && [ "$compressor" != no_compressor ]; then + continue + fi + + echo "I: Testing $n_early early + $n_main main archive(s) with $compressor" + + # Construct initramfs image + : > "$AUTOPKGTEST_TMP/initrd.img" + for i in $(seq 0 $((n_early - 1))); do + cat "$AUTOPKGTEST_TMP/input/early$i.cpio" \ + >> "$AUTOPKGTEST_TMP/initrd.img" + done + for i in $(seq 0 $((n_main - 2))); do + cat "$AUTOPKGTEST_TMP/input/main$i.cpio" \ + >> "$AUTOPKGTEST_TMP/initrd.img" + done + if [ $n_main -ge 1 ]; then + $compressor -c < "$AUTOPKGTEST_TMP/input/main$((n_main - 1)).cpio" \ + >> "$AUTOPKGTEST_TMP/initrd.img" + fi + + # Unpack it + rm -rf "$AUTOPKGTEST_TMP/output" + unmkinitramfs "$AUTOPKGTEST_TMP/initrd.img" \ + "$AUTOPKGTEST_TMP/output" \ + || { + echo >&2 'E: unmkinitramfs failed' + rc=1 + continue + } + + # Construct what we expect output to look like + rm -rf "$AUTOPKGTEST_TMP/reference" + mkdir "$AUTOPKGTEST_TMP/reference" + for i in $(seq 0 $((n_early - 1))); do + if [ "$i" -eq 0 ]; then + dir_name=early + else + dir_name=early$((i + 1)) + fi + ln -s ../input/"early$i" \ + "$AUTOPKGTEST_TMP/reference/$dir_name" + done + for i in $(seq 0 $((n_main - 1))); do + if [ $n_early = 0 ]; then + ln -s ../input/"main$i/dir$i" \ + "$AUTOPKGTEST_TMP/reference/dir$i" + else + mkdir -p "$AUTOPKGTEST_TMP/reference/main" + ln -s ../../input/"main$i/dir$i" \ + "$AUTOPKGTEST_TMP/reference/main/dir$i" + fi + done + + # Compare reference and output + diff -r "$AUTOPKGTEST_TMP/reference" \ + "$AUTOPKGTEST_TMP/output" \ + || { + echo >&2 'E: Output files do not match input' + rc=1 + } + done + done +done + +exit $rc