Tighten up range checks for pg_resetwal arguments

Started by Heikki Linnakangasabout 1 month ago5 messages
#1Heikki Linnakangas
hlinnaka@iki.fi
2 attachment(s)

While working on the 64-bit multixid offsets patch and commit
94939c5f3a, I got a little annoyed by how lax pg_resetwal is about
out-of-range values. These are currently accepted, for example:

# Negative XID
pg_resetwal -D data -x -1000

# XID larger than 2^32 (on some platforms)
pg_resetwal -D data -x 10000000000

The first attached patch tightens up the parsing to reject those.

The second attached patch is just refactoring. Currently, we use invalid
values for the variables backing each of the options to mean "option was
not given". I think it would be more clear to have separate boolean
variables for that. I did that for the --multixact-ids option in commit
f99e30149f already, because there was no unused value for multixid that
we could use. This patch expands that to all the options.

- Heikki

Attachments:

0001-pg_resetwal-Reject-negative-and-out-of-range-argumen.patchtext/x-patch; charset=UTF-8; name=0001-pg_resetwal-Reject-negative-and-out-of-range-argumen.patchDownload
From c0c07bcf36c7d9c06ca45619ccca5ca38010e483 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 19 Nov 2025 16:36:00 +0200
Subject: [PATCH 1/2] pg_resetwal: Reject negative and out of range arguments

The strtoul() function that we used to parse many of the options
accepts negative values, and silently wraps them to the equivalent
unsigned values. For example, -1 becomes 0xFFFFFFFF, on platforms
where unsigned long is 32 bits wide. Also, on platforms where
"unsigned long" is 64 bits wide, we silently casted values larger than
UINT32_MAX to the equivalent 32-bit value. Both of those behaviors
seem undesireable, so tighten up the parsing to reject negative and
too large values.
---
 src/bin/pg_resetwal/pg_resetwal.c  | 64 ++++++++++++++++++++++++------
 src/bin/pg_resetwal/t/001_basic.pl | 19 ++++++++-
 2 files changed, 68 insertions(+), 15 deletions(-)

diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 8d5d9805279..8ca8dad01a0 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -92,6 +92,7 @@ static void KillExistingArchiveStatus(void);
 static void KillExistingWALSummaries(void);
 static void WriteEmptyXLOG(void);
 static void usage(void);
+static uint32 strtouint32_strict(const char *restrict s, char **restrict endptr, int base);
 
 
 int
@@ -120,7 +121,6 @@ main(int argc, char *argv[])
 	MultiXactId set_oldestmxid = 0;
 	char	   *endptr;
 	char	   *endptr2;
-	int64		tmpi64;
 	char	   *DataDir = NULL;
 	char	   *log_fname = NULL;
 	int			fd;
@@ -162,7 +162,7 @@ main(int argc, char *argv[])
 
 			case 'e':
 				errno = 0;
