No error checking when reading from file using zstd in pg_dump

Started by Evgeniy Gorbanev7 months ago29 messages
#1Evgeniy Gorbanev
gorbanyoves@basealt.ru
1 attachment(s)

Hello.

In src/bin/pg_dump/compress_zstd.c, the Zstd_read function always
returns true. But if you look at the Zstd_gets and Zstd_getc functions,
where Zstd_read is called via CFH->read_func, it is expected that
the Zstd_read function can also return false. In case of
a read-from-file error, the process is expected to terminate, but
pg_dump will continue the process.
I assume that after checking
if (cnt == 0)
should be
return false;

Patch attached.

Found by Linux Verification Center (linuxtesting.org) with SVACE.

Reporter: Evgeniy Gorbanev (gorbanyoves@basealt.ru).

Organization: BaseALT (org@basealt.ru).

Best regards,
Evgeniy

Attachments:

compress_zstd.difftext/x-patch; charset=UTF-8; name=compress_zstd.diffDownload
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..d0a58861228 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -298,7 +298,7 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 
 			/* If we have no more input to consume, we're done */
 			if (cnt == 0)
-				break;
+				return false;
 		}
 
 		while (input->pos < input->size)
#2Daniel Gustafsson
daniel@yesql.se
In reply to: Evgeniy Gorbanev (#1)
Re: No error checking when reading from file using zstd in pg_dump

On 16 Jun 2025, at 10:14, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:

In src/bin/pg_dump/compress_zstd.c, the Zstd_read function always
returns true. But if you look at the Zstd_gets and Zstd_getc functions,
where Zstd_read is called via CFH->read_func, it is expected that
the Zstd_read function can also return false. In case of
a read-from-file error, the process is expected to terminate, but
pg_dump will continue the process.
I assume that after checking
if (cnt == 0)
should be
return false;

 	if (cnt == 0)
-		break;
+		return false;

As cnt is coming from fread() returning false here would be wrong as you cannot
from 0 alone know if it's EOF or an error. Instead it needs to inspect the
stream with feof() and ferror() to know how to proceed.

--
Daniel Gustafsson

#3Evgeniy Gorbanev
gorbanyoves@basealt.ru
In reply to: Daniel Gustafsson (#2)
Re: No error checking when reading from file using zstd in pg_dump

16.06.2025 14:25, Daniel Gustafsson пишет:

On 16 Jun 2025, at 10:14, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:
In src/bin/pg_dump/compress_zstd.c, the Zstd_read function always
returns true. But if you look at the Zstd_gets and Zstd_getc functions,
where Zstd_read is called via CFH->read_func, it is expected that
the Zstd_read function can also return false. In case of
a read-from-file error, the process is expected to terminate, but
pg_dump will continue the process.
I assume that after checking
if (cnt == 0)
should be
return false;

if (cnt == 0)
-		break;
+		return false;

As cnt is coming from fread() returning false here would be wrong as you cannot
from 0 alone know if it's EOF or an error. Instead it needs to inspect the
stream with feof() and ferror() to know how to proceed.

--
Daniel Gustafsson

The feof() check is in the calling functions, e.g. in the Zstd_getc
function.

Regards, Evgeniy

#4Daniel Gustafsson
daniel@yesql.se
In reply to: Evgeniy Gorbanev (#3)
Re: No error checking when reading from file using zstd in pg_dump

On 16 Jun 2025, at 10:52, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:

16.06.2025 14:25, Daniel Gustafsson пишет:

On 16 Jun 2025, at 10:14, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:
In src/bin/pg_dump/compress_zstd.c, the Zstd_read function always
returns true. But if you look at the Zstd_gets and Zstd_getc functions,
where Zstd_read is called via CFH->read_func, it is expected that
the Zstd_read function can also return false. In case of
a read-from-file error, the process is expected to terminate, but
pg_dump will continue the process.
I assume that after checking
if (cnt == 0)
should be
return false;

if (cnt == 0)
- break;
+ return false;

As cnt is coming from fread() returning false here would be wrong as you cannot
from 0 alone know if it's EOF or an error. Instead it needs to inspect the
stream with feof() and ferror() to know how to proceed.

The feof() check is in the calling functions, e.g. in the Zstd_getc
function.

That function is using feof/ferror to log an appropriate error message, what
you are proposing is to consider all cases of EOF as an error. If you test
that you will see lots of test starting to fail.

--
Daniel Gustafsson

#5Evgeniy Gorbanev
gorbanyoves@basealt.ru
In reply to: Daniel Gustafsson (#4)
1 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

16.06.2025 15:00, Daniel Gustafsson пишет:

On 16 Jun 2025, at 10:52, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:

16.06.2025 14:25, Daniel Gustafsson пишет:

On 16 Jun 2025, at 10:14, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:
In src/bin/pg_dump/compress_zstd.c, the Zstd_read function always
returns true. But if you look at the Zstd_gets and Zstd_getc functions,
where Zstd_read is called via CFH->read_func, it is expected that
the Zstd_read function can also return false. In case of
a read-from-file error, the process is expected to terminate, but
pg_dump will continue the process.
I assume that after checking
if (cnt == 0)
should be
return false;

if (cnt == 0)
- break;
+ return false;

As cnt is coming from fread() returning false here would be wrong as you cannot
from 0 alone know if it's EOF or an error. Instead it needs to inspect the
stream with feof() and ferror() to know how to proceed.

The feof() check is in the calling functions, e.g. in the Zstd_getc
function.

That function is using feof/ferror to log an appropriate error message, what
you are proposing is to consider all cases of EOF as an error. If you test
that you will see lots of test starting to fail.

--
Daniel Gustafsson

I ran tests for pg_dump and they all passed. Logs attached.
Please check.

Attachments:

pg_dump_tests.logtext/x-log; charset=UTF-8; name=pg_dump_tests.logDownload
#6Daniel Gustafsson
daniel@yesql.se
In reply to: Evgeniy Gorbanev (#1)
Re: No error checking when reading from file using zstd in pg_dump

On 16 Jun 2025, at 11:26, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:

I ran tests for pg_dump and they all passed. Logs attached.

Files=7, Tests=11918, 35 wallclock secs ( 0.59 usr 0.07 sys + 7.92 cusr 4.32 csys = 12.90 CPU)

That seems like a low number of tests executed, with ZStd enabled I see over
13200 tests in the log. Are you sure you are building with ZStd compression
enabled?

--
Daniel Gustafsson

#7Evgeniy Gorbanev
gorbanyoves@basealt.ru
In reply to: Daniel Gustafsson (#6)
Re: No error checking when reading from file using zstd in pg_dump

16.06.2025 15:43, Daniel Gustafsson пишет:

On 16 Jun 2025, at 11:26, Evgeniy Gorbanev <gorbanyoves@basealt.ru> wrote:
I ran tests for pg_dump and they all passed. Logs attached.

Files=7, Tests=11918, 35 wallclock secs ( 0.59 usr 0.07 sys + 7.92 cusr 4.32 csys = 12.90 CPU)

That seems like a low number of tests executed, with ZStd enabled I see over
13200 tests in the log. Are you sure you are building with ZStd compression
enabled?

--
Daniel Gustafsson

Yes, you are right. Now I see the errors.

#8Tom Lane
tgl@sss.pgh.pa.us
In reply to: Evgeniy Gorbanev (#7)
Re: No error checking when reading from file using zstd in pg_dump

I think the real problem here is that what the code is doing is
precisely not what is documented in compress_io.h:

/*
* Read 'size' bytes of data from the file and store them into 'ptr'.
* Optionally it will store the number of bytes read in 'rsize'.
*
* Returns true on success and throws an internal error otherwise.
*/
bool (*read_func) (void *ptr, size_t size, size_t *rsize,
CompressFileHandle *CFH);

I've not checked to see what the other users of this API do, but
if they're all like this then we need to fix that comment.
Otherwise we probably need to make them all alike; even if there's
not a bug today, one will soon appear from someone believing the
comment.

regards, tom lane

#9Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#8)
Re: No error checking when reading from file using zstd in pg_dump

On 16 Jun 2025, at 15:56, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I've not checked to see what the other users of this API do, but
if they're all like this then we need to fix that comment.

AFAICT all other callers of this API are throwing an error with pg_fatal, and
so does the function in question for ZStd decompression errors. If we handle
the case of fread() returning 0 to indicate an error like the below *untested
sketch* (with a better error message) this function is fully API compliant as
well.

                /* If we have no more input to consume, we're done */
                if (cnt == 0)
+               {
+                       if (ferror(unconstify(void *, input->src)))
+                               pg_fatal("could not read data to decompress: %m");
+
                        break;
+               }

If this seems like a good approach then Zstd_getc can be simplified as well as
it no longer needs to call ferror, it still needs to check feof though.

--
Daniel Gustafsson

#10Tom Lane
tgl@sss.pgh.pa.us
In reply to: Daniel Gustafsson (#9)
Re: No error checking when reading from file using zstd in pg_dump

Daniel Gustafsson <daniel@yesql.se> writes:

On 16 Jun 2025, at 15:56, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I've not checked to see what the other users of this API do, but
if they're all like this then we need to fix that comment.

AFAICT all other callers of this API are throwing an error with pg_fatal, and
so does the function in question for ZStd decompression errors.

I think I agree that we need to handle the ferror() case inside the
read_func for consistency. But there is another problem, which is
that neither of its callers are paying attention to the API: as
defined, the read_func can never return anything but "true",
which is how Zstd_read behaves. But both the callers in
compress_zstd.c seem to think they should test its result to
detect EOF. Apparently, our tests do not cover the case of an
unexpected EOF.

This API is actually quite bizarrely defined, if you ask me.
Returning the byte count is optional, but if you don't pay
attention to the byte count you cannot know if you got any
data. At best, that's bug-encouraging.

regards, tom lane

#11Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#10)
1 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

On 16 Jun 2025, at 16:20, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Daniel Gustafsson <daniel@yesql.se> writes:

On 16 Jun 2025, at 15:56, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I've not checked to see what the other users of this API do, but
if they're all like this then we need to fix that comment.

AFAICT all other callers of this API are throwing an error with pg_fatal, and
so does the function in question for ZStd decompression errors.

I think I agree that we need to handle the ferror() case inside the
read_func for consistency. But there is another problem, which is
that neither of its callers are paying attention to the API: as
defined, the read_func can never return anything but "true",
which is how Zstd_read behaves. But both the callers in
compress_zstd.c seem to think they should test its result to
detect EOF.

Attached is a stab at fixing both the error handling in read_func as well as
the callers misuse of the API.

Apparently, our tests do not cover the case of an
unexpected EOF.

I admittedly ran out of steam when looking at adding something like this to our
pg_dump tests.

This API is actually quite bizarrely defined, if you ask me.
Returning the byte count is optional, but if you don't pay
attention to the byte count you cannot know if you got any
data. At best, that's bug-encouraging.

Agreed. Given that many of implementations in the gzip support code directly
return the gzXXX function I suspect it was modeled around GZip but thats an
unresearched guess. The fact that this returns Z_NULL where the API defines
NULL is unlikely to ever be an issue, but it also doesn't seem entirely neat..

--
Daniel Gustafsson

Attachments:

0001-pg_dump-Handle-errors-in-reading-ZStd-streams.patchapplication/octet-stream; name=0001-pg_dump-Handle-errors-in-reading-ZStd-streams.patch; x-unix-mode=0644Download
From d2d684901b10686c5cc4b98219fdd2300683a689 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Mon, 16 Jun 2025 17:28:35 +0200
Subject: [PATCH] pg_dump: Handle errors in reading ZStd streams

The read_func API is defined to always return true with errors during
reading generating a pg_fatal. The ZStd code was however not checking
if 0 returned from fread was due to error or an EOF, which would mask
errors as EOF. Also fix all internal callers to correctly use the API
and not check the returnvalue.

Reported-by: Evgeniy Gorbanev <gorbanyoves@basealt.ru>
Discussion: https://postgr.es/m/6b9817a8-88ec-4efd-b441-9e2a0439c6b8@basealt.ru
---
 src/bin/pg_dump/compress_zstd.c | 28 ++++++++++++++++++++++++----
 1 file changed, 24 insertions(+), 4 deletions(-)

diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..9e79619c955 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -272,6 +272,13 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 	output->dst = ptr;
 	output->pos = 0;
 
+	/*
+	 * Clear the error/eof flag before calling fread so that we can inspect
+	 * for errors after reading.  This also protects reading against fread
+	 * implementations which won't read when set.
+	 */
+	clearerr(zstdcs->fp);
+
 	for (;;)
 	{
 		Assert(input->pos <= input->size);
@@ -296,9 +303,15 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 
 			Assert(cnt <= input_allocated_size);
 
-			/* If we have no more input to consume, we're done */
+			/* check if the returned zero is due to EOF or an error */
 			if (cnt == 0)
+			{
+				if (ferror(zstdcs->fp))
+					pg_fatal("could not read from input file: %m");
+
+				/* If we have no more input to consume, we're done */
 				break;
+			}
 		}
 
 		while (input->pos < input->size)
@@ -366,8 +379,15 @@ Zstd_getc(CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	int			ret;
+	size_t		readsz;
 
-	if (CFH->read_func(&ret, 1, NULL, CFH) != 1)
+	CFH->read_func(&ret, 1, &readsz, CFH);
+
+	/*
+	 * read_func will throw an error on ferror but not feof, but getc_func is
+	 * defined to throw an error on EOF so we need to test that here.
+	 */
+	if (readsz == 0)
 	{
 		if (feof(zstdcs->fp))
 			pg_fatal("could not read from input file: end of file");
@@ -392,8 +412,8 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	{
 		size_t		readsz;
 
-		if (!CFH->read_func(&buf[i], 1, &readsz, CFH))
-			break;
+		CFH->read_func(&buf[i], 1, &readsz, CFH);
+
 		if (readsz != 1)
 			break;
 		if (buf[i] == '\n')
-- 
2.39.3 (Apple Git-146)

#12Tom Lane
tgl@sss.pgh.pa.us
In reply to: Daniel Gustafsson (#11)
1 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

Daniel Gustafsson <daniel@yesql.se> writes:

On 16 Jun 2025, at 16:20, Tom Lane <tgl@sss.pgh.pa.us> wrote:

This API is actually quite bizarrely defined, if you ask me.
Returning the byte count is optional, but if you don't pay
attention to the byte count you cannot know if you got any
data. At best, that's bug-encouraging.

Agreed. Given that many of implementations in the gzip support code directly
return the gzXXX function I suspect it was modeled around GZip but thats an
unresearched guess. The fact that this returns Z_NULL where the API defines
NULL is unlikely to ever be an issue, but it also doesn't seem entirely neat..

After looking around a bit more I realized that this API is a complete
disaster: it's not only bug-prone, but there are actual bugs in
multiple callers, eg _ReadBuf() totally fails to detect early EOF as
it intends to. We need to fix it, not slap some band-aids on.
Draft patch attached.

The write_func side is a bit of a mess too. I fixed some obvious API
violations in the attached, but I think we need to do more, because
it seems that zero thought was given to reporting errors sanely.
The callers all assume that strerror() is what to use to report an
incomplete write, but this is not appropriate in every case, for
instance we need to pay attention to gzerror() in gzip cases.
I'm inclined to think that the best answer is to move error reporting
into the write_funcs, and just make the API spec be "write or die".
I've not tackled that here though.

I was depressed to find that Zstd_getc reads one byte into
an otherwise-uninitialized int and then returns the whole int.
Even if we're lucky enough for the variable to start off zero,
this would fail utterly on big-endian machines. The only
reason we've not noticed is that the regression tests do not
reach Zstd_getc, nor most of the other getc_func implementations.

Now I'm afraid to look at the rest of the compress_io.h API.
If it's as badly written as these parts, there are more bugs
to be found.

regards, tom lane

Attachments:

v1-fix-broken-read_func-API.patchtext/x-diff; charset=us-ascii; name=v1-fix-broken-read_func-API.patchDownload
diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 5a30ebf9bf5..8db121b94a3 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -251,14 +251,14 @@ InitCompressorGzip(CompressorState *cs,
  *----------------------
  */
 
-static bool
-Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret <= 0 && !gzeof(gzfp))
+	if (gzret < 0)
 	{
 		int			errnum;
 		const char *errmsg = gzerror(gzfp, &errnum);
@@ -267,10 +267,7 @@ Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
 	}
 
-	if (rsize)
-		*rsize = (size_t) gzret;
-
-	return true;
+	return (size_t) gzret;
 }
 
 static bool
@@ -278,7 +275,7 @@ Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 
-	return gzwrite(gzfp, ptr, size) > 0;
+	return gzwrite(gzfp, ptr, size) == size;
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index db9b38744c8..dad0c91fec5 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -123,21 +123,23 @@ struct CompressFileHandle
 									CompressFileHandle *CFH);
 
 	/*
-	 * Read 'size' bytes of data from the file and store them into 'ptr'.
-	 * Optionally it will store the number of bytes read in 'rsize'.
+	 * Read up to 'size' bytes of data from the file and store them into
+	 * 'ptr'.
 	 *
-	 * Returns true on success and throws an internal error otherwise.
+	 * Returns number of bytes read (this might be less than 'size' if EOF was
+	 * reached).  Throws error for error conditions other than EOF.
 	 */
-	bool		(*read_func) (void *ptr, size_t size, size_t *rsize,
+	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
 
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error.
+	 * Returns true on success and false on error (it is caller's
+	 * responsibility to deal with error conditions).
 	 */
 	bool		(*write_func) (const void *ptr, size_t size,
-							   struct CompressFileHandle *CFH);
+							   CompressFileHandle *CFH);
 
 	/*
 	 * Read at most size - 1 characters from the compress file handle into
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index e99f0cad71f..c94f9dcd4cf 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -583,6 +583,7 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 			return false;
 		}
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -598,8 +599,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 /*
  * fread() equivalent implementation for LZ4 compressed files.
  */
-static bool
-LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+LZ4Stream_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	int			ret;
@@ -607,10 +608,7 @@ LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 	if ((ret = LZ4Stream_read_internal(state, ptr, size, false)) < 0)
 		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	if (rsize)
-		*rsize = (size_t) ret;
-
-	return true;
+	return (size_t) ret;
 }
 
 /*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 3fc89c99854..4924935a5bd 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -83,23 +83,17 @@ InitCompressorNone(CompressorState *cs,
  * Private routines
  */
 
-static bool
-read_none(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	FILE	   *fp = (FILE *) CFH->private_data;
 	size_t		ret;
 
-	if (size == 0)
-		return true;
-
 	ret = fread(ptr, 1, size, fp);
-	if (ret != size && !feof(fp))
+	if (ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
-	if (rsize)
-		*rsize = ret;
-
-	return true;
+	return ret;
 }
 
 static bool
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..8f718212d9f 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -258,8 +258,8 @@ InitCompressorZstd(CompressorState *cs,
  * Compressed stream API
  */
 
-static bool
-Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -292,6 +292,9 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 		if (input->pos == input->size)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
+			if (ferror(zstdcs->fp))
+				pg_fatal("could not read from input file: %m");
+
 			input->size = cnt;
 
 			Assert(cnt <= input_allocated_size);
@@ -320,10 +323,7 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			break;				/* We read all the data that fits */
 	}
 
-	if (rdsize != NULL)
-		*rdsize = output->pos;
-
-	return true;
+	return output->pos;
 }
 
 static bool
@@ -358,22 +358,16 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		}
 	}
 
-	return size;
+	return true;
 }
 
 static int
 Zstd_getc(CompressFileHandle *CFH)
 {
-	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
-	int			ret;
+	unsigned char ret;
 
-	if (CFH->read_func(&ret, 1, NULL, CFH) != 1)
-	{
-		if (feof(zstdcs->fp))
-			pg_fatal("could not read from input file: end of file");
-		else
-			pg_fatal("could not read from input file: %m");
-	}
+	if (CFH->read_func(&ret, 1, CFH) != 1)
+		pg_fatal("could not read from input file: end of file");
 	return ret;
 }
 
@@ -390,11 +384,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		size_t		readsz;
-
-		if (!CFH->read_func(&buf[i], 1, &readsz, CFH))
-			break;
-		if (readsz != 1)
+		if (CFH->read_func(&buf[i], 1, CFH) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index bc2a2fb4797..eb2ce8d5a6c 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -351,7 +351,7 @@ _EndData(ArchiveHandle *AH, TocEntry *te)
 static void
 _PrintFileData(ArchiveHandle *AH, char *filename)
 {
-	size_t		cnt = 0;
+	size_t		cnt;
 	char	   *buf;
 	size_t		buflen;
 	CompressFileHandle *CFH;
@@ -366,7 +366,7 @@ _PrintFileData(ArchiveHandle *AH, char *filename)
 	buflen = DEFAULT_IO_BUFFER_SIZE;
 	buf = pg_malloc(buflen);
 
-	while (CFH->read_func(buf, buflen, &cnt, CFH) && cnt > 0)
+	while ((cnt = CFH->read_func(buf, buflen, CFH)) > 0)
 	{
 		ahwrite(buf, 1, cnt, AH);
 	}
@@ -531,10 +531,10 @@ _ReadBuf(ArchiveHandle *AH, void *buf, size_t len)
 	CompressFileHandle *CFH = ctx->dataFH;
 
 	/*
-	 * If there was an I/O error, we already exited in readF(), so here we
-	 * exit on short reads.
+	 * We do not expect a short read, so fail if we get one.  The read_func
+	 * already dealt with any outright I/O error.
 	 */
-	if (!CFH->read_func(buf, len, NULL, CFH))
+	if (CFH->read_func(buf, len, CFH) != len)
 		pg_fatal("could not read from input file: end of file");
 }
 
#13Tomas Vondra
tomas@vondra.me
In reply to: Daniel Gustafsson (#11)
Re: No error checking when reading from file using zstd in pg_dump

On 6/16/25 17:41, Daniel Gustafsson wrote:

On 16 Jun 2025, at 16:20, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Daniel Gustafsson <daniel@yesql.se> writes:

On 16 Jun 2025, at 15:56, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I've not checked to see what the other users of this API do, but
if they're all like this then we need to fix that comment.

AFAICT all other callers of this API are throwing an error with pg_fatal, and
so does the function in question for ZStd decompression errors.

I think I agree that we need to handle the ferror() case inside the
read_func for consistency. But there is another problem, which is
that neither of its callers are paying attention to the API: as
defined, the read_func can never return anything but "true",
which is how Zstd_read behaves. But both the callers in
compress_zstd.c seem to think they should test its result to
detect EOF.

Attached is a stab at fixing both the error handling in read_func as well as
the callers misuse of the API.

Apparently, our tests do not cover the case of an
unexpected EOF.

I admittedly ran out of steam when looking at adding something like this to our
pg_dump tests.

This API is actually quite bizarrely defined, if you ask me.
Returning the byte count is optional, but if you don't pay
attention to the byte count you cannot know if you got any
data. At best, that's bug-encouraging.

Agreed. Given that many of implementations in the gzip support code directly
return the gzXXX function I suspect it was modeled around GZip but thats an
unresearched guess. The fact that this returns Z_NULL where the API defines
NULL is unlikely to ever be an issue, but it also doesn't seem entirely neat..

Yes. It's been a while since this commit, but I recall we started with a
gzip-only implementation. 16 introduced this API, but it's definitely
the case it was based on the initial gzip code.

Regarding the Z_NULL, I believe it has always been ignored like this, at
least since 9.1. The code simply returns what gzgets() returns, and then
compares that to NULL, etc. Is there there's a better way to deal with
Z_NULL? I suppose we could explicitly check/translate Z_NULL to NULL,
although Z_NULL is simply defined as 0. I don't recall if NULL has some
additional magic.

regards

--
Tomas Vondra

#14Tomas Vondra
tomas@vondra.me
In reply to: Tom Lane (#12)
Re: No error checking when reading from file using zstd in pg_dump

On 6/16/25 19:56, Tom Lane wrote:

...

After looking around a bit more I realized that this API is a complete
disaster: it's not only bug-prone, but there are actual bugs in
multiple callers, eg _ReadBuf() totally fails to detect early EOF as
it intends to. We need to fix it, not slap some band-aids on.
Draft patch attached.

The write_func side is a bit of a mess too. I fixed some obvious API
violations in the attached, but I think we need to do more, because
it seems that zero thought was given to reporting errors sanely.
The callers all assume that strerror() is what to use to report an
incomplete write, but this is not appropriate in every case, for
instance we need to pay attention to gzerror() in gzip cases.
I'm inclined to think that the best answer is to move error reporting
into the write_funcs, and just make the API spec be "write or die".
I've not tackled that here though.

I was depressed to find that Zstd_getc reads one byte into
an otherwise-uninitialized int and then returns the whole int.
Even if we're lucky enough for the variable to start off zero,
this would fail utterly on big-endian machines. The only
reason we've not noticed is that the regression tests do not
reach Zstd_getc, nor most of the other getc_func implementations.

Now I'm afraid to look at the rest of the compress_io.h API.
If it's as badly written as these parts, there are more bugs
to be found.

Well, that doesn't sound great :-( Most of this is likely my fault, as
the one who pushed e9960732a961 (and some commits that built on it).
Sorry about that. I'll take a fresh look at the commits this week, but
considering I missed the issues before commit ...

For a moment I was worried about breaking ABI when fixing this in the
backbranches, but I guess that's not an issue for tools like pg_dump.

regards

--
Tomas Vondra

#15Tom Lane
tgl@sss.pgh.pa.us
In reply to: Tomas Vondra (#14)
Re: No error checking when reading from file using zstd in pg_dump

Tomas Vondra <tomas@vondra.me> writes:

For a moment I was worried about breaking ABI when fixing this in the
backbranches, but I guess that's not an issue for tools like pg_dump.

Yeah, I think it'd be okay to change compress_io.h APIs in the back
branches; it's hard to see how anything outside pg_dump+pg_restore
would be depending on that.

regards, tom lane

#16Daniel Gustafsson
daniel@yesql.se
In reply to: Tomas Vondra (#13)
Re: No error checking when reading from file using zstd in pg_dump

On 16 Jun 2025, at 21:45, Tomas Vondra <tomas@vondra.me> wrote:

Regarding the Z_NULL, I believe it has always been ignored like this, at
least since 9.1. The code simply returns what gzgets() returns, and then
compares that to NULL, etc. Is there there's a better way to deal with
Z_NULL? I suppose we could explicitly check/translate Z_NULL to NULL,
although Z_NULL is simply defined as 0. I don't recall if NULL has some
additional magic.

Right, to be clear, I don't think there is a bug here (or the risk one going
forward). It's just my own preference of not mixing API concepts

--
Daniel Gustafsson

#17Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#15)
6 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

On 16 Jun 2025, at 22:49, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Tomas Vondra <tomas@vondra.me> writes:

For a moment I was worried about breaking ABI when fixing this in the
backbranches, but I guess that's not an issue for tools like pg_dump.

Yeah, I think it'd be okay to change compress_io.h APIs in the back
branches; it's hard to see how anything outside pg_dump+pg_restore
would be depending on that.

Agreed, and the attached patchset takes that one step further by also changing
the signature of write_func to make errorhandling easier. More on that below.

I spent a little bit of time reading over all the implementations and cross
referencing the API for conformity, and came up with the attached. The 0001
patch is the one from upstream, and each subsequent commit is fixing one
function for all the implementations. Before pushing it should all be squashed
into a single commit IMHO.

Each patch has a commitmessage which describes the what/why so I won't go into
full detail here, but the main changes introduced are:

* Make write_func throw an error on all error conditions
* Ensure that functions return an error when assumed to, and call pg_fatal
when assumed to not return on error
* Try to capture more errors and ensure that errno has been reset

The reason for the jump in version number is that I've traded patches off-list
with Tomas over the past few days, some of which are omitted here due to being
v19 material. I might well have missed some changes which aren't required to
backpatch, and if so we need to pull those out.

One thing this doesn't address is the lack of testing, which also should be
tackled (having fault injection capabilities for client utils would be neat).

--
Daniel Gustafsson

Attachments:

v5-0001-Initial-patch-by-Tom-Lane.patchapplication/octet-stream; name=v5-0001-Initial-patch-by-Tom-Lane.patch; x-unix-mode=0644Download
From 29aa480c2b3dd824fec1532d549498df1b02362e Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 11:46:54 +0200
Subject: [PATCH v5 1/6] Initial patch by Tom Lane

The patch posted by Tom in 644360.1750096605@sss.pgh.pa.us
verbatim without any changes.
---
 src/bin/pg_dump/compress_gzip.c       | 13 +++++------
 src/bin/pg_dump/compress_io.h         | 14 +++++++-----
 src/bin/pg_dump/compress_lz4.c        | 10 ++++-----
 src/bin/pg_dump/compress_none.c       | 14 ++++--------
 src/bin/pg_dump/compress_zstd.c       | 32 +++++++++------------------
 src/bin/pg_dump/pg_backup_directory.c | 10 ++++-----
 6 files changed, 37 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 5a30ebf9bf5..8db121b94a3 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -251,14 +251,14 @@ InitCompressorGzip(CompressorState *cs,
  *----------------------
  */
 
-static bool
-Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret <= 0 && !gzeof(gzfp))
+	if (gzret < 0)
 	{
 		int			errnum;
 		const char *errmsg = gzerror(gzfp, &errnum);
@@ -267,10 +267,7 @@ Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
 	}
 
-	if (rsize)
-		*rsize = (size_t) gzret;
-
-	return true;
+	return (size_t) gzret;
 }
 
 static bool
@@ -278,7 +275,7 @@ Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 
-	return gzwrite(gzfp, ptr, size) > 0;
+	return gzwrite(gzfp, ptr, size) == size;
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index db9b38744c8..dad0c91fec5 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -123,21 +123,23 @@ struct CompressFileHandle
 									CompressFileHandle *CFH);
 
 	/*
-	 * Read 'size' bytes of data from the file and store them into 'ptr'.
-	 * Optionally it will store the number of bytes read in 'rsize'.
+	 * Read up to 'size' bytes of data from the file and store them into
+	 * 'ptr'.
 	 *
-	 * Returns true on success and throws an internal error otherwise.
+	 * Returns number of bytes read (this might be less than 'size' if EOF was
+	 * reached).  Throws error for error conditions other than EOF.
 	 */
-	bool		(*read_func) (void *ptr, size_t size, size_t *rsize,
+	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
 
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error.
+	 * Returns true on success and false on error (it is caller's
+	 * responsibility to deal with error conditions).
 	 */
 	bool		(*write_func) (const void *ptr, size_t size,
-							   struct CompressFileHandle *CFH);
+							   CompressFileHandle *CFH);
 
 	/*
 	 * Read at most size - 1 characters from the compress file handle into
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index e99f0cad71f..c94f9dcd4cf 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -583,6 +583,7 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 			return false;
 		}
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -598,8 +599,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 /*
  * fread() equivalent implementation for LZ4 compressed files.
  */
-static bool
-LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+LZ4Stream_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	int			ret;
@@ -607,10 +608,7 @@ LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 	if ((ret = LZ4Stream_read_internal(state, ptr, size, false)) < 0)
 		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	if (rsize)
-		*rsize = (size_t) ret;
-
-	return true;
+	return (size_t) ret;
 }
 
 /*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 3fc89c99854..4924935a5bd 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -83,23 +83,17 @@ InitCompressorNone(CompressorState *cs,
  * Private routines
  */
 
-static bool
-read_none(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	FILE	   *fp = (FILE *) CFH->private_data;
 	size_t		ret;
 
-	if (size == 0)
-		return true;
-
 	ret = fread(ptr, 1, size, fp);
-	if (ret != size && !feof(fp))
+	if (ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
-	if (rsize)
-		*rsize = ret;
-
-	return true;
+	return ret;
 }
 
 static bool
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..8f718212d9f 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -258,8 +258,8 @@ InitCompressorZstd(CompressorState *cs,
  * Compressed stream API
  */
 
-static bool
-Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -292,6 +292,9 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 		if (input->pos == input->size)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
+			if (ferror(zstdcs->fp))
+				pg_fatal("could not read from input file: %m");
+
 			input->size = cnt;
 
 			Assert(cnt <= input_allocated_size);
@@ -320,10 +323,7 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			break;				/* We read all the data that fits */
 	}
 
-	if (rdsize != NULL)
-		*rdsize = output->pos;
-
-	return true;
+	return output->pos;
 }
 
 static bool
@@ -358,22 +358,16 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		}
 	}
 
-	return size;
+	return true;
 }
 
 static int
 Zstd_getc(CompressFileHandle *CFH)
 {
-	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
-	int			ret;
+	unsigned char ret;
 
-	if (CFH->read_func(&ret, 1, NULL, CFH) != 1)
-	{
-		if (feof(zstdcs->fp))
-			pg_fatal("could not read from input file: end of file");
-		else
-			pg_fatal("could not read from input file: %m");
-	}
+	if (CFH->read_func(&ret, 1, CFH) != 1)
+		pg_fatal("could not read from input file: end of file");
 	return ret;
 }
 
@@ -390,11 +384,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		size_t		readsz;
-
-		if (!CFH->read_func(&buf[i], 1, &readsz, CFH))
-			break;
-		if (readsz != 1)
+		if (CFH->read_func(&buf[i], 1, CFH) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index bc2a2fb4797..eb2ce8d5a6c 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -351,7 +351,7 @@ _EndData(ArchiveHandle *AH, TocEntry *te)
 static void
 _PrintFileData(ArchiveHandle *AH, char *filename)
 {
-	size_t		cnt = 0;
+	size_t		cnt;
 	char	   *buf;
 	size_t		buflen;
 	CompressFileHandle *CFH;
@@ -366,7 +366,7 @@ _PrintFileData(ArchiveHandle *AH, char *filename)
 	buflen = DEFAULT_IO_BUFFER_SIZE;
 	buf = pg_malloc(buflen);
 
-	while (CFH->read_func(buf, buflen, &cnt, CFH) && cnt > 0)
+	while ((cnt = CFH->read_func(buf, buflen, CFH)) > 0)
 	{
 		ahwrite(buf, 1, cnt, AH);
 	}
@@ -531,10 +531,10 @@ _ReadBuf(ArchiveHandle *AH, void *buf, size_t len)
 	CompressFileHandle *CFH = ctx->dataFH;
 
 	/*
-	 * If there was an I/O error, we already exited in readF(), so here we
-	 * exit on short reads.
+	 * We do not expect a short read, so fail if we get one.  The read_func
+	 * already dealt with any outright I/O error.
 	 */
-	if (!CFH->read_func(buf, len, NULL, CFH))
+	if (CFH->read_func(buf, len, CFH) != len)
 		pg_fatal("could not read from input file: end of file");
 }
 
-- 
2.39.3 (Apple Git-146)

v5-0002-pg_dump-compression-API-write_func.patchapplication/octet-stream; name=v5-0002-pg_dump-compression-API-write_func.patch; x-unix-mode=0644Download
From ea3f5767a45e3b522559ad1538d03aa58f3a4099 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:24:20 +0200
Subject: [PATCH v5 2/6] pg_dump compression API: write_func

Make write_func throw an error on all error conditions.  All callers of
write_func were already checking for success and calling pg_fatal on all
errors, so we might as well make the API support that case directly with
simpler errorhandling as a result.
---
 src/bin/pg_dump/compress_gzip.c       | 11 +++++--
 src/bin/pg_dump/compress_io.h         |  5 ++--
 src/bin/pg_dump/compress_lz4.c        | 17 ++++-------
 src/bin/pg_dump/compress_none.c       |  7 ++---
 src/bin/pg_dump/compress_zstd.c       | 15 +++-------
 src/bin/pg_dump/pg_backup_archiver.c  |  4 +--
 src/bin/pg_dump/pg_backup_directory.c | 42 ++++-----------------------
 7 files changed, 31 insertions(+), 70 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 8db121b94a3..2edcbffdd8d 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -270,12 +270,19 @@ Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	return (size_t) gzret;
 }
 
-static bool
+static void
 Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
+	int			errnum;
+	const char *errmsg;
 
-	return gzwrite(gzfp, ptr, size) == size;
+	if (gzwrite(gzfp, ptr, size) != size)
+	{
+		errmsg = gzerror(gzfp, &errnum);
+		pg_fatal("could not write to file: %s",
+				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
+	}
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index dad0c91fec5..0f678a155e2 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -135,10 +135,9 @@ struct CompressFileHandle
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error (it is caller's
-	 * responsibility to deal with error conditions).
+	 * Returns true on success and throws error for all error conditions.
 	 */
-	bool		(*write_func) (const void *ptr, size_t size,
+	void		(*write_func) (const void *ptr, size_t size,
 							   CompressFileHandle *CFH);
 
 	/*
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index c94f9dcd4cf..67f2e8e3dce 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -558,7 +558,7 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 /*
  * Compress size bytes from ptr and write them to the stream.
  */
-static bool
+static void
 LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
@@ -567,7 +567,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, true))
-		return false;
+		pg_fatal("unable to initialize LZ4 library: %s",
+				 LZ4F_getErrorName(state->errcode));
 
 	while (remaining > 0)
 	{
@@ -578,22 +579,14 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		status = LZ4F_compressUpdate(state->ctx, state->buffer, state->buflen,
 									 ptr, chunk, NULL);
 		if (LZ4F_isError(status))
-		{
-			state->errcode = status;
-			return false;
-		}
+			pg_fatal("error during writing: %s", LZ4F_getErrorName(status));
 
 		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
-		{
-			errno = (errno) ? errno : ENOSPC;
-			return false;
-		}
+			pg_fatal("error during writing: %m");
 
 		ptr = ((const char *) ptr) + chunk;
 	}
-
-	return true;
 }
 
 /*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 4924935a5bd..7c456136096 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -96,16 +96,15 @@ read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 	return ret;
 }
 
-static bool
+static void
 write_none(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	size_t		ret;
 
+	errno = 0;
 	ret = fwrite(ptr, 1, size, (FILE *) CFH->private_data);
 	if (ret != size)
-		return false;
-
-	return true;
+		pg_fatal("coud not write to file: %m");
 }
 
 static const char *
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 8f718212d9f..96e8b9d8057 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -326,7 +326,7 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	return output->pos;
 }
 
-static bool
+static void
 Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
@@ -345,20 +345,13 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		output->pos = 0;
 		res = ZSTD_compressStream2(zstdcs->cstream, output, input, ZSTD_e_continue);
 		if (ZSTD_isError(res))
-		{
-			zstdcs->zstderror = ZSTD_getErrorName(res);
-			return false;
-		}
+			pg_fatal("could not write to file: %s", ZSTD_getErrorName(res));
 
+		errno = 0;
 		cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 		if (cnt != output->pos)
-		{
-			zstdcs->zstderror = strerror(errno);
-			return false;
-		}
+			pg_fatal("could not write to file: %m");
 	}
-
-	return true;
 }
 
 static int
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93..9145598ff33 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1868,8 +1868,8 @@ ahwrite(const void *ptr, size_t size, size_t nmemb, ArchiveHandle *AH)
 	{
 		CompressFileHandle *CFH = (CompressFileHandle *) AH->OF;
 
-		if (CFH->write_func(ptr, size * nmemb, CFH))
-			bytes_written = size * nmemb;
+		CFH->write_func(ptr, size * nmemb, CFH);
+		bytes_written = size * nmemb;
 	}
 
 	if (bytes_written != size * nmemb)
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index eb2ce8d5a6c..94d401d8a4e 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -316,15 +316,9 @@ _WriteData(ArchiveHandle *AH, const void *data, size_t dLen)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (dLen > 0 && !CFH->write_func(data, dLen, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	if (dLen <= 0)
+		return;
+	CFH->write_func(data, dLen, CFH);
 }
 
 /*
@@ -470,16 +464,7 @@ _WriteByte(ArchiveHandle *AH, const int i)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(&c, 1, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
-
+	CFH->write_func(&c, 1, CFH);
 	return 1;
 }
 
@@ -508,15 +493,7 @@ _WriteBuf(ArchiveHandle *AH, const void *buf, size_t len)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
@@ -677,14 +654,7 @@ _EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid)
 
 	/* register the LO in blobs_NNN.toc */
 	len = snprintf(buf, sizeof(buf), "%u blob_%u.dat\n", oid, oid);
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to LOs TOC file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v5-0003-pg_dump-compression-API-open_func.patchapplication/octet-stream; name=v5-0003-pg_dump-compression-API-open_func.patch; x-unix-mode=0644Download
From 6bd143d710f86c4c4d2444bf1131c4034ff531dd Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:38:23 +0200
Subject: [PATCH v5 3/6] pg_dump compression API: open_func

open_func is defined to return true on success and false on error,
with errorhandling left to the caller.

 * zstd: move stream initialization from the open function to read
   and write functions as they can have fatal errors.  Also ensure
   to dup() the file descriptor like none and gzip already do.
 * lz4: Ensure to dup() the file descriptor like none and gzip
   already do.

A TODO to future self is to move all initialization into the init
function, but that requires passing the mode in order to do it in
a sane manner.  This is left as an excercise for v19.
---
 src/bin/pg_dump/compress_io.c   |  1 +
 src/bin/pg_dump/compress_lz4.c  | 10 +++---
 src/bin/pg_dump/compress_zstd.c | 62 ++++++++++++++++++++++-----------
 3 files changed, 46 insertions(+), 27 deletions(-)

diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index 8c3d9c911c4..cf5358de741 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -269,6 +269,7 @@ InitDiscoverCompressFileHandle(const char *path, const char *mode)
 	}
 
 	CFH = InitCompressFileHandle(compression_spec);
+	errno = 0;
 	if (!CFH->open_func(fname, -1, mode, CFH))
 	{
 		free_keep_errno(CFH);
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 67f2e8e3dce..2e1a3c33386 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -12,6 +12,7 @@
  *-------------------------------------------------------------------------
  */
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_lz4.h"
 #include "pg_backup_utils.h"
@@ -702,21 +703,18 @@ static bool
 LZ4Stream_open(const char *path, int fd, const char *mode,
 			   CompressFileHandle *CFH)
 {
-	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		state->fp = fdopen(dup(fd), mode);
 	else
-		fp = fopen(path, mode);
-	if (fp == NULL)
+		state->fp = fopen(path, mode);
+	if (state->fp == NULL)
 	{
 		state->errcode = errno;
 		return false;
 	}
 
-	state->fp = fp;
-
 	return true;
 }
 
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 96e8b9d8057..1a8fcd4d508 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -13,6 +13,7 @@
  */
 
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_zstd.h"
 #include "pg_backup_utils.h"
@@ -268,6 +269,18 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	size_t		res,
 				cnt;
 
+	/*
+	 * If this is the first call to the reading function, initialize the
+	 * required datastructures.
+	 */
+	if (zstdcs->dstream == NULL)
+	{
+		zstdcs->input.src = pg_malloc0(input_allocated_size);
+		zstdcs->dstream = ZSTD_createDStream();
+		if (zstdcs->dstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	output->size = size;
 	output->dst = ptr;
 	output->pos = 0;
@@ -339,6 +352,15 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 	input->size = size;
 	input->pos = 0;
 
+	if (zstdcs->cstream == NULL)
+	{
+		zstdcs->output.size = ZSTD_CStreamOutSize();
+		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
+		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
+		if (zstdcs->cstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	/* Consume all input, to be flushed later */
 	while (input->pos != input->size)
 	{
@@ -455,35 +477,33 @@ Zstd_open(const char *path, int fd, const char *mode,
 	FILE	   *fp;
 	ZstdCompressorState *zstdcs;
 
+	/*
+	 * Clear state storage to avoid having the fd point to non-NULL memory on
+	 * error return.
+	 */
+	CFH->private_data = NULL;
+
+	zstdcs = (ZstdCompressorState *) pg_malloc_extended(sizeof(*zstdcs),
+														MCXT_ALLOC_NO_OOM | MCXT_ALLOC_ZERO);
+	if (!zstdcs)
+	{
+		errno = ENOMEM;
+		return false;
+	}
+
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		fp = fdopen(dup(fd), mode);
 	else
 		fp = fopen(path, mode);
 
 	if (fp == NULL)
+	{
+		pg_free(zstdcs);
 		return false;
+	}
 
-	zstdcs = (ZstdCompressorState *) pg_malloc0(sizeof(*zstdcs));
-	CFH->private_data = zstdcs;
 	zstdcs->fp = fp;
-
-	if (mode[0] == 'r')
-	{
-		zstdcs->input.src = pg_malloc0(ZSTD_DStreamInSize());
-		zstdcs->dstream = ZSTD_createDStream();
-		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else if (mode[0] == 'w' || mode[0] == 'a')
-	{
-		zstdcs->output.size = ZSTD_CStreamOutSize();
-		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
-		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
-		if (zstdcs->cstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else
-		pg_fatal("unhandled mode \"%s\"", mode);
+	CFH->private_data = zstdcs;
 
 	return true;
 }
-- 
2.39.3 (Apple Git-146)

v5-0004-pg_dump-compression-API-close_func.patchapplication/octet-stream; name=v5-0004-pg_dump-compression-API-close_func.patch; x-unix-mode=0644Download
From c4dc6e1fb6f9a6b18be3b4bf7c400c0b3ef980b3 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:48:15 +0200
Subject: [PATCH v5 4/6] pg_dump compression API: close_func

close_func is defined as returning true on success and false on error
leaving errorhandling to the caller.

 * zstd: Ensure to close the FD even if closing down the (de)compressor
   fails, and clean up state allocation on fclose failures.  Make sure
   to capture errors set by fclose.
 * lz4: Ensure to close the FD even if closing down the (de)compressor
   fails and don't call pg_fatal but instead log the failures using
   pg_log_error. Make sure to capture errors set by fclose.
 * none: Make sure to catch errors set by fclose.
---
 src/bin/pg_dump/compress_io.c   |  1 +
 src/bin/pg_dump/compress_lz4.c  | 36 ++++++++++++++++++++++++---------
 src/bin/pg_dump/compress_none.c |  5 +++++
 src/bin/pg_dump/compress_zstd.c | 17 ++++++++++++----
 4 files changed, 45 insertions(+), 14 deletions(-)

diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index cf5358de741..9cadc6f2a3f 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -290,6 +290,7 @@ EndCompressFileHandle(CompressFileHandle *CFH)
 {
 	bool		ret = false;
 
+	errno = 0;
 	if (CFH->private_data)
 		ret = CFH->close_func(CFH);
 
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 2e1a3c33386..a34c1119f22 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -661,6 +661,7 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	size_t		status;
+	int			ret;
 
 	fp = state->fp;
 	if (state->inited)
@@ -669,25 +670,31 @@ LZ4Stream_close(CompressFileHandle *CFH)
 		{
 			status = LZ4F_compressEnd(state->ctx, state->buffer, state->buflen, NULL);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
-			else if (fwrite(state->buffer, 1, status, state->fp) != status)
 			{
-				errno = (errno) ? errno : ENOSPC;
-				WRITE_ERROR_EXIT;
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
+			}
+			else
+			{
+				errno = 0;
+				if (fwrite(state->buffer, 1, status, state->fp) != status)
+				{
+					errno = (errno) ? errno : ENOSPC;
+					pg_log_error("could not write to output file: %m");
+				}
 			}
 
 			status = LZ4F_freeCompressionContext(state->ctx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
 		}
 		else
 		{
 			status = LZ4F_freeDecompressionContext(state->dtx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end decompression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end decompression: %s",
+							 LZ4F_getErrorName(status));
 			pg_free(state->overflowbuf);
 		}
 
@@ -695,8 +702,17 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	}
 
 	pg_free(state);
+	CFH->private_data = NULL;
+
+	errno = 0;
+	ret = fclose(fp);
+	if (ret != 0)
+	{
+		pg_log_error("could not close file: %m");
+		return false;
+	}
 
-	return fclose(fp) == 0;
+	return true;
 }
 
 static bool
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 7c456136096..452151fb9a2 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -146,7 +146,12 @@ close_none(CompressFileHandle *CFH)
 	CFH->private_data = NULL;
 
 	if (fp)
+	{
+		errno = 0;
 		ret = fclose(fp);
+		if (ret != 0)
+			pg_log_error("could not close file: %m");
+	}
 
 	return ret == 0;
 }
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 1a8fcd4d508..c0ee5654cb2 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -415,6 +415,7 @@ static bool
 Zstd_close(CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
+	bool		success = true;
 
 	if (zstdcs->cstream)
 	{
@@ -431,14 +432,17 @@ Zstd_close(CompressFileHandle *CFH)
 			if (ZSTD_isError(res))
 			{
 				zstdcs->zstderror = ZSTD_getErrorName(res);
-				return false;
+				success = false;
+				break;
 			}
 
+			errno = 0;
 			cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 			if (cnt != output->pos)
 			{
 				zstdcs->zstderror = strerror(errno);
-				return false;
+				success = false;
+				break;
 			}
 
 			if (res == 0)
@@ -455,11 +459,16 @@ Zstd_close(CompressFileHandle *CFH)
 		pg_free(unconstify(void *, zstdcs->input.src));
 	}
 
+	errno = 0;
 	if (fclose(zstdcs->fp) != 0)
-		return false;
+	{
+		zstdcs->zstderror = strerror(errno);
+		success = false;
+	}
 
 	pg_free(zstdcs);
-	return true;
+	CFH->private_data = NULL;
+	return success;
 }
 
 static bool
-- 
2.39.3 (Apple Git-146)

v5-0005-pg_dump-compression-API-LZ4Stream_init.patchapplication/octet-stream; name=v5-0005-pg_dump-compression-API-LZ4Stream_init.patch; x-unix-mode=0644Download
From a78784536c023b6ec6476c0031c84f0f5bc1e1d4 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 15:08:12 +0200
Subject: [PATCH v5 5/6] pg_dump compression API: LZ4Stream_init

Make sure to not switch to state inited until we know that initialization
succeeded and reset errno just in case.
---
 src/bin/pg_dump/compress_lz4.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index a34c1119f22..82d6c0cd55d 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -359,7 +359,6 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		return true;
 
 	state->compressing = compressing;
-	state->inited = true;
 
 	/* When compressing, write LZ4 header to the output stream. */
 	if (state->compressing)
@@ -368,6 +367,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		if (!LZ4State_compression_init(state))
 			return false;
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, state->compressedlen, state->fp) != state->compressedlen)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -391,6 +391,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		state->overflowlen = 0;
 	}
 
+	state->inited = true;
 	return true;
 }
 
-- 
2.39.3 (Apple Git-146)

v5-0006-pg_dump-compression-API-read_func-gets_func.patchapplication/octet-stream; name=v5-0006-pg_dump-compression-API-read_func-gets_func.patch; x-unix-mode=0644Download
From 14e49ca9eadb0e62245f4fa2139e6bc130f01041 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 15:53:00 +0200
Subject: [PATCH v5 6/6] pg_dump compression API: read_func / gets_func

read_func is defined to return the number of bytes read, zero for EOF
and throw an error on all error conditions

 * lz4: Make sure to call throw an error and not return -1
 * gzip: gzread returning zero cannot be assumed to indicate EOF as
   it is documented to return zero for some types of errors.
 * none: Don't call ferror on every fread unless it returne zero and
   we can expect there to be an error or EOF.

gets_func is defined go return *s on success and NULL on error or
if EOF is found before any char has been read.

 * lz4, zstd: Convert _read_internal functions to not automatically
   call pg_fatal on errors to be able to handle gets returning NULL
   on error.
---
 src/bin/pg_dump/compress_gzip.c | 15 +++++++++++++--
 src/bin/pg_dump/compress_lz4.c  | 19 +++++++++++++++----
 src/bin/pg_dump/compress_none.c |  2 +-
 src/bin/pg_dump/compress_zstd.c | 28 +++++++++++++++++++++++-----
 4 files changed, 52 insertions(+), 12 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 2edcbffdd8d..4a067e1402c 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -258,10 +258,21 @@ Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret < 0)
+
+	/*
+	 * gzread returns zero on EOF as well as some error conditions, and less
+	 * than zero on other error conditions, so we need to inspect for EOF on
+	 * zero.
+	 */
+	if (gzret <= 0)
 	{
 		int			errnum;
-		const char *errmsg = gzerror(gzfp, &errnum);
+		const char *errmsg;
+
+		if (gzret == 0 && gzeof(gzfp))
+			return 0;
+
+		errmsg = gzerror(gzfp, &errnum);
 
 		pg_fatal("could not read from input file: %s",
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 82d6c0cd55d..d0a094090a2 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -459,7 +459,11 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, false /* decompressing */ ))
+	{
+		pg_log_error("unable to initialize LZ4 library: %s",
+					 LZ4F_getErrorName(state->errcode));
 		return -1;
+	}
 
 	/* No work needs to be done for a zero-sized output buffer */
 	if (size <= 0)
@@ -486,7 +490,10 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 		rsize = fread(readbuf, 1, size, state->fp);
 		if (rsize < size && !feof(state->fp))
+		{
+			pg_log_error("could not read from input file: %m");
 			return -1;
+		}
 
 		rp = (char *) readbuf;
 		rend = (char *) readbuf + rsize;
@@ -503,6 +510,8 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 			if (LZ4F_isError(status))
 			{
 				state->errcode = status;
+				pg_log_error("could not read from input file: %s",
+							 LZ4F_getErrorName(state->errcode));
 				return -1;
 			}
 
@@ -636,11 +645,13 @@ LZ4Stream_gets(char *ptr, int size, CompressFileHandle *CFH)
 	int			ret;
 
 	ret = LZ4Stream_read_internal(state, ptr, size - 1, true);
-	if (ret < 0 || (ret == 0 && !LZ4Stream_eof(CFH)))
-		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	/* Done reading */
-	if (ret == 0)
+	/*
+	 * LZ4Stream_read_internal returning 0 or -1 means that it was either an
+	 * EOF or an error, but gets_func is defined to return NULL in either case
+	 * so we can treat both the same here.
+	 */
+	if (ret <= 0)
 		return NULL;
 
 	/*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 452151fb9a2..1d52fa7c367 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -90,7 +90,7 @@ read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 	size_t		ret;
 
 	ret = fread(ptr, 1, size, fp);
-	if (ferror(fp))
+	if (ret == 0 && ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
 	return ret;
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index c0ee5654cb2..faeee57a90f 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -260,7 +260,7 @@ InitCompressorZstd(CompressorState *cs,
  */
 
 static size_t
-Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+Zstd_read_internal(void *ptr, size_t size, CompressFileHandle *CFH, bool exit_on_error)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -278,7 +278,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 		zstdcs->input.src = pg_malloc0(input_allocated_size);
 		zstdcs->dstream = ZSTD_createDStream();
 		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
+		{
+			if (exit_on_error)
+				pg_fatal("could not initialize compression library");
+			return -1;
+		}
 	}
 
 	output->size = size;
@@ -306,7 +310,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
 			if (ferror(zstdcs->fp))
-				pg_fatal("could not read from input file: %m");
+			{
+				if (exit_on_error)
+					pg_fatal("could not read from input file: %m");
+				return -1;
+			}
 
 			input->size = cnt;
 
@@ -323,7 +331,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 			res = ZSTD_decompressStream(zstdcs->dstream, output, input);
 
 			if (ZSTD_isError(res))
-				pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+			{
+				if (exit_on_error)
+					pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+				return -1;
+			}
 
 			if (output->pos == output->size)
 				break;			/* No more room for output */
@@ -399,7 +411,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		if (CFH->read_func(&buf[i], 1, CFH) != 1)
+		if (Zstd_read_internal(&buf[i], 1, CFH, false) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
@@ -411,6 +423,12 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	return i > 0 ? buf : NULL;
 }
 
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+{
+	return Zstd_read_internal(ptr, size, CFH, true);
+}
+
 static bool
 Zstd_close(CompressFileHandle *CFH)
 {
-- 
2.39.3 (Apple Git-146)

#18Tom Lane
tgl@sss.pgh.pa.us
In reply to: Daniel Gustafsson (#17)
Re: No error checking when reading from file using zstd in pg_dump

Daniel Gustafsson <daniel@yesql.se> writes:

I spent a little bit of time reading over all the implementations and cross
referencing the API for conformity, and came up with the attached. The 0001
patch is the one from upstream, and each subsequent commit is fixing one
function for all the implementations. Before pushing it should all be squashed
into a single commit IMHO.

Thanks for tackling this!

I looked over this patchset briefly, and found a couple of nits:

v5-0002, in compress_io.h:

+ * Returns true on success and throws error for all error conditions.

It doesn't return true anymore. Should be more like

+ * Returns nothing. Exits via pg_fatal for all error conditions.

In LZ4Stream_write: you dropped the bit about

- errno = (errno) ? errno : ENOSPC;

but I think that's still necessary: we must assume ENOSPC if fwrite
doesn't set errno. Other fwrite callers (write_none, Zstd_write) need
this too. v5-0004 has an instance too, in Zstd_close. I did not check
to see if other fwrite calls are OK, but it'd be good to verify
that they all follow the pattern of presetting errno to 0 and then
replacing that with ENOSPC.

regards, tom lane

#19Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#18)
6 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

On 25 Jun 2025, at 17:58, Tom Lane <tgl@sss.pgh.pa.us> wrote:

I looked over this patchset briefly, and found a couple of nits:

Thanks for looking!

v5-0002, in compress_io.h:

+ * Returns true on success and throws error for all error conditions.

It doesn't return true anymore. Should be more like

+ * Returns nothing. Exits via pg_fatal for all error conditions.

Instead of this I changed the write_func signature to return the number of
bytes written as size_t. The API is loosely modelled around the libc stream API so
going to void seemed less justifiable than size_t, even if the actual
returnvalue is fairly useless due to erroring out via pg_fatal.

In LZ4Stream_write: you dropped the bit about

- errno = (errno) ? errno : ENOSPC;

but I think that's still necessary: we must assume ENOSPC if fwrite
doesn't set errno.

Doh, of course, fixed in the attached.

Other fwrite callers (write_none, Zstd_write) need
this too. v5-0004 has an instance too, in Zstd_close. I did not check
to see if other fwrite calls are OK, but it'd be good to verify
that they all follow the pattern of presetting errno to 0 and then
replacing that with ENOSPC.

I went over all the fwrite callsites *in this patchset* and made sure they
follow the pattern. There are a number of more fwrite calls in pg_dump (and
elsewhere) which might need the same treatment but I left those for a separate
patch to keep this focused on the compression API.

--
Daniel Gustafsson

Attachments:

v6-0001-Initial-patch-by-Tom-Lane.patchapplication/octet-stream; name=v6-0001-Initial-patch-by-Tom-Lane.patch; x-unix-mode=0644Download
From c4459bcbd657abad54c5dc04d6c863eca492f5ee Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 11:46:54 +0200
Subject: [PATCH v6 1/6] Initial patch by Tom Lane

The patch posted by Tom in 644360.1750096605@sss.pgh.pa.us
verbatim without any changes.
---
 src/bin/pg_dump/compress_gzip.c       | 13 +++++------
 src/bin/pg_dump/compress_io.h         | 14 +++++++-----
 src/bin/pg_dump/compress_lz4.c        | 10 ++++-----
 src/bin/pg_dump/compress_none.c       | 14 ++++--------
 src/bin/pg_dump/compress_zstd.c       | 32 +++++++++------------------
 src/bin/pg_dump/pg_backup_directory.c | 10 ++++-----
 6 files changed, 37 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 5a30ebf9bf5..8db121b94a3 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -251,14 +251,14 @@ InitCompressorGzip(CompressorState *cs,
  *----------------------
  */
 
-static bool
-Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret <= 0 && !gzeof(gzfp))
+	if (gzret < 0)
 	{
 		int			errnum;
 		const char *errmsg = gzerror(gzfp, &errnum);
@@ -267,10 +267,7 @@ Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
 	}
 
-	if (rsize)
-		*rsize = (size_t) gzret;
-
-	return true;
+	return (size_t) gzret;
 }
 
 static bool
@@ -278,7 +275,7 @@ Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 
-	return gzwrite(gzfp, ptr, size) > 0;
+	return gzwrite(gzfp, ptr, size) == size;
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index db9b38744c8..dad0c91fec5 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -123,21 +123,23 @@ struct CompressFileHandle
 									CompressFileHandle *CFH);
 
 	/*
-	 * Read 'size' bytes of data from the file and store them into 'ptr'.
-	 * Optionally it will store the number of bytes read in 'rsize'.
+	 * Read up to 'size' bytes of data from the file and store them into
+	 * 'ptr'.
 	 *
-	 * Returns true on success and throws an internal error otherwise.
+	 * Returns number of bytes read (this might be less than 'size' if EOF was
+	 * reached).  Throws error for error conditions other than EOF.
 	 */
-	bool		(*read_func) (void *ptr, size_t size, size_t *rsize,
+	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
 
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error.
+	 * Returns true on success and false on error (it is caller's
+	 * responsibility to deal with error conditions).
 	 */
 	bool		(*write_func) (const void *ptr, size_t size,
-							   struct CompressFileHandle *CFH);
+							   CompressFileHandle *CFH);
 
 	/*
 	 * Read at most size - 1 characters from the compress file handle into
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index e99f0cad71f..c94f9dcd4cf 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -583,6 +583,7 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 			return false;
 		}
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -598,8 +599,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 /*
  * fread() equivalent implementation for LZ4 compressed files.
  */
-static bool
-LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+LZ4Stream_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	int			ret;
@@ -607,10 +608,7 @@ LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 	if ((ret = LZ4Stream_read_internal(state, ptr, size, false)) < 0)
 		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	if (rsize)
-		*rsize = (size_t) ret;
-
-	return true;
+	return (size_t) ret;
 }
 
 /*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 3fc89c99854..4924935a5bd 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -83,23 +83,17 @@ InitCompressorNone(CompressorState *cs,
  * Private routines
  */
 
-static bool
-read_none(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	FILE	   *fp = (FILE *) CFH->private_data;
 	size_t		ret;
 
-	if (size == 0)
-		return true;
-
 	ret = fread(ptr, 1, size, fp);
-	if (ret != size && !feof(fp))
+	if (ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
-	if (rsize)
-		*rsize = ret;
-
-	return true;
+	return ret;
 }
 
 static bool
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..8f718212d9f 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -258,8 +258,8 @@ InitCompressorZstd(CompressorState *cs,
  * Compressed stream API
  */
 
-static bool
-Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -292,6 +292,9 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 		if (input->pos == input->size)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
+			if (ferror(zstdcs->fp))
+				pg_fatal("could not read from input file: %m");
+
 			input->size = cnt;
 
 			Assert(cnt <= input_allocated_size);
@@ -320,10 +323,7 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			break;				/* We read all the data that fits */
 	}
 
-	if (rdsize != NULL)
-		*rdsize = output->pos;
-
-	return true;
+	return output->pos;
 }
 
 static bool
@@ -358,22 +358,16 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		}
 	}
 
-	return size;
+	return true;
 }
 
 static int
 Zstd_getc(CompressFileHandle *CFH)
 {
-	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
-	int			ret;
+	unsigned char ret;
 
-	if (CFH->read_func(&ret, 1, NULL, CFH) != 1)
-	{
-		if (feof(zstdcs->fp))
-			pg_fatal("could not read from input file: end of file");
-		else
-			pg_fatal("could not read from input file: %m");
-	}
+	if (CFH->read_func(&ret, 1, CFH) != 1)
+		pg_fatal("could not read from input file: end of file");
 	return ret;
 }
 
