Skip to content

Commit cded2c4

Browse files
committed
Fix Portal snapshot tracking to handle subtransactions properly.
Commit 84f5c29 forgot to consider the possibility that EnsurePortalSnapshotExists could run inside a subtransaction with lifespan shorter than the Portal's. In that case, the new active snapshot would be popped at the end of the subtransaction, leaving a dangling pointer in the Portal, with mayhem ensuing. To fix, make sure the ActiveSnapshot stack entry is marked with the same subtransaction nesting level as the associated Portal. It's certainly safe to do so since we won't be here at all unless the stack is empty; hence we can't create an out-of-order stack. Let's also apply this logic in the case where PortalRunUtility sets portalSnapshot, just to be sure that path can't cause similar problems. It's slightly less clear that that path can't create an out-of-order stack, so add an assertion guarding it. Report and patch by Bertrand Drouvot (with kibitzing by me). Back-patch to v11, like the previous commit. Discussion: https://postgr.es/m/ff82b8c5-77f4-3fe7-6028-fcf3303e82dd@amazon.com
1 parent f2cf745 commit cded2c4

File tree

8 files changed

+95
-8
lines changed

8 files changed

+95
-8
lines changed

src/backend/access/transam/xact.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4817,6 +4817,7 @@ CommitSubTransaction(void)
48174817
AfterTriggerEndSubXact(true);
48184818
AtSubCommit_Portals(s->subTransactionId,
48194819
s->parent->subTransactionId,
4820+
s->parent->nestingLevel,
48204821
s->parent->curTransactionOwner);
48214822
AtEOSubXact_LargeObject(true, s->subTransactionId,
48224823
s->parent->subTransactionId);

src/backend/tcop/pquery.c

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,9 @@ PortalStart(Portal portal, ParamListInfo params,
493493
* We could remember the snapshot in portal->portalSnapshot,
494494
* but presently there seems no need to, as this code path
495495
* cannot be used for non-atomic execution. Hence there can't
496-
* be any commit/abort that might destroy the snapshot.
496+
* be any commit/abort that might destroy the snapshot. Since
497+
* we don't do that, there's also no need to force a
498+
* non-default nesting level for the snapshot.
497499
*/
498500

499501
/*
@@ -1152,9 +1154,15 @@ PortalRunUtility(Portal portal, PlannedStmt *pstmt,
11521154
snapshot = RegisterSnapshot(snapshot);
11531155
portal->holdSnapshot = snapshot;
11541156
}
1155-
/* In any case, make the snapshot active and remember it in portal */
1156-
PushActiveSnapshot(snapshot);
1157-
/* PushActiveSnapshot might have copied the snapshot */
1157+
1158+
/*
1159+
* In any case, make the snapshot active and remember it in portal.
1160+
* Because the portal now references the snapshot, we must tell
1161+
* snapmgr.c that the snapshot belongs to the portal's transaction
1162+
* level, else we risk portalSnapshot becoming a dangling pointer.
1163+
*/
1164+
PushActiveSnapshotWithLevel(snapshot, portal->createLevel);
1165+
/* PushActiveSnapshotWithLevel might have copied the snapshot */
11581166
portal->portalSnapshot = GetActiveSnapshot();
11591167
}
11601168
else
@@ -1810,8 +1818,13 @@ EnsurePortalSnapshotExists(void)
18101818
elog(ERROR, "cannot execute SQL without an outer snapshot or portal");
18111819
Assert(portal->portalSnapshot == NULL);
18121820

1813-
/* Create a new snapshot and make it active */
1814-
PushActiveSnapshot(GetTransactionSnapshot());
1815-
/* PushActiveSnapshot might have copied the snapshot */
1821+
/*
1822+
* Create a new snapshot, make it active, and remember it in portal.
1823+
* Because the portal now references the snapshot, we must tell snapmgr.c
1824+
* that the snapshot belongs to the portal's transaction level, else we
1825+
* risk portalSnapshot becoming a dangling pointer.
1826+
*/
1827+
PushActiveSnapshotWithLevel(GetTransactionSnapshot(), portal->createLevel);
1828+
/* PushActiveSnapshotWithLevel might have copied the snapshot */
18161829
portal->portalSnapshot = GetActiveSnapshot();
18171830
}

src/backend/utils/mmgr/portalmem.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ CreatePortal(const char *name, bool allowDup, bool dupSilent)
210210
portal->cleanup = PortalCleanup;
211211
portal->createSubid = GetCurrentSubTransactionId();
212212
portal->activeSubid = portal->createSubid;
213+
portal->createLevel = GetCurrentTransactionNestLevel();
213214
portal->strategy = PORTAL_MULTI_QUERY;
214215
portal->cursorOptions = CURSOR_OPT_NO_SCROLL;
215216
portal->atStart = true;
@@ -655,6 +656,7 @@ HoldPortal(Portal portal)
655656
*/
656657
portal->createSubid = InvalidSubTransactionId;
657658
portal->activeSubid = InvalidSubTransactionId;
659+
portal->createLevel = 0;
658660
}
659661