-				set_xid_epoch = strtoul(optarg, &endptr, 0);
+				set_xid_epoch = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					/*------
@@ -177,7 +177,7 @@ main(int argc, char *argv[])
 
 			case 'u':
 				errno = 0;
-				set_oldest_xid = strtoul(optarg, &endptr, 0);
+				set_oldest_xid = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-u");
@@ -190,7 +190,7 @@ main(int argc, char *argv[])
 
 			case 'x':
 				errno = 0;
-				set_xid = strtoul(optarg, &endptr, 0);
+				set_xid = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-x");
@@ -203,7 +203,7 @@ main(int argc, char *argv[])
 
 			case 'c':
 				errno = 0;
-				set_oldest_commit_ts_xid = strtoul(optarg, &endptr, 0);
+				set_oldest_commit_ts_xid = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != ',' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-c");
@@ -229,7 +229,7 @@ main(int argc, char *argv[])
 
 			case 'o':
 				errno = 0;
-				set_oid = strtoul(optarg, &endptr, 0);
+				set_oid = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
@@ -242,7 +242,7 @@ main(int argc, char *argv[])
 
 			case 'm':
 				errno = 0;
-				set_mxid = strtoul(optarg, &endptr, 0);
+				set_mxid = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != ',' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-m");
@@ -250,7 +250,7 @@ main(int argc, char *argv[])
 					exit(1);
 				}
 
-				set_oldestmxid = strtoul(endptr + 1, &endptr2, 0);
+				set_oldestmxid = strtouint32_strict(endptr + 1, &endptr2, 0);
 				if (endptr2 == endptr + 1 || *endptr2 != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-m");
@@ -269,17 +269,13 @@ main(int argc, char *argv[])
 
 			case 'O':
 				errno = 0;
-				tmpi64 = strtoi64(optarg, &endptr, 0);
+				set_mxoff = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-O");
 					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 					exit(1);
 				}
-				if (tmpi64 < 0 || tmpi64 > (int64) MaxMultiXactOffset)
-					pg_fatal("multitransaction offset (-O) must be between 0 and %u", MaxMultiXactOffset);
-
-				set_mxoff = (MultiXactOffset) tmpi64;
 				mxoff_given = true;
 				break;
 
@@ -1214,3 +1210,45 @@ usage(void)
 	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
 	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
 }
+
+/*
+ * strtouint32_strict -- like strtoul(), but returns uint32 and doesn't accept
+ * negative values
+ */
+static uint32
+strtouint32_strict(const char *restrict s, char **restrict endptr, int base)
+{
+	unsigned long val;
+	bool		is_neg;
+
+	/* skip leading whitespace */
+	while (isspace(*s))
+		s++;
+
+	/*
+	 * Is it negative?  We still call strtoul() if it was, to set 'endptr'.
+	 * (The current callers don't care though.)
+	 */
+	is_neg = (*s == '-');
+
+	val = strtoul(s, endptr, base);
+
+	/* reject if it was negative */
+	if (errno == 0 && is_neg)
+	{
+		errno = ERANGE;
+		val = 0;
+	}
+
+	/*
+	 * reject values larger than UINT32_MAX on platforms where long is 64 bits
+	 * wide.
+	 */
+	if (errno == 0 && val != (uint32) val)
+	{
+		errno = ERANGE;
+		val = UINT32_MAX;
+	}
+
+	return (uint32) val;
+}
diff --git a/src/bin/pg_resetwal/t/001_basic.pl b/src/bin/pg_resetwal/t/001_basic.pl
index 90ecb8afe18..e9780dbe2a6 100644
--- a/src/bin/pg_resetwal/t/001_basic.pl
+++ b/src/bin/pg_resetwal/t/001_basic.pl
@@ -103,7 +103,7 @@ command_fails_like(
 	'fails with incorrect -e option');
 command_fails_like(
 	[ 'pg_resetwal', '-e' => '-1', $node->data_dir ],
-	qr/must not be -1/,
+	qr/error: invalid argument for option -e/,
 	'fails with -e value -1');
 # -l
 command_fails_like(
@@ -145,7 +145,7 @@ command_fails_like(
 	'fails with incorrect -O option');
 command_fails_like(
 	[ 'pg_resetwal', '-O' => '-1', $node->data_dir ],
-	qr/must be between 0 and 4294967295/,
+	qr/error: invalid argument for option -O/,
 	'fails with -O value -1');
 # --wal-segsize
 command_fails_like(
@@ -175,6 +175,21 @@ command_fails_like(
 	qr/must be greater than/,
 	'fails with -x value too small');
 
+# Check out of range values with -x. These are forbidden for all other
+# 32-bit values too, but we use just -x to exercise the parsing.
+command_fails_like(
+	[ 'pg_resetwal', '-x' => '-1', $node->data_dir ],
+	qr/error: invalid argument for option -x/,
+	'fails with -x value -1');
+command_fails_like(
+	[ 'pg_resetwal', '-x' => '-100', $node->data_dir ],
+	qr/error: invalid argument for option -x/,
+	'fails with negative -x value');
+command_fails_like(
+	[ 'pg_resetwal', '-x' => '10000000000', $node->data_dir ],
+	qr/error: invalid argument for option -x/,
+	'fails with -x value too large');
+
 # --char-signedness
 command_fails_like(
 	[ 'pg_resetwal', '--char-signedness', 'foo', $node->data_dir ],
-- 
2.47.3

0002-pg_resetwal-Use-separate-flags-for-whether-an-option.patchtext/x-patch; charset=UTF-8; name=0002-pg_resetwal-Use-separate-flags-for-whether-an-option.patchDownload
From f99e30149fcbfc700b16f4c1f6d7ca7fb11ba376 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 3 Dec 2025 20:48:48 +0200
Subject: [PATCH 2/2] pg_resetwal: Use separate flags for whether an option is
 given

Currently, we use special values that are otherwise invalid for each
option to indicate "option was not given". Replace that with separate
boolean variables for each option. It seems more clear to be explicit.

We were already doing that for the -m option, because there were no
invalid values for nextMulti that we could use (since commit
94939c5f3a).
---
 src/bin/pg_resetwal/pg_resetwal.c | 166 +++++++++++++++++-------------
 1 file changed, 95 insertions(+), 71 deletions(-)

diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c
index 8ca8dad01a0..c667a11cb6a 100644
--- a/src/bin/pg_resetwal/pg_resetwal.c
+++ b/src/bin/pg_resetwal/pg_resetwal.c
@@ -64,21 +64,43 @@ static ControlFileData ControlFile; /* pg_control values */
 static XLogSegNo newXlogSegNo;	/* new XLOG segment # */
 static bool guessed = false;	/* T if we had to guess at any values */
 static const char *progname;
-static uint32 set_xid_epoch = (uint32) -1;
-static TransactionId set_oldest_xid = 0;
-static TransactionId set_xid = 0;
-static TransactionId set_oldest_commit_ts_xid = 0;
-static TransactionId set_newest_commit_ts_xid = 0;
-static Oid	set_oid = 0;
-static bool mxid_given = false;
-static MultiXactId set_mxid = 0;
-static bool mxoff_given = false;
-static MultiXactOffset set_mxoff = 0;
+
+/*
+ * New values given on the command-line
+ */
+static bool next_xid_epoch_given = false;
+static uint32 next_xid_epoch_val;
+
+static bool oldest_xid_given = false;
+static TransactionId oldest_xid_val;
+
+static bool next_xid_given = false;
+static TransactionId next_xid_val;
+
+static bool commit_ts_xids_given = false;
+static TransactionId oldest_commit_ts_xid_val;
+static TransactionId newest_commit_ts_xid_val;
+
+static bool next_oid_given = false;
+static Oid	next_oid_val;
+
+static bool mxids_given = false;
+static MultiXactId next_mxid_val;
+static MultiXactId oldest_mxid_val = 0;
+
+static bool next_mxoff_given = false;
+static MultiXactOffset next_mxoff_val;
+
+static bool wal_segsize_given = false;
+static int	wal_segsize_val;
+
+static bool char_signedness_given = false;
+static bool char_signedness_val;
+
+
 static TimeLineID minXlogTli = 0;
 static XLogSegNo minXlogSegNo = 0;
 static int	WalSegSz;
-static int	set_wal_segsize;
-static int	set_char_signedness = -1;
 
 static void CheckDataVersion(void);
 static bool read_controlfile(void);
@@ -118,7 +140,6 @@ main(int argc, char *argv[])
 	int			c;
 	bool		force = false;
 	bool		noupdate = false;
-	MultiXactId set_oldestmxid = 0;
 	char	   *endptr;
 	char	   *endptr2;
 	char	   *DataDir = NULL;
@@ -162,7 +183,7 @@ main(int argc, char *argv[])
 
 			case 'e':
 				errno = 0;
-				set_xid_epoch = strtouint32_strict(optarg, &endptr, 0);
+				next_xid_epoch_val = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					/*------
@@ -171,46 +192,47 @@ main(int argc, char *argv[])
 					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 					exit(1);
 				}
-				if (set_xid_epoch == -1)
-					pg_fatal("transaction ID epoch (-e) must not be -1");
+				next_xid_epoch_given = true;
 				break;
 
 			case 'u':
 				errno = 0;
-				set_oldest_xid = strtouint32_strict(optarg, &endptr, 0);
+				oldest_xid_val = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-u");
 					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 					exit(1);
 				}
-				if (!TransactionIdIsNormal(set_oldest_xid))
+				if (!TransactionIdIsNormal(oldest_xid_val))
 					pg_fatal("oldest transaction ID (-u) must be greater than or equal to %u", FirstNormalTransactionId);
+				oldest_xid_given = true;
 				break;
 
 			case 'x':
 				errno = 0;
-				set_xid = strtouint32_strict(optarg, &endptr, 0);
+				next_xid_val = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-x");
 					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 					exit(1);
 				}
-				if (!TransactionIdIsNormal(set_xid))
+				if (!TransactionIdIsNormal(next_xid_val))
 					pg_fatal("transaction ID (-x) must be greater than or equal to %u", FirstNormalTransactionId);
+				next_xid_given = true;
 				break;
 
 			case 'c':
 				errno = 0;
-				set_oldest_commit_ts_xid = strtouint32_strict(optarg, &endptr, 0);
+				oldest_commit_ts_xid_val = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != ',' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-c");
 					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 					exit(1);
 				}
-				set_newest_commit_ts_xid = strtoul(endptr + 1, &endptr2, 0);
+				newest_commit_ts_xid_val = strtoul(endptr + 1, &endptr2, 0);
 				if (endptr2 == endptr + 1 || *endptr2 != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-c");
@@ -218,31 +240,33 @@ main(int argc, char *argv[])
 					exit(1);
 				}
 
-				if (set_oldest_commit_ts_xid < FirstNormalTransactionId &&
-					set_oldest_commit_ts_xid != InvalidTransactionId)
+				if (oldest_commit_ts_xid_val < FirstNormalTransactionId &&
+					oldest_commit_ts_xid_val != InvalidTransactionId)
 					pg_fatal("transaction ID (-c) must be either %u or greater than or equal to %u", InvalidTransactionId, FirstNormalTransactionId);
 
-				if (set_newest_commit_ts_xid < FirstNormalTransactionId &&
-					set_newest_commit_ts_xid != InvalidTransactionId)
+				if (newest_commit_ts_xid_val < FirstNormalTransactionId &&
+					newest_commit_ts_xid_val != InvalidTransactionId)
 					pg_fatal("transaction ID (-c) must be either %u or greater than or equal to %u", InvalidTransactionId, FirstNormalTransactionId);
+				commit_ts_xids_given = true;
 				break;
 
 			case 'o':
 				errno = 0;
-				set_oid = strtouint32_strict(optarg, &endptr, 0);
+				next_oid_val = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-o");
 					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 					exit(1);
 				}
-				if (set_oid == 0)
+				if (next_oid_val == 0)
 					pg_fatal("OID (-o) must not be 0");
+				next_oid_given = true;
 				break;
 
 			case 'm':
 				errno = 0;
-				set_mxid = strtouint32_strict(optarg, &endptr, 0);
+				next_mxid_val = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != ',' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-m");
@@ -250,7 +274,7 @@ main(int argc, char *argv[])
 					exit(1);
 				}
 
-				set_oldestmxid = strtouint32_strict(endptr + 1, &endptr2, 0);
+				oldest_mxid_val = strtouint32_strict(endptr + 1, &endptr2, 0);
 				if (endptr2 == endptr + 1 || *endptr2 != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-m");
@@ -262,21 +286,21 @@ main(int argc, char *argv[])
 				 * XXX It'd be nice to have more sanity checks here, e.g. so
 				 * that oldest is not wrapped around w.r.t. nextMulti.
 				 */
-				if (set_oldestmxid == 0)
+				if (oldest_mxid_val == 0)
 					pg_fatal("oldest multitransaction ID (-m) must not be 0");
-				mxid_given = true;
+				mxids_given = true;
 				break;
 
 			case 'O':
 				errno = 0;
-				set_mxoff = strtouint32_strict(optarg, &endptr, 0);
+				next_mxoff_val = strtouint32_strict(optarg, &endptr, 0);
 				if (endptr == optarg || *endptr != '\0' || errno != 0)
 				{
 					pg_log_error("invalid argument for option %s", "-O");
 					pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 					exit(1);
 				}
-				mxoff_given = true;
+				next_mxoff_given = true;
 				break;
 
 			case 'l':
@@ -300,9 +324,10 @@ main(int argc, char *argv[])
 
 					if (!option_parse_int(optarg, "--wal-segsize", 1, 1024, &wal_segsize_mb))
 						exit(1);
-					set_wal_segsize = wal_segsize_mb * 1024 * 1024;
-					if (!IsValidWalSegSize(set_wal_segsize))
+					wal_segsize_val = wal_segsize_mb * 1024 * 1024;
+					if (!IsValidWalSegSize(wal_segsize_val))
 						pg_fatal("argument of %s must be a power of two between 1 and 1024", "--wal-segsize");
+					wal_segsize_given = true;
 					break;
 				}
 
@@ -311,15 +336,16 @@ main(int argc, char *argv[])
 					errno = 0;
 
 					if (pg_strcasecmp(optarg, "signed") == 0)
-						set_char_signedness = 1;
+						char_signedness_val = true;
 					else if (pg_strcasecmp(optarg, "unsigned") == 0)
-						set_char_signedness = 0;
+						char_signedness_val = false;
 					else
 					{
 						pg_log_error("invalid argument for option %s", "--char-signedness");
 						pg_log_error_hint("Try \"%s --help\" for more information.", progname);
 						exit(1);
 					}
+					char_signedness_given = true;
 					break;
 				}
 
@@ -407,8 +433,8 @@ main(int argc, char *argv[])
 	/*
 	 * If no new WAL segment size was specified, use the control file value.
 	 */
-	if (set_wal_segsize != 0)
-		WalSegSz = set_wal_segsize;
+	if (wal_segsize_given)
+		WalSegSz = wal_segsize_val;
 	else
 		WalSegSz = ControlFile.xlog_seg_size;
 
@@ -431,42 +457,43 @@ main(int argc, char *argv[])
 	 * Adjust fields if required by switches.  (Do this now so that printout,
 	 * if any, includes these values.)
 	 */
-	if (set_xid_epoch != -1)
+	if (next_xid_epoch_given)
 		ControlFile.checkPointCopy.nextXid =
-			FullTransactionIdFromEpochAndXid(set_xid_epoch,
+			FullTransactionIdFromEpochAndXid(next_xid_epoch_val,
 											 XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
 
-	if (set_oldest_xid != 0)
+	if (oldest_xid_given)
 	{
-		ControlFile.checkPointCopy.oldestXid = set_oldest_xid;
+		ControlFile.checkPointCopy.oldestXid = oldest_xid_val;
 		ControlFile.checkPointCopy.oldestXidDB = InvalidOid;
 	}
 
-	if (set_xid != 0)
+	if (next_xid_given)
 		ControlFile.checkPointCopy.nextXid =
 			FullTransactionIdFromEpochAndXid(EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid),
-											 set_xid);
+											 next_xid_val);
 
-	if (set_oldest_commit_ts_xid != 0)
-		ControlFile.checkPointCopy.oldestCommitTsXid = set_oldest_commit_ts_xid;
-	if (set_newest_commit_ts_xid != 0)
-		ControlFile.checkPointCopy.newestCommitTsXid = set_newest_commit_ts_xid;
+	if (commit_ts_xids_given)
+	{
+		ControlFile.checkPointCopy.oldestCommitTsXid = oldest_commit_ts_xid_val;
+		ControlFile.checkPointCopy.newestCommitTsXid = newest_commit_ts_xid_val;
+	}
 
-	if (set_oid != 0)
-		ControlFile.checkPointCopy.nextOid = set_oid;
+	if (next_oid_given)
+		ControlFile.checkPointCopy.nextOid = next_oid_val;
 
-	if (mxid_given)
+	if (mxids_given)
 	{
-		ControlFile.checkPointCopy.nextMulti = set_mxid;
+		ControlFile.checkPointCopy.nextMulti = next_mxid_val;
 
-		ControlFile.checkPointCopy.oldestMulti = set_oldestmxid;
+		ControlFile.checkPointCopy.oldestMulti = oldest_mxid_val;
 		if (ControlFile.checkPointCopy.oldestMulti < FirstMultiXactId)
 			ControlFile.checkPointCopy.oldestMulti += FirstMultiXactId;
 		ControlFile.checkPointCopy.oldestMultiDB = InvalidOid;
 	}
 
-	if (mxoff_given)
-		ControlFile.checkPointCopy.nextMultiOffset = set_mxoff;
+	if (next_mxoff_given)
+		ControlFile.checkPointCopy.nextMultiOffset = next_mxoff_val;
 
 	if (minXlogTli > ControlFile.checkPointCopy.ThisTimeLineID)
 	{
@@ -474,11 +501,11 @@ main(int argc, char *argv[])
 		ControlFile.checkPointCopy.PrevTimeLineID = minXlogTli;
 	}
 
-	if (set_wal_segsize != 0)
+	if (wal_segsize_given)
 		ControlFile.xlog_seg_size = WalSegSz;
 
-	if (set_char_signedness != -1)
-		ControlFile.default_char_signedness = (set_char_signedness == 1);
+	if (char_signedness_given)
+		ControlFile.default_char_signedness = char_signedness_val;
 
 	if (minXlogSegNo > newXlogSegNo)
 		newXlogSegNo = minXlogSegNo;
@@ -809,7 +836,7 @@ PrintNewControlValues(void)
 				 newXlogSegNo, WalSegSz);
 	printf(_("First log segment after reset:        %s\n"), fname);
 
