From 49b5c015dcd05b9fd96bf6ef9320bacf4f4acd67 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Thu, 25 Jun 2026 16:30:50 +0900
Subject: [PATCH v1] plpython: Fix NULL pointer dereference for broken sequence
 objects

In several places PL/Python fetches the elements of a Python sequence
with PySequence_GetItem() and then uses the returned object without
first checking it for NULL.  This is done when converting a returned
sequence into a SQL array or composite value, when reading the
argument list passed to plpy.execute() or plpy.cursor(), and when
reading the list of type names given to plpy.prepare().  The
jsonb_plpython transform does the same while building a jsonb array.

PySequence_GetItem() returns NULL, with a Python exception set, when
an element cannot be retrieved, and the unchecked dereference that
follows crashes the backend.

Fix this by checking the result of PySequence_GetItem() in each of
these places and reporting a regular error if it is NULL, so that the
underlying Python exception is surfaced instead of taking down the
session.
---
 .../expected/jsonb_plpython.out               | 19 +++++++
 contrib/jsonb_plpython/jsonb_plpython.c       |  5 +-
 contrib/jsonb_plpython/sql/jsonb_plpython.sql | 16 ++++++
 .../plpython/expected/plpython_composite.out  | 16 ++++++
 src/pl/plpython/expected/plpython_spi.out     | 51 +++++++++++++++++++
 src/pl/plpython/expected/plpython_types.out   | 16 ++++++
 src/pl/plpython/plpy_cursorobject.c           |  5 ++
 src/pl/plpython/plpy_spi.c                    | 10 ++++
 src/pl/plpython/plpy_typeio.c                 |  9 +++-
 src/pl/plpython/sql/plpython_composite.sql    | 12 +++++
 src/pl/plpython/sql/plpython_spi.sql          | 39 ++++++++++++++
 src/pl/plpython/sql/plpython_types.sql        | 13 +++++
 12 files changed, 209 insertions(+), 2 deletions(-)

diff --git a/contrib/jsonb_plpython/expected/jsonb_plpython.out b/contrib/jsonb_plpython/expected/jsonb_plpython.out
index cac963de69c..0b32583f5a5 100644
--- a/contrib/jsonb_plpython/expected/jsonb_plpython.out
+++ b/contrib/jsonb_plpython/expected/jsonb_plpython.out
@@ -304,3 +304,22 @@ SELECT test_dict1();
  {"": 2, "a": 1, "33": 3}
 (1 row)
 
