Add code to prevent transaction ID wraparound by enforcing a safe limit

in GetNewTransactionId().  Since the limit value has to be computed
before we run any real transactions, this requires adding code to database
startup to scan pg_database and determine the oldest datfrozenxid.
This can conveniently be combined with the first stage of an attack on
the problem that the 'flat file' copies of pg_shadow and pg_group are
not properly updated during WAL recovery.  The code I've added to
startup resides in a new file src/backend/utils/init/flatfiles.c, and
it is responsible for rewriting the flat files as well as initializing
the XID wraparound limit value.  This will eventually allow us to get
rid of GetRawDatabaseInfo too, but we'll need an initdb so we can add
a trigger to pg_database.
This commit is contained in:
Tom Lane 2005-02-20 02:22:07 +00:00
parent 617d16f4ff
commit 60b2444cc3
15 changed files with 1191 additions and 571 deletions

View File

@ -1,5 +1,5 @@
<!--
$PostgreSQL: pgsql/doc/src/sgml/maintenance.sgml,v 1.40 2004/12/27 22:30:10 tgl Exp $
$PostgreSQL: pgsql/doc/src/sgml/maintenance.sgml,v 1.41 2005/02/20 02:21:26 tgl Exp $
-->
<chapter id="maintenance">
@ -290,7 +290,7 @@ $PostgreSQL: pgsql/doc/src/sgml/maintenance.sgml,v 1.40 2004/12/27 22:30:10 tgl
transaction's XID is <quote>in the future</> and should not be visible
to the current transaction. But since transaction IDs have limited size
(32 bits at this writing) a cluster that runs for a long time (more
than 4 billion transactions) will suffer <firstterm>transaction ID
than 4 billion transactions) would suffer <firstterm>transaction ID
wraparound</>: the XID counter wraps around to zero, and all of a sudden
transactions that were in the past appear to be in the future &mdash; which
means their outputs become invisible. In short, catastrophic data loss.
@ -313,8 +313,13 @@ $PostgreSQL: pgsql/doc/src/sgml/maintenance.sgml,v 1.40 2004/12/27 22:30:10 tgl
In practice this isn't an onerous requirement, but since the
consequences of failing to meet it can be complete data loss (not
just wasted disk space or slow performance), some special provisions
have been made to help database administrators keep track of the
time since the last <command>VACUUM</>. The remainder of this
have been made to help database administrators avoid disaster.
For each database in the cluster, <productname>PostgreSQL</productname>
keeps track of the time of the last database-wide <command>VACUUM</>.
When any database approaches the billion-transaction danger level,
the system begins to emit warning messages. If nothing is done, it
will eventually shut down normal operations until appropriate
manual maintenance is done. The remainder of this
section gives the details.
</para>
@ -363,7 +368,8 @@ $PostgreSQL: pgsql/doc/src/sgml/maintenance.sgml,v 1.40 2004/12/27 22:30:10 tgl
statistics in the system table <literal>pg_database</>. In particular,
the <literal>datfrozenxid</> column of a database's
<literal>pg_database</> row is updated at the completion of any
database-wide <command>VACUUM</command> operation (i.e., <command>VACUUM</> that does not
database-wide <command>VACUUM</command> operation (i.e.,
<command>VACUUM</> that does not
name a specific table). The value stored in this field is the freeze
cutoff XID that was used by that <command>VACUUM</> command. All normal
XIDs older than this cutoff XID are guaranteed to have been replaced by
@ -391,12 +397,37 @@ SELECT datname, age(datfrozenxid) FROM pg_database;
<programlisting>
play=# VACUUM;
WARNING: some databases have not been vacuumed in 1613770184 transactions
HINT: Better vacuum them within 533713463 transactions, or you may have a wraparound failure.
WARNING: database "mydb" must be vacuumed within 177009986 transactions
HINT: To avoid a database shutdown, execute a full-database VACUUM in "mydb".
VACUUM
</programlisting>
</para>
<para>
If the warnings emitted by <command>VACUUM</> go ignored, then
<productname>PostgreSQL</productname> will begin to emit a warning
like the above on every transaction start once there are fewer than 10
million transactions left until wraparound. If those warnings also are
ignored, the system will shut down and refuse to execute any new
transactions once there are fewer than 1 million transactions left
until wraparound:
<programlisting>
play=# select 2+2;
ERROR: database is shut down to avoid wraparound data loss in database "mydb"
HINT: Stop the postmaster and use a standalone backend to VACUUM in "mydb".
</programlisting>
The 1-million-transaction safety margin exists to let the
administrator recover without data loss, by manually executing the
required <command>VACUUM</> commands. However, since the system will not
execute commands once it has gone into the safety shutdown mode,
the only way to do this is to stop the postmaster and use a standalone
backend to execute <command>VACUUM</>. The shutdown mode is not enforced
by a standalone backend. See the <xref linkend="app-postgres"> reference
page for details about using a standalone backend.
</para>
<para>
<command>VACUUM</> with the <command>FREEZE</> option uses a more
aggressive freezing policy: row versions are frozen if they are old enough
@ -410,26 +441,19 @@ VACUUM
It should also be used to prepare any user-created databases that
are to be marked <literal>datallowconn</> = <literal>false</> in
<literal>pg_database</>, since there isn't any convenient way to
<command>VACUUM</command> a database that you can't connect to. Note that
<command>VACUUM</command>'s automatic warning message about
unvacuumed databases will ignore <literal>pg_database</> entries
with <literal>datallowconn</> = <literal>false</>, so as to avoid
giving false warnings about these databases; therefore it's up to
you to ensure that such databases are frozen correctly.
<command>VACUUM</command> a database that you can't connect to.
</para>
<warning>
<para>
To be sure of safety against transaction wraparound, it is necessary
to vacuum <emphasis>every</> table, including system catalogs, in
<emphasis>every</> database at least once every billion transactions.
We have seen data loss situations caused by people deciding that they
only needed to vacuum their active user tables, rather than issuing
database-wide vacuum commands. That will appear to work fine ...
for a while.
A database that is marked <literal>datallowconn</> = <literal>false</>
in <literal>pg_database</> is assumed to be properly frozen; the
automatic warnings and wraparound protection shutdown do not take
such databases into account. Therefore it's up to you to ensure
you've correctly frozen a database before you mark it with
<literal>datallowconn</> = <literal>false</>.
</para>
</warning>
</sect2>
</sect1>

View File

@ -6,7 +6,7 @@
* Copyright (c) 2000-2005, PostgreSQL Global Development Group
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/access/transam/varsup.c,v 1.60 2005/01/01 05:43:06 momjian Exp $
* $PostgreSQL: pgsql/src/backend/access/transam/varsup.c,v 1.61 2005/02/20 02:21:28 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -16,8 +16,10 @@
#include "access/clog.h"
#include "access/subtrans.h"
#include "access/transam.h"
#include "miscadmin.h"
#include "storage/ipc.h"
#include "storage/proc.h"
#include "utils/builtins.h"
/* Number of OIDs to prefetch (preallocate) per XLOG write */
@ -46,6 +48,37 @@ GetNewTransactionId(bool isSubXact)
xid = ShmemVariableCache->nextXid;
/*
* Check to see if it's safe to assign another XID. This protects
* against catastrophic data loss due to XID wraparound. The basic
* rules are: warn if we're past xidWarnLimit, and refuse to execute
* transactions if we're past xidStopLimit, unless we are running in
* a standalone backend (which gives an escape hatch to the DBA who
* ignored all those warnings).
*
* Test is coded to fall out as fast as possible during normal operation,
* ie, when the warn limit is set and we haven't violated it.
*/
if (TransactionIdFollowsOrEquals(xid, ShmemVariableCache->xidWarnLimit) &&
TransactionIdIsValid(ShmemVariableCache->xidWarnLimit))
{
if (IsUnderPostmaster &&
TransactionIdFollowsOrEquals(xid, ShmemVariableCache->xidStopLimit))
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("database is shut down to avoid wraparound data loss in database \"%s\"",
NameStr(ShmemVariableCache->limit_datname)),
errhint("Stop the postmaster and use a standalone backend to VACUUM in \"%s\".",
NameStr(ShmemVariableCache->limit_datname))));
else
ereport(WARNING,
(errmsg("database \"%s\" must be vacuumed within %u transactions",
NameStr(ShmemVariableCache->limit_datname),
ShmemVariableCache->xidWrapLimit - xid),
errhint("To avoid a database shutdown, execute a full-database VACUUM in \"%s\".",
NameStr(ShmemVariableCache->limit_datname))));
}
/*
* If we are allocating the first XID of a new page of the commit log,
* zero out that commit-log page before returning. We must do this
@ -137,6 +170,84 @@ ReadNewTransactionId(void)
return xid;
}
/*
* Determine the last safe XID to allocate given the currently oldest
* datfrozenxid (ie, the oldest XID that might exist in any database
* of our cluster).
*/
void
SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
Name oldest_datname)
{
TransactionId xidWarnLimit;
TransactionId xidStopLimit;
TransactionId xidWrapLimit;
TransactionId curXid;
Assert(TransactionIdIsValid(oldest_datfrozenxid));
/*
* The place where we actually get into deep trouble is halfway around
* from the oldest potentially-existing XID. (This calculation is
* probably off by one or two counts, because the special XIDs reduce the
* size of the loop a little bit. But we throw in plenty of slop below,
* so it doesn't matter.)
*/
xidWrapLimit = oldest_datfrozenxid + (MaxTransactionId >> 1);
if (xidWrapLimit < FirstNormalTransactionId)
xidWrapLimit += FirstNormalTransactionId;
/*
* We'll refuse to continue assigning XIDs in interactive mode once
* we get within 1M transactions of data loss. This leaves lots
* of room for the DBA to fool around fixing things in a standalone
* backend, while not being significant compared to total XID space.
* (Note that since vacuuming requires one transaction per table
* cleaned, we had better be sure there's lots of XIDs left...)
*/
xidStopLimit = xidWrapLimit - 1000000;
if (xidStopLimit < FirstNormalTransactionId)
xidStopLimit -= FirstNormalTransactionId;
/*
* We'll start complaining loudly when we get within 10M transactions
* of the stop point. This is kind of arbitrary, but if you let your
* gas gauge get down to 1% of full, would you be looking for the
* next gas station? We need to be fairly liberal about this number
* because there are lots of scenarios where most transactions are
* done by automatic clients that won't pay attention to warnings.
* (No, we're not gonna make this configurable. If you know enough to
* configure it, you know enough to not get in this kind of trouble in
* the first place.)
*/
xidWarnLimit = xidStopLimit - 10000000;
if (xidWarnLimit < FirstNormalTransactionId)
xidWarnLimit -= FirstNormalTransactionId;
/* Grab lock for just long enough to set the new limit values */
LWLockAcquire(XidGenLock, LW_EXCLUSIVE);
ShmemVariableCache->xidWarnLimit = xidWarnLimit;
ShmemVariableCache->xidStopLimit = xidStopLimit;
ShmemVariableCache->xidWrapLimit = xidWrapLimit;
namecpy(&ShmemVariableCache->limit_datname, oldest_datname);
curXid = ShmemVariableCache->nextXid;
LWLockRelease(XidGenLock);
/* Log the info */
ereport(LOG,
(errmsg("transaction ID wrap limit is %u, limited by database \"%s\"",
xidWrapLimit, NameStr(*oldest_datname))));
/* Give an immediate warning if past the wrap warn point */
if (TransactionIdFollowsOrEquals(curXid, xidWarnLimit))
ereport(WARNING,
(errmsg("database \"%s\" must be vacuumed within %u transactions",
NameStr(*oldest_datname),
xidWrapLimit - curXid),
errhint("To avoid a database shutdown, execute a full-database VACUUM in \"%s\".",
NameStr(*oldest_datname))));
}
/* ----------------------------------------------------------------
* object id generation support
* ----------------------------------------------------------------

View File

@ -10,7 +10,7 @@
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/access/transam/xact.c,v 1.195 2004/12/31 21:59:29 pgsql Exp $
* $PostgreSQL: pgsql/src/backend/access/transam/xact.c,v 1.196 2005/02/20 02:21:28 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -28,7 +28,6 @@
#include "commands/async.h"
#include "commands/tablecmds.h"
#include "commands/trigger.h"
#include "commands/user.h"
#include "executor/spi.h"
#include "libpq/be-fsstubs.h"
#include "miscadmin.h"
@ -36,6 +35,7 @@
#include "storage/proc.h"
#include "storage/sinval.h"
#include "storage/smgr.h"
#include "utils/flatfiles.h"
#include "utils/guc.h"
#include "utils/inval.h"
#include "utils/memutils.h"
@ -1354,6 +1354,7 @@ StartTransaction(void)
* start processing
*/
s->state = TRANS_START;
s->transactionId = InvalidTransactionId; /* until assigned */
/*
* Make sure we've freed any old snapshot, and reset xact state
@ -1464,9 +1465,9 @@ CommitTransaction(void)
/* NOTIFY commit must come before lower-level cleanup */
AtCommit_Notify();
/* Update the flat password file if we changed pg_shadow or pg_group */
/* Update flat files if we changed pg_database, pg_shadow or pg_group */
/* This should be the last step before commit */
AtEOXact_UpdatePasswordFile(true);
AtEOXact_UpdateFlatFiles(true);
/* Prevent cancel/die interrupt while cleaning up */
HOLD_INTERRUPTS();
@ -1654,10 +1655,14 @@ AbortTransaction(void)
AtAbort_Portals();
AtEOXact_LargeObject(false); /* 'false' means it's abort */
AtAbort_Notify();
AtEOXact_UpdatePasswordFile(false);
AtEOXact_UpdateFlatFiles(false);
/* Advertise the fact that we aborted in pg_clog. */
RecordTransactionAbort();
/*
* Advertise the fact that we aborted in pg_clog (assuming that we
* got as far as assigning an XID to advertise).
*/
if (TransactionIdIsValid(s->transactionId))
RecordTransactionAbort();
/*
* Let others know about no transaction in progress by me. Note that
@ -2034,10 +2039,25 @@ AbortCurrentTransaction(void)
switch (s->blockState)
{
/*
* we aren't in a transaction, so we do nothing.
*/
case TBLOCK_DEFAULT:
if (s->state == TRANS_DEFAULT)
{
/* we are idle, so nothing to do */
}
else
{
/*
* We can get here after an error during transaction start
* (state will be TRANS_START). Need to clean up the
* incompletely started transaction. First, adjust the
* low-level state to suppress warning message from
* AbortTransaction.
*/
if (s->state == TRANS_START)
s->state = TRANS_INPROGRESS;
AbortTransaction();
CleanupTransaction();
}
break;
/*
@ -3277,8 +3297,8 @@ CommitSubTransaction(void)
AtEOSubXact_LargeObject(true, s->subTransactionId,
s->parent->subTransactionId);
AtSubCommit_Notify();
AtEOSubXact_UpdatePasswordFile(true, s->subTransactionId,
s->parent->subTransactionId);
AtEOSubXact_UpdateFlatFiles(true, s->subTransactionId,
s->parent->subTransactionId);
CallSubXactCallbacks(SUBXACT_EVENT_COMMIT_SUB, s->subTransactionId,
s->parent->subTransactionId);
@ -3387,8 +3407,8 @@ AbortSubTransaction(void)
AtEOSubXact_LargeObject(false, s->subTransactionId,
s->parent->subTransactionId);
AtSubAbort_Notify();
AtEOSubXact_UpdatePasswordFile(false, s->subTransactionId,
s->parent->subTransactionId);
AtEOSubXact_UpdateFlatFiles(false, s->subTransactionId,
s->parent->subTransactionId);
/* Advertise the fact that we aborted in pg_clog. */
if (TransactionIdIsValid(s->transactionId))

