This is an automated email from the ASF dual-hosted git repository.

MuhammadTahaNaveed pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/age.git

commit 1e6aca5cd702e6470d70a44a7298d46fff5b4f1d
Author: John Gemignani <[email protected]>
AuthorDate: Thu Oct 8 05:15:37 2026 +0000

    Return null from vertex_stats for a vertex outside the graph
    
    vertex_stats() looked the vertex up in the traversal graph and used the
    entry without checking it. Since #2561, row-level security can hide a
    vertex from that graph while an untyped MATCH, which scans the parent
    label table, still returns it, so vertex_stats() dereferenced a NULL
    entry and terminated the backend (review issue 2 on #2561).
    
    Return null when the vertex is not in the graph the caller can traverse.
    The statistics of visible vertices are computed over that graph, so
    edges to hidden vertices are not counted.
    
    The new rls_vle part hides a vertex with a policy and checks the
    statistics of every vertex an untyped MATCH returns.
    
    Co-authored-by: Copilot <[email protected]>
---
 regress/expected/rls_vle.out             | 38 ++++++++++++++++++++++++++++++++
 regress/sql/rls_vle.sql                  | 24 ++++++++++++++++++++
 src/backend/utils/adt/age_global_graph.c | 11 +++++++++
 3 files changed, 73 insertions(+)

diff --git a/regress/expected/rls_vle.out b/regress/expected/rls_vle.out
index ec7220f9..b04f3a5d 100644
--- a/regress/expected/rls_vle.out
+++ b/regress/expected/rls_vle.out
@@ -1245,6 +1245,44 @@ NOTICE:  graph "rls_vle_after_rls" has been dropped
  
 (1 row)
 
+-- ============================================================================
+-- PART 9: vertex_stats on a vertex hidden from traversals. An untyped MATCH
+-- scans the parent label table, which the child table's policy does not
+-- filter, so it can return a vertex that the traversal graph does not hold.
+-- vertex_stats returns null for it; visible vertices get statistics computed
+-- over the graph the caller can traverse.
+-- ============================================================================
+ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY;
+CREATE POLICY node_visible ON rls_vle."Node"
+    FOR SELECT USING ((properties->>'"name"') <> 'n2');
+SET ROLE rls_vle_user;
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (n:Node) RETURN n.name
+$$) AS (name agtype) ORDER BY name;
+ name 
+------
+ "n1"
+ "n3"
+ "n4"
+(3 rows)
+
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (n)
+    WITH n, vertex_stats(n) AS s
+    RETURN n.name, s IS NULL, s.in_degree, s.out_degree
+$$) AS (name agtype, no_stats agtype, in_degree agtype, out_degree agtype)
+ORDER BY name;
+ name | no_stats | in_degree | out_degree 
+------+----------+-----------+------------
+ "n1" | false    | 0         | 0
+ "n2" | true     |           | 
+ "n3" | false    | 0         | 1
+ "n4" | false    | 1         | 0
+(4 rows)
+
+RESET ROLE;
+DROP POLICY node_visible ON rls_vle."Node";
+ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
 -- ============================================================================
 -- cleanup
 -- ============================================================================
diff --git a/regress/sql/rls_vle.sql b/regress/sql/rls_vle.sql
index b12df277..70c4703c 100644
--- a/regress/sql/rls_vle.sql
+++ b/regress/sql/rls_vle.sql
@@ -784,6 +784,30 @@ SELECT drop_graph('rls_vle_before_rls', true);
 SELECT drop_graph('rls_vle_after_plain', true);
 SELECT drop_graph('rls_vle_after_rls', true);
 
+-- ============================================================================
+-- PART 9: vertex_stats on a vertex hidden from traversals. An untyped MATCH
+-- scans the parent label table, which the child table's policy does not
+-- filter, so it can return a vertex that the traversal graph does not hold.
+-- vertex_stats returns null for it; visible vertices get statistics computed
+-- over the graph the caller can traverse.
+-- ============================================================================
+ALTER TABLE rls_vle."Node" ENABLE ROW LEVEL SECURITY;
+CREATE POLICY node_visible ON rls_vle."Node"
+    FOR SELECT USING ((properties->>'"name"') <> 'n2');
+SET ROLE rls_vle_user;
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (n:Node) RETURN n.name
+$$) AS (name agtype) ORDER BY name;
+SELECT * FROM cypher('rls_vle', $$
+    MATCH (n)
+    WITH n, vertex_stats(n) AS s
+    RETURN n.name, s IS NULL, s.in_degree, s.out_degree
+$$) AS (name agtype, no_stats agtype, in_degree agtype, out_degree agtype)
+ORDER BY name;
+RESET ROLE;
+DROP POLICY node_visible ON rls_vle."Node";
+ALTER TABLE rls_vle."Node" DISABLE ROW LEVEL SECURITY;
+
 -- ============================================================================
 -- cleanup
 -- ============================================================================
diff --git a/src/backend/utils/adt/age_global_graph.c 
b/src/backend/utils/adt/age_global_graph.c
index ce973075..460c946c 100644
--- a/src/backend/utils/adt/age_global_graph.c
+++ b/src/backend/utils/adt/age_global_graph.c
@@ -2787,6 +2787,17 @@ Datum age_vertex_stats(PG_FUNCTION_ARGS)
     /* get the vertex entry */
     ve = get_vertex_entry(ggctx, vid);
 
+    /*
+     * The vertex is absent from the graph the caller can traverse, for
+     * example when row-level security hides it while an untyped MATCH, which
+     * scans the parent label table, still returns it. There are no
+     * statistics for it.
+     */
+    if (ve == NULL)
+    {
+        PG_RETURN_NULL();
+    }
+
     /* zero the state */
     memset(&result, 0, sizeof(agtype_in_state));
 

Reply via email to