Skip to content

Commit 6a4e841

Browse files
Marios Trivyzascostin
authored andcommitted
SQL: Prevent StackOverflowError when parsing large statements (#33902)
Implement circuit breaker logic in the parser which catches expressions that can blow up the tree and result in StackOverflowError being thrown. Co-authored-by: Costin Leau <[email protected]>
1 parent 2b43c8a commit 6a4e841

File tree

3 files changed

+125
-7
lines changed

3 files changed

+125
-7
lines changed

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

Lines changed: 39 additions & 5 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.ObjectShortHashMap;
89
import org.antlr.v4.runtime.BaseErrorListener;
910
import org.antlr.v4.runtime.CharStream;
1011
import org.antlr.v4.runtime.CommonToken;
@@ -22,8 +23,8 @@
2223
import org.antlr.v4.runtime.dfa.DFA;
2324
import org.antlr.v4.runtime.misc.Pair;
2425
import org.antlr.v4.runtime.tree.TerminalNode;
26+
import org.apache.logging.log4j.LogManager;
2527
import org.apache.logging.log4j.Logger;
26-
import org.elasticsearch.common.logging.Loggers;
2728
import org.elasticsearch.xpack.sql.expression.Expression;
2829
import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan;
2930
import org.elasticsearch.xpack.sql.proto.SqlTypedParamValue;
@@ -41,7 +42,8 @@
4142
import static java.lang.String.format;
4243

4344
public class SqlParser {
44-
private static final Logger log = Loggers.getLogger(SqlParser.class);
45+
46+
private static final Logger log = LogManager.getLogger();
4547

4648
private final boolean DEBUG = false;
4749

@@ -83,7 +85,9 @@ public Expression createExpression(String expression, List<SqlTypedParamValue> p
8385
return invokeParser(expression, params, SqlBaseParser::singleExpression, AstBuilder::expression);
8486
}
8587

86-
private <T> T invokeParser(String sql, List<SqlTypedParamValue> params, Function<SqlBaseParser, ParserRuleContext> parseFunction,
88+
private <T> T invokeParser(String sql,
89+
List<SqlTypedParamValue> params, Function<SqlBaseParser,
90+
ParserRuleContext> parseFunction,
8791
BiFunction<AstBuilder, ParserRuleContext, T> visitor) {
8892
SqlBaseLexer lexer = new SqlBaseLexer(new CaseInsensitiveStream(sql));
8993

@@ -96,6 +100,7 @@ private <T> T invokeParser(String sql, List<SqlTypedParamValue> params, Function
96100
CommonTokenStream tokenStream = new CommonTokenStream(tokenSource);
97101
SqlBaseParser parser = new SqlBaseParser(tokenStream);
98102

103+
parser.addParseListener(new CircuitBreakerListener());
99104
parser.addParseListener(new PostProcessor(Arrays.asList(parser.getRuleNames())));
100105

101106
parser.removeErrorListeners();
@@ -125,7 +130,7 @@ private <T> T invokeParser(String sql, List<SqlTypedParamValue> params, Function
125130
return visitor.apply(new AstBuilder(paramTokens), tree);
126131
}
127132

128-
private void debug(SqlBaseParser parser) {
133+
private static void debug(SqlBaseParser parser) {
129134

130135
// when debugging, use the exact prediction mode (needed for diagnostics as well)
131136
parser.getInterpreter().setPredictionMode(PredictionMode.LL_EXACT_AMBIG_DETECTION);
@@ -154,7 +159,7 @@ private class PostProcessor extends SqlBaseBaseListener {
154159
public void exitBackQuotedIdentifier(SqlBaseParser.BackQuotedIdentifierContext context) {
155160
Token token = context.BACKQUOTED_IDENTIFIER().getSymbol();
156161
throw new ParsingException(
157-
"backquoted indetifiers not supported; please use double quotes instead",
162+
"backquoted identifiers not supported; please use double quotes instead",
158163
null,
159164
token.getLine(),
160165
token.getCharPositionInLine());
@@ -205,6 +210,35 @@ public void exitNonReserved(SqlBaseParser.NonReservedContext context) {
205210
}
206211
}
207212

213+
/**
214+
* Used to catch large expressions that can lead to stack overflows
215+
*/
216+
private class CircuitBreakerListener extends SqlBaseBaseListener {
217+
218+
private static final short MAX_RULE_DEPTH = 100;
219+
220+
// Keep current depth for every rule visited.
221+
// The totalDepth alone cannot be used as expressions like: e1 OR e2 OR e3 OR ...
222+
// are processed as e1 OR (e2 OR (e3 OR (... and this results in the totalDepth not growing
223+
// while the stack call depth is, leading to a StackOverflowError.
224+
private ObjectShortHashMap<String> depthCounts = new ObjectShortHashMap<>();
225+
226+
@Override
227+
public void enterEveryRule(ParserRuleContext ctx) {
228+
short currentDepth = depthCounts.putOrAdd(ctx.getClass().getSimpleName(), (short) 1, (short) 1);
229+
if (currentDepth > MAX_RULE_DEPTH) {
230+
throw new ParsingException("expression is too large to parse, (tree's depth exceeds {})", MAX_RULE_DEPTH);
231+
}
232+
super.enterEveryRule(ctx);
233+
}
234+
235+
@Override
236+
public void exitEveryRule(ParserRuleContext ctx) {
237+
depthCounts.putOrAdd(ctx.getClass().getSimpleName(), (short) 0, (short) -1);
238+
super.exitEveryRule(ctx);
239+
}
240+
}
241+
208242
private static final BaseErrorListener ERROR_LISTENER = new BaseErrorListener() {
209243
@Override
210244
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/QuotingTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void testBackQuotedAttribute() {
7070
String name = "@timestamp";
7171
ParsingException ex = expectThrows(ParsingException.class, () ->
7272
new SqlParser().createExpression(quote + name + quote));
73-
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted indetifiers not supported; please use double quotes instead"));
73+
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted identifiers not supported; please use double quotes instead"));
7474
}
7575

7676
public void testQuotedAttributeAndQualifier() {
@@ -92,7 +92,7 @@ public void testBackQuotedAttributeAndQualifier() {
9292
String name = "@timestamp";
9393
ParsingException ex = expectThrows(ParsingException.class, () ->
9494
new SqlParser().createExpression(quote + qualifier + quote + "." + quote + name + quote));
95-
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted indetifiers not supported; please use double quotes instead"));
95+
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted identifiers not supported; please use double quotes instead"));
9696
}
9797

9898
public void testGreedyQuoting() {

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

Lines changed: 84 additions & 0 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.google.common.base.Joiner;
89
import org.elasticsearch.test.ESTestCase;
910
import org.elasticsearch.xpack.sql.expression.NamedExpression;
1011
import org.elasticsearch.xpack.sql.expression.Order;
@@ -22,6 +23,7 @@
2223
import java.util.ArrayList;
2324
import java.util.List;
2425

26+
import static java.util.Collections.nCopies;
2527
import static java.util.stream.Collectors.toList;
2628
import static org.hamcrest.Matchers.hasEntry;
2729
import static org.hamcrest.Matchers.hasSize;
@@ -136,6 +138,88 @@ public void testMultiMatchQuery() {
136138
assertThat(mmqp.optionMap(), hasEntry("fuzzy_rewrite", "scoring_boolean"));
137139
}
138140

141+
public void testLimitToPreventStackOverflowFromLargeUnaryBooleanExpression() {
142+
// Create expression in the form of NOT(NOT(NOT ... (b) ...)
143+
144+
// 40 elements is ok
145+
new SqlParser().createExpression(
146+
Joiner.on("").join(nCopies(40, "NOT(")).concat("b").concat(Joiner.on("").join(nCopies(40, ")"))));
147+
148+
// 100 elements parser's "circuit breaker" is triggered
149+
ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createExpression(
150+
Joiner.on("").join(nCopies(100, "NOT(")).concat("b").concat(Joiner.on("").join(nCopies(100, ")")))));
151+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
152+
}
153+
154+
public void testLimitToPreventStackOverflowFromLargeBinaryBooleanExpression() {
155+
// Create expression in the form of a = b OR a = b OR ... a = b
156+
157+
// 50 elements is ok
158+
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(50, "a = b")));
159+
160+
// 100 elements parser's "circuit breaker" is triggered
161+
ParsingException e = expectThrows(ParsingException.class, () ->
162+
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(100, "a = b"))));
163+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
164+
}
165+
166+
public void testLimitToPreventStackOverflowFromLargeUnaryArithmeticExpression() {
167+
// Create expression in the form of abs(abs(abs ... (i) ...)
168+
169+
// 50 elements is ok
170+
new SqlParser().createExpression(
171+
Joiner.on("").join(nCopies(50, "abs(")).concat("i").concat(Joiner.on("").join(nCopies(50, ")"))));
172+
173+
// 101 elements parser's "circuit breaker" is triggered
174+
ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createExpression(
175+
Joiner.on("").join(nCopies(101, "abs(")).concat("i").concat(Joiner.on("").join(nCopies(101, ")")))));
176+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
177+
}
178+
179+
public void testLimitToPreventStackOverflowFromLargeBinaryArithmeticExpression() {
180+
// Create expression in the form of a + a + a + ... + a
181+
182+
// 100 elements is ok
183+
new SqlParser().createExpression(Joiner.on(" + ").join(nCopies(100, "a")));
184+
185+
// 101 elements parser's "circuit breaker" is triggered
186+
ParsingException e = expectThrows(ParsingException.class, () ->
187+
new SqlParser().createExpression(Joiner.on(" + ").join(nCopies(101, "a"))));
188+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
189+
}
190+
191+
public void testLimitToPreventStackOverflowFromLargeSubselectTree() {
192+
// Test with queries in the form of `SELECT * FROM (SELECT * FROM (... t) ...)
193+
194+
// 100 elements is ok
195+
new SqlParser().createStatement(
196+
Joiner.on(" (").join(nCopies(100, "SELECT * FROM"))
197+
.concat("t")
198+
.concat(Joiner.on("").join(nCopies(99, ")"))));
199+
200+
// 101 elements parser's "circuit breaker" is triggered
201+
ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createStatement(
202+
Joiner.on(" (").join(nCopies(101, "SELECT * FROM"))
203+
.concat("t")
204+
.concat(Joiner.on("").join(nCopies(100, ")")))));
205+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
206+
}
207+
208+
public void testLimitToPreventStackOverflowFromLargeComplexSubselectTree() {
209+
// Test with queries in the form of `SELECT true OR true OR .. FROM (SELECT true OR true OR... FROM (... t) ...)
210+
211+
new SqlParser().createStatement(
212+
Joiner.on(" (").join(nCopies(20, "SELECT ")).
213+
concat(Joiner.on(" OR ").join(nCopies(50, "true"))).concat(" FROM")
214+
.concat("t").concat(Joiner.on("").join(nCopies(19, ")"))));
215+
216+
ParsingException e = expectThrows(ParsingException.class, () -> new SqlParser().createStatement(
217+
Joiner.on(" (").join(nCopies(20, "SELECT ")).
218+
concat(Joiner.on(" OR ").join(nCopies(100, "true"))).concat(" FROM")
219+
.concat("t").concat(Joiner.on("").join(nCopies(19, ")")))));
220+
assertEquals("expression is too large to parse, (tree's depth exceeds 100)", e.getErrorMessage());
221+
}
222+
139223
private LogicalPlan parseStatement(String sql) {
140224
return new SqlParser().createStatement(sql);
141225
}

0 commit comments

Comments
 (0)