@@ -390,11 +384,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		size_t		readsz;
-
-		if (!CFH->read_func(&buf[i], 1, &readsz, CFH))
-			break;
-		if (readsz != 1)
+		if (CFH->read_func(&buf[i], 1, CFH) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index bc2a2fb4797..eb2ce8d5a6c 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -351,7 +351,7 @@ _EndData(ArchiveHandle *AH, TocEntry *te)
 static void
 _PrintFileData(ArchiveHandle *AH, char *filename)
 {
-	size_t		cnt = 0;
+	size_t		cnt;
 	char	   *buf;
 	size_t		buflen;
 	CompressFileHandle *CFH;
@@ -366,7 +366,7 @@ _PrintFileData(ArchiveHandle *AH, char *filename)
 	buflen = DEFAULT_IO_BUFFER_SIZE;
 	buf = pg_malloc(buflen);
 
-	while (CFH->read_func(buf, buflen, &cnt, CFH) && cnt > 0)
+	while ((cnt = CFH->read_func(buf, buflen, CFH)) > 0)
 	{
 		ahwrite(buf, 1, cnt, AH);
 	}
@@ -531,10 +531,10 @@ _ReadBuf(ArchiveHandle *AH, void *buf, size_t len)
 	CompressFileHandle *CFH = ctx->dataFH;
 
 	/*
-	 * If there was an I/O error, we already exited in readF(), so here we
-	 * exit on short reads.
+	 * We do not expect a short read, so fail if we get one.  The read_func
+	 * already dealt with any outright I/O error.
 	 */
-	if (!CFH->read_func(buf, len, NULL, CFH))
+	if (CFH->read_func(buf, len, CFH) != len)
 		pg_fatal("could not read from input file: end of file");
 }
 
-- 
2.39.3 (Apple Git-146)

v6-0002-pg_dump-compression-API-write_func.patchapplication/octet-stream; name=v6-0002-pg_dump-compression-API-write_func.patch; x-unix-mode=0644Download
From eb9fb1f5ca9fcf8eab476908c34f05d4955ea27d Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:24:20 +0200
Subject: [PATCH v6 2/6] pg_dump compression API: write_func

Make write_func throw an error on all error conditions.  All callers of
write_func were already checking for success and calling pg_fatal on all
errors, so we might as well make the API support that case directly with
simpler errorhandling as a result.  Returns the number of bytes written
in case of success, which will match the size of the buffer passed in.
---
 src/bin/pg_dump/compress_gzip.c       | 13 +++++++--
 src/bin/pg_dump/compress_io.h         |  6 ++--
 src/bin/pg_dump/compress_lz4.c        | 14 ++++-----
 src/bin/pg_dump/compress_none.c       | 10 +++++--
 src/bin/pg_dump/compress_zstd.c       | 14 ++++-----
 src/bin/pg_dump/pg_backup_archiver.c  |  4 +--
 src/bin/pg_dump/pg_backup_directory.c | 42 ++++-----------------------
 7 files changed, 41 insertions(+), 62 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 8db121b94a3..b5fac207866 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -270,12 +270,21 @@ Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	return (size_t) gzret;
 }
 
