From 7aea3239e37d0588413458f1a97603069ee1c960 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Thu, 24 Sep 2026 09:07:48 +0900 Subject: [PATCH v2 1/2] Check EXECUTE privilege on functions invoked by the RI fast path ri_FastPathCheck() invokes two functions on the FK values: the equality operator's function, via the ScanKey the index AM evaluates, and, when the FK column's type differs from the PK column's, the implicit cast function. The SPI path had EXECUTE on both checked by ExecInitFunc() while initializing the generated query, as the referenced table's owner, but the fast path skipped both checks. Add ri_CheckFunctionPermissions() and call it once the fast-path metadata is populated, under the switch to the referenced table's owner, so the checks happen at the same point as the existing relation permission check. Add a regression test for each function, reaching the fast path through per-row validation of a new constraint. Reported-by: Nikolay Samokhvalov Discussion: https://postgr.es/m/ Backpatch-through: 19 --- src/backend/utils/adt/ri_triggers.c | 39 +++++++++++ src/test/regress/expected/foreign_key.out | 81 +++++++++++++++++++++++ src/test/regress/sql/foreign_key.sql | 81 +++++++++++++++++++++++ 3 files changed, 201 insertions(+) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 40c1591ac7b..e584df17980 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -32,11 +32,13 @@ #include "access/tableam.h" #include "access/xact.h" #include "catalog/index.h" +#include "catalog/objectaccess.h" #include "catalog/pg_am_d.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_index.h" #include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/spi.h" @@ -399,6 +401,8 @@ static void ri_CheckPermissions(const RI_ConstraintInfo *riinfo, Relation query_rel); static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, int nkeys, TupleTableSlot *new_slot); +static void ri_CheckFunctionPermissions(const RI_ConstraintInfo *riinfo, + const FastPathMeta *fpmeta); static void build_index_scankeys(const RI_ConstraintInfo *riinfo, FastPathMeta *fpmeta, Relation idx_rel, Datum *pk_vals, @@ -2971,6 +2975,7 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo, ri_populate_fastpath_metadata(riinfo, fk_rel, idx_rel); } Assert(riinfo->fpmeta); + ri_CheckFunctionPermissions(riinfo, riinfo->fpmeta); ri_ExtractValues(fk_rel, newslot, riinfo, false, pk_vals, pk_nulls); build_index_scankeys(riinfo, riinfo->fpmeta, idx_rel, pk_vals, pk_nulls, skey); @@ -3715,6 +3720,40 @@ recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, int nkeys, return matched; } +/* + * ri_CheckFunctionPermissions + * Check EXECUTE privilege on the functions the fast path invokes on the + * FK values, as the referenced table's owner. + * + * The SPI path had these checks done by ExecInitFunc() when initializing + * the generated query: the equality operator's function appears in its WHERE + * clause and the cast function, if any, in the cast applied to the parameter. + * Call with the user id already switched to the referenced table's owner. + */ +static void +ri_CheckFunctionPermissions(const RI_ConstraintInfo *riinfo, + const FastPathMeta *fpmeta) +{ + for (int i = 0; i < riinfo->nkeys; i++) + { + Oid funcs[2] = {fpmeta->regops[i], fpmeta->cast_func_finfo[i].fn_oid}; + + for (int j = 0; j < lengthof(funcs); j++) + { + AclResult aclresult; + + if (!OidIsValid(funcs[j])) + continue; + aclresult = object_aclcheck(ProcedureRelationId, funcs[j], + GetUserId(), ACL_EXECUTE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FUNCTION, + get_func_name(funcs[j])); + InvokeFunctionExecuteHook(funcs[j]); + } + } +} + /* * build_index_scankeys * Build ScanKeys for a direct index probe of the PK's unique index. diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 40be9c4f75d..f5b10c73c10 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -3803,6 +3803,87 @@ NOTICE: fp_auto_pk called NOTICE: fp_auto_pk called DROP TABLE fp_fk_cci, fp_pk_cci; DROP FUNCTION fp_auto_pk; +-- The fast path must check EXECUTE, as the referenced table's owner, on the +-- functions it invokes on the FK values: the equality operator's function +-- and, for a cross-type FK, the implicit cast function. Validation of a new +-- constraint by a role that cannot use the bulk check (RLS is enabled on the +-- referenced table) runs per-row checks through the fast path. +-- +-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere +-- with nearby tests. +BEGIN; +CREATE ROLE regress_ri_pkowner; +CREATE ROLE regress_ri_fkowner; +-- A private btree opclass whose equality function we can revoke without +-- affecting anything else. +CREATE FUNCTION regress_ri_int4eq(int4, int4) RETURNS bool + AS 'int4eq' LANGUAGE internal IMMUTABLE STRICT; +CREATE OPERATOR === (LEFTARG = int4, RIGHTARG = int4, + FUNCTION = regress_ri_int4eq, COMMUTATOR = ===, + RESTRICT = eqsel, JOIN = eqjoinsel); +CREATE OPERATOR CLASS regress_ri_int4_ops FOR TYPE int4 USING btree AS + OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >, + FUNCTION 1 btint4cmp(int4, int4); +REVOKE EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) TO regress_ri_fkowner; +-- An FK column type needing a user-defined implicit cast to the PK type. +CREATE TYPE fkenum AS ENUM ('one'); +CREATE FUNCTION fkenum_to_int4(fkenum) RETURNS int4 + AS 'SELECT 1' LANGUAGE sql IMMUTABLE STRICT; +CREATE CAST (fkenum AS int4) WITH FUNCTION fkenum_to_int4(fkenum) AS IMPLICIT; +REVOKE EXECUTE ON FUNCTION fkenum_to_int4(fkenum) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION fkenum_to_int4(fkenum) TO regress_ri_fkowner; +CREATE TABLE pktable_acl_op (a int4); +CREATE UNIQUE INDEX ON pktable_acl_op (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast (a int4 PRIMARY KEY); +CREATE TABLE pktable_acl_op_part (a int4) PARTITION BY LIST (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_op_part1 PARTITION OF pktable_acl_op_part DEFAULT; +CREATE UNIQUE INDEX ON pktable_acl_op_part (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast_part (a int4 PRIMARY KEY) PARTITION BY LIST (a); +INSERT INTO pktable_acl_op VALUES (1); +INSERT INTO pktable_acl_cast VALUES (1); +ALTER TABLE pktable_acl_op OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_op_part ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast_part ENABLE ROW LEVEL SECURITY; +GRANT REFERENCES ON pktable_acl_op, pktable_acl_cast, pktable_acl_op_part, pktable_acl_cast_part TO regress_ri_fkowner; +CREATE TABLE fktable_acl_op (a int4); +CREATE TABLE fktable_acl_cast (a fkenum); +INSERT INTO fktable_acl_op VALUES (1); +INSERT INTO fktable_acl_cast VALUES ('one'); +ALTER TABLE fktable_acl_op OWNER TO regress_ri_fkowner; +ALTER TABLE fktable_acl_cast OWNER TO regress_ri_fkowner; +-- The FK owner holds EXECUTE on both functions; the PK owner does not, and +-- it is the PK owner whose privileges apply. +SET ROLE regress_ri_fkowner; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op (a); -- fails +ERROR: permission denied for function regress_ri_int4eq +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails +ERROR: permission denied for function fkenum_to_int4 +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op_part (a); -- fails (SPI path message) +ERROR: permission denied for function regress_ri_int4eq +CONTEXT: SQL statement "SELECT 1 FROM "public"."pktable_acl_op_part" x WHERE "a" OPERATOR(public.===) $1 FOR KEY SHARE OF x" +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast_part (a); -- fails (SPI path message) +ERROR: permission denied for function fkenum_to_int4 +CONTEXT: SQL statement "SELECT 1 FROM "public"."pktable_acl_cast_part" x WHERE "a" OPERATOR(pg_catalog.=) $1::pg_catalog.int4 FOR KEY SHARE OF x" +ROLLBACK TO SAVEPOINT s; +RESET ROLE; +ROLLBACK; -- A STABLE cast used by an FK check must see changes made by earlier AFTER -- triggers, using the check's snapshot rather than the outer query's snapshot. -- Compare the per-row fast path with a partitioned-parent SPI check. diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 89405dff99e..bd5a47a836c 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -2768,6 +2768,87 @@ INSERT INTO fp_fk_cci VALUES (1), (2), (3); DROP TABLE fp_fk_cci, fp_pk_cci; DROP FUNCTION fp_auto_pk; +-- The fast path must check EXECUTE, as the referenced table's owner, on the +-- functions it invokes on the FK values: the equality operator's function +-- and, for a cross-type FK, the implicit cast function. Validation of a new +-- constraint by a role that cannot use the bulk check (RLS is enabled on the +-- referenced table) runs per-row checks through the fast path. +-- +-- Wrapped with BEGIN...ROLLBACK to ensure opclasses used here don't interfere +-- with nearby tests. +BEGIN; +CREATE ROLE regress_ri_pkowner; +CREATE ROLE regress_ri_fkowner; + +-- A private btree opclass whose equality function we can revoke without +-- affecting anything else. +CREATE FUNCTION regress_ri_int4eq(int4, int4) RETURNS bool + AS 'int4eq' LANGUAGE internal IMMUTABLE STRICT; +CREATE OPERATOR === (LEFTARG = int4, RIGHTARG = int4, + FUNCTION = regress_ri_int4eq, COMMUTATOR = ===, + RESTRICT = eqsel, JOIN = eqjoinsel); +CREATE OPERATOR CLASS regress_ri_int4_ops FOR TYPE int4 USING btree AS + OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >, + FUNCTION 1 btint4cmp(int4, int4); +REVOKE EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION regress_ri_int4eq(int4, int4) TO regress_ri_fkowner; + +-- An FK column type needing a user-defined implicit cast to the PK type. +CREATE TYPE fkenum AS ENUM ('one'); +CREATE FUNCTION fkenum_to_int4(fkenum) RETURNS int4 + AS 'SELECT 1' LANGUAGE sql IMMUTABLE STRICT; +CREATE CAST (fkenum AS int4) WITH FUNCTION fkenum_to_int4(fkenum) AS IMPLICIT; +REVOKE EXECUTE ON FUNCTION fkenum_to_int4(fkenum) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION fkenum_to_int4(fkenum) TO regress_ri_fkowner; + +CREATE TABLE pktable_acl_op (a int4); +CREATE UNIQUE INDEX ON pktable_acl_op (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast (a int4 PRIMARY KEY); +CREATE TABLE pktable_acl_op_part (a int4) PARTITION BY LIST (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_op_part1 PARTITION OF pktable_acl_op_part DEFAULT; +CREATE UNIQUE INDEX ON pktable_acl_op_part (a regress_ri_int4_ops); +CREATE TABLE pktable_acl_cast_part (a int4 PRIMARY KEY) PARTITION BY LIST (a); +INSERT INTO pktable_acl_op VALUES (1); +INSERT INTO pktable_acl_cast VALUES (1); +ALTER TABLE pktable_acl_op OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_cast_part OWNER TO regress_ri_pkowner; +ALTER TABLE pktable_acl_op ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_op_part ENABLE ROW LEVEL SECURITY; +ALTER TABLE pktable_acl_cast_part ENABLE ROW LEVEL SECURITY; +GRANT REFERENCES ON pktable_acl_op, pktable_acl_cast, pktable_acl_op_part, pktable_acl_cast_part TO regress_ri_fkowner; + +CREATE TABLE fktable_acl_op (a int4); +CREATE TABLE fktable_acl_cast (a fkenum); +INSERT INTO fktable_acl_op VALUES (1); +INSERT INTO fktable_acl_cast VALUES ('one'); +ALTER TABLE fktable_acl_op OWNER TO regress_ri_fkowner; +ALTER TABLE fktable_acl_cast OWNER TO regress_ri_fkowner; + +-- The FK owner holds EXECUTE on both functions; the PK owner does not, and +-- it is the PK owner whose privileges apply. +SET ROLE regress_ri_fkowner; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op (a); -- fails +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast (a); -- fails +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_op + ADD FOREIGN KEY (a) REFERENCES pktable_acl_op_part (a); -- fails (SPI path message) +ROLLBACK TO SAVEPOINT s; +SAVEPOINT s; +ALTER TABLE fktable_acl_cast + ADD FOREIGN KEY (a) REFERENCES pktable_acl_cast_part (a); -- fails (SPI path message) +ROLLBACK TO SAVEPOINT s; +RESET ROLE; +ROLLBACK; + -- A STABLE cast used by an FK check must see changes made by earlier AFTER -- triggers, using the check's snapshot rather than the outer query's snapshot. -- Compare the per-row fast path with a partitioned-parent SPI check. -- 2.50.1 (Apple Git-155)