Skip to content

Commit b7d1f32

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 06bfbe8 commit b7d1f32

File tree

8 files changed

+180
-51
lines changed

8 files changed

+180
-51
lines changed

src/backend/access/heap/heapam.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2459,6 +2459,10 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
24592459
Buffer vmbuffer = InvalidBuffer;
24602460
bool all_visible_cleared = false;
24612461

2462+
/* Cheap, simplistic check that the tuple matches the rel's rowtype. */
2463+
Assert(HeapTupleHeaderGetNatts(tup->t_data) <=
2464+
RelationGetNumberOfAttributes(relation));
2465+
24622466
/*
24632467
* Fill in tuple header fields, assign an OID, and toast the tuple if
24642468
* necessary.
@@ -3573,6 +3577,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
35733577

35743578
Assert(ItemPointerIsValid(otid));
35753579

3580+
/* Cheap, simplistic check that the tuple matches the rel's rowtype. */
3581+
Assert(HeapTupleHeaderGetNatts(newtup->t_data) <=
3582+
RelationGetNumberOfAttributes(relation));
3583+
35763584
/*
35773585
* Forbid this during a parallel operation, lest it allocate a combocid.
35783586
* Other workers might need that combocid for visibility checks, and we

src/backend/executor/execExpr.c

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,32 @@ ExecBuildProjectionInfo(List *targetList,
351351
TupleTableSlot *slot,
352352
PlanState *parent,
353353
TupleDesc inputDesc)
354+
{
355+
return ExecBuildProjectionInfoExt(targetList,
356+
econtext,
357+
slot,
358+
true,
359+
parent,
360+
inputDesc);
361+
}
362+
363+
/*
364+
* ExecBuildProjectionInfoExt
365+
*
366+
* As above, with one additional option.
367+
*
368+
* If assignJunkEntries is true (the usual case), resjunk entries in the tlist
369+
* are not handled specially: they are evaluated and assigned to the proper
370+
* column of the result slot. If assignJunkEntries is false, resjunk entries
371+
* are evaluated, but their result is discarded without assignment.
372+
*/
373+
ProjectionInfo *
374+
ExecBuildProjectionInfoExt(List *targetList,
375+
ExprContext *econtext,
376+
TupleTableSlot *slot,
377+
bool assignJunkEntries,
378+
PlanState *parent,
379+
TupleDesc inputDesc)
354380
{
355381
ProjectionInfo *projInfo = makeNode(ProjectionInfo);
356382
ExprState *state;
@@ -388,7 +414,8 @@ ExecBuildProjectionInfo(List *targetList,
388414
*/
389415
if (tle->expr != NULL &&
390416
IsA(tle->expr, Var) &&
391-
((Var *) tle->expr)->varattno > 0)
417+
((Var *) tle->expr)->varattno > 0 &&
418+
(assignJunkEntries || !tle->resjunk))
392419
{
393420
/* Non-system Var, but how safe is it? */
394421
variable = (Var *) tle->expr;
@@ -452,6 +479,10 @@ ExecBuildProjectionInfo(List *targetList,
452479
ExecInitExprRec(tle->expr, state,
453480
&state->resvalue, &state->resnull);
454481

482+
/* This makes it easy to discard resjunk results when told to. */
483+
if (!assignJunkEntries && tle->resjunk)
484+
continue;
485+
455486
/*
456487
* Column might be referenced multiple times in upper nodes, so
457488
* force value to R/O - but only if it could be an expanded datum.

src/backend/executor/execExprInterp.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
560560
* care of at compilation time. But see EEOP_INNER_VAR comments.
561561
*/
562562
Assert(attnum >= 0 && attnum < innerslot->tts_nvalid);
563+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
563564
resultslot->tts_values[resultnum] = innerslot->tts_values[attnum];
564565
resultslot->tts_isnull[resultnum] = innerslot->tts_isnull[attnum];
565566

@@ -576,6 +577,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
576577
* care of at compilation time. But see EEOP_INNER_VAR comments.
577578
*/
578579
Assert(attnum >= 0 && attnum < outerslot->tts_nvalid);
580+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
579581
resultslot->tts_values[resultnum] = outerslot->tts_values[attnum];
580582
resultslot->tts_isnull[resultnum] = outerslot->tts_isnull[attnum];
581583

@@ -592,6 +594,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
592594
* care of at compilation time. But see EEOP_INNER_VAR comments.
593595
*/
594596
Assert(attnum >= 0 && attnum < scanslot->tts_nvalid);
597+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
595598
resultslot->tts_values[resultnum] = scanslot->tts_values[attnum];
596599
resultslot->tts_isnull[resultnum] = scanslot->tts_isnull[attnum];
597600

@@ -602,6 +605,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
602605
{
603606
int resultnum = op->d.assign_tmp.resultnum;
604607

608+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
605609
resultslot->tts_values[resultnum] = state->resvalue;
606610
resultslot->tts_isnull[resultnum] = state->resnull;
607611

@@ -612,6 +616,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
612616
{
613617
int resultnum = op->d.assign_tmp.resultnum;
614618

619+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
615620
resultslot->tts_isnull[resultnum] = state->resnull;
616621
if (!resultslot->tts_isnull[resultnum])
617622
resultslot->tts_values[resultnum] =
@@ -2046,8 +2051,10 @@ ExecJustAssignInnerVar(ExprState *state, ExprContext *econtext, bool *isnull)
20462051
*
20472052
* Since we use slot_getattr(), we don't need to implement the FETCHSOME
20482053
* step explicitly, and we also needn't Assert that the attnum is in range
2049-
* --- slot_getattr() will take care of any problems.
2054+
* --- slot_getattr() will take care of any problems. Nonetheless, check
2055+
* that resultnum is in range.
20502056
*/
2057+
Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
20512058
outslot->tts_values[resultnum] =
20522059
slot_getattr(inslot, attnum, &outslot->tts_isnull[resultnum]);
20532060
return 0;
@@ -2064,6 +2071,7 @@ ExecJustAssignOuterVar(ExprState *state, ExprContext *econtext, bool *isnull)
20642071
TupleTableSlot *outslot = state->resultslot;
20652072

20662073
/* See comments in ExecJustAssignInnerVar */
2074+
Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
20672075
outslot->tts_values[resultnum] =
20682076
slot_getattr(inslot, attnum, &outslot->tts_isnull[resultnum]);
20692077
return 0;
@@ -2080,6 +2088,7 @@ ExecJustAssignScanVar(ExprState *state, ExprContext *econtext, bool *isnull)
20802088
TupleTableSlot *outslot = state->resultslot;
20812089

20822090
/* See comments in ExecJustAssignInnerVar */
2091+
Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
20832092
outslot->tts_values[resultnum] =
20842093
slot_getattr(inslot, attnum, &outslot->tts_isnull[resultnum]);
20852094
return 0;

src/backend/executor/execPartition.c

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -629,11 +629,11 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
629629
leaf_part_rri->ri_onConflict = rootResultRelInfo->ri_onConflict;
630630
else
631631
{
632+
OnConflictSetState *onconfl = makeNode(OnConflictSetState);
632633
List *onconflset;
633-
TupleDesc tupDesc;
634634
bool found_whole_row;
635635

636-
leaf_part_rri->ri_onConflict = makeNode(OnConflictSetState);
636+
leaf_part_rri->ri_onConflict = onconfl;
637637

638638
/*
639639
* Translate expressions in onConflictSet to account for
@@ -642,7 +642,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
642642
* pseudo-relation (INNER_VAR), and second to handle the main
643643
* target relation (firstVarno).
644644
*/
645-
onconflset = (List *) copyObject((Node *) node->onConflictSet);
645+
onconflset = copyObject(node->onConflictSet);
646646
if (part_attnos == NULL)
647647
part_attnos =
648648
convert_tuples_by_name_map(RelationGetDescr(partrel),
@@ -665,7 +665,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
665665
&found_whole_row);
666666
/* We ignore the value of found_whole_row. */
667667

668-
/* Finally, adjust this tlist to match the partition. */
668+
/* Finally, reorder the tlist to match the partition. */
669669
onconflset = adjust_partition_tlist(onconflset, map);
670670

671671
/*
@@ -675,13 +675,12 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
675675
* partition that's tupdesc-equal to the partitioned table;
676676
* partitions of different tupdescs must generate their own.
677677
*/
678-
tupDesc = ExecTypeFromTL(onconflset, partrelDesc->tdhasoid);
679-
ExecSetSlotDescriptor(mtstate->mt_conflproj, tupDesc);
680-
leaf_part_rri->ri_onConflict->oc_ProjInfo =
681-
ExecBuildProjectionInfo(onconflset, econtext,
682-
mtstate->mt_conflproj,
683-
&mtstate->ps, partrelDesc);
684-
leaf_part_rri->ri_onConflict->oc_ProjTupdesc = tupDesc;
678+
ExecSetSlotDescriptor(mtstate->mt_conflproj, partrelDesc);
679+
onconfl->oc_ProjInfo =
680+
ExecBuildProjectionInfoExt(onconflset, econtext,
681+
mtstate->mt_conflproj, false,
682+
&mtstate->ps, partrelDesc);
683+
onconfl->oc_ProjTupdesc = partrelDesc;
685684

686685
/*
687686
* If there is a WHERE clause, initialize state where it will
@@ -710,7 +709,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
710709
RelationGetForm(partrel)->reltype,
711710
&found_whole_row);
712711
/* We ignore the value of found_whole_row. */
713-
leaf_part_rri->ri_onConflict->oc_WhereClause =
712+
onconfl->oc_WhereClause =
714713
ExecInitQual((List *) clause, &mtstate->ps);
715714
}
716715
}
@@ -1332,18 +1331,15 @@ ExecBuildSlotPartitionKeyDescription(Relation rel,
13321331

13331332
/*
13341333
* adjust_partition_tlist
1335-
* Adjust the targetlist entries for a given partition to account for
1336-
* attribute differences between parent and the partition
1334+
* Re-order the targetlist entries for a given partition to account for
1335+
* column position differences between the parent and the partition.
13371336
*
1338-
* The expressions have already been fixed, but here we fix the list to make
1339-
* target resnos match the partition's attribute numbers. This results in a
1340-
* copy of the original target list in which the entries appear in resno
1341-
* order, including both the existing entries (that may have their resno
1342-
* changed in-place) and the newly added entries for columns that don't exist
1343-
* in the parent.
1337+
* The expressions have already been fixed, but we must now re-order the
1338+
* entries in case the partition has different column order, and possibly
1339+
* add or remove dummy entries for dropped columns.
13441340
*
1345-
* Scribbles on the input tlist, so callers must make sure to make a copy
1346-
* before passing it to us.
1341+
* Although a new List is returned, this feels free to scribble on resno
1342+
* fields of the given tlist, so that should be a working copy.
13471343
*/
13481344
static List *
13491345
adjust_partition_tlist(List *tlist, TupleConversionMap *map)
@@ -1352,31 +1348,35 @@ adjust_partition_tlist(List *tlist, TupleConversionMap *map)
13521348
TupleDesc tupdesc = map->outdesc;
13531349
AttrNumber *attrMap = map->attrMap;
13541350
AttrNumber attrno;
1351+
ListCell *lc;
13551352

13561353
for (attrno = 1; attrno <= tupdesc->natts; attrno++)
13571354
{
13581355
Form_pg_attribute att_tup = TupleDescAttr(tupdesc, attrno - 1);
1356+
AttrNumber parentattrno = attrMap[attrno - 1];
13591357
TargetEntry *tle;
13601358

1361-
if (attrMap[attrno - 1] != InvalidAttrNumber)
1359+
if (parentattrno != InvalidAttrNumber)
13621360
{
1363-
Assert(!att_tup->attisdropped);
1364-
13651361
/*
13661362
* Use the corresponding entry from the parent's tlist, adjusting
1367-
* the resno the match the partition's attno.
1363+
* the resno to match the partition's attno.
13681364
*/
1369-
tle = (TargetEntry *) list_nth(tlist, attrMap[attrno - 1] - 1);
1365+
Assert(!att_tup->attisdropped);
1366+
tle = (TargetEntry *) list_nth(tlist, parentattrno - 1);
1367+
Assert(!tle->resjunk);
1368+
Assert(tle->resno == parentattrno);
13701369
tle->resno = attrno;
13711370
}
13721371
else
13731372
{
1374-
Const *expr;
1375-
13761373
/*
13771374
* For a dropped attribute in the partition, generate a dummy
1378-
* entry with resno matching the partition's attno.
1375+
* entry with resno matching the partition's attno. This should
1376+
* match what expand_targetlist() does.
13791377
*/
1378+
Const *expr;
1379+
13801380
Assert(att_tup->attisdropped);
13811381
expr = makeConst(INT4OID,
13821382
-1,
@@ -1394,6 +1394,18 @@ adjust_partition_tlist(List *tlist, TupleConversionMap *map)
13941394
new_tlist = lappend(new_tlist, tle);
13951395
}
13961396

1397+
/* Finally, attach any resjunk entries to the end of the new tlist */
1398+
foreach(lc, tlist)
1399+
{
1400+
TargetEntry *tle = (TargetEntry *) lfirst(lc);
1401+
1402+
if (tle->resjunk)
1403+
{
1404+
tle->resno = list_length(new_tlist) + 1;
1405+
new_tlist = lappend(new_tlist, tle);
1406+
}
1407+
}
1408+
13971409
return new_tlist;
13981410
}
13991411

src/backend/executor/nodeModifyTable.c

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2533,9 +2533,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
25332533
*/
25342534
if (node->onConflictAction == ONCONFLICT_UPDATE)
25352535
{
2536+
OnConflictSetState *onconfl = makeNode(OnConflictSetState);
25362537
ExprContext *econtext;
25372538
TupleDesc relationDesc;
2538-
TupleDesc tupDesc;
25392539

25402540
/* insert may only have one plan, inheritance is not expanded */
25412541
Assert(nplans == 1);
@@ -2562,7 +2562,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
25622562
mtstate->mt_excludedtlist = node->exclRelTlist;
25632563

25642564
/* create state for DO UPDATE SET operation */
2565-
resultRelInfo->ri_onConflict = makeNode(OnConflictSetState);
2565+
resultRelInfo->ri_onConflict = onconfl;
25662566

25672567
/*
25682568
* Create the tuple slot for the UPDATE SET projection.
@@ -2573,19 +2573,27 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
25732573
* the tupdesc in the parent's state: it can be reused by partitions
25742574
* with an identical descriptor to the parent.
25752575
*/
2576-
tupDesc = ExecTypeFromTL((List *) node->onConflictSet,
2577-
relationDesc->tdhasoid);
25782576
mtstate->mt_conflproj =
25792577
ExecInitExtraTupleSlot(mtstate->ps.state,
25802578
mtstate->mt_partition_tuple_routing ?
2581-
NULL : tupDesc);
2582-
resultRelInfo->ri_onConflict->oc_ProjTupdesc = tupDesc;
2579+
NULL : relationDesc);
2580+
onconfl->oc_ProjTupdesc = relationDesc;
2581+
2582+
/*
2583+
* The onConflictSet tlist should already have been adjusted to emit
2584+
* the table's exact column list. It could also contain resjunk
2585+
* columns, which should be evaluated but not included in the
2586+
* projection result.
2587+
*/
2588+
ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
2589+
node->onConflictSet);
25832590

25842591
/* build UPDATE SET projection state */
2585-
resultRelInfo->ri_onConflict->oc_ProjInfo =
2586-
ExecBuildProjectionInfo(node->onConflictSet, econtext,
2587-
mtstate->mt_conflproj, &mtstate->ps,
2588-
relationDesc);
2592+
onconfl->oc_ProjInfo =
2593+
ExecBuildProjectionInfoExt(node->onConflictSet, econtext,
2594+
mtstate->mt_conflproj, false,
2595+
&mtstate->ps,
2596+
relationDesc);
25892597

25902598
/* initialize state to evaluate the WHERE clause, if any */
25912599
if (node->onConflictWhere)
@@ -2594,7 +2602,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
25942602

25952603
qualexpr = ExecInitQual((List *) node->onConflictWhere,
25962604
&mtstate->ps);
2597-
resultRelInfo->ri_onConflict->oc_WhereClause = qualexpr;
2605+
onconfl->oc_WhereClause = qualexpr;
25982606
}
25992607
}
26002608

src/include/executor/executor.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ extern ProjectionInfo *ExecBuildProjectionInfo(List *targetList,
269269
TupleTableSlot *slot,
270270
PlanState *parent,
271271
TupleDesc inputDesc);
272+
extern ProjectionInfo *ExecBuildProjectionInfoExt(List *targetList,
273+
ExprContext *econtext,
274+
TupleTableSlot *slot,
275+
bool assignJunkEntries,
276+
PlanState *parent,
277+
TupleDesc inputDesc);
272278
extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
273279
extern ExprState *ExecPrepareQual(List *qual, EState *estate);
274280
extern ExprState *ExecPrepareCheck(List *qual, EState *estate);

0 commit comments

Comments
 (0)