+-- A custom sequence whose __getitem__ raises should be reported as an error,
+-- not crash the backend
+CREATE FUNCTION test_broken_sequence() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+class C:
+    def __len__(self):
+        return 2
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+return C()
+$$;
+SELECT test_broken_sequence();
+ERROR:  could not get element 0 from sequence
+DETAIL:  ValueError: getitem failed
+CONTEXT:  Traceback (most recent call last):
+while creating return value
+PL/Python function "test_broken_sequence"
diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c
index 909612a6039..d27350589ea 100644
--- a/contrib/jsonb_plpython/jsonb_plpython.c
+++ b/contrib/jsonb_plpython/jsonb_plpython.c
@@ -337,7 +337,10 @@ PLySequence_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state)
 		for (i = 0; i < pcount; i++)
 		{
 			value = PySequence_GetItem(obj, i);
-			Assert(value);
+
+			/* PySequence_GetItem() can return NULL, with an exception set */
+			if (value == NULL)
+				PLy_elog(ERROR, "could not get element %d from sequence", (int) i);
 
 			PLyObject_ToJsonbValue(value, jsonb_state, true);
 			Py_XDECREF(value);
diff --git a/contrib/jsonb_plpython/sql/jsonb_plpython.sql b/contrib/jsonb_plpython/sql/jsonb_plpython.sql
index 29dc33279a0..44745e645f1 100644
--- a/contrib/jsonb_plpython/sql/jsonb_plpython.sql
+++ b/contrib/jsonb_plpython/sql/jsonb_plpython.sql
@@ -181,3 +181,19 @@ return x
 $$;
 
 SELECT test_dict1();
+
+-- A custom sequence whose __getitem__ raises should be reported as an error,
+-- not crash the backend
+CREATE FUNCTION test_broken_sequence() RETURNS jsonb
+LANGUAGE plpython3u
+TRANSFORM FOR TYPE jsonb
+AS $$
+class C:
+    def __len__(self):
+        return 2
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+return C()
+$$;
+
+SELECT test_broken_sequence();
diff --git a/src/pl/plpython/expected/plpython_composite.out b/src/pl/plpython/expected/plpython_composite.out
index 674af93ddcf..ffce7fc1be7 100644
--- a/src/pl/plpython/expected/plpython_composite.out
+++ b/src/pl/plpython/expected/plpython_composite.out
@@ -606,3 +606,19 @@ DETAIL:  Missing left parenthesis.
 HINT:  To return a composite type in an array, return the composite type as a Python tuple, e.g., "[('foo',)]".
 CONTEXT:  while creating return value
 PL/Python function "composite_type_as_list_broken"
+-- A custom sequence whose length matches the tuple but whose __getitem__
+-- raises should be reported as an error, not crash the backend.
+CREATE FUNCTION composite_type_as_broken_sequence() RETURNS type_record AS $$
+class C:
+    def __len__(self):
+        return 2
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+return C()
+$$ LANGUAGE plpython3u;
+SELECT * FROM composite_type_as_broken_sequence();
+ERROR:  could not get element 0 from sequence
+DETAIL:  ValueError: getitem failed
+CONTEXT:  Traceback (most recent call last):
+while creating return value
+PL/Python function "composite_type_as_broken_sequence"
diff --git a/src/pl/plpython/expected/plpython_spi.out b/src/pl/plpython/expected/plpython_spi.out
index b572f9bf73b..0320ff01f6b 100644
--- a/src/pl/plpython/expected/plpython_spi.out
+++ b/src/pl/plpython/expected/plpython_spi.out
@@ -451,3 +451,54 @@ SELECT plan_composite_args();
  (3,label)
 (1 row)
 
+-- A custom argument sequence whose length matches the plan but whose
+-- __getitem__ raises should be reported as an error, not crash the backend.
+CREATE FUNCTION plan_broken_arg_sequence() RETURNS void AS $$
+plan = plpy.prepare("select $1", ["int4"])
+class C:
+    def __len__(self):
+        return 1
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+plpy.execute(plan, C())
+$$ LANGUAGE plpython3u;
+SELECT plan_broken_arg_sequence();
+ERROR:  spiexceptions.ExternalRoutineException: could not get element 0 from sequence
+DETAIL:  ValueError: getitem failed
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "plan_broken_arg_sequence", line 8, in <module>
+    plpy.execute(plan, C())
+PL/Python function "plan_broken_arg_sequence"
+-- Likewise for the type-name list passed to plpy.prepare().
+CREATE FUNCTION prepare_broken_type_sequence() RETURNS void AS $$
+class C:
+    def __len__(self):
+        return 1
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+plpy.prepare("select $1", C())
+$$ LANGUAGE plpython3u;
+SELECT prepare_broken_type_sequence();
+ERROR:  spiexceptions.ExternalRoutineException: could not get element 0 from sequence
+DETAIL:  ValueError: getitem failed
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "prepare_broken_type_sequence", line 7, in <module>
+    plpy.prepare("select $1", C())
+PL/Python function "prepare_broken_type_sequence"
+-- Likewise for the argument sequence passed to plpy.cursor().
+CREATE FUNCTION cursor_broken_arg_sequence() RETURNS void AS $$
+plan = plpy.prepare("select $1", ["int4"])
+class C:
+    def __len__(self):
+        return 1
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+plpy.cursor(plan, C())
+$$ LANGUAGE plpython3u;
+SELECT cursor_broken_arg_sequence();
+ERROR:  spiexceptions.ExternalRoutineException: could not get element 0 from sequence
+DETAIL:  ValueError: getitem failed
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "cursor_broken_arg_sequence", line 8, in <module>
+    plpy.cursor(plan, C())
+PL/Python function "cursor_broken_arg_sequence"
diff --git a/src/pl/plpython/expected/plpython_types.out b/src/pl/plpython/expected/plpython_types.out
index 8a680e15c14..0cb3d6ea8c6 100644
--- a/src/pl/plpython/expected/plpython_types.out
+++ b/src/pl/plpython/expected/plpython_types.out
@@ -796,6 +796,22 @@ SELECT * FROM test_type_conversion_array_error();
 ERROR:  return value of function with array return type is not a Python sequence
 CONTEXT:  while creating return value
 PL/Python function "test_type_conversion_array_error"
+-- A custom sequence whose __getitem__ raises should be reported as an error,
+-- not crash the backend.
+CREATE FUNCTION test_type_conversion_array_getitem_fail() RETURNS int[] AS $$
+class C:
+    def __len__(self):
+        return 2
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+return C()
+$$ LANGUAGE plpython3u;
+SELECT * FROM test_type_conversion_array_getitem_fail();
+ERROR:  could not get element 0 from sequence
+DETAIL:  ValueError: getitem failed
+CONTEXT:  Traceback (most recent call last):
+while creating return value
+PL/Python function "test_type_conversion_array_getitem_fail"
 --
 -- Domains over arrays
 --
diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
index cc74c4df6ba..0725fbc19f2 100644
--- a/src/pl/plpython/plpy_cursorobject.c
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -258,6 +258,11 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 			PyObject   *elem;
 
 			elem = PySequence_GetItem(args, j);
+
+			/* PySequence_GetItem() can return NULL, with an exception set */
+			if (elem == NULL)
+				PLy_elog(ERROR, "could not get element %d from sequence", j);
+
 			PG_TRY(2);
 			{
 				bool		isnull;
diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c
index 4ad40bf78f3..4980873efcf 100644
--- a/src/pl/plpython/plpy_spi.c
+++ b/src/pl/plpython/plpy_spi.c
@@ -87,6 +87,11 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
 			int32		typmod;
 
 			optr = PySequence_GetItem(list, i);
+
+			/* PySequence_GetItem() can return NULL, with an exception set */
+			if (optr == NULL)
+				PLy_elog(ERROR, "could not get element %d from sequence", i);
+
 			if (PyUnicode_Check(optr))
 				sptr = PLyUnicode_AsString(optr);
 			else
@@ -250,6 +255,11 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
 			PyObject   *elem;
 
 			elem = PySequence_GetItem(list, j);
+
+			/* PySequence_GetItem() can return NULL, with an exception set */
+			if (elem == NULL)
+				PLy_elog(ERROR, "could not get element %d from sequence", j);
+
 			PG_TRY(2);
 			{
 				bool		isnull;
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index 92d55bf9f42..e499ed8cbfd 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -1210,6 +1210,10 @@ PLySequence_ToArray_recurse(PyObject *obj, ArrayBuildState **astatep,
 		/* fetch the array element */
 		PyObject   *subobj = PySequence_GetItem(obj, i);
 
+		/* PySequence_GetItem() can return NULL, with an exception set */
+		if (subobj == NULL)
+			PLy_elog(ERROR, "could not get element %d from sequence", i);
+
 		/* need PG_TRY to ensure we release the subobj's refcount */
 		PG_TRY();
 		{
@@ -1456,7 +1460,10 @@ PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence)
 		PG_TRY();
 		{
 			value = PySequence_GetItem(sequence, idx);
-			Assert(value);
+
+			/* PySequence_GetItem() can return NULL, with an exception set */
+			if (value == NULL)
+				PLy_elog(ERROR, "could not get element %d from sequence", idx);
 
 			values[i] = att->func(att, value, &nulls[i], false);
 
diff --git a/src/pl/plpython/sql/plpython_composite.sql b/src/pl/plpython/sql/plpython_composite.sql
index 1bb9b83b719..b401b3f2f6b 100644
--- a/src/pl/plpython/sql/plpython_composite.sql
+++ b/src/pl/plpython/sql/plpython_composite.sql
@@ -233,3 +233,15 @@ CREATE FUNCTION composite_type_as_list_broken()  RETURNS type_record[] AS $$
   return [['first', 1]];
 $$ LANGUAGE plpython3u;
 SELECT * FROM composite_type_as_list_broken();
+
+-- A custom sequence whose length matches the tuple but whose __getitem__
+-- raises should be reported as an error, not crash the backend.
+CREATE FUNCTION composite_type_as_broken_sequence() RETURNS type_record AS $$
+class C:
+    def __len__(self):
+        return 2
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+return C()
+$$ LANGUAGE plpython3u;
+SELECT * FROM composite_type_as_broken_sequence();
diff --git a/src/pl/plpython/sql/plpython_spi.sql b/src/pl/plpython/sql/plpython_spi.sql
index 00dcc8bb669..276d130431e 100644
--- a/src/pl/plpython/sql/plpython_spi.sql
+++ b/src/pl/plpython/sql/plpython_spi.sql
@@ -307,3 +307,42 @@ SELECT cursor_fetch_next_empty();
 SELECT cursor_plan();
 SELECT cursor_plan_wrong_args();
 SELECT plan_composite_args();
+
+-- A custom argument sequence whose length matches the plan but whose
+-- __getitem__ raises should be reported as an error, not crash the backend.
+CREATE FUNCTION plan_broken_arg_sequence() RETURNS void AS $$
+plan = plpy.prepare("select $1", ["int4"])
+class C:
+    def __len__(self):
+        return 1
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+plpy.execute(plan, C())
+$$ LANGUAGE plpython3u;
+
+SELECT plan_broken_arg_sequence();
+
+-- Likewise for the type-name list passed to plpy.prepare().
+CREATE FUNCTION prepare_broken_type_sequence() RETURNS void AS $$
+class C:
+    def __len__(self):
+        return 1
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+plpy.prepare("select $1", C())
+$$ LANGUAGE plpython3u;
+
+SELECT prepare_broken_type_sequence();
+
+-- Likewise for the argument sequence passed to plpy.cursor().
+CREATE FUNCTION cursor_broken_arg_sequence() RETURNS void AS $$
+plan = plpy.prepare("select $1", ["int4"])
+class C:
+    def __len__(self):
+        return 1
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+plpy.cursor(plan, C())
+$$ LANGUAGE plpython3u;
+
+SELECT cursor_broken_arg_sequence();
diff --git a/src/pl/plpython/sql/plpython_types.sql b/src/pl/plpython/sql/plpython_types.sql
index 0985a9cca2f..31549c7f4f1 100644
--- a/src/pl/plpython/sql/plpython_types.sql
+++ b/src/pl/plpython/sql/plpython_types.sql
@@ -417,6 +417,19 @@ $$ LANGUAGE plpython3u;
 
 SELECT * FROM test_type_conversion_array_error();
 
+-- A custom sequence whose __getitem__ raises should be reported as an error,
+-- not crash the backend.
+CREATE FUNCTION test_type_conversion_array_getitem_fail() RETURNS int[] AS $$
+class C:
+    def __len__(self):
+        return 2
+    def __getitem__(self, i):
+        raise ValueError('getitem failed')
+return C()
+$$ LANGUAGE plpython3u;
+
+SELECT * FROM test_type_conversion_array_getitem_fail();
+
 
 --
 -- Domains over arrays
-- 
2.39.5 (Apple Git-154)

