From b96af4a71b647323f15a3880da02698ed275211d Mon Sep 17 00:00:00 2001
From: Tristan Partin <tristan@partin.io>
Date: Tue, 29 Sep 2026 01:59:18 +0000
Subject: [PATCH v1] Fix out-of-bounds array indexing in JsonValueList

Commit 5a2043bf713 made JsonValueList a chunked list whose first chunk
lives on the caller's stack. All chunks shared one struct type, whose
trailing array was declared as items[BASE_JVL_ITEMS]. Extra chunks were
allocated with room for more entries and code indexed past the declared
bound of 2.

The allocations were always large enough, so no memory was ever overrun.
But C11 6.5.6p8 makes pointer arithmetic that does more than one element
past the end of an array undefined behavior, and Annex J.2 confirms this
holds "even if an object is apparently accessible with the given
subscript".

With -fsanitize=bounds, the following query

	SELECT jsonb_path_query('[1,2,3,4,5,6]', '$[*]');

logs errors from JsonValueListAppend() and JsonValueListNext():

	jsonpath_exec.c:3832:3: runtime error: index 3 out of bounds for type 'JsonbValue[2]'
	jsonpath_exec.c:3918:10: runtime error: index 3 out of bounds for type 'JsonbValue[2]'

Fix this by separating JsonValueList into two types: JsonValueList and
JsonValueListChunk. JsonValueList continues to hold the first
BASE_JVL_ITEMS entries in a base[BASE_JVL_ITEMS] array. Subsequent
entries are stored in JsonValueListChunks, whose items array is a true
flexible array member.

Author: Tristan Partin <tristan@partin.io>
Signed-off-by: Tristan Partin <tristan@partin.io>
---
 src/backend/utils/adt/jsonpath_exec.c | 109 ++++++++++++++++----------
 src/tools/pgindent/typedefs.list      |   1 +
 2 files changed, 70 insertions(+), 40 deletions(-)

diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index 2191d67bfc0..e6b4b974690 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -145,13 +145,13 @@ typedef enum JsonPathExecResult
  * List (or really array) of JsonbValues.  This is the output representation
  * of jsonpath evaluation.
  *
- * The initial or "base" chunk of a list is typically a local variable in
- * a calling function.  If we need more entries than will fit in the base
- * chunk, we palloc more chunks.  For notational simplicity, those are also
- * treated as being of type JsonValueList, although they will have items[]
- * arrays that are larger than BASE_JVL_ITEMS.
+ * A JsonValueList is typically a local variable in a calling function.  It
+ * holds up to BASE_JVL_ITEMS entries in its base[] array.  If we need more
+ * entries than will fit there, we palloc extra chunks, of type
+ * JsonValueListChunk, and link them into a list.  Extra chunks are only
+ * created once base[] is full.
  *
- * Callers *must* initialize the base chunk with JsonValueListInit().
+ * Callers *must* initialize the list with JsonValueListInit().
  * Typically they should free any extra chunks when done, using
  * JsonValueListClear(), although some top-level functions skip that
  * on the assumption that the caller's context will be reset soon.
@@ -165,23 +165,31 @@ typedef enum JsonPathExecResult
  * BASE_JVL_ITEMS small to conserve stack space, but grow the extra
  * chunks aggressively.
  */
-#define BASE_JVL_ITEMS 2		/* number of items a base chunk holds */
+#define BASE_JVL_ITEMS 2		/* number of items base[] holds */
 #define MIN_EXTRA_JVL_ITEMS 16	/* min number of items an extra chunk holds */
 
-typedef struct JsonValueList
+typedef struct JsonValueListChunk
 {
 	int			nitems;			/* number of items stored in this chunk */
 	int			maxitems;		/* allocated length of items[] */
-	struct JsonValueList *next; /* => next chunk, if any */
-	struct JsonValueList *last; /* => last chunk (only valid in base chunk) */
-	JsonbValue	items[BASE_JVL_ITEMS];
+	struct JsonValueListChunk *next;	/* => next chunk, if any */
+	JsonbValue	items[FLEXIBLE_ARRAY_MEMBER];
+} JsonValueListChunk;
+
+typedef struct JsonValueList
+{
+	int			nbase;			/* number of items stored in base[] */
+	JsonValueListChunk *next;	/* => first extra chunk, if any */
+	JsonValueListChunk *last;	/* => last extra chunk, if any */
+	JsonbValue	base[BASE_JVL_ITEMS];	/* storage for the first items */
 } JsonValueList;
 
 /* State data for iterating through a JsonValueList */
 typedef struct JsonValueListIterator
 {
-	JsonValueList *chunk;		/* current chunk of list */
-	int			nextitem;		/* index of next value to return in chunk */
+	JsonValueList *list;		/* list being iterated, or NULL when done */
+	JsonValueListChunk *chunk;	/* current extra chunk, or NULL for base[] */
+	int			nextitem;		/* index of next value to return */
 } JsonValueListIterator;
 
 /* Structures for JSON_TABLE execution  */
