diff --git a/src/htsselftest.c b/src/htsselftest.c index 5b2f2f3b..8e1f6551 100644 --- a/src/htsselftest.c +++ b/src/htsselftest.c @@ -2146,6 +2146,52 @@ static int st_cache_corrupt(httrackp *opt, int argc, char **argv) { return err; } +/* Drives unzRepair over a damaged local file header whose CRC field's high + 16-bit word has bit 15 set. Before the READ_32 fix that shifted an int and + overflowed, so UBSan aborts here; after it, repair recovers the one entry. */ +static int st_zip_repair_shift(httrackp *opt, int argc, char **argv) { + static const unsigned char zip[] = { + 0x50, 0x4b, 0x03, 0x04, /* local file header signature */ + 0x14, 0x00, /* version needed */ + 0x00, 0x00, /* general purpose flag */ + 0x00, 0x00, /* method */ + 0x00, 0x00, /* time */ + 0x00, 0x00, /* date */ + 0x00, 0x00, 0xe8, 0x8a, /* crc: high word 0x8ae8, bit 15 set */ + 0x00, 0x00, 0x00, 0x00, /* compressed size */ + 0x00, 0x00, 0x00, 0x00, /* uncompressed size */ + 0x01, 0x00, /* filename length */ + 0x00, 0x00, /* extra field length */ + 0x61 /* filename "a" */ + }; + char in[HTS_URLMAXSIZE], out[HTS_URLMAXSIZE], tmp[HTS_URLMAXSIZE]; + uLong nrec = 0, bytes = 0; + FILE *fp; + int err; + + (void) opt; + if (argc < 1) { + fprintf(stderr, "zip-repair-shift: needs a directory\n"); + return 1; + } + snprintf(in, sizeof(in), "%s/damaged.zip", argv[0]); + snprintf(out, sizeof(out), "%s/repair.zip", argv[0]); + snprintf(tmp, sizeof(tmp), "%s/repair.tmp", argv[0]); + fp = fopen(in, "wb"); + if (fp == NULL || fwrite(zip, 1, sizeof(zip), fp) != sizeof(zip)) { + if (fp != NULL) + fclose(fp); + fprintf(stderr, "zip-repair-shift: cannot write %s\n", in); + return 1; + } + fclose(fp); + err = unzRepair(in, out, tmp, &nrec, &bytes); + printf("zip-repair-shift: %s (recovered %lu entr%s)\n", + (err == Z_OK && nrec == 1) ? "OK" : "FAIL", (unsigned long) nrec, + nrec == 1 ? "y" : "ies"); + return (err == Z_OK && nrec == 1) ? 0 : 1; +} + static int st_cache_legacy(httrackp *opt, int argc, char **argv) { int err; @@ -3253,6 +3299,9 @@ static const struct selftest_entry { st_cache_legacy}, {"cache-corrupt", "", "cache read-side corruption self-test", st_cache_corrupt}, + {"zip-repair-shift", "", + "cache zip-repair header read must not overflow a signed shift", + st_zip_repair_shift}, {"dns", "", "DNS resolver/cache self-test", st_dns}, {"dnstimeout", "", "a slow DNS resolve is bounded and holds no lock", st_dnstimeout}, diff --git a/src/minizip/mztools.c b/src/minizip/mztools.c index b510c354..b357c9f7 100644 --- a/src/minizip/mztools.c +++ b/src/minizip/mztools.c @@ -14,7 +14,7 @@ #define READ_8(adr) ((unsigned char)*(adr)) #define READ_16(adr) ( READ_8(adr) | (READ_8(adr+1) << 8) ) -#define READ_32(adr) ( READ_16(adr) | (READ_16((adr)+2) << 16) ) +#define READ_32(adr) ((uLong) READ_16(adr) | ((uLong) READ_16((adr) + 2) << 16)) #define WRITE_8(buff, n) do { \ *((unsigned char*)(buff)) = (unsigned char) ((n) & 0xff); \ diff --git a/tests/01_zlib-repair-shift.test b/tests/01_zlib-repair-shift.test new file mode 100644 index 00000000..5062164d --- /dev/null +++ b/tests/01_zlib-repair-shift.test @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain +# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only +# tool flags despite the #!/bin/bash above. + +# unzRepair header read must not overflow a signed shift (-#test=zip-repair-shift +# ). A damaged local file header whose CRC high word has bit 15 set made +# READ_32 shift an int past INT_MAX; UBSan aborts before the fix casts to uLong. + +set -eu + +dir=$(mktemp -d) +trap 'rm -rf "$dir"' EXIT + +out=$(httrack -#test=zip-repair-shift "$dir") + +printf '%s\n' "$out" | grep -qx "zip-repair-shift: OK (recovered 1 entry)" || { + echo "expected 'zip-repair-shift: OK (recovered 1 entry)', got: $out" >&2 + exit 1 +} diff --git a/tests/Makefile.am b/tests/Makefile.am index 6666ac9a..492aeec3 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -84,6 +84,7 @@ TESTS = \ 01_zlib-cache-legacy.test \ 01_zlib-cache-golden.test \ 01_zlib-cache-writefail.test \ + 01_zlib-repair-shift.test \ 01_zlib-savename-cached.test \ 02_manpage-regen.test \ 02_update-cache.test \