From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Zhijie Hou Date: Wed, 21 Oct 2026 16:40:00 +0800 Subject: [PATCH] Top-up for v2-0001: Reproduce HEAP_XMAX_INVALID clobbering MultiXact lock. Add an injection point and isolation test permutation on top of v2-0001 to show that if an updating subtransaction in a MultiXact aborts after !HeapTupleHeaderIsOnlyLocked(tuple) returns true, v2-0001 sets HEAP_XMAX_INVALID on the MultiXact tuple and clobbers an active FOR KEY SHARE lock held by another transaction. --- src/backend/access/heap/heapam_visibility.c | 3 + .../expected/multixact-aborted-updater.out | 36 ++++++++++++ .../specs/multixact-aborted-updater.spec | 58 ++++++++++++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 7184adfe221..6e47109d781 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -76,6 +76,7 @@ #include "storage/bufmgr.h" #include "storage/procarray.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/snapmgr.h" @@ -1195,6 +1196,8 @@ HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *de { TransactionId xmax = HeapTupleHeaderGetUpdateXid(tuple); + INJECTION_POINT("heap-vacuum-horizon-inprogress-xmin", NULL); + if (!TransactionIdIsInProgress(xmax) && !TransactionIdDidCommit(xmax)) SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, diff --git a/src/test/modules/injection_points/expected/multixact-aborted-updater.out b/src/test/modules/injection_points/expected/multixact-aborted-updater.out index 40750714734..14a6d85dc21 100644 --- a/src/test/modules/injection_points/expected/multixact-aborted-updater.out +++ b/src/test/modules/injection_points/expected/multixact-aborted-updater.out @@ -82,3 +82,39 @@ id|filler 2|initial (1 row) + +starting permutation: s1begin s1update s2begin s2lock s1subupd s3attach s3htsv s1rollbacksp wake_htsv s1commit s3forupdnowait s2commit +step s1begin: BEGIN; +step s1update: UPDATE mxact_abort SET filler = 's1' WHERE id = 1; +step s2begin: BEGIN; +step s2lock: SELECT * FROM mxact_abort WHERE id = 1 FOR KEY SHARE; +id|filler +--+------- + 1|initial +(1 row) + +step s1subupd: + SELECT FROM injection_points_detach('transaction-abort-after-clog'); + SAVEPOINT sp; + UPDATE mxact_abort SET filler = repeat('x', 800) WHERE id = 1; + +step s3attach: + SELECT FROM injection_points_set_local(); + SELECT FROM injection_points_attach('heap-vacuum-horizon-inprogress-xmin', 'wait'); + +step s3htsv: SELECT * FROM mxact_abort; +step s1rollbacksp: ROLLBACK TO sp; +step wake_htsv: + SELECT FROM injection_points_detach('heap-vacuum-horizon-inprogress-xmin'); + SELECT FROM injection_points_wakeup('heap-vacuum-horizon-inprogress-xmin'); + +step s1commit: COMMIT; +step s3htsv: <... completed> +id|filler +--+------- + 1|initial +(1 row) + +step s3forupdnowait: SELECT * FROM mxact_abort WHERE id = 1 FOR UPDATE NOWAIT; +ERROR: could not obtain lock on row in relation "mxact_abort" +step s2commit: COMMIT; diff --git a/src/test/modules/injection_points/specs/multixact-aborted-updater.spec b/src/test/modules/injection_points/specs/multixact-aborted-updater.spec index 98d265a2e94..0eee3aa0611 100644 --- a/src/test/modules/injection_points/specs/multixact-aborted-updater.spec +++ b/src/test/modules/injection_points/specs/multixact-aborted-updater.spec @@ -19,7 +19,7 @@ setup { CREATE EXTENSION injection_points; - CREATE TABLE mxact_abort (id int PRIMARY KEY, filler text); + CREATE TABLE mxact_abort (id int PRIMARY KEY, filler text) WITH (fillfactor = 10); INSERT INTO mxact_abort VALUES (1, 'initial'); } @@ -38,26 +38,45 @@ setup { } step s1begin { BEGIN; } step s1update { UPDATE mxact_abort SET filler = 's1' WHERE id = 1; } +step s1subupd { + SELECT FROM injection_points_detach('transaction-abort-after-clog'); + SAVEPOINT sp; + UPDATE mxact_abort SET filler = repeat('x', 800) WHERE id = 1; +} +step s1rollbacksp { ROLLBACK TO sp; } +step s1commit { COMMIT; } step s1abort { ROLLBACK; } # Transaction 2 locks the row in KEY SHARE mode, to cause a multixact to be # created. session s2 +step s2begin { BEGIN; } step s2lock { SELECT * FROM mxact_abort WHERE id = 1 FOR KEY SHARE; } +step s2commit { COMMIT; } # Transaction 3 performs a concurrent operation while transaction 1 is aborting session s3 step s3nowait { SELECT * FROM mxact_abort WHERE id = 1 FOR NO KEY UPDATE NOWAIT; } +step s3forupdnowait { SELECT * FROM mxact_abort WHERE id = 1 FOR UPDATE NOWAIT; } step s3update { UPDATE mxact_abort SET filler = 's3' WHERE id = 1; } step s3delete { DELETE FROM mxact_abort WHERE id = 1; } step s3keyupdate { UPDATE mxact_abort SET id = 2 WHERE id = 1; } step s3check { SELECT * FROM mxact_abort ORDER BY id; } +step s3attach { + SELECT FROM injection_points_set_local(); + SELECT FROM injection_points_attach('heap-vacuum-horizon-inprogress-xmin', 'wait'); +} +step s3htsv { SELECT * FROM mxact_abort; } session waker step wake { SELECT FROM injection_points_detach('transaction-abort-after-clog'); SELECT FROM injection_points_wakeup('transaction-abort-after-clog'); } +step wake_htsv { + SELECT FROM injection_points_detach('heap-vacuum-horizon-inprogress-xmin'); + SELECT FROM injection_points_wakeup('heap-vacuum-horizon-inprogress-xmin'); +} # A SELECT .. NOWAIT while the updating transaction is paused in the abort # considers the updater as still running, and reports an error. @@ -105,3 +124,20 @@ permutation s3keyupdate wake s3check + +# HeapTupleSatisfiesVacuumHorizon() must not set HEAP_XMAX_INVALID on a tuple +# with a MultiXact Xmax when an updating subtransaction aborts while another +# transaction still holds a lock in the MultiXact. +permutation + s1begin + s1update + s2begin + s2lock + s1subupd + s3attach + s3htsv + s1rollbacksp + wake_htsv + s1commit + s3forupdnowait + s2commit -- 2.50.1 (Apple Git-155)