-static bool
+static size_t
 Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
+	int			errnum;
+	const char *errmsg;
+
+	if (gzwrite(gzfp, ptr, size) != size)
+	{
+		errmsg = gzerror(gzfp, &errnum);
+		pg_fatal("could not write to file: %s",
+				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
+	}
 
-	return gzwrite(gzfp, ptr, size) == size;
+	return size;
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index dad0c91fec5..9c40cf0e7bd 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -135,10 +135,10 @@ struct CompressFileHandle
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error (it is caller's
-	 * responsibility to deal with error conditions).
+	 * Returns number of bytes written on success and exits via pg_fatal for
+	 * all error conditions.
 	 */
-	bool		(*write_func) (const void *ptr, size_t size,
+	size_t		(*write_func) (const void *ptr, size_t size,
 							   CompressFileHandle *CFH);
 
 	/*
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index c94f9dcd4cf..1840d572216 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -558,7 +558,7 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 /*
  * Compress size bytes from ptr and write them to the stream.
  */
-static bool
+static size_t
 LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
@@ -567,7 +567,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, true))
-		return false;
+		pg_fatal("unable to initialize LZ4 library: %s",
+				 LZ4F_getErrorName(state->errcode));
 
 	while (remaining > 0)
 	{
@@ -578,22 +579,19 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		status = LZ4F_compressUpdate(state->ctx, state->buffer, state->buflen,
 									 ptr, chunk, NULL);
 		if (LZ4F_isError(status))
-		{
-			state->errcode = status;
-			return false;
-		}
+			pg_fatal("error during writing: %s", LZ4F_getErrorName(status));
 
 		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
-			return false;
+			pg_fatal("error during writing: %m");
 		}
 
 		ptr = ((const char *) ptr) + chunk;
 	}
 
