Skip to content

Commit 0fb42f4

Browse files
Merge pull request ClickHouse#77453 from ClickHouse/fuzzies
Add iteration limit in QueryFuzzer
2 parents 0a80d2d + e2ee466 commit 0fb42f4

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

src/Common/QueryFuzzer.cpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ Field QueryFuzzer::getRandomField(int type)
146146

147147
Field QueryFuzzer::fuzzField(Field field)
148148
{
149+
checkIterationLimit();
150+
149151
const auto type = field.getType();
150152

151153
int type_index = -1;
@@ -363,6 +365,8 @@ void QueryFuzzer::fuzzOrderByList(IAST * ast, const size_t nproj)
363365
return;
364366
}
365367

368+
checkIterationLimit();
369+
366370
auto * list = assert_cast<ASTExpressionList *>(ast);
367371

368372
/// Permute list
@@ -414,6 +418,8 @@ void QueryFuzzer::fuzzColumnLikeExpressionList(IAST * ast)
414418
return;
415419
}
416420

421+
checkIterationLimit();
422+
417423
auto * impl = assert_cast<ASTExpressionList *>(ast);
418424

419425
/// Permute list
@@ -477,6 +483,8 @@ void QueryFuzzer::fuzzNullsAction(NullsAction & action)
477483

478484
void QueryFuzzer::fuzzWindowFrame(ASTWindowDefinition & def)
479485
{
486+
checkIterationLimit();
487+
480488
switch (fuzz_rand() % 40)
481489
{
482490
case 0: {
@@ -624,6 +632,8 @@ void QueryFuzzer::fuzzColumnDeclaration(ASTColumnDeclaration & column)
624632

625633
DataTypePtr QueryFuzzer::fuzzDataType(DataTypePtr type)
626634
{
635+
checkIterationLimit();
636+
627637
/// Do not replace Array/Tuple/etc. with not Array/Tuple too often.
628638
const auto * type_array = typeid_cast<const DataTypeArray *>(type.get());
629639
if (type_array && fuzz_rand() % 4 != 0)
@@ -699,6 +709,8 @@ DataTypePtr QueryFuzzer::fuzzDataType(DataTypePtr type)
699709

700710
DataTypePtr QueryFuzzer::getRandomType()
701711
{
712+
checkIterationLimit();
713+
702714
static const std::vector<TypeIndex> & random_types
703715
= {TypeIndex::UInt8, TypeIndex::UInt16, TypeIndex::UInt32, TypeIndex::UInt64, TypeIndex::UInt128,
704716
TypeIndex::UInt256, TypeIndex::Int8, TypeIndex::Int16, TypeIndex::Int32, TypeIndex::Int64,
@@ -935,6 +947,8 @@ void QueryFuzzer::notifyQueryFailed(ASTPtr ast)
935947

936948
ASTPtr QueryFuzzer::fuzzLiteralUnderExpressionList(ASTPtr child)
937949
{
950+
checkIterationLimit();
951+
938952
const auto * l = child->as<ASTLiteral>();
939953
chassert(l);
940954
const auto type = l->value.getType();
@@ -1073,6 +1087,13 @@ struct ScopedIncrement
10731087
~ScopedIncrement() { --counter; }
10741088
};
10751089

1090+
void QueryFuzzer::checkIterationLimit()
1091+
{
1092+
if (++iteration_count > iteration_limit)
1093+
throw Exception(ErrorCodes::TOO_DEEP_RECURSION,
1094+
"AST complexity limit exceeded while fuzzing ({})", iteration_count);
1095+
}
1096+
10761097
static const Strings comparison_comparators
10771098
= {"equals", "notEquals", "greater", "greaterOrEquals", "less", "lessOrEquals", "isNotDistinctFrom"};
10781099

@@ -1504,6 +1525,8 @@ void QueryFuzzer::fuzz(ASTPtr & ast)
15041525
if (!ast)
15051526
return;
15061527

1528+
checkIterationLimit();
1529+
15071530
// Check for exceeding max depth.
15081531
ScopedIncrement depth_increment(current_ast_depth);
15091532
if (current_ast_depth > 500)
@@ -1528,7 +1551,7 @@ void QueryFuzzer::fuzz(ASTPtr & ast)
15281551
current_ast_depth,
15291552
debug_visited_nodes.size(),
15301553
(*debug_top_ast)->dumpTree());
1531-
assert(false);
1554+
std::abort();
15321555
}
15331556

15341557
// The fuzzing.
@@ -2169,6 +2192,7 @@ void QueryFuzzer::collectFuzzInfoRecurse(ASTPtr ast)
21692192
void QueryFuzzer::fuzzMain(ASTPtr & ast)
21702193
{
21712194
current_ast_depth = 0;
2195+
iteration_count = 0;
21722196
debug_visited_nodes.clear();
21732197
debug_top_ast = &ast;
21742198

src/Common/QueryFuzzer.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,26 @@ class QueryFuzzer
157157
// Used to track added tables in join clauses
158158
uint32_t alias_counter = 0;
159159

160+
// Similar to current_ast_depth, this is a limit on some measure of query size or number of
161+
// steps we take. Without it, with small probability, query size may explode even when depth is
162+
// limited. In particular, array lengths and depths in fuzzField() were seen to do that:
163+
// https://github.com/ClickHouse/ClickHouse/issues/77408
164+
//
165+
// I don't fully understand how this happens, but my impression is that recursive random
166+
// generators like this just generally tend to produce size distribution with heavy tail.
167+
// Maybe if the query size/depth/some-other-property reaches some critical mass, each recursive
168+
// call on average causes more than one additional recursive call (e.g. by copying a huge subtree
169+
// with some small-but-not-tiny probability), so the expected number of calls becomes infinite.
170+
// Despite infinite expected tree size, the p99 size may still be moderate
171+
// (see e.g. "St. Petersburg lottery"), so the failures can be rare in practice.
172+
//
173+
// (What does "infinite expected value" mean in practice? Suppose you keep generating more and
174+
// more values and averaging them. If the expected value is finite, the average will be
175+
// converging to it. If the expected value is infinite, the average will keep growing without
176+
// bound.)
177+
size_t iteration_count = 0;
178+
static constexpr size_t iteration_limit = 500000;
179+
160180
// These arrays hold parts of queries that we can substitute into the query
161181
// we are currently fuzzing. We add some part from each new query we are asked
162182
// to fuzz, and keep this state between queries, so the fuzzing output becomes
@@ -211,6 +231,7 @@ class QueryFuzzer
211231
void addTableLike(ASTPtr ast);
212232
void addColumnLike(ASTPtr ast);
213233
void collectFuzzInfoRecurse(ASTPtr ast);
234+
void checkIterationLimit();
214235

215236
void extractPredicates(const ASTPtr & node, ASTs & predicates, const std::string & op, int negProb);
216237
ASTPtr permutePredicateClause(const ASTPtr & predicate, int negProb);

0 commit comments

Comments
 (0)