View File

@ -8,7 +8,7 @@
* Portions Copyright (c) 1994, Regents of the University of California
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/bootstrap/bootstrap.c,v 1.198 2005/01/14 21:08:44 tgl Exp $
* $PostgreSQL: pgsql/src/backend/bootstrap/bootstrap.c,v 1.199 2005/02/20 02:21:31 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -39,6 +39,7 @@
#include "storage/proc.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/flatfiles.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
@ -407,6 +408,7 @@ BootstrapMain(int argc, char *argv[])
bootstrap_signals();
StartupXLOG();
LoadFreeSpaceMap();
BuildFlatFiles(false);
proc_exit(0); /* startup done */
case BS_XLOG_BGWRITER:

View File

@ -9,7 +9,7 @@
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/commands/dbcommands.c,v 1.149 2005/01/27 23:23:55 neilc Exp $
* $PostgreSQL: pgsql/src/backend/commands/dbcommands.c,v 1.150 2005/02/20 02:21:34 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -39,6 +39,7 @@
#include "utils/acl.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/flatfiles.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
@ -506,6 +507,11 @@ createdb(const CreatedbStmt *stmt)
/* Close pg_database, but keep exclusive lock till commit */
heap_close(pg_database_rel, NoLock);
/*
* Set flag to update flat database file at commit.
*/
database_file_update_needed();
}
@ -642,6 +648,11 @@ dropdb(const char *dbname)
/* Close pg_database, but keep exclusive lock till commit */
heap_close(pgdbrel, NoLock);
/*
* Set flag to update flat database file at commit.
*/
database_file_update_needed();
}
@ -741,6 +752,11 @@ RenameDatabase(const char *oldname, const char *newname)
/* Close pg_database, but keep exclusive lock till commit */
heap_close(rel, NoLock);
/*
* Set flag to update flat database file at commit.
*/
database_file_update_needed();
}
@ -834,6 +850,11 @@ AlterDatabaseSet(AlterDatabaseSetStmt *stmt)
/* Close pg_database, but keep exclusive lock till commit */
heap_close(rel, NoLock);
/*
* We don't bother updating the flat file since ALTER DATABASE SET
* doesn't affect it.
*/
}
@ -933,6 +954,11 @@ AlterDatabaseOwner(const char *dbname, AclId newOwnerSysId)
/* Close pg_database, but keep exclusive lock till commit */
heap_close(rel, NoLock);
/*
* We don't bother updating the flat file since ALTER DATABASE OWNER
* doesn't affect it.
*/
}

