From f6110b2337f226866c5d246df4407c2c5e051f5a Mon Sep 17 00:00:00 2001 From: Bryan Green Date: Tue, 4 Aug 2026 11:05:15 -0500 Subject: [PATCH] Fix pg_xact corruption from subtransaction abort after subcommit CommitSubTransaction() calls AtSubCommit_childXids() to copy this subtransaction's XID and its committed grandchildren into the parent's list of committed children. Several fallible steps run after that while the subtransaction is still in TRANS_COMMIT state. If one of them throws (for example, out of memory), the subtransaction aborts while in COMMIT state. RecordTransactionAbort() marks its XID aborted in pg_xact, but nothing removes that XID from the parent's committed-child list; AtSubAbort_childXids() only frees the child's own array. When the parent commits, TransactionIdSetTreeStatus() walks the list and tries to mark the aborted XID committed. That trips the assertion in TransactionIdSetStatusBit() in an assert build, and writes the wrong pg_xact status otherwise. The same list is stored in the parent's commit WAL record, so replay hits it too and recovery cannot complete. Have AtSubCommit_childXids() save the parent's child count before it appends, and have AbortSubTransaction() restore it when the subtransaction aborts after the transfer. The appended entries are at the tail of the parent's array, so restoring the saved length removes exactly this subtransaction's XID and its grandchildren; the grandchildren revert to implicitly aborted, which is correct because the whole subtree is rolling back. The restore runs ahead of the curTransactionOwner-guarded cleanup, so it happens whenever AtSubCommit_childXids() ran. The failure can be reproduced with an error thrown from a subtransaction-commit callback (or an injection point) after AtSubCommit_childXids() has run, inside a PL/pgSQL block whose EXCEPTION handler catches it so the surrounding transaction commits. Co-authored-by: Mark Dilger --- src/backend/access/transam/xact.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 3a89149016..e73944fded 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -208,6 +208,8 @@ typedef struct TransactionStateData TransactionId *childXids; /* subcommitted child XIDs, in XID order */ int nChildXids; /* # of subcommitted child XIDs */ int maxChildXids; /* allocated size of childXids[] */ + int savedParentNChildXids; /* parent's nChildXids before + * subcommit transfer, or -1 */ Oid prevUser; /* previous CurrentUserId setting */ int prevSecContext; /* previous SecurityRestrictionContext */ bool prevXactReadOnly; /* entry-time xact r/o state */ @@ -250,6 +252,7 @@ static TransactionStateData TopTransactionStateData = { .state = TRANS_DEFAULT, .blockState = TBLOCK_DEFAULT, .topXidLogged = false, + .savedParentNChildXids = -1, }; /* @@ -1710,6 +1713,12 @@ AtSubCommit_childXids(void) Assert(s->parent != NULL); + /* + * Remember the parent's child count so a later abort can undo this + * transfer (see AbortSubTransaction). + */ + s->savedParentNChildXids = s->parent->nChildXids; + /* * The parent childXids array will need to hold my XID and all my * childXids, in addition to the XIDs already there. @@ -5343,6 +5352,24 @@ AbortSubTransaction(void) s->state = TRANS_ABORT; + /* + * If AtSubCommit_childXids() moved our XID and childXids up to the + * parent, undo that here. An error escaping the later steps of + * CommitSubTransaction() aborts us in COMMIT state; leaving our aborted + * XID in the parent's committed-child list would make the parent's commit + * mark it committed and corrupt pg_xact. Our entries are the tail of the + * parent's array, so restoring the saved length drops exactly them. Done + * before the curTransactionOwner check below so it runs whenever + * AtSubCommit_childXids() did. + */ + if (s->savedParentNChildXids >= 0) + { + Assert(s->parent != NULL); + Assert(s->parent->nChildXids >= s->savedParentNChildXids); + s->parent->nChildXids = s->savedParentNChildXids; + s->savedParentNChildXids = -1; + } + /* * Reset user ID which might have been changed transiently. (See notes in * AbortTransaction.) @@ -5515,6 +5542,7 @@ PushTransaction(void) s->parallelModeLevel = 0; s->parallelChildXact = (p->parallelModeLevel != 0 || p->parallelChildXact); s->topXidLogged = false; + s->savedParentNChildXids = -1; CurrentTransactionState = s; -- 2.49.0