Skip to content

Commit 0fcb8e2

Browse files
committed
Fix mishandling of resjunk columns in ON CONFLICT ... UPDATE tlists.
It's unusual to have any resjunk columns in an ON CONFLICT ... UPDATE list, but it can happen when MULTIEXPR_SUBLINK SubPlans are present. If it happens, the ON CONFLICT UPDATE code path would end up storing tuples that include the values of the extra resjunk columns. That's fairly harmless in the short run, but if new columns are added to the table then the values would become accessible, possibly leading to malfunctions if they don't match the datatypes of the new columns. This had escaped notice through a confluence of missing sanity checks, including * There's no cross-check that a tuple presented to heap_insert or heap_update matches the table rowtype. While it's difficult to check that fully at reasonable cost, we can easily add assertions that there aren't too many columns. * The output-column-assignment cases in execExprInterp.c lacked any sanity checks on the output column numbers, which seems like an oversight considering there are plenty of assertion checks on input column numbers. Add assertions there too. * We failed to apply nodeModifyTable's ExecCheckPlanOutput() to the ON CONFLICT UPDATE tlist. That wouldn't have caught this specific error, since that function is chartered to ignore resjunk columns; but it sure seems like a bad omission now that we've seen this bug. In HEAD, the right way to fix this is to make the processing of ON CONFLICT UPDATE tlists work the same as regular UPDATE tlists now do, that is don't add "SET x = x" entries, and use ExecBuildUpdateProjection to evaluate the tlist and combine it with old values of the not-set columns. This adds a little complication to ExecBuildUpdateProjection, but allows removal of a comparable amount of now-dead code from the planner. In the back branches, the most expedient solution seems to be to (a) use an output slot for the ON CONFLICT UPDATE projection that actually matches the target table, and then (b) invent a variant of ExecBuildProjectionInfo that can be told to not store values resulting from resjunk columns, so it doesn't try to store into nonexistent columns of the output slot. (We can't simply ignore the resjunk columns altogether; they have to be evaluated for MULTIEXPR_SUBLINK to work.) This works back to v10. In 9.6, projections work much differently and we can't cheaply give them such an option. The 9.6 version of this patch works by inserting a JunkFilter when it's necessary to get rid of resjunk columns. In addition, v11 and up have the reverse problem when trying to perform ON CONFLICT UPDATE on a partitioned table. Through a further oversight, adjust_partition_tlist() discarded resjunk columns when re-ordering the ON CONFLICT UPDATE tlist to match a partition. This accidentally prevented the storing-bogus-tuples problem, but at the cost that MULTIEXPR_SUBLINK cases didn't work, typically crashing if more than one row has to be updated. Fix by preserving resjunk columns in that routine. (I failed to resist the temptation to add more assertions there too, and to do some minor code beautification.) Per report from Andres Freund. Back-patch to all supported branches. Security: CVE-2021-32028
1 parent 0c1caa4 commit 0fcb8e2

File tree

5 files changed

+68
-17
lines changed

5 files changed

+68
-17
lines changed

src/backend/access/heap/heapam.c

+8
Original file line numberDiff line numberDiff line change
@@ -2389,6 +2389,10 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
23892389
Buffer vmbuffer = InvalidBuffer;
23902390
bool all_visible_cleared = false;
23912391

2392+
/* Cheap, simplistic check that the tuple matches the rel's rowtype. */
2393+
Assert(HeapTupleHeaderGetNatts(tup->t_data) <=
2394+
RelationGetNumberOfAttributes(relation));
2395+
23922396
/*
23932397
* Fill in tuple header fields, assign an OID, and toast the tuple if
23942398
* necessary.
@@ -3490,6 +3494,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
34903494

34913495
Assert(ItemPointerIsValid(otid));
34923496

3497+
/* Cheap, simplistic check that the tuple matches the rel's rowtype. */
3498+
Assert(HeapTupleHeaderGetNatts(newtup->t_data) <=
3499+
RelationGetNumberOfAttributes(relation));
3500+
34933501
/*
34943502
* Forbid this during a parallel operation, lest it allocate a combocid.
34953503
* Other workers might need that combocid for visibility checks, and we

src/backend/executor/nodeModifyTable.c

+49-9
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,10 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
12331233
/* Project the new tuple version */
12341234
ExecProject(resultRelInfo->ri_onConflictSetProj, NULL);
12351235

