agora inbox for pgsql-bugs@postgresql.org  
help / color / mirror / Atom feed
BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
13+ messages / 4 participants
[nested] [flat]

* BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
@ 2026-08-02 17:49 PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: PG Bug reporting form @ 2026-08-02 17:49 UTC (permalink / raw)
  To: pgsql-bugs@lists.postgresql.org; +Cc: malis@pgrust.com

The following bug has been logged on the website:

Bug reference:      19598
Logged by:          Michael Malis
Email address:      malis@pgrust.com
PostgreSQL version: 18.3
Operating system:   Debian
Description:        

Both LSN-accepting options parse with sscanf(optarg, "%X/%X", &xlogid,
&xrecoff) into two uint32s, with no length or range check. %X converts via
strtoul: a component that overflows uint32 is truncated to its low 32 bits,
and one that overflows uint64 saturates and then truncates. In both cases
sscanf still returns 2, so the != 2 "invalid WAL location" guard never fires
and the tool proceeds with a value the user did not ask for. PostgreSQL's
own canonical LSN parser rejects the same input.

Reproducer (runnable against stock PostgreSQL 18.3)
---------------------------------------------------
    $ pg_waldump -s 123456789/0 000000010000000000000040
    pg_waldump: error: start WAL location 23456789/0 is not inside file
"000000010000000000000040"

Note the echoed value: the 9-hex-digit input 123456789 was silently reduced
to 23456789. The saturating case:
    $ pg_waldump -s FFFFFFFFFFFFFFFFFFFF/0 000000010000000000000040
    pg_waldump: error: start WAL location FFFFFFFF/0 is not inside file
"..."

Control — a genuinely malformed value is rejected, so the guard works, it
just never sees these inputs:
    $ pg_waldump -s ZZZ/0 000000010000000000000040
    pg_waldump: error: invalid WAL location: "ZZZ/0"

Contrast with the server's own parser on the identical string:
    SELECT '123456789/0'::pg_lsn;
    ERROR:  invalid input syntax for type pg_lsn: "123456789/0"

Expected vs. actual
-------------------
- Expected: pg_waldump: error: invalid WAL location: "123456789/0", as for
  any other unparseable value.
- Actual: the value is accepted, silently mangled to 23456789/0, and used.
  The error text the user eventually sees reports the mangled location,
  which actively misleads: it reads as "the location you asked for isn't in
  this file" when the location asked for was never used.








^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
@ 2026-08-04 08:54 ` Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Zexin Li @ 2026-08-04 08:54 UTC (permalink / raw)
  To: pgsql-bugs@lists.postgresql.org; +Cc: malis@pgrust.com

On Sun, Aug 2, 2026, Michael Malis wrote:
> Note the echoed value: the 9-hex-digit input 123456789 was silently
> reduced to 23456789.

Thanks for the report -- reproduced on current master (03f420c37f).
Patch attached.

The != 2 guard only fires when fewer than two conversions complete,
i.e. when %X or the '/' literal fails outright, as with "ZZZ/0". It
cannot reject inputs where both conversions succeed on the wrong
bytes, which happens through three properties of sscanf():

1. The first %X carries no field width, so a component wider than 32
bits overflows the uint32 argument -- undefined behavior per C99
7.19.6.2p10; glibc keeps the low-order 32 bits, which is the mangling
you observed. A component wider than 64 bits saturates to ULONG_MAX
at the strtoul() level first, so FFFFFFFFFFFFFFFFFFFF/0 runs as
FFFFFFFF/0.

2. sscanf() succeeds without consuming the whole string. On master
the low component is %08X, so "0/123456789" stops after eight digits
and runs as 0/12345678 ("1/2/3" runs as 1/2); on 18.x, with a bare %X
there, the same input instead wraps modulo 2^32.

3. %X follows strtoul()'s subject-sequence rules, accepting leading
whitespace, signs, and "0x" prefixes: "-1/0" runs as FFFFFFFF/0.

The patch replaces the two sscanf() calls with a static helper that
follows the backend's parser for this syntax, pg_lsn_in_safe() in
src/backend/utils/adt/pg_lsn.c: strspn() over the hex charset, one to
eight digits per component, separator and terminating NUL checked by
position. The patch intentionally does not change the "invalid WAL
location" error text, the treatment of any input the server considers
valid (including 8-digit and mixed-case components), or the
already-rejected cases, so scripts matching on the error text are
unaffected.

Measured on master against a real segment. After the patch, -s/-e
accept exactly what the server accepts as pg_lsn -- one to eight hex
digits, a slash, one to eight hex digits, nothing else. Each line
shows unpatched behavior first, patched behavior second:

123456789/0: ran as 23456789/00000000; now rejected
FFFFFFFFFFFFFFFFFFFF/0: ran as FFFFFFFF/00000000; now rejected
0/123456789: ran as 0/12345678; now rejected
1/2/3: ran as 1/00000002; now rejected
0x1/0: ran as 1/00000000; now rejected
-1/0: ran as FFFFFFFF/00000000; now rejected
" 1/0": ran as 1/00000000; now rejected

"Rejected" is the existing "invalid WAL location" error; all seven
inputs are already rejected by the server when cast to pg_lsn, so the
tool and the server now agree on every string. Unchanged: valid
inputs (0/1000028, 0/0, FFFFFFFF/FFFFFFFF, abcdef/ABCDEF) parse as
before, and inputs that already failed ("bad", "ZZZ/0") keep failing
the same way.

Regression tests are included next to the existing invalid-LSN checks;
without the fix the four new cases fail. The pg_waldump TAP suite and
make check pass here.

The same pattern parses user-supplied LSNs in pg_recvlogical (-I/-E)
and pg_receivewal (-E); pg_basebackup and pg_rewind only parse
server-returned strings. I kept this patch to pg_waldump to match
the report's scope, and can send a follow-up moving the helper next
to option_parse_int() in fe_utils to cover the other two if that
seems worthwhile.

Regards,
Zexin Li

On Mon, Aug 03, 2026 03:12 AM, PG Bug reporting form <noreply@postgresql.org>
wrote:

> The following bug has been logged on the website:
>
> Bug reference:      19598
> Logged by:          Michael Malis
> Email address:      malis@pgrust.com
> PostgreSQL version: 18.3
> Operating system:   Debian
> Description:
>
> Both LSN-accepting options parse with sscanf(optarg, "%X/%X", &xlogid,
> &xrecoff) into two uint32s, with no length or range check. %X converts via
> strtoul: a component that overflows uint32 is truncated to its low 32 bits,
> and one that overflows uint64 saturates and then truncates. In both cases
> sscanf still returns 2, so the != 2 "invalid WAL location" guard never
> fires
> and the tool proceeds with a value the user did not ask for. PostgreSQL's
> own canonical LSN parser rejects the same input.
>
> Reproducer (runnable against stock PostgreSQL 18.3)
> ---------------------------------------------------
>     $ pg_waldump -s 123456789/0 000000010000000000000040
>     pg_waldump: error: start WAL location 23456789/0 is not inside file
> "000000010000000000000040"
>
> Note the echoed value: the 9-hex-digit input 123456789 was silently reduced
> to 23456789. The saturating case:
>     $ pg_waldump -s FFFFFFFFFFFFFFFFFFFF/0 000000010000000000000040
>     pg_waldump: error: start WAL location FFFFFFFF/0 is not inside file
> "..."
>
> Control — a genuinely malformed value is rejected, so the guard works, it
> just never sees these inputs:
>     $ pg_waldump -s ZZZ/0 000000010000000000000040
>     pg_waldump: error: invalid WAL location: "ZZZ/0"
>
> Contrast with the server's own parser on the identical string:
>     SELECT '123456789/0'::pg_lsn;
>     ERROR:  invalid input syntax for type pg_lsn: "123456789/0"
>
> Expected vs. actual
> -------------------
> - Expected: pg_waldump: error: invalid WAL location: "123456789/0", as for
>   any other unparseable value.
> - Actual: the value is accepted, silently mangled to 23456789/0, and used.
>   The error text the user eventually sees reports the mangled location,
>   which actively misleads: it reads as "the location you asked for isn't in
>   this file" when the location asked for was never used.
>
>
>
>
>

Attachments:

  [application/x-patch] 0001-Reject-invalid-WAL-locations-in-pg_waldump-s-s-e-opt.patch (5.1K, ../../CAAP6ZkTzff9LQ3Qja1edo5wuKjGiYLq3WmODouuHfA3oHcOpPA@mail.gmail.com/3-0001-Reject-invalid-WAL-locations-in-pg_waldump-s-s-e-opt.patch)
  download | inline diff:
From 8b81786b31c1b94ae248a8d0235e79d94ed6ac01 Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Tue, 4 Aug 2026 06:01:51 +0000
Subject: [PATCH] Reject invalid WAL locations in pg_waldump's -s/-e options

pg_waldump parsed its --start/--end arguments with sscanf("%X/%08X"),
which accepts several forms of input that the backend's pg_lsn type
rejects, and in each case proceeds with a location the user did not
specify:

* A first component wider than 32 bits overflows its uint32 argument,
  which is undefined behavior per C99 7.19.6.2p10; glibc keeps the
  low-order 32 bits, so -s 123456789/0 runs with 23456789/0.  A
  component wider than 64 bits additionally saturates to ULONG_MAX
  before the truncation.
* sscanf() succeeds without consuming the whole string, so trailing
  characters are silently ignored: 0/123456789 is read as 0/12345678
  and 1/2/3 as 1/2.
* The %X conversion follows strtoul()'s rules, so leading whitespace,
  signs, and "0x" prefixes are accepted: -s -1/0 runs with FFFFFFFF/0.

The eventual "is not inside file" error then reports the mangled
location rather than the given one, which is actively misleading.

Replace the sscanf() calls with a helper that follows the backend's
pg_lsn_in_safe(): one to eight hex digits, a slash, one to eight hex
digits, and nothing else.  Inputs the server accepts as pg_lsn are
accepted unchanged; everything else now fails with the existing
"invalid WAL location" error.

Add regression tests for the previously-accepted forms.

Bug: #19598
Reported-by: Michael Malis <malis@pgrust.com>
Discussion: https://postgr.es/m/19598-aa67c8f4331611b4@postgresql.org
---
 src/bin/pg_waldump/pg_waldump.c   | 41 +++++++++++++++++++++++++------
 src/bin/pg_waldump/t/001_basic.pl | 16 ++++++++++++
 2 files changed, 50 insertions(+), 7 deletions(-)

diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index ffe6e8a6bc..9bb6031610 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -88,6 +88,38 @@ sigint_handler(SIGNAL_ARGS)
 }
 #endif
 
+#define MAXPG_LSNCOMPONENT	8
+
+/*
+ * Parse an LSN in the "%X/%X" format used for pg_lsn values, requiring
+ * one to eight hex digits in each component and nothing else, as the
+ * backend's pg_lsn_in_safe() does.  sscanf() is not strict enough here:
+ * its %X conversion has no field-width bound, so a component wider than
+ * 32 bits silently overflows the uint32 argument, and it also accepts
+ * leading whitespace, signs, "0x" prefixes, and trailing garbage.
+ *
+ * Returns true and sets *lsn on success.
+ */
+static bool
+parse_wal_location(const char *str, XLogRecPtr *lsn)
+{
+	int			len1,
+				len2;
+
+	len1 = strspn(str, "0123456789abcdefABCDEF");
+	if (len1 < 1 || len1 > MAXPG_LSNCOMPONENT || str[len1] != '/')
+		return false;
+
+	len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF");
+	if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0')
+		return false;
+
+	*lsn = ((uint64) strtoul(str, NULL, 16)) << 32 |
+		(uint32) strtoul(str + len1 + 1, NULL, 16);
+
+	return true;
+}
+
 static void
 print_rmgr_list(void)
 {
@@ -928,8 +960,6 @@ usage(void)
 int
 main(int argc, char **argv)
 {
-	uint32		xlogid;
-	uint32		xrecoff;
 	XLogReaderState *xlogreader_state;
 	XLogDumpPrivate private;
 	XLogDumpConfig config;
@@ -1047,13 +1077,12 @@ main(int argc, char **argv)
 				config.filter_by_extended = true;
 				break;
 			case 'e':
-				if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2)
+				if (!parse_wal_location(optarg, &private.endptr))
 				{
 					pg_log_error("invalid WAL location: \"%s\"",
 								 optarg);
 					goto bad_argument;
 				}
-				private.endptr = (uint64) xlogid << 32 | xrecoff;
 				break;
 			case 'f':
 				config.follow = true;
@@ -1145,14 +1174,12 @@ main(int argc, char **argv)
 				config.filter_by_extended = true;
 				break;
 			case 's':
-				if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2)
+				if (!parse_wal_location(optarg, &private.startptr))
 				{
 					pg_log_error("invalid WAL location: \"%s\"",
 								 optarg);
 					goto bad_argument;
 				}
-				else
-					private.startptr = (uint64) xlogid << 32 | xrecoff;
 				break;
 			case 't':
 
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 53b2f016b8..4fa507cfa2 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -54,6 +54,22 @@ command_fails_like(
 	[ 'pg_waldump', '--end' => 'bad' ],
 	qr/error: invalid WAL location/,
 	'invalid end LSN');
+command_fails_like(
+	[ 'pg_waldump', '--start' => '123456789/0' ],
+	qr/error: invalid WAL location/,
+	'start LSN with first component wider than 32 bits');
+command_fails_like(
+	[ 'pg_waldump', '--start' => '0/123456789' ],
+	qr/error: invalid WAL location/,
+	'start LSN with second component wider than 32 bits');
+command_fails_like(
+	[ 'pg_waldump', '--end' => '1/2/3' ],
+	qr/error: invalid WAL location/,
+	'end LSN with trailing garbage');
+command_fails_like(
+	[ 'pg_waldump', '--end' => '0x1/0' ],
+	qr/error: invalid WAL location/,
+	'end LSN with 0x prefix');
 
 # rmgr list: If you add one to the list, consider also adding a test
 # case exercising the new rmgr below.
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
@ 2026-08-05 06:03   ` Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Fujii Masao @ 2026-08-05 06:03 UTC (permalink / raw)
  To: Zexin Li <lizi.openmind@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com

