Skip to content

Commit 049e1e2

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 f02b908 commit 049e1e2

File tree

15 files changed

+265
-205
lines changed

15 files changed

+265
-205
lines changed

src/backend/access/heap/heapam.c

+8
Original file line numberDiff line numberDiff line change
@@ -2070,6 +2070,10 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
20702070
bool all_frozen_set = false;
20712071
uint8 vmstatus = 0;
20722072

2073+
/* Cheap, simplistic check that the tuple matches the rel's rowtype. */
2074+
Assert(HeapTupleHeaderGetNatts(tup->t_data) <=
2075+
RelationGetNumberOfAttributes(relation));
2076+
20732077
/*
20742078
* Fill in tuple header fields and toast the tuple if necessary.
20752079
*
@@ -3255,6 +3259,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
32553259

32563260
Assert(ItemPointerIsValid(otid));
32573261

3262+
/* Cheap, simplistic check that the tuple matches the rel's rowtype. */
3263+
Assert(HeapTupleHeaderGetNatts(newtup->t_data) <=
3264+
RelationGetNumberOfAttributes(relation));
3265+
32583266
/*
32593267
* Forbid this during a parallel operation, lest it allocate a combo CID.
32603268
* Other workers might need that combo CID for visibility checks, and we

src/backend/executor/execExpr.c

+83-29
Original file line numberDiff line numberDiff line change
@@ -485,14 +485,21 @@ ExecBuildProjectionInfo(List *targetList,
485485
* be stored into the given tuple slot. (Caller must have ensured that tuple
486486
* slot has a descriptor matching the target rel!)
487487
*
488-
* subTargetList is the tlist of the subplan node feeding ModifyTable.
489-
* We use this mainly to cross-check that the expressions being assigned
490-
* are of the correct types. The values from this tlist are assumed to be
491-
* available from the "outer" tuple slot. They are assigned to target columns
492-
* listed in the corresponding targetColnos elements. (Only non-resjunk tlist
493-
* entries are assigned.) Columns not listed in targetColnos are filled from
494-
* the UPDATE's old tuple, which is assumed to be available in the "scan"
495-
* tuple slot.
488+
* When evalTargetList is false, targetList contains the UPDATE ... SET
489+
* expressions that have already been computed by a subplan node; the values
490+
* from this tlist are assumed to be available in the "outer" tuple slot.
491+
* When evalTargetList is true, targetList contains the UPDATE ... SET
492+
* expressions that must be computed (which could contain references to
493+
* the outer, inner, or scan tuple slots).
494+
*
495+
* In either case, targetColnos contains a list of the target column numbers
496+
* corresponding to the non-resjunk entries of targetList. The tlist values
497+
* are assigned into these columns of the result tuple slot. Target columns
498+
* not listed in targetColnos are filled from the UPDATE's old tuple, which
499+
* is assumed to be available in the "scan" tuple slot.
500+
*
501+
* targetList can also contain resjunk columns. These must be evaluated
502+
* if evalTargetList is true, but their values are discarded.
496503
*
497504
* relDesc must describe the relation we intend to update.
498505
*
@@ -503,7 +510,8 @@ ExecBuildProjectionInfo(List *targetList,
503510
* ExecCheckPlanOutput, so we must do our safety checks here.
504511
*/
505512
ProjectionInfo *
506-
ExecBuildUpdateProjection(List *subTargetList,
513+
ExecBuildUpdateProjection(List *targetList,
514+
bool evalTargetList,
507515
List *targetColnos,
508516
TupleDesc relDesc,
509517
ExprContext *econtext,
@@ -525,19 +533,22 @@ ExecBuildUpdateProjection(List *subTargetList,
525533
/* We embed ExprState into ProjectionInfo instead of doing extra palloc */
526534
projInfo->pi_state.tag = T_ExprState;
527535
state = &projInfo->pi_state;
528-
state->expr = NULL; /* not used */
536+
if (evalTargetList)
537+
state->expr = (Expr *) targetList;
538+
else
539+
state->expr = NULL; /* not used */
529540
state->parent = parent;
530541
state->ext_params = NULL;
531542

532543
state->resultslot = slot;
533544

534545
/*
535-
* Examine the subplan tlist to see how many non-junk columns there are,
536-
* and to verify that the non-junk columns come before the junk ones.
546+
* Examine the targetList to see how many non-junk columns there are, and
547+
* to verify that the non-junk columns come before the junk ones.
537548
*/
538549
nAssignableCols = 0;
539550
sawJunk = false;
540-
foreach(lc, subTargetList)
551+
foreach(lc, targetList)
541552
{
542553
TargetEntry *tle = lfirst_node(TargetEntry, lc);
543554

@@ -569,12 +580,10 @@ ExecBuildUpdateProjection(List *subTargetList,
569580
}
570581

571582
/*
572-
* We want to insert EEOP_*_FETCHSOME steps to ensure the outer and scan
573-
* tuples are sufficiently deconstructed. Outer tuple is easy, but for
574-
* scan tuple we must find out the last old column we need.
583+
* We need to insert EEOP_*_FETCHSOME steps to ensure the input tuples are
584+
* sufficiently deconstructed. The scan tuple must be deconstructed at
585+
* least as far as the last old column we need.
575586
*/
576-
deform.last_outer = nAssignableCols;
577-
578587
for (int attnum = relDesc->natts; attnum > 0; attnum--)
579588
{
580589
Form_pg_attribute attr = TupleDescAttr(relDesc, attnum - 1);
@@ -587,15 +596,26 @@ ExecBuildUpdateProjection(List *subTargetList,
587596
break;
588597
}
589598

599+
/*
600+
* If we're actually evaluating the tlist, incorporate its input
601+
* requirements too; otherwise, we'll just need to fetch the appropriate
602+
* number of columns of the "outer" tuple.
603+
*/
604+
if (evalTargetList)
605+
get_last_attnums_walker((Node *) targetList, &deform);
606+
else
607+
deform.last_outer = nAssignableCols;
608+
590609
ExecPushExprSlots(state, &deform);
591610

592611
/*
593-
* Now generate code to fetch data from the outer tuple, incidentally
594-
* validating that it'll be of the right type. The checks above ensure
595-
* that the forboth() will iterate over exactly the non-junk columns.
612+
* Now generate code to evaluate the tlist's assignable expressions or
613+
* fetch them from the outer tuple, incidentally validating that they'll
614+
* be of the right data type. The checks above ensure that the forboth()
615+
* will iterate over exactly the non-junk columns.
596616
*/
597617
outerattnum = 0;
598-
forboth(lc, subTargetList, lc2, targetColnos)
618+
forboth(lc, targetList, lc2, targetColnos)
599619
{
600620
TargetEntry *tle = lfirst_node(TargetEntry, lc);
601621
AttrNumber targetattnum = lfirst_int(lc2);
@@ -628,13 +648,47 @@ ExecBuildUpdateProjection(List *subTargetList,
628648
targetattnum,
629649
format_type_be(exprType((Node *) tle->expr)))));
630650

631-
/*
632-
* OK, build an outer-tuple reference.
633-
*/
634-
scratch.opcode = EEOP_ASSIGN_OUTER_VAR;
635-
scratch.d.assign_var.attnum = outerattnum++;
636-
scratch.d.assign_var.resultnum = targetattnum - 1;
637-
ExprEvalPushStep(state, &scratch);
651+
/* OK, generate code to perform the assignment. */
652+
if (evalTargetList)
653+
{
654+
/*
655+
* We must evaluate the TLE's expression and assign it. We do not
656+
* bother jumping through hoops for "safe" Vars like
657+
* ExecBuildProjectionInfo does; this is a relatively less-used
658+
* path and it doesn't seem worth expending code for that.
659+
*/
660+
ExecInitExprRec(tle->expr, state,
661+
&state->resvalue, &state->resnull);
662+
/* Needn't worry about read-only-ness here, either. */
663+
scratch.opcode = EEOP_ASSIGN_TMP;
664+
scratch.d.assign_tmp.resultnum = targetattnum - 1;
665+
ExprEvalPushStep(state, &scratch);
666+
}
667+
else
668+
{
669+
/* Just assign from the outer tuple. */
670+
scratch.opcode = EEOP_ASSIGN_OUTER_VAR;
671+
scratch.d.assign_var.attnum = outerattnum;
672+
scratch.d.assign_var.resultnum = targetattnum - 1;
673+
ExprEvalPushStep(state, &scratch);
674+
}
675+
outerattnum++;
676+
}
677+
678+
/*
679+
* If we're evaluating the tlist, must evaluate any resjunk columns too.
680+
* (This matters for things like MULTIEXPR_SUBLINK SubPlans.)
681+
*/
682+
if (evalTargetList)
683+
{
684+
for_each_cell(lc, targetList, lc)
685+
{
686+
TargetEntry *tle = lfirst_node(TargetEntry, lc);
687+
688+
Assert(tle->resjunk);
689+
ExecInitExprRec(tle->expr, state,
690+
&state->resvalue, &state->resnull);
691+
}
638692
}
639693

640694
/*

src/backend/executor/execExprInterp.c

+9-1
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
626626
* care of at compilation time. But see EEOP_INNER_VAR comments.
627627
*/
628628
Assert(attnum >= 0 && attnum < innerslot->tts_nvalid);
629+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
629630
resultslot->tts_values[resultnum] = innerslot->tts_values[attnum];
630631
resultslot->tts_isnull[resultnum] = innerslot->tts_isnull[attnum];
631632

@@ -642,6 +643,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
642643
* care of at compilation time. But see EEOP_INNER_VAR comments.
643644
*/
644645
Assert(attnum >= 0 && attnum < outerslot->tts_nvalid);
646+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
645647
resultslot->tts_values[resultnum] = outerslot->tts_values[attnum];
646648
resultslot->tts_isnull[resultnum] = outerslot->tts_isnull[attnum];
647649

@@ -658,6 +660,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
658660
* care of at compilation time. But see EEOP_INNER_VAR comments.
659661
*/
660662
Assert(attnum >= 0 && attnum < scanslot->tts_nvalid);
663+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
661664
resultslot->tts_values[resultnum] = scanslot->tts_values[attnum];
662665
resultslot->tts_isnull[resultnum] = scanslot->tts_isnull[attnum];
663666

@@ -668,6 +671,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
668671
{
669672
int resultnum = op->d.assign_tmp.resultnum;
670673

674+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
671675
resultslot->tts_values[resultnum] = state->resvalue;
672676
resultslot->tts_isnull[resultnum] = state->resnull;
673677

@@ -678,6 +682,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
678682
{
679683
int resultnum = op->d.assign_tmp.resultnum;
680684

685+
Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
681686
resultslot->tts_isnull[resultnum] = state->resnull;
682687
if (!resultslot->tts_isnull[resultnum])
683688
resultslot->tts_values[resultnum] =
@@ -2091,8 +2096,10 @@ ExecJustAssignVarImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull)
20912096
*
20922097
* Since we use slot_getattr(), we don't need to implement the FETCHSOME
20932098
* step explicitly, and we also needn't Assert that the attnum is in range
2094-
* --- slot_getattr() will take care of any problems.
2099+
* --- slot_getattr() will take care of any problems. Nonetheless, check
2100+
* that resultnum is in range.
20952101
*/
2102+
Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
20962103
outslot->tts_values[resultnum] =
20972104
slot_getattr(inslot, attnum, &outslot->tts_isnull[resultnum]);
20982105
return 0;
@@ -2224,6 +2231,7 @@ ExecJustAssignVarVirtImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull
22242231
Assert(TTS_IS_VIRTUAL(inslot));
22252232
Assert(TTS_FIXED(inslot));
22262233
Assert(attnum >= 0 && attnum < inslot->tts_nvalid);
2234+
Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
22272235

22282236
outslot->tts_values[resultnum] = inslot->tts_values[attnum];
22292237
outslot->tts_isnull[resultnum] = inslot->tts_isnull[attnum];

0 commit comments

Comments
 (0)