1236+
if (mtstate->mt_confljunk)
1237+
(void) ExecFilterJunk(mtstate->mt_confljunk,
1238+
resultRelInfo->ri_onConflictSetProj->pi_slot);
1239+
12361240
/*
12371241
* Note that it is possible that the target tuple has been modified in
12381242
* this session, after the above heap_lock_tuple. We choose to not error
@@ -1744,7 +1748,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
17441748
{
17451749
ExprContext *econtext;
17461750
ExprState *setexpr;
1747-
TupleDesc tupDesc;
17481751

17491752
/* insert may only have one plan, inheritance is not expanded */
17501753
Assert(nplans == 1);
@@ -1764,17 +1767,54 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
17641767
mtstate->mt_excludedtlist = node->exclRelTlist;
17651768

17661769
/* create target slot for UPDATE SET projection */
1767-
tupDesc = ExecTypeFromTL((List *) node->onConflictSet,
1768-
resultRelInfo->ri_RelationDesc->rd_rel->relhasoids);
17691770
mtstate->mt_conflproj = ExecInitExtraTupleSlot(mtstate->ps.state);
1770-
ExecSetSlotDescriptor(mtstate->mt_conflproj, tupDesc);
1771+
ExecSetSlotDescriptor(mtstate->mt_conflproj,
1772+
resultRelInfo->ri_RelationDesc->rd_att);
17711773

1772-
/* build UPDATE SET expression and projection state */
1774+
/* initialize UPDATE SET tlist expressions */
17731775
setexpr = ExecInitExpr((Expr *) node->onConflictSet, &mtstate->ps);
1774-
resultRelInfo->ri_onConflictSetProj =
1775-
ExecBuildProjectionInfo((List *) setexpr, econtext,
1776-
mtstate->mt_conflproj,
1777-
resultRelInfo->ri_RelationDesc->rd_att);
1776+
1777+
/*
1778+
* The onConflictSet tlist should already have been adjusted to emit
1779+
* the table's exact column list.
1780+
*/
1781+
ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
1782+
node->onConflictSet);
1783+
1784+
/*
1785+
* However, it might also contain resjunk columns, in which case we'll
1786+
* need a junkfilter to get rid of those.
1787+
*/
1788+
if (ExecCleanTargetListLength(node->onConflictSet) ==
1789+
list_length(node->onConflictSet))
1790+
{
1791+
/* No junk columns, so we'll just project into mt_conflproj. */
1792+
resultRelInfo->ri_onConflictSetProj =
1793+
ExecBuildProjectionInfo((List *) setexpr, econtext,
1794+
mtstate->mt_conflproj,
1795+
resultRelInfo->ri_RelationDesc->rd_att);
1796+
mtstate->mt_confljunk = NULL;
1797+
}
1798+
else
1799+
{
1800+
/*
1801+
* Project into a slot matching the tlist's output rowtype, then
1802+
* apply a junkfilter.
1803+
*/
1804+
TupleDesc tupDesc = ExecTypeFromTL(node->onConflictSet, false);
1805+
TupleTableSlot *ocsSlot;
1806+
1807+
ocsSlot = ExecInitExtraTupleSlot(mtstate->ps.state);
1808+
ExecSetSlotDescriptor(ocsSlot, tupDesc);
1809+
resultRelInfo->ri_onConflictSetProj =
1810+
ExecBuildProjectionInfo((List *) setexpr, econtext,
1811+
ocsSlot,
1812+
resultRelInfo->ri_RelationDesc->rd_att);
1813+
mtstate->mt_confljunk =
1814+
ExecInitJunkFilter(node->onConflictSet,
1815+
resultRelInfo->ri_RelationDesc->rd_att->tdhasoid,
1816+
mtstate->mt_conflproj);
1817+
}
17781818

