postgresql/contrib/hstore_plpython/expected/hstore_plpython.out
Richard Guo 53482fcb94 plpython: Fix NULL pointer dereferences for broken sequence and mapping objects
PL/Python and its hstore and jsonb transforms build SQL values from
Python containers by calling Python C API functions that can return
NULL, and in several places the result was used without first checking
it.

On the sequence side, PySequence_GetItem() is used 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().  On the
mapping side, the hstore and jsonb transforms call PyMapping_Size()
and PyMapping_Items() and then index the result with PyList_GetItem()
and PyTuple_GetItem().

All of these return NULL (or -1), with a Python exception set, for a
broken object: for example one whose __getitem__() or items() raises,
or which reports a length that disagrees with what it actually yields.
The unchecked result was then dereferenced, crashing the backend.

Fix this by checking the result of each call and reporting a regular
error if it failed, so that the underlying Python exception is
surfaced instead of taking down the session.

Author: Richard Guo <guofenglinux@gmail.com>
Reviewed-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Discussion: https://postgr.es/m/CAMbWs49BKM9wP6m8bCXEpHwQKp7usvOGV6Jf=J7FYr_BCpxLqg@mail.gmail.com
Backpatch-through: 14
2026-06-29 11:39:50 +09:00

226 lines
5.6 KiB
Text

CREATE EXTENSION hstore_plpython3u CASCADE;
NOTICE: installing required extension "plpython3u"
-- test hstore -> python
CREATE FUNCTION test1(val hstore) RETURNS int
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
assert isinstance(val, dict)
plpy.info(sorted(val.items()))
return len(val)
$$;
SELECT test1('aa=>bb, cc=>NULL'::hstore);
INFO: [('aa', 'bb'), ('cc', None)]
test1
-------
2
(1 row)
-- the same with the versioned language name
CREATE FUNCTION test1n(val hstore) RETURNS int
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
assert isinstance(val, dict)
plpy.info(sorted(val.items()))
return len(val)
$$;
SELECT test1n('aa=>bb, cc=>NULL'::hstore);
INFO: [('aa', 'bb'), ('cc', None)]
test1n
--------
2
(1 row)
-- test that a non-mapping result is correctly rejected
CREATE FUNCTION test1bad() RETURNS hstore
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
return "foo"
$$;
SELECT test1bad();
ERROR: not a Python mapping
CONTEXT: while creating return value
PL/Python function "test1bad"
-- A mapping whose items() raises should be reported as an error, not crash
-- the backend
CREATE FUNCTION test1broken() RETURNS hstore
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
class C(dict):
def items(self):
raise ValueError('items failed')
d = C()
d['x'] = 1
return d
$$;
SELECT test1broken();
ERROR: could not get items from Python mapping
CONTEXT: while creating return value
PL/Python function "test1broken"
-- Likewise for a mapping whose items() does not return key/value pairs
CREATE FUNCTION test1malformed() RETURNS hstore
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
class C(dict):
def items(self):
return [42]
d = C()
d['x'] = 1
return d
$$;
SELECT test1malformed();
ERROR: items() of a Python mapping must return key/value pairs
CONTEXT: while creating return value
PL/Python function "test1malformed"
-- Likewise for a mapping whose items() yields fewer pairs than its length
CREATE FUNCTION test1short() RETURNS hstore
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
class C(dict):
def items(self):
return []
d = C()
d['x'] = 1
return d
$$;
SELECT test1short();
ERROR: items() of a Python mapping must return key/value pairs
CONTEXT: while creating return value
PL/Python function "test1short"
-- Likewise for a mapping whose __len__() raises
CREATE FUNCTION test1brokenlen() RETURNS hstore
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
class C(dict):
def __len__(self):
raise ValueError('len failed')
d = C()
d['x'] = 1
return d
$$;
SELECT test1brokenlen();
ERROR: could not get size of Python mapping
CONTEXT: while creating return value
PL/Python function "test1brokenlen"
-- test hstore[] -> python
CREATE FUNCTION test1arr(val hstore[]) RETURNS int
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
assert(val == [{'aa': 'bb', 'cc': None}, {'dd': 'ee'}])
return len(val)
$$;
SELECT test1arr(array['aa=>bb, cc=>NULL'::hstore, 'dd=>ee']);
test1arr
----------
2
(1 row)
-- test python -> hstore
CREATE FUNCTION test2(a int, b text) RETURNS hstore
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
val = {'a': a, 'b': b, 'c': None}
return val
$$;
SELECT test2(1, 'boo');
test2
---------------------------------
"a"=>"1", "b"=>"boo", "c"=>NULL
(1 row)
--- test ruleutils
\sf test2
CREATE OR REPLACE FUNCTION public.test2(a integer, b text)
RETURNS hstore
TRANSFORM FOR TYPE hstore
LANGUAGE plpython3u
AS $function$
val = {'a': a, 'b': b, 'c': None}
return val
$function$
-- test python -> hstore[]
CREATE FUNCTION test2arr() RETURNS hstore[]
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
val = [{'a': 1, 'b': 'boo', 'c': None}, {'d': 2}]
return val
$$;
SELECT test2arr();
test2arr
--------------------------------------------------------------
{"\"a\"=>\"1\", \"b\"=>\"boo\", \"c\"=>NULL","\"d\"=>\"2\""}
(1 row)
-- test python -> domain over hstore
CREATE DOMAIN hstore_foo AS hstore CHECK(VALUE ? 'foo');
CREATE FUNCTION test2dom(fn text) RETURNS hstore_foo
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
return {'a': 1, fn: 'boo', 'c': None}
$$;
SELECT test2dom('foo');
test2dom
-----------------------------------
"a"=>"1", "c"=>NULL, "foo"=>"boo"
(1 row)
SELECT test2dom('bar'); -- fail
ERROR: value for domain hstore_foo violates check constraint "hstore_foo_check"
CONTEXT: while creating return value
PL/Python function "test2dom"
-- test as part of prepare/execute
CREATE FUNCTION test3() RETURNS void
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
rv = plpy.execute("SELECT 'aa=>bb, cc=>NULL'::hstore AS col1")
assert(rv[0]["col1"] == {'aa': 'bb', 'cc': None})
val = {'a': 1, 'b': 'boo', 'c': None}
plan = plpy.prepare("SELECT $1::text AS col1", ["hstore"])
rv = plpy.execute(plan, [val])
assert(rv[0]["col1"] == '"a"=>"1", "b"=>"boo", "c"=>NULL')
$$;
SELECT test3();
test3
-------
(1 row)
-- test trigger
CREATE TABLE test1 (a int, b hstore);
INSERT INTO test1 VALUES (1, 'aa=>bb, cc=>NULL');
SELECT * FROM test1;
a | b
---+------------------------
1 | "aa"=>"bb", "cc"=>NULL
(1 row)
CREATE FUNCTION test4() RETURNS trigger
LANGUAGE plpython3u
TRANSFORM FOR TYPE hstore
AS $$
assert(TD["new"] == {'a': 1, 'b': {'aa': 'bb', 'cc': None}})
if TD["new"]["a"] == 1:
TD["new"]["b"] = {'a': 1, 'b': 'boo', 'c': None}
return "MODIFY"
$$;
CREATE TRIGGER test4 BEFORE UPDATE ON test1 FOR EACH ROW EXECUTE PROCEDURE test4();
UPDATE test1 SET a = a;
SELECT * FROM test1;
a | b
---+---------------------------------
1 | "a"=>"1", "b"=>"boo", "c"=>NULL
(1 row)