On Tue, Aug 4, 2026 at 5:54 PM Zexin Li <lizi.openmind@gmail.com> wrote:
> The same pattern parses user-supplied LSNs in pg_recvlogical (-I/-E)
> and pg_receivewal (-E); pg_basebackup and pg_rewind only parse
> server-returned strings. I kept this patch to pg_waldump to match
> the report's scope, and can send a follow-up moving the helper next
> to option_parse_int() in fe_utils to cover the other two if that
> seems worthwhile.

I think it would be better to improve pg_recvlogical and pg_receivewal as well,
not just pg_waldump, by introducing a common LSN parsing helper in,
for example, src/common. That would let frontend tools share exactly
the same LSN syntax checks.

pg_basebackup, pg_verifybackup, pg_rewind, and pg_combinebackup also parse LSN
but are a bit different, since they mostly parse LSNs from server responses,
backup manifests, backup_label files, or timeline history files rather than
direct command-line input. So they're less likely to see arbitrary invalid LSNs
from users.

Still, if we introduce a common LSN parser, it seems worth considering
converting those existing sscanf("%X/%08X") call sites as well. That would
make malformed metadata fail earlier and avoid having several slightly
different LSN parsers in frontend code. This should be done as a separate
patch from the pg_waldump/pg_recvlogical/pg_receivewal improvement,
though.

BTW, at least for me this looks more like an improvement than a bug fix.
So I think it should target v20devel.

Regards,

-- 
Fujii Masao






^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
@ 2026-08-07 01:58     ` Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Zexin Li @ 2026-08-07 01:58 UTC (permalink / raw)
  To: masao.fujii@gmail.com; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com

On Wed, Aug 5, 2026, Fujii Masao wrote:
> I think it would be better to improve pg_recvlogical and pg_receivewal
> as well, not just pg_waldump, by introducing a common LSN parsing
> helper in, for example, src/common. That would let frontend tools
> share exactly the same LSN syntax checks.

Agreed -- v2 attached, done that way.

The helper is pg_parse_lsn() in the new src/common/pg_parse_lsn.c,
with the same rules as the backend's pg_lsn_in_safe(): one to eight
hex digits, a slash, one to eight hex digits, and nothing else.
pg_waldump's static helper from v1 moves there, and pg_recvlogical
(-I/-E) and pg_receivewal (-E) now go through it as well.

Three choices worth calling out:

* The helper only parses and returns bool; each tool keeps its own
existing error message ("invalid WAL location" in pg_waldump,
"could not parse start/end position" in the other two), so no error
text changes anywhere. This follows the existing split between
strtoint() in src/common and option_parse_int() in fe_utils.

* No endptr-style variant yet. The three command-line options all
want whole-string parsing. Among the call sites for the separate
patch you describe, pg_rewind's timeline.c and pg_combinebackup's
backup_label.c parse an LSN as a prefix of a longer line, so that
patch will want a second entry point taking an endptr; I did not
add an API with no in-tree caller here. One more data point for
it: parse_manifest.c contains one more sscanf of the same shape,
and it lives in src/common itself, out of reach of fe_utils code
-- which also argues for src/common as the helper's home.

* The backend's pg_lsn_in_safe() is left untouched for now.

The behavior change is confined to the three options: strings the
server rejects as pg_lsn (overlong components, trailing garbage,
leading whitespace, signs, 0x prefixes) now fail with each tool's
existing error instead of running with a mangled location. Strings
the server accepts parse exactly as before; I re-ran the v1 input
matrix against all three tools to check both directions.

The new TAP cases for pg_recvlogical and pg_receivewal fail without
the code change and pass with it; the pg_waldump cases from v1 are
kept. make check-world, a full meson build, and git am on current
master are all clean here.

> BTW, at least for me this looks more like an improvement than a bug fix.
> So I think it should target v20devel.

Makes sense. I'll register the patch in the September commitfest.

I'd appreciate any feedback .


Regards,
Zexin Li

On Wed, Aug 05, 2026 03:03 PM, Fujii Masao <masao.fujii@gmail.com> wrote:

> On Tue, Aug 4, 2026 at 5:54 PM Zexin Li <lizi.openmind@gmail.com> wrote:
> > The same pattern parses user-supplied LSNs in pg_recvlogical (-I/-E)
> > and pg_receivewal (-E); pg_basebackup and pg_rewind only parse
> > server-returned strings. I kept this patch to pg_waldump to match
> > the report's scope, and can send a follow-up moving the helper next
> > to option_parse_int() in fe_utils to cover the other two if that
> > seems worthwhile.
>
> I think it would be better to improve pg_recvlogical and pg_receivewal as
> well,
> not just pg_waldump, by introducing a common LSN parsing helper in,
> for example, src/common. That would let frontend tools share exactly
> the same LSN syntax checks.
>
> pg_basebackup, pg_verifybackup, pg_rewind, and pg_combinebackup also parse
> LSN
> but are a bit different, since they mostly parse LSNs from server
> responses,
> backup manifests, backup_label files, or timeline history files rather
than
> direct command-line input. So they're less likely to see arbitrary invalid
> LSNs
> from users.
>
> Still, if we introduce a common LSN parser, it seems worth considering
> converting those existing sscanf("%X/%08X") call sites as well. That would
> make malformed metadata fail earlier and avoid having several slightly
> different LSN parsers in frontend code. This should be done as a separate
> patch from the pg_waldump/pg_recvlogical/pg_receivewal improvement,
> though.
>
> BTW, at least for me this looks more like an improvement than a bug fix.
> So I think it should target v20devel.
>
> Regards,
>
> --
> Fujii Masao
>

On Wed, Aug 05, 2026 03:03 PM, Fujii Masao <masao.fujii@gmail.com> wrote:

> On Tue, Aug 4, 2026 at 5:54 PM Zexin Li <lizi.openmind@gmail.com> wrote:
> > The same pattern parses user-supplied LSNs in pg_recvlogical (-I/-E)
> > and pg_receivewal (-E); pg_basebackup and pg_rewind only parse
> > server-returned strings. I kept this patch to pg_waldump to match
> > the report's scope, and can send a follow-up moving the helper next
> > to option_parse_int() in fe_utils to cover the other two if that
> > seems worthwhile.
>
> I think it would be better to improve pg_recvlogical and pg_receivewal as
> well,
> not just pg_waldump, by introducing a common LSN parsing helper in,
> for example, src/common. That would let frontend tools share exactly
> the same LSN syntax checks.
>
> pg_basebackup, pg_verifybackup, pg_rewind, and pg_combinebackup also parse
> LSN
> but are a bit different, since they mostly parse LSNs from server
> responses,
> backup manifests, backup_label files, or timeline history files rather than
> direct command-line input. So they're less likely to see arbitrary invalid
> LSNs
> from users.
>
> Still, if we introduce a common LSN parser, it seems worth considering
> converting those existing sscanf("%X/%08X") call sites as well. That would
> make malformed metadata fail earlier and avoid having several slightly
> different LSN parsers in frontend code. This should be done as a separate
> patch from the pg_waldump/pg_recvlogical/pg_receivewal improvement,
> though.
>
> BTW, at least for me this looks more like an improvement than a bug fix.
> So I think it should target v20devel.
>
> Regards,
>
> --
> Fujii Masao
>

Attachments:

  [application/octet-stream] v2-0001-Introduce-pg_parse_lsn-to-validate-LSN-command-li.patch (12.5K, ../../CAAP6ZkSaM7BoyWhgcscAgxYAKvvqRn-m4NdYuvvAuuqUbwq=7A@mail.gmail.com/3-v2-0001-Introduce-pg_parse_lsn-to-validate-LSN-command-li.patch)
  download | inline diff:
From 75c0939fdc2ddd1dfce7385f44f23302802d1e63 Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Thu, 6 Aug 2026 02:51:40 +0000
Subject: [PATCH v2] Introduce pg_parse_lsn() to validate LSN command-line
 options

pg_waldump (--start/--end), pg_recvlogical (--startpos/--endpos), and
pg_receivewal (--endpos) parsed user-supplied WAL locations with
sscanf("%X/%08X"), which accepts several forms of input that the
backend's pg_lsn type rejects, and in each case proceeds with a
location the user did not specify:

* A first component wider than 32 bits overflows its uint32 argument,
  which is undefined behavior per C99 7.19.6.2p10; glibc keeps the
  low-order 32 bits, so --startpos 123456789/0 runs with 23456789/0.
  A component wider than 64 bits additionally saturates to ULONG_MAX
  before the truncation.
* sscanf() succeeds without consuming the whole string, so trailing
  characters are silently ignored: 0/123456789 is read as 0/12345678
  and 1/2/3 as 1/2.
* The %X conversion follows strtoul()'s rules, so leading whitespace,
  signs, and "0x" prefixes are accepted: --endpos -1/0 runs with
  FFFFFFFF/0.

Add pg_parse_lsn() to src/common, following the backend's
pg_lsn_in_safe(): one to eight hex digits, a slash, one to eight hex
digits, and nothing else.  Use it for the three options above.  Inputs
the server accepts as pg_lsn are accepted unchanged; everything else
now fails with each tool's existing "invalid WAL location" or "could
not parse start/end position" error, so the error texts are unchanged.

The other frontend parsers of the same shape (in pg_basebackup,
pg_rewind, pg_combinebackup, and parse_manifest.c) read
server-generated strings rather than command-line input and are left
alone, as is the backend's pg_lsn_in_safe() itself.

Add regression tests for the previously-accepted forms.

Bug: #19598
Reported-by: Michael Malis <malis@pgrust.com>
Suggested-by: Fujii Masao <masao.fujii@gmail.com>
Discussion: https://postgr.es/m/19598-aa67c8f4331611b4@postgresql.org
---
 src/bin/pg_basebackup/pg_receivewal.c         |  6 +-
 src/bin/pg_basebackup/pg_recvlogical.c        |  9 +--
 src/bin/pg_basebackup/t/020_pg_receivewal.pl  |  8 +++
 src/bin/pg_basebackup/t/030_pg_recvlogical.pl | 16 +++++
 src/bin/pg_waldump/pg_waldump.c               | 10 +---
 src/bin/pg_waldump/t/001_basic.pl             | 16 +++++
 src/common/Makefile                           |  1 +
 src/common/meson.build                        |  1 +
 src/common/pg_parse_lsn.c                     | 58 +++++++++++++++++++
 src/include/common/pg_parse_lsn.h             | 20 +++++++
 10 files changed, 128 insertions(+), 17 deletions(-)
 create mode 100644 src/common/pg_parse_lsn.c
 create mode 100644 src/include/common/pg_parse_lsn.h

diff --git a/src/bin/pg_basebackup/pg_receivewal.c b/src/bin/pg_basebackup/pg_receivewal.c
index 20506fc356..13bd318f67 100644
--- a/src/bin/pg_basebackup/pg_receivewal.c
+++ b/src/bin/pg_basebackup/pg_receivewal.c
@@ -30,6 +30,7 @@
 #include "access/xlog_internal.h"
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "fe_utils/option_utils.h"
 #include "getopt_long.h"
 #include "libpq-fe.h"
@@ -651,8 +652,6 @@ main(int argc, char **argv)
 	int			c;
 	int			option_index;
 	char	   *db_name;
-	uint32		hi,
-				lo;
 	pg_compress_specification compression_spec;
 	char	   *compression_detail = NULL;
 	char	   *compression_algorithm_str = "none";
@@ -689,9 +688,8 @@ main(int argc, char **argv)
 				basedir = pg_strdup(optarg);
 				break;
 			case 'E':
-				if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2)
+				if (!pg_parse_lsn(optarg, &endpos))
 					pg_fatal("could not parse end position \"%s\"", optarg);
-				endpos = ((uint64) hi) << 32 | lo;
 				break;
 			case 'h':
 				dbhost = pg_strdup(optarg);
diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c
index 40f6f65f75..feba45095e 100644
--- a/src/bin/pg_basebackup/pg_recvlogical.c
+++ b/src/bin/pg_basebackup/pg_recvlogical.c
@@ -20,6 +20,7 @@
 
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "fe_utils/option_utils.h"
 #include "getopt_long.h"
 #include "libpq-fe.h"
@@ -729,8 +730,6 @@ main(int argc, char **argv)
 	};
 	int			c;
 	int			option_index;
-	uint32		hi,
-				lo;
 	char	   *db_name;
 
 	pg_logging_init(argv[0]);
@@ -801,14 +800,12 @@ main(int argc, char **argv)
 				break;
 /* replication options */
 			case 'I':
-				if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2)
+				if (!pg_parse_lsn(optarg, &startpos))
 					pg_fatal("could not parse start position \"%s\"", optarg);
-				startpos = ((uint64) hi) << 32 | lo;
 				break;
 			case 'E':
-				if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2)
+				if (!pg_parse_lsn(optarg, &endpos))
 					pg_fatal("could not parse end position \"%s\"", optarg);
-				endpos = ((uint64) hi) << 32 | lo;
 				break;
 			case 'o':
 				{
diff --git a/src/bin/pg_basebackup/t/020_pg_receivewal.pl b/src/bin/pg_basebackup/t/020_pg_receivewal.pl
index 8da7cc86ba..9f3b0a12e9 100644
--- a/src/bin/pg_basebackup/t/020_pg_receivewal.pl
+++ b/src/bin/pg_basebackup/t/020_pg_receivewal.pl
@@ -24,6 +24,14 @@ mkdir($stream_dir);
 # Sanity checks for command line options.
 $primary->command_fails(['pg_receivewal'],
 	'pg_receivewal needs target directory specified');
+$primary->command_fails_like(
+	[ 'pg_receivewal', '--endpos' => '123456789/0' ],
+	qr/error: could not parse end position/,
+	'end position with first component wider than 32 bits');
+$primary->command_fails_like(
+	[ 'pg_receivewal', '--endpos' => '1/2/3' ],
+	qr/error: could not parse end position/,
+	'end position with trailing garbage');
 $primary->command_fails(
 	[
 		'pg_receivewal',
diff --git a/src/bin/pg_basebackup/t/030_pg_recvlogical.pl b/src/bin/pg_basebackup/t/030_pg_recvlogical.pl
index 5e3e36cc4f..7893fb94ce 100644
--- a/src/bin/pg_basebackup/t/030_pg_recvlogical.pl
+++ b/src/bin/pg_basebackup/t/030_pg_recvlogical.pl
@@ -43,6 +43,22 @@ $node->command_fails(
 		'--start',
 	],
 	'no destination file');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--startpos' => '123456789/0' ],
+	qr/error: could not parse start position/,
+	'start position with first component wider than 32 bits');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--startpos' => '0x1/0' ],
+	qr/error: could not parse start position/,
+	'start position with 0x prefix');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--endpos' => '0/123456789' ],
+	qr/error: could not parse end position/,
+	'end position with second component wider than 32 bits');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--endpos' => '1/2/3' ],
+	qr/error: could not parse end position/,
+	'end position with trailing garbage');
 
 $node->command_ok(
 	[
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index ffe6e8a6bc..6f0574a876 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -27,6 +27,7 @@
 #include "common/file_perm.h"
 #include "common/file_utils.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "common/relpath.h"
 #include "getopt_long.h"
 #include "pg_waldump.h"
@@ -928,8 +929,6 @@ usage(void)
 int
 main(int argc, char **argv)
 {
-	uint32		xlogid;
-	uint32		xrecoff;
 	XLogReaderState *xlogreader_state;
 	XLogDumpPrivate private;
 	XLogDumpConfig config;
@@ -1047,13 +1046,12 @@ main(int argc, char **argv)
 				config.filter_by_extended = true;
 				break;
 			case 'e':
-				if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2)
+				if (!pg_parse_lsn(optarg, &private.endptr))
 				{
 					pg_log_error("invalid WAL location: \"%s\"",
 								 optarg);
 					goto bad_argument;
 				}
-				private.endptr = (uint64) xlogid << 32 | xrecoff;
 				break;
 			case 'f':
 				config.follow = true;
@@ -1145,14 +1143,12 @@ main(int argc, char **argv)
 				config.filter_by_extended = true;
 				break;
 			case 's':
-				if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2)
+				if (!pg_parse_lsn(optarg, &private.startptr))
 				{
 					pg_log_error("invalid WAL location: \"%s\"",
 								 optarg);
 					goto bad_argument;
 				}
-				else
-					private.startptr = (uint64) xlogid << 32 | xrecoff;
 				break;
 			case 't':
 
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 53b2f016b8..4fa507cfa2 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -54,6 +54,22 @@ command_fails_like(
 	[ 'pg_waldump', '--end' => 'bad' ],
 	qr/error: invalid WAL location/,
 	'invalid end LSN');
+command_fails_like(
+	[ 'pg_waldump', '--start' => '123456789/0' ],
+	qr/error: invalid WAL location/,
+	'start LSN with first component wider than 32 bits');
+command_fails_like(
+	[ 'pg_waldump', '--start' => '0/123456789' ],
+	qr/error: invalid WAL location/,
+	'start LSN with second component wider than 32 bits');
+command_fails_like(
+	[ 'pg_waldump', '--end' => '1/2/3' ],
+	qr/error: invalid WAL location/,
+	'end LSN with trailing garbage');
+command_fails_like(
+	[ 'pg_waldump', '--end' => '0x1/0' ],
+	qr/error: invalid WAL location/,
+	'end LSN with 0x prefix');
 
 # rmgr list: If you add one to the list, consider also adding a test
 # case exercising the new rmgr below.
diff --git a/src/common/Makefile b/src/common/Makefile
index 1a2fbbe887..3404601b6b 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -70,6 +70,7 @@ OBJS_COMMON = \
 	percentrepl.o \
 	pg_get_line.o \
 	pg_lzcompress.o \
+	pg_parse_lsn.o \
 	pg_prng.o \
 	pgfnames.o \
 	psprintf.o \
diff --git a/src/common/meson.build b/src/common/meson.build
index 9bd55cda95..fc89a17334 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -24,6 +24,7 @@ common_sources = files(
   'percentrepl.c',
   'pg_get_line.c',
   'pg_lzcompress.c',
+  'pg_parse_lsn.c',
   'pg_prng.c',
   'pgfnames.c',
   'psprintf.c',
diff --git a/src/common/pg_parse_lsn.c b/src/common/pg_parse_lsn.c
new file mode 100644
index 0000000000..11f5b37309
--- /dev/null
+++ b/src/common/pg_parse_lsn.c
@@ -0,0 +1,58 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_parse_lsn.c
+ *	  Parse a WAL location (LSN) in its text form.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/common/pg_parse_lsn.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/pg_parse_lsn.h"
+
+/* same limit as in the backend's pg_lsn.c */
+#define MAXPG_LSNCOMPONENT	8
+
+/*
+ * pg_parse_lsn
+ *
+ * Parse a WAL location in the "%X/%X" text form used for pg_lsn values,
+ * requiring one to eight hexadecimal digits in each component and nothing
+ * else, exactly as the backend's pg_lsn_in_safe() does.  sscanf() is not
+ * strict enough for this purpose: its %X conversion has no field-width
+ * bound, so a component wider than 32 bits silently overflows a uint32
+ * argument, and it also accepts leading whitespace, signs, and "0x"
+ * prefixes, and does not insist on consuming the whole string.
+ *
+ * Returns true and sets *result on success; returns false on syntax
+ * error, leaving *result unchanged.
+ */
+bool
+pg_parse_lsn(const char *str, XLogRecPtr *result)
+{
+	int			len1,
+				len2;
+
+	len1 = strspn(str, "0123456789abcdefABCDEF");
+	if (len1 < 1 || len1 > MAXPG_LSNCOMPONENT || str[len1] != '/')
+		return false;
+
+	len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF");
+	if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0')
+		return false;
+
+	*result = ((uint64) strtoul(str, NULL, 16)) << 32 |
+		(uint32) strtoul(str + len1 + 1, NULL, 16);
+
+	return true;
+}
diff --git a/src/include/common/pg_parse_lsn.h b/src/include/common/pg_parse_lsn.h
new file mode 100644
index 0000000000..0a80785a4e
--- /dev/null
+++ b/src/include/common/pg_parse_lsn.h
@@ -0,0 +1,20 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_parse_lsn.h
+ *	  Parse a WAL location (LSN) in its text form.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/common/pg_parse_lsn.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PARSE_LSN_H
+#define PG_PARSE_LSN_H
+
+#include "access/xlogdefs.h"
+
+extern bool pg_parse_lsn(const char *str, XLogRecPtr *result);
+
+#endif							/* PG_PARSE_LSN_H */
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
@ 2026-08-10 05:47       ` Fujii Masao <masao.fujii@gmail.com>
  2026-08-10 08:23         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Ayush Tiwari <ayushtiwari.slg01@gmail.com>
  2026-08-11 00:40         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  0 siblings, 2 replies; 13+ messages in thread