-	if (mxid_given)
+	if (mxids_given)
 	{
 		printf(_("NextMultiXactId:                      %u\n"),
 			   ControlFile.checkPointCopy.nextMulti);
@@ -819,25 +846,25 @@ PrintNewControlValues(void)
 			   ControlFile.checkPointCopy.oldestMultiDB);
 	}
 
-	if (mxoff_given)
+	if (next_mxoff_given)
 	{
 		printf(_("NextMultiOffset:                      %u\n"),
 			   ControlFile.checkPointCopy.nextMultiOffset);
 	}
 
-	if (set_oid != 0)
+	if (next_oid_given)
 	{
 		printf(_("NextOID:                              %u\n"),
 			   ControlFile.checkPointCopy.nextOid);
 	}
 
-	if (set_xid != 0)
+	if (next_xid_given)
 	{
 		printf(_("NextXID:                              %u\n"),
 			   XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
 	}
 
-	if (set_oldest_xid != 0)
+	if (oldest_xid_given)
 	{
 		printf(_("OldestXID:                            %u\n"),
 			   ControlFile.checkPointCopy.oldestXid);
@@ -845,24 +872,21 @@ PrintNewControlValues(void)
 			   ControlFile.checkPointCopy.oldestXidDB);
 	}
 
-	if (set_xid_epoch != -1)
+	if (next_xid_epoch_given)
 	{
 		printf(_("NextXID epoch:                        %u\n"),
 			   EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid));
 	}
 