View File

@ -6,17 +6,12 @@
* Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* $PostgreSQL: pgsql/src/backend/commands/user.c,v 1.148 2005/01/27 23:23:56 neilc Exp $
* $PostgreSQL: pgsql/src/backend/commands/user.c,v 1.149 2005/02/20 02:21:34 tgl Exp $
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include "access/heapam.h"
#include "catalog/catname.h"
#include "catalog/indexing.h"
@ -27,50 +22,17 @@
#include "commands/user.h"
#include "libpq/crypt.h"
#include "miscadmin.h"
#include "storage/fd.h"
#include "storage/pmsignal.h"
#include "utils/acl.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/flatfiles.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
#define PWD_FILE "pg_pwd"
#define USER_GROUP_FILE "pg_group"
extern bool Password_encryption;
/*
* The need-to-update-files flags are a pair of SubTransactionIds that show
* what level of the subtransaction tree requested the update. To register
* an update, the subtransaction saves its own SubTransactionId in the flag,
* unless the value was already set to a valid SubTransactionId (which implies
* that it or a parent level has already requested the same). If it aborts
* and the value is its SubTransactionId, it resets the flag to
* InvalidSubTransactionId. If it commits, it changes the value to its
* parent's SubTransactionId. This way the value is propagated up to the
* top-level transaction, which will update the files if a valid
* SubTransactionId is detected.
*/
static SubTransactionId user_file_update_subid = InvalidSubTransactionId;
static SubTransactionId group_file_update_subid = InvalidSubTransactionId;
#define user_file_update_needed() \
do { \
if (user_file_update_subid == InvalidSubTransactionId) \
user_file_update_subid = GetCurrentSubTransactionId(); \
} while (0)
#define group_file_update_needed() \
do { \
if (group_file_update_subid == InvalidSubTransactionId) \
group_file_update_subid = GetCurrentSubTransactionId(); \
} while (0)
static void CheckPgUserAclNotNull(void);
static void UpdateGroupMembership(Relation group_rel, HeapTuple group_tuple,
@ -79,454 +41,6 @@ static IdList *IdListToArray(List *members);
static List *IdArrayToList(IdList *oldarray);
/*
* fputs_quote
*
* Outputs string in quotes, with double-quotes duplicated.
* We could use quote_ident(), but that expects a TEXT argument.
*/
static void
fputs_quote(char *str, FILE *fp)
{
fputc('"', fp);
while (*str)
{
fputc(*str, fp);
if (*str == '"')
fputc('"', fp);
str++;
}
fputc('"', fp);
}
/*
* group_getfilename --- get full pathname of group file
*
* Note that result string is palloc'd, and should be freed by the caller.
*/
char *
group_getfilename(void)
{
int bufsize;
char *pfnam;
bufsize = strlen(DataDir) + strlen("/global/") +
strlen(USER_GROUP_FILE) + 1;
pfnam = (char *) palloc(bufsize);
snprintf(pfnam, bufsize, "%s/global/%s", DataDir, USER_GROUP_FILE);
return pfnam;
}
/*
* Get full pathname of password file.
*
* Note that result string is palloc'd, and should be freed by the caller.
*/
char *
user_getfilename(void)
{
int bufsize;
char *pfnam;
bufsize = strlen(DataDir) + strlen("/global/") +
strlen(PWD_FILE) + 1;
pfnam = (char *) palloc(bufsize);
snprintf(pfnam, bufsize, "%s/global/%s", DataDir, PWD_FILE);
return pfnam;
}
/*
* write_group_file: update the flat group file
*/
static void
write_group_file(Relation grel)
{
char *filename,
*tempname;
int bufsize;
FILE *fp;
mode_t oumask;
HeapScanDesc scan;
HeapTuple tuple;
TupleDesc dsc = RelationGetDescr(grel);
/*
* Create a temporary filename to be renamed later. This prevents the
* backend from clobbering the pg_group file while the postmaster
* might be reading from it.
*/
filename = group_getfilename();
bufsize = strlen(filename) + 12;
tempname = (char *) palloc(bufsize);
snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid);
oumask = umask((mode_t) 077);
fp = AllocateFile(tempname, "w");
umask(oumask);
if (fp == NULL)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m", tempname)));
/*
* Read pg_group and write the file. Note we use SnapshotSelf to
* ensure we see all effects of current transaction. (Perhaps could
* do a CommandCounterIncrement beforehand, instead?)
*/
scan = heap_beginscan(grel, SnapshotSelf, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Datum datum,
grolist_datum;
bool isnull;
char *groname;
IdList *grolist_p;
AclId *aidp;
int i,
j,
num;
char *usename;
bool first_user = true;
datum = heap_getattr(tuple, Anum_pg_group_groname, dsc, &isnull);
/* ignore NULL groupnames --- shouldn't happen */
if (isnull)
continue;
groname = NameStr(*DatumGetName(datum));
/*
* Check for invalid characters in the group name.
*/
i = strcspn(groname, "\n");
if (groname[i] != '\0')
{
ereport(LOG,
(errmsg("invalid group name \"%s\"", groname)));
continue;
}
grolist_datum = heap_getattr(tuple, Anum_pg_group_grolist, dsc, &isnull);
/* Ignore NULL group lists */
if (isnull)
continue;
/* be sure the IdList is not toasted */
grolist_p = DatumGetIdListP(grolist_datum);
/* scan grolist */
num = IDLIST_NUM(grolist_p);
aidp = IDLIST_DAT(grolist_p);
for (i = 0; i < num; ++i)
{
tuple = SearchSysCache(SHADOWSYSID,
PointerGetDatum(aidp[i]),
0, 0, 0);
if (HeapTupleIsValid(tuple))
{
usename = NameStr(((Form_pg_shadow) GETSTRUCT(tuple))->usename);
/*
* Check for illegal characters in the user name.
*/
j = strcspn(usename, "\n");
if (usename[j] != '\0')
{
ereport(LOG,
(errmsg("invalid user name \"%s\"", usename)));
continue;
}
/*
* File format is: "dbname" "user1" "user2" "user3"
*/
if (first_user)
{
fputs_quote(groname, fp);
fputs("\t", fp);
}
else
fputs(" ", fp);
first_user = false;
fputs_quote(usename, fp);
ReleaseSysCache(tuple);
}
}
if (!first_user)
fputs("\n", fp);
/* if IdList was toasted, free detoasted copy */
if ((Pointer) grolist_p != DatumGetPointer(grolist_datum))
pfree(grolist_p);
}
heap_endscan(scan);
if (FreeFile(fp))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Rename the temp file to its final name, deleting the old pg_pwd. We
* expect that rename(2) is an atomic action.
*/
if (rename(tempname, filename))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not rename file \"%s\" to \"%s\": %m",
tempname, filename)));
pfree(tempname);
pfree(filename);
}
/*
* write_user_file: update the flat password file
*/
static void
write_user_file(Relation urel)
{
char *filename,
*tempname;
int bufsize;
FILE *fp;
mode_t oumask;
HeapScanDesc scan;
HeapTuple tuple;
TupleDesc dsc = RelationGetDescr(urel);
/*
* Create a temporary filename to be renamed later. This prevents the
* backend from clobbering the pg_pwd file while the postmaster might
* be reading from it.
*/
filename = user_getfilename();
bufsize = strlen(filename) + 12;
tempname = (char *) palloc(bufsize);
snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid);
oumask = umask((mode_t) 077);
fp = AllocateFile(tempname, "w");
umask(oumask);
if (fp == NULL)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m", tempname)));
/*
* Read pg_shadow and write the file. Note we use SnapshotSelf to
* ensure we see all effects of current transaction. (Perhaps could
* do a CommandCounterIncrement beforehand, instead?)
*/
scan = heap_beginscan(urel, SnapshotSelf, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Datum datum;
bool isnull;
char *usename,
*passwd,
*valuntil;
int i;
datum = heap_getattr(tuple, Anum_pg_shadow_usename, dsc, &isnull);
/* ignore NULL usernames (shouldn't happen) */
if (isnull)
continue;
usename = NameStr(*DatumGetName(datum));
datum = heap_getattr(tuple, Anum_pg_shadow_passwd, dsc, &isnull);
/*
* It can be argued that people having a null password shouldn't
* be allowed to connect under password authentication, because
* they need to have a password set up first. If you think
* assuming an empty password in that case is better, change this
* logic to look something like the code for valuntil.
*/
if (isnull)
continue;
passwd = DatumGetCString(DirectFunctionCall1(textout, datum));
datum = heap_getattr(tuple, Anum_pg_shadow_valuntil, dsc, &isnull);
if (isnull)
valuntil = pstrdup("");
else
valuntil = DatumGetCString(DirectFunctionCall1(abstimeout, datum));
/*
* Check for illegal characters in the username and password.
*/
i = strcspn(usename, "\n");
if (usename[i] != '\0')
{
ereport(LOG,
(errmsg("invalid user name \"%s\"", usename)));
continue;
}
i = strcspn(passwd, "\n");
if (passwd[i] != '\0')
{
ereport(LOG,
(errmsg("invalid user password \"%s\"", passwd)));
continue;
}
/*
* The extra columns we emit here are not really necessary. To
* remove them, the parser in backend/libpq/crypt.c would need to
* be adjusted.
*/
fputs_quote(usename, fp);
fputs(" ", fp);
fputs_quote(passwd, fp);
fputs(" ", fp);
fputs_quote(valuntil, fp);
fputs("\n", fp);
pfree(passwd);
pfree(valuntil);
}
heap_endscan(scan);
if (FreeFile(fp))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Rename the temp file to its final name, deleting the old pg_pwd. We
* expect that rename(2) is an atomic action.
*/
if (rename(tempname, filename))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not rename file \"%s\" to \"%s\": %m",
tempname, filename)));
pfree(tempname);
pfree(filename);
}
/*
* This trigger is fired whenever someone modifies pg_shadow or pg_group
* via general-purpose INSERT/UPDATE/DELETE commands.
*
* XXX should probably have two separate triggers.
*/
Datum
update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS)
{
user_file_update_needed();
group_file_update_needed();
return PointerGetDatum(NULL);
}
/*
* This routine is called during transaction commit or abort.
*
* On commit, if we've written pg_shadow or pg_group during the current
* transaction, update the flat files and signal the postmaster.
*
* On abort, just reset the static flags so we don't try to do it on the
* next successful commit.
*
* NB: this should be the last step before actual transaction commit.
* If any error aborts the transaction after we run this code, the postmaster
* will still have received and cached the changed data; so minimize the
* window for such problems.
*/
void
AtEOXact_UpdatePasswordFile(bool isCommit)
{
Relation urel = NULL;
Relation grel = NULL;
if (user_file_update_subid == InvalidSubTransactionId &&
group_file_update_subid == InvalidSubTransactionId)
return;
if (!isCommit)
{
user_file_update_subid = InvalidSubTransactionId;
group_file_update_subid = InvalidSubTransactionId;
return;
}
/*
* We use ExclusiveLock to ensure that only one backend writes the
* flat file(s) at a time. That's sufficient because it's okay to
* allow plain reads of the tables in parallel. There is some chance
* of a deadlock here (if we were triggered by a user update of
* pg_shadow or pg_group, which likely won't have gotten a strong
* enough lock), so get the locks we need before writing anything.
*/
if (user_file_update_subid != InvalidSubTransactionId)
urel = heap_openr(ShadowRelationName, ExclusiveLock);
if (group_file_update_subid != InvalidSubTransactionId)
grel = heap_openr(GroupRelationName, ExclusiveLock);
/* Okay to write the files */
if (user_file_update_subid != InvalidSubTransactionId)
{
user_file_update_subid = InvalidSubTransactionId;
write_user_file(urel);
heap_close(urel, NoLock);
}
if (group_file_update_subid != InvalidSubTransactionId)
{
group_file_update_subid = InvalidSubTransactionId;
write_group_file(grel);
heap_close(grel, NoLock);
}
/*
* Signal the postmaster to reload its password & group-file cache.
*/
SendPostmasterSignal(PMSIGNAL_PASSWORD_CHANGE);
}
/*
* AtEOSubXact_UpdatePasswordFile
*
* Called at subtransaction end, this routine resets or updates the
* need-to-update-files flags.
*/
void
AtEOSubXact_UpdatePasswordFile(bool isCommit,
SubTransactionId mySubid,
SubTransactionId parentSubid)
{
if (isCommit)
{
if (user_file_update_subid == mySubid)
user_file_update_subid = parentSubid;
if (group_file_update_subid == mySubid)
group_file_update_subid = parentSubid;
}
else
{
if (user_file_update_subid == mySubid)
user_file_update_subid = InvalidSubTransactionId;
if (group_file_update_subid == mySubid)
group_file_update_subid = InvalidSubTransactionId;
}
}
/*
* CREATE USER
*/
@ -1060,7 +574,6 @@ AlterUserSet(AlterUserSetStmt *stmt)
}
/*
* DROP USER
*/
@ -1318,7 +831,6 @@ CheckPgUserAclNotNull(void)
}
/*
* CREATE GROUP
*/

