mirror of
https://salsa.debian.org/kernel-team/initramfs-tools.git
synced 2026-01-26 15:39:08 +00:00
unmkinitramfs used to assume that any uncompressed cpio archives at
the beginning of an initramfs image belonged to the early initramfs
and only a final compressed archive belonged to the the main
initramfs. If it found any uncompressed archives it extracted them
into "early", "early2", etc. subdirectories and the compressed archive
into a "main" subdirectory.
The reason for using a separate subdirectory for each archive is to
guard against a symlink traversal attack from an untrusted initramfs,
e.g. the extraction of "link" as a symlink to "/etc" followed by
"link/shadow" which overwrites "/etc/shadow". cpio itself protects
against this if we extract a single archive into an empty directory,
but not if we extract multiple archives successively into the same
directory.
mkinitramfs now splits the main initramfs files between uncompressed
and compressed archives. unmkinitramfs was changed to use
subdirectory names "cpio1", "cpio2", etc. since the previous
distinction was no longer valid.
Several packages that integrate with initramfs-tools have autopkgtests
that run unmkinitramfs and were broken by this new behaviour. It's
also quite possible that there are also user scripts that would also
be broken.
Therefore, try to restore the old behaviour in unmkinitramfs:
1. Distinguish whether uncompressed archives are "early" or "main"
by checking for a kernel/ subdirectory. Currently all filenames
the kernel looks for in an early initramfs are in this
subdirectory, but we should never create this in the main
initramfs.
2. Extract early archives as before, but concatenate any "main"
uncompressed archives to a temporary file. Exclude the trailer
from them so that cpio won't stop early when reading them.
3. Pass both the "main" uncompressed archives and the compressed
archive to xcpio, and make it concatenate the uncompressed and
decompressed archives as input to cpio.
The concatenation in steps 2 and 3 is done to preserve the protection
against symlink traversal.
Fixes: 81fd41f72dd8 ("Put compressed kernel modules and firmware in an uncompressed cpio")
Fixes: cb0618177b26 ("unmkinitramfs: use directory names 'cpio1', 'cpio2', etc.")
Closes: #1100008
Signed-off-by: Ben Hutchings <benh@debian.org>
220 lines
4.9 KiB
Bash
Executable File
220 lines
4.9 KiB
Bash
Executable File
#!/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
|