Merge branch 'unmkinitramfs-c' into 'debian/latest'

unmkinitramfs: Rewrite in C to make it acceptably fast

See merge request kernel-team/initramfs-tools!168
This commit is contained in:
Ben Hutchings 2025-04-23 00:28:22 +00:00
commit c7fc4a424f
15 changed files with 855 additions and 230 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*~ *~
.#* .#*
/unmkinitramfs

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
CFLAGS += -Wall -Wextra -O2
all: unmkinitramfs
clean:
rm -f unmkinitramfs
.PHONY: all clean

1
debian/.gitignore vendored
View File

@ -4,4 +4,5 @@
/.debhelper /.debhelper
/files /files
/initramfs-tools/ /initramfs-tools/
/initramfs-tools-bin/
/initramfs-tools-core/ /initramfs-tools-core/

10
debian/control vendored
View File

@ -30,6 +30,7 @@ Recommends: zstd, ${busybox:Recommends}
Depends: coreutils (>= 8.24), Depends: coreutils (>= 8.24),
cpio (>= 2.12), cpio (>= 2.12),
dracut-install, dracut-install,
initramfs-tools-bin (>= ${source:Version}),
klibc-utils (>= 2.0.4-8~), klibc-utils (>= 2.0.4-8~),
kmod, kmod,
logsave | e2fsprogs (<< 1.45.3-1~), 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 create a bootable initramfs for a Linux kernel. The initramfs should
be loaded along with the kernel and is then responsible for mounting be loaded along with the kernel and is then responsible for mounting
the root filesystem and starting the main init system. 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.

1
debian/initramfs-tools-bin.install vendored Normal file
View File

@ -0,0 +1 @@
unmkinitramfs usr/bin

1
debian/initramfs-tools-bin.manpages vendored Normal file
View File

@ -0,0 +1 @@
unmkinitramfs.8

View File

@ -5,4 +5,3 @@ hooks usr/share/initramfs-tools
init usr/share/initramfs-tools init usr/share/initramfs-tools
lsinitramfs usr/bin lsinitramfs usr/bin
scripts usr/share/initramfs-tools scripts usr/share/initramfs-tools
unmkinitramfs usr/bin

View File

@ -2,4 +2,3 @@ initramfs-tools.7
initramfs.conf.5 initramfs.conf.5
lsinitramfs.8 lsinitramfs.8
mkinitramfs.8 mkinitramfs.8
unmkinitramfs.8

1
debian/rules vendored
View File

@ -14,6 +14,7 @@ override_dh_gencontrol:
dh_gencontrol dh_gencontrol
override_dh_install: 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),' \ sed -e 's,@BUSYBOX_PACKAGES@,$(wordlist 2,100,$(BUSYBOX_PACKAGES:%=or %)),;s,@VERSION@,$(DEB_VERSION),' \
mkinitramfs > debian/initramfs-tools-core/usr/sbin/mkinitramfs mkinitramfs > debian/initramfs-tools-core/usr/sbin/mkinitramfs
chmod 755 debian/initramfs-tools-core/usr/sbin/mkinitramfs chmod 755 debian/initramfs-tools-core/usr/sbin/mkinitramfs

5
debian/salsa-ci.yml vendored
View File

@ -4,11 +4,6 @@ include:
variables: variables:
RELEASE: 'unstable' 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: shellcheck:
stage: test stage: test

View File

@ -2,6 +2,10 @@ Tests: copy-file
Restrictions: needs-root, breaks-testbed, skip-not-installable, superficial, allow-stderr Restrictions: needs-root, breaks-testbed, skip-not-installable, superficial, allow-stderr
Depends: linux-image-generic, zstd, @ Depends: linux-image-generic, zstd, @
Tests: unmkinitramfs
Restrictions: superficial
Depends: bzip2, xz-utils, lzop, lz4, zstd, @
Tests: qemu-klibc Tests: qemu-klibc
Architecture: amd64 armhf s390x Architecture: amd64 armhf s390x
Restrictions: needs-root, breaks-testbed Restrictions: needs-root, breaks-testbed

127
debian/tests/unmkinitramfs vendored Executable file
View File

@ -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

View File

@ -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

View File

@ -18,8 +18,8 @@ command extracts the content of a given initramfs image using
and the appropriate decompressor command. and the appropriate decompressor command.
The initramfs image may be a single compressed cpio archive, or the The initramfs image may be a single compressed cpio archive, or the
concatenation of any number of uncompressed cpio archives followed by concatenation of any number of uncompressed cpio archives optionally
a compressed cpio archive. followed by a compressed cpio archive.
When the initramfs image includes one or more "early initramfs" When the initramfs image includes one or more "early initramfs"
archives, that is uncompressed archives with microcode or other files archives, that is uncompressed archives with microcode or other files
@ -51,8 +51,8 @@ Extract initramfs content of current running kernel:
.SH BUGS .SH BUGS
.BR unmkinitramfs .BR unmkinitramfs
does not support initramfs images with more or less than one does not support initramfs images with more than one compressed cpio
compressed cpio archive. archive.
.SH AUTHOR .SH AUTHOR
The initramfs-tools are written by Maximilian Attems <maks@debian.org> The initramfs-tools are written by Maximilian Attems <maks@debian.org>

698
unmkinitramfs.c Normal file
View File

@ -0,0 +1,698 @@
/* unmkinitramfs: Unpack an initramfs */
#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#include <getopt.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
/*
* 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<n>"
* 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;
}