Skip to content

Commit 74502d7

Browse files
authored
SQL: Fix wrong appliance of StackOverflow limit for IN (#36724)
Fix grammar so that each element inside the list of values for IN is a valueExpression and not a more generic expression. Introduce a mapping for context names as some rules in the grammar are exited with a different rule from the one they entered.This helps so that the decrement of depth counts in the Parser's CircuitBreakerListener works correctly. For the list of values for IN, don't count the PrimaryExpressionContext as this is not visited on exitRule() due to the peculiarity in our gramamr with the predicate and predicated. Fixes: #36592
1 parent 1afcfc9 commit 74502d7

File tree

5 files changed

+127
-18
lines changed

5 files changed

+127
-18
lines changed

x-pack/plugin/sql/src/main/antlr/SqlBase.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ predicated
186186
// instead the property kind is used to differentiate
187187
predicate
188188
: NOT? kind=BETWEEN lower=valueExpression AND upper=valueExpression
189-
| NOT? kind=IN '(' expression (',' expression)* ')'
189+
| NOT? kind=IN '(' valueExpression (',' valueExpression)* ')'
190190
| NOT? kind=IN '(' query ')'
191191
| NOT? kind=LIKE pattern
192192
| NOT? kind=RLIKE regex=string

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ public Expression visitPredicated(PredicatedContext ctx) {
226226
if (pCtx.query() != null) {
227227
throw new ParsingException(loc, "IN query not supported yet");
228228
}
229-
e = new In(loc, exp, expressions(pCtx.expression()));
229+
e = new In(loc, exp, expressions(pCtx.valueExpression()));
230230
break;
231231
case SqlBaseParser.LIKE:
232232
e = new Like(loc, exp, visitPattern(pCtx.pattern()));

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3363,12 +3363,6 @@ public ValueExpressionContext valueExpression(int i) {
33633363
return getRuleContext(ValueExpressionContext.class,i);
33643364
}
33653365
public TerminalNode NOT() { return getToken(SqlBaseParser.NOT, 0); }
3366-
public List<ExpressionContext> expression() {
3367-
return getRuleContexts(ExpressionContext.class);
3368-
}
3369-
public ExpressionContext expression(int i) {
3370-
return getRuleContext(ExpressionContext.class,i);
3371-
}
33723366
public TerminalNode IN() { return getToken(SqlBaseParser.IN, 0); }
33733367
public QueryContext query() {
33743368
return getRuleContext(QueryContext.class,0);
@@ -3449,7 +3443,7 @@ public final PredicateContext predicate() throws RecognitionException {
34493443
setState(502);
34503444
match(T__0);
34513445
setState(503);
3452-
expression();
3446+
valueExpression(0);
34533447
setState(508);
34543448
_errHandler.sync(this);
34553449
_la = _input.LA(1);
@@ -3459,7 +3453,7 @@ public final PredicateContext predicate() throws RecognitionException {
34593453
setState(504);
34603454
match(T__2);
34613455
setState(505);
3462-
expression();
3456+
valueExpression(0);
34633457
}
34643458
}
34653459
setState(510);
@@ -6616,7 +6610,7 @@ private boolean valueExpression_sempred(ValueExpressionContext _localctx, int pr
66166610
"\u01f0\7\16\2\2\u01f0\u01f1\5<\37\2\u01f1\u01f2\7\n\2\2\u01f2\u01f3\5"+
66176611
"<\37\2\u01f3\u021b\3\2\2\2\u01f4\u01f6\7=\2\2\u01f5\u01f4\3\2\2\2\u01f5"+
66186612
"\u01f6\3\2\2\2\u01f6\u01f7\3\2\2\2\u01f7\u01f8\7-\2\2\u01f8\u01f9\7\3"+
6619-
"\2\2\u01f9\u01fe\5,\27\2\u01fa\u01fb\7\5\2\2\u01fb\u01fd\5,\27\2\u01fc"+
6613+
"\2\2\u01f9\u01fe\5<\37\2\u01fa\u01fb\7\5\2\2\u01fb\u01fd\5<\37\2\u01fc"+
66206614
"\u01fa\3\2\2\2\u01fd\u0200\3\2\2\2\u01fe\u01fc\3\2\2\2\u01fe\u01ff\3\2"+
66216615
"\2\2\u01ff\u0201\3\2\2\2\u0200\u01fe\3\2\2\2\u0201\u0202\7\4\2\2\u0202"+
66226616
"\u021b\3\2\2\2\u0203\u0205\7=\2\2\u0204\u0203\3\2\2\2\u0204\u0205\3\2"+

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

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@
2626
import org.apache.logging.log4j.LogManager;
2727
import org.apache.logging.log4j.Logger;
2828
import org.elasticsearch.xpack.sql.expression.Expression;
29+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BackQuotedIdentifierContext;
30+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BooleanDefaultContext;
31+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BooleanExpressionContext;
32+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.PrimaryExpressionContext;
33+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryPrimaryDefaultContext;
34+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryTermContext;
35+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QuoteIdentifierContext;
36+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementContext;
37+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementDefaultContext;
38+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.UnquoteIdentifierContext;
39+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionContext;
40+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionDefaultContext;
2941
import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan;
3042
import org.elasticsearch.xpack.sql.proto.SqlTypedParamValue;
3143

@@ -214,10 +226,26 @@ public void exitNonReserved(SqlBaseParser.NonReservedContext context) {
214226
/**
215227
* Used to catch large expressions that can lead to stack overflows
216228
*/
217-
private class CircuitBreakerListener extends SqlBaseBaseListener {
229+
static class CircuitBreakerListener extends SqlBaseBaseListener {
218230

219231
private static final short MAX_RULE_DEPTH = 200;
220232

233+
/**
234+
* Due to the structure of the grammar and our custom handling in {@link ExpressionBuilder}
235+
* some expressions can exit with a different class than they entered:
236+
* e.g.: ValueExpressionContext can exit as ValueExpressionDefaultContext
237+
*/
238+
private static final Map<String, String> ENTER_EXIT_RULE_MAPPING = new HashMap<>();
239+
240+
static {
241+
ENTER_EXIT_RULE_MAPPING.put(StatementDefaultContext.class.getSimpleName(), StatementContext.class.getSimpleName());
242+
ENTER_EXIT_RULE_MAPPING.put(QueryPrimaryDefaultContext.class.getSimpleName(), QueryTermContext.class.getSimpleName());
243+
ENTER_EXIT_RULE_MAPPING.put(BooleanDefaultContext.class.getSimpleName(), BooleanExpressionContext.class.getSimpleName());
244+
ENTER_EXIT_RULE_MAPPING.put(ValueExpressionDefaultContext.class.getSimpleName(), ValueExpressionContext.class.getSimpleName());
245+
}
246+
247+
private boolean insideIn = false;
248+
221249
// Keep current depth for every rule visited.
222250
// The totalDepth alone cannot be used as expressions like: e1 OR e2 OR e3 OR ...
223251
// are processed as e1 OR (e2 OR (e3 OR (... and this results in the totalDepth not growing
@@ -226,9 +254,18 @@ private class CircuitBreakerListener extends SqlBaseBaseListener {
226254

227255
@Override
228256
public void enterEveryRule(ParserRuleContext ctx) {
229-
if (ctx.getClass() != SqlBaseParser.UnquoteIdentifierContext.class &&
230-
ctx.getClass() != SqlBaseParser.QuoteIdentifierContext.class &&
231-
ctx.getClass() != SqlBaseParser.BackQuotedIdentifierContext.class) {
257+
if (inDetected(ctx)) {
258+
insideIn = true;
259+
}
260+
261+
// Skip PrimaryExpressionContext for IN as it's not visited on exit due to
262+
// the grammar's peculiarity rule with "predicated" and "predicate".
263+
// Also skip the Identifiers as they are "cheap".
264+
if (ctx.getClass() != UnquoteIdentifierContext.class &&
265+
ctx.getClass() != QuoteIdentifierContext.class &&
266+
ctx.getClass() != BackQuotedIdentifierContext.class &&
267+
(insideIn == false || ctx.getClass() != PrimaryExpressionContext.class)) {
268+
232269
int currentDepth = depthCounts.putOrAdd(ctx.getClass().getSimpleName(), (short) 1, (short) 1);
233270
if (currentDepth > MAX_RULE_DEPTH) {
234271
throw new ParsingException(source(ctx), "SQL statement too large; " +
@@ -240,12 +277,35 @@ public void enterEveryRule(ParserRuleContext ctx) {
240277

241278
@Override
242279
public void exitEveryRule(ParserRuleContext ctx) {
243-
// Avoid having negative numbers
244-
if (depthCounts.containsKey(ctx.getClass().getSimpleName())) {
245-
depthCounts.putOrAdd(ctx.getClass().getSimpleName(), (short) 0, (short) -1);
280+
if (inDetected(ctx)) {
281+
insideIn = false;
246282
}
283+
284+
decrementCounter(ctx);
247285
super.exitEveryRule(ctx);
248286
}
287+
288+
ObjectShortHashMap<String> depthCounts() {
289+
return depthCounts;
290+
}
291+
292+
private void decrementCounter(ParserRuleContext ctx) {
293+
String className = ctx.getClass().getSimpleName();
294+
String classNameToDecrement = ENTER_EXIT_RULE_MAPPING.getOrDefault(className, className);
295+
296+
// Avoid having negative numbers
297+
if (depthCounts.containsKey(classNameToDecrement)) {
298+
depthCounts.putOrAdd(classNameToDecrement, (short) 0, (short) -1);
299+
}
300+
}
301+
302+
private boolean inDetected(ParserRuleContext ctx) {
303+
if (ctx.getParent() != null && ctx.getParent().getClass() == SqlBaseParser.PredicateContext.class) {
304+
SqlBaseParser.PredicateContext pc = (SqlBaseParser.PredicateContext) ctx.getParent();
305+
return pc.kind != null && pc.kind.getType() == SqlBaseParser.IN;
306+
}
307+
return false;
308+
}
249309
}
250310

251311
private static final BaseErrorListener ERROR_LISTENER = new BaseErrorListener() {

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,19 @@
1515
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MatchQueryPredicate;
1616
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MultiMatchQueryPredicate;
1717
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.StringQueryPredicate;
18+
import org.elasticsearch.xpack.sql.expression.predicate.operator.comparison.In;
19+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BooleanExpressionContext;
20+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryPrimaryDefaultContext;
21+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryTermContext;
22+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementContext;
23+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementDefaultContext;
24+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionContext;
25+
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionDefaultContext;
1826
import org.elasticsearch.xpack.sql.plan.logical.Filter;
1927
import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan;
2028
import org.elasticsearch.xpack.sql.plan.logical.OrderBy;
2129
import org.elasticsearch.xpack.sql.plan.logical.Project;
30+
import org.elasticsearch.xpack.sql.plan.logical.With;
2231

2332
import java.util.ArrayList;
2433
import java.util.List;
@@ -28,6 +37,7 @@
2837
import static org.hamcrest.Matchers.hasEntry;
2938
import static org.hamcrest.Matchers.hasSize;
3039
import static org.hamcrest.Matchers.instanceOf;
40+
import static org.hamcrest.Matchers.startsWith;
3141

3242
public class SqlParserTests extends ESTestCase {
3343

@@ -254,6 +264,51 @@ public void testLimitToPreventStackOverflowFromLargeComplexSubselectTree() {
254264
e.getMessage());
255265
}
256266

267+
public void testLimitStackOverflowForInAndLiteralsIsNotApplied() {
268+
int noChildren = 100_000;
269+
LogicalPlan plan = parseStatement("SELECT * FROM t WHERE a IN(" +
270+
Joiner.on(",").join(nCopies(noChildren, "a + b")) + ")");
271+
272+
assertEquals(With.class, plan.getClass());
273+
assertEquals(Project.class, ((With) plan).child().getClass());
274+
assertEquals(Filter.class, ((Project) ((With) plan).child()).child().getClass());
275+
Filter filter = (Filter) ((Project) ((With) plan).child()).child();
276+
assertEquals(In.class, filter.condition().getClass());
277+
In in = (In) filter.condition();
278+
assertEquals("?a", in.value().toString());
279+
assertEquals(noChildren, in.list().size());
280+
assertThat(in.list().get(0).toString(), startsWith("(a) + (b)#"));
281+
}
282+
283+
public void testDecrementOfDepthCounter() {
284+
SqlParser.CircuitBreakerListener cbl = new SqlParser.CircuitBreakerListener();
285+
StatementContext sc = new StatementContext();
286+
QueryTermContext qtc = new QueryTermContext();
287+
ValueExpressionContext vec = new ValueExpressionContext();
288+
BooleanExpressionContext bec = new BooleanExpressionContext();
289+
290+
cbl.enterEveryRule(sc);
291+
cbl.enterEveryRule(sc);
292+
cbl.enterEveryRule(qtc);
293+
cbl.enterEveryRule(qtc);
294+
cbl.enterEveryRule(qtc);
295+
cbl.enterEveryRule(vec);
296+
cbl.enterEveryRule(bec);
297+
cbl.enterEveryRule(bec);
298+
299+
cbl.exitEveryRule(new StatementDefaultContext(sc));
300+
cbl.exitEveryRule(new StatementDefaultContext(sc));
301+
cbl.exitEveryRule(new QueryPrimaryDefaultContext(qtc));
302+
cbl.exitEveryRule(new QueryPrimaryDefaultContext(qtc));
303+
cbl.exitEveryRule(new ValueExpressionDefaultContext(vec));
304+
cbl.exitEveryRule(new SqlBaseParser.BooleanDefaultContext(bec));
305+
306+
assertEquals(0, cbl.depthCounts().get(SqlBaseParser.StatementContext.class.getSimpleName()));
307+
assertEquals(1, cbl.depthCounts().get(SqlBaseParser.QueryTermContext.class.getSimpleName()));
308+
assertEquals(0, cbl.depthCounts().get(SqlBaseParser.ValueExpressionContext.class.getSimpleName()));
309+
assertEquals(1, cbl.depthCounts().get(SqlBaseParser.BooleanExpressionContext.class.getSimpleName()));
310+
}
311+
257312
private LogicalPlan parseStatement(String sql) {
258313
return new SqlParser().createStatement(sql);
259314
}

0 commit comments

Comments
 (0)