diff --git a/package/clamav/0014-libclamav-enforce-xar-limits-against-inflated-toc-size.patch b/package/clamav/0014-libclamav-enforce-xar-limits-against-inflated-toc-size.patch new file mode 100644 index 0000000000..962ccfcf40 --- /dev/null +++ b/package/clamav/0014-libclamav-enforce-xar-limits-against-inflated-toc-size.patch @@ -0,0 +1,277 @@ +From: John Humlick <15677335+jhumlick@users.noreply.github.com> +Date: Fri, 17 Jul 2026 15:27:26 -0700 +Subject: libclamav: Enforce XAR limits against inflated TOC size + +Inflate XAR table-of-contents data incrementally and enforce scan and +allocation limits against the actual output instead of the declared +header length. Require the compressed stream to finish before parsing. + +Add regression coverage for mismatched TOC lengths, oversized actual +output, and incomplete compressed streams. + +CLAM-3010 + +--- +Upstream: https://github.com/Cisco-Talos/clamav/commit/10ee017ebe51ecd593bf13944a25c6c8fe70aa8a +CVE: CVE-2026-20348 +[Titouan: fix merge conflict with clamav-1.4.3: drop diff for test file not in tree] +Signed-off-by: Titouan Christophe +--- + libclamav/xar.c | 190 +++++++++++++++++++++++++++++++++++++----------- + 1 file changed, 149 insertions(+), 41 deletions(-) + +diff --git a/libclamav/xar.c b/libclamav/xar.c +index 12f911551..a4fac0b3a 100644 +--- a/libclamav/xar.c ++++ b/libclamav/xar.c +@@ -34,6 +34,8 @@ + #include "inflate64.h" + #include "lzma_iface.h" + ++#define XAR_TOC_INFLATE_CHUNK_SIZE (64 * 1024) ++ + /* + xar_cleanup_temp_file - cleanup after cli_gentempfd + parameters: +@@ -408,6 +410,124 @@ static int xar_hash_check(int hash, const void *result, const void *expected) + return memcmp(result, expected, len); + } + ++static cl_error_t xar_inflate_toc(cli_ctx *ctx, const unsigned char *compressed_toc, size_t compressed_length, ++ char **toc_out, size_t *toc_length_out) ++{ ++ cl_error_t ret = CL_SUCCESS; ++ z_stream strm; ++ char *toc = NULL; ++ size_t capacity = XAR_TOC_INFLATE_CHUNK_SIZE; ++ size_t toc_length = 0; ++ bool inflate_initialized = false; ++ ++ memset(&strm, 0, sizeof(strm)); ++ ++ if (compressed_length > UINT_MAX) { ++ cli_dbgmsg("cli_scanxar: Compressed TOC is too large for zlib.\n"); ++ return CL_EFORMAT; ++ } ++ ++ toc = cli_max_malloc(capacity); ++ if (toc == NULL) { ++ cli_dbgmsg("cli_scanxar: Failed to allocate the initial TOC buffer.\n"); ++ return CL_EMEM; ++ } ++ ++ strm.next_in = (unsigned char *)compressed_toc; ++ strm.avail_in = (uInt)compressed_length; ++ ++ if (inflateInit(&strm) != Z_OK) { ++ cli_dbgmsg("cli_scanxar: inflateInit failed.\n"); ++ ret = CL_EFORMAT; ++ goto done; ++ } ++ inflate_initialized = true; ++ ++ while (true) { ++ uInt avail_in_before; ++ uInt output_available; ++ size_t produced; ++ int zret; ++ ++ output_available = (uInt)(capacity - toc_length - 1); ++ strm.next_out = (unsigned char *)toc + toc_length; ++ strm.avail_out = output_available; ++ avail_in_before = strm.avail_in; ++ ++ zret = inflate(&strm, Z_NO_FLUSH); ++ produced = output_available - strm.avail_out; ++ ++ if (toc_length > SIZE_MAX - produced) { ++ cli_dbgmsg("cli_scanxar: Decompressed TOC length overflow.\n"); ++ ret = CL_EFORMAT; ++ goto done; ++ } ++ toc_length += produced; ++ ++ ret = cli_checklimits("cli_scanxar", ctx, toc_length, 0, 0); ++ if (ret != CL_SUCCESS) { ++ goto done; ++ } ++ ++ if (zret == Z_STREAM_END) { ++ break; ++ } ++ if (zret != Z_OK) { ++ cli_dbgmsg("cli_scanxar: inflate failed with status %d.\n", zret); ++ ret = CL_EFORMAT; ++ goto done; ++ } ++ if (produced == 0 && strm.avail_in == avail_in_before) { ++ cli_dbgmsg("cli_scanxar: inflate made no progress before reaching the end of the TOC stream.\n"); ++ ret = CL_EFORMAT; ++ goto done; ++ } ++ ++ if (strm.avail_out == 0) { ++ char *new_toc; ++ size_t new_capacity; ++ ++ if (capacity == CLI_MAX_ALLOCATION) { ++ cli_dbgmsg("cli_scanxar: Decompressed TOC exceeds the internal allocation limit.\n"); ++ ret = CL_EFORMAT; ++ goto done; ++ } ++ ++ new_capacity = capacity + XAR_TOC_INFLATE_CHUNK_SIZE; ++ if (new_capacity < capacity || new_capacity > CLI_MAX_ALLOCATION) { ++ new_capacity = CLI_MAX_ALLOCATION; ++ } ++ ++ new_toc = cli_max_realloc(toc, new_capacity); ++ if (new_toc == NULL) { ++ cli_dbgmsg("cli_scanxar: Failed to grow the TOC buffer.\n"); ++ ret = CL_EMEM; ++ goto done; ++ } ++ toc = new_toc; ++ capacity = new_capacity; ++ } ++ } ++ ++ if (inflateEnd(&strm) != Z_OK) { ++ cli_dbgmsg("cli_scanxar: inflateEnd failed.\n"); ++ ret = CL_EFORMAT; ++ goto done; ++ } ++ inflate_initialized = false; ++ ++ toc[toc_length] = '\0'; ++ *toc_out = toc; ++ *toc_length_out = toc_length; ++ toc = NULL; ++ ++done: ++ if (inflate_initialized) ++ inflateEnd(&strm); ++ free(toc); ++ return ret; ++} ++ + /* + cli_scanxar - scan an xar archive. + Parameters: +@@ -427,7 +547,8 @@ int cli_scanxar(cli_ctx *ctx) + size_t length, offset, size, at; + int encoding; + z_stream strm; +- char *toc, *tmpname = NULL; ++ char *toc = NULL, *tmpname = NULL; ++ size_t toc_length = 0; + xmlTextReaderPtr reader = NULL; + int a_hash, e_hash; + unsigned char *a_cksum = NULL, *e_cksum = NULL; +@@ -435,8 +556,6 @@ int cli_scanxar(cli_ctx *ctx) + char e_hash_result[SHA1_HASH_SIZE]; + char a_hash_result[SHA1_HASH_SIZE]; + +- memset(&strm, 0x00, sizeof(z_stream)); +- + /* retrieve xar header */ + if (fmap_readn(ctx->fmap, &hdr, 0, sizeof(hdr)) != sizeof(hdr)) { + cli_dbgmsg("cli_scanxar: Invalid header, too short.\n"); +@@ -463,46 +582,35 @@ int cli_scanxar(cli_ctx *ctx) + /* cli_dbgmsg("hdr.toc_length_decompressed %lu\n", hdr.toc_length_decompressed); */ + /* cli_dbgmsg("hdr.chksum_alg %i\n", hdr.chksum_alg); */ + +- /* Uncompress TOC */ +- strm.next_in = (unsigned char *)fmap_need_off_once(ctx->fmap, hdr.size, hdr.toc_length_compressed); +- if (strm.next_in == NULL) { +- cli_dbgmsg("cli_scanxar: fmap_need_off_once fails on TOC.\n"); +- return CL_EREAD; +- } +- strm.avail_in = hdr.toc_length_compressed; +- toc = cli_max_malloc(hdr.toc_length_decompressed + 1); +- if (toc == NULL) { +- cli_dbgmsg("cli_scanxar: cli_max_malloc fails on TOC decompress buffer.\n"); +- return CL_EMEM; ++ /* Check time and file-count limits before inflating. Size limits are ++ * enforced against the actual decompressed TOC length below. */ ++ rc = cli_checklimits("cli_scanxar", ctx, 0, 0, 0); ++ if (rc != CL_SUCCESS) { ++ return rc; + } +- toc[hdr.toc_length_decompressed] = '\0'; +- strm.avail_out = hdr.toc_length_decompressed; +- strm.next_out = (unsigned char *)toc; +- rc = inflateInit(&strm); +- if (rc != Z_OK) { +- cli_dbgmsg("cli_scanxar:inflateInit error %i \n", rc); +- rc = CL_EFORMAT; +- goto exit_toc; ++ ++ if (hdr.toc_length_compressed > SIZE_MAX) { ++ cli_dbgmsg("cli_scanxar: Compressed TOC length cannot be represented safely.\n"); ++ return CL_EFORMAT; + } +- rc = inflate(&strm, Z_SYNC_FLUSH); +- if (rc != Z_OK && rc != Z_STREAM_END) { +- inflateEnd(&strm); +- cli_dbgmsg("cli_scanxar:inflate error %i \n", rc); +- rc = CL_EFORMAT; +- goto exit_toc; ++ ++ /* Uncompress TOC */ ++ { ++ const unsigned char *compressed_toc = fmap_need_off_once(ctx->fmap, hdr.size, (size_t)hdr.toc_length_compressed); ++ ++ if (compressed_toc == NULL) { ++ cli_dbgmsg("cli_scanxar: fmap_need_off_once fails on TOC.\n"); ++ return CL_EREAD; ++ } ++ rc = xar_inflate_toc(ctx, compressed_toc, (size_t)hdr.toc_length_compressed, &toc, &toc_length); + } +- rc = inflateEnd(&strm); +- if (rc != Z_OK) { +- cli_dbgmsg("cli_scanxar:inflateEnd error %i \n", rc); +- rc = CL_EFORMAT; +- goto exit_toc; ++ if (rc != CL_SUCCESS) { ++ return rc; + } + +- if (hdr.toc_length_decompressed != strm.total_out) { +- cli_dbgmsg("TOC decompress length %" PRIu64 " does not match amount decompressed %lu\n", +- hdr.toc_length_decompressed, strm.total_out); +- toc[strm.total_out] = '\0'; +- hdr.toc_length_decompressed = strm.total_out; ++ if (hdr.toc_length_decompressed != toc_length) { ++ cli_dbgmsg("TOC declared decompress length %" PRIu64 " does not match amount decompressed %zu\n", ++ hdr.toc_length_decompressed, toc_length); + } + + /* cli_dbgmsg("cli_scanxar: TOC xml:\n%s\n", toc); */ +@@ -512,7 +620,7 @@ int cli_scanxar(cli_ctx *ctx) + + /* scan the xml */ + cli_dbgmsg("cli_scanxar: scanning xar TOC xml in memory.\n"); +- rc = cli_magic_scan_buff(toc, hdr.toc_length_decompressed, ctx, NULL, LAYER_ATTRIBUTES_NONE); ++ rc = cli_magic_scan_buff(toc, toc_length, ctx, NULL, LAYER_ATTRIBUTES_NONE); + if (rc != CL_SUCCESS) { + goto exit_toc; + } +@@ -523,7 +631,7 @@ int cli_scanxar(cli_ctx *ctx) + cli_dbgmsg("cli_scanxar: Can't create temporary file for TOC.\n"); + goto exit_toc; + } +- if (cli_writen(fd, toc, hdr.toc_length_decompressed) == (size_t)-1) { ++ if (cli_writen(fd, toc, toc_length) == (size_t)-1) { + cli_dbgmsg("cli_scanxar: cli_writen error writing TOC.\n"); + rc = CL_EWRITE; + xar_cleanup_temp_file(ctx, fd, tmpname); +@@ -535,7 +643,7 @@ int cli_scanxar(cli_ctx *ctx) + goto exit_toc; + } + +- reader = xmlReaderForMemory(toc, hdr.toc_length_decompressed, "noname.xml", NULL, CLAMAV_MIN_XMLREADER_FLAGS); ++ reader = xmlReaderForMemory(toc, toc_length, "noname.xml", NULL, CLAMAV_MIN_XMLREADER_FLAGS); + if (reader == NULL) { + cli_dbgmsg("cli_scanxar: xmlReaderForMemory error for TOC\n"); + goto exit_toc; diff --git a/package/clamav/clamav.mk b/package/clamav/clamav.mk index a9bc1243c1..9152028b85 100644 --- a/package/clamav/clamav.mk +++ b/package/clamav/clamav.mk @@ -60,6 +60,9 @@ CLAMAV_IGNORE_CVES += CVE-2026-20346 # 0013-libclamav-harden-mach-o-section-validation-96.patch CLAMAV_IGNORE_CVES += CVE-2026-20347 +# 0014-libclamav-enforce-xar-limits-against-inflated-toc-size.patch +CLAMAV_IGNORE_CVES += CVE-2026-20348 + CLAMAV_DEPENDENCIES = \ bzip2 \ host-pkgconf \