Fix handling of expanded objects in CoerceToDomain and CASE execution.
authorTom Lane <[email protected]>
Thu, 22 Dec 2016 20:01:28 +0000 (15:01 -0500)
committerTom Lane <[email protected]>
Thu, 22 Dec 2016 20:01:39 +0000 (15:01 -0500)
When the input value to a CoerceToDomain expression node is a read-write
expanded datum, we should pass a read-only pointer to any domain CHECK
expressions and then return the original read-write pointer as the
expression result.  Previously we were blindly passing the same pointer to
all the consumers of the value, making it possible for a function in CHECK
to modify or even delete the expanded value.  (Since a plpgsql function
will absorb a passed-in read-write expanded array as a local variable
value, it will in fact delete the value on exit.)

A similar hazard of passing the same read-write pointer to multiple
consumers exists in domain_check() and in ExecEvalCase, so fix those too.

The fix requires adding MakeExpandedObjectReadOnly calls at the appropriate
places, which is simple enough except that we need to get the data type's
typlen from somewhere.  For the domain cases, solve this by redefining
DomainConstraintRef.tcache as okay for callers to access; there wasn't any
reason for the original convention against that, other than not wanting the
API of typcache.c to be any wider than it had to be.  For CASE, there's
no good solution except to add a syscache lookup during executor start.

Per bug #14472 from Marcos Castedo.  Back-patch to 9.5 where expanded
values were introduced.

Discussion: https://postgr.es/m/15225.1482431619@sss.pgh.pa.us

src/backend/executor/execQual.c
src/backend/utils/adt/domains.c
src/include/nodes/execnodes.h
src/include/utils/typcache.h
src/test/regress/expected/case.out
src/test/regress/expected/plpgsql.out
src/test/regress/sql/case.sql
src/test/regress/sql/plpgsql.sql

