From 644610f27fe9898cc5d7736a6db40449b79ab2f1 Mon Sep 17 00:00:00 2001 From: Ewan Young Date: Wed, 8 Jul 2026 01:51:50 +0800 Subject: [PATCH v1] jsonb_plpython: Check for negative PySequence_Size() result Commit 8612f0b7ce0 hardened the jsonb and hstore PL/Python transforms against broken container objects, adding a pcount < 0 check after PyMapping_Size() in PLyMapping_ToJsonbValue(). The sibling function PLySequence_ToJsonbValue() was left without the equivalent check on the result of PySequence_Size(). PySequence_Size() returns -1, with a Python exception set, for an object that passes PySequence_Check() but whose length cannot be determined: for example one whose __len__() raises, or a legacy sequence that implements __getitem__() but no __len__() at all. Because the negative result was used directly as the loop bound, the loop body was simply skipped, so the transform silently produced an empty jsonb array instead of reporting the error. Worse, the Python error indicator was left set, which could cause a subsequent, unrelated PL/Python call in the same session to fail with a confusing "returned a result with an error set" SystemError. Fix this the same way the mapping side was fixed, by checking for a negative result and surfacing the underlying Python exception. Discussion: https://postgr.es/m/CAMbWs49BKM9wP6m8bCXEpHwQKp7usvOGV6Jf=J7FYr_BCpxLqg@mail.gmail.com Backpatch-through: 14 --- .../expected/jsonb_plpython.out | 36 +++++++++++++++++++ contrib/jsonb_plpython/jsonb_plpython.c | 2 ++ contrib/jsonb_plpython/sql/jsonb_plpython.sql | 31 ++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/contrib/jsonb_plpython/expected/jsonb_plpython.out b/contrib/jsonb_plpython/expected/jsonb_plpython.out index 8d3f5328809..dac06e4918d 100644 --- a/contrib/jsonb_plpython/expected/jsonb_plpython.out +++ b/contrib/jsonb_plpython/expected/jsonb_plpython.out @@ -393,3 +393,39 @@ DETAIL: ValueError: len failed CONTEXT: Traceback (most recent call last): while creating return value PL/Python function "test_broken_len_mapping" +-- A custom sequence whose __len__() raises should be reported as an error, +-- not silently produce an empty array +CREATE FUNCTION test_broken_len_sequence() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C: + def __getitem__(self, i): + return i + def __len__(self): + raise ValueError('len failed') +return C() +$$; +SELECT test_broken_len_sequence(); +ERROR: could not get size of Python sequence +DETAIL: ValueError: len failed +CONTEXT: Traceback (most recent call last): +while creating return value +PL/Python function "test_broken_len_sequence" +-- Likewise for a sequence-like object that has no __len__() at all +CREATE FUNCTION test_no_len_sequence() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C: + def __getitem__(self, i): + if i < 3: + return i + raise IndexError +return C() +$$; +SELECT test_no_len_sequence(); +ERROR: could not get size of Python sequence +DETAIL: TypeError: object of type 'C' has no len() +CONTEXT: while creating return value +PL/Python function "test_no_len_sequence" diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c index dc17c9ee570..e142f2bf215 100644 --- a/contrib/jsonb_plpython/jsonb_plpython.c +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -341,6 +341,8 @@ PLySequence_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state) PyObject *volatile value = NULL; pcount = PySequence_Size(obj); + if (pcount < 0) + PLy_elog(ERROR, "could not get size of Python sequence"); pushJsonbValue(jsonb_state, WJB_BEGIN_ARRAY, NULL); diff --git a/contrib/jsonb_plpython/sql/jsonb_plpython.sql b/contrib/jsonb_plpython/sql/jsonb_plpython.sql index fd8485c89c1..acceb088bff 100644 --- a/contrib/jsonb_plpython/sql/jsonb_plpython.sql +++ b/contrib/jsonb_plpython/sql/jsonb_plpython.sql @@ -258,3 +258,34 @@ return d $$; SELECT test_broken_len_mapping(); + +-- A custom sequence whose __len__() raises should be reported as an error, +-- not silently produce an empty array +CREATE FUNCTION test_broken_len_sequence() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C: + def __getitem__(self, i): + return i + def __len__(self): + raise ValueError('len failed') +return C() +$$; + +SELECT test_broken_len_sequence(); + +-- Likewise for a sequence-like object that has no __len__() at all +CREATE FUNCTION test_no_len_sequence() RETURNS jsonb +LANGUAGE plpython3u +TRANSFORM FOR TYPE jsonb +AS $$ +class C: + def __getitem__(self, i): + if i < 3: + return i + raise IndexError +return C() +$$; + +SELECT test_no_len_sequence(); -- 2.47.3