From 2fc0352fd10d1542a87fc3d2fb0777d1120a78c9 Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Tue, 11 Aug 2026 07:44:40 +0000
Subject: [PATCH v2] Reject out-of-range octets in macaddr input

macaddr_in() parsed the colon- and dash-separated notations with
unbounded sscanf %x conversions.  A field with more than eight significant hex
digits overflows the int variable, which the C standard leaves
undefined; in practice the value wrapped, so the later 0..255 range
check tested the wrapped result and passed.  '100000001:0:0:0:0:0'
was therefore accepted and stored as 01:00:00:00:00:00, an address
other than the one given, and whether an overlong field was rejected
at all depended on where the wrapped value happened to land.

Parse the fields of those two notations with strtol() instead, which
handles an out-of-range value in a defined way on every platform: it
yields the exact value whenever that fits in a long, and LONG_MIN or
LONG_MAX otherwise.  Either way the existing octet range check
rejects the field.  This follows what from_char_parse_int_len() in
formatting.c does, parsing with strtol() and range-checking the
result.

Because %x is defined in terms of strtoul()'s subject sequence,
strtol() accepts the same field syntax, so every input that is
accepted today and stores the correct value keeps working.  That
includes forms which are not documented but have been accepted since
the type was added in 1998: zero-padded fields such as
'001:00:2b:01:02:03', 0x-prefixed fields, signed fields, single-digit
fields, and whitespace after a separator.  Both error messages stay
as they are.

The one exception is a bare '0x' field with no hex digit after it:
glibc's sscanf read it as zero, strtol() stops at the '0', and that
acceptance already varied by platform.  The colon and dash notations
therefore no longer accept such fields.  What happens to an input
containing one is then up to the unchanged condensed templates: most
such inputs draw the syntax error; on glibc, a few dash-separated
ones still match a condensed template and either draw the octet
error or stay accepted with the same stored value; and ones that
used to fail the octet check draw the syntax error now.

The five condensed notations are left alone, as their %2x
conversions cannot overflow.

Bug: #19583
Reported-by: Michael Malis <malis@pgrust.com>
Discussion: https://postgr.es/m/19583-ca7c85d40164f18b@postgresql.org
---
 src/backend/utils/adt/mac.c           | 120 +++++++++++++++++++-------
 src/test/regress/expected/macaddr.out |  75 ++++++++++++++++
 src/test/regress/sql/macaddr.sql      |  20 +++++
 3 files changed, 182 insertions(+), 33 deletions(-)

diff --git a/src/backend/utils/adt/mac.c b/src/backend/utils/adt/mac.c
index 3edaac8bb..65018658d 100644
--- a/src/backend/utils/adt/mac.c
+++ b/src/backend/utils/adt/mac.c
@@ -35,6 +35,51 @@ static int	macaddr_cmp_internal(macaddr *a1, macaddr *a2);
 static int	macaddr_fast_cmp(Datum x, Datum y, SortSupport ssup);
 static bool macaddr_abbrev_abort(int memtupcount, SortSupport ssup);
 static Datum macaddr_abbrev_convert(Datum original, SortSupport ssup);