View File

@ -13,7 +13,7 @@
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/commands/vacuum.c,v 1.300 2005/02/15 03:50:07 momjian Exp $
* $PostgreSQL: pgsql/src/backend/commands/vacuum.c,v 1.301 2005/02/20 02:21:34 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -32,6 +32,7 @@
#include "catalog/namespace.h"
#include "catalog/pg_database.h"
#include "catalog/pg_index.h"
#include "commands/dbcommands.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
#include "miscadmin.h"
@ -761,8 +762,13 @@ vac_update_dbstats(Oid dbid,
*
* Scan pg_database to determine the system-wide oldest datvacuumxid,
* and use it to truncate the transaction commit log (pg_clog).
* Also generate a warning if the system-wide oldest datfrozenxid
* seems to be in danger of wrapping around.
* Also update the XID wrap limit point maintained by varsup.c.
*
* We also generate a warning if the system-wide oldest datfrozenxid
* seems to be in danger of wrapping around. This is a long-in-advance
* warning; if we start getting uncomfortably close, GetNewTransactionId
* will generate more-annoying warnings, and ultimately refuse to issue
* any more new XIDs.
*
* The passed XIDs are simply the ones I just wrote into my pg_database
* entry. They're used to initialize the "min" calculations.
@ -778,10 +784,17 @@ vac_truncate_clog(TransactionId vacuumXID, TransactionId frozenXID)
HeapScanDesc scan;
HeapTuple tuple;
int32 age;
NameData oldest_datname;
bool vacuumAlreadyWrapped = false;
bool frozenAlreadyWrapped = false;
/* init oldest_datname to sync with my frozenXID */
namestrcpy(&oldest_datname, get_database_name(MyDatabaseId));
/*
* Note: the "already wrapped" cases should now be impossible due to the
* defenses in GetNewTransactionId, but we keep them anyway.
*/
relation = heap_openr(DatabaseRelationName, AccessShareLock);
scan = heap_beginscan(relation, SnapshotNow, 0, NULL);
@ -807,7 +820,10 @@ vac_truncate_clog(TransactionId vacuumXID, TransactionId frozenXID)
if (TransactionIdPrecedes(myXID, dbform->datfrozenxid))
frozenAlreadyWrapped = true;
else if (TransactionIdPrecedes(dbform->datfrozenxid, frozenXID))
{
frozenXID = dbform->datfrozenxid;
namecpy(&oldest_datname, &dbform->datname);
}
}
}
@ -830,24 +846,30 @@ vac_truncate_clog(TransactionId vacuumXID, TransactionId frozenXID)
/* Truncate CLOG to the oldest vacuumxid */
TruncateCLOG(vacuumXID);
/* Give warning about impending wraparound problems */
/*
* Do not update varsup.c if we seem to have suffered wraparound
* already; the computed XID might be bogus.
*/
if (frozenAlreadyWrapped)
{
ereport(WARNING,
(errmsg("some databases have not been vacuumed in over 1 billion transactions"),
errhint("Better vacuum them soon, or you may have a wraparound failure.")));
return;
}
else
{
age = (int32) (myXID - frozenXID);
if (age > (int32) ((MaxTransactionId >> 3) * 3))
ereport(WARNING,
(errmsg("some databases have not been vacuumed in %d transactions",
age),
errhint("Better vacuum them within %d transactions, "
"or you may have a wraparound failure.",
(int32) (MaxTransactionId >> 1) - age)));
}
/* Update the wrap limit for GetNewTransactionId */
SetTransactionIdLimit(frozenXID, &oldest_datname);
/* Give warning about impending wraparound problems */
age = (int32) (myXID - frozenXID);
if (age > (int32) ((MaxTransactionId >> 3) * 3))
ereport(WARNING,
(errmsg("database \"%s\" must be vacuumed within %u transactions",
NameStr(oldest_datname),
(MaxTransactionId >> 1) - age),
errhint("To avoid a database shutdown, execute a full-database VACUUM in \"%s\".",
NameStr(oldest_datname))));
}

