]> granicus.if.org Git - postgresql/commitdiff
Fix race between GetNewTransactionId and GetOldestActiveTransactionId.
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>
Thu, 13 Jul 2017 12:47:02 +0000 (15:47 +0300)
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>
Thu, 13 Jul 2017 12:47:02 +0000 (15:47 +0300)
The race condition goes like this:

1. GetNewTransactionId advances nextXid e.g. from 100 to 101
2. GetOldestActiveTransactionId reads the new nextXid, 101
3. GetOldestActiveTransactionId loops through the proc array. There are no
   active XIDs there, so it returns 101 as the oldest active XID.
4. GetNewTransactionid stores XID 100 to MyPgXact->xid

So, GetOldestActiveTransactionId returned XID 101, even though 100 only
just started and is surely still running.

This would be hard to hit in practice, and even harder to spot any ill
effect if it happens. GetOldestActiveTransactionId is only used when
creating a checkpoint in a master server, and the race condition can only
happen on an online checkpoint, as there are no backends running during a
shutdown checkpoint. The oldestActiveXid value of an online checkpoint is
only used when starting up a hot standby server, to determine the starting
point where pg_subtrans is initialized from. For the race condition to
happen, there must be no other XIDs in the proc array that would hold back
the oldest-active XID value, which means that the missed XID must be a top
transaction's XID. However, pg_subtrans is not used for top XIDs, so I
believe an off-by-one error is in fact inconsequential. Nevertheless, let's
fix it, as it's clearly wrong and the fix is simple.

This has been wrong ever since hot standby was introduced, so backport to
all supported versions.

Discussion: https://www.postgresql.org/message-id/e7258662-82b6-7a45-56d4-99b337a32bf7@iki.fi

src/backend/storage/ipc/procarray.c

index 6eb7c72ec329aa0010fde9857786ec76297fcad4..a7e8cf2d43ac45903a71cdd7f17142f073661cc5 100644 (file)
@@ -2096,20 +2096,21 @@ GetOldestActiveTransactionId(void)
 
        Assert(!RecoveryInProgress());
 
-       LWLockAcquire(ProcArrayLock, LW_SHARED);
-
        /*
-        * It's okay to read nextXid without acquiring XidGenLock because (1) we
-        * assume TransactionIds can be read atomically and (2) we don't care if
-        * we get a slightly stale value.  It can't be very stale anyway, because
-        * the LWLockAcquire above will have done any necessary memory
-        * interlocking.
+        * Read nextXid, as the upper bound of what's still active.
+        *
+        * Reading a TransactionId is atomic, but we must grab the lock to make
+        * sure that all XIDs < nextXid are already present in the proc array (or
+        * have already completed), when we spin over it.
         */
+       LWLockAcquire(XidGenLock, LW_SHARED);
        oldestRunningXid = ShmemVariableCache->nextXid;
+       LWLockRelease(XidGenLock);
 
        /*
         * Spin over procArray collecting all xids and subxids.
         */
+       LWLockAcquire(ProcArrayLock, LW_SHARED);
        for (index = 0; index < arrayP->numProcs; index++)
        {
                int                     pgprocno = arrayP->pgprocnos[index];
@@ -2131,7 +2132,6 @@ GetOldestActiveTransactionId(void)
                 * smaller than oldestRunningXid
                 */
        }
-
        LWLockRelease(ProcArrayLock);
 
        return oldestRunningXid;