From 9a1d56537b7ae4a0d1049aba54fdac357c93127c Mon Sep 17 00:00:00 2001 From: Srinath Reddy Sadipiralla Date: Thu, 24 Sep 2026 22:28:41 +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 fe8aa927b5e..5ab9a323447 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 0767db9cfff..1d430577412 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