From: Álvaro Herrera Date: Thu, 11 Jun 2026 14:17:58 +0000 (+0200) Subject: IS JSON/JSON(): Protect against expressions uncoercible to text X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=7dd15325952fe85521b1fefea3ad39cf1b46e0c8;p=thirdparty%2Fpostgresql.git IS JSON/JSON(): Protect against expressions uncoercible to text 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 Reported-by: Chi Zhang <798604270@qq.com> Reviewed-by: Srinath Reddy Sadipiralla Backpatch-through: 16 Discussion: https://postgr.es/m/19491-7aafc221ec63f288@postgresql.org --- diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 3cd35c5c457..40b09958ac2 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -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; diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index f1003e57fb2..9adc9d4c0f6 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -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; } diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 143d961c077..0f337bda325 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -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}')) diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index ed044d81fdd..a68747733a1 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -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}'))