databasevm_step (src/postgresql/database_postgresql.c) catches a PostgreSQL error in PG_CATCH, calls FlushErrorState(), and then keeps working with the SPI cursor: clear_fetch_batch(stmt) and close_portal(stmt), which calls SPI_cursor_close. No subtransaction is rolled back in between.
PostgreSQL releases resources such as LWLocks during (sub)transaction abort. Catching an error and continuing without one is only safe while the error was not raised while holding such a resource. When it was, the cleanup runs with the lock still held.
Observed crash
An assertion-enabled PostgreSQL 17 server terminated during a fragment apply:
TRAP: failed Assert("!LWLockHeldByMe(BufferDescriptorGetContentLock(buf))"), File: "bufmgr.c", Line: 2861
ExceptionalConditionsigned ...
ResourceOwnerRelease
PortalDrop
SPI_cursor_close
cloudsync.so(databasevm_step)
cloudsync.so(cloudsync_payload_apply)
The trigger was a serialization failure raised by SSI inside a heap scan, which happens while a buffer content lock is held. The backend aborted with signal 6 and the server restarted ("terminating any other active server processes").
A release build does not assert, but releasing a buffer pin while its content lock is still held is not safe either.
How it was reached
Applying pieces of one fragmented value from two concurrent SERIALIZABLE transactions, while the per-value advisory lock made the second one wait and read after the first committed. The lock is no longer taken under SERIALIZABLE (ce0557f), which is why the suite is green, but nothing prevents another 40001 or any other error raised under a buffer lock from reaching the same handler.
Suggested fix
Wrap the SPI work in a subtransaction (BeginInternalSubTransaction / RollbackAndReleaseCurrentSubTransaction) so that catching an error goes through a real abort, or re-throw and let the existing savepoints handle it. The cleanup after a caught error should not touch portals or tuple tables before the abort.
databasevm_step(src/postgresql/database_postgresql.c) catches a PostgreSQL error inPG_CATCH, callsFlushErrorState(), and then keeps working with the SPI cursor:clear_fetch_batch(stmt)andclose_portal(stmt), which callsSPI_cursor_close. No subtransaction is rolled back in between.PostgreSQL releases resources such as LWLocks during (sub)transaction abort. Catching an error and continuing without one is only safe while the error was not raised while holding such a resource. When it was, the cleanup runs with the lock still held.
Observed crash
An assertion-enabled PostgreSQL 17 server terminated during a fragment apply:
The trigger was a serialization failure raised by SSI inside a heap scan, which happens while a buffer content lock is held. The backend aborted with signal 6 and the server restarted ("terminating any other active server processes").
A release build does not assert, but releasing a buffer pin while its content lock is still held is not safe either.
How it was reached
Applying pieces of one fragmented value from two concurrent
SERIALIZABLEtransactions, while the per-value advisory lock made the second one wait and read after the first committed. The lock is no longer taken underSERIALIZABLE(ce0557f), which is why the suite is green, but nothing prevents another 40001 or any other error raised under a buffer lock from reaching the same handler.Suggested fix
Wrap the SPI work in a subtransaction (
BeginInternalSubTransaction/RollbackAndReleaseCurrentSubTransaction) so that catching an error goes through a real abort, or re-throw and let the existing savepoints handle it. The cleanup after a caught error should not touch portals or tuple tables before the abort.