From 60c2405ea935ed58b1ed1f2031400a374333748a Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Sat, 5 Sep 2026 21:42:26 +0000
Subject: [PATCH 2/2] test_bitmapset: Decode arguments with readBitmapset()

The SQL wrappers of this module decoded their text arguments with the
generic stringToNode(), casting whatever came out to a Bitmapset.
stringToNode() assumes its input to be valid: the read routines of
readfuncs.c fetch tokens with pg_strtok() and feed them to atoi()
without checking for NULL, so any other node type name, like '{QUERY}',
crashes the backend on the first missing field.  Strings for other
node types that do read fine, like '42' or '(i 1 2)', were then
misinterpreted as a Bitmapset.  The functions are executable by any
user, so this was reachable without any privileges.

To fix, decode with readBitmapset(), the reader used where a Bitmapset
is known to be expected, which checks every token it reads and rejects
anything else.  "<>", the representation of an empty set written by
nodeToString(), is still accepted.

Oversight in commit 00c3d87a5ca.
---
 .../expected/test_bitmapset.out               | 29 ++++++++++++++++++
 .../test_bitmapset/sql/test_bitmapset.sql     | 13 ++++++++
 .../modules/test_bitmapset/test_bitmapset.c   | 30 +++++++++++++++++--
 3 files changed, 69 insertions(+), 3 deletions(-)

diff --git a/src/test/modules/test_bitmapset/expected/test_bitmapset.out b/src/test/modules/test_bitmapset/expected/test_bitmapset.out
index f8f5cf5e7f0..ab98cbd448b 100644
--- a/src/test/modules/test_bitmapset/expected/test_bitmapset.out
+++ b/src/test/modules/test_bitmapset/expected/test_bitmapset.out
@@ -1656,4 +1656,33 @@ SELECT test_random_offset_operations(NULL, 1000, 0, 1024) AS result;
    1000
 (1 row)
 
+-- malformed inputs, rejected before reaching the read routines of other
+-- node types
+SELECT test_bms_num_members('{QUERY}'); -- error
+ERROR:  unrecognized token: "{"
+SELECT test_bms_copy('{VAR}'); -- error
+ERROR:  unrecognized token: "{"
+SELECT test_bms_num_members('42'); -- error
+ERROR:  unrecognized token: "42"
+SELECT test_bms_num_members('(i 1 2)'); -- error
+ERROR:  unrecognized token: "i"
+SELECT test_bms_num_members('(b 1'); -- error
+ERROR:  unterminated Bitmapset structure
+SELECT test_bms_num_members('(b x)'); -- error
+ERROR:  unrecognized integer: "x"
+SELECT test_bms_num_members(''); -- error
+ERROR:  incomplete Bitmapset structure
+-- empty set, as written by nodeToString()
+SELECT test_bms_num_members('<>') AS result;
+ result 
+--------
+      0
+(1 row)
+
+SELECT test_bms_copy('<>') AS result;
+ result 
+--------
+ <>
+(1 row)
+
 DROP EXTENSION test_bitmapset;
diff --git a/src/test/modules/test_bitmapset/sql/test_bitmapset.sql b/src/test/modules/test_bitmapset/sql/test_bitmapset.sql
index d44cda114a4..4f3cb7bfc6d 100644
--- a/src/test/modules/test_bitmapset/sql/test_bitmapset.sql
+++ b/src/test/modules/test_bitmapset/sql/test_bitmapset.sql
@@ -426,4 +426,17 @@ SELECT test_random_operations(NULL, 10000, 0, 81920) > 0 AS result;
 -- perform some random tests on bms_offset_members()
 SELECT test_random_offset_operations(NULL, 1000, 0, 1024) AS result;
 
+-- malformed inputs, rejected before reaching the read routines of other
+-- node types
+SELECT test_bms_num_members('{QUERY}'); -- error
+SELECT test_bms_copy('{VAR}'); -- error
+SELECT test_bms_num_members('42'); -- error
+SELECT test_bms_num_members('(i 1 2)'); -- error
+SELECT test_bms_num_members('(b 1'); -- error
+SELECT test_bms_num_members('(b x)'); -- error
+SELECT test_bms_num_members(''); -- error
+-- empty set, as written by nodeToString()
+SELECT test_bms_num_members('<>') AS result;
+SELECT test_bms_copy('<>') AS result;
+
 DROP EXTENSION test_bitmapset;
diff --git a/src/test/modules/test_bitmapset/test_bitmapset.c b/src/test/modules/test_bitmapset/test_bitmapset.c
index af8c664949e..9123c5efd7a 100644
--- a/src/test/modules/test_bitmapset/test_bitmapset.c
+++ b/src/test/modules/test_bitmapset/test_bitmapset.c
@@ -24,6 +24,7 @@
 #include "nodes/bitmapset.h"
 #include "nodes/nodes.h"
 #include "nodes/pg_list.h"
+#include "nodes/readfuncs.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/timestamp.h"
@@ -86,16 +87,39 @@ PG_FUNCTION_INFO_V1(test_random_offset_operations);
 				 #expr, __FILE__, __LINE__); \
 	} while (0)
 
-/* Encode/Decode to/from TEXT and Bitmapset */
+/* Encode a Bitmapset into its serialized representation */
 #define BITMAPSET_TO_TEXT(bms) cstring_to_text(nodeToString(bms))
-#define TEXT_TO_BITMAPSET(str) ((Bitmapset *) stringToNode(text_to_cstring(str)))
+
+/*
+ * Decode a Bitmapset from its serialized representation, either "<>" for an
+ * empty set or "(b member ...)".
+ *
+ * This goes through readBitmapset(), the reader for contexts where a
+ * Bitmapset is known to be expected, rather than through the generic
+ * stringToNode().  The latter assumes its input to be valid, and the read
+ * routines of readfuncs.c it dispatches to for other node types do not check
+ * for missing tokens, so a string naming any of those could crash the
+ * backend.  readBitmapset() checks everything it reads.
+ */
+static Bitmapset *
+text_to_bitmapset(text *txt)
+{
+	char	   *str = text_to_cstring(txt);
+	ReadNodeContext ctx = {.str = str};
+
+	/* nodeToString() writes an empty set as "<>" */
+	if (strcmp(str, "<>") == 0)
+		return NULL;
+
+	return readBitmapset(&ctx);
+}
 
 /*
  * Helper macro to fetch text parameters as Bitmapsets. SQL-NULL means empty
  * set.
  */
 #define PG_ARG_GETBITMAPSET(n) \
-	(PG_ARGISNULL(n) ? NULL : TEXT_TO_BITMAPSET(PG_GETARG_TEXT_PP(n)))
+	(PG_ARGISNULL(n) ? NULL : text_to_bitmapset(PG_GETARG_TEXT_PP(n)))
 
 /*
  * Helper macro to handle converting sets back to text, returning the
-- 
2.55.0