View File

@ -10,13 +10,12 @@
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.137 2005/02/12 23:53:42 momjian Exp $
* $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.138 2005/02/20 02:21:40 tgl Exp $
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include <errno.h>
#include <pwd.h>
#include <fcntl.h>
#include <sys/param.h>
@ -29,12 +28,12 @@
#include <arpa/inet.h>
#include <unistd.h>
#include "commands/user.h"
#include "libpq/crypt.h"
#include "libpq/libpq.h"
#include "miscadmin.h"
#include "nodes/pg_list.h"
#include "storage/fd.h"
#include "utils/flatfiles.h"
#include "utils/guc.h"
@ -936,7 +935,7 @@ load_group(void)
group_length = 0;
/* Read in the file contents */
filename = group_getfilename();
filename = group_getflatfilename();
group_file = AllocateFile(filename, "r");
if (group_file == NULL)
@ -993,7 +992,7 @@ load_user(void)
user_length = 0;
/* Read in the file contents */
filename = user_getfilename();
filename = user_getflatfilename();
user_file = AllocateFile(filename, "r");
if (user_file == NULL)

View File

@ -37,7 +37,7 @@
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/postmaster/postmaster.c,v 1.443 2005/01/12 16:38:17 tgl Exp $
* $PostgreSQL: pgsql/src/backend/postmaster/postmaster.c,v 1.444 2005/02/20 02:21:54 tgl Exp $
*
* NOTES
*
@ -903,12 +903,10 @@ PostmasterMain(int argc, char *argv[])
pgstat_init();
/*
* Load cached files for client authentication.
* Load configuration files for client authentication.
*/
load_hba();
load_ident();
load_user();
load_group();
/*
* We're ready to rock and roll...
@ -1797,6 +1795,8 @@ SIGHUP_handler(SIGNAL_ARGS)
if (SysLoggerPID != 0)
kill(SysLoggerPID, SIGHUP);
/* PgStatPID does not currently need SIGHUP */
/* Reload authentication config files too */
load_hba();
load_ident();
@ -2006,6 +2006,14 @@ reaper(SIGNAL_ARGS)
*/
FatalError = false;
/*
* Load the flat user/group files into postmaster's caches.
* The startup process has recomputed these from the database
* contents, so we wait till it finishes before loading them.
*/
load_user();
load_group();
/*
* Crank up the background writer. It doesn't matter if this
* fails, we'll just try again later.
@ -2662,7 +2670,7 @@ BackendRun(Port *port)
port->remote_port = strdup(remote_port);
/*
* In EXEC_BACKEND case, we didn't inherit the contents of pg_hba.c
* In EXEC_BACKEND case, we didn't inherit the contents of pg_hba.conf
* etcetera from the postmaster, and have to load them ourselves.
* Build the PostmasterContext (which didn't exist before, in this
* process) to contain the data.

View File

@ -8,7 +8,7 @@
*
*
* IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/tcop/postgres.c,v 1.440 2004/12/31 22:01:16 pgsql Exp $
* $PostgreSQL: pgsql/src/backend/tcop/postgres.c,v 1.441 2005/02/20 02:21:57 tgl Exp $
*
* NOTES
* this is the "main" module of the postgres backend and
@ -55,6 +55,7 @@
#include "tcop/pquery.h"
#include "tcop/tcopprot.h"
#include "tcop/utility.h"
#include "utils/flatfiles.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
@ -2706,6 +2707,12 @@ PostgresMain(int argc, char *argv[], const char *username)
*/
LoadFreeSpaceMap();
on_shmem_exit(DumpFreeSpaceMap, 0);
/*
* We have to build the flat file for pg_database, but not for
* the user and group tables, since we won't try to do authentication.
*/
BuildFlatFiles(true);
}
/*

View File

@ -4,7 +4,7 @@
# Makefile for utils/init
#
# IDENTIFICATION
# $PostgreSQL: pgsql/src/backend/utils/init/Makefile,v 1.17 2004/05/11 21:57:14 momjian Exp $
# $PostgreSQL: pgsql/src/backend/utils/init/Makefile,v 1.18 2005/02/20 02:22:00 tgl Exp $
#
#-------------------------------------------------------------------------
@ -12,7 +12,7 @@ subdir = src/backend/utils/init
top_builddir = ../../../..
include $(top_builddir)/src/Makefile.global
OBJS = globals.o miscinit.o postinit.o
OBJS = flatfiles.o globals.o miscinit.o postinit.o
all: SUBSYS.o

View File

@ -0,0 +1,857 @@
/*-------------------------------------------------------------------------
*
* flatfiles.c
* Routines for maintaining "flat file" images of the shared catalogs.
*
* We use flat files so that the postmaster and not-yet-fully-started
* backends can look at the contents of pg_database, pg_shadow, and pg_group
* for authentication purposes. This module is responsible for keeping the
* flat-file images as nearly in sync with database reality as possible.
*
* The tricky part of the write_xxx_file() routines in this module is that
* they need to be able to operate in the context of the database startup
* process (which calls BuildFlatFiles()) as well as a normal backend.
* This means for example that we can't assume a fully functional relcache
* and we can't use syscaches at all. The major restriction imposed by
* all that is that there's no way to read an out-of-line-toasted datum,
* because the tuptoaster.c code is not prepared to cope with such an
* environment. Fortunately we can design the shared catalogs in such
* a way that this is OK.
*
*
* Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* $PostgreSQL: pgsql/src/backend/utils/init/flatfiles.c,v 1.1 2005/02/20 02:22:00 tgl Exp $
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include <sys/stat.h>
#include <unistd.h>
#include "access/heapam.h"
#include "catalog/catname.h"
#include "catalog/pg_database.h"
#include "catalog/pg_group.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_shadow.h"
#include "catalog/pg_tablespace.h"
#include "commands/trigger.h"
#include "miscadmin.h"
#include "storage/fd.h"
#include "storage/pmsignal.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/flatfiles.h"
#include "utils/resowner.h"
#include "utils/syscache.h"
#define DATABASE_FLAT_FILE "pg_database"
#define GROUP_FLAT_FILE "pg_group"
#define USER_FLAT_FILE "pg_pwd"
/*
* The need-to-update-files flags are SubTransactionIds that show
* what level of the subtransaction tree requested the update. To register
* an update, the subtransaction saves its own SubTransactionId in the flag,
* unless the value was already set to a valid SubTransactionId (which implies
* that it or a parent level has already requested the same). If it aborts
* and the value is its SubTransactionId, it resets the flag to
* InvalidSubTransactionId. If it commits, it changes the value to its
* parent's SubTransactionId. This way the value is propagated up to the
* top-level transaction, which will update the files if a valid
* SubTransactionId is seen at top-level commit.
*/
static SubTransactionId database_file_update_subid = InvalidSubTransactionId;
static SubTransactionId group_file_update_subid = InvalidSubTransactionId;
static SubTransactionId user_file_update_subid = InvalidSubTransactionId;
/*
* Mark flat database file as needing an update (because pg_database changed)
*/
void
database_file_update_needed(void)
{
if (database_file_update_subid == InvalidSubTransactionId)
database_file_update_subid = GetCurrentSubTransactionId();
}
/*
* Mark flat group file as needing an update (because pg_group changed)
*/
void
group_file_update_needed(void)
{
if (group_file_update_subid == InvalidSubTransactionId)
group_file_update_subid = GetCurrentSubTransactionId();
}
/*
* Mark flat user file as needing an update (because pg_shadow changed)
*/
void
user_file_update_needed(void)
{
if (user_file_update_subid == InvalidSubTransactionId)
user_file_update_subid = GetCurrentSubTransactionId();
}
/*
* database_getflatfilename --- get full pathname of database file
*
* Note that result string is palloc'd, and should be freed by the caller.
*/
char *
database_getflatfilename(void)
{
int bufsize;
char *pfnam;
bufsize = strlen(DataDir) + strlen("/global/") +
strlen(DATABASE_FLAT_FILE) + 1;
pfnam = (char *) palloc(bufsize);
snprintf(pfnam, bufsize, "%s/global/%s", DataDir, DATABASE_FLAT_FILE);
return pfnam;
}
/*
* group_getflatfilename --- get full pathname of group file
*
* Note that result string is palloc'd, and should be freed by the caller.
*/
char *
group_getflatfilename(void)
{
int bufsize;
char *pfnam;
bufsize = strlen(DataDir) + strlen("/global/") +
strlen(GROUP_FLAT_FILE) + 1;
pfnam = (char *) palloc(bufsize);
snprintf(pfnam, bufsize, "%s/global/%s", DataDir, GROUP_FLAT_FILE);
return pfnam;
}
/*
* Get full pathname of password file.
*
* Note that result string is palloc'd, and should be freed by the caller.
*/
char *
user_getflatfilename(void)
{
int bufsize;
char *pfnam;
bufsize = strlen(DataDir) + strlen("/global/") +
strlen(USER_FLAT_FILE) + 1;
pfnam = (char *) palloc(bufsize);
snprintf(pfnam, bufsize, "%s/global/%s", DataDir, USER_FLAT_FILE);
return pfnam;
}
/*
* fputs_quote
*
* Outputs string in quotes, with double-quotes duplicated.
* We could use quote_ident(), but that expects a TEXT argument.
*/
static void
fputs_quote(const char *str, FILE *fp)
{
fputc('"', fp);
while (*str)
{
fputc(*str, fp);
if (*str == '"')
fputc('"', fp);
str++;
}
fputc('"', fp);
}
/*
* name_okay
*
* We must disallow newlines in user and group names because
* hba.c's parser won't handle fields split across lines, even if quoted.
*/
static bool
name_okay(const char *str)
{
int i;
i = strcspn(str, "\r\n");
return (str[i] == '\0');
}
/*
* write_database_file: update the flat database file
*
* A side effect is to determine the oldest database's datfrozenxid
* so we can set or update the XID wrap limit.
*/
static void
write_database_file(Relation drel)
{
char *filename,
*tempname;
int bufsize;
FILE *fp;
mode_t oumask;
HeapScanDesc scan;
HeapTuple tuple;
NameData oldest_datname;
TransactionId oldest_datfrozenxid = InvalidTransactionId;
/*
* Create a temporary filename to be renamed later. This prevents the
* backend from clobbering the flat file while the postmaster
* might be reading from it.
*/
filename = database_getflatfilename();
bufsize = strlen(filename) + 12;
tempname = (char *) palloc(bufsize);
snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid);
oumask = umask((mode_t) 077);
fp = AllocateFile(tempname, "w");
umask(oumask);
if (fp == NULL)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Read pg_database and write the file. Note we use SnapshotSelf to
* ensure we see all effects of current transaction. (Perhaps could
* do a CommandCounterIncrement beforehand, instead?)
*/
scan = heap_beginscan(drel, SnapshotSelf, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_database dbform = (Form_pg_database) GETSTRUCT(tuple);
char *datname;
Oid datoid;
TransactionId datfrozenxid;
datname = NameStr(dbform->datname);
datoid = HeapTupleGetOid(tuple);
datfrozenxid = dbform->datfrozenxid;
/*
* Identify the oldest datfrozenxid, ignoring databases that are not
* connectable (we assume they are safely frozen). This must match
* the logic in vac_truncate_clog() in vacuum.c.
*/
if (dbform->datallowconn &&
TransactionIdIsNormal(datfrozenxid))
{
if (oldest_datfrozenxid == InvalidTransactionId ||
TransactionIdPrecedes(datfrozenxid, oldest_datfrozenxid))
{
oldest_datfrozenxid = datfrozenxid;
namestrcpy(&oldest_datname, datname);
}
}
/*
* Check for illegal characters in the database name.
*/
if (!name_okay(datname))
{
ereport(LOG,
(errmsg("invalid database name \"%s\"", datname)));
continue;
}
/*
* File format is: "dbname" oid frozenxid
*
* The xid is not needed for backend startup, but may be of use
* for forensic purposes.
*/
fputs_quote(datname, fp);
fprintf(fp, " %u %u\n", datoid, datfrozenxid);
}
heap_endscan(scan);
if (FreeFile(fp))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Rename the temp file to its final name, deleting the old flat file.
* We expect that rename(2) is an atomic action.
*/
if (rename(tempname, filename))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not rename file \"%s\" to \"%s\": %m",
tempname, filename)));
pfree(tempname);
pfree(filename);
/*
* Set the transaction ID wrap limit using the oldest datfrozenxid
*/
if (oldest_datfrozenxid != InvalidTransactionId)
SetTransactionIdLimit(oldest_datfrozenxid, &oldest_datname);
}
/*
* write_group_file: update the flat group file
*
* XXX this will never be able to work during system bootstrap: we don't
* have either TOAST support or SysCache support. Need to redefine both
* the catalog and file contents to fix this completely. In the short term
* we can handle everything except an out-of-line-toasted grolist, if we
* change the flat file definition to store numeric sysids instead of
* user names.
*/
static void
write_group_file(Relation grel)
{
char *filename,
*tempname;
int bufsize;
FILE *fp;
mode_t oumask;
HeapScanDesc scan;
HeapTuple tuple;
TupleDesc dsc = RelationGetDescr(grel);
/*
* Create a temporary filename to be renamed later. This prevents the
* backend from clobbering the flat file while the postmaster
* might be reading from it.
*/
filename = group_getflatfilename();
bufsize = strlen(filename) + 12;
tempname = (char *) palloc(bufsize);
snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid);
oumask = umask((mode_t) 077);
fp = AllocateFile(tempname, "w");
umask(oumask);
if (fp == NULL)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Read pg_group and write the file. Note we use SnapshotSelf to
* ensure we see all effects of current transaction. (Perhaps could
* do a CommandCounterIncrement beforehand, instead?)
*/
scan = heap_beginscan(grel, SnapshotSelf, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Datum datum,
grolist_datum;
bool isnull;
char *groname;
IdList *grolist_p;
AclId *aidp;
int i,
num;
char *usename;
bool first_user = true;
datum = heap_getattr(tuple, Anum_pg_group_groname, dsc, &isnull);
/* ignore NULL groupnames --- shouldn't happen */
if (isnull)
continue;
groname = NameStr(*DatumGetName(datum));
/*
* Check for illegal characters in the group name.
*/
if (!name_okay(groname))
{
ereport(LOG,
(errmsg("invalid group name \"%s\"", groname)));
continue;
}
grolist_datum = heap_getattr(tuple, Anum_pg_group_grolist, dsc, &isnull);
/* Ignore NULL group lists */
if (isnull)
continue;
/* be sure the IdList is not toasted */
grolist_p = DatumGetIdListP(grolist_datum);
/* scan grolist */
num = IDLIST_NUM(grolist_p);
aidp = IDLIST_DAT(grolist_p);
for (i = 0; i < num; ++i)
{
tuple = SearchSysCache(SHADOWSYSID,
PointerGetDatum(aidp[i]),
0, 0, 0);
if (HeapTupleIsValid(tuple))
{
usename = NameStr(((Form_pg_shadow) GETSTRUCT(tuple))->usename);
/*
* Check for illegal characters in the user name.
*/
if (!name_okay(usename))
{
ereport(LOG,
(errmsg("invalid user name \"%s\"", usename)));
continue;
}
/*
* File format is: "groupname" "user1" "user2" "user3"
*/
if (first_user)
{
fputs_quote(groname, fp);
fputs("\t", fp);
first_user = false;
}
else
fputs(" ", fp);
fputs_quote(usename, fp);
ReleaseSysCache(tuple);
}
}
if (!first_user)
fputs("\n", fp);
/* if IdList was toasted, free detoasted copy */
if ((Pointer) grolist_p != DatumGetPointer(grolist_datum))
pfree(grolist_p);
}
heap_endscan(scan);
if (FreeFile(fp))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Rename the temp file to its final name, deleting the old flat file.
* We expect that rename(2) is an atomic action.
*/
if (rename(tempname, filename))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not rename file \"%s\" to \"%s\": %m",
tempname, filename)));
pfree(tempname);
pfree(filename);
}
/*
* write_user_file: update the flat password file
*/
static void
write_user_file(Relation urel)
{
char *filename,
*tempname;
int bufsize;
FILE *fp;
mode_t oumask;
HeapScanDesc scan;
HeapTuple tuple;
/*
* Create a temporary filename to be renamed later. This prevents the
* backend from clobbering the flat file while the postmaster might
* be reading from it.
*/
filename = user_getflatfilename();
bufsize = strlen(filename) + 12;
tempname = (char *) palloc(bufsize);
snprintf(tempname, bufsize, "%s.%d", filename, MyProcPid);
oumask = umask((mode_t) 077);
fp = AllocateFile(tempname, "w");
umask(oumask);
if (fp == NULL)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Read pg_shadow and write the file. Note we use SnapshotSelf to
* ensure we see all effects of current transaction. (Perhaps could
* do a CommandCounterIncrement beforehand, instead?)
*/
scan = heap_beginscan(urel, SnapshotSelf, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_shadow pwform = (Form_pg_shadow) GETSTRUCT(tuple);
HeapTupleHeader tup = tuple->t_data;
char *tp; /* ptr to tuple data */
long off; /* offset in tuple data */
bits8 *bp = tup->t_bits; /* ptr to null bitmask in tuple */
Datum datum;
char *usename,
*passwd,
*valuntil;
usename = NameStr(pwform->usename);
/*
* We can't use heap_getattr() here because during startup we will
* not have any tupdesc for pg_shadow. Fortunately it's not too
* hard to work around this. passwd is the first possibly-null
* field so we can compute its offset directly.
*/
tp = (char *) tup + tup->t_hoff;
off = offsetof(FormData_pg_shadow, passwd);
if (HeapTupleHasNulls(tuple) &&
att_isnull(Anum_pg_shadow_passwd - 1, bp))
{
/*
* It can be argued that people having a null password shouldn't
* be allowed to connect under password authentication, because
* they need to have a password set up first. If you think
* assuming an empty password in that case is better, change this
* logic to look something like the code for valuntil.
*/
continue;
}
/* assume passwd is pass-by-ref */
datum = PointerGetDatum(tp + off);
/*
* The password probably shouldn't ever be out-of-line toasted;
* if it is, ignore it, since we can't handle that in startup mode.
*/
if (VARATT_IS_EXTERNAL(DatumGetPointer(datum)))
continue;
passwd = DatumGetCString(DirectFunctionCall1(textout, datum));
/* assume passwd has attlen -1 */
off = att_addlength(off, -1, tp + off);
if (HeapTupleHasNulls(tuple) &&
att_isnull(Anum_pg_shadow_valuntil - 1, bp))
{
/* valuntil is null, emit as an empty string */
valuntil = pstrdup("");
}
else
{
/* assume valuntil has attalign 'i' */
off = att_align(off, 'i');
/* assume valuntil is pass-by-value, integer size */
datum = Int32GetDatum(*((int32 *) (tp + off)));
valuntil = DatumGetCString(DirectFunctionCall1(abstimeout, datum));
}
/*
* Check for illegal characters in the user name and password.
*/
if (!name_okay(usename))
{
ereport(LOG,
(errmsg("invalid user name \"%s\"", usename)));
continue;
}
if (!name_okay(passwd))
{
ereport(LOG,
(errmsg("invalid user password \"%s\"", passwd)));
continue;
}
fputs_quote(usename, fp);
fputs(" ", fp);
fputs_quote(passwd, fp);
fputs(" ", fp);
fputs_quote(valuntil, fp);
fputs("\n", fp);
pfree(passwd);
pfree(valuntil);
}
heap_endscan(scan);
if (FreeFile(fp))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not write to temporary file \"%s\": %m",
tempname)));
/*
* Rename the temp file to its final name, deleting the old flat file.
* We expect that rename(2) is an atomic action.
*/
if (rename(tempname, filename))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not rename file \"%s\" to \"%s\": %m",
tempname, filename)));
pfree(tempname);
pfree(filename);
}
/*
* This routine is called once during database startup, after completing
* WAL replay if needed. Its purpose is to sync the flat files with the
* current state of the database tables. This is particularly important
* during PITR operation, since the flat files will come from the
* base backup which may be far out of sync with the current state.
*
* In theory we could skip rebuilding the flat files if no WAL replay
* occurred, but it seems safest to just do it always. We have to
* scan pg_database to compute the XID wrap limit anyway.
*
* In a standalone backend we pass database_only = true to skip processing
* the user and group files. We won't need them, and building them could
* fail if there's something corrupt in those catalogs.
*/
void
BuildFlatFiles(bool database_only)
{
ResourceOwner owner;
RelFileNode rnode;
Relation rel;
/*
* We don't have any hope of running a real relcache, but we can use
* the same fake-relcache facility that WAL replay uses.
*/
XLogInitRelationCache();
/* Need a resowner to keep the heapam and buffer code happy */
owner = ResourceOwnerCreate(NULL, "BuildFlatFiles");
CurrentResourceOwner = owner;
/* hard-wired path to pg_database */
rnode.spcNode = GLOBALTABLESPACE_OID;
rnode.dbNode = 0;
rnode.relNode = RelOid_pg_database;
/* No locking is needed because no one else is alive yet */
rel = XLogOpenRelation(true, 0, rnode);
write_database_file(rel);
if (!database_only)
{
#ifdef NOT_YET
/* XXX doesn't work yet for reasons stated above */
/* hard-wired path to pg_group */
rnode.spcNode = GLOBALTABLESPACE_OID;
rnode.dbNode = 0;
rnode.relNode = RelOid_pg_group;
rel = XLogOpenRelation(true, 0, rnode);
write_group_file(rel);
#endif
/* hard-wired path to pg_shadow */
rnode.spcNode = GLOBALTABLESPACE_OID;
rnode.dbNode = 0;
rnode.relNode = RelOid_pg_shadow;
rel = XLogOpenRelation(true, 0, rnode);
write_user_file(rel);
}
CurrentResourceOwner = NULL;
ResourceOwnerDelete(owner);
XLogCloseRelationCache();
}
/*
* This routine is called during transaction commit or abort.
*
* On commit, if we've written any of the critical database tables during
* the current transaction, update the flat files and signal the postmaster.
*
* On abort, just reset the static flags so we don't try to do it on the
* next successful commit.
*
* NB: this should be the last step before actual transaction commit.
* If any error aborts the transaction after we run this code, the postmaster
* will still have received and cached the changed data; so minimize the
* window for such problems.
*/
void
AtEOXact_UpdateFlatFiles(bool isCommit)
{
Relation drel = NULL;
Relation grel = NULL;
Relation urel = NULL;
if (database_file_update_subid == InvalidSubTransactionId &&
group_file_update_subid == InvalidSubTransactionId &&
user_file_update_subid == InvalidSubTransactionId)
return; /* nothing to do */
if (!isCommit)
{
database_file_update_subid = InvalidSubTransactionId;
group_file_update_subid = InvalidSubTransactionId;
user_file_update_subid = InvalidSubTransactionId;
return;
}
/*
* We use ExclusiveLock to ensure that only one backend writes the
* flat file(s) at a time. That's sufficient because it's okay to
* allow plain reads of the tables in parallel. There is some chance
* of a deadlock here (if we were triggered by a user update of one
* of the tables, which likely won't have gotten a strong enough lock),
* so get the locks we need before writing anything.
*/
if (database_file_update_subid != InvalidSubTransactionId)
drel = heap_openr(DatabaseRelationName, ExclusiveLock);
if (group_file_update_subid != InvalidSubTransactionId)
grel = heap_openr(GroupRelationName, ExclusiveLock);
if (user_file_update_subid != InvalidSubTransactionId)
urel = heap_openr(ShadowRelationName, ExclusiveLock);
/* Okay to write the files */
if (database_file_update_subid != InvalidSubTransactionId)
{
database_file_update_subid = InvalidSubTransactionId;
write_database_file(drel);
heap_close(drel, NoLock);
}
if (group_file_update_subid != InvalidSubTransactionId)
{
group_file_update_subid = InvalidSubTransactionId;
write_group_file(grel);
heap_close(grel, NoLock);
}
if (user_file_update_subid != InvalidSubTransactionId)
{
user_file_update_subid = InvalidSubTransactionId;
write_user_file(urel);
heap_close(urel, NoLock);
}
/*
* Signal the postmaster to reload its caches.
*/
SendPostmasterSignal(PMSIGNAL_PASSWORD_CHANGE);
}
/*
* AtEOSubXact_UpdateFlatFiles
*
* Called at subtransaction end, this routine resets or updates the
* need-to-update-files flags.
*/
void
AtEOSubXact_UpdateFlatFiles(bool isCommit,
SubTransactionId mySubid,
SubTransactionId parentSubid)
{
if (isCommit)
{
if (database_file_update_subid == mySubid)
database_file_update_subid = parentSubid;
if (group_file_update_subid == mySubid)
group_file_update_subid = parentSubid;
if (user_file_update_subid == mySubid)
user_file_update_subid = parentSubid;
}
else
{
if (database_file_update_subid == mySubid)
database_file_update_subid = InvalidSubTransactionId;
if (group_file_update_subid == mySubid)
group_file_update_subid = InvalidSubTransactionId;
if (user_file_update_subid == mySubid)
user_file_update_subid = InvalidSubTransactionId;
}
}
/*
* This trigger is fired whenever someone modifies pg_database, pg_shadow
* or pg_group via general-purpose INSERT/UPDATE/DELETE commands.
*
* It is sufficient for this to be a STATEMENT trigger since we don't
* care which individual rows changed. It doesn't much matter whether
* it's a BEFORE or AFTER trigger.
*/
Datum
flatfile_update_trigger(PG_FUNCTION_ARGS)
{
TriggerData *trigdata = (TriggerData *) fcinfo->context;
if (!CALLED_AS_TRIGGER(fcinfo))
elog(ERROR,
"flatfile_update_trigger was not called by trigger manager");
if (RelationGetNamespace(trigdata->tg_relation) != PG_CATALOG_NAMESPACE)
elog(ERROR, "flatfile_update_trigger was called for wrong table");
switch (RelationGetRelid(trigdata->tg_relation))
{
case RelOid_pg_database:
database_file_update_needed();
break;
case RelOid_pg_group:
group_file_update_needed();
break;
case RelOid_pg_shadow:
user_file_update_needed();
break;
default:
elog(ERROR, "flatfile_update_trigger was called for wrong table");
break;
}
return PointerGetDatum(NULL);
}
/*
* Old version of trigger --- remove after we can force an initdb
*/
extern Datum update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS);
Datum
update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS)
{
return flatfile_update_trigger(fcinfo);
}

