From be47a5d4bb89d2d095e10932f9adcdc94b7dda6a Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Fri, 7 Aug 2026 17:09:55 +0800 Subject: [PATCH v5] 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 Co-authored-by: Daniel Gustafsson 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 | 21 ++++++++ src/fe_utils/astreamer_zstd.c | 53 +++++++++++++++++++ 4 files changed, 94 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..dd2bd1d44a5 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, + FRAME_HAS_DATA, + FRAME_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,12 @@ astreamer_lz4_decompressor_content(astreamer *streamer, pg_fatal("could not decompress data: %s", LZ4F_getErrorName(ret)); + /* The frame is only done when LZ4F_decompress returns 0 */ + if (ret) + mystreamer->state = FRAME_HAS_DATA; + else + mystreamer->state = FRAME_FINISHED; + /* Update input buffer based on number of bytes consumed */ avail_in -= read_size; next_in += read_size; @@ -395,6 +411,11 @@ astreamer_lz4_decompressor_finalize(astreamer *streamer) mystreamer = (astreamer_lz4_frame *) streamer; + if (mystreamer->state == FRAME_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..065d1f8d251 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, + FRAME_HAS_DATA, + FRAME_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 frame is only done when ZSTD_decompressStream returns 0 */ + if (ret) + mystreamer->state = FRAME_HAS_DATA; + else + mystreamer->state = FRAME_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 == FRAME_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 frame is only done when ZSTD_decompressStream returns 0 */ + if (ret) + mystreamer->state = FRAME_HAS_DATA; + else + mystreamer->state = FRAME_FINISHED; + } + + if (unlikely(mystreamer->state == STREAM_NEW)) + pg_fatal("could not decompress data: compressed stream is empty"); + else if (mystreamer->state != FRAME_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. -- 2.50.1 (Apple Git-155)