Subject: Enforce INSERT RLS checks for FOR PORTION OF leftovers?

From: Ayush Tiwari <[email-masked]>
Date: Sat, 2 May 2026 00:23:04 +0530

Hi,

I found what looks like a  discrepancy where UPDATE/DELETE FOR
PORTION OF commands bypass INSERT RLS WITH CHECK
policies when inserting temporal leftover rows. Not sure if it's already
flagged (could not find it in DL).

While it is intentional that ExecForPortionOfLeftovers() skips INSERT ACL
permission checks, the leftover rows are newly inserted rows and should
still satisfy INSERT/ALL RLS policies unless I'm missing something.

Currently, the rewrite phase only
attaches UPDATE/DELETE RLS checks for the target relation, leaving
ExecInsert() without a WCO_RLS_INSERT_CHECK to enforce for the
leftovers.

Maybe we should address this in rowsecurity.c by fetching CMD_INSERT
policies and adding them as WCO_RLS_INSERT_CHECK entries for queries
with a FOR PORTION OF clause?

Something like this:

--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -393,6 +393,34 @@ get_row_security_policies(Query *root, RangeTblEntry
*rte, int rt_index,
  }
  }

+ /*
+ * UPDATE/DELETE FOR PORTION OF commands insert temporal leftovers via
+ * ExecInsert().  Those internal inserts intentionally skip INSERT ACL
+ * permission checks, but they still create new rows and must satisfy any
+ * INSERT/ALL RLS WITH CHECK policies.
+ */
+ if ((commandType == CMD_UPDATE || commandType == CMD_DELETE) &&
+ root->forPortionOf != NULL)
+ {
+ List   *insert_permissive_policies;
+ List   *insert_restrictive_policies;
+
+ /* This should be the target relation */
+ Assert(rt_index == root->resultRelation);
+
+ get_policies_for_relation(rel, CMD_INSERT, user_id,
+  &insert_permissive_policies,
+  &insert_restrictive_policies);
+
+ add_with_check_options(rel, rt_index,
+   WCO_RLS_INSERT_CHECK,
+   insert_permissive_policies,
+   insert_restrictive_policies,
+   withCheckOptions,
+   hasSubLinks,
+   false);
+ }
+

Thoughts?

Regards,
Ayush

---

From: Ayush Tiwari <[email-masked]>
Date: Mon, 4 May 2026 16:59:21 +0530

Hi,

> still satisfy INSERT/ALL RLS policies unless I'm missing something.
Sharing a SQL repro example:

CREATE ROLE u;
CREATE TABLE t (id int, valid_at daterange NOT NULL, name text);
ALTER TABLE t ENABLE ROW LEVEL SECURITY;
CREATE POLICY p_all ON t FOR ALL TO u USING (true) WITH CHECK (true);
CREATE POLICY p_ins ON t FOR INSERT TO u WITH CHECK (false);
GRANT SELECT, INSERT, UPDATE, DELETE ON t TO u;
INSERT INTO t VALUES (1, daterange('2018-01-01','2020-01-01'), 'ok');

SET ROLE u;

-- (A) Fails as expected: new row violates row-level security policy
INSERT INTO t VALUES (2, daterange('2018-01-01','2020-01-01'), 'ok');

-- (B) Should fail the same way (creates leftover rows), but silently
succeeds
UPDATE t FOR PORTION OF valid_at FROM '2019-01-01' TO '2019-06-01'
  SET name = 'ok' WHERE id = 1;

If this is expected we need to change the documentation of policy
and if it is not, should we go with something like I shared in
upthread, I can send a patch file if required.

Regards,
Ayush

---

From: Paul A Jungwirth <[email-masked]>
Date: Sat, 16 May 2026 09:50:35 -0700

On Mon, May 4, 2026 at 4:29 AM Ayush Tiwari <[email-masked]> wrote:
>
> On Sat, 2 May 2026 at 00:23, Ayush Tiwari <[email-masked]> wrote:
>>
> -- (B) Should fail the same way (creates leftover rows), but silently succeeds

Skipping the RLS checks to insert the leftovers seems like the correct
behavior to me, since we are skipping the ACL checks (per the
standard). Shouldn't it be consistent?

I think the reason we skip the checks is that semantically, the
leftovers aren't changing anything: they are preserving the history
that is already there.

I'm happy to write a doc patch to make this explicit. Does anyone
disagree about the correct behavior though?

Yours,

-- 
Paul              ~{:-)
[email-masked]

---

From: Ayush Tiwari <[email-masked]>
Date: Fri, 5 Jun 2026 22:16:57 +0530

Hi,

On Sat, 16 May 2026 at 22:20, Paul A Jungwirth <[email-masked]>
wrote:

> disagree about the correct behavior though?

I'd treat them separately.  Skipping INSERT ACL is a standard-driven
call about command privilege; RLS WITH CHECK is a per-row predicate
on the data, and the leftover really is a new heap tuple written by
the current role.

With `WITH CHECK (name <> 'denied')` and a pre-existing 'denied' row
(loaded by COPY or before the policy), UPDATE FOR PORTION OF that
touches only the range column writes leftovers the role could not
have INSERTed directly.  That's the part that bothers me, especially
since RLS tends to be deployed as a hard security boundary rather
than something users expect to opt out of per statement type.
Please correct me if I'm wrong here.

Regards,
Ayush

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 15 Jul 2026 10:35:09 -0500

[RMT hat]

This is listed as an open item for v19 [0], but there's been no update to
the thread since June 5th.  Is it on track for resolution sometime soon?

[0] https://wiki.postgresql.org/wiki/PostgreSQL_19_Open_Items

-- 
nathan

---

From: Paul A Jungwirth <[email-masked]>
Date: Wed, 15 Jul 2026 10:17:50 -0700

> the thread since June 5th.  Is it on track for resolution sometime soon?

I think the patch at [1] is ready to go, but it's on a separate thread.

I updated the Open Items list in include both conversations.

[1] https://www.postgresql.org/message-id/20DF964A-C291-491A-9126-62753DCBD7CE%40gmail.com

Yours,