-	return true;
+	return size;
 }
 
 /*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 4924935a5bd..791e10c8683 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -96,16 +96,20 @@ read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 	return ret;
 }
 
-static bool
+static size_t
 write_none(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	size_t		ret;
 
+	errno = 0;
 	ret = fwrite(ptr, 1, size, (FILE *) CFH->private_data);
 	if (ret != size)
-		return false;
+	{
+		errno = (errno) ? errno : ENOSPC;
+		pg_fatal("coud not write to file: %m");
+	}
 
-	return true;
+	return ret;
 }
 
 static const char *
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 8f718212d9f..7ae93d94f66 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -326,7 +326,7 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	return output->pos;
 }
 
-static bool
+static size_t
 Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
@@ -345,20 +345,18 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		output->pos = 0;
 		res = ZSTD_compressStream2(zstdcs->cstream, output, input, ZSTD_e_continue);
 		if (ZSTD_isError(res))
-		{
-			zstdcs->zstderror = ZSTD_getErrorName(res);
-			return false;
-		}
+			pg_fatal("could not write to file: %s", ZSTD_getErrorName(res));
 
+		errno = 0;
 		cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 		if (cnt != output->pos)
 		{
-			zstdcs->zstderror = strerror(errno);
-			return false;
+			errno = (errno) ? errno : ENOSPC;
+			pg_fatal("could not write to file: %m");
 		}
 	}
 
-	return true;
+	return size;
 }
 
 static int
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93..9145598ff33 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1868,8 +1868,8 @@ ahwrite(const void *ptr, size_t size, size_t nmemb, ArchiveHandle *AH)
 	{
 		CompressFileHandle *CFH = (CompressFileHandle *) AH->OF;
 
-		if (CFH->write_func(ptr, size * nmemb, CFH))
-			bytes_written = size * nmemb;
+		CFH->write_func(ptr, size * nmemb, CFH);
+		bytes_written = size * nmemb;
 	}
 
 	if (bytes_written != size * nmemb)
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index eb2ce8d5a6c..94d401d8a4e 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -316,15 +316,9 @@ _WriteData(ArchiveHandle *AH, const void *data, size_t dLen)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (dLen > 0 && !CFH->write_func(data, dLen, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	if (dLen <= 0)
+		return;
+	CFH->write_func(data, dLen, CFH);
 }
 
 /*
@@ -470,16 +464,7 @@ _WriteByte(ArchiveHandle *AH, const int i)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(&c, 1, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
-
+	CFH->write_func(&c, 1, CFH);
 	return 1;
 }
 
@@ -508,15 +493,7 @@ _WriteBuf(ArchiveHandle *AH, const void *buf, size_t len)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
@@ -677,14 +654,7 @@ _EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid)
 
 	/* register the LO in blobs_NNN.toc */
 	len = snprintf(buf, sizeof(buf), "%u blob_%u.dat\n", oid, oid);
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to LOs TOC file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v6-0003-pg_dump-compression-API-open_func.patchapplication/octet-stream; name=v6-0003-pg_dump-compression-API-open_func.patch; x-unix-mode=0644Download
From 554b93d117f135ad4ba702d524f1415a385ef8f5 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:38:23 +0200
Subject: [PATCH v6 3/6] pg_dump compression API: open_func

open_func is defined to return true on success and false on error,
with errorhandling left to the caller.

 * zstd: move stream initialization from the open function to read
   and write functions as they can have fatal errors.  Also ensure
   to dup() the file descriptor like none and gzip already do.
 * lz4: Ensure to dup() the file descriptor like none and gzip
   already do.

A TODO to future self is to move all initialization into the init
function, but that requires passing the mode in order to do it in
a sane manner.  This is left as an excercise for v19.
---
 src/bin/pg_dump/compress_io.c   |  1 +
 src/bin/pg_dump/compress_lz4.c  | 10 +++---
 src/bin/pg_dump/compress_zstd.c | 62 ++++++++++++++++++++++-----------
 3 files changed, 46 insertions(+), 27 deletions(-)

diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index 8c3d9c911c4..cf5358de741 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -269,6 +269,7 @@ InitDiscoverCompressFileHandle(const char *path, const char *mode)
 	}
 
 	CFH = InitCompressFileHandle(compression_spec);
+	errno = 0;
 	if (!CFH->open_func(fname, -1, mode, CFH))
 	{
 		free_keep_errno(CFH);
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 1840d572216..881700f83bb 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -12,6 +12,7 @@
  *-------------------------------------------------------------------------
  */
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_lz4.h"
 #include "pg_backup_utils.h"
@@ -707,21 +708,18 @@ static bool
 LZ4Stream_open(const char *path, int fd, const char *mode,
 			   CompressFileHandle *CFH)
 {
-	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		state->fp = fdopen(dup(fd), mode);
 	else
-		fp = fopen(path, mode);
-	if (fp == NULL)
+		state->fp = fopen(path, mode);
+	if (state->fp == NULL)
 	{
 		state->errcode = errno;
 		return false;
 	}
 
-	state->fp = fp;
-
 	return true;
 }
 
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 7ae93d94f66..d58cc1d5c88 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -13,6 +13,7 @@
  */
 
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_zstd.h"
 #include "pg_backup_utils.h"
@@ -268,6 +269,18 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	size_t		res,
 				cnt;
 
+	/*
+	 * If this is the first call to the reading function, initialize the
+	 * required datastructures.
+	 */
+	if (zstdcs->dstream == NULL)
+	{
+		zstdcs->input.src = pg_malloc0(input_allocated_size);
+		zstdcs->dstream = ZSTD_createDStream();
+		if (zstdcs->dstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	output->size = size;
 	output->dst = ptr;
 	output->pos = 0;
@@ -339,6 +352,15 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 	input->size = size;
 	input->pos = 0;
 
+	if (zstdcs->cstream == NULL)
+	{
+		zstdcs->output.size = ZSTD_CStreamOutSize();
+		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
+		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
+		if (zstdcs->cstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	/* Consume all input, to be flushed later */
 	while (input->pos != input->size)
 	{
@@ -460,35 +482,33 @@ Zstd_open(const char *path, int fd, const char *mode,
 	FILE	   *fp;
 	ZstdCompressorState *zstdcs;
 
+	/*
+	 * Clear state storage to avoid having the fd point to non-NULL memory on
+	 * error return.
+	 */
+	CFH->private_data = NULL;
+
+	zstdcs = (ZstdCompressorState *) pg_malloc_extended(sizeof(*zstdcs),
+														MCXT_ALLOC_NO_OOM | MCXT_ALLOC_ZERO);
+	if (!zstdcs)
+	{
+		errno = ENOMEM;
+		return false;
+	}
+
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		fp = fdopen(dup(fd), mode);
 	else
 		fp = fopen(path, mode);
 
 	if (fp == NULL)
+	{
+		pg_free(zstdcs);
 		return false;
+	}
 
-	zstdcs = (ZstdCompressorState *) pg_malloc0(sizeof(*zstdcs));
-	CFH->private_data = zstdcs;
 	zstdcs->fp = fp;
-
-	if (mode[0] == 'r')
-	{
-		zstdcs->input.src = pg_malloc0(ZSTD_DStreamInSize());
-		zstdcs->dstream = ZSTD_createDStream();
-		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else if (mode[0] == 'w' || mode[0] == 'a')
-	{
-		zstdcs->output.size = ZSTD_CStreamOutSize();
-		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
-		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
-		if (zstdcs->cstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else
-		pg_fatal("unhandled mode \"%s\"", mode);
+	CFH->private_data = zstdcs;
 
 	return true;
 }
-- 
2.39.3 (Apple Git-146)

v6-0004-pg_dump-compression-API-close_func.patchapplication/octet-stream; name=v6-0004-pg_dump-compression-API-close_func.patch; x-unix-mode=0644Download
From 0298a794917bde01f78858c38c274c0ee9888c99 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:48:15 +0200
Subject: [PATCH v6 4/6] pg_dump compression API: close_func

close_func is defined as returning true on success and false on error
leaving errorhandling to the caller.

 * zstd: Ensure to close the FD even if closing down the (de)compressor
   fails, and clean up state allocation on fclose failures.  Make sure
   to capture errors set by fclose.
 * lz4: Ensure to close the FD even if closing down the (de)compressor
   fails and don't call pg_fatal but instead log the failures using
   pg_log_error. Make sure to capture errors set by fclose.
 * none: Make sure to catch errors set by fclose.
---
 src/bin/pg_dump/compress_io.c   |  1 +
 src/bin/pg_dump/compress_lz4.c  | 36 ++++++++++++++++++++++++---------
 src/bin/pg_dump/compress_none.c |  5 +++++
 src/bin/pg_dump/compress_zstd.c | 18 +++++++++++++----
 4 files changed, 46 insertions(+), 14 deletions(-)

diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index cf5358de741..9cadc6f2a3f 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -290,6 +290,7 @@ EndCompressFileHandle(CompressFileHandle *CFH)
 {
 	bool		ret = false;
 
+	errno = 0;
 	if (CFH->private_data)
 		ret = CFH->close_func(CFH);
 
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 881700f83bb..8c3d9217c1a 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -666,6 +666,7 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	size_t		status;
+	int			ret;
 
 	fp = state->fp;
 	if (state->inited)
@@ -674,25 +675,31 @@ LZ4Stream_close(CompressFileHandle *CFH)
 		{
 			status = LZ4F_compressEnd(state->ctx, state->buffer, state->buflen, NULL);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
-			else if (fwrite(state->buffer, 1, status, state->fp) != status)
 			{
-				errno = (errno) ? errno : ENOSPC;
-				WRITE_ERROR_EXIT;
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
+			}
+			else
+			{
+				errno = 0;
+				if (fwrite(state->buffer, 1, status, state->fp) != status)
+				{
+					errno = (errno) ? errno : ENOSPC;
+					pg_log_error("could not write to output file: %m");
+				}
 			}
 
 			status = LZ4F_freeCompressionContext(state->ctx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
 		}
 		else
 		{
 			status = LZ4F_freeDecompressionContext(state->dtx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end decompression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end decompression: %s",
+							 LZ4F_getErrorName(status));
 			pg_free(state->overflowbuf);
 		}
 
@@ -700,8 +707,17 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	}
 
 	pg_free(state);
+	CFH->private_data = NULL;
+
+	errno = 0;
+	ret = fclose(fp);
+	if (ret != 0)
+	{
+		pg_log_error("could not close file: %m");
+		return false;
+	}
 
-	return fclose(fp) == 0;
+	return true;
 }
 
 static bool
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 791e10c8683..3a1a5e6f7e7 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -151,7 +151,12 @@ close_none(CompressFileHandle *CFH)
 	CFH->private_data = NULL;
 
 	if (fp)
+	{
+		errno = 0;
 		ret = fclose(fp);
+		if (ret != 0)
+			pg_log_error("could not close file: %m");
+	}
 
 	return ret == 0;
 }
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index d58cc1d5c88..29f88f17a78 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -420,6 +420,7 @@ static bool
 Zstd_close(CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
+	bool		success = true;
 
 	if (zstdcs->cstream)
 	{
@@ -436,14 +437,18 @@ Zstd_close(CompressFileHandle *CFH)
 			if (ZSTD_isError(res))
 			{
 				zstdcs->zstderror = ZSTD_getErrorName(res);
-				return false;
+				success = false;
+				break;
 			}
 
+			errno = 0;
 			cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 			if (cnt != output->pos)
 			{
+				errno = (errno) ? errno : ENOSPC;
 				zstdcs->zstderror = strerror(errno);
-				return false;
+				success = false;
+				break;
 			}
 
 			if (res == 0)
@@ -460,11 +465,16 @@ Zstd_close(CompressFileHandle *CFH)
 		pg_free(unconstify(void *, zstdcs->input.src));
 	}
 
+	errno = 0;
 	if (fclose(zstdcs->fp) != 0)
-		return false;
+	{
+		zstdcs->zstderror = strerror(errno);
+		success = false;
+	}
 
 	pg_free(zstdcs);
-	return true;
+	CFH->private_data = NULL;
+	return success;
 }
 
 static bool
-- 
2.39.3 (Apple Git-146)

v6-0005-pg_dump-compression-API-LZ4Stream_init.patchapplication/octet-stream; name=v6-0005-pg_dump-compression-API-LZ4Stream_init.patch; x-unix-mode=0644Download
From a28a719ded17647e7fb5930975e3925889503450 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 15:08:12 +0200
Subject: [PATCH v6 5/6] pg_dump compression API: LZ4Stream_init

Make sure to not switch to state inited until we know that initialization
succeeded and reset errno just in case.
---
 src/bin/pg_dump/compress_lz4.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 8c3d9217c1a..32ced77d562 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -359,7 +359,6 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		return true;
 
 	state->compressing = compressing;
-	state->inited = true;
 
 	/* When compressing, write LZ4 header to the output stream. */
 	if (state->compressing)
@@ -368,6 +367,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		if (!LZ4State_compression_init(state))
 			return false;
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, state->compressedlen, state->fp) != state->compressedlen)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -391,6 +391,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		state->overflowlen = 0;
 	}
 
+	state->inited = true;
 	return true;
 }
 
-- 
2.39.3 (Apple Git-146)

v6-0006-pg_dump-compression-API-read_func-gets_func.patchapplication/octet-stream; name=v6-0006-pg_dump-compression-API-read_func-gets_func.patch; x-unix-mode=0644Download
From bc4152b8da68fe719ab04ffbe98c36282e9feb83 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 15:53:00 +0200
Subject: [PATCH v6 6/6] pg_dump compression API: read_func / gets_func

read_func is defined to return the number of bytes read, zero for EOF
and throw an error on all error conditions

 * lz4: Make sure to call throw an error and not return -1
 * gzip: gzread returning zero cannot be assumed to indicate EOF as
   it is documented to return zero for some types of errors.
 * none: Don't call ferror on every fread unless it returne zero and
   we can expect there to be an error or EOF.

gets_func is defined go return *s on success and NULL on error or
if EOF is found before any char has been read.

 * lz4, zstd: Convert _read_internal functions to not automatically
   call pg_fatal on errors to be able to handle gets returning NULL
   on error.
---
 src/bin/pg_dump/compress_gzip.c | 15 +++++++++++++--
 src/bin/pg_dump/compress_io.h   |  2 +-
 src/bin/pg_dump/compress_lz4.c  | 19 +++++++++++++++----
 src/bin/pg_dump/compress_none.c |  2 +-
 src/bin/pg_dump/compress_zstd.c | 28 +++++++++++++++++++++++-----
 5 files changed, 53 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index b5fac207866..9772c936e38 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -258,10 +258,21 @@ Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret < 0)
+
+	/*
+	 * gzread returns zero on EOF as well as some error conditions, and less
+	 * than zero on other error conditions, so we need to inspect for EOF on
+	 * zero.
+	 */
+	if (gzret <= 0)
 	{
 		int			errnum;
-		const char *errmsg = gzerror(gzfp, &errnum);
+		const char *errmsg;
+
+		if (gzret == 0 && gzeof(gzfp))
+			return 0;
+
+		errmsg = gzerror(gzfp, &errnum);
 
 		pg_fatal("could not read from input file: %s",
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index 9c40cf0e7bd..99d21065488 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -127,7 +127,7 @@ struct CompressFileHandle
 	 * 'ptr'.
 	 *
 	 * Returns number of bytes read (this might be less than 'size' if EOF was
-	 * reached).  Throws error for error conditions other than EOF.
+	 * reached).  Exits via pg_fatal all for error conditions.
 	 */
 	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 32ced77d562..8660977e41c 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -459,7 +459,11 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, false /* decompressing */ ))
+	{
+		pg_log_error("unable to initialize LZ4 library: %s",
+					 LZ4F_getErrorName(state->errcode));
 		return -1;
+	}
 
 	/* No work needs to be done for a zero-sized output buffer */
 	if (size <= 0)
@@ -486,7 +490,10 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 		rsize = fread(readbuf, 1, size, state->fp);
 		if (rsize < size && !feof(state->fp))
+		{
+			pg_log_error("could not read from input file: %m");
 			return -1;
+		}
 
 		rp = (char *) readbuf;
 		rend = (char *) readbuf + rsize;
@@ -503,6 +510,8 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 			if (LZ4F_isError(status))
 			{
 				state->errcode = status;
+				pg_log_error("could not read from input file: %s",
+							 LZ4F_getErrorName(state->errcode));
 				return -1;
 			}
 
@@ -641,11 +650,13 @@ LZ4Stream_gets(char *ptr, int size, CompressFileHandle *CFH)
 	int			ret;
 
 	ret = LZ4Stream_read_internal(state, ptr, size - 1, true);
-	if (ret < 0 || (ret == 0 && !LZ4Stream_eof(CFH)))
-		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	/* Done reading */
-	if (ret == 0)
+	/*
+	 * LZ4Stream_read_internal returning 0 or -1 means that it was either an
+	 * EOF or an error, but gets_func is defined to return NULL in either case
+	 * so we can treat both the same here.
+	 */
+	if (ret <= 0)
 		return NULL;
 
 	/*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 3a1a5e6f7e7..27ecd36cb61 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -90,7 +90,7 @@ read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 	size_t		ret;
 
 	ret = fread(ptr, 1, size, fp);
-	if (ferror(fp))
+	if (ret == 0 && ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
 	return ret;
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 29f88f17a78..ba09c38748d 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -260,7 +260,7 @@ InitCompressorZstd(CompressorState *cs,
  */
 
 static size_t
-Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+Zstd_read_internal(void *ptr, size_t size, CompressFileHandle *CFH, bool exit_on_error)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -278,7 +278,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 		zstdcs->input.src = pg_malloc0(input_allocated_size);
 		zstdcs->dstream = ZSTD_createDStream();
 		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
+		{
+			if (exit_on_error)
+				pg_fatal("could not initialize compression library");
+			return -1;
+		}
 	}
 
 	output->size = size;
@@ -306,7 +310,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
 			if (ferror(zstdcs->fp))
-				pg_fatal("could not read from input file: %m");
+			{
+				if (exit_on_error)
+					pg_fatal("could not read from input file: %m");
+				return -1;
+			}
 
 			input->size = cnt;
 
@@ -323,7 +331,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 			res = ZSTD_decompressStream(zstdcs->dstream, output, input);
 
 			if (ZSTD_isError(res))
-				pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+			{
+				if (exit_on_error)
+					pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+				return -1;
+			}
 
 			if (output->pos == output->size)
 				break;			/* No more room for output */
@@ -404,7 +416,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		if (CFH->read_func(&buf[i], 1, CFH) != 1)
+		if (Zstd_read_internal(&buf[i], 1, CFH, false) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
@@ -416,6 +428,12 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	return i > 0 ? buf : NULL;
 }
 
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+{
+	return Zstd_read_internal(ptr, size, CFH, true);
+}
+
 static bool
 Zstd_close(CompressFileHandle *CFH)
 {
-- 
2.39.3 (Apple Git-146)

#20Tom Lane
tgl@sss.pgh.pa.us
In reply to: Daniel Gustafsson (#19)
Re: No error checking when reading from file using zstd in pg_dump

Daniel Gustafsson <daniel@yesql.se> writes:

On 25 Jun 2025, at 17:58, Tom Lane <tgl@sss.pgh.pa.us> wrote:

It doesn't return true anymore. Should be more like
+ * Returns nothing. Exits via pg_fatal for all error conditions.

Instead of this I changed the write_func signature to return the number of
bytes written as size_t. The API is loosely modelled around the libc stream API so
going to void seemed less justifiable than size_t, even if the actual
returnvalue is fairly useless due to erroring out via pg_fatal.

Hm. My one concern about that is that using "void" ensures that the
compiler will catch any write_funcs or callsites that we missed
updating, whereas replacing bool with size_t probably prevents that
detection.

Of course this is now moot for the in-core code since we presumably
caught everything already. But I wonder about patches (say to
support an additional compression method) that might be in the
pipeline, or in use in some local fork somewhere. There's no
certainty that they'd get the word, especially since any tests
that didn't exercise failure conditions would still work.

So on the whole I prefer the "void" approach. I'm not dead
set on that though, it's just a niggling worry.

regards, tom lane

#21Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#20)
6 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

On 26 Jun 2025, at 15:33, Tom Lane <tgl@sss.pgh.pa.us> wrote:

So on the whole I prefer the "void" approach. I'm not dead
set on that though, it's just a niggling worry.

I think the likelyhood of it being a problem in practice is pretty slim, but
it's still a stronger argument than my "match an API we're still not aligned
with". The attached v7 reverts back to void return.

--
Daniel Gustafsson

Attachments:

v7-0001-Initial-patch-by-Tom-Lane.patchapplication/octet-stream; name=v7-0001-Initial-patch-by-Tom-Lane.patch; x-unix-mode=0644Download
From 26d0962146b6762f10a73ecfa9c887c1ce823ff4 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 11:46:54 +0200
Subject: [PATCH v7 1/6] Initial patch by Tom Lane

The patch posted by Tom in 644360.1750096605@sss.pgh.pa.us
verbatim without any changes.
---
 src/bin/pg_dump/compress_gzip.c       | 13 +++++------
 src/bin/pg_dump/compress_io.h         | 14 +++++++-----
 src/bin/pg_dump/compress_lz4.c        | 10 ++++-----
 src/bin/pg_dump/compress_none.c       | 14 ++++--------
 src/bin/pg_dump/compress_zstd.c       | 32 +++++++++------------------
 src/bin/pg_dump/pg_backup_directory.c | 10 ++++-----
 6 files changed, 37 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 5a30ebf9bf5..8db121b94a3 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -251,14 +251,14 @@ InitCompressorGzip(CompressorState *cs,
  *----------------------
  */
 
-static bool
-Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret <= 0 && !gzeof(gzfp))
+	if (gzret < 0)
 	{
 		int			errnum;
 		const char *errmsg = gzerror(gzfp, &errnum);
@@ -267,10 +267,7 @@ Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
 	}
 
-	if (rsize)
-		*rsize = (size_t) gzret;
-
-	return true;
+	return (size_t) gzret;
 }
 
 static bool
@@ -278,7 +275,7 @@ Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 
-	return gzwrite(gzfp, ptr, size) > 0;
+	return gzwrite(gzfp, ptr, size) == size;
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index db9b38744c8..dad0c91fec5 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -123,21 +123,23 @@ struct CompressFileHandle
 									CompressFileHandle *CFH);
 
 	/*
-	 * Read 'size' bytes of data from the file and store them into 'ptr'.
-	 * Optionally it will store the number of bytes read in 'rsize'.
+	 * Read up to 'size' bytes of data from the file and store them into
+	 * 'ptr'.
 	 *
-	 * Returns true on success and throws an internal error otherwise.
+	 * Returns number of bytes read (this might be less than 'size' if EOF was
+	 * reached).  Throws error for error conditions other than EOF.
 	 */
-	bool		(*read_func) (void *ptr, size_t size, size_t *rsize,
+	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
 
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error.
+	 * Returns true on success and false on error (it is caller's
+	 * responsibility to deal with error conditions).
 	 */
 	bool		(*write_func) (const void *ptr, size_t size,
-							   struct CompressFileHandle *CFH);
+							   CompressFileHandle *CFH);
 
 	/*
 	 * Read at most size - 1 characters from the compress file handle into
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index e99f0cad71f..c94f9dcd4cf 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -583,6 +583,7 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 			return false;
 		}
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -598,8 +599,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 /*
  * fread() equivalent implementation for LZ4 compressed files.
  */
-static bool
-LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+LZ4Stream_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	int			ret;
@@ -607,10 +608,7 @@ LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 	if ((ret = LZ4Stream_read_internal(state, ptr, size, false)) < 0)
 		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	if (rsize)
-		*rsize = (size_t) ret;
-
-	return true;
+	return (size_t) ret;
 }
 
 /*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 3fc89c99854..4924935a5bd 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -83,23 +83,17 @@ InitCompressorNone(CompressorState *cs,
  * Private routines
  */
 