View File

@ -7,7 +7,7 @@
* Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* $PostgreSQL: pgsql/src/include/access/transam.h,v 1.51 2004/12/31 22:03:21 pgsql Exp $
* $PostgreSQL: pgsql/src/include/access/transam.h,v 1.52 2005/02/20 02:22:03 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@ -75,13 +75,21 @@
/*
* VariableCache is placed in shmem and used by
* backends to get next available XID & OID.
* backends to get next available OID & XID.
*
* Note: xidWrapLimit and limit_datname are not "active" values, but are
* used just to generate useful messages when xidWarnLimit or xidStopLimit
* are exceeded.
*/
typedef struct VariableCacheData
{
TransactionId nextXid; /* next XID to assign */
Oid nextOid; /* next OID to assign */
uint32 oidCount; /* OIDs available before must do XLOG work */
TransactionId nextXid; /* next XID to assign */
TransactionId xidWarnLimit; /* start complaining here */
TransactionId xidStopLimit; /* refuse to advance nextXid beyond here */
TransactionId xidWrapLimit; /* where the world ends */
NameData limit_datname; /* database that needs vacuumed first */
} VariableCacheData;
typedef VariableCacheData *VariableCache;
@ -118,6 +126,8 @@ extern bool TransactionIdFollowsOrEquals(TransactionId id1, TransactionId id2);
/* in transam/varsup.c */
extern TransactionId GetNewTransactionId(bool isSubXact);
extern TransactionId ReadNewTransactionId(void);
extern void SetTransactionIdLimit(TransactionId oldest_datfrozenxid,
Name oldest_datname);
extern Oid GetNewObjectId(void);
extern void CheckMaxObjectId(Oid assigned_oid);

