Hi,
I discovered several bugs in jsonb_plperl and jsonb_plpython.
The first bug causes a segfault when dealing with deeply nested JSONB
values. As an example:
```
$ ./reproduce_stack_overflow.py
plpython3u (depth=100000): SIGSEGV
2026-06-16 16:42:56.989 MSK [3209763] LOG: client backend (PID
3209810) was terminated by signal 11: Segmentation fault
2026-06-16 16:42:56.989 MSK [3209763] DETAIL: Failed process was
running: SELECT py_deep(100000);
plperl (depth=100000): SIGSEGV
2026-06-16 16:42:59.101 MSK [3209763] LOG: client backend (PID
3209827) was terminated by signal 11: Segmentation fault
2026-06-16 16:42:59.101 MSK [3209763] DETAIL: Failed process was
running: SELECT perl_deep(100000);
```
The second bug affects only jsonb_plperl. It's possible to construct a
Perl object with circular references which will cause
SV_to_JsonbValue() to go into an infinite loop here:
```
while (SvROK(in))
in = SvRV(in);
```
The attached script reproduce_circular_ref.py reproduces the issue. Be
careful if you decide to run it because the backend will become
unresponsive to pg_cancel_backend() and you will be unable to stop the
cluster in a standard way.
I suggest fixing it by rewriting the while loop into a recursion with
check_stack_depth() call. This will make the behavior consistent with
jsonb_plpython.
Patches are attached. Thoughts?
--
Best regards,
Aleksander Alekseev
From 4a600d76b7e4ec50d0e91da291da4fc9e00deeaf Mon Sep 17 00:00:00 2001
From: Aleksander Alekseev <[email protected]>
Date: Tue, 16 Jun 2026 15:54:08 +0300
Subject: [PATCH v1 2/2] jsonb_plperl: Replace reference-unwinding loop with
recursion
SV_to_JsonbValue() used a while loop to dereference chains of Perl
references before processing the underlying value. This loop had no
protection against circular reference chains (e.g. $x = \$x), causing
the backend to spin indefinitely.
Replace the loop with a small recursive helper SV_deref() that calls
check_stack_depth() on each step. This makes circular and pathologically
deep reference chains raise "stack depth limit exceeded" instead of
hanging the backend, consistent with how jsonb_plpython handles circular
Python object references.
Author: Aleksander Alekseev <[email protected]>
Reviewed-by: TODO FIXME
Discussion: TODO FIXME
---
contrib/jsonb_plperl/jsonb_plperl.c | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/contrib/jsonb_plperl/jsonb_plperl.c b/contrib/jsonb_plperl/jsonb_plperl.c
index fdcec7760da..40c283d3132 100644
--- a/contrib/jsonb_plperl/jsonb_plperl.c
+++ b/contrib/jsonb_plperl/jsonb_plperl.c
@@ -176,6 +176,20 @@ HV_to_JsonbValue(HV *obj, JsonbInState *jsonb_state)
pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL);
}
+/*
+ * Recursively dereference a Perl reference to reach the underlying value.
+ * Using recursion rather than a loop lets check_stack_depth() detect
+ * circular reference chains.
+ */
+static SV *
+SV_deref(SV *in)
+{
+ check_stack_depth();
+ if (SvROK(in))
+ return SV_deref(SvRV(in));
+ return in;
+}
+
static void
SV_to_JsonbValue(SV *in, JsonbInState *jsonb_state, bool is_elem)
{
@@ -184,9 +198,7 @@ SV_to_JsonbValue(SV *in, JsonbInState *jsonb_state, bool is_elem)
check_stack_depth();
- /* Dereference references recursively. */
- while (SvROK(in))
- in = SvRV(in);
+ in = SV_deref(in);
switch (SvTYPE(in))
{
--
2.43.0
From 604409b67d55102c38ff5e735817c115e0073cc7 Mon Sep 17 00:00:00 2001
From: Aleksander Alekseev <[email protected]>
Date: Tue, 16 Jun 2026 14:45:51 +0300
Subject: [PATCH v1 1/2] jsonb_plperl, jsonb_plpython: Add missing
check_stack_depth() calls
The functions responsible for converting JSONB to and from Perl and
Python values recurse mutually: Jsonb_to_SV() calls JsonbValue_to_SV()
which may call Jsonb_to_SV() again, and similarly SV_to_JsonbValue()
calls AV_to_JsonbValue() or HV_to_JsonbValue() which call
SV_to_JsonbValue() back. Likewise in jsonb_plpython.c.
Without a stack depth check, converting a deeply nested JSONB value
would cause a stack overflow and crash the backend process with SIGSEGV.
Add check_stack_depth() at the start of the entry function for each
recursion cycle so that an informative error is raised instead.
Note that while the JSON text parser already calls check_stack_depth()
during parsing, that protection does not cover JSONB values built via
jsonb_build_object() or other means that bypass the text parser.
Author: Aleksander Alekseev <[email protected]>
Reviewed-by: TODO FIXME
Discussion: TODO FIXME
---
contrib/jsonb_plperl/jsonb_plperl.c | 5 +++++
contrib/jsonb_plpython/jsonb_plpython.c | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/contrib/jsonb_plperl/jsonb_plperl.c b/contrib/jsonb_plperl/jsonb_plperl.c
index f8e4a584fdd..fdcec7760da 100644
--- a/contrib/jsonb_plperl/jsonb_plperl.c
+++ b/contrib/jsonb_plperl/jsonb_plperl.c
@@ -3,6 +3,7 @@
#include <math.h>
#include "fmgr.h"
+#include "miscadmin.h"
#include "plperl.h"
#include "utils/fmgrprotos.h"
#include "utils/jsonb.h"
@@ -66,6 +67,8 @@ Jsonb_to_SV(JsonbContainer *jsonb)
JsonbIterator *it;
JsonbIteratorToken r;
+ check_stack_depth();
+
it = JsonbIteratorInit(jsonb);
r = JsonbIteratorNext(&it, &v, true);
@@ -179,6 +182,8 @@ SV_to_JsonbValue(SV *in, JsonbInState *jsonb_state, bool is_elem)
dTHX;
JsonbValue out; /* result */
+ check_stack_depth();
+
/* Dereference references recursively. */
while (SvROK(in))
in = SvRV(in);
diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c
index 4de75a04e76..f6eddb81c48 100644
--- a/contrib/jsonb_plpython/jsonb_plpython.c
+++ b/contrib/jsonb_plpython/jsonb_plpython.c
@@ -1,5 +1,6 @@
#include "postgres.h"
+#include "miscadmin.h"
#include "plpy_elog.h"
#include "plpy_typeio.h"
#include "plpy_util.h"
@@ -143,6 +144,8 @@ PLyObject_FromJsonbContainer(JsonbContainer *jsonb)
JsonbIterator *it;
PyObject *result;
+ check_stack_depth();
+
it = JsonbIteratorInit(jsonb);
r = JsonbIteratorNext(&it, &v, true);
@@ -410,6 +413,8 @@ PLyObject_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state, bool is_elem)
{
JsonbValue *out;
+ check_stack_depth();
+
if (!PyUnicode_Check(obj))
{
if (PySequence_Check(obj))
--
2.43.0
#!/usr/bin/env python3
# Reproduce infinite loop in SV_to_JsonbValue on a circular Perl reference.
# The while (SvROK(in)) in = SvRV(in); loop in jsonb_plperl.c never terminates.
import subprocess
TIMEOUT = 5
def psql(sql):
subprocess.run(["psql", "-c", sql], check=True, capture_output=True)
psql("CREATE EXTENSION IF NOT EXISTS jsonb_plperl CASCADE;")
psql("""
CREATE OR REPLACE FUNCTION perl_circular() RETURNS jsonb
LANGUAGE plperl TRANSFORM FOR TYPE jsonb AS $$
my $x;
$x = \\$x;
return $x;
$$;
""")
print(f"calling perl_circular(), waiting {TIMEOUT}s for hang... ", end="", flush=True)
proc = subprocess.Popen(["psql", "-c", "SELECT perl_circular();"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
stdout, stderr = proc.communicate(timeout=TIMEOUT)
output = (stdout + stderr).decode().strip()
print(f"no hang:\n{output}")
except subprocess.TimeoutExpired:
print("hung")
proc.kill()
proc.wait()
print("psql client killed")
#!/usr/bin/env python3
# Reproduce stack overflow (SIGSEGV) in jsonb_plperl and jsonb_plpython3u.
import subprocess
import time
DEPTH = 100_000
PGLOG = "/home/eax/pginstall/data/logfile"
def psql(sql):
subprocess.run(["psql", "-c", sql], check=True, capture_output=True)
def run_test(label, query):
with open(PGLOG) as f:
f.seek(0, 2)
log_pos = f.tell()
print(f"{label} (depth={DEPTH}): ", end="", flush=True)
result = subprocess.run(["psql", "-c", query], capture_output=True)
time.sleep(2)
with open(PGLOG) as f:
f.seek(log_pos)
log = f.read()
if "signal 11" in log:
print("SIGSEGV")
for line in log.splitlines():
if "signal 11" in line or "DETAIL" in line:
print(" ", line.strip())
else:
output = result.stdout.decode().strip()
print(f"no crash:\n{output}")
if result.stderr:
print(result.stderr.decode().strip())
psql("CREATE EXTENSION IF NOT EXISTS jsonb_plpython3u CASCADE;")
psql("CREATE EXTENSION IF NOT EXISTS jsonb_plperl CASCADE;")
psql("""
CREATE OR REPLACE FUNCTION py_deep(n int) RETURNS jsonb
LANGUAGE plpython3u TRANSFORM FOR TYPE jsonb AS $$
d = 1
for i in range(n): d = {'x': d}
return d
$$;
""")
psql("""
CREATE OR REPLACE FUNCTION perl_deep(n int) RETURNS jsonb
LANGUAGE plperl TRANSFORM FOR TYPE jsonb AS $$
my $h = 1;
for my $i (1..$_[0]) { $h = {x => $h}; }
return $h;
$$;
""")
run_test("plpython3u", f"SELECT py_deep({DEPTH});")
# Wait for postmaster to restart the backend after the crash.
for _ in range(5):
result = subprocess.run(["psql", "-c", "SELECT 1;"], capture_output=True)
if result.returncode == 0:
break
time.sleep(2)
run_test("plperl ", f"SELECT perl_deep({DEPTH});")