From b1c0524bf55c13ba025ce8da5151b7329a3740aa Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Fri, 7 Aug 2026 17:09:55 +0800 Subject: [PATCH v4] Fix detection of truncated compressed backups pg_verifybackup failed to detect truncated compressed tar backups for zstd, lz4 and gzip compression. astreamer_zstd_decompressor checked 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 state and verify at finalization that zstd completed the frame. Also flush any pending internally buffered output before performing that check. astreamer_gzip_decompressor did not check whether inflate() had reached Z_STREAM_END before finalizing. Record whether inflate() completed the gzip stream, and reject finalization if it did not. astreamer_lz4_decompressor did not check whether LZ4F_decompress() completed the final frame before finalizing. Record the status of decompression and reject finalization unless the frame completed. This prevents pg_verifybackup from accepting a truncated compressed tar backup. Author: Chao Li Reviewed-by: Discussion: https://postgr.es/m/5962B878-C43D-4EBC-9E95-1F945CE5E586@gmail.com --- src/bin/pg_verifybackup/t/010_client_untar.pl | 13 +++++ src/fe_utils/astreamer_gzip.c | 8 ++- src/fe_utils/astreamer_lz4.c | 20 +++++++ src/fe_utils/astreamer_zstd.c | 53 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl index db8b96ceb43..438658b52cb 100644 --- a/src/bin/pg_verifybackup/t/010_client_untar.pl +++ b/src/bin/pg_verifybackup/t/010_client_untar.pl @@ -138,6 +138,19 @@ for my $tc (@test_configuration) [ 'pg_verifybackup', '--exit-on-error', $backup_path, ], "verify backup, compression $method"); + if ($method ne 'none') + { + my $flen = -s $backup_path . "/" . $tc->{'backup_archive'}; + ok( truncate( + $backup_path . "/" . $tc->{'backup_archive'}, + $flen - 1), + "file truncated"); + $primary->command_fails_like( + [ 'pg_verifybackup', '--exit-on-error', $backup_path, ], + qr/could not decompress data/, + "backup is corrupted, compression $method"); + } + # Cleanup. rmtree($backup_path); } diff --git a/src/fe_utils/astreamer_gzip.c b/src/fe_utils/astreamer_gzip.c index bc3d53076e1..dc884eb3f1e 100644 --- a/src/fe_utils/astreamer_gzip.c +++ b/src/fe_utils/astreamer_gzip.c @@ -48,6 +48,7 @@ typedef struct astreamer_gzip_decompressor astreamer base; z_stream zstream; size_t bytes_written; + bool stream_finished; } astreamer_gzip_decompressor; static void astreamer_gzip_writer_content(astreamer *streamer, @@ -318,7 +319,9 @@ astreamer_gzip_decompressor_content(astreamer *streamer, */ res = inflate(zs, Z_NO_FLUSH); - if (res != Z_OK && res != Z_STREAM_END && res != Z_BUF_ERROR) + if (res == Z_STREAM_END) + mystreamer->stream_finished = true; + else if (res != Z_OK && res != Z_BUF_ERROR) pg_fatal("could not decompress data: %s", zs->msg ? zs->msg : "unknown error"); @@ -346,6 +349,9 @@ astreamer_gzip_decompressor_finalize(astreamer *streamer) mystreamer = (astreamer_gzip_decompressor *) streamer; + if (!mystreamer->stream_finished) + 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. diff --git a/src/fe_utils/astreamer_lz4.c b/src/fe_utils/astreamer_lz4.c index 12dfde2c837..92a29fa4581 100644 --- a/src/fe_utils/astreamer_lz4.c +++ b/src/fe_utils/astreamer_lz4.c @@ -25,6 +25,14 @@ #include "fe_utils/astreamer.h" #ifdef USE_LZ4 + +typedef enum +{ + STREAM_NEW, + STREAM_HAS_DATA, + STREAM_FINISHED, +} pg_stream_state; + typedef struct astreamer_lz4_frame { astreamer base; @@ -35,6 +43,7 @@ typedef struct astreamer_lz4_frame size_t bytes_written; bool header_written; + pg_stream_state state; } astreamer_lz4_frame; static void astreamer_lz4_compressor_content(astreamer *streamer, @@ -297,6 +306,7 @@ astreamer_lz4_decompressor_new(astreamer *next) pg_fatal("could not initialize compression library: %s", LZ4F_getErrorName(ctxError)); + streamer->state = STREAM_NEW; return &streamer->base; #else pg_fatal("this build does not support compression with %s", "LZ4"); @@ -358,6 +368,11 @@ astreamer_lz4_decompressor_content(astreamer *streamer, pg_fatal("could not decompress data: %s", LZ4F_getErrorName(ret)); + if (ret) + mystreamer->state = STREAM_HAS_DATA; + else + mystreamer->state = STREAM_FINISHED; + /* Update input buffer based on number of bytes consumed */ avail_in -= read_size; next_in += read_size; @@ -395,6 +410,11 @@ astreamer_lz4_decompressor_finalize(astreamer *streamer) mystreamer = (astreamer_lz4_frame *) streamer; + if (mystreamer->state == STREAM_HAS_DATA) + pg_fatal("could not decompress data: compressed stream is incomplete"); + else if (unlikely(mystreamer->state == STREAM_NEW)) + pg_fatal("could not decompress data: compressed stream is empty"); + /* * End of the stream, if there is some pending data in output buffers then * we must forward it to next streamer. diff --git a/src/fe_utils/astreamer_zstd.c b/src/fe_utils/astreamer_zstd.c index 98e8a700efe..33175185eb3 100644 --- a/src/fe_utils/astreamer_zstd.c +++ b/src/fe_utils/astreamer_zstd.c @@ -26,6 +26,13 @@ #ifdef USE_ZSTD +typedef enum +{ + STREAM_NEW, + STREAM_HAS_DATA, + STREAM_FINISHED, +} pg_stream_state; + typedef struct astreamer_zstd_frame { astreamer base; @@ -33,6 +40,7 @@ typedef struct astreamer_zstd_frame ZSTD_CCtx *cctx; ZSTD_DCtx *dctx; ZSTD_outBuffer zstd_outBuf; + pg_stream_state state; } astreamer_zstd_frame; static void astreamer_zstd_compressor_content(astreamer *streamer, @@ -280,6 +288,7 @@ astreamer_zstd_decompressor_new(astreamer *next) streamer->zstd_outBuf.size = streamer->base.bbs_buffer.maxlen; streamer->zstd_outBuf.pos = 0; + streamer->state = STREAM_NEW; return &streamer->base; #else pg_fatal("this build does not support compression with %s", "ZSTD"); @@ -329,6 +338,12 @@ astreamer_zstd_decompressor_content(astreamer *streamer, if (ZSTD_isError(ret)) pg_fatal("could not decompress data: %s", ZSTD_getErrorName(ret)); + + /* The stream is only done when ZSTD_decompressStream returns 0 */ + if (ret) + mystreamer->state = STREAM_HAS_DATA; + else + mystreamer->state = STREAM_FINISHED; } } @@ -340,6 +355,44 @@ 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->state == STREAM_HAS_DATA && + 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)); + + /* The stream is only done when ZSTD_decompressStream returns 0 */ + if (ret) + mystreamer->state = STREAM_HAS_DATA; + else + mystreamer->state = STREAM_FINISHED; + } + + if (mystreamer->state != STREAM_FINISHED) + pg_fatal("could not decompress data: compressed stream is incomplete"); + else if (unlikely(mystreamer->state == STREAM_NEW)) + pg_fatal("could not decompress data: compressed stream is empty"); + /* * End of the stream, if there is some pending data in output buffers then * we must forward it to next streamer. -- 2.39.3 (Apple Git-146)