@@ -3797,36 +3805,41 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id)
 static void
 JsonValueListInit(JsonValueList *jvl)
 {
-	jvl->nitems = 0;
-	jvl->maxitems = BASE_JVL_ITEMS;
+	jvl->nbase = 0;
 	jvl->next = NULL;
-	jvl->last = jvl;
+	jvl->last = NULL;
 }
 
 static void
 JsonValueListClear(JsonValueList *jvl)
 {
-	JsonValueList *nxt;
+	JsonValueListChunk *nxt;
 
 	/* Release any extra chunks */
-	for (JsonValueList *chunk = jvl->next; chunk != NULL; chunk = nxt)
+	for (JsonValueListChunk *chunk = jvl->next; chunk != NULL; chunk = nxt)
 	{
 		nxt = chunk->next;
 		pfree(chunk);
 	}
 	/* ... and reset to empty */
-	jvl->nitems = 0;
-	Assert(jvl->maxitems == BASE_JVL_ITEMS);
+	jvl->nbase = 0;
 	jvl->next = NULL;
-	jvl->last = jvl;
+	jvl->last = NULL;
 }
 
 static void
 JsonValueListAppend(JsonValueList *jvl, const JsonbValue *jbv)
 {
-	JsonValueList *last = jvl->last;
+	JsonValueListChunk *last = jvl->last;
 
-	if (last->nitems < last->maxitems)
+	if (jvl->nbase < BASE_JVL_ITEMS)
+	{
+		/* there's still room in base[] */
+		Assert(last == NULL);
+		jvl->base[jvl->nbase] = *jbv;
+		jvl->nbase++;
+	}
+	else if (last != NULL && last->nitems < last->maxitems)
 	{
 		/* there's still room in the last existing chunk */
 		last->items[last->nitems] = *jbv;
@@ -3835,18 +3848,22 @@ JsonValueListAppend(JsonValueList *jvl, const JsonbValue *jbv)
 	else
 	{
 		/* need a new last chunk */
-		JsonValueList *nxt;
+		JsonValueListChunk *nxt;
 		int			nxtsize;
 
-		nxtsize = last->maxitems * 2;	/* double the size with each chunk */
+		/* double the size with each chunk */
+		nxtsize = (last != NULL ? last->maxitems : BASE_JVL_ITEMS) * 2;
 		nxtsize = Max(nxtsize, MIN_EXTRA_JVL_ITEMS);	/* but at least this */
-		nxt = palloc(offsetof(JsonValueList, items) +
+		nxt = palloc(offsetof(JsonValueListChunk, items) +
 					 nxtsize * sizeof(JsonbValue));
 		nxt->nitems = 1;
 		nxt->maxitems = nxtsize;
 		nxt->next = NULL;
 		nxt->items[0] = *jbv;
-		last->next = nxt;
+		if (last != NULL)
+			last->next = nxt;
+		else
+			jvl->next = nxt;
 		jvl->last = nxt;
 	}
 }
@@ -3855,7 +3872,7 @@ static bool
 JsonValueListIsEmpty(const JsonValueList *jvl)
 {
 	/* We need not examine extra chunks for this */
-	return (jvl->nitems == 0);
+	return (jvl->nbase == 0);
 }
 
 static bool
@@ -3863,9 +3880,9 @@ JsonValueListIsSingleton(const JsonValueList *jvl)
 {
 #if BASE_JVL_ITEMS > 1
 	/* We need not examine extra chunks in this case */
-	return (jvl->nitems == 1);
+	return (jvl->nbase == 1);
 #else
-	return (jvl->nitems == 1 && jvl->next == NULL);
+	return (jvl->nbase == 1 && jvl->next == NULL);
 #endif
 }
 
@@ -3874,17 +3891,17 @@ JsonValueListHasMultipleItems(const JsonValueList *jvl)
 {
 #if BASE_JVL_ITEMS > 1
 	/* We need not examine extra chunks in this case */
-	return (jvl->nitems > 1);
+	return (jvl->nbase > 1);
 #else
-	return (jvl->nitems == 1 && jvl->next != NULL);
+	return (jvl->nbase == 1 && jvl->next != NULL);
 #endif
 }
 
 static JsonbValue *
 JsonValueListHead(JsonValueList *jvl)
 {
-	Assert(jvl->nitems > 0);
-	return &jvl->items[0];
+	Assert(jvl->nbase > 0);
+	return &jvl->base[0];
 }
 
 /*
@@ -3894,7 +3911,8 @@ JsonValueListHead(JsonValueList *jvl)
 static void
 JsonValueListInitIterator(JsonValueList *jvl, JsonValueListIterator *it)
 {
-	it->chunk = jvl;
+	it->list = jvl;
+	it->chunk = NULL;
 	it->nextitem = 0;
 }
 
@@ -3905,16 +3923,27 @@ JsonValueListInitIterator(JsonValueList *jvl, JsonValueListIterator *it)
 static JsonbValue *
 JsonValueListNext(JsonValueListIterator *it)
 {
-	if (it->chunk == NULL)
+	if (it->list == NULL)
 		return NULL;
-	if (it->nextitem >= it->chunk->nitems)
+	if (it->chunk == NULL)
+	{
+		/* return items from base[] first */
+		if (it->nextitem < it->list->nbase)
+			return &it->list->base[it->nextitem++];
+		it->chunk = it->list->next;
+		it->nextitem = 0;
+	}
+	else if (it->nextitem >= it->chunk->nitems)
 	{
 		it->chunk = it->chunk->next;
-		if (it->chunk == NULL)
-			return NULL;
 		it->nextitem = 0;
-		Assert(it->chunk->nitems > 0);
 	}
+	if (it->chunk == NULL)
+	{
+		it->list = NULL;
+		return NULL;
+	}
+	Assert(it->nextitem < it->chunk->nitems);
 	return &it->chunk->items[it->nextitem++];
 }
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 656f1f60862..c9a228551d9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1500,6 +1500,7 @@ JsonUniqueParsingState
 JsonUniqueStackEntry
 JsonValueExpr
 JsonValueList
+JsonValueListChunk
 JsonValueListIterator
 JsonValueType
 JsonWrapper
-- 
Tristan Partin
https://tristan.partin.io

