Skip to content

Commit 7e09985

Browse files
committed
Full implementation of tree depth Circuit Breaker
1 parent 37fb6bf commit 7e09985

File tree

2 files changed

+67
-15
lines changed

2 files changed

+67
-15
lines changed

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/SqlParser.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package org.elasticsearch.xpack.sql.parser;
77

8+
import com.carrotsearch.hppc.ObjectIntHashMap;
89
import org.antlr.v4.runtime.BaseErrorListener;
910
import org.antlr.v4.runtime.CharStream;
1011
import org.antlr.v4.runtime.CommonToken;
@@ -99,7 +100,7 @@ private <T> T invokeParser(String sql,
99100
CommonTokenStream tokenStream = new CommonTokenStream(tokenSource);
100101
SqlBaseParser parser = new SqlBaseParser(tokenStream);
101102

102-
parser.addParseListener(new CircuitBreakerProcessor());
103+
parser.addParseListener(new CircuitBreakerListener());
103104
parser.addParseListener(new PostProcessor(Arrays.asList(parser.getRuleNames())));
104105

105106
parser.removeErrorListeners();
@@ -212,18 +213,26 @@ public void exitNonReserved(SqlBaseParser.NonReservedContext context) {
212213
/**
213214
* Used to catch large expressions that can lead to stack overflows
214215
*/
215-
private class CircuitBreakerProcessor extends SqlBaseBaseListener {
216+
private class CircuitBreakerListener extends SqlBaseBaseListener {
216217

217-
private static final short MAX_BOOLEAN_ELEMENTS = 1000;
218-
private short countElementsInBooleanExpressions = 0;
218+
private static final short MAX_DEPTH = 100;
219+
220+
// Keep current depth for every rule visited
221+
ObjectIntHashMap<String> depthCounts = new ObjectIntHashMap<>(100);
219222

220223
@Override
221-
public void enterLogicalBinary(SqlBaseParser.LogicalBinaryContext ctx) {
222-
if (++countElementsInBooleanExpressions == MAX_BOOLEAN_ELEMENTS) {
223-
throw new ParsingException("boolean expression is too large to parse, (exceeds {} elements)",
224-
MAX_BOOLEAN_ELEMENTS);
224+
public void enterEveryRule(ParserRuleContext ctx) {
225+
int currentDepth = depthCounts.putOrAdd(ctx.getClass().getSimpleName(), 1, 1);
226+
if (currentDepth > MAX_DEPTH) {
227+
throw new ParsingException("expression is too large to parse, (tree's depth exceeds {})", MAX_DEPTH);
225228
}
226-
super.enterLogicalBinary(ctx);
229+
super.enterEveryRule(ctx);
230+
}
231+
232+
@Override
233+
public void exitEveryRule(ParserRuleContext ctx) {
234+
depthCounts.putOrAdd(ctx.getClass().getSimpleName(), 0, -1);
235+
super.exitEveryRule(ctx);
227236
}
228237
}
229238

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,57 @@ public void testMultiMatchQuery() {
138138
assertThat(mmqp.optionMap(), hasEntry("fuzzy_rewrite", "scoring_boolean"));
139139
}
140140

141-
public void testLimitToPreventStackOverflowFromLargeBooleanExpression() {
142-
// 1000 elements is ok
143-
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(1000, "a = b")));
141+
public void testLimitToPreventStackOverflowFromLargeUnaryBooleanExpression() {
142+
// 100 elements is ok
143+
new SqlParser().createExpression(
144+
Joiner.on("NOT(").join(nCopies(100, "true")).concat(Joiner.on("").join(nCopies(99, ")"))));
144145

145-
// 1001 elements parser's "circuit breaker" is triggered
146+
// 500 elements parser's "circuit breaker" is triggered
147+
ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createExpression(
148+
Joiner.on("NOT(").join(nCopies(101, "true")).concat(Joiner.on("").join(nCopies(100, ")")))));
149+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
150+
}
151+
152+
public void testLimitToPreventStackOverflowFromLargeBinaryBooleanExpression() {
153+
// 100 elements is ok
154+
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(100, "true")));
155+
156+
// 101 elements parser's "circuit breaker" is triggered
146157
ParsingException e = expectThrows(ParsingException.class, () ->
147-
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(1001, "a = b"))));
148-
assertEquals("boolean expression is too large to parse, (exceeds 1000 elements)", e.getErrorMessage());
158+
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(101, "a = b"))));
159+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
160+
}
161+
162+
public void testLimitToPreventStackOverflowFromLargeUnaryArithmeticExpression() {
163+
// 100 elements is ok
164+
new SqlParser().createExpression(
165+
Joiner.on("abs(").join(nCopies(100, "i")).concat(Joiner.on("").join(nCopies(99, ")"))));
166+
167+
// 101 elements parser's "circuit breaker" is triggered
168+
ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createExpression(
169+
Joiner.on("abs(").join(nCopies(101, "i")).concat(Joiner.on("").join(nCopies(100, ")")))));
170+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
171+
}
172+
173+
public void testLimitToPreventStackOverflowFromLargeBinaryArithmeticExpression() {
174+
// 100 elements is ok
175+
new SqlParser().createExpression(Joiner.on(" + ").join(nCopies(100, "a")));
176+
177+
// 101 elements parser's "circuit breaker" is triggered
178+
ParsingException e = expectThrows(ParsingException.class, () ->
179+
new SqlParser().createExpression(Joiner.on(" + ").join(nCopies(101, "a"))));
180+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
181+
}
182+
183+
public void testLimitToPreventStackOverflowFromLargeSubselectTree() {
184+
// 100 elements is ok
185+
new SqlParser().createStatement(
186+
Joiner.on(" (").join(nCopies(100, "SELECT * FROM")).concat("t").concat(Joiner.on("").join(nCopies(99, ")"))));
187+
188+
// 101 elements parser's "circuit breaker" is triggered
189+
ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createStatement(
190+
Joiner.on(" (").join(nCopies(101, "SELECT * FROM")).concat("t").concat(Joiner.on("").join(nCopies(100, ")")))));
191+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
149192
}
150193

151194
private LogicalPlan parseStatement(String sql) {

0 commit comments

Comments
 (0)