+static bool macaddr_parse_octets(const char *str, char separator, long *octets);
+
+/*
+ *	Parse the six octets of one of the separator-based notations.
+ *
+ *	strtol() accepts the same field syntax as the %x conversions this
+ *	replaces, but has defined behavior when a field is out of range: it
+ *	yields the exact value whenever that fits in a long, and LONG_MIN or
+ *	LONG_MAX otherwise.  Either way the caller's 0..255 range check
+ *	rejects the field, so no errno test is needed here.  One divergence:
+ *	strtol() stops at the "0" of a bare "0x" with no hex digit after it,
+ *	which glibc's sscanf read as zero (that varied across platforms).
+ *
+ *	Returns true if str matched this notation, false if it did not, in
+ *	which case the caller falls through to the remaining notations.
+ */
+static bool
+macaddr_parse_octets(const char *str, char separator, long *octets)
+{
+	const char *ptr = str;
+	int			i;
+
+	for (i = 0; i < 6; i++)
+	{
+		char	   *endptr;
+
+		octets[i] = strtol(ptr, &endptr, 16);
+		if (endptr == ptr)
+			return false;		/* no digits where an octet was expected */
+		ptr = endptr;
+
+		if (i < 5)
+		{
+			if (*ptr != separator)
+				return false;
+			ptr++;
+		}
+	}
+
+	/* trailing whitespace is accepted, anything else is garbage */
+	while (isspace((unsigned char) *ptr))
+		ptr++;
+
+	return (*ptr == '\0');
+}
 
 /*
  *	MAC address reader.  Accepts several common notations.
@@ -46,6 +91,7 @@ macaddr_in(PG_FUNCTION_ARGS)
 	char	   *str = PG_GETARG_CSTRING(0);
 	Node	   *escontext = fcinfo->context;
 	macaddr    *result;
+	long		octets[6];
 	int			a,
 				b,
 				c,
@@ -55,49 +101,57 @@ macaddr_in(PG_FUNCTION_ARGS)
 	char		junk[2];
 	int			count;
 
-	/* %1s matches iff there is trailing non-whitespace garbage */
+	if (!macaddr_parse_octets(str, ':', octets) &&
+		!macaddr_parse_octets(str, '-', octets))
+	{
+		/* %1s matches iff there is trailing non-whitespace garbage */
 
-	count = sscanf(str, "%x:%x:%x:%x:%x:%x%1s",
-				   &a, &b, &c, &d, &e, &f, junk);
-	if (count != 6)
-		count = sscanf(str, "%x-%x-%x-%x-%x-%x%1s",
-					   &a, &b, &c, &d, &e, &f, junk);
-	if (count != 6)
 		count = sscanf(str, "%2x%2x%2x:%2x%2x%2x%1s",
 					   &a, &b, &c, &d, &e, &f, junk);
-	if (count != 6)
-		count = sscanf(str, "%2x%2x%2x-%2x%2x%2x%1s",
-					   &a, &b, &c, &d, &e, &f, junk);
-	if (count != 6)
-		count = sscanf(str, "%2x%2x.%2x%2x.%2x%2x%1s",
-					   &a, &b, &c, &d, &e, &f, junk);
-	if (count != 6)
-		count = sscanf(str, "%2x%2x-%2x%2x-%2x%2x%1s",
-					   &a, &b, &c, &d, &e, &f, junk);
-	if (count != 6)
-		count = sscanf(str, "%2x%2x%2x%2x%2x%2x%1s",
-					   &a, &b, &c, &d, &e, &f, junk);
-	if (count != 6)
-		ereturn(escontext, (Datum) 0,
-				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-				 errmsg("invalid input syntax for type %s: \"%s\"", "macaddr",
-						str)));
+		if (count != 6)
+			count = sscanf(str, "%2x%2x%2x-%2x%2x%2x%1s",
+						   &a, &b, &c, &d, &e, &f, junk);
+		if (count != 6)
+			count = sscanf(str, "%2x%2x.%2x%2x.%2x%2x%1s",
+						   &a, &b, &c, &d, &e, &f, junk);
+		if (count != 6)
+			count = sscanf(str, "%2x%2x-%2x%2x-%2x%2x%1s",
+						   &a, &b, &c, &d, &e, &f, junk);
+		if (count != 6)
+			count = sscanf(str, "%2x%2x%2x%2x%2x%2x%1s",
+						   &a, &b, &c, &d, &e, &f, junk);
+		if (count != 6)
+			ereturn(escontext, (Datum) 0,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("invalid input syntax for type %s: \"%s\"", "macaddr",
+							str)));
+
+		octets[0] = a;
+		octets[1] = b;
+		octets[2] = c;
+		octets[3] = d;
+		octets[4] = e;
+		octets[5] = f;
+	}
 
