package/clamav: add patch for CVE-2026-20348

Signed-off-by: Titouan Christophe <titouan.christophe@mind.be>
Signed-off-by: Raphaël Mélotte <raphael.melotte@mind.be>
This commit is contained in:
Titouan Christophe
2026-09-02 15:16:40 +02:00
committed by Raphaël Mélotte
parent c4a2c7371c
commit b24489289f
2 changed files with 280 additions and 0 deletions

View File

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

View File

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