index a6c9b6a66b460a7ed2da9954db3b692974b53cf9..169134d45c61cdc2a50a77045e545bcf59869c27 100644 (file)
@@ -2960,12 +2960,18 @@ ExecEvalCase(CaseExprState *caseExpr, ExprContext *econtext,
 
        if (caseExpr->arg)
        {
+               Datum           arg_value;
                bool            arg_isNull;
 
-               econtext->caseValue_datum = ExecEvalExpr(caseExpr->arg,
-                                                                                                econtext,
-                                                                                                &arg_isNull,
-                                                                                                NULL);
+               arg_value = ExecEvalExpr(caseExpr->arg,
+                                                                econtext,
+                                                                &arg_isNull,
+                                                                NULL);
+               /* Since caseValue_datum may be read multiple times, force to R/O */
+               econtext->caseValue_datum =
+                       MakeExpandedObjectReadOnly(arg_value,
+                                                                          arg_isNull,
+                                                                          caseExpr->argtyplen);
                econtext->caseValue_isNull = arg_isNull;
        }
 
@@ -4026,11 +4032,18 @@ ExecEvalCoerceToDomain(CoerceToDomainState *cstate, ExprContext *econtext,
                                         * nodes. We must save and restore prior setting of
                                         * econtext's domainValue fields, in case this node is
                                         * itself within a check expression for another domain.
+                                        *
+                                        * Also, if we are working with a read-write expanded
+                                        * datum, be sure that what we pass to CHECK expressions
+                                        * is a read-only pointer; else called functions might
+                                        * modify or even delete the expanded object.
                                         */
                                        save_datum = econtext->domainValue_datum;
                                        save_isNull = econtext->domainValue_isNull;
 
-                                       econtext->domainValue_datum = result;
+                                       econtext->domainValue_datum =
+                                               MakeExpandedObjectReadOnly(result, *isNull,
+                                                                        cstate->constraint_ref->tcache->typlen);
                                        econtext->domainValue_isNull = *isNull;
 
                                        conResult = ExecEvalExpr(con->check_expr,
@@ -4858,6 +4871,8 @@ ExecInitExpr(Expr *node, PlanState *parent)
                                }
                                cstate->args = outlist;
                                cstate->defresult = ExecInitExpr(caseexpr->defresult, parent);
+                               if (caseexpr->arg)
+                                       cstate->argtyplen = get_typlen(exprType((Node *) caseexpr->arg));
                                state = (ExprState *) cstate;
                        }
                        break;
index ac8c25266e0770ed86fdde1933294a560d25d684..9cf98a8f6b4beef912b62e0b411dc5e609a29caa 100644 (file)
@@ -35,6 +35,7 @@
 #include "executor/executor.h"
 #include "lib/stringinfo.h"
 #include "utils/builtins.h"
+#include "utils/expandeddatum.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
@@ -157,9 +158,14 @@ domain_check_input(Datum value, bool isnull, DomainIOData *my_extra)
                                         * Set up value to be returned by CoerceToDomainValue
                                         * nodes.  Unlike ExecEvalCoerceToDomain, this econtext
                                         * couldn't be shared with anything else, so no need to
-                                        * save and restore fields.
+                                        * save and restore fields.  But we do need to protect the
+                                        * passed-in value against being changed by called
+                                        * functions.  (It couldn't be a R/W expanded object for
+                                        * most uses, but that seems possible for domain_check().)
                                         */
-                                       econtext->domainValue_datum = value;
+                                       econtext->domainValue_datum =
+                                               MakeExpandedObjectReadOnly(value, isnull,
+                                                                       my_extra->constraint_ref.tcache->typlen);
                                        econtext->domainValue_isNull = isnull;
 
                                        conResult = ExecEvalExprSwitchContext(con->check_expr,
index 0da5fb478adce32bd8f15249e284749ba75fa81b..ef9b5d03aa205fe8c1908710de26edd5666c01b3 100644 (file)
@@ -874,6 +874,7 @@ typedef struct CaseExprState
        ExprState  *arg;                        /* implicit equality comparison argument */
        List       *args;                       /* the arguments (list of WHEN clauses) */
        ExprState  *defresult;          /* the default result (ELSE clause) */
+       int16           argtyplen;              /* if arg is provided, its typlen */
 } CaseExprState;
 
 /* ----------------
index 23618cc6100bf7594767e338ab970102098953b3..438bb34fc6c5b7ac24c94dbc03565d6b95681d66 100644 (file)
@@ -131,9 +131,9 @@ typedef struct DomainConstraintRef
 {
        List       *constraints;        /* list of DomainConstraintState nodes */
        MemoryContext refctx;           /* context holding DomainConstraintRef */
+       TypeCacheEntry *tcache;         /* typcache entry for domain type */
 
        /* Management data --- treat these fields as private to typcache.c */
-       TypeCacheEntry *tcache;         /* owning typcache entry */
        DomainConstraintCache *dcc; /* current constraints, or NULL if none */
        MemoryContextCallback callback;         /* used to release refcount when done */
 } DomainConstraintRef;
index 5f6aa16d31985bc16c8e5088e81d4df6bc1635db..09d5516fb5fa80f7f71274a1008892ba738ed0ad 100644 (file)
@@ -338,6 +338,32 @@ SELECT CASE volfoo('bar') WHEN 'foo'::foodomain THEN 'is foo' ELSE 'is not foo'
  is not foo
 (1 row)
 
