]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
IS JSON/JSON(): Protect against expressions uncoercible to text
authorÁlvaro Herrera <alvherre@kurilemu.de>
Thu, 11 Jun 2026 14:17:58 +0000 (16:17 +0200)
committerÁlvaro Herrera <alvherre@kurilemu.de>
Thu, 11 Jun 2026 14:17:58 +0000 (16:17 +0200)
transformJsonParseArg() was not careful enough on generation of
transformed expressions when starting from expressions that are not
coercible to text but are in the string type category: it failed to
verify that coerce_to_target_type() succeeds, and returned a NULL
pointer.  This leads to a later NULL dereference and crash at executor
time.

This escaped noticed because it cannot happen for built-in types, all of
which have casts to text.  Only user-created types are potentially
problematic.

Fix by raising an error when a cast to text doesn't exist.

This mistake came in with commit 6ee30209a6f1.

Author: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Reported-by: Chi Zhang <798604270@qq.com>
Reviewed-by: Srinath Reddy Sadipiralla <srinath2133@gmail.com>
Backpatch-through: 16
Discussion: https://postgr.es/m/19491-7aafc221ec63f288@postgresql.org

src/backend/nodes/makefuncs.c
src/backend/parser/parse_expr.c
src/test/regress/expected/sqljson.out
src/test/regress/sql/sqljson.sql

index 3cd35c5c457ee7ad614cc68c24d308827230b04a..40b09958ac2cd24bca7beb3910528e289c4c93f5 100644 (file)
@@ -988,6 +988,8 @@ makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type,
 {
        JsonIsPredicate *n = makeNode(JsonIsPredicate);
 
+       Assert(expr != NULL);
+
        n->expr = expr;
        n->format = format;
        n->item_type = item_type;
index f1003e57fb2992cd4de4683a16144582187b2069..9adc9d4c0f63768a5bf05cde0230f84a22ba607c 100644 (file)
@@ -4199,10 +4199,20 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format,
 
                if (*exprtype == UNKNOWNOID || typcategory == TYPCATEGORY_STRING)
                {
+                       int                     location = exprLocation(expr);
+
                        expr = coerce_to_target_type(pstate, expr, *exprtype,
                                                                                 TEXTOID, -1,
                                                                                 COERCION_IMPLICIT,
                                                                                 COERCE_IMPLICIT_CAST, -1);
+                       if (expr == NULL)
+                               ereport(ERROR,
+                                               errcode(ERRCODE_CANNOT_COERCE),
+                                               errmsg("cannot cast type %s to %s",
+                                                          format_type_be(*exprtype),
+                                                          format_type_be(TEXTOID)),
+                                               parser_errposition(pstate, location));
+
                        *exprtype = TEXTOID;
                }
 
index 143d961c0773012386787fda5e2b0d41828cc378..0f337bda3255e3c75c9134a51c6850a4884c26d7 100644 (file)
@@ -1470,6 +1470,44 @@ LINE 1: SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS;
 -- domain constraint violation during cast
 SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error
 ERROR:  value for domain jd2 violates check constraint "jd2_check"
+-- A user-defined string-category type with no implicit cast to text must
+-- produce a clean error rather than crash for IS JSON / JSON() input
+-- (per bug #19491).
+CREATE FUNCTION sqljson_mystr_in(cstring) RETURNS sqljson_mystr
+       AS 'textin' LANGUAGE internal IMMUTABLE STRICT;
+NOTICE:  type "sqljson_mystr" is not yet defined
+DETAIL:  Creating a shell type definition.
+CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cstring
+       AS 'textout' LANGUAGE internal IMMUTABLE STRICT;
+NOTICE:  argument type sqljson_mystr is only a shell
+LINE 1: CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cst...
+                                          ^
+CREATE TYPE sqljson_mystr (
+       INPUT = sqljson_mystr_in,
+       OUTPUT = sqljson_mystr_out,
+       LIKE = text,
+       CATEGORY = 'S'
+);
+SELECT '{"a":1}'::sqljson_mystr IS JSON;                -- error
+ERROR:  cannot cast type sqljson_mystr to text
+LINE 1: SELECT '{"a":1}'::sqljson_mystr IS JSON;
+               ^
+SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); -- error
+ERROR:  cannot cast type sqljson_mystr to text
+LINE 1: SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS);
+                    ^
+-- An implicit cast to text lets the same query work normally.
+CREATE CAST (sqljson_mystr AS text) WITHOUT FUNCTION AS IMPLICIT;
+SELECT '{"a":1}'::sqljson_mystr IS JSON;
+ ?column? 
+----------
+ t
+(1 row)
+
+\set VERBOSITY terse
+DROP TYPE sqljson_mystr CASCADE;
+NOTICE:  drop cascades to 3 other objects
+\set VERBOSITY default
 -- view creation and deparsing with domain IS JSON
 CREATE VIEW domain_isjson AS
 WITH cte(a) AS (VALUES('{"a": 1, "a": 2}'))
index ed044d81fdd48c10bd278327b4b8f4d82e468a5c..a68747733a1cd4371ea888d6933cbbdd6a6fec06 100644 (file)
@@ -559,6 +559,28 @@ SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- error
 -- domain constraint violation during cast
 SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error
 
+-- A user-defined string-category type with no implicit cast to text must
+-- produce a clean error rather than crash for IS JSON / JSON() input
+-- (per bug #19491).
+CREATE FUNCTION sqljson_mystr_in(cstring) RETURNS sqljson_mystr
+       AS 'textin' LANGUAGE internal IMMUTABLE STRICT;
+CREATE FUNCTION sqljson_mystr_out(sqljson_mystr) RETURNS cstring
+       AS 'textout' LANGUAGE internal IMMUTABLE STRICT;
+CREATE TYPE sqljson_mystr (
+       INPUT = sqljson_mystr_in,
+       OUTPUT = sqljson_mystr_out,
+       LIKE = text,
+       CATEGORY = 'S'
+);
+SELECT '{"a":1}'::sqljson_mystr IS JSON;                -- error
+SELECT JSON('{"a":1}'::sqljson_mystr WITH UNIQUE KEYS); -- error
+-- An implicit cast to text lets the same query work normally.
+CREATE CAST (sqljson_mystr AS text) WITHOUT FUNCTION AS IMPLICIT;
+SELECT '{"a":1}'::sqljson_mystr IS JSON;
+\set VERBOSITY terse
+DROP TYPE sqljson_mystr CASCADE;
+\set VERBOSITY default
+
 -- view creation and deparsing with domain IS JSON
 CREATE VIEW domain_isjson AS
 WITH cte(a) AS (VALUES('{"a": 1, "a": 2}'))