----- Forwarded message from Tristan <TristanInSec(a)gmail.com> -----
Date: Mon, 24 Aug 2026 10:33:21 +0200
From: Tristan <TristanInSec(a)gmail.com>
To: rjones(a)redhat.com
Subject: Security Vulnerability in nbdkit QCOW2 ZSTD Decompression
Hello,
I am writing to report a security vulnerability in nbdkit's qcow2dec
filter, confirmed at HEAD (commit 053f5a3).
Summary of findings:
ZSTD decompression heap over-read via incorrect buffer size (CVSS 6.5)
The zstd_compressed_cluster() function sets the ZSTD input buffer
size to cluster_size (uncompressed size) instead of compressed_size
(actual allocation size). The equivalent zlib path correctly uses
compressed_size, confirming this is a copy-paste bug. In worst case
(2MB clusters, 512-byte compressed), this causes a 2MB over-read
past a 512-byte allocation.
The finding detail is attached.
I would appreciate an acknowledgment of receipt.
Thank you and please let me know if you need anything else.
Regards,
Tristan
=================================================================
Heap Over-Read in ZSTD Decompression of QCOW2 Compressed Clusters
=================================================================
CVSS 3.1: 6.5 (AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H)
CWE: CWE-125 (Out-of-bounds Read)
Auth: None (malicious QCOW2 file)
Version: nbdkit 1.42.3, commit 053f5a3 (HEAD, 2026-07-21)
File: filters/qcow2dec/qcow2dec.c, zstd_compressed_cluster(), line 712
Root Cause:
The ZSTD input buffer size is set to cluster_size (the uncompressed
cluster size) instead of compressed_size (the actual allocated buffer
size). The equivalent zlib/deflate path correctly uses compressed_size.
ZSTD path (INCORRECT) -- line 710-712:
ZSTD_outBuffer out = { .dst = buf, .size = cluster_size, .pos = 0 };
ZSTD_inBuffer in =
{ .src = compressed_cluster, .size = cluster_size, .pos = 0 };
// ^^^^^^^^^^^^
// BUG: should be compressed_size
zlib/deflate path (CORRECT) -- line 664-665:
strm.next_in = (void *) compressed_cluster;
strm.avail_in = compressed_size;
// ^^^^^^^^^^^^^^^ correct: uses actual buffer size
Buffer allocation -- line 840:
compressed_cluster = malloc(compressed_size);
Data Flow:
1. qcow2dec filter parses QCOW2 image with ZSTD-compressed clusters
2. L2 entry encodes nr_sectors (line 789), minimum 1 sector (512 bytes)
3. compressed_size = nr_sectors * 512 (line 806)
4. malloc(compressed_size) allocates buffer (line 840)
5. ZSTD input buffer declares .size = cluster_size (line 712)
6. ZSTD_decompressStream() may read up to cluster_size bytes
7. Over-read: cluster_size - compressed_size bytes past allocation
Worst Case:
cluster_bits=21 (2 MB clusters), single-sector compressed data:
- compressed_size = 512 bytes
- cluster_size = 2,097,152 bytes
- Over-read: up to 2,096,640 bytes past the 512-byte allocation
Impact:
Crash (DoS) when serving a crafted QCOW2 image via nbdkit. In cloud
and virtualization environments where untrusted disk images are served,
an attacker can trigger this by providing a crafted QCOW2 image with
ZSTD-compressed clusters. Potential heap data disclosure if the
process survives the over-read.
Fix:
Change line 712 from:
{ .src = compressed_cluster, .size = cluster_size, .pos = 0 };
to:
{ .src = compressed_cluster, .size = compressed_size, .pos = 0 };
----- End forwarded message -----
--
Richard Jones, Virtualization Group, Red Hat
http://people.redhat.com/~rjones
Read my programming and virtualization blog:
http://rwmj.wordpress.com
virt-builder quickly builds VMs from scratch
http://libguestfs.org/virt-builder.1.html