17791819
/* build DO UPDATE WHERE clause expression */
17801820
if (node->onConflictWhere)

src/include/nodes/execnodes.h

+1
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,7 @@ typedef struct ModifyTableState
11411141
* tlist */
11421142
TupleTableSlot *mt_conflproj; /* CONFLICT ... SET ... projection
11431143
* target */
1144+
JunkFilter *mt_confljunk; /* CONFLICT ... SET might need a junkfilter */
11441145
} ModifyTableState;
11451146

11461147
/* ----------------

src/test/regress/expected/update.out

+7-5
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ SELECT a, b, char_length(c) FROM update_test;
201201
(4 rows)
202202

203203
-- Test ON CONFLICT DO UPDATE
204-
INSERT INTO upsert_test VALUES(1, 'Boo');
204+
INSERT INTO upsert_test VALUES(1, 'Boo'), (3, 'Zoo');
205205
-- uncorrelated sub-select:
206206
WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
207207
VALUES (1, 'Bar') ON CONFLICT(a)
@@ -212,22 +212,24 @@ WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
212212
(1 row)
213213

214214
-- correlated sub-select:
215-
INSERT INTO upsert_test VALUES (1, 'Baz') ON CONFLICT(a)
215+
INSERT INTO upsert_test VALUES (1, 'Baz'), (3, 'Zaz') ON CONFLICT(a)
216216
DO UPDATE SET (b, a) = (SELECT b || ', Correlated', a from upsert_test i WHERE i.a = upsert_test.a)
217217
RETURNING *;
218218
a | b
219219
---+-----------------
220220
1 | Foo, Correlated
221-
(1 row)
221+
3 | Zoo, Correlated
222+
(2 rows)
222223

223224
-- correlated sub-select (EXCLUDED.* alias):
224-
INSERT INTO upsert_test VALUES (1, 'Bat') ON CONFLICT(a)
225+
INSERT INTO upsert_test VALUES (1, 'Bat'), (3, 'Zot') ON CONFLICT(a)
225226
DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a)
226227
RETURNING *;
227228
a | b
228229
---+---------------------------
229230
1 | Foo, Correlated, Excluded
230-
(1 row)
231+
3 | Zoo, Correlated, Excluded
232+
(2 rows)
231233

232234
DROP TABLE update_test;
233235
DROP TABLE upsert_test;

src/test/regress/sql/update.sql

+3-3
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,17 @@ UPDATE update_test t
9999
SELECT a, b, char_length(c) FROM update_test;
100100

101101
-- Test ON CONFLICT DO UPDATE
102-
INSERT INTO upsert_test VALUES(1, 'Boo');
102+
INSERT INTO upsert_test VALUES(1, 'Boo'), (3, 'Zoo');
103103
-- uncorrelated sub-select:
104104
WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
105105
VALUES (1, 'Bar') ON CONFLICT(a)
106106
DO UPDATE SET (b, a) = (SELECT b, a FROM aaa) RETURNING *;
107107
-- correlated sub-select:
108-
INSERT INTO upsert_test VALUES (1, 'Baz') ON CONFLICT(a)
108+
INSERT INTO upsert_test VALUES (1, 'Baz'), (3, 'Zaz') ON CONFLICT(a)
109109
DO UPDATE SET (b, a) = (SELECT b || ', Correlated', a from upsert_test i WHERE i.a = upsert_test.a)
110110
RETURNING *;
111111
-- correlated sub-select (EXCLUDED.* alias):
112-
INSERT INTO upsert_test VALUES (1, 'Bat') ON CONFLICT(a)
112+
INSERT INTO upsert_test VALUES (1, 'Bat'), (3, 'Zot') ON CONFLICT(a)
113113
DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a)
114114
RETURNING *;
115115

0 commit comments

Comments
 (0)