new file mode 100644
@@ -0,0 +1,224 @@
+From: "Val S." <valsnyde@cisco.com>
+Date: Tue, 28 Jul 2026 09:04:39 -0400
+Subject: Libclamav: fix PESpin rebuilt section size overflow (#101)
+
+The PESpin unpacker accumulated rebuilt section sizes in a signed int
+before allocating the intermediate rebuilt image buffer. A crafted PE with
+many sections could wrap the total to a small value and then copy full
+section contents into that undersized allocation.
+
+Track the rebuilt size in a 64-bit accumulator and reject additions that
+would exceed CLI_MAX_ALLOCATION before allocating. Also avoid related
+undefined behavior in the PESpin checksum and resource bitmap paths, and
+share bitmap-based cleanup through one helper.
+
+Reported-by: Feng Xue
+Also independently reported by: Yazdan Soltani
+
+CLAM-3008
+
+---
+Upstream: https://github.com/Cisco-Talos/clamav/commit/d442da6418391b84c6124717e0153237187972dd
+CVE: CVE-2026-20339
+Signed-off-by: Titouan Christophe <titouan.christophe@mind.be>
+---
+ libclamav/spin.c | 112 +++++++++++++++++++++++++++++++++++++----------
+ 1 file changed, 89 insertions(+), 23 deletions(-)
+
+diff --git a/libclamav/spin.c b/libclamav/spin.c
+index d0470b317a..a7e3a2d01d 100644
+--- a/libclamav/spin.c
++++ b/libclamav/spin.c
+@@ -62,6 +62,64 @@
+ #include "packlibs.h"
+ #include "spin.h"
+
++/**
++ * @brief Add a section size to the rebuilt PESpin image size.
++ *
++ * @return 0 if the size was added, 1 if the rebuilt image would exceed
++ * CLI_MAX_ALLOCATION.
++ */
++static int spin_add_blob_size(uint64_t *blobsz, uint32_t size)
++{
++ if (*blobsz > (uint64_t)CLI_MAX_ALLOCATION ||
++ (uint64_t)size > (uint64_t)CLI_MAX_ALLOCATION - *blobsz) {
++ cli_dbgmsg("spin: rebuilt section data exceeds allocation limit\n");
++ return 1;
++ }
++
++ *blobsz += size;
++ return 0;
++}
++
++/**
++ * @brief Replace an already-counted section size in the rebuilt PESpin image.
++ *
++ * @return 0 if the size was replaced, 1 if the previous size was not counted
++ * or the rebuilt image would exceed CLI_MAX_ALLOCATION.
++ */
++static int spin_replace_blob_size(uint64_t *blobsz, uint32_t old_size, uint32_t new_size)
++{
++ uint64_t adjusted_blobsz;
++
++ if (*blobsz < old_size) {
++ cli_dbgmsg("spin: rebuilt section data accounting underflow\n");
++ return 1;
++ }
++
++ adjusted_blobsz = *blobsz - old_size;
++ if (adjusted_blobsz > (uint64_t)CLI_MAX_ALLOCATION ||
++ (uint64_t)new_size > (uint64_t)CLI_MAX_ALLOCATION - adjusted_blobsz) {
++ cli_dbgmsg("spin: rebuilt section data exceeds allocation limit\n");
++ return 1;
++ }
++
++ *blobsz = adjusted_blobsz + new_size;
++ return 0;
++}
++
++/**
++ * @brief Free PESpin section buffers that are marked as grown in the bitmap.
++ */
++static void spin_free_grown_sections(char **sects, int sectcnt, uint32_t bitmap)
++{
++ int j;
++
++ for (j = 0; j < sectcnt; j++) {
++ if (bitmap & 1)
++ free(sects[j]);
++ bitmap >>= 1;
++ }
++}
++
+ static char exec86(uint8_t aelle, uint8_t cielle, char *curremu, int *retval)
+ {
+ int len = 0;
+@@ -143,7 +201,7 @@ static uint32_t summit(char *src, int size)
+ int i;
+
+ while (size) {
+- eax ^= *src++ << 8 & 0xff00;
++ eax ^= ((uint32_t)(uint8_t)*src++ << 8) & 0xff00;
+ eax = eax >> 3 & 0x1fffffff;
+ for (i = 0; i < 4; i++) {
+ uint32_t swap;
+@@ -164,7 +222,8 @@ int unspin(char *src, int ssize, struct cli_exe_section *sections, int sectcnt,
+ {
+ char *curr, *emu, *ep, *spinned;
+ char **sects;
+- int blobsz = 0, j;
++ uint64_t blobsz = 0;
++ int j;
+ uint32_t key32, bitmap, bitman;
+ uint32_t len;
+ uint8_t key8;
+@@ -402,12 +461,16 @@ int unspin(char *src, int ssize, struct cli_exe_section *sections, int sectcnt,
+ len = 0;
+ for (j = 0; j < sectcnt; j++) {
+ if (bitmap & 1) {
++ if (spin_add_blob_size(&blobsz, sections[j].vsz)) {
++ len = 1;
++ break;
++ }
++
+ if ((sects[j] = (char *)cli_max_malloc(sections[j].vsz)) == NULL) {
+ cli_dbgmsg("spin: malloc(%u) failed\n", sections[j].vsz);
+ len = 1;
+ break;
+ }
+- blobsz += sections[j].vsz;
+ memset(sects[j], 0, sections[j].vsz);
+ cli_dbgmsg("spin: Growing sect%d: was %x will be %x\n", j, sections[j].rsz, sections[j].vsz);
+ if (cli_unfsg(src + sections[j].raw, sects[j], sections[j].rsz, sections[j].vsz, NULL, NULL) == -1) {
+@@ -415,7 +478,11 @@ int unspin(char *src, int ssize, struct cli_exe_section *sections, int sectcnt,
+ cli_dbgmsg("spin: Unpack failure\n");
+ }
+ } else {
+- blobsz += sections[j].rsz;
++ if (spin_add_blob_size(&blobsz, sections[j].rsz)) {
++ len = 1;
++ break;
++ }
++
+ sects[j] = src + sections[j].raw;
+ cli_dbgmsg("spin: Not growing sect%d\n", j);
+ }
+@@ -425,12 +492,7 @@ int unspin(char *src, int ssize, struct cli_exe_section *sections, int sectcnt,
+ cli_dbgmsg("spin: decompression complete\n");
+
+ if (len) {
+- int t;
+- for (t = 0; t < j; t++) {
+- if (bitman & 1)
+- free(sects[t]);
+- bitman = bitman >> 1 & 0x7fffffff;
+- }
++ spin_free_grown_sections(sects, j, bitman);
+ free(sects);
+ return 1;
+ }
+@@ -444,7 +506,7 @@ int unspin(char *src, int ssize, struct cli_exe_section *sections, int sectcnt,
+ break;
+ }
+
+- if (j != sectcnt && ((bitman & (1 << j)) == 0)) { /* FIXME: not really sure either the res sect is lamed or just compressed, but this'll save some major headaches */
++ if (j != sectcnt && j < 32 && ((bitman & ((uint32_t)1 << j)) == 0)) { /* FIXME: not really sure either the res sect is lamed or just compressed, but this'll save some major headaches */
+ cli_dbgmsg("spin: Resources (sect%d) appear to be compressed\n\tuncompressed offset %x, len %x\n\tcompressed offset %x, len %x\n", j, sections[j].rva, key32 - sections[j].rva, key32, sections[j].vsz - (key32 - sections[j].rva));
+
+ if ((curr = (char *)cli_max_malloc(sections[j].vsz)) != NULL) {
+@@ -454,26 +516,34 @@ int unspin(char *src, int ssize, struct cli_exe_section *sections, int sectcnt,
+
+ free(curr);
+ cli_dbgmsg("spin: Failed to grow resources, continuing anyway\n");
+- blobsz += sections[j].rsz;
+ } else {
+- sects[j] = curr;
+- bitman |= 1 << j;
+- cli_dbgmsg("spin: Resources grown\n");
+- blobsz += sections[j].vsz;
++ if (spin_replace_blob_size(&blobsz, sections[j].rsz, sections[j].vsz)) {
++ free(curr);
++ len = 1;
++ } else {
++ sects[j] = curr;
++ bitman |= (uint32_t)1 << j;
++ cli_dbgmsg("spin: Resources grown\n");
++ }
+ }
+ } else {
+ /* malloc failed but i'm too deep into this crap to quit without leaking more :( */
+ cli_dbgmsg("spin: memory allocation failed, continuing anyway\n");
+- blobsz += sections[j].rsz;
+ }
+ } else {
+ cli_dbgmsg("spin: No res?!\n");
+ }
+ }
+
++ if (len) {
++ spin_free_grown_sections(sects, sectcnt, bitman);
++ free(sects);
++ return 1;
++ }
++
+ bitmap = bitman; /* save as a free() bitmap */
+
+- if ((ep = (char *)cli_max_malloc(blobsz)) != NULL) {
++ if ((ep = (char *)cli_max_malloc((size_t)blobsz)) != NULL) {
+ struct cli_exe_section *rebhlp;
+ if ((rebhlp = (struct cli_exe_section *)cli_max_malloc(sizeof(struct cli_exe_section) * (sectcnt))) != NULL) {
+ char *to = ep;
+@@ -505,11 +575,7 @@ int unspin(char *src, int ssize, struct cli_exe_section *sections, int sectcnt,
+ }
+
+ cli_dbgmsg("spin: free bitmap is %x\n", bitman);
+- for (j = 0; j < sectcnt; j++) {
+- if (bitmap & 1)
+- free(sects[j]);
+- bitmap = bitmap >> 1 & 0x7fffffff;
+- }
++ spin_free_grown_sections(sects, sectcnt, bitmap);
+ free(sects);
+ return 1; /* :( */
+ }
@@ -51,6 +51,9 @@ CLAMAV_IGNORE_CVES += CVE-2026-20244
# 0010-libclamav-fix-gpt-partition-name-conversion-index-103.patch
CLAMAV_IGNORE_CVES += CVE-2026-20345
+# 0011-libclamav-fix-pespin-rebuilt-section-size-overflow-101.patch
+CLAMAV_IGNORE_CVES += CVE-2026-20339
+
CLAMAV_DEPENDENCIES = \
bzip2 \
host-pkgconf \
Signed-off-by: Titouan Christophe <titouan.christophe@mind.be> --- ...in-rebuilt-section-size-overflow-101.patch | 224 ++++++++++++++++++ package/clamav/clamav.mk | 3 + 2 files changed, 227 insertions(+) create mode 100644 package/clamav/0011-libclamav-fix-pespin-rebuilt-section-size-overflow-101.patch