Skip to content

Commit 04ef202

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 649e561 commit 04ef202

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
@@ -4842,6 +4842,7 @@ CommitSubTransaction(void)
48424842
AfterTriggerEndSubXact(true);
48434843
AtSubCommit_Portals(s->subTransactionId,
48444844
s->parent->subTransactionId,
4845+
s->parent->nestingLevel,
48454846
s->parent->curTransactionOwner);
48464847
AtEOSubXact_LargeObject(true, s->subTransactionId,
48474848
s->parent->subTransactionId);

src/backend/tcop/pquery.c

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

486488
/*
@@ -1134,9 +1136,15 @@ PortalRunUtility(Portal portal, PlannedStmt *pstmt,
11341136
snapshot = RegisterSnapshot(snapshot);
11351137
portal->holdSnapshot = snapshot;
11361138
}
1137-
/* In any case, make the snapshot active and remember it in portal */
1138-
PushActiveSnapshot(snapshot);
1139-
/* PushActiveSnapshot might have copied the snapshot */
1139+
1140+
/*
1141+
* In any case, make the snapshot active and remember it in portal.
1142+
* Because the portal now references the snapshot, we must tell
1143+
* snapmgr.c that the snapshot belongs to the portal's transaction
1144+
* level, else we risk portalSnapshot becoming a dangling pointer.
1145+
*/
1146+
PushActiveSnapshotWithLevel(snapshot, portal->createLevel);
1147+
/* PushActiveSnapshotWithLevel might have copied the snapshot */
11401148
portal->portalSnapshot = GetActiveSnapshot();
11411149
}
11421150
else
@@ -1786,8 +1794,13 @@ EnsurePortalSnapshotExists(void)
17861794
elog(ERROR, "cannot execute SQL without an outer snapshot or portal");
17871795
Assert(portal->portalSnapshot == NULL);
17881796

1789-
/* Create a new snapshot and make it active */
1790-
PushActiveSnapshot(GetTransactionSnapshot());
1791-
/* PushActiveSnapshot might have copied the snapshot */
1797+
/*
1798+
* Create a new snapshot, make it active, and remember it in portal.
1799+
* Because the portal now references the snapshot, we must tell snapmgr.c
1800+
* that the snapshot belongs to the portal's transaction level, else we
1801+
* risk portalSnapshot becoming a dangling pointer.
1802+
*/
1803+
PushActiveSnapshotWithLevel(GetTransactionSnapshot(), portal->createLevel);
1804+
/* PushActiveSnapshotWithLevel might have copied the snapshot */
17921805
portal->portalSnapshot = GetActiveSnapshot();
17931806
}

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;
@@ -657,6 +658,7 @@ HoldPortal(Portal portal)
657658
*/
658659
portal->createSubid = InvalidSubTransactionId;
659660
portal->activeSubid = InvalidSubTransactionId;
661+
portal->createLevel = 0;
660662
}
661663

662664
/*
@@ -940,6 +942,7 @@ PortalErrorCleanup(void)
940942
void
941943
AtSubCommit_Portals(SubTransactionId mySubid,
942944
SubTransactionId parentSubid,
945+
int parentLevel,
943946
ResourceOwner parentXactOwner)
944947
{
945948
HASH_SEQ_STATUS status;
@@ -954,6 +957,7 @@ AtSubCommit_Portals(SubTransactionId mySubid,
954957
if (portal->createSubid == mySubid)
955958
{
956959
portal->createSubid = parentSubid;
960+
portal->createLevel = parentLevel;
957961
if (portal->resowner)
958962
ResourceOwnerNewParent(portal->resowner, parentXactOwner);
959963
}

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
@@ -195,13 +195,16 @@ typedef struct PortalData
195195
TimestampTz creation_time; /* time at which this portal was defined */
196196
bool visible; /* include this portal in pg_cursors? */
197197

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

207210
/*
@@ -219,6 +222,7 @@ extern void AtCleanup_Portals(void);
219222
extern void PortalErrorCleanup(void);
220223
extern void AtSubCommit_Portals(SubTransactionId mySubid,
221224
SubTransactionId parentSubid,
225+
int parentLevel,
222226
ResourceOwner parentXactOwner);
223227
extern void AtSubAbort_Portals(SubTransactionId mySubid,
224228
SubTransactionId parentSubid,

src/include/utils/snapmgr.h

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

112112
extern void PushActiveSnapshot(Snapshot snapshot);
113+
extern void PushActiveSnapshotWithLevel(Snapshot snapshot, int snap_level);
113114
extern void PushCopiedSnapshot(Snapshot snapshot);
114115
extern void UpdateActiveSnapshotCommandId(void);
115116
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)