View File

@ -4,20 +4,16 @@
* Commands for manipulating users and groups.
*
*
* $PostgreSQL: pgsql/src/include/commands/user.h,v 1.25 2004/09/16 16:58:39 tgl Exp $
* $PostgreSQL: pgsql/src/include/commands/user.h,v 1.26 2005/02/20 02:22:05 tgl Exp $
*
*-------------------------------------------------------------------------
*/
#ifndef USER_H
#define USER_H
#include "fmgr.h"
#include "nodes/parsenodes.h"
extern char *group_getfilename(void);
extern char *user_getfilename(void);
extern void CreateUser(CreateUserStmt *stmt);
extern void AlterUser(AlterUserStmt *stmt);
extern void AlterUserSet(AlterUserSetStmt *stmt);
@ -29,11 +25,4 @@ extern void AlterGroup(AlterGroupStmt *stmt, const char *tag);
extern void DropGroup(DropGroupStmt *stmt);
extern void RenameGroup(const char *oldname, const char *newname);
extern Datum update_pg_pwd_and_pg_group(PG_FUNCTION_ARGS);
extern void AtEOXact_UpdatePasswordFile(bool isCommit);
extern void AtEOSubXact_UpdatePasswordFile(bool isCommit,
SubTransactionId mySubid,
SubTransactionId parentSubid);
#endif /* USER_H */

View File

@ -0,0 +1,33 @@
/*-------------------------------------------------------------------------
*
* flatfiles.h
* Routines for maintaining "flat file" images of the shared catalogs.
*
*
* $PostgreSQL: pgsql/src/include/utils/flatfiles.h,v 1.1 2005/02/20 02:22:07 tgl Exp $
*
*-------------------------------------------------------------------------
*/
#ifndef FLATFILES_H
#define FLATFILES_H
#include "fmgr.h"
extern void database_file_update_needed(void);
extern void group_file_update_needed(void);
extern void user_file_update_needed(void);
extern char *database_getflatfilename(void);
extern char *group_getflatfilename(void);
extern char *user_getflatfilename(void);
extern void BuildFlatFiles(bool database_only);
extern void AtEOXact_UpdateFlatFiles(bool isCommit);
extern void AtEOSubXact_UpdateFlatFiles(bool isCommit,
SubTransactionId mySubid,
SubTransactionId parentSubid);
extern Datum flatfile_update_trigger(PG_FUNCTION_ARGS);
#endif /* FLATFILES_H */