diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2007-02-02 00:08:01 +0000 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2007-02-02 00:08:01 +0000 |
commit | 7fbfa4e96a3fc2012bbdb394a2b7ada846770ce3 (patch) | |
tree | 4e49fa7deffa4d2839ec3d5715ac0890f62be5f3 | |
parent | f67497141888dfeea36401ba2a93d9bd74a6ecd7 (diff) | |
download | postgresql-7fbfa4e96a3fc2012bbdb394a2b7ada846770ce3.tar.gz postgresql-7fbfa4e96a3fc2012bbdb394a2b7ada846770ce3.zip |
Repair failure to check that a table is still compatible with a previously
made query plan. Use of ALTER COLUMN TYPE creates a hazard for cached
query plans: they could contain Vars that claim a column has a different
type than it now has. Fix this by checking during plan startup that Vars
at relation scan level match the current relation tuple descriptor. Since
at that point we already have at least AccessShareLock, we can be sure the
column type will not change underneath us later in the query. However,
since a backend's locks do not conflict against itself, there is still a
hole for an attacker to exploit: he could try to execute ALTER COLUMN TYPE
while a query is in progress in the current backend. Seal that hole by
rejecting ALTER TABLE whenever the target relation is already open in
the current backend.
This is a significant security hole: not only can one trivially crash the
backend, but with appropriate misuse of pass-by-reference datatypes it is
possible to read out arbitrary locations in the server process's memory,
which could allow retrieving database content the user should not be able
to see. Our thanks to Jeff Trout for the initial report.
Security: CVE-2007-0556
-rw-r--r-- | src/backend/commands/tablecmds.c | 57 | ||||
-rw-r--r-- | src/backend/executor/execQual.c | 277 | ||||
-rw-r--r-- | src/backend/executor/execScan.c | 9 |
3 files changed, 262 insertions, 81 deletions
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cf665a6eb4f..ea62f7db454 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/commands/tablecmds.c,v 1.142.4.6 2006/07/10 22:10:57 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/commands/tablecmds.c,v 1.142.4.7 2007/02/02 00:08:01 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1812,22 +1812,47 @@ update_ri_trigger_args(Oid relid, void AlterTable(AlterTableStmt *stmt) { - ATController(relation_openrv(stmt->relation, AccessExclusiveLock), - stmt->cmds, - interpretInhOption(stmt->relation->inhOpt)); + Relation rel = relation_openrv(stmt->relation, AccessExclusiveLock); + int expected_refcnt; + + /* + * Disallow ALTER TABLE when the current backend has any open reference + * to it besides the one we just got (such as an open cursor or active + * plan); our AccessExclusiveLock doesn't protect us against stomping on + * our own foot, only other people's feet! + * + * Note: the only case known to cause serious trouble is ALTER COLUMN TYPE, + * and some changes are obviously pretty benign, so this could possibly + * be relaxed to only error out for certain types of alterations. But + * the use-case for allowing any of these things is not obvious, so we + * won't work hard at it for now. + */ + expected_refcnt = rel->rd_isnailed ? 2 : 1; + if (rel->rd_refcnt != expected_refcnt) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("relation \"%s\" is being used by active queries in this session", + RelationGetRelationName(rel)))); + + ATController(rel, stmt->cmds, interpretInhOption(stmt->relation->inhOpt)); } /* * AlterTableInternal * * ALTER TABLE with target specified by OID + * + * We do not reject if the relation is already open, because it's quite + * likely that one or more layers of caller have it open. That means it + * is unsafe to use this entry point for alterations that could break + * existing query plans. */ void AlterTableInternal(Oid relid, List *cmds, bool recurse) { - ATController(relation_open(relid, AccessExclusiveLock), - cmds, - recurse); + Relation rel = relation_open(relid, AccessExclusiveLock); + + ATController(rel, cmds, recurse); } static void @@ -2714,6 +2739,12 @@ ATSimpleRecursion(List **wqueue, Relation rel, if (childrelid == relid) continue; childrel = relation_open(childrelid, AccessExclusiveLock); + /* check for child relation in use in this session */ + if (childrel->rd_refcnt != 1) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("relation \"%s\" is being used by active queries in this session", + RelationGetRelationName(childrel)))); ATPrepCmd(wqueue, childrel, cmd, false, true); relation_close(childrel, NoLock); } @@ -2745,6 +2776,12 @@ ATOneLevelRecursion(List **wqueue, Relation rel, Relation childrel; childrel = relation_open(childrelid, AccessExclusiveLock); + /* check for child relation in use in this session */ + if (childrel->rd_refcnt != 1) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("relation \"%s\" is being used by active queries in this session", + RelationGetRelationName(childrel)))); ATPrepCmd(wqueue, childrel, cmd, true, true); relation_close(childrel, NoLock); } @@ -3568,6 +3605,12 @@ ATExecDropColumn(Relation rel, const char *colName, Form_pg_attribute childatt; childrel = heap_open(childrelid, AccessExclusiveLock); + /* check for child relation in use in this session */ + if (childrel->rd_refcnt != 1) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("relation \"%s\" is being used by active queries in this session", + RelationGetRelationName(childrel)))); tuple = SearchSysCacheCopyAttName(childrelid, colName); if (!HeapTupleIsValid(tuple)) /* shouldn't happen */ diff --git a/src/backend/executor/execQual.c b/src/backend/executor/execQual.c index 91271423b0f..be3e1c12188 100644 --- a/src/backend/executor/execQual.c +++ b/src/backend/executor/execQual.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/execQual.c,v 1.171.4.2 2006/11/06 18:21:47 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/executor/execQual.c,v 1.171.4.3 2007/02/02 00:08:01 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -63,6 +63,8 @@ static Datum ExecEvalAggref(AggrefExprState *aggref, bool *isNull, ExprDoneCond *isDone); static Datum ExecEvalVar(ExprState *exprstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); +static Datum ExecEvalScalarVar(ExprState *exprstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone); static Datum ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone); static Datum ExecEvalConst(ExprState *exprstate, ExprContext *econtext, @@ -433,6 +435,10 @@ ExecEvalAggref(AggrefExprState *aggref, ExprContext *econtext, * * Returns a Datum whose value is the value of a range * variable with respect to given expression context. + * + * Note: ExecEvalVar is executed only the first time through in a given plan; + * it changes the ExprState's function pointer to pass control directly to + * ExecEvalScalarVar or ExecEvalWholeRowVar after making one-time checks. * ---------------------------------------------------------------- */ static Datum @@ -440,17 +446,15 @@ ExecEvalVar(ExprState *exprstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone) { Var *variable = (Var *) exprstate->expr; - Datum result; TupleTableSlot *slot; AttrNumber attnum; - HeapTuple heapTuple; - TupleDesc tuple_type; + TupleDesc slot_tupdesc; if (isDone) *isDone = ExprSingleResult; /* - * Get the slot and attribute number we want + * Get the input slot and attribute number we want * * The asserts check that references to system attributes only appear at * the level of a relation scan; at higher levels, system attributes @@ -480,63 +484,194 @@ ExecEvalVar(ExprState *exprstate, ExprContext *econtext, /* * extract tuple information from the slot */ - heapTuple = slot->val; - tuple_type = slot->ttc_tupleDescriptor; + slot_tupdesc = slot->ttc_tupleDescriptor; - /* - * Some checks that are only applied for user attribute numbers (bogus - * system attnums will be caught inside heap_getattr). - */ - if (attnum > 0) + if (attnum != InvalidAttrNumber) { /* - * This assert checks that the attnum is valid. - */ - Assert(attnum <= tuple_type->natts && - tuple_type->attrs[attnum - 1] != NULL); - - /* - * If the attribute's column has been dropped, we force a NULL - * result. This case should not happen in normal use, but it could - * happen if we are executing a plan cached before the column was - * dropped. + * Scalar variable case. + * + * If it's a user attribute, check validity (bogus system attnums will + * be caught inside heap_getattr). What we have to check for here + * is the possibility of an attribute having been changed in type + * since the plan tree was created. Ideally the plan would get + * invalidated and not re-used, but until that day arrives, we need + * defenses. Fortunately it's sufficient to check once on the first + * time through. + * + * Note: we allow a reference to a dropped attribute, and force a + * NULL result. This path is not fast. + * + * Note: we check typmod, but allow the case that the Var has + * unspecified typmod while the column has a specific typmod. */ - if (tuple_type->attrs[attnum - 1]->attisdropped) + if (attnum > 0) { - *isNull = true; - return (Datum) 0; + Form_pg_attribute attr; + + if (attnum > slot_tupdesc->natts) /* should never happen */ + elog(ERROR, "attribute number %d exceeds number of columns %d", + attnum, slot_tupdesc->natts); + + attr = slot_tupdesc->attrs[attnum - 1]; + + /* + * If the attribute's column has been dropped, we force a NULL + * result. This case should not happen in normal use, but it could + * happen if we are executing a plan cached before the column was + * dropped. + * + * Can't check type if dropped, since atttypid is probably 0 + */ + if (attr->attisdropped) + { + *isNull = true; + return (Datum) 0; + } + + if (variable->vartype != attr->atttypid || + (variable->vartypmod != attr->atttypmod && + variable->vartypmod != -1)) + ereport(ERROR, + (errmsg("attribute %d has wrong type", attnum), + errdetail("Table has type %s, but query expects %s.", + format_type_be(attr->atttypid), + format_type_be(variable->vartype)))); } + /* Skip the checking on future executions of node */ + exprstate->evalfunc = ExecEvalScalarVar; + + /* Fetch the value */ + return ExecEvalScalarVar(exprstate, econtext, isNull, isDone); + } + else + { /* - * This assert checks that the datatype the plan expects to get - * (as told by our "variable" argument) is in fact the datatype of - * the attribute being fetched (as seen in the current context, - * identified by our "econtext" argument). Otherwise crashes are - * likely. + * Whole-row variable. * - * Note that we can't check dropped columns, since their atttypid has - * been zeroed. + * If it's a RECORD Var, we'll use the slot's type ID info. It's + * likely that the slot's type is also RECORD; if so, make sure it's + * been "blessed", so that the Datum can be interpreted later. + * + * If the Var identifies a named composite type, we must check that + * the actual tuple type is compatible with it. */ - Assert(variable->vartype == tuple_type->attrs[attnum - 1]->atttypid); + if (variable->vartype == RECORDOID) + { + if (slot_tupdesc->tdtypeid == RECORDOID && + slot_tupdesc->tdtypmod < 0) + assign_record_type_typmod(slot_tupdesc); + } + else + { + TupleDesc var_tupdesc; + int i; + + /* + * We really only care about number of attributes and data type. + * Also, we can ignore type mismatch on columns that are dropped + * in the destination type, so long as the physical storage + * matches. This is helpful in some cases involving out-of-date + * cached plans. + */ + var_tupdesc = lookup_rowtype_tupdesc(variable->vartype, -1); + + if (var_tupdesc->natts != slot_tupdesc->natts) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Table row contains %d attributes, but query expects %d.", + slot_tupdesc->natts, var_tupdesc->natts))); + + for (i = 0; i < var_tupdesc->natts; i++) + { + Form_pg_attribute vattr = var_tupdesc->attrs[i]; + Form_pg_attribute sattr = slot_tupdesc->attrs[i]; + + if (vattr->atttypid == sattr->atttypid) + continue; /* no worries */ + if (!vattr->attisdropped) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Table has type %s at ordinal position %d, but query expects %s.", + format_type_be(sattr->atttypid), + i + 1, + format_type_be(vattr->atttypid)))); + + if (vattr->attlen != sattr->attlen || + vattr->attalign != sattr->attalign) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("table row type and query-specified row type do not match"), + errdetail("Physical storage mismatch on dropped attribute at ordinal position %d.", + i + 1))); + } + } + + /* Skip the checking on future executions of node */ + exprstate->evalfunc = ExecEvalWholeRowVar; + + /* Fetch the value */ + return ExecEvalWholeRowVar(exprstate, econtext, isNull, isDone); } +} - result = heap_getattr(heapTuple, /* tuple containing attribute */ - attnum, /* attribute number of desired - * attribute */ - tuple_type, /* tuple descriptor of tuple */ - isNull); /* return: is attribute null? */ +/* ---------------------------------------------------------------- + * ExecEvalScalarVar + * + * Returns a Datum for a scalar variable. + * ---------------------------------------------------------------- + */ +static Datum +ExecEvalScalarVar(ExprState *exprstate, ExprContext *econtext, + bool *isNull, ExprDoneCond *isDone) +{ + Var *variable = (Var *) exprstate->expr; + TupleTableSlot *slot; + AttrNumber attnum; + HeapTuple heapTuple; + TupleDesc slot_tupdesc; - return result; + if (isDone) + *isDone = ExprSingleResult; + + /* Get the input slot and attribute number we want */ + switch (variable->varno) + { + case INNER: /* get the tuple from the inner node */ + slot = econtext->ecxt_innertuple; + break; + + case OUTER: /* get the tuple from the outer node */ + slot = econtext->ecxt_outertuple; + break; + + default: /* get the tuple from the relation being + * scanned */ + slot = econtext->ecxt_scantuple; + break; + } + + attnum = variable->varattno; + + /* extract tuple information from the slot */ + heapTuple = slot->val; + slot_tupdesc = slot->ttc_tupleDescriptor; + + /* Fetch the value from the tuple */ + return heap_getattr(heapTuple, /* tuple containing attribute */ + attnum, /* attribute number of desired + * attribute */ + slot_tupdesc, /* tuple descriptor of tuple */ + isNull); /* return: is attribute null? */ } /* ---------------------------------------------------------------- * ExecEvalWholeRowVar * * Returns a Datum for a whole-row variable. - * - * This could be folded into ExecEvalVar, but we make it a separate - * routine so as not to slow down ExecEvalVar with tests for this - * uncommon case. * ---------------------------------------------------------------- */ static Datum @@ -544,7 +679,7 @@ ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, bool *isNull, ExprDoneCond *isDone) { Var *variable = (Var *) exprstate->expr; - TupleTableSlot *slot; + TupleTableSlot *slot = econtext->ecxt_scantuple; HeapTuple tuple; TupleDesc tupleDesc; HeapTupleHeader dtuple; @@ -553,16 +688,6 @@ ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, *isDone = ExprSingleResult; *isNull = false; - Assert(variable->varattno == InvalidAttrNumber); - - /* - * Whole-row Vars can only appear at the level of a relation scan, - * never in a join. - */ - Assert(variable->varno != INNER); - Assert(variable->varno != OUTER); - slot = econtext->ecxt_scantuple; - tuple = slot->val; tupleDesc = slot->ttc_tupleDescriptor; @@ -578,10 +703,6 @@ ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, /* * If the Var identifies a named composite type, label the tuple * with that type; otherwise use what is in the tupleDesc. - * - * It's likely that the slot's tupleDesc is a record type; if so, - * make sure it's been "blessed", so that the Datum can be interpreted - * later. */ if (variable->vartype != RECORDOID) { @@ -590,9 +711,6 @@ ExecEvalWholeRowVar(ExprState *exprstate, ExprContext *econtext, } else { - if (tupleDesc->tdtypeid == RECORDOID && - tupleDesc->tdtypmod < 0) - assign_record_type_typmod(tupleDesc); HeapTupleHeaderSetTypeId(dtuple, tupleDesc->tdtypeid); HeapTupleHeaderSetTypMod(dtuple, tupleDesc->tdtypmod); } @@ -2686,12 +2804,14 @@ ExecEvalFieldSelect(FieldSelectState *fstate, ExprDoneCond *isDone) { FieldSelect *fselect = (FieldSelect *) fstate->xprstate.expr; + AttrNumber fieldnum = fselect->fieldnum; Datum result; Datum tupDatum; HeapTupleHeader tuple; Oid tupType; int32 tupTypmod; TupleDesc tupDesc; + Form_pg_attribute attr; HeapTupleData tmptup; tupDatum = ExecEvalExpr(fstate->arg, econtext, isNull, isDone); @@ -2723,6 +2843,28 @@ ExecEvalFieldSelect(FieldSelectState *fstate, MemoryContextSwitchTo(oldcontext); } + /* Check for dropped column, and force a NULL result if so */ + if (fieldnum <= 0 || + fieldnum > tupDesc->natts) /* should never happen */ + elog(ERROR, "attribute number %d exceeds number of columns %d", + fieldnum, tupDesc->natts); + attr = tupDesc->attrs[fieldnum - 1]; + if (attr->attisdropped) + { + *isNull = true; + return (Datum) 0; + } + + /* Check for type mismatch --- possible after ALTER COLUMN TYPE? */ + if (fselect->resulttype != attr->atttypid || + (fselect->resulttypmod != attr->atttypmod && + fselect->resulttypmod != -1)) + ereport(ERROR, + (errmsg("attribute %d has wrong type", fieldnum), + errdetail("Table has type %s, but query expects %s.", + format_type_be(attr->atttypid), + format_type_be(fselect->resulttype)))); + /* * heap_getattr needs a HeapTuple not a bare HeapTupleHeader. We set * all the fields in the struct just in case user tries to inspect @@ -2734,7 +2876,7 @@ ExecEvalFieldSelect(FieldSelectState *fstate, tmptup.t_data = tuple; result = heap_getattr(&tmptup, - fselect->fieldnum, + fieldnum, tupDesc, isNull); return result; @@ -2936,15 +3078,8 @@ ExecInitExpr(Expr *node, PlanState *parent) switch (nodeTag(node)) { case T_Var: - { - Var *var = (Var *) node; - - state = (ExprState *) makeNode(ExprState); - if (var->varattno != InvalidAttrNumber) - state->evalfunc = ExecEvalVar; - else - state->evalfunc = ExecEvalWholeRowVar; - } + state = (ExprState *) makeNode(ExprState); + state->evalfunc = ExecEvalVar; break; case T_Const: state = (ExprState *) makeNode(ExprState); diff --git a/src/backend/executor/execScan.c b/src/backend/executor/execScan.c index 538b0faf370..b3053f06460 100644 --- a/src/backend/executor/execScan.c +++ b/src/backend/executor/execScan.c @@ -12,7 +12,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/executor/execScan.c,v 1.34.4.1 2007/01/24 01:26:02 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/executor/execScan.c,v 1.34.4.2 2007/02/02 00:08:01 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -208,6 +208,7 @@ tlist_matches_tupdesc(PlanState *ps, List *tlist, Index varno, TupleDesc tupdesc var = (Var *) ((TargetEntry *) lfirst(tlist_item))->expr; if (!var || !IsA(var, Var)) return false; /* tlist item not a Var */ + /* if these Asserts fail, planner messed up */ Assert(var->varno == varno); Assert(var->varlevelsup == 0); if (var->varattno != attrno) @@ -224,8 +225,10 @@ tlist_matches_tupdesc(PlanState *ps, List *tlist, Index varno, TupleDesc tupdesc * projection steps just to convert from specific typmod to typmod -1, * which is pretty silly. */ - Assert(var->vartype == att_tup->atttypid); - Assert(var->vartypmod == att_tup->atttypmod || var->vartypmod == -1); + if (var->vartype != att_tup->atttypid || + (var->vartypmod != att_tup->atttypmod && + var->vartypmod != -1)) + return false; /* type mismatch */ tlist_item = lnext(tlist_item); } |