-	if (set_oldest_commit_ts_xid != 0)
+	if (commit_ts_xids_given)
 	{
 		printf(_("oldestCommitTsXid:                    %u\n"),
 			   ControlFile.checkPointCopy.oldestCommitTsXid);
-	}
-	if (set_newest_commit_ts_xid != 0)
-	{
 		printf(_("newestCommitTsXid:                    %u\n"),
 			   ControlFile.checkPointCopy.newestCommitTsXid);
 	}
 
-	if (set_wal_segsize != 0)
+	if (wal_segsize_given)
 	{
 		printf(_("Bytes per WAL segment:                %u\n"),
 			   ControlFile.xlog_seg_size);
-- 
2.47.3

#2Chao Li
li.evan.chao@gmail.com
In reply to: Heikki Linnakangas (#1)
Re: Tighten up range checks for pg_resetwal arguments

Hi Heikki,

This patch looks like a straightforward change, but I see a correctness issue and a few small comments.

On Dec 4, 2025, at 03:07, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

While working on the 64-bit multixid offsets patch and commit 94939c5f3a, I got a little annoyed by how lax pg_resetwal is about out-of-range values. These are currently accepted, for example:

# Negative XID
pg_resetwal -D data -x -1000

# XID larger than 2^32 (on some platforms)
pg_resetwal -D data -x 10000000000

The first attached patch tightens up the parsing to reject those.

The second attached patch is just refactoring. Currently, we use invalid values for the variables backing each of the options to mean "option was not given". I think it would be more clear to have separate boolean variables for that. I did that for the --multixact-ids option in commit f99e30149f already, because there was no unused value for multixid that we could use. This patch expands that to all the options.

- Heikki
<0001-pg_resetwal-Reject-negative-and-out-of-range-argumen.patch><0002-pg_resetwal-Use-separate-flags-for-whether-an-option.patch>

1 - 0002 - correctness issue
```
-	if (set_oid != 0)
-		ControlFile.checkPointCopy.nextOid = set_oid;
+	if (next_oid_given)
+		ControlFile.checkPointCopy.nextOid = next_oid_val;
```

As OID 0 is invalid, the old code checks that. But the new code checks only next_oid_given, which loses the validation of invalid OID 0.

This issue applies to multiple parameters.

2 - 0001
```
+/*
+ * strtouint32_strict -- like strtoul(), but returns uint32 and doesn't accept
+ * negative values
+ */
+static uint32
+strtouint32_strict(const char *restrict s, char **restrict endptr, int base)
+{
+	unsigned long val;
+	bool		is_neg;
+
+	/* skip leading whitespace */
+	while (isspace(*s))
+		s++;
+
+	/*
+	 * Is it negative?  We still call strtoul() if it was, to set 'endptr'.
+	 * (The current callers don't care though.)
+	 */
+	is_neg = (*s == '-');
+
+	val = strtoul(s, endptr, base);
+
+	/* reject if it was negative */
+	if (errno == 0 && is_neg)
+	{
+		errno = ERANGE;
+		val = 0;
+	}
+
+	/*
+	 * reject values larger than UINT32_MAX on platforms where long is 64 bits
+	 * wide.
+	 */
+	if (errno == 0 && val != (uint32) val)
+	{
+		errno = ERANGE;
+		val = UINT32_MAX;
+	}
+
+	return (uint32) val;
+}
```

I guess this function doesn’t have to check “-“ by itself, it leads some edge-case not to be well handled, for example “-0” is still 0, not a negative value. We can use strtoll() convert input string to a singed long long, and check if value is negative.

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

#3Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Chao Li (#2)
Re: Tighten up range checks for pg_resetwal arguments

On 04/12/2025 03:08, Chao Li wrote:

Hi Heikki,

This patch looks like a straightforward change, but I see a correctness issue and a few small comments.

On Dec 4, 2025, at 03:07, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

While working on the 64-bit multixid offsets patch and commit 94939c5f3a, I got a little annoyed by how lax pg_resetwal is about out-of-range values. These are currently accepted, for example:

# Negative XID
pg_resetwal -D data -x -1000

# XID larger than 2^32 (on some platforms)
pg_resetwal -D data -x 10000000000

The first attached patch tightens up the parsing to reject those.

The second attached patch is just refactoring. Currently, we use invalid values for the variables backing each of the options to mean "option was not given". I think it would be more clear to have separate boolean variables for that. I did that for the --multixact-ids option in commit f99e30149f already, because there was no unused value for multixid that we could use. This patch expands that to all the options.

- Heikki
<0001-pg_resetwal-Reject-negative-and-out-of-range-argumen.patch><0002-pg_resetwal-Use-separate-flags-for-whether-an-option.patch>

1 - 0002 - correctness issue
```
-	if (set_oid != 0)
-		ControlFile.checkPointCopy.nextOid = set_oid;
+	if (next_oid_given)
+		ControlFile.checkPointCopy.nextOid = next_oid_val;
```

As OID 0 is invalid, the old code checks that. But the new code checks only next_oid_given, which loses the validation of invalid OID 0.

This issue applies to multiple parameters.

There's this check earlier:

case 'o':
errno = 0;
next_oid_val = strtouint32_strict(optarg, &endptr, 0);
if (endptr == optarg || *endptr != '\0' || errno != 0)
{
pg_log_error("invalid argument for option %s", "-o");
pg_log_error_hint("Try \"%s --help\" for more information.", progname);
exit(1);
}
if (next_oid_val == 0)
pg_fatal("OID (-o) must not be 0");
next_oid_given = true;
break;

That's covered by the tap test too.

2 - 0001
```
+/*
+ * strtouint32_strict -- like strtoul(), but returns uint32 and doesn't accept
+ * negative values
+ */
+static uint32
+strtouint32_strict(const char *restrict s, char **restrict endptr, int base)
+{
+	unsigned long val;
+	bool		is_neg;
+
+	/* skip leading whitespace */
+	while (isspace(*s))
+		s++;
+
+	/*
+	 * Is it negative?  We still call strtoul() if it was, to set 'endptr'.
+	 * (The current callers don't care though.)
+	 */
+	is_neg = (*s == '-');
+
+	val = strtoul(s, endptr, base);
+
+	/* reject if it was negative */
+	if (errno == 0 && is_neg)
+	{
+		errno = ERANGE;
+		val = 0;
+	}
+
+	/*
+	 * reject values larger than UINT32_MAX on platforms where long is 64 bits
+	 * wide.
+	 */
+	if (errno == 0 && val != (uint32) val)
+	{
+		errno = ERANGE;
+		val = UINT32_MAX;
+	}
+
+	return (uint32) val;
+}
```

I guess this function doesn’t have to check “-“ by itself, it leads some edge-case not to be well handled, for example “-0” is still 0, not a negative value. We can use strtoll() convert input string to a singed long long, and check if value is negative.

True. I originally wrote this for the 64-bit variant which will be used
in the 64-bit offsets patch. For that we can't use strtoll().

Thanks for the review!

- Heikki

#4Andrey Borodin
x4mmm@yandex-team.ru
In reply to: Heikki Linnakangas (#1)
Re: Tighten up range checks for pg_resetwal arguments

On 4 Dec 2025, at 00:07, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I got a little annoyed by how lax pg_resetwal is about out-of-range values.

A little bit offtopic, but anyway.

It's kind of common practice for many tools.
We actually had a corruption after pg_upgrade inflicted by the bug in upgrade script.
Here's the bugfix:
        exe(
            '/usr/bin/timeout 300 '
-            'sudo -u postgres /usr/lib/postgresql/{version_to}/bin/vacuumdb  --port {port}'
+            'sudo -u postgres /usr/lib/postgresql/{version_to}/bin/vacuumdb  --port {port} '
            '--analyze-in-stages --all -j 8',
            context={'port': 7432},
            allow_fail=True,
        )

Absence of space was ignored by vacuumdb. Executed command:

vacuumdb --port 7432--analyze-in-stages

was expected to analyze only, but made a vacuum. That was not rsynced later.

So +1 from me on the strict parsing of arguments by sharp tools like pg_resetwal.

Best regards, Andrey Borodin.

#5Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Heikki Linnakangas (#3)
Re: Tighten up range checks for pg_resetwal arguments

On 05/12/2025 20:09, Heikki Linnakangas wrote:

On 04/12/2025 03:08, Chao Li wrote:

I guess this function doesn’t have to check “-“ by itself, it leads
some edge-case not to be well handled, for example “-0” is still 0,
not a negative value. We can use strtoll() convert input string to a
singed long long, and check if value is negative.

True. I originally wrote this for the 64-bit variant which will be used
in the 64-bit offsets patch. For that we can't use strtoll().

I think it's best to reject the "-0" case, so I kept the code so that
it's rejected, and added a test for that.

Committed, thanks for the review!

- Heikki