-	if ((a < 0) || (a > 255) || (b < 0) || (b > 255) ||
-		(c < 0) || (c > 255) || (d < 0) || (d > 255) ||
-		(e < 0) || (e > 255) || (f < 0) || (f > 255))
+	if ((octets[0] < 0) || (octets[0] > 255) ||
+		(octets[1] < 0) || (octets[1] > 255) ||
+		(octets[2] < 0) || (octets[2] > 255) ||
+		(octets[3] < 0) || (octets[3] > 255) ||
+		(octets[4] < 0) || (octets[4] > 255) ||
+		(octets[5] < 0) || (octets[5] > 255))
 		ereturn(escontext, (Datum) 0,
 				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
 				 errmsg("invalid octet value in \"macaddr\" value: \"%s\"", str)));
 
 	result = palloc_object(macaddr);
 
-	result->a = a;
-	result->b = b;
-	result->c = c;
-	result->d = d;
-	result->e = e;
-	result->f = f;
+	result->a = octets[0];
+	result->b = octets[1];
+	result->c = octets[2];
+	result->d = octets[3];
+	result->e = octets[4];
+	result->f = octets[5];
 
 	PG_RETURN_MACADDR_P(result);
 }
diff --git a/src/test/regress/expected/macaddr.out b/src/test/regress/expected/macaddr.out
index 8d5b22195..bd904e441 100644
--- a/src/test/regress/expected/macaddr.out
+++ b/src/test/regress/expected/macaddr.out
@@ -17,6 +17,68 @@ INSERT INTO macaddr_data VALUES (9, 'not even close'); -- invalid
 ERROR:  invalid input syntax for type macaddr: "not even close"
 LINE 1: INSERT INTO macaddr_data VALUES (9, 'not even close');
                                             ^