-static bool
-read_none(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	FILE	   *fp = (FILE *) CFH->private_data;
 	size_t		ret;
 
-	if (size == 0)
-		return true;
-
 	ret = fread(ptr, 1, size, fp);
-	if (ret != size && !feof(fp))
+	if (ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
-	if (rsize)
-		*rsize = ret;
-
-	return true;
+	return ret;
 }
 
 static bool
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..8f718212d9f 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -258,8 +258,8 @@ InitCompressorZstd(CompressorState *cs,
  * Compressed stream API
  */
 
-static bool
-Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -292,6 +292,9 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 		if (input->pos == input->size)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
+			if (ferror(zstdcs->fp))
+				pg_fatal("could not read from input file: %m");
+
 			input->size = cnt;
 
 			Assert(cnt <= input_allocated_size);
@@ -320,10 +323,7 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			break;				/* We read all the data that fits */
 	}
 
-	if (rdsize != NULL)
-		*rdsize = output->pos;
-
-	return true;
+	return output->pos;
 }
 
 static bool
@@ -358,22 +358,16 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		}
 	}
 
-	return size;
+	return true;
 }
 
 static int
 Zstd_getc(CompressFileHandle *CFH)
 {
-	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
-	int			ret;
+	unsigned char ret;
 
-	if (CFH->read_func(&ret, 1, NULL, CFH) != 1)
-	{
-		if (feof(zstdcs->fp))
-			pg_fatal("could not read from input file: end of file");
-		else
-			pg_fatal("could not read from input file: %m");
-	}
+	if (CFH->read_func(&ret, 1, CFH) != 1)
+		pg_fatal("could not read from input file: end of file");
 	return ret;
 }
 
@@ -390,11 +384,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		size_t		readsz;
-
-		if (!CFH->read_func(&buf[i], 1, &readsz, CFH))
-			break;
-		if (readsz != 1)
+		if (CFH->read_func(&buf[i], 1, CFH) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index bc2a2fb4797..eb2ce8d5a6c 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -351,7 +351,7 @@ _EndData(ArchiveHandle *AH, TocEntry *te)
 static void
 _PrintFileData(ArchiveHandle *AH, char *filename)
 {
-	size_t		cnt = 0;
+	size_t		cnt;
 	char	   *buf;
 	size_t		buflen;
 	CompressFileHandle *CFH;
@@ -366,7 +366,7 @@ _PrintFileData(ArchiveHandle *AH, char *filename)
 	buflen = DEFAULT_IO_BUFFER_SIZE;
 	buf = pg_malloc(buflen);
 
-	while (CFH->read_func(buf, buflen, &cnt, CFH) && cnt > 0)
+	while ((cnt = CFH->read_func(buf, buflen, CFH)) > 0)
 	{
 		ahwrite(buf, 1, cnt, AH);
 	}
@@ -531,10 +531,10 @@ _ReadBuf(ArchiveHandle *AH, void *buf, size_t len)
 	CompressFileHandle *CFH = ctx->dataFH;
 
 	/*
-	 * If there was an I/O error, we already exited in readF(), so here we
-	 * exit on short reads.
+	 * We do not expect a short read, so fail if we get one.  The read_func
+	 * already dealt with any outright I/O error.
 	 */
-	if (!CFH->read_func(buf, len, NULL, CFH))
+	if (CFH->read_func(buf, len, CFH) != len)
 		pg_fatal("could not read from input file: end of file");
 }
 
-- 
2.39.3 (Apple Git-146)

v7-0002-pg_dump-compression-API-write_func.patchapplication/octet-stream; name=v7-0002-pg_dump-compression-API-write_func.patch; x-unix-mode=0644Download
From 96f77d9430383de96e3d81427d0f4a8811e1b7c8 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:24:20 +0200
Subject: [PATCH v7 2/6] pg_dump compression API: write_func

Make write_func throw an error on all error conditions.  All callers of
write_func were already checking for success and calling pg_fatal on all
errors, so we might as well make the API support that case directly with
simpler errorhandling as a result.  Returns the number of bytes written
in case of success, which will match the size of the buffer passed in.
---
 src/bin/pg_dump/compress_gzip.c       | 11 +++++--
 src/bin/pg_dump/compress_io.h         |  5 ++--
 src/bin/pg_dump/compress_lz4.c        | 14 ++++-----
 src/bin/pg_dump/compress_none.c       | 10 ++++---
 src/bin/pg_dump/compress_zstd.c       | 14 ++++-----
 src/bin/pg_dump/pg_backup_archiver.c  |  4 +--
 src/bin/pg_dump/pg_backup_directory.c | 42 ++++-----------------------
 7 files changed, 35 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 8db121b94a3..2edcbffdd8d 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -270,12 +270,19 @@ Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	return (size_t) gzret;
 }
 
-static bool
+static void
 Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
+	int			errnum;
+	const char *errmsg;
 
-	return gzwrite(gzfp, ptr, size) == size;
+	if (gzwrite(gzfp, ptr, size) != size)
+	{
+		errmsg = gzerror(gzfp, &errnum);
+		pg_fatal("could not write to file: %s",
+				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
+	}
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index dad0c91fec5..e4d98ca08d7 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -135,10 +135,9 @@ struct CompressFileHandle
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error (it is caller's
-	 * responsibility to deal with error conditions).
+	 * Returns nothing, exits via pg_fatal for all error conditions.
 	 */
-	bool		(*write_func) (const void *ptr, size_t size,
+	void		(*write_func) (const void *ptr, size_t size,
 							   CompressFileHandle *CFH);
 
 	/*
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index c94f9dcd4cf..3242c062acf 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -558,7 +558,7 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 /*
  * Compress size bytes from ptr and write them to the stream.
  */
-static bool
+static void
 LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
@@ -567,7 +567,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, true))
-		return false;
+		pg_fatal("unable to initialize LZ4 library: %s",
+				 LZ4F_getErrorName(state->errcode));
 
 	while (remaining > 0)
 	{
@@ -578,22 +579,17 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		status = LZ4F_compressUpdate(state->ctx, state->buffer, state->buflen,
 									 ptr, chunk, NULL);
 		if (LZ4F_isError(status))
-		{
-			state->errcode = status;
-			return false;
-		}
+			pg_fatal("error during writing: %s", LZ4F_getErrorName(status));
 
 		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
-			return false;
+			pg_fatal("error during writing: %m");
 		}
 
 		ptr = ((const char *) ptr) + chunk;
 	}
-
-	return true;
 }
 
 /*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 4924935a5bd..a5179c1050a 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -96,16 +96,18 @@ read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 	return ret;
 }
 
-static bool
+static void
 write_none(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	size_t		ret;
 
+	errno = 0;
 	ret = fwrite(ptr, 1, size, (FILE *) CFH->private_data);
 	if (ret != size)
-		return false;
-
-	return true;
+	{
+		errno = (errno) ? errno : ENOSPC;
+		pg_fatal("coud not write to file: %m");
+	}
 }
 
 static const char *
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 8f718212d9f..9ca88386ddb 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -326,7 +326,7 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	return output->pos;
 }
 
-static bool
+static void
 Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
@@ -345,20 +345,16 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		output->pos = 0;
 		res = ZSTD_compressStream2(zstdcs->cstream, output, input, ZSTD_e_continue);
 		if (ZSTD_isError(res))
-		{
-			zstdcs->zstderror = ZSTD_getErrorName(res);
-			return false;
-		}
+			pg_fatal("could not write to file: %s", ZSTD_getErrorName(res));
 
+		errno = 0;
 		cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 		if (cnt != output->pos)
 		{
-			zstdcs->zstderror = strerror(errno);
-			return false;
+			errno = (errno) ? errno : ENOSPC;
+			pg_fatal("could not write to file: %m");
 		}
 	}
-
-	return true;
 }
 
 static int
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93..9145598ff33 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1868,8 +1868,8 @@ ahwrite(const void *ptr, size_t size, size_t nmemb, ArchiveHandle *AH)
 	{
 		CompressFileHandle *CFH = (CompressFileHandle *) AH->OF;
 
-		if (CFH->write_func(ptr, size * nmemb, CFH))
-			bytes_written = size * nmemb;
+		CFH->write_func(ptr, size * nmemb, CFH);
+		bytes_written = size * nmemb;
 	}
 
 	if (bytes_written != size * nmemb)
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index eb2ce8d5a6c..94d401d8a4e 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -316,15 +316,9 @@ _WriteData(ArchiveHandle *AH, const void *data, size_t dLen)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (dLen > 0 && !CFH->write_func(data, dLen, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	if (dLen <= 0)
+		return;
+	CFH->write_func(data, dLen, CFH);
 }
 
 /*
@@ -470,16 +464,7 @@ _WriteByte(ArchiveHandle *AH, const int i)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(&c, 1, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
-
+	CFH->write_func(&c, 1, CFH);
 	return 1;
 }
 
@@ -508,15 +493,7 @@ _WriteBuf(ArchiveHandle *AH, const void *buf, size_t len)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
@@ -677,14 +654,7 @@ _EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid)
 
 	/* register the LO in blobs_NNN.toc */
 	len = snprintf(buf, sizeof(buf), "%u blob_%u.dat\n", oid, oid);
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to LOs TOC file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v7-0003-pg_dump-compression-API-open_func.patchapplication/octet-stream; name=v7-0003-pg_dump-compression-API-open_func.patch; x-unix-mode=0644Download
From ab1795f6c52d1ee105b82ae72a1fd71a27e0bdb1 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:38:23 +0200
Subject: [PATCH v7 3/6] pg_dump compression API: open_func

open_func is defined to return true on success and false on error,
with errorhandling left to the caller.

 * zstd: move stream initialization from the open function to read
   and write functions as they can have fatal errors.  Also ensure
   to dup() the file descriptor like none and gzip already do.
 * lz4: Ensure to dup() the file descriptor like none and gzip
   already do.

A TODO to future self is to move all initialization into the init
function, but that requires passing the mode in order to do it in
a sane manner.  This is left as an excercise for v19.
---
 src/bin/pg_dump/compress_io.c   |  1 +
 src/bin/pg_dump/compress_lz4.c  | 10 +++---
 src/bin/pg_dump/compress_zstd.c | 62 ++++++++++++++++++++++-----------
 3 files changed, 46 insertions(+), 27 deletions(-)

diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index 8c3d9c911c4..cf5358de741 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -269,6 +269,7 @@ InitDiscoverCompressFileHandle(const char *path, const char *mode)
 	}
 
 	CFH = InitCompressFileHandle(compression_spec);
