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