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.cgi?a=commitdiff_plain;h=35d9a6263407563f9948a38fced5419deab297a2;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 e2d9e9be41a..bc48966c5d1 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 b6d30ddc298..a00b46a25ca 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4088,10 +4088,20 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, if (*exprtype == UNKNOWNOID || typcategory == TYPCATEGORY_STRING) { + int location = exprLocation(expr); + expr = coerce_to_target_type(pstate, (Node *) 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 625acf3019a..6b0d8815508 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1148,6 +1148,44 @@ SELECT NULL::bytea IS JSON; SELECT NULL::int IS JSON; ERROR: cannot use type integer in IS JSON predicate +-- 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 SELECT '' IS JSON; ?column? ---------- diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 343d344d270..0b77deb3b24 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -395,6 +395,28 @@ SELECT NULL::text IS JSON; SELECT NULL::bytea IS JSON; SELECT NULL::int IS JSON; +-- 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 + SELECT '' IS JSON; SELECT bytea '\x00' IS JSON;