kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

run.sh (11077B)


      1 #!/bin/sh
      2 # Driver-level checks for `kit cpio` (SVR4 newc initramfs archives).
      3 #
      4 # Self-checking (no golden files): create -> list -> extract round-trips,
      5 # determinism, the newc shape (070701 magic, TRAILER!!! record, 512-byte tail
      6 # pad), gzip/lz4 compression with read auto-detect, archive concatenation, the
      7 # 070702 CRC variant, and the security / usage negatives are asserted inline
      8 # via the shared kit_* verbs (ok/run_ok/run_fail/contains/same_file).
      9 #
     10 # Set KIT_CPIO_TEST_HOST=1 to additionally cross-check against the host `cpio`.
     11 
     12 set -u
     13 
     14 script_dir=$(cd "$(dirname "$0")" && pwd)
     15 repo_root=$(cd "$script_dir/../.." && pwd)
     16 
     17 KIT="${KIT:-$repo_root/build/kit}"
     18 
     19 if [ ! -x "$KIT" ]; then
     20     echo "cpio: kit binary not found at $KIT" >&2
     21     exit 2
     22 fi
     23 
     24 work=$(mktemp -d "${TMPDIR:-/tmp}/kit-cpio-test.XXXXXX")
     25 trap 'rm -rf "$work"' EXIT
     26 
     27 KIT_KIT_DIR="$repo_root/test/lib"
     28 . "$repo_root/test/lib/kit_sh_kit.sh"
     29 kit_report_init
     30 
     31 # Assert the documented 0/1/2 command classification, preserving stdout and
     32 # stderr beside the other lane artifacts when a case fails.
     33 expect_status() {
     34     es_name=$1
     35     es_want=$2
     36     shift 2
     37     "$@" > "$work/$es_name.out" 2> "$work/$es_name.err"
     38     es_got=$?
     39     if [ "$es_got" -eq "$es_want" ]; then
     40         ok "$es_name"
     41     else
     42         {
     43             printf 'status=%s (expected %s)\n' "$es_got" "$es_want"
     44             sed 's/^/stdout: /' "$work/$es_name.out"
     45             sed 's/^/stderr: /' "$work/$es_name.err"
     46         } > "$work/$es_name.diag"
     47         not_ok "$es_name" "$work/$es_name.diag"
     48     fi
     49 }
     50 
     51 cpio_hdr() { printf '%s' "$1"; shift; for f in "$@"; do printf '%08X' "$f"; done; }
     52 
     53 # ---- fixture: a small tree exercising every supported member type -----------
     54 # regular file, executable regular file, zero-byte file, binary file, a nested
     55 # subdirectory, an empty directory, and a symlink.
     56 mkdir -p "$work/payload/root/sub" "$work/payload/root/emptydir"
     57 printf 'hello cpio\n'              > "$work/payload/root/a.txt"
     58 printf '#!/bin/sh\necho hi\n'      > "$work/payload/root/run.sh"
     59 chmod +x "$work/payload/root/run.sh"
     60 : > "$work/payload/root/empty.txt"
     61 printf '\000\001\002\377\376zz'    > "$work/payload/root/sub/b.bin"
     62 ln -s a.txt "$work/payload/root/link"
     63 
     64 create() { ( cd "$work/payload" && "$KIT" cpio "$@" ); }
     65 
     66 # ---- create + list ---------------------------------------------------------
     67 run_ok cpio-create create -o -F "$work/out.cpio" root
     68 "$KIT" cpio -t -F "$work/out.cpio" > "$work/list.txt" 2> "$work/list.err"
     69 for name in root root/a.txt root/run.sh root/empty.txt root/emptydir \
     70             root/link root/sub root/sub/b.bin; do
     71     contains "cpio-list-$name" "$work/list.txt" "$name"
     72 done
     73 
     74 # Deterministic ordering: directory entries precede their children, sorted.
     75 printf 'root\nroot/a.txt\nroot/empty.txt\nroot/emptydir\nroot/link\nroot/run.sh\nroot/sub\nroot/sub/b.bin\n' \
     76     > "$work/list.want"
     77 same_file cpio-list-sorted "$work/list.want" "$work/list.txt"
     78 
     79 # ---- newc shape ------------------------------------------------------------
     80 magic=$(dd if="$work/out.cpio" bs=1 count=6 2>/dev/null)
     81 if [ "$magic" = "070701" ]; then ok cpio-magic-newc
     82 else echo "magic=$magic" > "$work/magic.diag"; not_ok cpio-magic-newc "$work/magic.diag"; fi
     83 
     84 contains cpio-has-trailer "$work/out.cpio" "TRAILER!!!"
     85 
     86 sz=$(wc -c < "$work/out.cpio")
     87 if [ "$((sz % 512))" -eq 0 ]; then ok cpio-512-pad
     88 else echo "size=$sz" > "$work/pad.diag"; not_ok cpio-512-pad "$work/pad.diag"; fi
     89 
     90 # ---- determinism -----------------------------------------------------------
     91 run_ok cpio-create-again create -o -F "$work/out2.cpio" root
     92 same_file cpio-deterministic "$work/out.cpio" "$work/out2.cpio"
     93 
     94 # ---- extract + content compare ---------------------------------------------
     95 mkdir -p "$work/ex"
     96 ( cd "$work/ex" && "$KIT" cpio -i -F "$work/out.cpio" ) 2> "$work/ex.err"
     97 same_file cpio-extract-a    "$work/payload/root/a.txt"     "$work/ex/root/a.txt"
     98 same_file cpio-extract-bin  "$work/payload/root/sub/b.bin" "$work/ex/root/sub/b.bin"
     99 same_file cpio-extract-empty "$work/payload/root/empty.txt" "$work/ex/root/empty.txt"
    100 assert_file_exists cpio-extract-run "$work/ex/root/run.sh"
    101 is_executable cpio-extract-exec-bit "$work/ex/root/run.sh"
    102 if [ -d "$work/ex/root/emptydir" ]; then ok cpio-extract-emptydir
    103 else not_ok cpio-extract-emptydir; fi
    104 if [ -L "$work/ex/root/link" ] && [ "$(readlink "$work/ex/root/link")" = a.txt ]; then
    105     ok cpio-extract-symlink
    106 else not_ok cpio-extract-symlink; fi
    107 
    108 # ---- gzip / lz4 round-trips (read auto-detects the codec) ------------------
    109 for z in "-z gzip" "--lz4 lz4"; do
    110     set -- $z
    111     flag=$1; tag=$2
    112     run_ok "cpio-create-$tag" create -o "$flag" -F "$work/out.$tag" root
    113     "$KIT" cpio -t -F "$work/out.$tag" > "$work/list.$tag" 2> "$work/list.$tag.err"
    114     same_file "cpio-$tag-list-matches" "$work/list.txt" "$work/list.$tag"
    115     mkdir -p "$work/ex-$tag"
    116     ( cd "$work/ex-$tag" && "$KIT" cpio -i -F "$work/out.$tag" ) 2>/dev/null
    117     same_file "cpio-$tag-extract" "$work/payload/root/sub/b.bin" \
    118         "$work/ex-$tag/root/sub/b.bin"
    119 done
    120 
    121 # ---- 070702 CRC variant ----------------------------------------------------
    122 run_ok cpio-create-crc create -o -H crc -F "$work/out.crc" root
    123 crcmagic=$(dd if="$work/out.crc" bs=1 count=6 2>/dev/null)
    124 if [ "$crcmagic" = "070702" ]; then ok cpio-magic-crc
    125 else echo "magic=$crcmagic" > "$work/crc.diag"; not_ok cpio-magic-crc "$work/crc.diag"; fi
    126 mkdir -p "$work/ex-crc"
    127 ( cd "$work/ex-crc" && "$KIT" cpio -i -F "$work/out.crc" ) 2>/dev/null
    128 same_file cpio-crc-extract "$work/payload/root/a.txt" "$work/ex-crc/root/a.txt"
    129 
    130 # ---- concatenation: two archives back-to-back list as both segments --------
    131 cat "$work/out.cpio" "$work/out.cpio" > "$work/cat.cpio"
    132 "$KIT" cpio -t -F "$work/cat.cpio" > "$work/cat.list" 2> "$work/cat.err"
    133 n=$(grep -c '^root/a.txt$' "$work/cat.list" || true)
    134 if [ "$n" = 2 ]; then ok cpio-concat-both-segments
    135 else echo "count=$n" > "$work/cat.diag"; not_ok cpio-concat-both-segments "$work/cat.diag"; fi
    136 
    137 # ---- negatives -------------------------------------------------------------
    138 run_fail cpio-reject-zstd-flag create -o --compress=zstd -F "$work/z.cpio" root
    139 printf '\050\265\057\375 fake zstd payload' > "$work/fake.zst"
    140 run_fail cpio-reject-zstd-magic "$KIT" cpio -t -F "$work/fake.zst"
    141 "$KIT" cpio -t -F "$work/fake.zst" > "$work/zst.out" 2>&1 || true
    142 contains cpio-zstd-message "$work/zst.out" "gzip and lz4 only"
    143 run_fail cpio-d-with-create create -o -d -F "$work/d.cpio" root
    144 run_fail cpio-no-mode "$KIT" cpio -F "$work/out.cpio"
    145 run_fail cpio-create-dotdot "$KIT" cpio -o -F "$work/dd.cpio" ../payload
    146 
    147 # Mode conflicts are usage errors; archive/decompression/pattern failures are
    148 # operational errors. Assert their exact classes instead of merely nonzero.
    149 expect_status cpio-mode-create-extract 2 "$KIT" cpio -o -i -F "$work/out.cpio"
    150 expect_status cpio-mode-create-list 2 "$KIT" cpio -o -t -F "$work/out.cpio"
    151 expect_status cpio-mode-extract-list 2 "$KIT" cpio -i -t -F "$work/out.cpio"
    152 expect_status cpio-explicit-decompress-raw 1 "$KIT" cpio -t -d -F "$work/out.cpio"
    153 expect_status cpio-unsupported-read-pattern 1 "$KIT" cpio -t -F "$work/out.cpio" root
    154 
    155 printf '070701' > "$work/truncated.cpio"
    156 expect_status cpio-truncated-header 1 "$KIT" cpio -t -F "$work/truncated.cpio"
    157 
    158 {
    159     printf '999999'
    160     dd if="$work/out.cpio" bs=1 skip=6 2>/dev/null
    161 } > "$work/bad-magic.cpio"
    162 expect_status cpio-bad-magic 1 "$KIT" cpio -t -F "$work/bad-magic.cpio"
    163 
    164 {
    165     # One valid record with no TRAILER!!! record.
    166     cpio_hdr 070701 0 33188 0 0 1 0 2 0 0 0 0 10 0
    167     printf 'safe-file\000'
    168     printf 'ok\000\000'
    169 } > "$work/no-trailer.cpio"
    170 expect_status cpio-missing-trailer 1 "$KIT" cpio -t -F "$work/no-trailer.cpio"
    171 
    172 cp "$work/out.cpio" "$work/trailing-garbage.cpio"
    173 printf 'not-an-archive' >> "$work/trailing-garbage.cpio"
    174 expect_status cpio-trailing-garbage 1 "$KIT" cpio -t -F "$work/trailing-garbage.cpio"
    175 
    176 # ---- extract-time path-traversal safety ------------------------------------
    177 # Hand-craft a newc archive whose sole member is named "../escape" and confirm
    178 # extract refuses it (no file is written outside the destination). The 110-byte
    179 # header is built field-by-field (magic + 13 x 8-hex: ino mode uid gid nlink
    180 # mtime filesize devmajor devminor rdevmajor rdevminor namesize check) so the
    181 # field widths can't drift.
    182 ev="$work/evil.cpio"
    183 {
    184     # mode 0100644 = 33188; name "../escape\0" = 10 bytes; data "pwn" = 3 bytes.
    185     cpio_hdr 070701 0 33188 0 0 1 0 3 0 0 0 0 10 0
    186     printf '../escape\000'                 # 10-byte name; 110+10=120 is 4-aligned
    187     printf 'pwn\000'                       # 3 data bytes + 1 pad to 4-align
    188     cpio_hdr 070701 0 0 0 0 1 0 0 0 0 0 0 11 0
    189     printf 'TRAILER!!!\000\000\000\000'    # 11-byte name + 3 pad
    190 } > "$ev"
    191 mkdir -p "$work/exsafe"
    192 ( cd "$work/exsafe" && "$KIT" cpio -i -F "$ev" ) > "$work/safe.out" 2>&1 && safe_rc=0 || safe_rc=1
    193 if [ "$safe_rc" = 1 ] && [ ! -e "$work/escape" ] && [ ! -e "$work/exsafe/escape" ]; then
    194     ok cpio-extract-refuses-dotdot
    195 else
    196     { echo "rc=$safe_rc"; cat "$work/safe.out"; } > "$work/safe.diag"
    197     not_ok cpio-extract-refuses-dotdot "$work/safe.diag"
    198 fi
    199 # List still inspects the crafted name (read path is not blocked).
    200 "$KIT" cpio -t -F "$ev" > "$work/evil.list" 2>/dev/null
    201 contains cpio-list-shows-dotdot "$work/evil.list" "../escape"
    202 
    203 # Validation is a complete first pass: a later unsafe member must prevent an
    204 # earlier, otherwise-valid member from being materialized.
    205 mixed="$work/mixed-evil.cpio"
    206 {
    207     cpio_hdr 070701 0 33188 0 0 1 0 2 0 0 0 0 10 0
    208     printf 'safe-file\000'
    209     printf 'ok\000\000'
    210     cpio_hdr 070701 0 33188 0 0 1 0 3 0 0 0 0 10 0
    211     printf '../escape\000'
    212     printf 'pwn\000'
    213     cpio_hdr 070701 0 0 0 0 1 0 0 0 0 0 0 11 0
    214     printf 'TRAILER!!!\000\000\000\000'
    215 } > "$mixed"
    216 mkdir -p "$work/ex-mixed"
    217 ( cd "$work/ex-mixed" && "$KIT" cpio -i -F "$mixed" ) \
    218     > "$work/mixed.out" 2> "$work/mixed.err"
    219 mixed_rc=$?
    220 if [ "$mixed_rc" -eq 1 ] && [ ! -e "$work/ex-mixed/safe-file" ]; then
    221     ok cpio-prevalidate-before-extract
    222 else
    223     {
    224         printf 'status=%s safe-file=%s\n' "$mixed_rc" \
    225             "$([ -e "$work/ex-mixed/safe-file" ] && printf present || printf absent)"
    226         sed 's/^/stderr: /' "$work/mixed.err"
    227     } > "$work/mixed.diag"
    228     not_ok cpio-prevalidate-before-extract "$work/mixed.diag"
    229 fi
    230 
    231 # ---- optional cross-check against the host cpio ----------------------------
    232 if [ "${KIT_CPIO_TEST_HOST:-0}" = 1 ] && command -v cpio >/dev/null 2>&1; then
    233     if cpio -t < "$work/out.cpio" > "$work/host.list" 2>/dev/null; then
    234         contains cpio-host-lists-kit "$work/host.list" "root/a.txt"
    235     else
    236         not_ok cpio-host-lists-kit
    237     fi
    238     mkdir -p "$work/hostex"
    239     ( cd "$work/hostex" && cpio -id < "$work/out.cpio" ) 2>/dev/null
    240     same_file cpio-host-extracts-kit "$work/payload/root/a.txt" \
    241         "$work/hostex/root/a.txt"
    242 else
    243     skip_test cpio-host-lists-kit "KIT_CPIO_TEST_HOST!=1 or host cpio absent"
    244     skip_test cpio-host-extracts-kit "KIT_CPIO_TEST_HOST!=1 or host cpio absent"
    245 fi
    246 
    247 kit_summary cpio-driver
    248 kit_exit