+ROLLBACK;
+-- Test multiple evaluation of a CASE arg that is a read/write object (#14472)
+-- Wrap this in a single transaction so the transient '=' operator doesn't
+-- cause problems in concurrent sessions
+BEGIN;
+CREATE DOMAIN arrdomain AS int[];
+CREATE FUNCTION make_ad(int,int) returns arrdomain as
+  'declare x arrdomain;
+   begin
+     x := array[$1,$2];
+     return x;
+   end' language plpgsql volatile;
+CREATE FUNCTION ad_eq(arrdomain, arrdomain) returns boolean as
+  'begin return array_eq($1, $2); end' language plpgsql;
+CREATE OPERATOR = (procedure = ad_eq,
+                   leftarg = arrdomain, rightarg = arrdomain);
+SELECT CASE make_ad(1,2)
+  WHEN array[2,4]::arrdomain THEN 'wrong'
+  WHEN array[2,5]::arrdomain THEN 'still wrong'
+  WHEN array[1,2]::arrdomain THEN 'right'
+  END;
+ case  
+-------
+ right
+(1 row)
+
 ROLLBACK;
 --
 -- Clean up
index 98a377339832511cfbb4490ecfbcaa328a6c1434..ac7187559c0afca7314a253eff5ff8ce6b92a05c 100644 (file)
@@ -5630,3 +5630,23 @@ end;
 $$;
 ERROR:  value for domain plpgsql_domain violates check constraint "plpgsql_domain_check"
 CONTEXT:  PL/pgSQL function inline_code_block line 4 at assignment
+-- Test handling of expanded array passed to a domain constraint (bug #14472)
+create function plpgsql_arr_domain_check(val int[]) returns boolean as $$
+begin return val[1] > 0; end
+$$ language plpgsql immutable;
+create domain plpgsql_arr_domain as int[] check(plpgsql_arr_domain_check(value));
+do $$
+declare v_test plpgsql_arr_domain;
+begin
+  v_test := array[1];
+  v_test := v_test || 2;
+end;
+$$;
+do $$
+declare v_test plpgsql_arr_domain := array[1];
+begin
+  v_test := 0 || v_test;  -- fail
+end;
+$$;
+ERROR:  value for domain plpgsql_arr_domain violates check constraint "plpgsql_arr_domain_check"
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at assignment
index c860fae258a07eb857f78951aa0e3e480f605d51..a7ae7b4a9ebfc9f4516a94c142edb8817a6a1d38 100644 (file)
@@ -200,6 +200,34 @@ SELECT CASE volfoo('bar') WHEN 'foo'::foodomain THEN 'is foo' ELSE 'is not foo'
 
 ROLLBACK;
 
+-- Test multiple evaluation of a CASE arg that is a read/write object (#14472)
+-- Wrap this in a single transaction so the transient '=' operator doesn't
+-- cause problems in concurrent sessions
+BEGIN;
+
+CREATE DOMAIN arrdomain AS int[];
+
+CREATE FUNCTION make_ad(int,int) returns arrdomain as
+  'declare x arrdomain;
+   begin
+     x := array[$1,$2];
+     return x;
+   end' language plpgsql volatile;
+
+CREATE FUNCTION ad_eq(arrdomain, arrdomain) returns boolean as
+  'begin return array_eq($1, $2); end' language plpgsql;
+
+CREATE OPERATOR = (procedure = ad_eq,
+                   leftarg = arrdomain, rightarg = arrdomain);
+
+SELECT CASE make_ad(1,2)
+  WHEN array[2,4]::arrdomain THEN 'wrong'
+  WHEN array[2,5]::arrdomain THEN 'still wrong'
+  WHEN array[1,2]::arrdomain THEN 'right'
+  END;
+
+ROLLBACK;
+
 --
 -- Clean up
 --
index 468dfd3111ebe723f75c40c2e5dd99e99eb9ec92..9b30ff1434e1a93da575c4a776eea285adac0843 100644 (file)
@@ -4412,3 +4412,26 @@ begin
   v_test := 0;  -- fail
 end;
 $$;
+
+-- Test handling of expanded array passed to a domain constraint (bug #14472)
+
+create function plpgsql_arr_domain_check(val int[]) returns boolean as $$
+begin return val[1] > 0; end
+$$ language plpgsql immutable;
+
+create domain plpgsql_arr_domain as int[] check(plpgsql_arr_domain_check(value));
+
+do $$
+declare v_test plpgsql_arr_domain;
+begin
+  v_test := array[1];
+  v_test := v_test || 2;
+end;
+$$;
+
+do $$
+declare v_test plpgsql_arr_domain := array[1];
+begin
+  v_test := 0 || v_test;  -- fail
+end;
+$$;