[PATCH]Fix pg_xact corruption from subtransaction abort after subcommit

From: Bryan Green <dbryan(dot)green(at)gmail(dot)com>
To: pgsql-hackers(at)lists(dot)postgresql(dot)org
Subject: [PATCH]Fix pg_xact corruption from subtransaction abort after subcommit
Date: 2026-08-09 03:39:01
Message-ID: 447185db-3f06-4a38-8518-ecbd42d8b7d5@gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Greetings,

The subtransaction commit path can corrupt pg_xact, and the offending code
is all stock.  A subtransaction that has already subcommitted can still be
forced to abort, and when that happens its XID is left in the parent's list
of committed children while pg_xact records it as aborted.  When the parent
commits, TransactionIdSetTreeStatus() tries to mark that aborted XID
committed.  With assertions on, that's a TRAP in clog.c; with them off, it
writes the wrong status, and since the parent's commit record carries the
same child list, replay hits it too and recovery never finishes.

The window is in CommitSubTransaction():

    s->state = TRANS_COMMIT;
    ...
    if (FullTransactionIdIsValid(s->fullTransactionId))
        AtSubCommit_childXids();
    AfterTriggerEndSubXact(true);
    AtSubCommit_Portals(...);
    ...

By the time AfterTriggerEndSubXact() runs, AtSubCommit_childXids() has
already copied our XID and any committed grandchildren into the parent's
array.  If one of the later steps throws (OOM being the obvious case),
control longjmps into AbortSubTransaction() with the subtransaction still in
TRANS_COMMIT state.  We record the XID aborted and leave it sitting in the
parent's list; AtSubAbort_childXids() only frees our own array, not the
parent's.

An error thrown at a subtransaction's commit inside a PL/pgSQL EXCEPTION 
block is caught right there, so the subtransaction aborts while the 
surrounding transaction goes on to commit.  That is the shape that bites.  
A subtransaction-commit callback that raises on 
SUBXACT_EVENT_COMMIT_SUB reproduces it with no core changes and no 
injection points; an injection point just after AtSubCommit_childXids() 
does too:

    BEGIN;
    DO $$
    BEGIN
        BEGIN
            INSERT INTO t VALUES (1);   -- subtransaction acquires an XID
        EXCEPTION WHEN OTHERS THEN
            NULL;                       -- swallow the commit-time error
        END;
    END $$;
    COMMIT;                             -- crashes here

    TRAP: failed Assert("curval == 0 || ... || curval == status"),
          File: "clog.c", Line: 702
        TransactionIdSetStatusBit
        TransactionIdSetTreeStatus
        TransactionIdCommitTree
        RecordTransactionCommit

The fix is small and stays in xact.c.  AtSubCommit_childXids() records the
parent's child count before it appends, and AbortSubTransaction() restores
that count when the subtransaction aborts after the transfer.  The entries
we added are the tail of the parent's array, so restoring the length drops
exactly them; the grandchildren revert to aborted, which is correct because
the whole subtree is rolling back.

I confirmed on current master that the reproduction crashes without the
patch and commits cleanly with it, that the aborted row is gone, that a
committed sibling savepoint survives, and that the regression tests pass.

--
Bryan Green
EDB: https://www.enterprisedb.com

Attachment Content-Type Size
0001-Fix-pg_xact-corruption-from-subtransaction-abort-aft.patch text/plain 4.5 KB

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Bryan Green 2026-08-09 03:44:50 [PATCH] Harden recovery/t/051_effective_wal_level against WAL recycling
Previous Message Bryan Green 2026-08-09 03:33:30 [PATCH] Release a replication slot leaked by a caught subtransaction error