660662
/*
@@ -938,6 +940,7 @@ PortalErrorCleanup(void)
938940
void
939941
AtSubCommit_Portals(SubTransactionId mySubid,
940942
SubTransactionId parentSubid,
943+
int parentLevel,
941944
ResourceOwner parentXactOwner)
942945
{
943946
HASH_SEQ_STATUS status;
@@ -952,6 +955,7 @@ AtSubCommit_Portals(SubTransactionId mySubid,
952955
if (portal->createSubid == mySubid)
953956
{
954957
portal->createSubid = parentSubid;
958+
portal->createLevel = parentLevel;
955959
if (portal->resowner)
956960
ResourceOwnerNewParent(portal->resowner, parentXactOwner);
957961
}

src/backend/utils/time/snapmgr.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,10 +733,25 @@ FreeSnapshot(Snapshot snapshot)
733733
*/
734734
void
735735
PushActiveSnapshot(Snapshot snap)
736+
{
737+
PushActiveSnapshotWithLevel(snap, GetCurrentTransactionNestLevel());
738+
}
739+
740+
/*
741+
* PushActiveSnapshotWithLevel
742+
* Set the given snapshot as the current active snapshot
743+
*
744+
* Same as PushActiveSnapshot except that caller can specify the
745+
* transaction nesting level that "owns" the snapshot. This level
746+
* must not be deeper than the current top of the snapshot stack.
747+
*/
748+
void
749+
PushActiveSnapshotWithLevel(Snapshot snap, int snap_level)
736750
{
737751
ActiveSnapshotElt *newactive;
738752

739753
Assert(snap != InvalidSnapshot);
754+
Assert(ActiveSnapshot == NULL || snap_level >= ActiveSnapshot->as_level);
740755

741756
newactive = MemoryContextAlloc(TopTransactionContext, sizeof(ActiveSnapshotElt));
742757

@@ -750,7 +765,7 @@ PushActiveSnapshot(Snapshot snap)
750765
newactive->as_snap = snap;
751766

752767
newactive->as_next = ActiveSnapshot;
753-
newactive->as_level = GetCurrentTransactionNestLevel();
768+
newactive->as_level = snap_level;
754769

755770
newactive->as_snap->active_count++;
756771

src/include/utils/portal.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,16 @@ typedef struct PortalData
193193
TimestampTz creation_time; /* time at which this portal was defined */
194194
bool visible; /* include this portal in pg_cursors? */
195195

196+
/* Stuff added at the end to avoid ABI break in stable branches: */
197+
196198
/*
197199
* Outermost ActiveSnapshot for execution of the portal's queries. For
198200
* all but a few utility commands, we require such a snapshot to exist.
199201
* This ensures that TOAST references in query results can be detoasted,
200202
* and helps to reduce thrashing of the process's exposed xmin.
201203
*/
202204
Snapshot portalSnapshot; /* active snapshot, or NULL if none */
205+
int createLevel; /* creating subxact's nesting level */
203206
} PortalData;
204207

205208
/*
@@ -217,6 +220,7 @@ extern void AtCleanup_Portals(void);
217220
extern void PortalErrorCleanup(void);
218221
extern void AtSubCommit_Portals(SubTransactionId mySubid,
219222
SubTransactionId parentSubid,
223+
int parentLevel,
220224
ResourceOwner parentXactOwner);
221225
extern void AtSubAbort_Portals(SubTransactionId mySubid,
222226
SubTransactionId parentSubid,

src/include/utils/snapmgr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ extern void InvalidateCatalogSnapshot(void);
111111
extern void InvalidateCatalogSnapshotConditionally(void);
112112

113113
extern void PushActiveSnapshot(Snapshot snapshot);
114+
extern void PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level);
114115
extern void PushCopiedSnapshot(Snapshot snapshot);
115116
extern void UpdateActiveSnapshotCommandId(void);
116117
extern void PopActiveSnapshot(void);

src/pl/plpgsql/src/expected/plpgsql_transaction.out

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,34 @@ SELECT * FROM test1;
430430
---+---
431431
(0 rows)
432432

433+
-- test commit/rollback inside exception handler, too
434+
TRUNCATE test1;
435+
DO LANGUAGE plpgsql $$
436+
BEGIN
437+
FOR i IN 1..10 LOOP
438+
BEGIN
439+
INSERT INTO test1 VALUES (i, 'good');
440+
INSERT INTO test1 VALUES (i/0, 'bad');
441+
EXCEPTION
442+
WHEN division_by_zero THEN
443+
INSERT INTO test1 VALUES (i, 'exception');
444+
IF (i % 3) > 0 THEN COMMIT; ELSE ROLLBACK; END IF;
445+
END;
446+
END LOOP;
447+
END;
448+
$$;
449+
SELECT * FROM test1;
450+
a | b
451+
----+-----------
452+
1 | exception
453+
2 | exception
454+
4 | exception
455+
5 | exception
456+
7 | exception
457+
8 | exception
458+
10 | exception
459+
(7 rows)
460+
433461
-- detoast result of simple expression after commit
434462
CREATE TEMP TABLE test4(f1 text);
435463
ALTER TABLE test4 ALTER COLUMN f1 SET STORAGE EXTERNAL; -- disable compression

src/pl/plpgsql/src/sql/plpgsql_transaction.sql

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,27 @@ $$;
354354
SELECT * FROM test1;
355355

356356

357+
-- test commit/rollback inside exception handler, too
358+
TRUNCATE test1;
359+
360+
DO LANGUAGE plpgsql $$
361+
BEGIN
362+
FOR i IN 1..10 LOOP
363+
BEGIN
364+
INSERT INTO test1 VALUES (i, 'good');
365+
INSERT INTO test1 VALUES (i/0, 'bad');
366+
EXCEPTION
367+
WHEN division_by_zero THEN
368+
INSERT INTO test1 VALUES (i, 'exception');
369+
IF (i % 3) > 0 THEN COMMIT; ELSE ROLLBACK; END IF;
370+
END;
371+
END LOOP;
372+
END;
373+
$$;
374+
375+
SELECT * FROM test1;
376+
377+
357378
-- detoast result of simple expression after commit
358379
CREATE TEMP TABLE test4(f1 text);
359380
ALTER TABLE test4 ALTER COLUMN f1 SET STORAGE EXTERNAL; -- disable compression

0 commit comments

Comments
 (0)