new file mode 100644
@@ -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 <titouan.christophe@mind.be>
+---
+ 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;
@@ -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 \
Signed-off-by: Titouan Christophe <titouan.christophe@mind.be> --- ...xar-limits-against-inflated-toc-size.patch | 277 ++++++++++++++++++ package/clamav/clamav.mk | 3 + 2 files changed, 280 insertions(+) create mode 100644 package/clamav/0014-libclamav-enforce-xar-limits-against-inflated-toc-size.patch