Re: BUG #18782: Inconsistent behaviour with triggers and row level security - depends on prior number of inserts - Mailing list pgsql-bugs

From Tom Lane
Subject Re: BUG #18782: Inconsistent behaviour with triggers and row level security - depends on prior number of inserts
Date
Msg-id 762350.1737834988@sss.pgh.pa.us
Whole thread Raw
Responses Re: BUG #18782: Inconsistent behaviour with triggers and row level security - depends on prior number of inserts
List pgsql-bugs
PG Bug reporting form <noreply@postgresql.org> writes:
> We have been seeing behaviour of the interaction between triggers and row
> level security being inconsistent depending on the number of inserts that
> have previously been made to the table which the trigger is attached to. I
> will attach full a code reproduction at the end of the issue.

Thanks for the carefully-documented bug report!  Often we have to
ask a lot of questions before we can get to the bottom of a problem.

> Our expectation is that a trigger which calls RLS which throws an exception
> should always throw an exception.

I think this expectation is faulty, and there's not really any bug
here.  Poking at your example, I get

regression=# set role app_user;
SET
regression=> explain verbose
SELECT 1
            FROM rls_table
            WHERE rls_table.should_not_duplicate = gen_random_uuid();
ERROR:  invalid input syntax for type uuid: ""
regression=> set team.team_id = '6a43cea8-4a5c-4989-bae2-ef5a77d92620';
SET
regression=> explain verbose
SELECT 1
            FROM rls_table
            WHERE rls_table.should_not_duplicate = gen_random_uuid();
                                                                QUERY PLAN
                  

------------------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on public.rls_table  (cost=0.00..40.00 rows=1 width=4)
   Output: 1
   Filter: ((rls_table.should_not_duplicate = gen_random_uuid()) AND (rls_table.team_id =
(current_setting('team.team_id'::text))::uuid))
(3 rows)

Now first off, notice that I couldn't even do an EXPLAIN as app_user
until I set team.team_id.  That indicates that the error is being
thrown during planning.  The planner is trying to estimate the
selectivity of the "rls_table.team_id = something" clause, and
since current_setting is not volatile it sees no reason not to
evaluate it to get a constant to check against the statistics for
team_id.  Then when it tries to evaluate the cast to uuid, kaboom.

Secondly, notice that the RLS-derived team_id clause is placed
after the one about should_not_duplicate.  This is perhaps
surprising, but it's legitimate because uuid_eq is marked
leakproof, meaning we trust it not to leak any information
about the table contents.  What that means is that at runtime,
the cast to uuid will blow up only if there's at least one match
in rls_table.should_not_duplicate to the proposed new value.
In your test case, rls_table remains empty so there will never
be a run-time failure, whether team.team_id is valid or not.

So the error is not coming from where you think.  It's coming from
planning, and the reason it stops appearing after a few successful
trigger executions is that the plancache switches over to a generic
plan and stops invoking the planner.

(I did verify that the test on rls_table.team_id is still present
in the generic plan, so there's not a bug of that sort.)

In general, I would be wary of depending on errors to be thrown in
specific contexts and not other contexts.  That's a hard property to
guarantee in the face of any optimization, and we don't really try.
In the situation at hand, that means that RLS conditions that could
throw errors are a bad idea.  This one, far from enforcing that the
app_user can't learn about what is in rls_table, actually facilitates
doing so :-(.  I think it'd be safer to form the RLS tests as

    team_id::text = current_setting('team.team_id', true)

(where "true" prevents failure if team.team_id is unset).

            regards, tom lane



pgsql-bugs by date:

Previous
From: Fujii Masao
Date:
Subject: Re: BUG #18663: synchronous_standby_names vs synchronous_commit vs pg_stat_replication
Next
From: Alexander Kukushkin
Date:
Subject: Superuser can't revoke role granted by non-superuser