From e6bccb501793daeaa4564d18b9168160aee83bc8 Mon Sep 17 00:00:00 2001 From: Srinath Reddy Sadipiralla Date: Sat, 19 Sep 2026 06:24:14 +0530 Subject: [PATCH 1/1] Fix state leakage in JSON_VALUE returning json/jsonb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If JSON_VALUE(... RETURNING json/jsonb) evaluated to NULL for a row  (e.g: due to a JSON null or missing key), all subsequent rows in the  same statement would incorrectly return NULL, even if they had valid data. This happened because the expression executor reuses its state across rows.  In ExecEvalJsonExprPath(), the code extracted the valid string for json and jsonb return types, but forgot to reset the *op->resnull  flag back to false. Because the stale true flag from an earlier row  was never cleared, the executor discarded the valid string and output a  SQL NULL instead. Fix this by explicitly clearing the null flag when a valid JSON value  is processed. --- src/backend/executor/execExprInterp.c | 1 + .../regress/expected/sqljson_queryfuncs.out | 46 +++++++++++++++++++ src/test/regress/sql/sqljson_queryfuncs.sql | 23 ++++++++++ 3 files changed, 70 insertions(+) diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 397219f7a3a..bfcf13769ca 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4979,6 +4979,7 @@ ExecEvalJsonExprPath(ExprState *state, ExprEvalStep *op, { val_string = DatumGetCString(DirectFunctionCall1(jsonb_out, JsonbPGetDatum(JsonbValueToJsonb(jbv)))); + *op->resnull = false; } else if (jsexpr->use_json_coercion) { diff --git a/src/test/regress/expected/sqljson_queryfuncs.out b/src/test/regress/expected/sqljson_queryfuncs.out index ff64dce0c59..51f7098cd5c 100644 --- a/src/test/regress/expected/sqljson_queryfuncs.out +++ b/src/test/regress/expected/sqljson_queryfuncs.out @@ -554,6 +554,52 @@ select json_value('{"a": 1.234}', '$.a' returning int error on error); ERROR: invalid input syntax for type integer: "1.234" select json_value('{"a": "1.234"}', '$.a' returning int error on error); ERROR: invalid input syntax for type integer: "1.234" +-- Test state leakage of null flags in JSON_VALUE with RETURNING json/jsonb +-- (Checks that a NULL evaluation doesn't poison subsequent evaluations in the same statement) +-- 1. Same statement, separate columns (JSONB) +SELECT JSON_VALUE('null', '$' RETURNING jsonb) AS col1, + JSON_VALUE('123', '$' RETURNING jsonb) AS col2; + col1 | col2 +------+------ + | 123 +(1 row) + +-- 2. Across multiple rows (JSONB) +SELECT JSON_VALUE(x, '$' RETURNING jsonb) +FROM (VALUES ('1'::jsonb), ('null'), ('2')) v(x); + json_value +------------ + 1 + + 2 +(3 rows) + +-- 3. Same statement, separate columns (JSON) +SELECT JSON_VALUE('null', '$' RETURNING json) AS col1, + JSON_VALUE('123', '$' RETURNING json) AS col2; + col1 | col2 +------+------ + | 123 +(1 row) + +-- 4. Across multiple rows (JSON) +SELECT JSON_VALUE(x, '$' RETURNING json) +FROM (VALUES ('1'::json), ('null'), ('2')) v(x); + json_value +------------ + 1 + + 2 +(3 rows) + +-- 5. Triggering NULL via EMPTY/NO MATCH (JSONB) +SELECT JSON_VALUE('{"a": 1}', '$.b' RETURNING jsonb) AS col1, + JSON_VALUE('{"a": 1}', '$.a' RETURNING jsonb) AS col2; + col1 | col2 +------+------ + | 1 +(1 row) + -- JSON_QUERY SELECT JSON_VALUE(NULL::jsonb, '$'); json_value diff --git a/src/test/regress/sql/sqljson_queryfuncs.sql b/src/test/regress/sql/sqljson_queryfuncs.sql index a69ef253f66..e523ea78c63 100644 --- a/src/test/regress/sql/sqljson_queryfuncs.sql +++ b/src/test/regress/sql/sqljson_queryfuncs.sql @@ -149,6 +149,29 @@ SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 + select json_value('{"a": 1.234}', '$.a' returning int error on error); select json_value('{"a": "1.234"}', '$.a' returning int error on error); +-- Test state leakage of null flags in JSON_VALUE with RETURNING json/jsonb +-- (Checks that a NULL evaluation doesn't poison subsequent evaluations in the same statement) + +-- 1. Same statement, separate columns (JSONB) +SELECT JSON_VALUE('null', '$' RETURNING jsonb) AS col1, + JSON_VALUE('123', '$' RETURNING jsonb) AS col2; + +-- 2. Across multiple rows (JSONB) +SELECT JSON_VALUE(x, '$' RETURNING jsonb) +FROM (VALUES ('1'::jsonb), ('null'), ('2')) v(x); + +-- 3. Same statement, separate columns (JSON) +SELECT JSON_VALUE('null', '$' RETURNING json) AS col1, + JSON_VALUE('123', '$' RETURNING json) AS col2; + +-- 4. Across multiple rows (JSON) +SELECT JSON_VALUE(x, '$' RETURNING json) +FROM (VALUES ('1'::json), ('null'), ('2')) v(x); + +-- 5. Triggering NULL via EMPTY/NO MATCH (JSONB) +SELECT JSON_VALUE('{"a": 1}', '$.b' RETURNING jsonb) AS col1, + JSON_VALUE('{"a": 1}', '$.a' RETURNING jsonb) AS col2; + -- JSON_QUERY SELECT JSON_VALUE(NULL::jsonb, '$'); -- 2.43.0