From: Fujii Masao @ 2026-08-10 05:47 UTC (permalink / raw)
  To: Zexin Li <lizi.openmind@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com

On Fri, Aug 7, 2026 at 10:58 AM Zexin Li <lizi.openmind@gmail.com> wrote:
> The helper is pg_parse_lsn() in the new src/common/pg_parse_lsn.c,
> with the same rules as the backend's pg_lsn_in_safe(): one to eight
> hex digits, a slash, one to eight hex digits, and nothing else.
> pg_waldump's static helper from v1 moves there, and pg_recvlogical
> (-I/-E) and pg_receivewal (-E) now go through it as well.

> * The backend's pg_lsn_in_safe() is left untouched for now.

Thanks for updating the patch!

Attached is a revised version. It keeps the v2 approach of adding
pg_parse_lsn() in src/common and using it for user-supplied LSN
command-line options in pg_waldump, pg_recvlogical, and
pg_receivewal.

The main change from v2 is that pg_lsn_in_safe() now also uses
pg_parse_lsn(), leaving only the backend-specific soft-error handling
there. This avoids duplicating the LSN syntax checks.

Thought?

Regards,

-- 
Fujii Masao

Attachments:

  [application/octet-stream] v3-0001-Add-common-LSN-parser-for-user-supplied-locations.patch (13.0K, ../../CAHGQGwEACikthuAtwOM9kagkLfBqUNcTR6jiqrspSurjswyYow@mail.gmail.com/2-v3-0001-Add-common-LSN-parser-for-user-supplied-locations.patch)
  download | inline diff:
From 19c0eadc5678225d1227eaf5292e0688d06c60e7 Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Thu, 6 Aug 2026 02:51:40 +0000
Subject: [PATCH v3] Add common LSN parser for user-supplied locations

pg_waldump (--start/--end), pg_recvlogical (--startpos/--endpos), and
pg_receivewal (--endpos) parsed user-supplied WAL locations with
sscanf("%X/%08X").  That accepts several forms rejected by pg_lsn input
and can proceed with a different location than the user specified:
overlong components, trailing characters, leading whitespace, signs, and
0x prefixes.

Add pg_parse_lsn() to src/common and use it for those command-line
options.  The helper accepts the pg_lsn text syntax only: one to eight
hexadecimal digits, a slash, one to eight hexadecimal digits, and no
trailing characters.  Each frontend tool keeps its existing error
message.

Use the same helper from pg_lsn_in_safe(), leaving only backend-specific
soft error reporting there.  This keeps frontend command-line validation
and backend pg_lsn input tied to one parser.

Bug: #19598
Reported-by: Michael Malis <malis@pgrust.com>
Author: Zexin Li <lizi.openmind@gmail.com>
Reviewed-by: Fujii Masao <masao.fujii@gmail.com>
Discussion: https://postgr.es/m/19598-aa67c8f4331611b4@postgresql.org
---
 src/backend/utils/adt/pg_lsn.c                | 24 +-------
 src/bin/pg_basebackup/pg_receivewal.c         |  6 +-
 src/bin/pg_basebackup/pg_recvlogical.c        |  9 +--
 src/bin/pg_basebackup/t/020_pg_receivewal.pl  |  8 +++
 src/bin/pg_basebackup/t/030_pg_recvlogical.pl | 16 ++++++
 src/bin/pg_waldump/pg_waldump.c               | 10 +---
 src/bin/pg_waldump/t/001_basic.pl             | 16 ++++++
 src/common/Makefile                           |  1 +
 src/common/meson.build                        |  1 +
 src/common/pg_parse_lsn.c                     | 55 +++++++++++++++++++
 src/include/common/pg_parse_lsn.h             | 20 +++++++
 11 files changed, 128 insertions(+), 38 deletions(-)
 create mode 100644 src/common/pg_parse_lsn.c
 create mode 100644 src/include/common/pg_parse_lsn.h