+	errno = 0;
 	if (!CFH->open_func(fname, -1, mode, CFH))
 	{
 		free_keep_errno(CFH);
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 3242c062acf..2af1a1ffc84 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -12,6 +12,7 @@
  *-------------------------------------------------------------------------
  */
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_lz4.h"
 #include "pg_backup_utils.h"
@@ -705,21 +706,18 @@ static bool
 LZ4Stream_open(const char *path, int fd, const char *mode,
 			   CompressFileHandle *CFH)
 {
-	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		state->fp = fdopen(dup(fd), mode);
 	else
-		fp = fopen(path, mode);
-	if (fp == NULL)
+		state->fp = fopen(path, mode);
+	if (state->fp == NULL)
 	{
 		state->errcode = errno;
 		return false;
 	}
 
-	state->fp = fp;
-
 	return true;
 }
 
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 9ca88386ddb..d133c32cabf 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -13,6 +13,7 @@
  */
 
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_zstd.h"
 #include "pg_backup_utils.h"
@@ -268,6 +269,18 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	size_t		res,
 				cnt;
 
+	/*
+	 * If this is the first call to the reading function, initialize the
+	 * required datastructures.
+	 */
+	if (zstdcs->dstream == NULL)
+	{
+		zstdcs->input.src = pg_malloc0(input_allocated_size);
+		zstdcs->dstream = ZSTD_createDStream();
+		if (zstdcs->dstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	output->size = size;
 	output->dst = ptr;
 	output->pos = 0;
@@ -339,6 +352,15 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 	input->size = size;
 	input->pos = 0;
 
+	if (zstdcs->cstream == NULL)
+	{
+		zstdcs->output.size = ZSTD_CStreamOutSize();
+		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
+		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
+		if (zstdcs->cstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	/* Consume all input, to be flushed later */
 	while (input->pos != input->size)
 	{
@@ -458,35 +480,33 @@ Zstd_open(const char *path, int fd, const char *mode,
 	FILE	   *fp;
 	ZstdCompressorState *zstdcs;
 
+	/*
+	 * Clear state storage to avoid having the fd point to non-NULL memory on
+	 * error return.
+	 */
+	CFH->private_data = NULL;
+
+	zstdcs = (ZstdCompressorState *) pg_malloc_extended(sizeof(*zstdcs),
+														MCXT_ALLOC_NO_OOM | MCXT_ALLOC_ZERO);
+	if (!zstdcs)
+	{
+		errno = ENOMEM;
+		return false;
+	}
+
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		fp = fdopen(dup(fd), mode);
 	else
 		fp = fopen(path, mode);
 
 	if (fp == NULL)
+	{
+		pg_free(zstdcs);
 		return false;
+	}
 
-	zstdcs = (ZstdCompressorState *) pg_malloc0(sizeof(*zstdcs));
-	CFH->private_data = zstdcs;
 	zstdcs->fp = fp;
-
-	if (mode[0] == 'r')
-	{
-		zstdcs->input.src = pg_malloc0(ZSTD_DStreamInSize());
-		zstdcs->dstream = ZSTD_createDStream();
-		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else if (mode[0] == 'w' || mode[0] == 'a')
-	{
-		zstdcs->output.size = ZSTD_CStreamOutSize();
-		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
-		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
-		if (zstdcs->cstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else
-		pg_fatal("unhandled mode \"%s\"", mode);
+	CFH->private_data = zstdcs;
 
 	return true;
 }
-- 
2.39.3 (Apple Git-146)

v7-0004-pg_dump-compression-API-close_func.patchapplication/octet-stream; name=v7-0004-pg_dump-compression-API-close_func.patch; x-unix-mode=0644Download
From f8cdae2da35d1fff6ffb57b9db17a0779ea2e67a Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 14:48:15 +0200
Subject: [PATCH v7 4/6] pg_dump compression API: close_func

close_func is defined as returning true on success and false on error
leaving errorhandling to the caller.

 * zstd: Ensure to close the FD even if closing down the (de)compressor
   fails, and clean up state allocation on fclose failures.  Make sure
   to capture errors set by fclose.
 * lz4: Ensure to close the FD even if closing down the (de)compressor
   fails and don't call pg_fatal but instead log the failures using
   pg_log_error. Make sure to capture errors set by fclose.
 * none: Make sure to catch errors set by fclose.
---
 src/bin/pg_dump/compress_io.c   |  1 +
 src/bin/pg_dump/compress_lz4.c  | 36 ++++++++++++++++++++++++---------
 src/bin/pg_dump/compress_none.c |  5 +++++
 src/bin/pg_dump/compress_zstd.c | 18 +++++++++++++----
 4 files changed, 46 insertions(+), 14 deletions(-)

diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index cf5358de741..9cadc6f2a3f 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -290,6 +290,7 @@ EndCompressFileHandle(CompressFileHandle *CFH)
 {
 	bool		ret = false;
 
+	errno = 0;
 	if (CFH->private_data)
 		ret = CFH->close_func(CFH);
 
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 2af1a1ffc84..78dfc066ca1 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -664,6 +664,7 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	size_t		status;
+	int			ret;
 
 	fp = state->fp;
 	if (state->inited)
@@ -672,25 +673,31 @@ LZ4Stream_close(CompressFileHandle *CFH)
 		{
 			status = LZ4F_compressEnd(state->ctx, state->buffer, state->buflen, NULL);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
-			else if (fwrite(state->buffer, 1, status, state->fp) != status)
 			{
-				errno = (errno) ? errno : ENOSPC;
-				WRITE_ERROR_EXIT;
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
+			}
+			else
+			{
+				errno = 0;
+				if (fwrite(state->buffer, 1, status, state->fp) != status)
+				{
+					errno = (errno) ? errno : ENOSPC;
+					pg_log_error("could not write to output file: %m");
+				}
 			}
 
 			status = LZ4F_freeCompressionContext(state->ctx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
 		}
 		else
 		{
 			status = LZ4F_freeDecompressionContext(state->dtx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end decompression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end decompression: %s",
+							 LZ4F_getErrorName(status));
 			pg_free(state->overflowbuf);
 		}
 
@@ -698,8 +705,17 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	}
 
 	pg_free(state);
+	CFH->private_data = NULL;
+
+	errno = 0;
+	ret = fclose(fp);
+	if (ret != 0)
+	{
+		pg_log_error("could not close file: %m");
+		return false;
+	}
 
-	return fclose(fp) == 0;
+	return true;
 }
 
 static bool
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index a5179c1050a..9a24ef96cd0 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -149,7 +149,12 @@ close_none(CompressFileHandle *CFH)
 	CFH->private_data = NULL;
 
 	if (fp)
+	{
+		errno = 0;
 		ret = fclose(fp);
+		if (ret != 0)
+			pg_log_error("could not close file: %m");
+	}
 
 	return ret == 0;
 }
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index d133c32cabf..706c2427212 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -418,6 +418,7 @@ static bool
 Zstd_close(CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
+	bool		success = true;
 
 	if (zstdcs->cstream)
 	{
@@ -434,14 +435,18 @@ Zstd_close(CompressFileHandle *CFH)
 			if (ZSTD_isError(res))
 			{
 				zstdcs->zstderror = ZSTD_getErrorName(res);
-				return false;
+				success = false;
+				break;
 			}
 
+			errno = 0;
 			cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 			if (cnt != output->pos)
 			{
+				errno = (errno) ? errno : ENOSPC;
 				zstdcs->zstderror = strerror(errno);
-				return false;
+				success = false;
+				break;
 			}
 
 			if (res == 0)
@@ -458,11 +463,16 @@ Zstd_close(CompressFileHandle *CFH)
 		pg_free(unconstify(void *, zstdcs->input.src));
 	}
 
+	errno = 0;
 	if (fclose(zstdcs->fp) != 0)
-		return false;
+	{
+		zstdcs->zstderror = strerror(errno);
+		success = false;
+	}
 
 	pg_free(zstdcs);
-	return true;
+	CFH->private_data = NULL;
+	return success;
 }
 
 static bool
-- 
2.39.3 (Apple Git-146)

v7-0005-pg_dump-compression-API-LZ4Stream_init.patchapplication/octet-stream; name=v7-0005-pg_dump-compression-API-LZ4Stream_init.patch; x-unix-mode=0644Download
From a2418edc58157200f9fed0e04d448e1c062ce8e8 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 15:08:12 +0200
Subject: [PATCH v7 5/6] pg_dump compression API: LZ4Stream_init

Make sure to not switch to state inited until we know that initialization
succeeded and reset errno just in case.
---
 src/bin/pg_dump/compress_lz4.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 78dfc066ca1..700915fa90d 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -359,7 +359,6 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		return true;
 
 	state->compressing = compressing;
-	state->inited = true;
 
 	/* When compressing, write LZ4 header to the output stream. */
 	if (state->compressing)
@@ -368,6 +367,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		if (!LZ4State_compression_init(state))
 			return false;
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, state->compressedlen, state->fp) != state->compressedlen)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -391,6 +391,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		state->overflowlen = 0;
 	}
 
+	state->inited = true;
 	return true;
 }
 
-- 
2.39.3 (Apple Git-146)

v7-0006-pg_dump-compression-API-read_func-gets_func.patchapplication/octet-stream; name=v7-0006-pg_dump-compression-API-read_func-gets_func.patch; x-unix-mode=0644Download
From d127c9afdff0710babd8db28d6b9870c537cbff7 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Wed, 25 Jun 2025 15:53:00 +0200
Subject: [PATCH v7 6/6] pg_dump compression API: read_func / gets_func

read_func is defined to return the number of bytes read, zero for EOF
and throw an error on all error conditions

 * lz4: Make sure to call throw an error and not return -1
 * gzip: gzread returning zero cannot be assumed to indicate EOF as
   it is documented to return zero for some types of errors.
 * none: Don't call ferror on every fread unless it returne zero and
   we can expect there to be an error or EOF.

gets_func is defined go return *s on success and NULL on error or
if EOF is found before any char has been read.

 * lz4, zstd: Convert _read_internal functions to not automatically
   call pg_fatal on errors to be able to handle gets returning NULL
   on error.
---
 src/bin/pg_dump/compress_gzip.c | 15 +++++++++++++--
 src/bin/pg_dump/compress_io.h   |  2 +-
 src/bin/pg_dump/compress_lz4.c  | 19 +++++++++++++++----
 src/bin/pg_dump/compress_none.c |  2 +-
 src/bin/pg_dump/compress_zstd.c | 28 +++++++++++++++++++++++-----
 5 files changed, 53 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 2edcbffdd8d..4a067e1402c 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -258,10 +258,21 @@ Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret < 0)
+
+	/*
+	 * gzread returns zero on EOF as well as some error conditions, and less
+	 * than zero on other error conditions, so we need to inspect for EOF on
+	 * zero.
+	 */
+	if (gzret <= 0)
 	{
 		int			errnum;
-		const char *errmsg = gzerror(gzfp, &errnum);
+		const char *errmsg;
+
+		if (gzret == 0 && gzeof(gzfp))
+			return 0;
+
+		errmsg = gzerror(gzfp, &errnum);
 
 		pg_fatal("could not read from input file: %s",
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index e4d98ca08d7..82f5c71f764 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -127,7 +127,7 @@ struct CompressFileHandle
 	 * 'ptr'.
 	 *
 	 * Returns number of bytes read (this might be less than 'size' if EOF was
-	 * reached).  Throws error for error conditions other than EOF.
+	 * reached).  Exits via pg_fatal all for error conditions.
 	 */
 	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index 700915fa90d..e2f7c468293 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -459,7 +459,11 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, false /* decompressing */ ))
+	{
+		pg_log_error("unable to initialize LZ4 library: %s",
+					 LZ4F_getErrorName(state->errcode));
 		return -1;
+	}
 
 	/* No work needs to be done for a zero-sized output buffer */
 	if (size <= 0)
@@ -486,7 +490,10 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 		rsize = fread(readbuf, 1, size, state->fp);
 		if (rsize < size && !feof(state->fp))
+		{
+			pg_log_error("could not read from input file: %m");
 			return -1;
+		}
 
 		rp = (char *) readbuf;
 		rend = (char *) readbuf + rsize;
@@ -503,6 +510,8 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 			if (LZ4F_isError(status))
 			{
 				state->errcode = status;
+				pg_log_error("could not read from input file: %s",
+							 LZ4F_getErrorName(state->errcode));
 				return -1;
 			}
 
@@ -639,11 +648,13 @@ LZ4Stream_gets(char *ptr, int size, CompressFileHandle *CFH)
 	int			ret;
 
 	ret = LZ4Stream_read_internal(state, ptr, size - 1, true);
-	if (ret < 0 || (ret == 0 && !LZ4Stream_eof(CFH)))
-		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	/* Done reading */
-	if (ret == 0)
+	/*
+	 * LZ4Stream_read_internal returning 0 or -1 means that it was either an
+	 * EOF or an error, but gets_func is defined to return NULL in either case
+	 * so we can treat both the same here.
+	 */
+	if (ret <= 0)
 		return NULL;
 
 	/*
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 9a24ef96cd0..c6234eec25c 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -90,7 +90,7 @@ read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 	size_t		ret;
 
 	ret = fread(ptr, 1, size, fp);
-	if (ferror(fp))
+	if (ret == 0 && ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
 	return ret;
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index 706c2427212..e24d45e1bbe 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -260,7 +260,7 @@ InitCompressorZstd(CompressorState *cs,
  */
 
 static size_t
-Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+Zstd_read_internal(void *ptr, size_t size, CompressFileHandle *CFH, bool exit_on_error)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -278,7 +278,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 		zstdcs->input.src = pg_malloc0(input_allocated_size);
 		zstdcs->dstream = ZSTD_createDStream();
 		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
+		{
+			if (exit_on_error)
+				pg_fatal("could not initialize compression library");
+			return -1;
+		}
 	}
 
 	output->size = size;
@@ -306,7 +310,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
 			if (ferror(zstdcs->fp))
-				pg_fatal("could not read from input file: %m");
+			{
+				if (exit_on_error)
+					pg_fatal("could not read from input file: %m");
+				return -1;
+			}
 
 			input->size = cnt;
 
@@ -323,7 +331,11 @@ Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
 			res = ZSTD_decompressStream(zstdcs->dstream, output, input);
 
 			if (ZSTD_isError(res))
-				pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+			{
+				if (exit_on_error)
+					pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+				return -1;
+			}
 
 			if (output->pos == output->size)
 				break;			/* No more room for output */
@@ -402,7 +414,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		if (CFH->read_func(&buf[i], 1, CFH) != 1)
+		if (Zstd_read_internal(&buf[i], 1, CFH, false) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
@@ -414,6 +426,12 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	return i > 0 ? buf : NULL;
 }
 
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+{
+	return Zstd_read_internal(ptr, size, CFH, true);
+}
+
 static bool
 Zstd_close(CompressFileHandle *CFH)
 {
-- 
2.39.3 (Apple Git-146)

#22Daniel Gustafsson
daniel@yesql.se
In reply to: Daniel Gustafsson (#21)
1 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

On 26 Jun 2025, at 20:01, Daniel Gustafsson <daniel@yesql.se> wrote:

On 26 Jun 2025, at 15:33, Tom Lane <tgl@sss.pgh.pa.us> wrote:

So on the whole I prefer the "void" approach. I'm not dead
set on that though, it's just a niggling worry.

I think the likelyhood of it being a problem in practice is pretty slim, but
it's still a stronger argument than my "match an API we're still not aligned
with". The attached v7 reverts back to void return.

In preparing for concluding this I've attached a v8 which is the patchset in v7
squashed into a single commit with an attempt at a commit message.

This version has been tested against v17 and v16 where it applies and passes
all tests (the latter isn't as assuring as it should be since there is a lack
of testcoverage).

--
Daniel Gustafsson

Attachments:

v8-0001-pg_dump-Fix-compression-API-errorhandling.patchapplication/octet-stream; name=v8-0001-pg_dump-Fix-compression-API-errorhandling.patch; x-unix-mode=0644Download
From 531a239e3e254e48378d806889396a55b4412a7e Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Tue, 1 Jul 2025 16:23:33 +0200
Subject: [PATCH v8] pg_dump: Fix compression API errorhandling

Compression in pg_dump is implemented with an API where multiple
implementations can co-exist and the selected at runtime by the
user.  The API and its implementations have evolved over time, a
few notable commits include bf9aa490db, e9960732a9, 84adc8e20f5,
and 0da243fed0875.  Upon inspection it was found that the error-
handling specified by the API, and the implementations of it, had
a few deficiencies.  This commit fixes the API as well as all the
implementations to align with the new API definition, as well as
a few other implementations issues.  A full list of changes can
be seen below.

This work was initiated by a report of API misuse, which turned
into a larger examination of the API and its implementations.  As
this is an internal API and not externally exposed it can be back-
patched into all affected branches.

 * write_func:
   - Make write_func throw an error on all error conditions.  All
     callers of write_func were already checking for success and
     calling pg_fatal on all errors, so we might as well make the
     API support that case directly with simpler errorhandling as
     a result.

 * open_func:
   - zstd: move stream initialization from the open function to
     the read and write functions as they can have fatal errors.
     Also ensure to dup the file descriptor like none and gzip.
   - lz4: Ensure to dup the file descriptor like none and gzip.

 * close_func:
   - zstd: Ensure to close the file descriptor even if closing
     down the compressor fails, and clean up state allocation on
     fclose failures.  Make sure to capture errors set by fclose.
   - lz4: Ensure to close the file descriptor even if closing
     down the compressor fails, and instead of calling pg_fatal
     log the failures using pg_log_error. Make sure to capture
     errors set by fclose.
   - none: Make sure to catch errors set by fclose.

 * read_func / gets_func:
   - Make read_func unconditionally return the number of read
     bytes instead of making it optional per implementation.
   - lz4: Make sure to call throw an error and not return -1
   - gzip: gzread returning zero cannot be assumed to indicate
     EOF as it is documented to return zero for some types of
     errors.
   - lz4, zstd: Convert the _read_internal helper functions to
     not call pg_fatal on errors to be able to handle gets_func
     returning NULL on error.

 * getc_func:
   - zstd: Use an unsigned char rather than an int to read char
     into.

 * LZ4Stream_init:
   - Make sure to not switch to inited state until we know that
     initialization succeeded and reset errno just in case.

On top of these changes there are minor comment cleanups and
improvements as well as an attempt to consistently reset errno
in codepaths where it is inspected.

Author: Tom Lane <tgl@sss.pgh.pa.us>
Author: Daniel Gustafsson <daniel@yesql.se>
Reported-by: Evgeniy Gorbanev <gorbanyoves@basealt.ru>
Discussion: https://postgr.es/m/517794.1750082166@sss.pgh.pa.us
Backpatch-through: 16
---
 src/bin/pg_dump/compress_gzip.c       |  35 +++++--
 src/bin/pg_dump/compress_io.c         |   2 +
 src/bin/pg_dump/compress_io.h         |  15 +--
 src/bin/pg_dump/compress_lz4.c        |  92 +++++++++-------
 src/bin/pg_dump/compress_none.c       |  29 +++---
 src/bin/pg_dump/compress_zstd.c       | 144 ++++++++++++++++----------
 src/bin/pg_dump/pg_backup_archiver.c  |   4 +-
 src/bin/pg_dump/pg_backup_directory.c |  52 ++--------
 8 files changed, 208 insertions(+), 165 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 5a30ebf9bf5..4a067e1402c 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -251,34 +251,49 @@ InitCompressorGzip(CompressorState *cs,
  *----------------------
  */
 
-static bool
-Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret <= 0 && !gzeof(gzfp))
+
+	/*
+	 * gzread returns zero on EOF as well as some error conditions, and less
+	 * than zero on other error conditions, so we need to inspect for EOF on
+	 * zero.
+	 */
+	if (gzret <= 0)
 	{
 		int			errnum;
-		const char *errmsg = gzerror(gzfp, &errnum);
+		const char *errmsg;
+
+		if (gzret == 0 && gzeof(gzfp))
+			return 0;
+
+		errmsg = gzerror(gzfp, &errnum);
 
 		pg_fatal("could not read from input file: %s",
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
 	}
 
-	if (rsize)
-		*rsize = (size_t) gzret;
-
-	return true;
+	return (size_t) gzret;
 }
 
-static bool
+static void
 Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
+	int			errnum;
+	const char *errmsg;
 
-	return gzwrite(gzfp, ptr, size) > 0;
+	if (gzwrite(gzfp, ptr, size) != size)
+	{
+		errmsg = gzerror(gzfp, &errnum);
+		pg_fatal("could not write to file: %s",
+				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
+	}
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index 8c3d9c911c4..9cadc6f2a3f 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -269,6 +269,7 @@ InitDiscoverCompressFileHandle(const char *path, const char *mode)
 	}
 
 	CFH = InitCompressFileHandle(compression_spec);
+	errno = 0;
 	if (!CFH->open_func(fname, -1, mode, CFH))
 	{
 		free_keep_errno(CFH);
@@ -289,6 +290,7 @@ EndCompressFileHandle(CompressFileHandle *CFH)
 {
 	bool		ret = false;
 
+	errno = 0;
 	if (CFH->private_data)
 		ret = CFH->close_func(CFH);
 
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index db9b38744c8..82f5c71f764 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -123,21 +123,22 @@ struct CompressFileHandle
 									CompressFileHandle *CFH);
 
 	/*
-	 * Read 'size' bytes of data from the file and store them into 'ptr'.
-	 * Optionally it will store the number of bytes read in 'rsize'.
+	 * Read up to 'size' bytes of data from the file and store them into
+	 * 'ptr'.
 	 *
-	 * Returns true on success and throws an internal error otherwise.
+	 * Returns number of bytes read (this might be less than 'size' if EOF was
+	 * reached).  Exits via pg_fatal all for error conditions.
 	 */
-	bool		(*read_func) (void *ptr, size_t size, size_t *rsize,
+	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
 
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error.
+	 * Returns nothing, exits via pg_fatal for all error conditions.
 	 */
-	bool		(*write_func) (const void *ptr, size_t size,
-							   struct CompressFileHandle *CFH);
+	void		(*write_func) (const void *ptr, size_t size,
+							   CompressFileHandle *CFH);
 
 	/*
 	 * Read at most size - 1 characters from the compress file handle into
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index e99f0cad71f..e2f7c468293 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -12,6 +12,7 @@
  *-------------------------------------------------------------------------
  */
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_lz4.h"
 #include "pg_backup_utils.h"
@@ -358,7 +359,6 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		return true;
 
 	state->compressing = compressing;
-	state->inited = true;
 
 	/* When compressing, write LZ4 header to the output stream. */
 	if (state->compressing)
@@ -367,6 +367,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		if (!LZ4State_compression_init(state))
 			return false;
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, state->compressedlen, state->fp) != state->compressedlen)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -390,6 +391,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		state->overflowlen = 0;
 	}
 
+	state->inited = true;
 	return true;
 }
 
@@ -457,7 +459,11 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, false /* decompressing */ ))
+	{
+		pg_log_error("unable to initialize LZ4 library: %s",
+					 LZ4F_getErrorName(state->errcode));
 		return -1;
+	}
 
 	/* No work needs to be done for a zero-sized output buffer */
 	if (size <= 0)
@@ -484,7 +490,10 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 		rsize = fread(readbuf, 1, size, state->fp);
 		if (rsize < size && !feof(state->fp))
+		{
+			pg_log_error("could not read from input file: %m");
 			return -1;
+		}
 
 		rp = (char *) readbuf;
 		rend = (char *) readbuf + rsize;
@@ -501,6 +510,8 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 			if (LZ4F_isError(status))
 			{
 				state->errcode = status;
+				pg_log_error("could not read from input file: %s",
+							 LZ4F_getErrorName(state->errcode));
 				return -1;
 			}
 
@@ -558,7 +569,7 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 /*
  * Compress size bytes from ptr and write them to the stream.
  */
-static bool
+static void
 LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
@@ -567,7 +578,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, true))
-		return false;
+		pg_fatal("unable to initialize LZ4 library: %s",
+				 LZ4F_getErrorName(state->errcode));
 
 	while (remaining > 0)
 	{
@@ -578,28 +590,24 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		status = LZ4F_compressUpdate(state->ctx, state->buffer, state->buflen,
 									 ptr, chunk, NULL);
 		if (LZ4F_isError(status))
-		{
-			state->errcode = status;
-			return false;
-		}
+			pg_fatal("error during writing: %s", LZ4F_getErrorName(status));
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
-			return false;
+			pg_fatal("error during writing: %m");
 		}
 
 		ptr = ((const char *) ptr) + chunk;
 	}
-
-	return true;
 }
 
 /*
  * fread() equivalent implementation for LZ4 compressed files.
  */
-static bool
-LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+LZ4Stream_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	int			ret;
@@ -607,10 +615,7 @@ LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 	if ((ret = LZ4Stream_read_internal(state, ptr, size, false)) < 0)
 		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	if (rsize)
