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});")

Reply via email to