Ensure result of an aggregate's finalfunc is made read-only.
authorTom Lane <[email protected]>
Sun, 16 Apr 2023 18:16:40 +0000 (14:16 -0400)
committerTom Lane <[email protected]>
Sun, 16 Apr 2023 18:16:40 +0000 (14:16 -0400)
The finalfunc might return a read-write expanded object.  If we
de-duplicate multiple call sites for the aggregate, any function(s)
receiving the aggregate result earlier could alter or destroy the
value that reaches the ones called later.  This is a brown-paper-bag
bug in commit 42b746d4c, because we actually considered the need
for read-only-ness but failed to realize that it applied to the case
with a finalfunc as well as the case without.

Per report from Justin Pryzby.  New error in HEAD,
no need for back-patch.

Discussion: https://postgr.es/m/[email protected]

src/backend/executor/nodeAgg.c
src/backend/executor/nodeWindowAgg.c
src/test/regress/expected/aggregates.out
src/test/regress/sql/aggregates.sql

index 19342a420c10b0add0b68db1bddf411d53983d12..28205f74331c08036d4ab7b1b8985290441dfa60 100644 (file)
@@ -1040,9 +1040,10 @@ process_ordered_aggregate_multi(AggState *aggstate,
  * (But note that in some cases, such as when there is no finalfn, the
  * result might be a pointer to or into the agg's transition value.)
  *
- * The finalfn uses the state as set in the transno. This also might be
+ * The finalfn uses the state as set in the transno.  This also might be
  * being used by another aggregate function, so it's important that we do
- * nothing destructive here.
+ * nothing destructive here.  Moreover, the aggregate's final value might
+ * get used in multiple places, so we mustn't return a R/W expanded datum.
  */
 static void
 finalize_aggregate(AggState *aggstate,
@@ -1116,8 +1117,13 @@ finalize_aggregate(AggState *aggstate,
                }
                else
                {
-                       *resultVal = FunctionCallInvoke(fcinfo);
+                       Datum           result;
+
+                       result = FunctionCallInvoke(fcinfo);
                        *resultIsNull = fcinfo->isnull;
+                       *resultVal = MakeExpandedObjectReadOnly(result,
+                                                                                                       fcinfo->isnull,
+                                                                                                       peragg->resulttypeLen);
                }
                aggstate->curperagg = NULL;
        }
@@ -1165,6 +1171,7 @@ finalize_partialaggregate(AggState *aggstate,
                else
                {
                        FunctionCallInfo fcinfo = pertrans->serialfn_fcinfo;
+                       Datum           result;
 
                        fcinfo->args[0].value =
                                MakeExpandedObjectReadOnly(pergroupstate->transValue,
@@ -1173,8 +1180,11 @@ finalize_partialaggregate(AggState *aggstate,
                        fcinfo->args[0].isnull = pergroupstate->transValueIsNull;
                        fcinfo->isnull = false;
 
-                       *resultVal = FunctionCallInvoke(fcinfo);
+                       result = FunctionCallInvoke(fcinfo);
                        *resultIsNull = fcinfo->isnull;
+                       *resultVal = MakeExpandedObjectReadOnly(result,
+                                                                                                       fcinfo->isnull,
+                                                                                                       peragg->resulttypeLen);
                }
        }
        else
index 7c07fb0684806ee49944cdc5457f5795428c214f..3ac581a71137aaca879598c5ea3b55d56a725b00 100644 (file)
@@ -623,10 +623,15 @@ finalize_windowaggregate(WindowAggState *winstate,
                }
                else
                {
+                       Datum           res;
+
                        winstate->curaggcontext = peraggstate->aggcontext;
-                       *result = FunctionCallInvoke(fcinfo);
+                       res = FunctionCallInvoke(fcinfo);
                        winstate->curaggcontext = NULL;
                        *isnull = fcinfo->isnull;
+                       *result = MakeExpandedObjectReadOnly(res,
+                                                                                                fcinfo->isnull,
+                                                                                                peraggstate->resulttypeLen);
                }
        }
        else
index f0517f95b603039e83b286d5ddd1e5eb322c41c1..d8271da4d1fb098023ec6a27fa1502aea9d44f92 100644 (file)
@@ -2758,6 +2758,43 @@ SELECT balk(hundred) FROM tenk1;
      
 (1 row)
 
+ROLLBACK;
+-- test multiple usage of an aggregate whose finalfn returns a R/W datum
+BEGIN;
+CREATE FUNCTION rwagg_sfunc(x anyarray, y anyarray) RETURNS anyarray
+LANGUAGE plpgsql IMMUTABLE AS $$
+BEGIN
+    RETURN array_fill(y[1], ARRAY[4]);
+END;
+$$;
+CREATE FUNCTION rwagg_finalfunc(x anyarray) RETURNS anyarray
+LANGUAGE plpgsql STRICT IMMUTABLE AS $$
+DECLARE
+    res x%TYPE;
+BEGIN
+    -- assignment is essential for this test, it expands the array to R/W
+    res := array_fill(x[1], ARRAY[4]);
+    RETURN res;
+END;
+$$;
+CREATE AGGREGATE rwagg(anyarray) (
+    STYPE = anyarray,
+    SFUNC = rwagg_sfunc,
+    FINALFUNC = rwagg_finalfunc
+);
+CREATE FUNCTION eatarray(x real[]) RETURNS real[]
+LANGUAGE plpgsql STRICT IMMUTABLE AS $$
+BEGIN
+    x[1] := x[1] + 1;
+    RETURN x;
+END;
+$$;
+SELECT eatarray(rwagg(ARRAY[1.0::real])), eatarray(rwagg(ARRAY[1.0::real]));
+ eatarray  | eatarray  
+-----------+-----------
+ {2,1,1,1} | {2,1,1,1}
+(1 row)
+
 ROLLBACK;
 -- test coverage for aggregate combine/serial/deserial functions
 BEGIN;
index 1783d19bd5e571ee537ebb2da058d383ef2e763f..75c78be640b2914ab624a9eff4bafabd529d21d1 100644 (file)
@@ -1207,6 +1207,45 @@ SELECT balk(hundred) FROM tenk1;
 
 ROLLBACK;
 
+-- test multiple usage of an aggregate whose finalfn returns a R/W datum
+BEGIN;
+
+CREATE FUNCTION rwagg_sfunc(x anyarray, y anyarray) RETURNS anyarray
+LANGUAGE plpgsql IMMUTABLE AS $$
+BEGIN
+    RETURN array_fill(y[1], ARRAY[4]);
+END;
+$$;
+
+CREATE FUNCTION rwagg_finalfunc(x anyarray) RETURNS anyarray
+LANGUAGE plpgsql STRICT IMMUTABLE AS $$
+DECLARE
+    res x%TYPE;
+BEGIN
+    -- assignment is essential for this test, it expands the array to R/W
+    res := array_fill(x[1], ARRAY[4]);
+    RETURN res;
+END;
+$$;
+
+CREATE AGGREGATE rwagg(anyarray) (
+    STYPE = anyarray,
+    SFUNC = rwagg_sfunc,
+    FINALFUNC = rwagg_finalfunc
+);
+
+CREATE FUNCTION eatarray(x real[]) RETURNS real[]
+LANGUAGE plpgsql STRICT IMMUTABLE AS $$
+BEGIN
+    x[1] := x[1] + 1;
+    RETURN x;
+END;
+$$;
+
+SELECT eatarray(rwagg(ARRAY[1.0::real])), eatarray(rwagg(ARRAY[1.0::real]));
+
+ROLLBACK;
+
 -- test coverage for aggregate combine/serial/deserial functions
 BEGIN;