Forbid arrayJoin in row policy filter expressions

A row policy USING expression is applied as a per-row filter at the
storage read stage (a row-level, PREWHERE-like step). arrayJoin changes
the number of rows, which breaks the reader's invariant and aborts with
the logical error 'column->size() == num_rows'.

Reject arrayJoin in a row policy filter both when the policy is created
or altered and when it is applied, so existing on-disk or config-defined
policies fail the query cleanly instead of crashing. arrayJoin scoped
inside a nested subquery is still allowed, as it does not change the
outer row count.

CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109493&sha=6774b64cd3bd77f1c9b010978b9810caa9dd015d&name_0=PR&name_1=Stress%20test%20%28amd_tsan%29

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Raúl Marín
2026-07-08 11:44:25 +02:00
co-authored by Claude Opus 4.8
parent e01cbab452
commit a3bdef6580
6 changed files with 69 additions and 0 deletions
+3
View File
@@ -557,6 +557,9 @@ RowPolicyFilterPtr ContextAccess::getRowPolicyFilter(const String & database, co
}
}
if (filter)
checkRowPolicyFilterExpression(filter->expression);
if (filter && filter->policies.empty())
{
if (access_control->shouldThrowOnUnmatchedRowPolicies())
+27
View File
@@ -1,6 +1,8 @@
#include <Access/RowPolicy.h>
#include <Common/Exception.h>
#include <Common/quoteString.h>
#include <Parsers/ASTFunction.h>
#include <Parsers/ASTSelectQuery.h>
#include <boost/range/algorithm/equal.hpp>
@@ -9,6 +11,31 @@ namespace DB
namespace ErrorCodes
{
extern const int NOT_IMPLEMENTED;
extern const int ILLEGAL_PREWHERE;
}
namespace
{
/// `arrayJoin` inside a nested subquery has its own scope and does not multiply the outer rows.
bool expressionContainsArrayJoin(const ASTPtr & ast)
{
if (!ast)
return false;
if (const auto * function = ast->as<ASTFunction>(); function && function->name == "arrayJoin")
return true;
for (const auto & child : ast->children)
{
if (!child->as<ASTSelectQuery>() && expressionContainsArrayJoin(child))
return true;
}
return false;
}
}
void checkRowPolicyFilterExpression(const ASTPtr & expression)
{
if (expressionContainsArrayJoin(expression))
throw Exception(ErrorCodes::ILLEGAL_PREWHERE, "arrayJoin is not allowed in a row policy filter expression");
}
+5
View File
@@ -4,6 +4,7 @@
#include <Access/RolesOrUsersSet.h>
#include <Access/Common/RowPolicyDefs.h>
#include <Core/Types.h>
#include <Parsers/IAST_fwd.h>
#include <array>
@@ -70,4 +71,8 @@ private:
using RowPolicyPtr = std::shared_ptr<const RowPolicy>;
/// A row policy filter is applied as a per-row predicate at the storage read stage, so a
/// row-count-changing function like `arrayJoin` breaks the reader's invariants. Reject it.
void checkRowPolicyFilterExpression(const ASTPtr & expression);
}
@@ -41,7 +41,10 @@ namespace
policy.setRestrictive(*query.is_restrictive);
for (const auto & [filter_type, filter] : query.filters)
{
checkRowPolicyFilterExpression(filter);
policy.filters[static_cast<size_t>(filter_type)] = filter ? filter->formatWithSecretsOneLine() : String{};
}
if (override_to_roles)
policy.to_roles = *override_to_roles;
@@ -0,0 +1,4 @@
1 10
2 20
1 10
2 20
@@ -0,0 +1,27 @@
-- A row policy filter is applied as a per-row predicate at the storage read stage. `arrayJoin`
-- changes the number of rows, which used to abort with 'column->size() == num_rows' (a logical
-- error). It must be rejected instead: when the policy is created, when it is altered, and when
-- it is applied.
DROP ROW POLICY IF EXISTS policy_with_array_join ON row_policy_table;
DROP ROW POLICY IF EXISTS valid_policy ON row_policy_table;
DROP TABLE IF EXISTS row_policy_table;
CREATE TABLE row_policy_table (id UInt32, value UInt32) ENGINE = MergeTree ORDER BY id;
INSERT INTO row_policy_table VALUES (1, 10), (2, 20);
-- Creating a policy whose filter contains arrayJoin is rejected.
CREATE ROW POLICY policy_with_array_join ON row_policy_table FOR SELECT USING arrayJoin([1, 2]) OR (value = 0) TO ALL; -- { serverError ILLEGAL_PREWHERE }
-- Altering a valid policy to introduce arrayJoin is rejected too; the original filter is kept.
CREATE ROW POLICY valid_policy ON row_policy_table FOR SELECT USING value > 0 TO ALL;
ALTER ROW POLICY valid_policy ON row_policy_table FOR SELECT USING arrayJoin([1, 2]) OR (value > 0); -- { serverError ILLEGAL_PREWHERE }
SELECT * FROM row_policy_table ORDER BY id;
DROP ROW POLICY valid_policy ON row_policy_table;
-- arrayJoin scoped inside a subquery does not change the outer row count and stays allowed.
CREATE ROW POLICY valid_policy ON row_policy_table FOR SELECT USING value IN (SELECT arrayJoin([10, 20])) TO ALL;
SELECT * FROM row_policy_table ORDER BY id;
DROP ROW POLICY valid_policy ON row_policy_table;
DROP TABLE row_policy_table;