Skip to content

Fix segfault on DETACH DELETE under RLS with a function-based edge policy (#2474)#2475

Open
BenSpex wants to merge 1 commit into
apache:masterfrom
BenSpex:fix/detach-delete-rls-segfault
Open

Fix segfault on DETACH DELETE under RLS with a function-based edge policy (#2474)#2475
BenSpex wants to merge 1 commit into
apache:masterfrom
BenSpex:fix/detach-delete-rls-segfault

Conversation

@BenSpex

@BenSpex BenSpex commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Fixes #2474.

A non-superuser subject to FORCE ROW LEVEL SECURITY crashed the backend with SIGSEGV when running MATCH (n) DETACH DELETE n on a vertex that has a connected edge, if the edge label carried an RLS policy whose USING/WITH CHECK qual invokes a function (e.g. a STABLE tenant/owner accessor). Reproduces on release_PG18_1.7.0 and current master.

Root cause / mechanism

AGE deletes a vertex's connected edges in check_for_connected_edges() (src/backend/executor/cypher_delete.c), which is called from end_cypher_delete() — i.e. during executor shutdown (ExecEndPlan, reached via PortalCleanupExecutorEnd when the portal is dropped). By that point the portal's active snapshot has already been popped, so the active-snapshot stack is empty.

RLS for Cypher DELETE is enforced at the executor level (added in #2309): check_for_connected_edges() compiles the edge label's security quals and evaluates them per candidate edge via check_security_quals()ExecQual(). When the qual calls a SQL-language function, ExecQual() dispatches into fmgr_sql()postquel_start(), which runs the function's query and reads the current snapshot with GetActiveSnapshot(). With no snapshot on the stack that is a NULL-pointer dereference → SIGSEGV. (A cassert build trips the Assert(ActiveSnapshotSet()) in postquel_start first — that assert is exactly the "caller should have ensured a suitable snapshot is active" contract.)

Backtrace (assert build) at the crash site:

Assertion failed: ActiveSnapshotSet(), functions.c
  postquel_start (functions.c)
  fmgr_sql
  ExecQual -> check_security_quals   (cypher_utils.c)
  process_edges_by_index             (cypher_delete.c)
  check_for_connected_edges          (cypher_delete.c)
  end_cypher_delete                  (cypher_delete.c)
  ExecEndCustomScan -> ExecEndPlan -> standard_ExecutorEnd

This explains the full narrowing in the issue:

  • vertex path (process_delete_list) runs during normal execution while a snapshot is active → fine;
  • edge-only DELETE and node-only DETACH DELETE never evaluate an edge qual at teardown → fine;
  • superuser bypasses RLS → fine;
  • a constant-only edge policy never enters fmgr_sql → fine;
  • only DETACH DELETE of an edge-connected vertex under a function-bearing edge policy trips it.

Fix

Ensure an active snapshot for the duration of the connected-edge scan in check_for_connected_edges(). es_snapshot is still valid there (the EState is not torn down until this returns) and is the correct snapshot for reading the edges, so push it if none is active and pop it before returning. The scans themselves already pass es_snapshot explicitly, so this only affects RLS qual evaluation; non-RLS paths are unchanged. On an error path (e.g. an RLS denial raised mid-scan) transaction abort resets the active-snapshot stack, so the unpaired push is cleaned up.

if (!ActiveSnapshotSet())
{
    PushActiveSnapshot(estate->es_snapshot);
    pushed_snapshot = true;
}
...
if (pushed_snapshot)
    PopActiveSnapshot();

Test

Adds a regression to the security suite (PART 11b): an edge-label policy whose qual calls a STABLE SQL function, then DETACH DELETE of an edge-connected vertex as the RLS-bound role. It must delete the vertex and its edge (leaving the other endpoint) instead of crashing. The existing PART 11 uses a constant-only edge policy, which does not exercise the snapshot-dependent function path.

Verification

  • A/B on an identical release-style build (PG18, -O2, --enable-cassert off): unpatched → client backend ... was terminated by signal 11: Segmentation fault; patched → deletes cleanly, no crash.
  • make installcheck against PG18 (--enable-cassert) — all 43 tests pass, including the new case.
  • The issue's function-based repro no longer crashes and deletes correctly.

…licy

A non-superuser subject to row-level security crashed the backend with
SIGSEGV when running DETACH DELETE on a vertex that has a connected edge,
if the edge label carried an RLS policy whose USING/WITH CHECK qual
invokes a function (e.g. a STABLE tenant/owner accessor). Reported in
issue apache#2474; reproduces on 1.7.0 and master.

Mechanism:

AGE deletes a vertex's connected edges in check_for_connected_edges(),
which is called from end_cypher_delete() -- i.e. during executor shutdown
(ExecEndPlan, reached via PortalCleanup -> ExecutorEnd when the portal is
dropped). By that point the portal's active snapshot has already been
popped, so the active-snapshot stack is empty.

RLS for Cypher DELETE is enforced at the executor level (added in apache#2309):
check_for_connected_edges() compiles the edge label's security quals and
evaluates them per candidate edge with check_security_quals() -> ExecQual().
When the qual calls a SQL-language function, ExecQual() dispatches into
fmgr_sql() -> postquel_start(), which runs the function's query and reads
the current snapshot with GetActiveSnapshot(). With no snapshot on the
stack that is a NULL-pointer dereference -> SIGSEGV (an assert build trips
the Assert(ActiveSnapshotSet()) in postquel_start first).

This is why the narrowing in the report holds: the vertex path
(process_delete_list) runs during normal execution while a snapshot is
active; edge-only DELETE and node-only DETACH DELETE never evaluate an
edge qual at teardown; a superuser bypasses RLS; and a constant-only edge
policy never enters fmgr_sql, so only DETACH DELETE of an edge-connected
vertex under a function-bearing edge policy trips it.

Fix:

Ensure an active snapshot for the duration of the connected-edge scan in
check_for_connected_edges(). es_snapshot is still valid there (the EState
is not torn down until this returns) and is the correct snapshot for
reading the edges, so push it if none is active and pop it before return.
The scans themselves already pass es_snapshot explicitly, so this only
affects RLS qual evaluation; non-RLS paths are unchanged. On the error
path (e.g. an RLS denial raised mid-scan) transaction abort resets the
active-snapshot stack, so the unpaired push is cleaned up.

Test:

Adds a regression to the security suite (PART 11b): an edge label policy
whose qual calls a STABLE SQL function, then DETACH DELETE of an
edge-connected vertex as the RLS-bound role. It must delete the vertex and
its edge (leaving the other endpoint) instead of crashing. The existing
PART 11 uses a constant-only edge policy, which does not exercise the
snapshot-dependent function path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Segfault: DETACH DELETE of an edge-connected vertex under FORCE ROW LEVEL SECURITY as a non-superuser

1 participant