From 100fd38aec4c045f2b33b03ea62d17be93e6e920 Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Thu, 30 Jul 2026 08:26:26 +0000 Subject: [PATCH v1 3/5] Fix empty proto_version integer parsing in pgoutput strtoul() can set two different values for errno: EINVAL and ERANGE. Quoting the man page for EINVAL: > The value of base is not supported or no conversion could be performed > (the last feature is not portable across all platforms). Emphasis on the portion in parentheses. Take the following example: strtoul("", &endptr, 10); On glibc, the output value is 0, errno *is not* set, and endptr points to the empty string. On Apple libc, the output value is 0, errno *is* set to EINVAL, and endptr points to the empty string. In this case, when we try to parse an empty string, glibc treats that as a protocol version of 0, while Apple libc treats that as an invalid protocol version. Signed-off-by: Tristan Partin --- src/backend/replication/pgoutput/pgoutput.c | 5 ++-- src/test/subscription/t/100_bugs.pl | 31 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c index 4ecfcbff7ab..0afdb1432ca 100644 --- a/src/backend/replication/pgoutput/pgoutput.c +++ b/src/backend/replication/pgoutput/pgoutput.c @@ -316,6 +316,7 @@ parse_output_parameters(List *options, PGOutputData *data) { unsigned long parsed; char *endptr; + const char *val = strVal(defel->arg); if (protocol_version_given) ereport(ERROR, @@ -324,8 +325,8 @@ parse_output_parameters(List *options, PGOutputData *data) protocol_version_given = true; errno = 0; - parsed = strtoul(strVal(defel->arg), &endptr, 10); - if (errno != 0 || *endptr != '\0') + parsed = strtoul(val, &endptr, 10); + if (endptr == val || errno != 0 || *endptr != '\0') ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid proto_version"))); diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 075c52f98fd..d9045e4d871 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -650,6 +650,37 @@ BEGIN "DROP PUBLICATION pub_rowfilter_error"); $node_publisher->safe_psql('postgres', "DROP TABLE tab_upsert"); +# Verify that an empty proto_version option is rejected as invalid +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE tab_proto_version (a INT PRIMARY KEY); + CREATE PUBLICATION pub_proto_version FOR TABLE tab_proto_version; + SELECT * FROM pg_create_logical_replication_slot('proto_version_slot', 'pgoutput'); +)); + +($ret, $stdout, $stderr) = $node_publisher->psql( + 'postgres', qq( + SELECT * + FROM pg_logical_slot_peek_binary_changes( + 'proto_version_slot', + NULL, + NULL, + 'proto_version', '', + 'publication_names', 'pub_proto_version' + ); +)); + +ok($stderr =~ qr/invalid proto_version/, + 'empty proto_version is rejected as invalid' +); + +# Clean up +$node_publisher->safe_psql('postgres', + "SELECT pg_drop_replication_slot('proto_version_slot')"); +$node_publisher->safe_psql('postgres', + "DROP PUBLICATION pub_proto_version"); +$node_publisher->safe_psql('postgres', "DROP TABLE tab_proto_version"); + $node_publisher->stop('fast'); done_testing(); -- Tristan Partin https://tristan.partin.io