From c993d9f7c8d4bcada0d5c0bd6ea32416d956fc8f Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Thu, 8 Oct 2026 09:26:33 +0900 Subject: [PATCH] Refuse RI fast-path row locks in read-only transactions The SPI path checks a foreign key with SELECT ... FOR KEY SHARE, which ExecCheckXactReadOnly() refuses in a read-only transaction unless the referenced table is temporary. The fast path locks the referenced row without that check, so for example a deferred check that fires at COMMIT after SET TRANSACTION READ ONLY succeeded, having locked a row in a read-only transaction, where the SPI path fails with "cannot execute SELECT FOR KEY SHARE in a read-only transaction". Fix the fast path to refuse it the same way so the behavior matches with checks going through SPI. Discussion: https://postgr.es/m/CA+HiwqG79XK1oObdZ2AwT660CeJ6s3Mn4LrFPCme-k4L2rF_ag@mail.gmail.com Backpatch-through: 19 --- src/backend/utils/adt/ri_triggers.c | 10 ++++++++ src/test/regress/expected/foreign_key.out | 31 +++++++++++++++++++++++ src/test/regress/sql/foreign_key.sql | 21 +++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 56ca5c648e7..618b86a0ad3 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -31,6 +31,7 @@ #include "access/tableam.h" #include "access/xact.h" #include "catalog/index.h" +#include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am_d.h" #include "catalog/pg_collation.h" @@ -45,6 +46,7 @@ #include "miscadmin.h" #include "parser/parse_coerce.h" #include "parser/parse_relation.h" +#include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/datum.h" @@ -2789,6 +2791,14 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo, pk_rel = table_open(riinfo->pk_relid, RowShareLock); + /* + * The row lock taken below is refused in a read-only transaction unless + * the referenced table is temporary, as ExecCheckXactReadOnly() refuses + * the SPI path's SELECT ... FOR KEY SHARE. + */ + if (XactReadOnly && !isTempNamespace(RelationGetNamespace(pk_rel))) + PreventCommandIfReadOnly("SELECT FOR KEY SHARE"); + /* * Advance the command counter so the check sees the effects of prior * triggers in this statement, as SPI does when executing the query issued diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index abafaff785a..e2709279eae 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -4119,3 +4119,34 @@ DROP TYPE fkint CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to function fkint_in(cstring) drop cascades to function fkint_out(fkint) +-- An FK check locks the referenced row, which a read-only transaction does +-- not allow unless the referenced table is temporary. +CREATE TABLE fp_pk_ro (a int PRIMARY KEY); +INSERT INTO fp_pk_ro VALUES (1); +CREATE TABLE fp_fk_ro (a int REFERENCES fp_pk_ro DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- fails +ERROR: cannot execute SELECT FOR KEY SHARE in a read-only transaction +CREATE TEMP TABLE fp_pk_ro_tmp (a int PRIMARY KEY); +INSERT INTO fp_pk_ro_tmp VALUES (1); +CREATE TEMP TABLE fp_fk_ro_tmp (a int REFERENCES fp_pk_ro_tmp + DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro_tmp VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- succeeds +SELECT count(*) FROM fp_fk_ro; + count +------- + 0 +(1 row) + +SELECT count(*) FROM fp_fk_ro_tmp; + count +------- + 1 +(1 row) + +DROP TABLE fp_fk_ro, fp_pk_ro, fp_fk_ro_tmp, fp_pk_ro_tmp; diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index da62f601b46..a706083948d 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -3065,3 +3065,24 @@ DROP TABLE pktable_inval; DROP CAST (fkint AS int4); DROP FUNCTION fkint_to_int4(fkint); DROP TYPE fkint CASCADE; + +-- An FK check locks the referenced row, which a read-only transaction does +-- not allow unless the referenced table is temporary. +CREATE TABLE fp_pk_ro (a int PRIMARY KEY); +INSERT INTO fp_pk_ro VALUES (1); +CREATE TABLE fp_fk_ro (a int REFERENCES fp_pk_ro DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- fails +CREATE TEMP TABLE fp_pk_ro_tmp (a int PRIMARY KEY); +INSERT INTO fp_pk_ro_tmp VALUES (1); +CREATE TEMP TABLE fp_fk_ro_tmp (a int REFERENCES fp_pk_ro_tmp + DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro_tmp VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- succeeds +SELECT count(*) FROM fp_fk_ro; +SELECT count(*) FROM fp_fk_ro_tmp; +DROP TABLE fp_fk_ro, fp_pk_ro, fp_fk_ro_tmp, fp_pk_ro_tmp;