diff --git a/src/backend/utils/adt/pg_lsn.c b/src/backend/utils/adt/pg_lsn.c
index e3480b051ce..7886e9b83e8 100644
--- a/src/backend/utils/adt/pg_lsn.c
+++ b/src/backend/utils/adt/pg_lsn.c
@@ -13,13 +13,13 @@
  */
 #include "postgres.h"
 
+#include "common/pg_parse_lsn.h"
 #include "libpq/pqformat.h"
 #include "utils/fmgrprotos.h"
 #include "utils/numeric.h"
 #include "utils/pg_lsn.h"
 
 #define MAXPG_LSNLEN			17
-#define MAXPG_LSNCOMPONENT	8
 
 /*----------------------------------------------------------
  * Formatting and conversion routines.
@@ -31,29 +31,11 @@
 XLogRecPtr
 pg_lsn_in_safe(const char *str, Node *escontext)
 {
-	int			len1,
-				len2;
-	uint32		id,
-				off;
 	XLogRecPtr	result;
 
-	/* Sanity check input format. */
-	len1 = strspn(str, "0123456789abcdefABCDEF");
-	if (len1 < 1 || len1 > MAXPG_LSNCOMPONENT || str[len1] != '/')
-		goto syntax_error;
+	if (pg_parse_lsn(str, &result))
+		return result;
 
-	len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF");
-	if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0')
-		goto syntax_error;
-
-	/* Decode result. */
-	id = (uint32) strtoul(str, NULL, 16);
-	off = (uint32) strtoul(str + len1 + 1, NULL, 16);
-	result = ((uint64) id << 32) | off;
-
-	return result;
-
-syntax_error:
 	ereturn(escontext, InvalidXLogRecPtr,
 			(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
 			 errmsg("invalid input syntax for type %s: \"%s\"",
diff --git a/src/bin/pg_basebackup/pg_receivewal.c b/src/bin/pg_basebackup/pg_receivewal.c
index 20506fc3560..13bd318f672 100644
--- a/src/bin/pg_basebackup/pg_receivewal.c
+++ b/src/bin/pg_basebackup/pg_receivewal.c
@@ -30,6 +30,7 @@
 #include "access/xlog_internal.h"
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "fe_utils/option_utils.h"
 #include "getopt_long.h"
 #include "libpq-fe.h"
@@ -651,8 +652,6 @@ main(int argc, char **argv)
 	int			c;
 	int			option_index;
 	char	   *db_name;
-	uint32		hi,
-				lo;
 	pg_compress_specification compression_spec;
 	char	   *compression_detail = NULL;
 	char	   *compression_algorithm_str = "none";
@@ -689,9 +688,8 @@ main(int argc, char **argv)
 				basedir = pg_strdup(optarg);
 				break;
 			case 'E':
-				if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2)
+				if (!pg_parse_lsn(optarg, &endpos))
 					pg_fatal("could not parse end position \"%s\"", optarg);
-				endpos = ((uint64) hi) << 32 | lo;
 				break;
 			case 'h':
 				dbhost = pg_strdup(optarg);
diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c
index 40f6f65f757..feba45095e4 100644
--- a/src/bin/pg_basebackup/pg_recvlogical.c
+++ b/src/bin/pg_basebackup/pg_recvlogical.c
@@ -20,6 +20,7 @@
 
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "fe_utils/option_utils.h"
 #include "getopt_long.h"
 #include "libpq-fe.h"
@@ -729,8 +730,6 @@ main(int argc, char **argv)
 	};
 	int			c;
 	int			option_index;
-	uint32		hi,
-				lo;
 	char	   *db_name;
 
 	pg_logging_init(argv[0]);
@@ -801,14 +800,12 @@ main(int argc, char **argv)
 				break;
 /* replication options */
 			case 'I':
-				if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2)
+				if (!pg_parse_lsn(optarg, &startpos))
 					pg_fatal("could not parse start position \"%s\"", optarg);
-				startpos = ((uint64) hi) << 32 | lo;
 				break;
 			case 'E':
-				if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2)
+				if (!pg_parse_lsn(optarg, &endpos))
 					pg_fatal("could not parse end position \"%s\"", optarg);
