From 13f6f89bd1101cdf78564ea778d40b2ff68a9a76 Mon Sep 17 00:00:00 2001 From: Josh Curtis Date: Fri, 20 Feb 2026 11:48:34 -0800 Subject: [PATCH v1] Add test to demonstrate bug in SSI --- src/backend/storage/lmgr/predicate.c | 9 ++ src/test/modules/injection_points/Makefile | 1 + .../expected/predicate-lock-page-split.out | 86 +++++++++++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/predicate-lock-page-split.spec | 92 +++++++++++++++++++ 5 files changed, 189 insertions(+) create mode 100644 src/test/modules/injection_points/expected/predicate-lock-page-split.out create mode 100644 src/test/modules/injection_points/specs/predicate-lock-page-split.spec diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c index fe75ead350..de8a84b7e8 100644 --- a/src/backend/storage/lmgr/predicate.c +++ b/src/backend/storage/lmgr/predicate.c @@ -212,6 +212,7 @@ #include "storage/proc.h" #include "storage/procarray.h" #include "utils/guc_hooks.h" +#include "utils/injection_point.h" #include "utils/rel.h" #include "utils/snapmgr.h" @@ -3156,6 +3157,7 @@ PredicateLockPageSplit(Relation relation, BlockNumber oldblkno, * memory barrier in the LWLock acquisition guarantees that this read * occurs while the buffer page lock is held. */ + INJECTION_POINT("predicate-lock-page-split", NULL); if (!TransactionIdIsValid(PredXact->SxactGlobalXmin)) return; @@ -3255,6 +3257,10 @@ SetNewSxactGlobalXmin(void) PredXact->SxactGlobalXmin = InvalidTransactionId; PredXact->SxactGlobalXminCount = 0; +#ifdef USE_INJECTION_POINTS + INJECTION_POINT_CACHED("predicate-set-sxact-global-xmin-invalid", NULL); +#endif + dlist_foreach(iter, &PredXact->activeList) { SERIALIZABLEXACT *sxact = @@ -3368,6 +3374,9 @@ ReleasePredicateLocks(bool isCommit, bool isReadOnlySafe) Assert(LocalPredicateLockHash == NULL); return; } +#ifdef USE_INJECTION_POINTS + INJECTION_POINT_LOAD("predicate-set-sxact-global-xmin-invalid"); +#endif LWLockAcquire(SerializableXactHashLock, LW_EXCLUSIVE); diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index a41d781f8c..73ed09eaa2 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -14,6 +14,7 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ inplace \ + predicate-lock-page-split \ syscache-update-pruned \ heap_lock_update diff --git a/src/test/modules/injection_points/expected/predicate-lock-page-split.out b/src/test/modules/injection_points/expected/predicate-lock-page-split.out new file mode 100644 index 0000000000..b0a6493519 --- /dev/null +++ b/src/test/modules/injection_points/expected/predicate-lock-page-split.out @@ -0,0 +1,86 @@ +Parsed test spec with 5 sessions + +starting permutation: s1_begin bump_xmin s2_begin s3_begin s1_insert s2_insert_wait_at_page_split s1_commit_wait_in_SetNewSxactGlobalXmin wakeup_s2_then_s1 s3_insert s3_commit s2_commit verify_order +step s1_begin: + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) FROM test_table; + +max +--- +406 +(1 row) + +step bump_xmin: + CREATE TABLE tmp_bump_xmin(); + DROP TABLE tmp_bump_xmin; + +step s2_begin: + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) FROM test_table; + +max +--- +406 +(1 row) + +step s3_begin: + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) FROM test_table; + +max +--- +406 +(1 row) + +step s1_insert: + INSERT INTO test_table SELECT max(id) + 1 FROM test_table; + +step s2_insert_wait_at_page_split: + SELECT FROM injection_points_set_local(); + SELECT FROM injection_points_attach('predicate-lock-page-split', 'wait'); + INSERT INTO test_table SELECT max(id) + 1 FROM test_table; + +step s1_commit_wait_in_SetNewSxactGlobalXmin: + SELECT injection_points_set_local(); + SELECT injection_points_attach('predicate-set-sxact-global-xmin-invalid', 'wait'); + COMMIT; + +step wakeup_s2_then_s1: + SELECT FROM injection_points_wakeup('predicate-lock-page-split'); + SELECT FROM injection_points_wakeup('predicate-set-sxact-global-xmin-invalid'); + +step s2_insert_wait_at_page_split: <... completed> +step s1_commit_wait_in_SetNewSxactGlobalXmin: <... completed> +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s3_insert: + INSERT INTO test_table SELECT max(id) + 1 FROM test_table; + +step wakeup_s2_then_s1: <... completed> +step s3_commit: + COMMIT; + +step s2_commit: + COMMIT; + +ERROR: could not serialize access due to read/write dependencies among transactions +step verify_order: + select id from test_table order by id desc limit 5; + + id +--- +407 +407 +406 +405 +404 +(5 rows) + diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index fcc8541451..6ff7005a9b 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -45,6 +45,7 @@ tests += { 'specs': [ 'basic', 'inplace', + 'predicate-lock-page-split', 'syscache-update-pruned', 'heap_lock_update', ], diff --git a/src/test/modules/injection_points/specs/predicate-lock-page-split.spec b/src/test/modules/injection_points/specs/predicate-lock-page-split.spec new file mode 100644 index 0000000000..1566090272 --- /dev/null +++ b/src/test/modules/injection_points/specs/predicate-lock-page-split.spec @@ -0,0 +1,92 @@ +# Test for race condition in PredicateLockPageSplit +# +# When SetNewSxactGlobalXmin() temporarily sets SxactGlobalXmin to +# InvalidTransactionId, a concurrent PredicateLockPageSplit() will skip +# transferring SIREAD locks to the new page. This lets a third transaction +# insert onto the new page without SSI detecting the conflict. +setup +{ + CREATE EXTENSION IF NOT EXISTS injection_points; + DROP TABLE IF EXISTS test_table; + + CREATE TABLE test_table (id int); + CREATE INDEX test_table_id_idx ON test_table USING btree (id); + INSERT INTO test_table SELECT i FROM generate_series(1, 406) i; +} + + +teardown +{ + DROP TABLE IF EXISTS test_table; + DROP EXTENSION IF EXISTS injection_points; +} + +session s0 +step bump_xmin { + CREATE TABLE tmp_bump_xmin(); + DROP TABLE tmp_bump_xmin; +} +step verify_order { + select id from test_table order by id desc limit 5; +} +session s1 +step s1_begin { + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) FROM test_table; +} +step s1_insert { + INSERT INTO test_table SELECT max(id) + 1 FROM test_table; +} +step s1_commit_wait_in_SetNewSxactGlobalXmin { + SELECT injection_points_set_local(); + SELECT injection_points_attach('predicate-set-sxact-global-xmin-invalid', 'wait'); + COMMIT; +} + +session s2 +step s2_begin { + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) FROM test_table; +} +step s2_insert_wait_at_page_split { + SELECT FROM injection_points_set_local(); + SELECT FROM injection_points_attach('predicate-lock-page-split', 'wait'); + INSERT INTO test_table SELECT max(id) + 1 FROM test_table; +} +step s2_commit { + COMMIT; +} + +session s3 +step s3_begin { + BEGIN ISOLATION LEVEL SERIALIZABLE; + SELECT max(id) FROM test_table; +} +step s3_insert { + INSERT INTO test_table SELECT max(id) + 1 FROM test_table; +} +step s3_commit { + COMMIT; +} + +session s4 +step wakeup_s2_then_s1 { + SELECT FROM injection_points_wakeup('predicate-lock-page-split'); + SELECT FROM injection_points_wakeup('predicate-set-sxact-global-xmin-invalid'); +} + +# s1_begin: Start transaction 1, read max(id) = 406 +# bump_xmin: Advance xmin so s2 and s3 get a higher xmin than s1 +# s2_begin, s3_begin: Start transactions 2 and 3, read max(id) = 406 +# +# at this point s1, s2, and s3 are all concurrent +# +# s1_insert: s1 inserts 407 into index page 1 +# s2_insert_wait_at_page_split: s2 inserts 407, triggers a page split and waits at the start of PredicateLockPageSplit +# s1_commit_wait_in_SetNewSxactGlobalXmin: s1 commits then waits after setting SxactGlobalXmin to InvalidTransactionId in SetNewSxactGlobalXmin +# wakeup_s2_then_s1: s2 wakes up, sees InvalidTransactionId, and skips transferring SIREAD locks to the new page +# then s1 wakes up and finishes committing +# s3_insert: s3 inserts 407 into index page 2 (no SIREAD locks were transferred = bug) +# s3_commit: s3 commits successfully (should have been aborted) +# s2_commit: s2 tries to commit but aborts due to serialization failure (cycle with s1) +permutation s1_begin bump_xmin s2_begin s3_begin s1_insert s2_insert_wait_at_page_split s1_commit_wait_in_SetNewSxactGlobalXmin wakeup_s2_then_s1(s2_insert_wait_at_page_split,s1_commit_wait_in_SetNewSxactGlobalXmin) s3_insert s3_commit s2_commit verify_order -- 2.51.2