-		*rsize = (size_t) ret;
-
-	return true;
+	return (size_t) ret;
 }
 
 /*
@@ -643,11 +648,13 @@ LZ4Stream_gets(char *ptr, int size, CompressFileHandle *CFH)
 	int			ret;
 
 	ret = LZ4Stream_read_internal(state, ptr, size - 1, true);
-	if (ret < 0 || (ret == 0 && !LZ4Stream_eof(CFH)))
-		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	/* Done reading */
-	if (ret == 0)
+	/*
+	 * LZ4Stream_read_internal returning 0 or -1 means that it was either an
+	 * EOF or an error, but gets_func is defined to return NULL in either case
+	 * so we can treat both the same here.
+	 */
+	if (ret <= 0)
 		return NULL;
 
 	/*
@@ -669,6 +676,7 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	size_t		status;
+	int			ret;
 
 	fp = state->fp;
 	if (state->inited)
@@ -677,25 +685,31 @@ LZ4Stream_close(CompressFileHandle *CFH)
 		{
 			status = LZ4F_compressEnd(state->ctx, state->buffer, state->buflen, NULL);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
-			else if (fwrite(state->buffer, 1, status, state->fp) != status)
 			{
-				errno = (errno) ? errno : ENOSPC;
-				WRITE_ERROR_EXIT;
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
+			}
+			else
+			{
+				errno = 0;
+				if (fwrite(state->buffer, 1, status, state->fp) != status)
+				{
+					errno = (errno) ? errno : ENOSPC;
+					pg_log_error("could not write to output file: %m");
+				}
 			}
 
 			status = LZ4F_freeCompressionContext(state->ctx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
 		}
 		else
 		{
 			status = LZ4F_freeDecompressionContext(state->dtx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end decompression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end decompression: %s",
+							 LZ4F_getErrorName(status));
 			pg_free(state->overflowbuf);
 		}
 
@@ -703,29 +717,35 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	}
 
 	pg_free(state);
+	CFH->private_data = NULL;
 
-	return fclose(fp) == 0;
+	errno = 0;
+	ret = fclose(fp);
+	if (ret != 0)
+	{
+		pg_log_error("could not close file: %m");
+		return false;
+	}
+
+	return true;
 }
 
 static bool
 LZ4Stream_open(const char *path, int fd, const char *mode,
 			   CompressFileHandle *CFH)
 {
-	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		state->fp = fdopen(dup(fd), mode);
 	else
-		fp = fopen(path, mode);
-	if (fp == NULL)
+		state->fp = fopen(path, mode);
+	if (state->fp == NULL)
 	{
 		state->errcode = errno;
 		return false;
 	}
 
-	state->fp = fp;
-
 	return true;
 }
 
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 3fc89c99854..9a24ef96cd0 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -83,35 +83,31 @@ InitCompressorNone(CompressorState *cs,
  * Private routines
  */
 
-static bool
-read_none(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	FILE	   *fp = (FILE *) CFH->private_data;
 	size_t		ret;
 
-	if (size == 0)
-		return true;
-
 	ret = fread(ptr, 1, size, fp);
-	if (ret != size && !feof(fp))
+	if (ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
-	if (rsize)
-		*rsize = ret;
-
-	return true;
+	return ret;
 }
 
-static bool
+static void
 write_none(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	size_t		ret;
 
+	errno = 0;
 	ret = fwrite(ptr, 1, size, (FILE *) CFH->private_data);
 	if (ret != size)
-		return false;
-
-	return true;
+	{
+		errno = (errno) ? errno : ENOSPC;
+		pg_fatal("coud not write to file: %m");
+	}
 }
 
 static const char *
@@ -153,7 +149,12 @@ close_none(CompressFileHandle *CFH)
 	CFH->private_data = NULL;
 
 	if (fp)
+	{
+		errno = 0;
 		ret = fclose(fp);
+		if (ret != 0)
+			pg_log_error("could not close file: %m");
+	}
 
 	return ret == 0;
 }
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..e24d45e1bbe 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -13,6 +13,7 @@
  */
 
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_zstd.h"
 #include "pg_backup_utils.h"
@@ -258,8 +259,8 @@ InitCompressorZstd(CompressorState *cs,
  * Compressed stream API
  */
 
-static bool
-Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
+static size_t
+Zstd_read_internal(void *ptr, size_t size, CompressFileHandle *CFH, bool exit_on_error)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -268,6 +269,22 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 	size_t		res,
 				cnt;
 
+	/*
+	 * If this is the first call to the reading function, initialize the
+	 * required datastructures.
+	 */
+	if (zstdcs->dstream == NULL)
+	{
+		zstdcs->input.src = pg_malloc0(input_allocated_size);
+		zstdcs->dstream = ZSTD_createDStream();
+		if (zstdcs->dstream == NULL)
+		{
+			if (exit_on_error)
+				pg_fatal("could not initialize compression library");
+			return -1;
+		}
+	}
+
 	output->size = size;
 	output->dst = ptr;
 	output->pos = 0;
@@ -292,6 +309,13 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 		if (input->pos == input->size)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
+			if (ferror(zstdcs->fp))
+			{
+				if (exit_on_error)
+					pg_fatal("could not read from input file: %m");
+				return -1;
+			}
+
 			input->size = cnt;
 
 			Assert(cnt <= input_allocated_size);
@@ -307,7 +331,11 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			res = ZSTD_decompressStream(zstdcs->dstream, output, input);
 
 			if (ZSTD_isError(res))
-				pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+			{
+				if (exit_on_error)
+					pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+				return -1;
+			}
 
 			if (output->pos == output->size)
 				break;			/* No more room for output */
@@ -320,13 +348,10 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			break;				/* We read all the data that fits */
 	}
 
-	if (rdsize != NULL)
-		*rdsize = output->pos;
-
-	return true;
+	return output->pos;
 }
 
-static bool
+static void
 Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
@@ -339,41 +364,40 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 	input->size = size;
 	input->pos = 0;
 
+	if (zstdcs->cstream == NULL)
+	{
+		zstdcs->output.size = ZSTD_CStreamOutSize();
+		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
+		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
+		if (zstdcs->cstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	/* Consume all input, to be flushed later */
 	while (input->pos != input->size)
 	{
 		output->pos = 0;
 		res = ZSTD_compressStream2(zstdcs->cstream, output, input, ZSTD_e_continue);
 		if (ZSTD_isError(res))
-		{
-			zstdcs->zstderror = ZSTD_getErrorName(res);
-			return false;
-		}
+			pg_fatal("could not write to file: %s", ZSTD_getErrorName(res));
 
+		errno = 0;
 		cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 		if (cnt != output->pos)
 		{
-			zstdcs->zstderror = strerror(errno);
-			return false;
+			errno = (errno) ? errno : ENOSPC;
+			pg_fatal("could not write to file: %m");
 		}
 	}
-
-	return size;
 }
 
 static int
 Zstd_getc(CompressFileHandle *CFH)
 {
-	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
-	int			ret;
+	unsigned char ret;
 
-	if (CFH->read_func(&ret, 1, NULL, CFH) != 1)
-	{
-		if (feof(zstdcs->fp))
-			pg_fatal("could not read from input file: end of file");
-		else
-			pg_fatal("could not read from input file: %m");
-	}
+	if (CFH->read_func(&ret, 1, CFH) != 1)
+		pg_fatal("could not read from input file: end of file");
 	return ret;
 }
 
@@ -390,11 +414,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		size_t		readsz;
-
-		if (!CFH->read_func(&buf[i], 1, &readsz, CFH))
-			break;
-		if (readsz != 1)
+		if (Zstd_read_internal(&buf[i], 1, CFH, false) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
@@ -406,10 +426,17 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	return i > 0 ? buf : NULL;
 }
 
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+{
+	return Zstd_read_internal(ptr, size, CFH, true);
+}
+
 static bool
 Zstd_close(CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
+	bool		success = true;
 
 	if (zstdcs->cstream)
 	{
@@ -426,14 +453,18 @@ Zstd_close(CompressFileHandle *CFH)
 			if (ZSTD_isError(res))
 			{
 				zstdcs->zstderror = ZSTD_getErrorName(res);
-				return false;
+				success = false;
+				break;
 			}
 
+			errno = 0;
 			cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 			if (cnt != output->pos)
 			{
+				errno = (errno) ? errno : ENOSPC;
 				zstdcs->zstderror = strerror(errno);
-				return false;
+				success = false;
+				break;
 			}
 
 			if (res == 0)
@@ -450,11 +481,16 @@ Zstd_close(CompressFileHandle *CFH)
 		pg_free(unconstify(void *, zstdcs->input.src));
 	}
 
+	errno = 0;
 	if (fclose(zstdcs->fp) != 0)
-		return false;
+	{
+		zstdcs->zstderror = strerror(errno);
+		success = false;
+	}
 
 	pg_free(zstdcs);
-	return true;
+	CFH->private_data = NULL;
+	return success;
 }
 
 static bool
@@ -472,35 +508,33 @@ Zstd_open(const char *path, int fd, const char *mode,
 	FILE	   *fp;
 	ZstdCompressorState *zstdcs;
 
+	/*
+	 * Clear state storage to avoid having the fd point to non-NULL memory on
+	 * error return.
+	 */
+	CFH->private_data = NULL;
+
+	zstdcs = (ZstdCompressorState *) pg_malloc_extended(sizeof(*zstdcs),
+														MCXT_ALLOC_NO_OOM | MCXT_ALLOC_ZERO);
+	if (!zstdcs)
+	{
+		errno = ENOMEM;
+		return false;
+	}
+
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		fp = fdopen(dup(fd), mode);
 	else
 		fp = fopen(path, mode);
 
 	if (fp == NULL)
+	{
+		pg_free(zstdcs);
 		return false;
+	}
 
-	zstdcs = (ZstdCompressorState *) pg_malloc0(sizeof(*zstdcs));
-	CFH->private_data = zstdcs;
 	zstdcs->fp = fp;
-
-	if (mode[0] == 'r')
-	{
-		zstdcs->input.src = pg_malloc0(ZSTD_DStreamInSize());
-		zstdcs->dstream = ZSTD_createDStream();
-		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else if (mode[0] == 'w' || mode[0] == 'a')
-	{
-		zstdcs->output.size = ZSTD_CStreamOutSize();
-		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
-		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
-		if (zstdcs->cstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else
-		pg_fatal("unhandled mode \"%s\"", mode);
+	CFH->private_data = zstdcs;
 
 	return true;
 }
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93..9145598ff33 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1868,8 +1868,8 @@ ahwrite(const void *ptr, size_t size, size_t nmemb, ArchiveHandle *AH)
 	{
 		CompressFileHandle *CFH = (CompressFileHandle *) AH->OF;
 
-		if (CFH->write_func(ptr, size * nmemb, CFH))
-			bytes_written = size * nmemb;
+		CFH->write_func(ptr, size * nmemb, CFH);
+		bytes_written = size * nmemb;
 	}
 
 	if (bytes_written != size * nmemb)
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index bc2a2fb4797..94d401d8a4e 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -316,15 +316,9 @@ _WriteData(ArchiveHandle *AH, const void *data, size_t dLen)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (dLen > 0 && !CFH->write_func(data, dLen, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	if (dLen <= 0)
+		return;
+	CFH->write_func(data, dLen, CFH);
 }
 
 /*
@@ -351,7 +345,7 @@ _EndData(ArchiveHandle *AH, TocEntry *te)
 static void
 _PrintFileData(ArchiveHandle *AH, char *filename)
 {
-	size_t		cnt = 0;
+	size_t		cnt;
 	char	   *buf;
 	size_t		buflen;
 	CompressFileHandle *CFH;
@@ -366,7 +360,7 @@ _PrintFileData(ArchiveHandle *AH, char *filename)
 	buflen = DEFAULT_IO_BUFFER_SIZE;
 	buf = pg_malloc(buflen);
 
-	while (CFH->read_func(buf, buflen, &cnt, CFH) && cnt > 0)
+	while ((cnt = CFH->read_func(buf, buflen, CFH)) > 0)
 	{
 		ahwrite(buf, 1, cnt, AH);
 	}
@@ -470,16 +464,7 @@ _WriteByte(ArchiveHandle *AH, const int i)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(&c, 1, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
-
+	CFH->write_func(&c, 1, CFH);
 	return 1;
 }
 
@@ -508,15 +493,7 @@ _WriteBuf(ArchiveHandle *AH, const void *buf, size_t len)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
@@ -531,10 +508,10 @@ _ReadBuf(ArchiveHandle *AH, void *buf, size_t len)
 	CompressFileHandle *CFH = ctx->dataFH;
 
 	/*
-	 * If there was an I/O error, we already exited in readF(), so here we
-	 * exit on short reads.
+	 * We do not expect a short read, so fail if we get one.  The read_func
+	 * already dealt with any outright I/O error.
 	 */
-	if (!CFH->read_func(buf, len, NULL, CFH))
+	if (CFH->read_func(buf, len, CFH) != len)
 		pg_fatal("could not read from input file: end of file");
 }
 
@@ -677,14 +654,7 @@ _EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid)
 
 	/* register the LO in blobs_NNN.toc */
 	len = snprintf(buf, sizeof(buf), "%u blob_%u.dat\n", oid, oid);
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to LOs TOC file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

#23Tomas Vondra
tomas@vondra.me
In reply to: Daniel Gustafsson (#22)
Re: No error checking when reading from file using zstd in pg_dump

On 7/1/25 16:24, Daniel Gustafsson wrote:

On 26 Jun 2025, at 20:01, Daniel Gustafsson <daniel@yesql.se> wrote:

On 26 Jun 2025, at 15:33, Tom Lane <tgl@sss.pgh.pa.us> wrote:

So on the whole I prefer the "void" approach. I'm not dead
set on that though, it's just a niggling worry.

I think the likelyhood of it being a problem in practice is pretty slim, but
it's still a stronger argument than my "match an API we're still not aligned
with". The attached v7 reverts back to void return.

In preparing for concluding this I've attached a v8 which is the patchset in v7
squashed into a single commit with an attempt at a commit message.

Thanks!

This version has been tested against v17 and v16 where it applies and passes
all tests (the latter isn't as assuring as it should be since there is a lack
of testcoverage).

Could you elaborate what you mean by lack of test coverage? Doesn't
pg_dump have TAP tests exercising all compression methods? Perhaps it
does not exercise all parts of the code, and we could improve that?

regards

--
Tomas Vondra

#24Daniel Gustafsson
daniel@yesql.se
In reply to: Tomas Vondra (#23)
Re: No error checking when reading from file using zstd in pg_dump

On 1 Jul 2025, at 17:11, Tomas Vondra <tomas@vondra.me> wrote:
On 7/1/25 16:24, Daniel Gustafsson wrote:

This version has been tested against v17 and v16 where it applies and passes
all tests (the latter isn't as assuring as it should be since there is a lack
of testcoverage).

Could you elaborate what you mean by lack of test coverage? Doesn't
pg_dump have TAP tests exercising all compression methods? Perhaps it
does not exercise all parts of the code, and we could improve that?

Sorry, I was unclear. There are indeed lots of pg_dump tests and all
compression methods are tested (the 0% for Zstd on coverage.pg.org is due to
that instance not being compiled with zstd support) but there are functions
which evade testing like getc_func. It's also quite hard to test all the error
paths as that would require some sort of fault injection.

I don't think the current coverage is cause for holding back this patch, but
there is room for improvement to made in the coming cycle.

--
Daniel Gustafsson

#25Tom Lane
tgl@sss.pgh.pa.us
In reply to: Daniel Gustafsson (#22)
Re: No error checking when reading from file using zstd in pg_dump

Daniel Gustafsson <daniel@yesql.se> writes:

In preparing for concluding this I've attached a v8 which is the patchset in v7
squashed into a single commit with an attempt at a commit message.

Glancing through this, I observe a couple of minor typos:

+     * Returns number of bytes read (this might be less than 'size' if EOF was
+     * reached).  Exits via pg_fatal all for error conditions.

s/all for/for all/

+ pg_fatal("coud not write to file: %m");

s/coud/could/

There are some minor typos in the proposed commit message, too.

regards, tom lane

#26Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#25)
1 attachment(s)
Re: No error checking when reading from file using zstd in pg_dump

On 1 Jul 2025, at 17:42, Tom Lane <tgl@sss.pgh.pa.us> wrote:

There are some minor typos in the proposed commit message, too.

It seems that my grasp of the english language went on holiday before I did.
The attached v9 is hopefully less terrible.

--
Daniel Gustafsson

Attachments:

v9-0001-pg_dump-Fix-compression-API-errorhandling.patchapplication/octet-stream; name=v9-0001-pg_dump-Fix-compression-API-errorhandling.patch; x-unix-mode=0644Download
From 5e6be6ac464378d9e28a9516be78c04ea7cb3219 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Tue, 1 Jul 2025 19:45:24 +0200
Subject: [PATCH v9] pg_dump: Fix compression API errorhandling

Compression in pg_dump is abstracted using an API with multiple
implementations which can be selected at runtime by the user.
The API and its implementations have evolved over time, notable
commits include bf9aa490db, e9960732a9, 84adc8e20, and 0da243fed.
The errorhandling defined by the API was however problematic and
the implementations had a few bugs and/or were not following the
API specification.  This commit modifies the API to ensure that
callers can perform errorhandling efficiently and fixes all the
implementations such that they all implement the API in the same
way.  A full list of the changes can be seen below.

 * write_func:
   - Make write_func throw an error on all error conditions.  All
     callers of write_func were already checking for success and
     calling pg_fatal on all errors, so we might as well make the
     API support that case directly with simpler errorhandling as
     a result.

 * open_func:
   - zstd: move stream initialization from the open function to
     the read and write functions as they can have fatal errors.
     Also ensure to dup the file descriptor like none and gzip.
   - lz4: Ensure to dup the file descriptor like none and gzip.

 * close_func:
   - zstd: Ensure to close the file descriptor even if closing
     down the compressor fails, and clean up state allocation on
     fclose failures.  Make sure to capture errors set by fclose.
   - lz4: Ensure to close the file descriptor even if closing
     down the compressor fails, and instead of calling pg_fatal
     log the failures using pg_log_error. Make sure to capture
     errors set by fclose.
   - none: Make sure to catch errors set by fclose.

 * read_func / gets_func:
   - Make read_func unconditionally return the number of read
     bytes instead of making it optional per implementation.
   - lz4: Make sure to call throw an error and not return -1
   - gzip: gzread returning zero cannot be assumed to indicate
     EOF as it is documented to return zero for some types of
     errors.
   - lz4, zstd: Convert the _read_internal helper functions to
     not call pg_fatal on errors to be able to handle gets_func
     returning NULL on error.

 * getc_func:
   - zstd: Use an unsigned char rather than an int to read char
     into.

 * LZ4Stream_init:
   - Make sure to not switch to inited state until we know that
     initialization succeeded and reset errno just in case.

On top of these changes there are minor comment cleanups and
improvements as well as an attempt to consistently reset errno
in codepaths where it is inspected.

This work was initiated by a report of API misuse, which turned
into a larger body of work.  As this is an internal API these
changes can be backpatched into all affected branches.

Author: Tom Lane <tgl@sss.pgh.pa.us>
Author: Daniel Gustafsson <daniel@yesql.se>
Reported-by: Evgeniy Gorbanev <gorbanyoves@basealt.ru>
Discussion: https://postgr.es/m/517794.1750082166@sss.pgh.pa.us
Backpatch-through: 16
---
 src/bin/pg_dump/compress_gzip.c       |  35 +++++--
 src/bin/pg_dump/compress_io.c         |   2 +
 src/bin/pg_dump/compress_io.h         |  15 +--
 src/bin/pg_dump/compress_lz4.c        |  92 +++++++++-------
 src/bin/pg_dump/compress_none.c       |  29 +++---
 src/bin/pg_dump/compress_zstd.c       | 144 ++++++++++++++++----------
 src/bin/pg_dump/pg_backup_archiver.c  |   4 +-
 src/bin/pg_dump/pg_backup_directory.c |  52 ++--------
 8 files changed, 208 insertions(+), 165 deletions(-)

diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c
index 5a30ebf9bf5..4a067e1402c 100644
--- a/src/bin/pg_dump/compress_gzip.c
+++ b/src/bin/pg_dump/compress_gzip.c
@@ -251,34 +251,49 @@ InitCompressorGzip(CompressorState *cs,
  *----------------------
  */
 
-static bool
-Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
 	int			gzret;
 
 	gzret = gzread(gzfp, ptr, size);
-	if (gzret <= 0 && !gzeof(gzfp))
+
+	/*
+	 * gzread returns zero on EOF as well as some error conditions, and less
+	 * than zero on other error conditions, so we need to inspect for EOF on
+	 * zero.
+	 */
+	if (gzret <= 0)
 	{
 		int			errnum;
-		const char *errmsg = gzerror(gzfp, &errnum);
+		const char *errmsg;
+
+		if (gzret == 0 && gzeof(gzfp))
+			return 0;
+
+		errmsg = gzerror(gzfp, &errnum);
 
 		pg_fatal("could not read from input file: %s",
 				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
 	}
 
-	if (rsize)
-		*rsize = (size_t) gzret;
-
-	return true;
+	return (size_t) gzret;
 }
 
-static bool
+static void
 Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	gzFile		gzfp = (gzFile) CFH->private_data;
+	int			errnum;
+	const char *errmsg;
 
-	return gzwrite(gzfp, ptr, size) > 0;
+	if (gzwrite(gzfp, ptr, size) != size)
+	{
+		errmsg = gzerror(gzfp, &errnum);
+		pg_fatal("could not write to file: %s",
+				 errnum == Z_ERRNO ? strerror(errno) : errmsg);
+	}
 }
 
 static int
diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c
index 8c3d9c911c4..9cadc6f2a3f 100644
--- a/src/bin/pg_dump/compress_io.c
+++ b/src/bin/pg_dump/compress_io.c
@@ -269,6 +269,7 @@ InitDiscoverCompressFileHandle(const char *path, const char *mode)
 	}
 
 	CFH = InitCompressFileHandle(compression_spec);
+	errno = 0;
 	if (!CFH->open_func(fname, -1, mode, CFH))
 	{
 		free_keep_errno(CFH);
@@ -289,6 +290,7 @@ EndCompressFileHandle(CompressFileHandle *CFH)
 {
 	bool		ret = false;
 
+	errno = 0;
 	if (CFH->private_data)
 		ret = CFH->close_func(CFH);
 
diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h
index db9b38744c8..25a7bf0904d 100644
--- a/src/bin/pg_dump/compress_io.h
+++ b/src/bin/pg_dump/compress_io.h
@@ -123,21 +123,22 @@ struct CompressFileHandle
 									CompressFileHandle *CFH);
 
 	/*
-	 * Read 'size' bytes of data from the file and store them into 'ptr'.
-	 * Optionally it will store the number of bytes read in 'rsize'.
+	 * Read up to 'size' bytes of data from the file and store them into
+	 * 'ptr'.
 	 *
-	 * Returns true on success and throws an internal error otherwise.
+	 * Returns number of bytes read (this might be less than 'size' if EOF was
+	 * reached).  Exits via pg_fatal for all error conditions.
 	 */
-	bool		(*read_func) (void *ptr, size_t size, size_t *rsize,
+	size_t		(*read_func) (void *ptr, size_t size,
 							  CompressFileHandle *CFH);
 
 	/*
 	 * Write 'size' bytes of data into the file from 'ptr'.
 	 *
-	 * Returns true on success and false on error.
+	 * Returns nothing, exits via pg_fatal for all error conditions.
 	 */
-	bool		(*write_func) (const void *ptr, size_t size,
-							   struct CompressFileHandle *CFH);
+	void		(*write_func) (const void *ptr, size_t size,
+							   CompressFileHandle *CFH);
 
 	/*
 	 * Read at most size - 1 characters from the compress file handle into
diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c
index e99f0cad71f..e2f7c468293 100644
--- a/src/bin/pg_dump/compress_lz4.c
+++ b/src/bin/pg_dump/compress_lz4.c
@@ -12,6 +12,7 @@
  *-------------------------------------------------------------------------
  */
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_lz4.h"
 #include "pg_backup_utils.h"
@@ -358,7 +359,6 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		return true;
 
 	state->compressing = compressing;
-	state->inited = true;
 
 	/* When compressing, write LZ4 header to the output stream. */
 	if (state->compressing)
@@ -367,6 +367,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		if (!LZ4State_compression_init(state))
 			return false;
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, state->compressedlen, state->fp) != state->compressedlen)
 		{
 			errno = (errno) ? errno : ENOSPC;
@@ -390,6 +391,7 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing)
 		state->overflowlen = 0;
 	}
 
+	state->inited = true;
 	return true;
 }
 
@@ -457,7 +459,11 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, false /* decompressing */ ))
+	{
+		pg_log_error("unable to initialize LZ4 library: %s",
+					 LZ4F_getErrorName(state->errcode));
 		return -1;
+	}
 
 	/* No work needs to be done for a zero-sized output buffer */
 	if (size <= 0)
@@ -484,7 +490,10 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 
 		rsize = fread(readbuf, 1, size, state->fp);
 		if (rsize < size && !feof(state->fp))
+		{
+			pg_log_error("could not read from input file: %m");
 			return -1;
+		}
 
 		rp = (char *) readbuf;
 		rend = (char *) readbuf + rsize;
@@ -501,6 +510,8 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 			if (LZ4F_isError(status))
 			{
 				state->errcode = status;
+				pg_log_error("could not read from input file: %s",
+							 LZ4F_getErrorName(state->errcode));
 				return -1;
 			}
 
@@ -558,7 +569,7 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag)
 /*
  * Compress size bytes from ptr and write them to the stream.
  */
