From 3ea103de205d6c5600625ecfa11cdb5f10dfe0f0 Mon Sep 17 00:00:00 2001 From: Nick Terrell Date: Tue, 10 Nov 2026 15:51:41 -0800 Subject: [PATCH] Fix overlapping copies in CLI read-buffer coalescing AIO_ReadPool_consumeBytes() can advance srcBuffer into coalesceBuffer. A later refill moves the remaining bytes back to the beginning of the same buffer, so memcpy() is undefined when those ranges overlap. Use memmove() for this compaction. A valid 17-byte stream consisting of an empty skippable frame and an empty zstd frame reproduces a 9-byte copy from coalesceBuffer + 8 under ASan. Add a CLI regression covering test and decompression modes with async I/O enabled and disabled. Require successful decoding and empty output so sanitizer failures cannot pass as expected input errors. --- programs/fileio_asyncio.c | 3 ++- .../decompression/skippable-overlap.sh | 17 +++++++++++++++++ .../skippable-overlap.sh.stderr.exact | 0 .../skippable-overlap.sh.stdout.exact | 0 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 tests/cli-tests/decompression/skippable-overlap.sh create mode 100644 tests/cli-tests/decompression/skippable-overlap.sh.stderr.exact create mode 100644 tests/cli-tests/decompression/skippable-overlap.sh.stdout.exact diff --git a/programs/fileio_asyncio.c b/programs/fileio_asyncio.c index 42a47201656..c9d8ea1718e 100644 --- a/programs/fileio_asyncio.c +++ b/programs/fileio_asyncio.c @@ -614,7 +614,8 @@ size_t AIO_ReadPool_fillBuffer(ReadPoolCtx_t* ctx, size_t n) { * and coalesce the remaining bytes with the next job's buffer */ if (ctx->srcBufferLoaded > 0) { useCoalesce = 1; - memcpy(ctx->coalesceBuffer, ctx->srcBuffer, ctx->srcBufferLoaded); + /* Remaining bytes may already be in coalesceBuffer. */ + memmove(ctx->coalesceBuffer, ctx->srcBuffer, ctx->srcBufferLoaded); ctx->srcBuffer = ctx->coalesceBuffer; } diff --git a/tests/cli-tests/decompression/skippable-overlap.sh b/tests/cli-tests/decompression/skippable-overlap.sh new file mode 100755 index 00000000000..d72d26cc599 --- /dev/null +++ b/tests/cli-tests/decompression/skippable-overlap.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -eu + +# An 8-byte empty skippable frame followed by a 9-byte empty zstd frame. +# The initial 18-byte header refill moves this 17-byte stream into the +# coalesce buffer. After consuming the skippable frame, the next refill +# moves 9 bytes from offset 8 back to offset 0. Run under ASan to detect +# an overlapping memcpy. +printf '\120\052\115\030\000\000\000\000\050\265\057\375\040\000\001\000\000' > skippable-overlap.zst + +# This is a valid stream and must decode successfully to empty output. +for io in --asyncio --no-asyncio; do + for operation in -t -dc; do + zstd -q "$io" "$operation" skippable-overlap.zst + done +done diff --git a/tests/cli-tests/decompression/skippable-overlap.sh.stderr.exact b/tests/cli-tests/decompression/skippable-overlap.sh.stderr.exact new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/cli-tests/decompression/skippable-overlap.sh.stdout.exact b/tests/cli-tests/decompression/skippable-overlap.sh.stdout.exact new file mode 100644 index 00000000000..e69de29bb2d