diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2021-05-10 11:02:29 -0400 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2021-05-10 11:02:29 -0400 |
commit | 049e1e2edb06854d7cd9460c22516efaa165fbf8 (patch) | |
tree | 6c3812862c6d16f0bff46dda76e6e6b6c1ec9337 /src/backend/optimizer/prep/preptlist.c | |
parent | f02b9085ad2f6fefd9c5cdf85579cb9f0ff0f0ea (diff) | |
download | postgresql-049e1e2edb06854d7cd9460c22516efaa165fbf8.tar.gz postgresql-049e1e2edb06854d7cd9460c22516efaa165fbf8.zip |
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
Diffstat (limited to 'src/backend/optimizer/prep/preptlist.c')
-rw-r--r-- | src/backend/optimizer/prep/preptlist.c | 94 |
1 files changed, 20 insertions, 74 deletions
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c index 363132185d0..aefb6f8d4e8 100644 --- a/src/backend/optimizer/prep/preptlist.c +++ b/src/backend/optimizer/prep/preptlist.c @@ -46,9 +46,7 @@ #include "parser/parsetree.h" #include "utils/rel.h" -static List *extract_update_colnos(List *tlist); -static List *expand_targetlist(List *tlist, int command_type, - Index result_relation, Relation rel); +static List *expand_insert_targetlist(List *tlist, Relation rel); /* @@ -59,9 +57,6 @@ static List *expand_targetlist(List *tlist, int command_type, * Also, if this is an UPDATE, we return a list of target column numbers * in root->update_colnos. (Resnos in processed_tlist will be consecutive, * so do not look at that to find out which columns are targets!) - * - * As a side effect, if there's an ON CONFLICT UPDATE clause, its targetlist - * is also preprocessed (and updated in-place). */ void preprocess_targetlist(PlannerInfo *root) @@ -107,10 +102,9 @@ preprocess_targetlist(PlannerInfo *root) */ tlist = parse->targetList; if (command_type == CMD_INSERT) - tlist = expand_targetlist(tlist, command_type, - result_relation, target_relation); + tlist = expand_insert_targetlist(tlist, target_relation); else if (command_type == CMD_UPDATE) - root->update_colnos = extract_update_colnos(tlist); + root->update_colnos = extract_update_targetlist_colnos(tlist); /* * For non-inherited UPDATE/DELETE, register any junk column(s) needed to @@ -245,23 +239,12 @@ preprocess_targetlist(PlannerInfo *root) root->processed_tlist = tlist; - /* - * If there's an ON CONFLICT UPDATE clause, preprocess its targetlist too - * while we have the relation open. - */ - if (parse->onConflict) - parse->onConflict->onConflictSet = - expand_targetlist(parse->onConflict->onConflictSet, - CMD_UPDATE, - result_relation, - target_relation); - if (target_relation) table_close(target_relation, NoLock); } /* - * extract_update_colnos + * extract_update_targetlist_colnos * Extract a list of the target-table column numbers that * an UPDATE's targetlist wants to assign to, then renumber. * @@ -270,9 +253,12 @@ preprocess_targetlist(PlannerInfo *root) * to assign to. Here, we extract that info into a separate list, and * then convert the tlist to the sequential-numbering convention that's * used by all other query types. + * + * This is also applied to the tlist associated with INSERT ... ON CONFLICT + * ... UPDATE, although not till much later in planning. */ -static List * -extract_update_colnos(List *tlist) +List * +extract_update_targetlist_colnos(List *tlist) { List *update_colnos = NIL; AttrNumber nextresno = 1; @@ -297,18 +283,16 @@ extract_update_colnos(List *tlist) *****************************************************************************/ /* - * expand_targetlist + * expand_insert_targetlist * Given a target list as generated by the parser and a result relation, * add targetlist entries for any missing attributes, and ensure the * non-junk attributes appear in proper field order. * - * command_type is a bit of an archaism now: it's CMD_INSERT when we're - * processing an INSERT, all right, but the only other use of this function - * is for ON CONFLICT UPDATE tlists, for which command_type is CMD_UPDATE. + * Once upon a time we also did more or less this with UPDATE targetlists, + * but now this code is only applied to INSERT targetlists. */ static List * -expand_targetlist(List *tlist, int command_type, - Index result_relation, Relation rel) +expand_insert_targetlist(List *tlist, Relation rel) { List *new_tlist = NIL; ListCell *tlist_item; @@ -347,15 +331,11 @@ expand_targetlist(List *tlist, int command_type, /* * Didn't find a matching tlist entry, so make one. * - * For INSERT, generate a NULL constant. (We assume the rewriter - * would have inserted any available default value.) Also, if the - * column isn't dropped, apply any domain constraints that might - * exist --- this is to catch domain NOT NULL. - * - * For UPDATE, generate a Var reference to the existing value of - * the attribute, so that it gets copied to the new tuple. But - * generate a NULL for dropped columns (we want to drop any old - * values). + * INSERTs should insert NULL in this case. (We assume the + * rewriter would have inserted any available non-NULL default + * value.) Also, if the column isn't dropped, apply any domain + * constraints that might exist --- this is to catch domain NOT + * NULL. * * When generating a NULL constant for a dropped column, we label * it INT4 (any other guaranteed-to-exist datatype would do as @@ -367,13 +347,9 @@ expand_targetlist(List *tlist, int command_type, * relation, however. */ Oid atttype = att_tup->atttypid; - int32 atttypmod = att_tup->atttypmod; Oid attcollation = att_tup->attcollation; Node *new_expr; - switch (command_type) - { - case CMD_INSERT: if (!att_tup->attisdropped) { new_expr = (Node *) makeConst(atttype, @@ -402,35 +378,6 @@ expand_targetlist(List *tlist, int command_type, true, /* isnull */ true /* byval */ ); } - break; - case CMD_UPDATE: - if (!att_tup->attisdropped) - { - new_expr = (Node *) makeVar(result_relation, - attrno, - atttype, - atttypmod, - attcollation, - 0); - } - else - { - /* Insert NULL for dropped column */ - new_expr = (Node *) makeConst(INT4OID, - -1, - InvalidOid, - sizeof(int32), - (Datum) 0, - true, /* isnull */ - true /* byval */ ); - } - break; - default: - elog(ERROR, "unrecognized command_type: %d", - (int) command_type); - new_expr = NULL; /* keep compiler quiet */ - break; - } new_tle = makeTargetEntry((Expr *) new_expr, attrno, @@ -445,9 +392,8 @@ expand_targetlist(List *tlist, int command_type, * The remaining tlist entries should be resjunk; append them all to the * end of the new tlist, making sure they have resnos higher than the last * real attribute. (Note: although the rewriter already did such - * renumbering, we have to do it again here in case we are doing an UPDATE - * in a table with dropped columns, or an inheritance child table with - * extra columns.) + * renumbering, we have to do it again here in case we added NULL entries + * above.) */ while (tlist_item) { |