diff mbox series

[v2,for,2025.02.x,15/15] package/clamav: add patch for CVE-2026-20348

Message ID 20260902131640.379588-16-titouan.christophe@mind.be
State New
Headers show
Series package/clamav: update to upstream LTS | expand

Commit Message

Titouan Christophe Sept. 2, 2026, 1:16 p.m. UTC
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
diff mbox series

Patch

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 <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;
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 \