1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

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 gets EXECUTE on both checked by
ExecInitFunc() when it initializes the generated query, as the
referenced table's owner, but the fast path performed neither check.

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, and
repeat each against a partitioned referenced table, which takes the
SPI path, to show that the two behave identically.

Reported-by: Nikolay Samokhvalov <nik@postgres.ai> (offlist)
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Discussion: https://postgr.es/m/CA+HiwqGH+b7sXmsH8sT18diujbheSObWs0gWXqLZqRccMKvZAA@mail.gmail.com
Backpatch-through: 19
This commit is contained in:
Amit Langote
2026-09-26 11:07:33 +09:00
parent aa60617b69
commit f8eb70f050
3 changed files with 202 additions and 0 deletions

View File

@@ -31,11 +31,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"
@@ -305,6 +307,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,
@@ -2851,6 +2855,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);
@@ -3196,6 +3201,41 @@ 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.
*
* This parallels the checks ExecInitFunc() performs when the SPI path
* initializes its generated query, where the equality operator's function
* appears in the 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.

View File

@@ -3801,6 +3801,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.

View File

@@ -2771,6 +2771,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.