From 19c0eadc5678225d1227eaf5292e0688d06c60e7 Mon Sep 17 00:00:00 2001 From: Zexin Li 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 Author: Zexin Li Reviewed-by: Fujii Masao 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