Commit 3422fa93a for clamav.net
commit 3422fa93a492b2628abd77c17fac95c658282ed8
Author: Val S. <valsnyde@cisco.com>
Date: Fri Aug 7 13:47:27 2026 -0400
Libclamav: fix ZIP local-header catalogue growth (#71)
The ZIP local-header indexing helper can append to the same temporary
catalogue across multiple bounded scans. It tracked allocation capacity
with a local value derived from the current record count, so later calls
could lose the true allocated size and append past the catalogue.
Keep the catalogue block count as caller-owned state and grow the
catalogue before parsing into the next record slot.
Reported-by: Kevin Stubbings
CLAM-2995
diff --git a/libclamav/unzip.c b/libclamav/unzip.c
index 3c6938ffa..5d0ae0aa4 100644
--- a/libclamav/unzip.c
+++ b/libclamav/unzip.c
@@ -1269,8 +1269,9 @@ done:
* @param start_offset The start file offset
* @param end_offset The end file offset
* @param file_count The number of files extracted from the zip file thus far
- * @param[out] temp_catalogue A catalogue of zip_records. Found files between the two offset bounds will be appended to this list.
- * @param[out] num_records The number of records in the catalogue.
+ * @param[out] temp_catalogue A catalogue of zip_records. Found files between the two offset bounds will be appended to this list.
+ * @param[in, out] num_records The number of records in the catalogue.
+ * @param[in, out] num_record_blocks The number of allocated ZIP_RECORDS_CHECK_BLOCKSIZE blocks in the catalogue.
* @return cl_error_t CL_SUCCESS if no overlapping files
* @return cl_error_t CL_VIRUS if overlapping files and heuristic alerts are enabled
* @return cl_error_t CL_EFORMAT if overlapping files and heuristic alerts are disabled
@@ -1285,20 +1286,20 @@ cl_error_t index_local_file_headers_within_bounds(
uint32_t end_offset,
uint32_t file_count,
struct zip_record **temp_catalogue,
- size_t *num_records)
+ size_t *num_records,
+ size_t *num_record_blocks)
{
cl_error_t status = CL_ERROR;
cl_error_t ret;
- size_t num_record_blocks = 0;
- size_t index = 0;
+ size_t index = 0;
uint32_t search_offset = 0;
uint32_t total_file_count = file_count;
struct zip_record *zip_catalogue = NULL;
bool exceeded_max_files = false;
- if (NULL == temp_catalogue || NULL == num_records) {
+ if (NULL == temp_catalogue || NULL == num_records || NULL == num_record_blocks) {
cli_errmsg("index_local_file_headers_within_bounds: Invalid NULL arguments\n");
goto done;
}
@@ -1315,11 +1316,15 @@ cl_error_t index_local_file_headers_within_bounds(
sizeof(struct zip_record) * ZIP_RECORDS_CHECK_BLOCKSIZE,
status = CL_EMEM);
- *num_records = 0;
+ *num_records = 0;
+ *num_record_blocks = 1;
+ } else if (0 == *num_record_blocks) {
+ cli_errmsg("index_local_file_headers_within_bounds: Invalid zero catalogue capacity\n");
+ status = CL_EARG;
+ goto done;
}
- num_record_blocks = (*num_records / ZIP_RECORDS_CHECK_BLOCKSIZE) + 1;
- index = *num_records;
+ index = *num_records;
if (start_offset > fsize || end_offset > fsize || start_offset > end_offset) {
cli_errmsg("index_local_file_headers_within_bounds: Invalid offset arguments: start_offset=%u, end_offset=%u, fsize=%u\n",
@@ -1340,6 +1345,24 @@ cl_error_t index_local_file_headers_within_bounds(
if (cli_readint32(local_file_header) == ZIP_MAGIC_LOCAL_FILE_HEADER) {
uint32_t local_file_header_offset = search_offset;
size_t file_record_size = 0;
+ size_t record_capacity = *num_record_blocks * ZIP_RECORDS_CHECK_BLOCKSIZE;
+
+ if (record_capacity <= index) {
+ size_t new_record_blocks = (index / ZIP_RECORDS_CHECK_BLOCKSIZE) + 1;
+
+ // Filled the available zip record blocks. Allocate more space before writing the next record.
+ cli_dbgmsg("cli_unzip: Filled zip record blocks. Allocating more space for zip records...\n");
+
+ CLI_MAX_REALLOC_OR_GOTO_DONE(
+ zip_catalogue,
+ sizeof(struct zip_record) * ZIP_RECORDS_CHECK_BLOCKSIZE * new_record_blocks,
+ status = CL_EMEM);
+
+ /* zero out the memory for the new records */
+ memset(&(zip_catalogue[record_capacity]), 0,
+ sizeof(struct zip_record) * (ZIP_RECORDS_CHECK_BLOCKSIZE * new_record_blocks - record_capacity));
+ *num_record_blocks = new_record_blocks;
+ }
ret = parse_local_file_header(
ctx,
@@ -1383,21 +1406,6 @@ cl_error_t index_local_file_headers_within_bounds(
// We still need to scan the files we found while reviewing the file records up to this limit.
break;
}
-
- if (num_record_blocks * ZIP_RECORDS_CHECK_BLOCKSIZE == index + 1) {
- // Filled up the current block of zip records, need to allocate more space to fit additional records.
- cli_dbgmsg("cli_unzip: Filled a zip record block. Allocating an additional block for more zip records...\n");
-
- CLI_MAX_REALLOC_OR_GOTO_DONE(
- zip_catalogue,
- sizeof(struct zip_record) * ZIP_RECORDS_CHECK_BLOCKSIZE * (num_record_blocks + 1),
- status = CL_EMEM);
-
- num_record_blocks++;
- /* zero out the memory for the new records */
- memset(&(zip_catalogue[index]), 0,
- sizeof(struct zip_record) * (ZIP_RECORDS_CHECK_BLOCKSIZE * num_record_blocks - index));
- }
}
}
@@ -1418,6 +1426,7 @@ done:
free(zip_catalogue);
zip_catalogue = NULL;
*temp_catalogue = NULL; // zip_catalogue and *temp_catalogue have the same value. Set temp_catalogue to NULL to ensure no use after free
+ *num_record_blocks = 0;
}
if (exceeded_max_files) {
@@ -1470,6 +1479,7 @@ cl_error_t index_local_file_headers(
struct zip_record *next_record = NULL;
struct zip_record *prev_record = NULL;
size_t local_file_headers_count = 0;
+ size_t local_file_headers_blocks = 0;
uint32_t num_overlapping_files = 0;
if (NULL == catalogue || NULL == num_records || NULL == *catalogue) {
@@ -1497,7 +1507,8 @@ cl_error_t index_local_file_headers(
end_offset,
total_files_found,
&temp_catalogue,
- &local_file_headers_count);
+ &local_file_headers_count,
+ &local_file_headers_blocks);
if (CL_SUCCESS != ret) {
goto done;
}
@@ -1532,7 +1543,8 @@ cl_error_t index_local_file_headers(
end_offset,
total_files_found,
&temp_catalogue,
- &local_file_headers_count);
+ &local_file_headers_count,
+ &local_file_headers_blocks);
if (CL_SUCCESS != ret) {
status = ret;
goto done;