mirror of
https://github.com/postgres/postgres.git
synced 2025-06-13 07:41:39 +03:00
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:
@ -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
|
||||
*/
|
||||
|
Reference in New Issue
Block a user