-static bool
+static void
 LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
@@ -567,7 +578,8 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 
 	/* Lazy init */
 	if (!LZ4Stream_init(state, size, true))
-		return false;
+		pg_fatal("unable to initialize LZ4 library: %s",
+				 LZ4F_getErrorName(state->errcode));
 
 	while (remaining > 0)
 	{
@@ -578,28 +590,24 @@ LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 		status = LZ4F_compressUpdate(state->ctx, state->buffer, state->buflen,
 									 ptr, chunk, NULL);
 		if (LZ4F_isError(status))
-		{
-			state->errcode = status;
-			return false;
-		}
+			pg_fatal("error during writing: %s", LZ4F_getErrorName(status));
 
+		errno = 0;
 		if (fwrite(state->buffer, 1, status, state->fp) != status)
 		{
 			errno = (errno) ? errno : ENOSPC;
-			return false;
+			pg_fatal("error during writing: %m");
 		}
 
 		ptr = ((const char *) ptr) + chunk;
 	}
-
-	return true;
 }
 
 /*
  * fread() equivalent implementation for LZ4 compressed files.
  */
-static bool
-LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+LZ4Stream_read(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	int			ret;
@@ -607,10 +615,7 @@ LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
 	if ((ret = LZ4Stream_read_internal(state, ptr, size, false)) < 0)
 		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	if (rsize)
-		*rsize = (size_t) ret;
-
-	return true;
+	return (size_t) ret;
 }
 
 /*
@@ -643,11 +648,13 @@ LZ4Stream_gets(char *ptr, int size, CompressFileHandle *CFH)
 	int			ret;
 
 	ret = LZ4Stream_read_internal(state, ptr, size - 1, true);
-	if (ret < 0 || (ret == 0 && !LZ4Stream_eof(CFH)))
-		pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH));
 
-	/* Done reading */
-	if (ret == 0)
+	/*
+	 * LZ4Stream_read_internal returning 0 or -1 means that it was either an
+	 * EOF or an error, but gets_func is defined to return NULL in either case
+	 * so we can treat both the same here.
+	 */
+	if (ret <= 0)
 		return NULL;
 
 	/*
@@ -669,6 +676,7 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 	size_t		status;
+	int			ret;
 
 	fp = state->fp;
 	if (state->inited)
@@ -677,25 +685,31 @@ LZ4Stream_close(CompressFileHandle *CFH)
 		{
 			status = LZ4F_compressEnd(state->ctx, state->buffer, state->buflen, NULL);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
-			else if (fwrite(state->buffer, 1, status, state->fp) != status)
 			{
-				errno = (errno) ? errno : ENOSPC;
-				WRITE_ERROR_EXIT;
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
+			}
+			else
+			{
+				errno = 0;
+				if (fwrite(state->buffer, 1, status, state->fp) != status)
+				{
+					errno = (errno) ? errno : ENOSPC;
+					pg_log_error("could not write to output file: %m");
+				}
 			}
 
 			status = LZ4F_freeCompressionContext(state->ctx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end compression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end compression: %s",
+							 LZ4F_getErrorName(status));
 		}
 		else
 		{
 			status = LZ4F_freeDecompressionContext(state->dtx);
 			if (LZ4F_isError(status))
-				pg_fatal("could not end decompression: %s",
-						 LZ4F_getErrorName(status));
+				pg_log_error("could not end decompression: %s",
+							 LZ4F_getErrorName(status));
 			pg_free(state->overflowbuf);
 		}
 
@@ -703,29 +717,35 @@ LZ4Stream_close(CompressFileHandle *CFH)
 	}
 
 	pg_free(state);
+	CFH->private_data = NULL;
 
-	return fclose(fp) == 0;
+	errno = 0;
+	ret = fclose(fp);
+	if (ret != 0)
+	{
+		pg_log_error("could not close file: %m");
+		return false;
+	}
+
+	return true;
 }
 
 static bool
 LZ4Stream_open(const char *path, int fd, const char *mode,
 			   CompressFileHandle *CFH)
 {
-	FILE	   *fp;
 	LZ4State   *state = (LZ4State *) CFH->private_data;
 
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		state->fp = fdopen(dup(fd), mode);
 	else
-		fp = fopen(path, mode);
-	if (fp == NULL)
+		state->fp = fopen(path, mode);
+	if (state->fp == NULL)
 	{
 		state->errcode = errno;
 		return false;
 	}
 
-	state->fp = fp;
-
 	return true;
 }
 
diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c
index 3fc89c99854..4abb2e95abc 100644
--- a/src/bin/pg_dump/compress_none.c
+++ b/src/bin/pg_dump/compress_none.c
@@ -83,35 +83,31 @@ InitCompressorNone(CompressorState *cs,
  * Private routines
  */
 
-static bool
-read_none(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH)
+static size_t
+read_none(void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	FILE	   *fp = (FILE *) CFH->private_data;
 	size_t		ret;
 
-	if (size == 0)
-		return true;
-
 	ret = fread(ptr, 1, size, fp);
-	if (ret != size && !feof(fp))
+	if (ferror(fp))
 		pg_fatal("could not read from input file: %m");
 
-	if (rsize)
-		*rsize = ret;
-
-	return true;
+	return ret;
 }
 
-static bool
+static void
 write_none(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	size_t		ret;
 
+	errno = 0;
 	ret = fwrite(ptr, 1, size, (FILE *) CFH->private_data);
 	if (ret != size)
-		return false;
-
-	return true;
+	{
+		errno = (errno) ? errno : ENOSPC;
+		pg_fatal("could not write to file: %m");
+	}
 }
 
 static const char *
@@ -153,7 +149,12 @@ close_none(CompressFileHandle *CFH)
 	CFH->private_data = NULL;
 
 	if (fp)
+	{
+		errno = 0;
 		ret = fclose(fp);
+		if (ret != 0)
+			pg_log_error("could not close file: %m");
+	}
 
 	return ret == 0;
 }
diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c
index cb595b10c2d..e24d45e1bbe 100644
--- a/src/bin/pg_dump/compress_zstd.c
+++ b/src/bin/pg_dump/compress_zstd.c
@@ -13,6 +13,7 @@
  */
 
 #include "postgres_fe.h"
+#include <unistd.h>
 
 #include "compress_zstd.h"
 #include "pg_backup_utils.h"
@@ -258,8 +259,8 @@ InitCompressorZstd(CompressorState *cs,
  * Compressed stream API
  */
 
-static bool
-Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
+static size_t
+Zstd_read_internal(void *ptr, size_t size, CompressFileHandle *CFH, bool exit_on_error)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
 	ZSTD_inBuffer *input = &zstdcs->input;
@@ -268,6 +269,22 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 	size_t		res,
 				cnt;
 
+	/*
+	 * If this is the first call to the reading function, initialize the
+	 * required datastructures.
+	 */
+	if (zstdcs->dstream == NULL)
+	{
+		zstdcs->input.src = pg_malloc0(input_allocated_size);
+		zstdcs->dstream = ZSTD_createDStream();
+		if (zstdcs->dstream == NULL)
+		{
+			if (exit_on_error)
+				pg_fatal("could not initialize compression library");
+			return -1;
+		}
+	}
+
 	output->size = size;
 	output->dst = ptr;
 	output->pos = 0;
@@ -292,6 +309,13 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 		if (input->pos == input->size)
 		{
 			cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp);
+			if (ferror(zstdcs->fp))
+			{
+				if (exit_on_error)
+					pg_fatal("could not read from input file: %m");
+				return -1;
+			}
+
 			input->size = cnt;
 
 			Assert(cnt <= input_allocated_size);
@@ -307,7 +331,11 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			res = ZSTD_decompressStream(zstdcs->dstream, output, input);
 
 			if (ZSTD_isError(res))
-				pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+			{
+				if (exit_on_error)
+					pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res));
+				return -1;
+			}
 
 			if (output->pos == output->size)
 				break;			/* No more room for output */
@@ -320,13 +348,10 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH)
 			break;				/* We read all the data that fits */
 	}
 
-	if (rdsize != NULL)
-		*rdsize = output->pos;
-
-	return true;
+	return output->pos;
 }
 
-static bool
+static void
 Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
@@ -339,41 +364,40 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH)
 	input->size = size;
 	input->pos = 0;
 
+	if (zstdcs->cstream == NULL)
+	{
+		zstdcs->output.size = ZSTD_CStreamOutSize();
+		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
+		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
+		if (zstdcs->cstream == NULL)
+			pg_fatal("could not initialize compression library");
+	}
+
 	/* Consume all input, to be flushed later */
 	while (input->pos != input->size)
 	{
 		output->pos = 0;
 		res = ZSTD_compressStream2(zstdcs->cstream, output, input, ZSTD_e_continue);
 		if (ZSTD_isError(res))
-		{
-			zstdcs->zstderror = ZSTD_getErrorName(res);
-			return false;
-		}
+			pg_fatal("could not write to file: %s", ZSTD_getErrorName(res));
 
+		errno = 0;
 		cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 		if (cnt != output->pos)
 		{
-			zstdcs->zstderror = strerror(errno);
-			return false;
+			errno = (errno) ? errno : ENOSPC;
+			pg_fatal("could not write to file: %m");
 		}
 	}
-
-	return size;
 }
 
 static int
 Zstd_getc(CompressFileHandle *CFH)
 {
-	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
-	int			ret;
+	unsigned char ret;
 
-	if (CFH->read_func(&ret, 1, NULL, CFH) != 1)
-	{
-		if (feof(zstdcs->fp))
-			pg_fatal("could not read from input file: end of file");
-		else
-			pg_fatal("could not read from input file: %m");
-	}
+	if (CFH->read_func(&ret, 1, CFH) != 1)
+		pg_fatal("could not read from input file: end of file");
 	return ret;
 }
 
@@ -390,11 +414,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	 */
 	for (i = 0; i < len - 1; ++i)
 	{
-		size_t		readsz;
-
-		if (!CFH->read_func(&buf[i], 1, &readsz, CFH))
-			break;
-		if (readsz != 1)
+		if (Zstd_read_internal(&buf[i], 1, CFH, false) != 1)
 			break;
 		if (buf[i] == '\n')
 		{
@@ -406,10 +426,17 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH)
 	return i > 0 ? buf : NULL;
 }
 
+static size_t
+Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH)
+{
+	return Zstd_read_internal(ptr, size, CFH, true);
+}
+
 static bool
 Zstd_close(CompressFileHandle *CFH)
 {
 	ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data;
+	bool		success = true;
 
 	if (zstdcs->cstream)
 	{
@@ -426,14 +453,18 @@ Zstd_close(CompressFileHandle *CFH)
 			if (ZSTD_isError(res))
 			{
 				zstdcs->zstderror = ZSTD_getErrorName(res);
-				return false;
+				success = false;
+				break;
 			}
 
+			errno = 0;
 			cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp);
 			if (cnt != output->pos)
 			{
+				errno = (errno) ? errno : ENOSPC;
 				zstdcs->zstderror = strerror(errno);
-				return false;
+				success = false;
+				break;
 			}
 
 			if (res == 0)
@@ -450,11 +481,16 @@ Zstd_close(CompressFileHandle *CFH)
 		pg_free(unconstify(void *, zstdcs->input.src));
 	}
 
+	errno = 0;
 	if (fclose(zstdcs->fp) != 0)
-		return false;
+	{
+		zstdcs->zstderror = strerror(errno);
+		success = false;
+	}
 
 	pg_free(zstdcs);
-	return true;
+	CFH->private_data = NULL;
+	return success;
 }
 
 static bool
@@ -472,35 +508,33 @@ Zstd_open(const char *path, int fd, const char *mode,
 	FILE	   *fp;
 	ZstdCompressorState *zstdcs;
 
+	/*
+	 * Clear state storage to avoid having the fd point to non-NULL memory on
+	 * error return.
+	 */
+	CFH->private_data = NULL;
+
+	zstdcs = (ZstdCompressorState *) pg_malloc_extended(sizeof(*zstdcs),
+														MCXT_ALLOC_NO_OOM | MCXT_ALLOC_ZERO);
+	if (!zstdcs)
+	{
+		errno = ENOMEM;
+		return false;
+	}
+
 	if (fd >= 0)
-		fp = fdopen(fd, mode);
+		fp = fdopen(dup(fd), mode);
 	else
 		fp = fopen(path, mode);
 
 	if (fp == NULL)
+	{
+		pg_free(zstdcs);
 		return false;
+	}
 
-	zstdcs = (ZstdCompressorState *) pg_malloc0(sizeof(*zstdcs));
-	CFH->private_data = zstdcs;
 	zstdcs->fp = fp;
-
-	if (mode[0] == 'r')
-	{
-		zstdcs->input.src = pg_malloc0(ZSTD_DStreamInSize());
-		zstdcs->dstream = ZSTD_createDStream();
-		if (zstdcs->dstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else if (mode[0] == 'w' || mode[0] == 'a')
-	{
-		zstdcs->output.size = ZSTD_CStreamOutSize();
-		zstdcs->output.dst = pg_malloc0(zstdcs->output.size);
-		zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec);
-		if (zstdcs->cstream == NULL)
-			pg_fatal("could not initialize compression library");
-	}
-	else
-		pg_fatal("unhandled mode \"%s\"", mode);
+	CFH->private_data = zstdcs;
 
 	return true;
 }
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 197c1295d93..9145598ff33 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -1868,8 +1868,8 @@ ahwrite(const void *ptr, size_t size, size_t nmemb, ArchiveHandle *AH)
 	{
 		CompressFileHandle *CFH = (CompressFileHandle *) AH->OF;
 
-		if (CFH->write_func(ptr, size * nmemb, CFH))
-			bytes_written = size * nmemb;
+		CFH->write_func(ptr, size * nmemb, CFH);
+		bytes_written = size * nmemb;
 	}
 
 	if (bytes_written != size * nmemb)
diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c
index bc2a2fb4797..94d401d8a4e 100644
--- a/src/bin/pg_dump/pg_backup_directory.c
+++ b/src/bin/pg_dump/pg_backup_directory.c
@@ -316,15 +316,9 @@ _WriteData(ArchiveHandle *AH, const void *data, size_t dLen)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (dLen > 0 && !CFH->write_func(data, dLen, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	if (dLen <= 0)
+		return;
+	CFH->write_func(data, dLen, CFH);
 }
 
 /*
@@ -351,7 +345,7 @@ _EndData(ArchiveHandle *AH, TocEntry *te)
 static void
 _PrintFileData(ArchiveHandle *AH, char *filename)
 {
-	size_t		cnt = 0;
+	size_t		cnt;
 	char	   *buf;
 	size_t		buflen;
 	CompressFileHandle *CFH;
@@ -366,7 +360,7 @@ _PrintFileData(ArchiveHandle *AH, char *filename)
 	buflen = DEFAULT_IO_BUFFER_SIZE;
 	buf = pg_malloc(buflen);
 
-	while (CFH->read_func(buf, buflen, &cnt, CFH) && cnt > 0)
+	while ((cnt = CFH->read_func(buf, buflen, CFH)) > 0)
 	{
 		ahwrite(buf, 1, cnt, AH);
 	}
@@ -470,16 +464,7 @@ _WriteByte(ArchiveHandle *AH, const int i)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(&c, 1, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
-
+	CFH->write_func(&c, 1, CFH);
 	return 1;
 }
 
@@ -508,15 +493,7 @@ _WriteBuf(ArchiveHandle *AH, const void *buf, size_t len)
 	lclContext *ctx = (lclContext *) AH->formatData;
 	CompressFileHandle *CFH = ctx->dataFH;
 
-	errno = 0;
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to output file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
@@ -531,10 +508,10 @@ _ReadBuf(ArchiveHandle *AH, void *buf, size_t len)
 	CompressFileHandle *CFH = ctx->dataFH;
 
 	/*
-	 * If there was an I/O error, we already exited in readF(), so here we
-	 * exit on short reads.
+	 * We do not expect a short read, so fail if we get one.  The read_func
+	 * already dealt with any outright I/O error.
 	 */
-	if (!CFH->read_func(buf, len, NULL, CFH))
+	if (CFH->read_func(buf, len, CFH) != len)
 		pg_fatal("could not read from input file: end of file");
 }
 
@@ -677,14 +654,7 @@ _EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid)
 
 	/* register the LO in blobs_NNN.toc */
 	len = snprintf(buf, sizeof(buf), "%u blob_%u.dat\n", oid, oid);
-	if (!CFH->write_func(buf, len, CFH))
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to LOs TOC file: %s",
-				 CFH->get_error_func(CFH));
-	}
+	CFH->write_func(buf, len, CFH);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

#27Tom Lane
tgl@sss.pgh.pa.us
In reply to: Daniel Gustafsson (#26)
Re: No error checking when reading from file using zstd in pg_dump

Daniel Gustafsson <daniel@yesql.se> writes:

On 1 Jul 2025, at 17:42, Tom Lane <tgl@sss.pgh.pa.us> wrote:
There are some minor typos in the proposed commit message, too.

It seems that my grasp of the english language went on holiday before I did.
The attached v9 is hopefully less terrible.

This thread seems to have faded away, which I fear is my fault.
I took another look through v9, and it seems fine. Do you want
to deal with pushing it, or shall I?

regards, tom lane

#28Daniel Gustafsson
daniel@yesql.se
In reply to: Tom Lane (#27)
Re: No error checking when reading from file using zstd in pg_dump

On 24 Aug 2025, at 23:21, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Daniel Gustafsson <daniel@yesql.se> writes:

On 1 Jul 2025, at 17:42, Tom Lane <tgl@sss.pgh.pa.us> wrote:
There are some minor typos in the proposed commit message, too.

It seems that my grasp of the english language went on holiday before I did.
The attached v9 is hopefully less terrible.

This thread seems to have faded away, which I fear is my fault.
I took another look through v9, and it seems fine. Do you want
to deal with pushing it, or shall I?

Thanks for the review, this one had admittedly slipped my mind. I'll work on
getting it in at the start of the week.

--
Daniel Gustafsson

#29Daniel Gustafsson
daniel@yesql.se
In reply to: Daniel Gustafsson (#28)
Re: No error checking when reading from file using zstd in pg_dump

On 24 Aug 2025, at 23:23, Daniel Gustafsson <daniel@yesql.se> wrote:

I'll work on getting it in at the start of the week.

While not exactly the start of the week, this is now done.

--
Daniel Gustafsson