+-- Overlong hex fields must be rejected, not silently wrapped (bug #19583)
+SELECT '100000001:0:0:0:0:0'::macaddr;
+ERROR:  invalid octet value in "macaddr" value: "100000001:0:0:0:0:0"
+LINE 1: SELECT '100000001:0:0:0:0:0'::macaddr;
+               ^
+SELECT '100000001-0-0-0-0-0'::macaddr;
+ERROR:  invalid octet value in "macaddr" value: "100000001-0-0-0-0-0"
+LINE 1: SELECT '100000001-0-0-0-0-0'::macaddr;
+               ^
+SELECT '-ffffff01:0:0:0:0:0'::macaddr;
+ERROR:  invalid octet value in "macaddr" value: "-ffffff01:0:0:0:0:0"
+LINE 1: SELECT '-ffffff01:0:0:0:0:0'::macaddr;
+               ^
+SELECT 'ffffffffffffffffff:0:0:0:0:0'::macaddr;  -- wider than any long
+ERROR:  invalid octet value in "macaddr" value: "ffffffffffffffffff:0:0:0:0:0"
+LINE 1: SELECT 'ffffffffffffffffff:0:0:0:0:0'::macaddr;
+               ^
+-- while the leniency inherited from sscanf's %x conversion is preserved
+SELECT '001:00:2b:01:02:03'::macaddr;
+      macaddr      
+-------------------
+ 01:00:2b:01:02:03
+(1 row)
+
+SELECT '0x1:00:2b:01:02:03'::macaddr;
+      macaddr      
+-------------------
+ 01:00:2b:01:02:03
+(1 row)
+
+SELECT '+f:00:2b:01:02:03'::macaddr;
+      macaddr      
+-------------------
+ 0f:00:2b:01:02:03
+(1 row)
+
+SELECT 'aa: bb:cc:dd:ee:ff'::macaddr;
+      macaddr      
+-------------------
+ aa:bb:cc:dd:ee:ff
+(1 row)
+
+SELECT '08:00:2b:01:02:03  '::macaddr;
+      macaddr      
+-------------------
+ 08:00:2b:01:02:03
+(1 row)
+
+-- but a bare '0x' field with no hex digit is not valid
+SELECT '0x:00:2b:01:02:03'::macaddr;
+ERROR:  invalid input syntax for type macaddr: "0x:00:2b:01:02:03"
+LINE 1: SELECT '0x:00:2b:01:02:03'::macaddr;
+               ^
+-- an octet that fits the field width but is out of range is still caught
+SELECT '1ff:0:0:0:0:0'::macaddr;
+ERROR:  invalid octet value in "macaddr" value: "1ff:0:0:0:0:0"
+LINE 1: SELECT '1ff:0:0:0:0:0'::macaddr;
+               ^
+SELECT '-f:0:0:0:0:0'::macaddr;
+ERROR:  invalid octet value in "macaddr" value: "-f:0:0:0:0:0"
+LINE 1: SELECT '-f:0:0:0:0:0'::macaddr;
+               ^
 INSERT INTO macaddr_data VALUES (10, '08:00:2b:01:02:04');
 INSERT INTO macaddr_data VALUES (11, '08:00:2b:01:02:02');
 INSERT INTO macaddr_data VALUES (12, '08:00:2a:01:02:03');
@@ -183,3 +245,16 @@ SELECT * FROM pg_input_error_info('08:00:2b:01:02:', 'macaddr');
  invalid input syntax for type macaddr: "08:00:2b:01:02:" |        |      | 22P02
 (1 row)
 
+-- overlong hex field (bug #19583)
+SELECT pg_input_is_valid('100000001:0:0:0:0:0', 'macaddr');
+ pg_input_is_valid 
+-------------------
+ f
+(1 row)
+
+SELECT * FROM pg_input_error_info('100000001:0:0:0:0:0', 'macaddr');
+                            message                            | detail | hint | sql_error_code 
+---------------------------------------------------------------+--------+------+----------------
+ invalid octet value in "macaddr" value: "100000001:0:0:0:0:0" |        |      | 22003
+(1 row)
+
diff --git a/src/test/regress/sql/macaddr.sql b/src/test/regress/sql/macaddr.sql
index f61bdebe8..305a2b751 100644
--- a/src/test/regress/sql/macaddr.sql
+++ b/src/test/regress/sql/macaddr.sql
@@ -14,6 +14,23 @@ INSERT INTO macaddr_data VALUES (7, '08002b010203');
 INSERT INTO macaddr_data VALUES (8, '0800:2b01:0203'); -- invalid
 INSERT INTO macaddr_data VALUES (9, 'not even close'); -- invalid
 
+-- Overlong hex fields must be rejected, not silently wrapped (bug #19583)
+SELECT '100000001:0:0:0:0:0'::macaddr;
+SELECT '100000001-0-0-0-0-0'::macaddr;
+SELECT '-ffffff01:0:0:0:0:0'::macaddr;
+SELECT 'ffffffffffffffffff:0:0:0:0:0'::macaddr;  -- wider than any long
+-- while the leniency inherited from sscanf's %x conversion is preserved
+SELECT '001:00:2b:01:02:03'::macaddr;
+SELECT '0x1:00:2b:01:02:03'::macaddr;
+SELECT '+f:00:2b:01:02:03'::macaddr;
+SELECT 'aa: bb:cc:dd:ee:ff'::macaddr;
+SELECT '08:00:2b:01:02:03  '::macaddr;
+-- but a bare '0x' field with no hex digit is not valid
+SELECT '0x:00:2b:01:02:03'::macaddr;
+-- an octet that fits the field width but is out of range is still caught
+SELECT '1ff:0:0:0:0:0'::macaddr;
+SELECT '-f:0:0:0:0:0'::macaddr;
+
 INSERT INTO macaddr_data VALUES (10, '08:00:2b:01:02:04');
 INSERT INTO macaddr_data VALUES (11, '08:00:2b:01:02:02');
 INSERT INTO macaddr_data VALUES (12, '08:00:2a:01:02:03');
@@ -47,3 +64,6 @@ SELECT pg_input_is_valid('08:00:2b:01:02:ZZ', 'macaddr');
 SELECT * FROM pg_input_error_info('08:00:2b:01:02:ZZ', 'macaddr');
 SELECT pg_input_is_valid('08:00:2b:01:02:', 'macaddr');
 SELECT * FROM pg_input_error_info('08:00:2b:01:02:', 'macaddr');
+-- overlong hex field (bug #19583)
+SELECT pg_input_is_valid('100000001:0:0:0:0:0', 'macaddr');
+SELECT * FROM pg_input_error_info('100000001:0:0:0:0:0', 'macaddr');
-- 
2.34.1