-				endpos = ((uint64) hi) << 32 | lo;
 				break;
 			case 'o':
 				{
diff --git a/src/bin/pg_basebackup/t/020_pg_receivewal.pl b/src/bin/pg_basebackup/t/020_pg_receivewal.pl
index 8da7cc86bae..9f3b0a12e9c 100644
--- a/src/bin/pg_basebackup/t/020_pg_receivewal.pl
+++ b/src/bin/pg_basebackup/t/020_pg_receivewal.pl
@@ -24,6 +24,14 @@ mkdir($stream_dir);
 # Sanity checks for command line options.
 $primary->command_fails(['pg_receivewal'],
 	'pg_receivewal needs target directory specified');
+$primary->command_fails_like(
+	[ 'pg_receivewal', '--endpos' => '123456789/0' ],
+	qr/error: could not parse end position/,
+	'end position with first component wider than 32 bits');
+$primary->command_fails_like(
+	[ 'pg_receivewal', '--endpos' => '1/2/3' ],
+	qr/error: could not parse end position/,
+	'end position with trailing garbage');
 $primary->command_fails(
 	[
 		'pg_receivewal',
diff --git a/src/bin/pg_basebackup/t/030_pg_recvlogical.pl b/src/bin/pg_basebackup/t/030_pg_recvlogical.pl
index 388c0a14f54..07ad07fef2a 100644
--- a/src/bin/pg_basebackup/t/030_pg_recvlogical.pl
+++ b/src/bin/pg_basebackup/t/030_pg_recvlogical.pl
@@ -43,6 +43,22 @@ $node->command_fails(
 		'--start',
 	],
 	'no destination file');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--startpos' => '123456789/0' ],
+	qr/error: could not parse start position/,
+	'start position with first component wider than 32 bits');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--startpos' => '0x1/0' ],
+	qr/error: could not parse start position/,
+	'start position with 0x prefix');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--endpos' => '0/123456789' ],
+	qr/error: could not parse end position/,
+	'end position with second component wider than 32 bits');
+$node->command_fails_like(
+	[ 'pg_recvlogical', '--endpos' => '1/2/3' ],
+	qr/error: could not parse end position/,
+	'end position with trailing garbage');
 
 $node->command_ok(
 	[
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index ffe6e8a6bca..6f0574a8764 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -27,6 +27,7 @@
 #include "common/file_perm.h"
 #include "common/file_utils.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "common/relpath.h"
 #include "getopt_long.h"
 #include "pg_waldump.h"
@@ -928,8 +929,6 @@ usage(void)
 int
 main(int argc, char **argv)
 {
-	uint32		xlogid;
-	uint32		xrecoff;
 	XLogReaderState *xlogreader_state;
 	XLogDumpPrivate private;
 	XLogDumpConfig config;
@@ -1047,13 +1046,12 @@ main(int argc, char **argv)
 				config.filter_by_extended = true;
 				break;
 			case 'e':
-				if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2)
+				if (!pg_parse_lsn(optarg, &private.endptr))
 				{
 					pg_log_error("invalid WAL location: \"%s\"",
 								 optarg);
 					goto bad_argument;
 				}
-				private.endptr = (uint64) xlogid << 32 | xrecoff;
 				break;
 			case 'f':
 				config.follow = true;
@@ -1145,14 +1143,12 @@ main(int argc, char **argv)
 				config.filter_by_extended = true;
 				break;
 			case 's':
-				if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2)
+				if (!pg_parse_lsn(optarg, &private.startptr))
 				{
 					pg_log_error("invalid WAL location: \"%s\"",
 								 optarg);
 					goto bad_argument;
 				}
-				else
-					private.startptr = (uint64) xlogid << 32 | xrecoff;
 				break;
 			case 't':
 
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 53b2f016b80..4fa507cfa20 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -54,6 +54,22 @@ command_fails_like(
 	[ 'pg_waldump', '--end' => 'bad' ],
 	qr/error: invalid WAL location/,
 	'invalid end LSN');
+command_fails_like(
+	[ 'pg_waldump', '--start' => '123456789/0' ],
+	qr/error: invalid WAL location/,
+	'start LSN with first component wider than 32 bits');
+command_fails_like(
+	[ 'pg_waldump', '--start' => '0/123456789' ],
+	qr/error: invalid WAL location/,
+	'start LSN with second component wider than 32 bits');
+command_fails_like(
+	[ 'pg_waldump', '--end' => '1/2/3' ],
+	qr/error: invalid WAL location/,
+	'end LSN with trailing garbage');
+command_fails_like(
+	[ 'pg_waldump', '--end' => '0x1/0' ],
+	qr/error: invalid WAL location/,
+	'end LSN with 0x prefix');
 
 # rmgr list: If you add one to the list, consider also adding a test
 # case exercising the new rmgr below.
diff --git a/src/common/Makefile b/src/common/Makefile
index 1a2fbbe887f..3404601b6bf 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -70,6 +70,7 @@ OBJS_COMMON = \
 	percentrepl.o \
 	pg_get_line.o \
 	pg_lzcompress.o \
+	pg_parse_lsn.o \
 	pg_prng.o \
 	pgfnames.o \
 	psprintf.o \
diff --git a/src/common/meson.build b/src/common/meson.build
index 9bd55cda95b..fc89a173346 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -24,6 +24,7 @@ common_sources = files(
   'percentrepl.c',
   'pg_get_line.c',
   'pg_lzcompress.c',
+  'pg_parse_lsn.c',
   'pg_prng.c',
   'pgfnames.c',
   'psprintf.c',
diff --git a/src/common/pg_parse_lsn.c b/src/common/pg_parse_lsn.c
new file mode 100644
index 00000000000..b6a83b57216
--- /dev/null
+++ b/src/common/pg_parse_lsn.c
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_parse_lsn.c
+ *	  Parse a WAL location (LSN) in its text form.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/common/pg_parse_lsn.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/pg_parse_lsn.h"
+
+#define MAXPG_LSNCOMPONENT	8
+
+/*
+ * pg_parse_lsn
+ *
+ * Parse a WAL location in the "%X/%X" text form used for pg_lsn values,
+ * requiring one to eight hexadecimal digits in each component and nothing
+ * else.  Unlike sscanf(), this rejects components longer than eight
+ * hexadecimal digits, leading whitespace, signs, "0x" prefixes, and
+ * trailing characters.
+ *
+ * Returns true and sets *result on success; returns false on syntax
+ * error, leaving *result unchanged.
+ */
+bool
+pg_parse_lsn(const char *str, XLogRecPtr *result)
+{
+	size_t		len1,
+				len2;
+
+	len1 = strspn(str, "0123456789abcdefABCDEF");
+	if (len1 < 1 || len1 > MAXPG_LSNCOMPONENT || str[len1] != '/')
+		return false;
+
+	len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF");
+	if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0')
+		return false;
+
+	*result = ((uint64) strtoul(str, NULL, 16)) << 32 |
+		(uint32) strtoul(str + len1 + 1, NULL, 16);
+
+	return true;
+}
diff --git a/src/include/common/pg_parse_lsn.h b/src/include/common/pg_parse_lsn.h
new file mode 100644
index 00000000000..0a80785a4e9
--- /dev/null
+++ b/src/include/common/pg_parse_lsn.h
@@ -0,0 +1,20 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_parse_lsn.h
+ *	  Parse a WAL location (LSN) in its text form.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/common/pg_parse_lsn.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PARSE_LSN_H
+#define PG_PARSE_LSN_H
+
+#include "access/xlogdefs.h"
+
+extern bool pg_parse_lsn(const char *str, XLogRecPtr *result);
+
+#endif							/* PG_PARSE_LSN_H */
-- 
2.55.0



^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
@ 2026-08-10 08:23         ` Ayush Tiwari <ayushtiwari.slg01@gmail.com>
  1 sibling, 0 replies; 13+ messages in thread

From: Ayush Tiwari @ 2026-08-10 08:23 UTC (permalink / raw)
  To: Fujii Masao <masao.fujii@gmail.com>; +Cc: Zexin Li <lizi.openmind@gmail.com>; pgsql-bugs@lists.postgresql.org; malis@pgrust.com

Hi,

On Mon, 10 Aug 2026 at 11:17, Fujii Masao <masao.fujii@gmail.com> wrote:

> On Fri, Aug 7, 2026 at 10:58 AM Zexin Li <lizi.openmind@gmail.com> wrote:
> > The helper is pg_parse_lsn() in the new src/common/pg_parse_lsn.c,
> > with the same rules as the backend's pg_lsn_in_safe(): one to eight
> > hex digits, a slash, one to eight hex digits, and nothing else.
> > pg_waldump's static helper from v1 moves there, and pg_recvlogical
> > (-I/-E) and pg_receivewal (-E) now go through it as well.
>
> > * The backend's pg_lsn_in_safe() is left untouched for now.
>
> Thanks for updating the patch!
>
> Attached is a revised version. It keeps the v2 approach of adding
> pg_parse_lsn() in src/common and using it for user-supplied LSN
> command-line options in pg_waldump, pg_recvlogical, and
> pg_receivewal.
>
> The main change from v2 is that pg_lsn_in_safe() now also uses
> pg_parse_lsn(), leaving only the backend-specific soft-error handling
> there. This avoids duplicating the LSN syntax checks.
>
> Thought?


I reviewed v3, and it looks good to me.
In parallel, I had reported and patched the same sscanf("%X/%08X")
issue in src/common/parse_manifest.c [0]

Once this is committed, I'll rebase that patch onto it and post a new
version on the earlier thread.

Regards,
Ayush

[0]
https://www.postgresql.org/message-id/CAJTYsWXieRHb-ooV2XfjHAtBsS7%2BP5La_-o8-Cqi15CNDhh9hQ%40mail.g...

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
@ 2026-08-11 00:40         ` Zexin Li <lizi.openmind@gmail.com>
  2026-08-12 08:09           ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  1 sibling, 1 reply; 13+ messages in thread

From: Zexin Li @ 2026-08-11 00:40 UTC (permalink / raw)
  To: masao.fujii@gmail.com; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com

On Sun, Aug 10, 2026, Fujii Masao wrote:
> The main change from v2 is that pg_lsn_in_safe() now also uses
> pg_parse_lsn(), leaving only the backend-specific soft-error handling
> there. This avoids duplicating the LSN syntax checks.

Thank you for the review. v3 looks good to me.

Regards,
Zexin Li

On Mon, Aug 10, 2026 02:47 PM, Fujii Masao <masao.fujii@gmail.com> wrote:

> On Fri, Aug 7, 2026 at 10:58 AM Zexin Li <lizi.openmind@gmail.com> wrote:
> > The helper is pg_parse_lsn() in the new src/common/pg_parse_lsn.c,
> > with the same rules as the backend's pg_lsn_in_safe(): one to eight
> > hex digits, a slash, one to eight hex digits, and nothing else.
> > pg_waldump's static helper from v1 moves there, and pg_recvlogical
> > (-I/-E) and pg_receivewal (-E) now go through it as well.
>
> > * The backend's pg_lsn_in_safe() is left untouched for now.
>
> Thanks for updating the patch!
>
> Attached is a revised version. It keeps the v2 approach of adding
> pg_parse_lsn() in src/common and using it for user-supplied LSN
> command-line options in pg_waldump, pg_recvlogical, and
> pg_receivewal.
>
> The main change from v2 is that pg_lsn_in_safe() now also uses
> pg_parse_lsn(), leaving only the backend-specific soft-error handling
> there. This avoids duplicating the LSN syntax checks.
>
> Thought?
>
> Regards,
>
> --
> Fujii Masao
>

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-11 00:40         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
@ 2026-08-12 08:09           ` Fujii Masao <masao.fujii@gmail.com>
  2026-08-14 06:33             ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Fujii Masao @ 2026-08-12 08:09 UTC (permalink / raw)
  To: Zexin Li <lizi.openmind@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com

On Tue, Aug 11, 2026 at 9:41 AM Zexin Li <lizi.openmind@gmail.com> wrote:
>
> On Sun, Aug 10, 2026, Fujii Masao wrote:
> > The main change from v2 is that pg_lsn_in_safe() now also uses
> > pg_parse_lsn(), leaving only the backend-specific soft-error handling
> > there. This avoids duplicating the LSN syntax checks.
>
> Thank you for the review. v3 looks good to me.

I've pushed the patch. Thanks!

Regards,

-- 
Fujii Masao






^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-11 00:40         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-12 08:09           ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
@ 2026-08-14 06:33             ` Zexin Li <lizi.openmind@gmail.com>
  2026-08-14 14:08               ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Zexin Li @ 2026-08-14 06:33 UTC (permalink / raw)
  To: masao.fujii@gmail.com; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com; ayushtiwari.slg01@gmail.com

On Wed, Aug 12, 2026, Fujii Masao wrote:
> I've pushed the patch. Thanks!

Thank you for committing this!

Attached is the separate patch you suggested for the remaining
frontend sscanf("%X/%08X") call sites: the LSNs that pg_basebackup
and pg_rewind read from server responses, pg_rewind reads from
timeline history files, and pg_combinebackup reads from backup_label
files. The backend's copies of these parsers are left untouched,
and parse_manifest.c is already being handled by Ayush's patch.

* Two of the converted call sites read a location out of a longer
line, so the patch adds pg_parse_lsn_prefix(), which reports the
first character after the location instead of requiring the string
to end there, and reimplements pg_parse_lsn() on top of it, keeping
a single implementation of the syntax rules. Each tool keeps its
existing error message.

* Malformed metadata now fails with each tool's existing error
instead of silently proceeding with a different location: components
wider than eight hex digits used to wrap around or be truncated, and
whitespace, signs, "0x" prefixes, and trailing characters used to be
consumed or ignored. Two error-path details change: pg_rewind's
history-file parser now requires the switchpoint to be followed by
whitespace or end of line, where trailing characters used to be
ignored, and an overlong second component in a backup_label LSN now
fails pg_combinebackup's "could not parse" check rather than its
"improper terminator" check.

* The new TAP tests (corrupted timeline history files for pg_rewind,
a corrupted backup_label for pg_combinebackup) fail without the code
change and pass with it. I could not find a way to exercise the
server-response call sites with malformed input in a TAP test, so
they are covered by the existing suites only; make check-world
passes here on current master.

I'd appreciate any feedback .

Regards,
Zexin Li


On Wed, Aug 12, 2026 05:09 PM, Fujii Masao <masao.fujii@gmail.com> wrote:

> On Tue, Aug 11, 2026 at 9:41 AM Zexin Li <lizi.openmind@gmail.com> wrote:
> >
> > On Sun, Aug 10, 2026, Fujii Masao wrote:
> > > The main change from v2 is that pg_lsn_in_safe() now also uses
> > > pg_parse_lsn(), leaving only the backend-specific soft-error handling
> > > there. This avoids duplicating the LSN syntax checks.
> >
> > Thank you for the review. v3 looks good to me.
>
> I've pushed the patch. Thanks!
>
> Regards,
>
> --
> Fujii Masao
>

Attachments:

  [application/octet-stream] v1-0001-Use-pg_parse_lsn-for-server-supplied-LSNs.patch (19.1K, ../../CAAP6ZkS_3OH3yhhAGK6vu+2V1C2Hv4K6SpRuZL415R-gxjdTSg@mail.gmail.com/3-v1-0001-Use-pg_parse_lsn-for-server-supplied-LSNs.patch)
  download | inline diff:
From 4d28b9e2e68ce0561111d770218c9d80e869039a Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Fri, 14 Aug 2026 00:37:28 +0000
Subject: [PATCH v1] Use pg_parse_lsn() for server-supplied LSNs

Commit d6bf0ab170 introduced pg_parse_lsn() to validate LSNs given on
the command line of pg_waldump, pg_recvlogical, and pg_receivewal.
The remaining frontend sscanf("%X/%08X") call sites parse LSNs that
arrive in server responses, timeline history files, and backup_label
files.  sscanf() accepts several forms that pg_lsn input rejects and
can silently continue with a different location than the input text:
a first component wider than eight hex digits wraps around, a wider
second component is truncated, and leading whitespace, signs, "0x"
prefixes, and trailing characters are consumed or ignored.

Convert those call sites as well.  Call sites that read a location
out of a longer line need to keep parsing where the location ended,
so add pg_parse_lsn_prefix(), which reports the first character after
the location instead of requiring the string to end there, and
reimplement pg_parse_lsn() on top of it.  Each tool keeps its
existing error message.

Malformed metadata now fails with each tool's existing error instead
of silently proceeding with a different location.  Two error-path
details change: pg_rewind's history-file parser now requires the
switchpoint to be followed by whitespace or end of line, where
trailing characters used to be ignored, and an overlong second
component in a backup_label LSN now fails pg_combinebackup's
"could not parse" check rather than its "improper terminator" check.

Author: Zexin Li <lizi.openmind@gmail.com>
---
 src/bin/pg_basebackup/pg_basebackup.c       |  16 +---
 src/bin/pg_basebackup/receivelog.c          |   8 +-
 src/bin/pg_basebackup/streamutil.c          |  24 ++---
 src/bin/pg_combinebackup/backup_label.c     |  12 +--
 src/bin/pg_combinebackup/t/005_integrity.pl |  26 +++++
 src/bin/pg_rewind/libpq_source.c            |   7 +-
 src/bin/pg_rewind/meson.build               |   1 +
 src/bin/pg_rewind/t/012_timeline_history.pl | 100 ++++++++++++++++++++
 src/bin/pg_rewind/timeline.c                |  22 +++--
 src/common/pg_parse_lsn.c                   |  53 +++++++++--
 src/include/common/pg_parse_lsn.h           |   2 +
 11 files changed, 206 insertions(+), 65 deletions(-)
 create mode 100644 src/bin/pg_rewind/t/012_timeline_history.pl

diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 12fc752bff5..c3b87a19e76 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -32,6 +32,7 @@
 #include "common/file_perm.h"
 #include "common/file_utils.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "fe_utils/option_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "getopt_long.h"
@@ -482,17 +483,14 @@ reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 		{
 			ssize_t		nread;
 			char		xlogend[64] = {0};
-			uint32		hi,
-						lo;
 
 			nread = read(bgpipe[0], xlogend, sizeof(xlogend) - 1);
 			if (nread < 0)
 				pg_fatal("could not read from ready pipe: %m");
 
-			if (sscanf(xlogend, "%X/%08X", &hi, &lo) != 2)
+			if (!pg_parse_lsn(xlogend, &xlogendptr))
 				pg_fatal("could not parse write-ahead log location \"%s\"",
 						 xlogend);
-			xlogendptr = ((uint64) hi) << 32 | lo;
 			has_xlogendptr = 1;
 
 			/*
@@ -620,8 +618,6 @@ StartLogStreamer(char *startpos, uint32 timeline, char *sysidentifier,
 				 int wal_compress_level)
 {
 	logstreamer_param *param;
-	uint32		hi,
-				lo;
 	char		statusdir[MAXPGPATH];
 
 	param = pg_malloc0_object(logstreamer_param);
@@ -631,10 +627,9 @@ StartLogStreamer(char *startpos, uint32 timeline, char *sysidentifier,
 	param->wal_compress_level = wal_compress_level;
 
 	/* Convert the starting position */
-	if (sscanf(startpos, "%X/%08X", &hi, &lo) != 2)
+	if (!pg_parse_lsn(startpos, &param->startptr))
 		pg_fatal("could not parse write-ahead log location \"%s\"",
 				 startpos);
-	param->startptr = ((uint64) hi) << 32 | lo;
 	/* Round off to even segment position */
 	param->startptr -= XLogSegmentOffset(param->startptr, WalSegSz);
 
@@ -2216,8 +2211,6 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 		 * casting to a different size on WIN64.
 		 */
 		intptr_t	bgchild_handle = bgchild;
-		uint32		hi,
-					lo;
 #endif
 
 		if (verbose)
@@ -2243,10 +2236,9 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 		 * value directly in the variable, and then set the flag that says
 		 * it's there.
 		 */
-		if (sscanf(xlogend, "%X/%08X", &hi, &lo) != 2)
+		if (!pg_parse_lsn(xlogend, &xlogendptr))
 			pg_fatal("could not parse write-ahead log location \"%s\"",
 					 xlogend);
-		xlogendptr = ((uint64) hi) << 32 | lo;
 		InterlockedIncrement(&has_xlogendptr);
 
 		/* First wait for the thread to exit */
diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c
index faa60711b1b..77a2b4458b3 100644
--- a/src/bin/pg_basebackup/receivelog.c
+++ b/src/bin/pg_basebackup/receivelog.c
@@ -20,6 +20,7 @@
 
 #include "access/xlog_internal.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "libpq-fe.h"
 #include "libpq/protocol.h"
 #include "receivelog.h"
@@ -704,9 +705,6 @@ error:
 static bool
 ReadEndOfStreamingResult(PGresult *res, XLogRecPtr *startpos, uint32 *timeline)
 {
-	uint32		startpos_xlogid,
-				startpos_xrecoff;
-
 	/*----------
 	 * The result set consists of one row and two columns, e.g:
 	 *
@@ -727,14 +725,12 @@ ReadEndOfStreamingResult(PGresult *res, XLogRecPtr *startpos, uint32 *timeline)
 	}
 
 	*timeline = atoi(PQgetvalue(res, 0, 0));
-	if (sscanf(PQgetvalue(res, 0, 1), "%X/%08X", &startpos_xlogid,
-			   &startpos_xrecoff) != 2)
+	if (!pg_parse_lsn(PQgetvalue(res, 0, 1), startpos))
 	{
 		pg_log_error("could not parse next timeline's starting point \"%s\"",
 					 PQgetvalue(res, 0, 1));
 		return false;
 	}
-	*startpos = ((uint64) startpos_xlogid << 32) | startpos_xrecoff;
 
 	return true;
 }
diff --git a/src/bin/pg_basebackup/streamutil.c b/src/bin/pg_basebackup/streamutil.c
index 8fcd690f155..e3c923cd317 100644
--- a/src/bin/pg_basebackup/streamutil.c
+++ b/src/bin/pg_basebackup/streamutil.c
@@ -21,6 +21,7 @@
 #include "common/connect.h"
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "common/string.h"
 #include "datatype/timestamp.h"
 #include "port/pg_bswap.h"
@@ -410,8 +411,6 @@ RunIdentifySystem(PGconn *conn, char **sysid, TimeLineID *starttli,
 				  XLogRecPtr *startpos, char **db_name)
 {
 	PGresult   *res;
-	uint32		hi,
-				lo;
 
 	/* Check connection existence */
 	Assert(conn != NULL);
@@ -445,7 +444,7 @@ RunIdentifySystem(PGconn *conn, char **sysid, TimeLineID *starttli,
 	/* Get LSN start position if necessary */
 	if (startpos != NULL)
 	{
-		if (sscanf(PQgetvalue(res, 0, 2), "%X/%08X", &hi, &lo) != 2)
+		if (!pg_parse_lsn(PQgetvalue(res, 0, 2), startpos))
 		{
 			pg_log_error("could not parse write-ahead log location \"%s\"",
 						 PQgetvalue(res, 0, 2));
@@ -453,7 +452,6 @@ RunIdentifySystem(PGconn *conn, char **sysid, TimeLineID *starttli,
 			PQclear(res);
 			return false;
 		}
-		*startpos = ((uint64) hi) << 32 | lo;
 	}
 
 	/* Get database name, only available in 9.4 and newer versions */
@@ -547,19 +545,13 @@ GetSlotInformation(PGconn *conn, const char *slot_name,
 	}
 
 	/* restart LSN */
-	if (!PQgetisnull(res, 0, 1))
+	if (!PQgetisnull(res, 0, 1) &&
+		!pg_parse_lsn(PQgetvalue(res, 0, 1), &lsn_loc))
 	{
-		uint32		hi,
-					lo;
-
-		if (sscanf(PQgetvalue(res, 0, 1), "%X/%08X", &hi, &lo) != 2)
-		{
-			pg_log_error("could not parse restart_lsn \"%s\" for replication slot \"%s\"",
-						 PQgetvalue(res, 0, 1), slot_name);
-			PQclear(res);
-			return false;
-		}
-		lsn_loc = ((uint64) hi) << 32 | lo;
+		pg_log_error("could not parse restart_lsn \"%s\" for replication slot \"%s\"",
+					 PQgetvalue(res, 0, 1), slot_name);
+		PQclear(res);
+		return false;
 	}
 
 	/* current TLI */
diff --git a/src/bin/pg_combinebackup/backup_label.c b/src/bin/pg_combinebackup/backup_label.c
index b757e772b92..ca9ed2e7b4a 100644
--- a/src/bin/pg_combinebackup/backup_label.c
+++ b/src/bin/pg_combinebackup/backup_label.c
@@ -17,6 +17,7 @@
 #include "backup_label.h"
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "write_manifest.h"
 
 static int	get_eol_offset(StringInfo buf);
@@ -242,20 +243,15 @@ static bool
 parse_lsn(char *s, char *e, XLogRecPtr *lsn, char **c)
 {
 	char		save = *e;
-	int			nchars;
 	bool		success;
-	unsigned	hi;
-	unsigned	lo;
+	const char *end;
 
 	*e = '\0';
-	success = (sscanf(s, "%X/%08X%n", &hi, &lo, &nchars) == 2);
+	success = pg_parse_lsn_prefix(s, lsn, &end);
 	*e = save;
 
 	if (success)
-	{
-		*lsn = ((XLogRecPtr) hi) << 32 | (XLogRecPtr) lo;
-		*c = s + nchars;
-	}
+		*c = unconstify(char *, end);
 
 	return success;
 }
diff --git a/src/bin/pg_combinebackup/t/005_integrity.pl b/src/bin/pg_combinebackup/t/005_integrity.pl
index 9e1af2a7a7f..546d70c6d11 100644
--- a/src/bin/pg_combinebackup/t/005_integrity.pl
+++ b/src/bin/pg_combinebackup/t/005_integrity.pl
@@ -100,6 +100,7 @@ $node2->command_ok(
 
 # Result directory.
 my $resultpath = $node1->backup_dir . '/result';
+my $badlsnpath = $node1->backup_dir . '/badlsn';
 
 # Can't combine 2 full backups.
 $node1->command_fails_like(
@@ -208,5 +209,30 @@ $node1->command_fails_like(
 	qr/starts at LSN.*but expected/,
 	"can't combine synthetic backup with included incremental");
 
+# A start location whose first component is wider than 32 bits must be
+# rejected; it used to wrap around silently.
+my $labelpath = $backup2path . '/backup_label';
+my $origlabel = slurp_file($labelpath);
+my $badlabel = $origlabel;
+$badlabel =~
+  s{^START WAL LOCATION: [0-9A-F]+/}{START WAL LOCATION: 123456789/}m;
+open my $lfh, '>', $labelpath or die "$labelpath: $!";
+print $lfh $badlabel;
+close $lfh;
+$node1->command_fails_like(
+	[
+		'pg_combinebackup', $backup1path, $backup2path,
+		'--output' => $badlsnpath,
+		$mode,
+	],
+	qr/could not parse START WAL LOCATION/,
+	"can't combine a backup whose start location is out of range");
+rmtree($badlsnpath) if -d $badlsnpath;
+
+# Restore the original file.
+open $lfh, '>', $labelpath or die "$labelpath: $!";
+print $lfh $origlabel;
+close $lfh;
+
 # OK, that's all.
 done_testing();
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 216755b3ddb..abfbd312580 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -11,6 +11,7 @@
 
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
+#include "common/pg_parse_lsn.h"
 #include "file_ops.h"
 #include "filemap.h"
 #include "lib/stringinfo.h"
@@ -209,17 +210,13 @@ libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
 	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
-	uint32		hi;
-	uint32		lo;
 	char	   *val;
 
 	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
-	if (sscanf(val, "%X/%08X", &hi, &lo) != 2)
+	if (!pg_parse_lsn(val, &result))
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
 
-	result = ((uint64) hi) << 32 | lo;
-
 	pg_free(val);
 
 	return result;
diff --git a/src/bin/pg_rewind/meson.build b/src/bin/pg_rewind/meson.build
index 52a6ab0a515..a328ab3e6b8 100644
--- a/src/bin/pg_rewind/meson.build
+++ b/src/bin/pg_rewind/meson.build
@@ -45,6 +45,7 @@ tests += {
       't/009_growing_files.pl',
       't/010_keep_recycled_wals.pl',
       't/011_wal_copy.pl',
+      't/012_timeline_history.pl',
     ],
   },
 }
diff --git a/src/bin/pg_rewind/t/012_timeline_history.pl b/src/bin/pg_rewind/t/012_timeline_history.pl
new file mode 100644
index 00000000000..f967dcfca4c
--- /dev/null
+++ b/src/bin/pg_rewind/t/012_timeline_history.pl
@@ -0,0 +1,100 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+#
+# Test that a malformed switchpoint in a timeline history file is rejected
+# rather than silently misinterpreted.
+#
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use RewindTest;
+
+RewindTest::setup_cluster('history');
+RewindTest::start_primary();
+RewindTest::create_standby('history');
+RewindTest::promote_standby();
+
+my $primary_pgdata = $node_primary->data_dir;
+my $standby_pgdata = $node_standby->data_dir;
+
+$node_standby->stop;
+$node_primary->stop;
+
+# The history file of the promoted standby is read by pg_rewind to find the
+# point where the two servers diverged.
+my $histfile = "$standby_pgdata/pg_wal/00000002.history";
+my $orig = slurp_file($histfile);
+
+sub write_history
+{
+	my $contents = shift;
+
+	open my $fh, '>', $histfile
+	  or BAIL_OUT("could not write \"$histfile\": $!");
+	print $fh $contents;
+	close $fh;
+	return;
+}
+
+# A switchpoint whose first component is wider than 32 bits must not be
+# accepted; the value used to wrap around silently.
+write_history("1\t123456789/0\tno recovery target specified\n");
+command_fails_like(
+	[
+		'pg_rewind',
+		'--dry-run',
+		'--source-pgdata' => $standby_pgdata,
+		'--target-pgdata' => $primary_pgdata,
+		'--no-sync',
+	],
+	qr/error: syntax error in history file/,
+	'switchpoint with first component wider than 32 bits');
+
+# Likewise for a second component that is too wide, which used to be
+# truncated to its first eight digits.
+write_history("1\t0/123456789\tno recovery target specified\n");
+command_fails_like(
+	[
+		'pg_rewind',
+		'--dry-run',
+		'--source-pgdata' => $standby_pgdata,
+		'--target-pgdata' => $primary_pgdata,
+		'--no-sync',
+	],
+	qr/error: syntax error in history file/,
+	'switchpoint with second component wider than 32 bits');
+
+# A switchpoint written with a "0x" prefix must also be rejected; sscanf's
+# %X used to consume the prefix silently.
+write_history("1\t0/0x3000000\tno recovery target specified\n");
+command_fails_like(
+	[
+		'pg_rewind',
+		'--dry-run',
+		'--source-pgdata' => $standby_pgdata,
+		'--target-pgdata' => $primary_pgdata,
+		'--no-sync',
+	],
+	qr/error: syntax error in history file/,
+	'switchpoint with 0x prefix');
+
+# The unmodified file is still accepted, so the failures above are caused by
+# the switchpoint and nothing else.
+write_history($orig);
+command_ok(
+	[
+		'pg_rewind',
+		'--dry-run',
+		'--source-pgdata' => $standby_pgdata,
+		'--target-pgdata' => $primary_pgdata,
+		'--no-sync',
+	],
+	'unmodified history file is accepted');
+
+done_testing();
diff --git a/src/bin/pg_rewind/timeline.c b/src/bin/pg_rewind/timeline.c
index dda06eaa0bc..957f1c88fcf 100644
--- a/src/bin/pg_rewind/timeline.c
+++ b/src/bin/pg_rewind/timeline.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"
 
 #include "access/timeline.h"
+#include "common/pg_parse_lsn.h"
 #include "pg_rewind.h"
 
 /*
@@ -45,9 +46,9 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries)
 	{
 		char	   *ptr;
 		TimeLineID	tli;
-		uint32		switchpoint_hi;
-		uint32		switchpoint_lo;
-		int			nfields;
+		XLogRecPtr	switchpoint;
+		const char *lsnend;
+		int			nchars;
 
 		fline = bufptr;
 		while (*bufptr && *bufptr != '\n')
@@ -66,16 +67,21 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries)
 		if (*ptr == '\0' || *ptr == '#')
 			continue;
 
-		nfields = sscanf(fline, "%u\t%X/%08X", &tli, &switchpoint_hi, &switchpoint_lo);
-
-		if (nfields < 1)
+		if (sscanf(fline, "%u%n", &tli, &nchars) != 1)
 		{
 			/* expect a numeric timeline ID as first field of line */
 			pg_log_error("syntax error in history file: %s", fline);
 			pg_log_error_detail("Expected a numeric timeline ID.");
 			exit(1);
 		}
-		if (nfields != 3)
+
+		/* the switchpoint location follows, separated by whitespace */
+		ptr = fline + nchars;
+		while (isspace((unsigned char) *ptr))
+			ptr++;
+
+		if (!pg_parse_lsn_prefix(ptr, &switchpoint, &lsnend) ||
+			(*lsnend != '\0' && !isspace((unsigned char) *lsnend)))
 		{
 			pg_log_error("syntax error in history file: %s", fline);
 			pg_log_error_detail("Expected a write-ahead log switchpoint location.");
@@ -96,7 +102,7 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries)
 		entry = &entries[nlines - 1];
 		entry->tli = tli;
 		entry->begin = prevend;
-		entry->end = ((uint64) (switchpoint_hi)) << 32 | (uint64) switchpoint_lo;
+		entry->end = switchpoint;
 		prevend = entry->end;
 
 		/* we ignore the remainder of each line */
diff --git a/src/common/pg_parse_lsn.c b/src/common/pg_parse_lsn.c
index b6a83b57216..1979fa02387 100644
--- a/src/common/pg_parse_lsn.c
+++ b/src/common/pg_parse_lsn.c
@@ -23,20 +23,21 @@
 #define MAXPG_LSNCOMPONENT	8
 
 /*
- * pg_parse_lsn
+ * pg_parse_lsn_prefix
  *
  * Parse a WAL location in the "%X/%X" text form used for pg_lsn values,
- * requiring one to eight hexadecimal digits in each component and nothing
- * else.  Unlike sscanf(), this rejects components longer than eight
- * hexadecimal digits, leading whitespace, signs, "0x" prefixes, and
- * trailing characters.
+ * requiring one to eight hexadecimal digits in each component.  Unlike
+ * sscanf(), this rejects components longer than eight hexadecimal digits,
+ * leading whitespace, signs, and "0x" prefixes.
  *
- * Returns true and sets *result on success; returns false on syntax
- * error, leaving *result unchanged.
+ * Returns true on success, setting *result to the location and *endptr to
+ * the first character after it.  Returns false on syntax error, leaving
+ * both unchanged.
  */
 bool
-pg_parse_lsn(const char *str, XLogRecPtr *result)
+pg_parse_lsn_prefix(const char *str, XLogRecPtr *result, const char **endptr)
 {
+	char		buf[MAXPG_LSNCOMPONENT + 1];
 	size_t		len1,
 				len2;
 
@@ -45,11 +46,43 @@ pg_parse_lsn(const char *str, XLogRecPtr *result)
 		return false;
 
 	len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF");
-	if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0')
+	if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT)
 		return false;
 
+	/*
+	 * Decode the second component from a bounded copy: on the original
+	 * string, strtoul() would accept a "0x" prefix that the check above did
+	 * not.  (The first component ends at the '/'.)
+	 */
+	memcpy(buf, str + len1 + 1, len2);
+	buf[len2] = '\0';
+
 	*result = ((uint64) strtoul(str, NULL, 16)) << 32 |
-		(uint32) strtoul(str + len1 + 1, NULL, 16);
+		(uint32) strtoul(buf, NULL, 16);
+	*endptr = str + len1 + 1 + len2;
+
+	return true;
+}
+
+/*
+ * pg_parse_lsn
+ *
+ * Same as pg_parse_lsn_prefix(), but the whole string has to be a WAL
+ * location.
+ *
+ * Returns true and sets *result on success; returns false on syntax
+ * error, leaving *result unchanged.
+ */
+bool
+pg_parse_lsn(const char *str, XLogRecPtr *result)
+{
+	XLogRecPtr	lsn;
+	const char *end;
+
+	if (!pg_parse_lsn_prefix(str, &lsn, &end) || *end != '\0')
+		return false;
+
+	*result = lsn;
 
 	return true;
 }
diff --git a/src/include/common/pg_parse_lsn.h b/src/include/common/pg_parse_lsn.h
index 0a80785a4e9..740ce6e1226 100644
--- a/src/include/common/pg_parse_lsn.h
+++ b/src/include/common/pg_parse_lsn.h
@@ -16,5 +16,7 @@
 #include "access/xlogdefs.h"
 
 extern bool pg_parse_lsn(const char *str, XLogRecPtr *result);
+extern bool pg_parse_lsn_prefix(const char *str, XLogRecPtr *result,
+								const char **endptr);
 
 #endif							/* PG_PARSE_LSN_H */
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-11 00:40         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-12 08:09           ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-14 06:33             ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
@ 2026-08-14 14:08               ` Fujii Masao <masao.fujii@gmail.com>
  2026-08-17 03:23                 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Fujii Masao @ 2026-08-14 14:08 UTC (permalink / raw)
  To: Zexin Li <lizi.openmind@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com; ayushtiwari.slg01@gmail.com

On Fri, Aug 14, 2026 at 3:33 PM Zexin Li <lizi.openmind@gmail.com> wrote:
>
> On Wed, Aug 12, 2026, Fujii Masao wrote:
> > I've pushed the patch. Thanks!
>
> Thank you for committing this!
>
> Attached is the separate patch you suggested for the remaining
> frontend sscanf("%X/%08X") call sites: the LSNs that pg_basebackup
> and pg_rewind read from server responses, pg_rewind reads from
> timeline history files, and pg_combinebackup reads from backup_label
> files.

Thanks for the patch!


> The backend's copies of these parsers are left untouched,

Okay, the backend sscanf()-based LSN parsers can be handled separately
in a later patch.


> and parse_manifest.c is already being handled by Ayush's patch.

Okay.


> * Two of the converted call sites read a location out of a longer
> line, so the patch adds pg_parse_lsn_prefix(), which reports the
> first character after the location instead of requiring the string
> to end there, and reimplements pg_parse_lsn() on top of it, keeping
> a single implementation of the syntax rules. Each tool keeps its
> existing error message.

I wonder if we really need pg_parse_lsn_prefix() for this. Instead,
how about isolating the LSN token by temporarily NUL-terminating it,
then passing it to pg_parse_lsn(), as follows?

parse_lsn(char *s, char *e, XLogRecPtr *lsn, char **c)
 {
  char save = *e;
- int nchars;
+ char   *token_end;
+ char save_token_end;
  bool success;
- unsigned hi;
- unsigned lo;

  *e = '\0';
- success = (sscanf(s, "%X/%08X%n", &hi, &lo, &nchars) == 2);
- *e = save;
+ token_end = s + strcspn(s, " \t\n\r\f\v");

+ save_token_end = *token_end;
+ *token_end = '\0';
+ success = pg_parse_lsn(s, lsn);
+ *token_end = save_token_end;
+ *e = save;
  if (success)
- {
- *lsn = ((XLogRecPtr) hi) << 32 | (XLogRecPtr) lo;
- *c = s + nchars;
- }
+ *c = token_end;

As for pg_parse_lsn_prefix(), it seems to accept 0/0x3000000 as 0/0,
for example. So, *if* we use pg_parse_lsn_prefix(), we'd also need to
verify that the next character is expected?


> * The new TAP tests (corrupted timeline history files for pg_rewind,
> a corrupted backup_label for pg_combinebackup) fail without the code
> change and pass with it. I could not find a way to exercise the
> server-response call sites with malformed input in a TAP test, so
> they are covered by the existing suites only; make check-world
> passes here on current master.

I'm not sure if it's really worth adding these TAP tests.

The tests cover a few manually corrupted timeline history / backup_label
cases, but there are many possible ways these files could be malformed.
I don't think testing only these specific corruption patterns adds much
value, especially since these files are normally generated by
PostgreSQL itself.

The important part of this change is to stop using sscanf() and route
the parsing through the common LSN parser. I think that's sufficient
here, so I'd prefer to keep the patch small and omit the new tests.
Thought?

Regards,

-- 
Fujii Masao






^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-11 00:40         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-12 08:09           ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-14 06:33             ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-14 14:08               ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
@ 2026-08-17 03:23                 ` Zexin Li <lizi.openmind@gmail.com>
  2026-08-19 17:57                   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Zexin Li @ 2026-08-17 03:23 UTC (permalink / raw)
  To: masao.fujii@gmail.com; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com; ayushtiwari.slg01@gmail.com

On Fri, Aug 14, 2026, Fujii Masao wrote:
> I wonder if we really need pg_parse_lsn_prefix() for this. Instead,
> how about isolating the LSN token by temporarily NUL-terminating it,
> then passing it to pg_parse_lsn(), as follows?

You're right, that is better. v2 attached does it that way in
backup_label.c, and applies the same approach in pg_rewind's
timeline.c, so the patch no longer touches src/common at all.

> As for pg_parse_lsn_prefix(), it seems to accept 0/0x3000000 as 0/0,
> for example. So, *if* we use pg_parse_lsn_prefix(), we'd also need to
> verify that the next character is expected?

Right about the helper on its own. In v1 both call sites checked the
next character -- in backup_label.c, the terminator check that was
already there -- so neither tool accepted that input. In v2 the
whole token goes to pg_parse_lsn(), so there is no such check left
for a caller to get wrong.

> I'm not sure if it's really worth adding these TAP tests.

Agreed.  Dropped in v2.

make check-world passes here.
I'd appreciate any feedback.

Regards,
Zexin Li

Attachments:

  [application/octet-stream] v2-0001-Use-pg_parse_lsn-for-server-supplied-LSNs.patch (11.3K, ../../CAAP6ZkT83DTh9qMcPddTAVii0CEYH9Q+xz8wj6NEAU7fyU+YPA@mail.gmail.com/3-v2-0001-Use-pg_parse_lsn-for-server-supplied-LSNs.patch)
  download | inline diff:
From fab6ab010726333bb17d7c60c7a91cd0e6b33577 Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Fri, 14 Aug 2026 00:37:28 +0000
Subject: [PATCH v2] Use pg_parse_lsn() for server-supplied LSNs

Commit d6bf0ab170 introduced pg_parse_lsn() to validate LSNs given on
the command line of pg_waldump, pg_recvlogical, and pg_receivewal.
The remaining sscanf("%X/%08X") call sites under src/bin parse LSNs
that arrive in server responses, timeline history files, and
backup_label files.  sscanf() accepts several forms that pg_lsn input
rejects and can silently continue with a different location than the
input text: a first component wider than eight hex digits wraps
around, a wider second component is truncated, and leading
whitespace, signs, "0x" prefixes, and trailing characters are
consumed or ignored.

Convert those call sites as well.  The two call sites that read a
location out of a longer line isolate it by temporarily terminating
the string at the next whitespace character, so that they can use
pg_parse_lsn() like the others.  Each tool keeps its existing error
message.

Malformed metadata now fails with each tool's existing error instead
of silently proceeding with a different location.  Two error paths
shift: pg_rewind's history-file parser now rejects trailing
characters attached to a switchpoint, which used to be ignored, and a
malformed backup_label location now fails pg_combinebackup's "could
not parse" check rather than its "improper terminator" check.

Author: Zexin Li <lizi.openmind@gmail.com>
---
 src/bin/pg_basebackup/pg_basebackup.c   | 16 ++++----------
 src/bin/pg_basebackup/receivelog.c      |  8 ++-----
 src/bin/pg_basebackup/streamutil.c      | 12 +++-------
 src/bin/pg_combinebackup/backup_label.c | 17 ++++++++-------
 src/bin/pg_rewind/libpq_source.c        |  7 ++----
 src/bin/pg_rewind/timeline.c            | 29 ++++++++++++++++++-------
 6 files changed, 41 insertions(+), 48 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 12fc752bff5..c3b87a19e76 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -32,6 +32,7 @@
 #include "common/file_perm.h"
 #include "common/file_utils.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "fe_utils/option_utils.h"
 #include "fe_utils/recovery_gen.h"
 #include "getopt_long.h"
@@ -482,17 +483,14 @@ reached_end_position(XLogRecPtr segendpos, uint32 timeline,
 		{
 			ssize_t		nread;
 			char		xlogend[64] = {0};
-			uint32		hi,
-						lo;
 
 			nread = read(bgpipe[0], xlogend, sizeof(xlogend) - 1);
 			if (nread < 0)
 				pg_fatal("could not read from ready pipe: %m");
 
-			if (sscanf(xlogend, "%X/%08X", &hi, &lo) != 2)
+			if (!pg_parse_lsn(xlogend, &xlogendptr))
 				pg_fatal("could not parse write-ahead log location \"%s\"",
 						 xlogend);
-			xlogendptr = ((uint64) hi) << 32 | lo;
 			has_xlogendptr = 1;
 
 			/*
@@ -620,8 +618,6 @@ StartLogStreamer(char *startpos, uint32 timeline, char *sysidentifier,
 				 int wal_compress_level)
 {
 	logstreamer_param *param;
-	uint32		hi,
-				lo;
 	char		statusdir[MAXPGPATH];
 
 	param = pg_malloc0_object(logstreamer_param);
@@ -631,10 +627,9 @@ StartLogStreamer(char *startpos, uint32 timeline, char *sysidentifier,
 	param->wal_compress_level = wal_compress_level;
 
 	/* Convert the starting position */
-	if (sscanf(startpos, "%X/%08X", &hi, &lo) != 2)
+	if (!pg_parse_lsn(startpos, &param->startptr))
 		pg_fatal("could not parse write-ahead log location \"%s\"",
 				 startpos);
-	param->startptr = ((uint64) hi) << 32 | lo;
 	/* Round off to even segment position */
 	param->startptr -= XLogSegmentOffset(param->startptr, WalSegSz);
 
@@ -2216,8 +2211,6 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 		 * casting to a different size on WIN64.
 		 */
 		intptr_t	bgchild_handle = bgchild;
-		uint32		hi,
-					lo;
 #endif
 
 		if (verbose)
@@ -2243,10 +2236,9 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 		 * value directly in the variable, and then set the flag that says
 		 * it's there.
 		 */
-		if (sscanf(xlogend, "%X/%08X", &hi, &lo) != 2)
+		if (!pg_parse_lsn(xlogend, &xlogendptr))
 			pg_fatal("could not parse write-ahead log location \"%s\"",
 					 xlogend);
-		xlogendptr = ((uint64) hi) << 32 | lo;
 		InterlockedIncrement(&has_xlogendptr);
 
 		/* First wait for the thread to exit */
diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c
index faa60711b1b..77a2b4458b3 100644
--- a/src/bin/pg_basebackup/receivelog.c
+++ b/src/bin/pg_basebackup/receivelog.c
@@ -20,6 +20,7 @@
 
 #include "access/xlog_internal.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "libpq-fe.h"
 #include "libpq/protocol.h"
 #include "receivelog.h"
@@ -704,9 +705,6 @@ error:
 static bool
 ReadEndOfStreamingResult(PGresult *res, XLogRecPtr *startpos, uint32 *timeline)
 {
-	uint32		startpos_xlogid,
-				startpos_xrecoff;
-
 	/*----------
 	 * The result set consists of one row and two columns, e.g:
 	 *
@@ -727,14 +725,12 @@ ReadEndOfStreamingResult(PGresult *res, XLogRecPtr *startpos, uint32 *timeline)
 	}
 
 	*timeline = atoi(PQgetvalue(res, 0, 0));
-	if (sscanf(PQgetvalue(res, 0, 1), "%X/%08X", &startpos_xlogid,
-			   &startpos_xrecoff) != 2)
+	if (!pg_parse_lsn(PQgetvalue(res, 0, 1), startpos))
 	{
 		pg_log_error("could not parse next timeline's starting point \"%s\"",
 					 PQgetvalue(res, 0, 1));
 		return false;
 	}
-	*startpos = ((uint64) startpos_xlogid << 32) | startpos_xrecoff;
 
 	return true;
 }
diff --git a/src/bin/pg_basebackup/streamutil.c b/src/bin/pg_basebackup/streamutil.c
index 8fcd690f155..8086fde84db 100644
--- a/src/bin/pg_basebackup/streamutil.c
+++ b/src/bin/pg_basebackup/streamutil.c
@@ -21,6 +21,7 @@
 #include "common/connect.h"
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "common/string.h"
 #include "datatype/timestamp.h"
 #include "port/pg_bswap.h"
@@ -410,8 +411,6 @@ RunIdentifySystem(PGconn *conn, char **sysid, TimeLineID *starttli,
 				  XLogRecPtr *startpos, char **db_name)
 {
 	PGresult   *res;
-	uint32		hi,
-				lo;
 
 	/* Check connection existence */
 	Assert(conn != NULL);
@@ -445,7 +444,7 @@ RunIdentifySystem(PGconn *conn, char **sysid, TimeLineID *starttli,
 	/* Get LSN start position if necessary */
 	if (startpos != NULL)
 	{
-		if (sscanf(PQgetvalue(res, 0, 2), "%X/%08X", &hi, &lo) != 2)
+		if (!pg_parse_lsn(PQgetvalue(res, 0, 2), startpos))
 		{
 			pg_log_error("could not parse write-ahead log location \"%s\"",
 						 PQgetvalue(res, 0, 2));
@@ -453,7 +452,6 @@ RunIdentifySystem(PGconn *conn, char **sysid, TimeLineID *starttli,
 			PQclear(res);
 			return false;
 		}
-		*startpos = ((uint64) hi) << 32 | lo;
 	}
 
 	/* Get database name, only available in 9.4 and newer versions */
@@ -549,17 +547,13 @@ GetSlotInformation(PGconn *conn, const char *slot_name,
 	/* restart LSN */
 	if (!PQgetisnull(res, 0, 1))
 	{
-		uint32		hi,
-					lo;
-
-		if (sscanf(PQgetvalue(res, 0, 1), "%X/%08X", &hi, &lo) != 2)
+		if (!pg_parse_lsn(PQgetvalue(res, 0, 1), &lsn_loc))
 		{
 			pg_log_error("could not parse restart_lsn \"%s\" for replication slot \"%s\"",
 						 PQgetvalue(res, 0, 1), slot_name);
 			PQclear(res);
 			return false;
 		}
-		lsn_loc = ((uint64) hi) << 32 | lo;
 	}
 
 	/* current TLI */
diff --git a/src/bin/pg_combinebackup/backup_label.c b/src/bin/pg_combinebackup/backup_label.c
index b757e772b92..fd8f493a262 100644
--- a/src/bin/pg_combinebackup/backup_label.c
+++ b/src/bin/pg_combinebackup/backup_label.c
@@ -17,6 +17,7 @@
 #include "backup_label.h"
 #include "common/file_perm.h"
 #include "common/logging.h"
+#include "common/pg_parse_lsn.h"
 #include "write_manifest.h"
 
 static int	get_eol_offset(StringInfo buf);
@@ -242,20 +243,20 @@ static bool
 parse_lsn(char *s, char *e, XLogRecPtr *lsn, char **c)
 {
 	char		save = *e;
-	int			nchars;
+	char	   *token_end;
+	char		save_token_end;
 	bool		success;
-	unsigned	hi;
-	unsigned	lo;
 
 	*e = '\0';
-	success = (sscanf(s, "%X/%08X%n", &hi, &lo, &nchars) == 2);
+	token_end = s + strcspn(s, " \t\n\r\f\v");
+	save_token_end = *token_end;
+	*token_end = '\0';
+	success = pg_parse_lsn(s, lsn);
+	*token_end = save_token_end;
 	*e = save;
 
 	if (success)
-	{
-		*lsn = ((XLogRecPtr) hi) << 32 | (XLogRecPtr) lo;
-		*c = s + nchars;
-	}
+		*c = token_end;
 
 	return success;
 }
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 216755b3ddb..abfbd312580 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -11,6 +11,7 @@
 
 #include "catalog/pg_type_d.h"
 #include "common/connect.h"
+#include "common/pg_parse_lsn.h"
 #include "file_ops.h"
 #include "filemap.h"
 #include "lib/stringinfo.h"
@@ -209,17 +210,13 @@ libpq_get_current_wal_insert_lsn(rewind_source *source)
 {
 	PGconn	   *conn = ((libpq_source *) source)->conn;
 	XLogRecPtr	result;
-	uint32		hi;
-	uint32		lo;
 	char	   *val;
 
 	val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()");
 
-	if (sscanf(val, "%X/%08X", &hi, &lo) != 2)
+	if (!pg_parse_lsn(val, &result))
 		pg_fatal("unrecognized result \"%s\" for current WAL insert location", val);
 
-	result = ((uint64) hi) << 32 | lo;
-
 	pg_free(val);
 
 	return result;
diff --git a/src/bin/pg_rewind/timeline.c b/src/bin/pg_rewind/timeline.c
index dda06eaa0bc..85f088ef684 100644
--- a/src/bin/pg_rewind/timeline.c
+++ b/src/bin/pg_rewind/timeline.c
@@ -10,6 +10,7 @@
 #include "postgres_fe.h"
 
 #include "access/timeline.h"
+#include "common/pg_parse_lsn.h"
 #include "pg_rewind.h"
 
 /*
@@ -44,10 +45,12 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries)
 	while (!lastline)
 	{
 		char	   *ptr;
+		char	   *token_end;
+		char		save;
 		TimeLineID	tli;
-		uint32		switchpoint_hi;
-		uint32		switchpoint_lo;
-		int			nfields;
+		XLogRecPtr	switchpoint;
+		bool		success;
+		int			nchars;
 
 		fline = bufptr;
 		while (*bufptr && *bufptr != '\n')
@@ -66,16 +69,26 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries)
 		if (*ptr == '\0' || *ptr == '#')
 			continue;
 
-		nfields = sscanf(fline, "%u\t%X/%08X", &tli, &switchpoint_hi, &switchpoint_lo);
-
-		if (nfields < 1)
+		if (sscanf(fline, "%u%n", &tli, &nchars) != 1)
 		{
 			/* expect a numeric timeline ID as first field of line */
 			pg_log_error("syntax error in history file: %s", fline);
 			pg_log_error_detail("Expected a numeric timeline ID.");
 			exit(1);
 		}
-		if (nfields != 3)
+
+		/* the switchpoint location follows, separated by whitespace */
+		ptr = fline + nchars;
+		ptr += strspn(ptr, " \t\n\r\f\v");
+
+		/* isolate the location from the rest of the line before parsing it */
+		token_end = ptr + strcspn(ptr, " \t\n\r\f\v");
+		save = *token_end;
+		*token_end = '\0';
+		success = pg_parse_lsn(ptr, &switchpoint);
+		*token_end = save;
+
+		if (!success)
 		{
 			pg_log_error("syntax error in history file: %s", fline);
 			pg_log_error_detail("Expected a write-ahead log switchpoint location.");
@@ -96,7 +109,7 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries)
 		entry = &entries[nlines - 1];
 		entry->tli = tli;
 		entry->begin = prevend;
-		entry->end = ((uint64) (switchpoint_hi)) << 32 | (uint64) switchpoint_lo;
+		entry->end = switchpoint;
 		prevend = entry->end;
 
 		/* we ignore the remainder of each line */
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-11 00:40         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-12 08:09           ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-14 06:33             ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-14 14:08               ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-17 03:23                 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
@ 2026-08-19 17:57                   ` Fujii Masao <masao.fujii@gmail.com>
  2026-08-21 07:55                     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 1 reply; 13+ messages in thread

From: Fujii Masao @ 2026-08-19 17:57 UTC (permalink / raw)
  To: Zexin Li <lizi.openmind@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com; ayushtiwari.slg01@gmail.com

On Mon, Aug 17, 2026 at 12:23 PM Zexin Li <lizi.openmind@gmail.com> wrote:
> You're right, that is better. v2 attached does it that way in
> backup_label.c, and applies the same approach in pg_rewind's
> timeline.c, so the patch no longer touches src/common at all.

Thanks for updating the patch! It looks good to me.
Barring any objections, I will commit it.

Regards,

-- 
Fujii Masao






^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits
  2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
  2026-08-04 08:54 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-05 06:03   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-07 01:58     ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-10 05:47       ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-11 00:40         ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-12 08:09           ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-14 06:33             ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-14 14:08               ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
  2026-08-17 03:23                 ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Zexin Li <lizi.openmind@gmail.com>
  2026-08-19 17:57                   ` Re: BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits Fujii Masao <masao.fujii@gmail.com>
@ 2026-08-21 07:55                     ` Fujii Masao <masao.fujii@gmail.com>
  0 siblings, 0 replies; 13+ messages in thread

From: Fujii Masao @ 2026-08-21 07:55 UTC (permalink / raw)
  To: Zexin Li <lizi.openmind@gmail.com>; +Cc: pgsql-bugs@lists.postgresql.org; malis@pgrust.com; ayushtiwari.slg01@gmail.com

On Thu, Aug 20, 2026 at 2:57 AM Fujii Masao <masao.fujii@gmail.com> wrote:
> Thanks for updating the patch! It looks good to me.
> Barring any objections, I will commit it.

I've pushed the patch with a small adjustment. Thanks!

Regards,

-- 
Fujii Masao






^ permalink  raw  reply  [nested|flat] 13+ messages in thread


end of thread, other threads:[~2026-08-21 07:55 UTC | newest]

Thread overview: 13+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-08-02 17:49 BUG #19598: pg_waldump: -s/-e accept out-of-range WAL locations and silently use the low 32 bits PG Bug reporting form <noreply@postgresql.org>
2026-08-04 08:54 ` Zexin Li <lizi.openmind@gmail.com>
2026-08-05 06:03   ` Fujii Masao <masao.fujii@gmail.com>
2026-08-07 01:58     ` Zexin Li <lizi.openmind@gmail.com>
2026-08-10 05:47       ` Fujii Masao <masao.fujii@gmail.com>
2026-08-10 08:23         ` Ayush Tiwari <ayushtiwari.slg01@gmail.com>
2026-08-11 00:40         ` Zexin Li <lizi.openmind@gmail.com>
2026-08-12 08:09           ` Fujii Masao <masao.fujii@gmail.com>
2026-08-14 06:33             ` Zexin Li <lizi.openmind@gmail.com>
2026-08-14 14:08               ` Fujii Masao <masao.fujii@gmail.com>
2026-08-17 03:23                 ` Zexin Li <lizi.openmind@gmail.com>
2026-08-19 17:57                   ` Fujii Masao <masao.fujii@gmail.com>
2026-08-21 07:55                     ` Fujii Masao <masao.fujii@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox