From 4e6bda0bab753fb601eb52f3fe90002428c8cd65 Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Fri, 7 Aug 2026 17:09:55 +0800 Subject: [PATCH v1] Fix detection of truncated zstd-compressed backups astreamer_zstd_decompressor checked only whether ZSTD_decompressStream() returned an error. However, a positive return value at end-of-input means that the final zstd frame has not completed. Record the return value and verify at finalization that zstd completed the frame. Also flush any pending internally buffered output before performing that check. This prevents pg_verifybackup from accepting a truncated zstd-compressed tar backup. Author: Chao Li --- src/fe_utils/astreamer_zstd.c | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/fe_utils/astreamer_zstd.c b/src/fe_utils/astreamer_zstd.c index 98e8a700efe..126d204f3cc 100644 --- a/src/fe_utils/astreamer_zstd.c +++ b/src/fe_utils/astreamer_zstd.c @@ -33,6 +33,7 @@ typedef struct astreamer_zstd_frame ZSTD_CCtx *cctx; ZSTD_DCtx *dctx; ZSTD_outBuffer zstd_outBuf; + size_t decompression_ret; } astreamer_zstd_frame; static void astreamer_zstd_compressor_content(astreamer *streamer, @@ -279,6 +280,8 @@ astreamer_zstd_decompressor_new(astreamer *next) streamer->zstd_outBuf.dst = streamer->base.bbs_buffer.data; streamer->zstd_outBuf.size = streamer->base.bbs_buffer.maxlen; streamer->zstd_outBuf.pos = 0; + /* Reject empty input, which does not contain a complete zstd frame. */ + streamer->decompression_ret = 1; return &streamer->base; #else @@ -329,7 +332,10 @@ astreamer_zstd_decompressor_content(astreamer *streamer, if (ZSTD_isError(ret)) pg_fatal("could not decompress data: %s", ZSTD_getErrorName(ret)); + + mystreamer->decompression_ret = ret; } + } /* @@ -340,6 +346,38 @@ astreamer_zstd_decompressor_finalize(astreamer *streamer) { astreamer_zstd_frame *mystreamer = (astreamer_zstd_frame *) streamer; + /* + * A full output buffer with a positive return value might leave data in + * zstd's internal buffers. Call the decompressor with empty input until + * it has flushed that data. + */ + while (mystreamer->decompression_ret > 0 && + mystreamer->zstd_outBuf.pos == mystreamer->zstd_outBuf.size) + { + ZSTD_inBuffer empty = {NULL, 0, 0}; + size_t ret; + + astreamer_content(mystreamer->base.bbs_next, NULL, + mystreamer->zstd_outBuf.dst, + mystreamer->zstd_outBuf.pos, ASTREAMER_UNKNOWN); + + mystreamer->zstd_outBuf.dst = mystreamer->base.bbs_buffer.data; + mystreamer->zstd_outBuf.size = mystreamer->base.bbs_buffer.maxlen; + mystreamer->zstd_outBuf.pos = 0; + + ret = ZSTD_decompressStream(mystreamer->dctx, + &mystreamer->zstd_outBuf, &empty); + + if (ZSTD_isError(ret)) + pg_fatal("could not decompress data: %s", + ZSTD_getErrorName(ret)); + + mystreamer->decompression_ret = ret; + } + + if (mystreamer->decompression_ret != 0) + pg_fatal("could not decompress data: compressed stream is incomplete"); + /* * End of the stream, if there is some pending data in output buffers then * we must forward it to next streamer. -- 2.50.1 (Apple Git-155)