-
Notifications
You must be signed in to change notification settings - Fork 24.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SQL: Fix wrong appliance of StackOverflow limit for IN #36724
Changes from 2 commits
3b4053d
de576de
450cc18
3ef248d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,14 @@ | |
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
import org.elasticsearch.xpack.sql.expression.Expression; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BooleanDefaultContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BooleanExpressionContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryPrimaryDefaultContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryTermContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementDefaultContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionDefaultContext; | ||
import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan; | ||
import org.elasticsearch.xpack.sql.proto.SqlTypedParamValue; | ||
|
||
|
@@ -214,10 +222,26 @@ public void exitNonReserved(SqlBaseParser.NonReservedContext context) { | |
/** | ||
* Used to catch large expressions that can lead to stack overflows | ||
*/ | ||
private class CircuitBreakerListener extends SqlBaseBaseListener { | ||
static class CircuitBreakerListener extends SqlBaseBaseListener { | ||
|
||
private static final short MAX_RULE_DEPTH = 200; | ||
|
||
/** | ||
* Due to the structure of the grammar and our custom handling in {@link ExpressionBuilder} | ||
* some expressions can exit with a different class than they entered: | ||
* e.g.: ValueExpressionContext can exit as ValueExpressionDefaultContext | ||
*/ | ||
private static final Map<String, String> ENTER_EXIT_RULE_MAPPING = new HashMap<>(); | ||
|
||
static { | ||
ENTER_EXIT_RULE_MAPPING.put(StatementDefaultContext.class.getSimpleName(), StatementContext.class.getSimpleName()); | ||
ENTER_EXIT_RULE_MAPPING.put(QueryPrimaryDefaultContext.class.getSimpleName(), QueryTermContext.class.getSimpleName()); | ||
ENTER_EXIT_RULE_MAPPING.put(BooleanDefaultContext.class.getSimpleName(), BooleanExpressionContext.class.getSimpleName()); | ||
ENTER_EXIT_RULE_MAPPING.put(ValueExpressionDefaultContext.class.getSimpleName(), ValueExpressionContext.class.getSimpleName()); | ||
} | ||
|
||
private boolean insideIn = false; | ||
|
||
// Keep current depth for every rule visited. | ||
// The totalDepth alone cannot be used as expressions like: e1 OR e2 OR e3 OR ... | ||
// are processed as e1 OR (e2 OR (e3 OR (... and this results in the totalDepth not growing | ||
|
@@ -226,9 +250,18 @@ private class CircuitBreakerListener extends SqlBaseBaseListener { | |
|
||
@Override | ||
public void enterEveryRule(ParserRuleContext ctx) { | ||
if (inDetected(ctx)) { | ||
insideIn = true; | ||
} | ||
|
||
// Skip PrimaryExpressionContext for IN as it's not visited on exit due to | ||
// the grammar's peculiarity rule with "predicated" and "predicate". | ||
// Also skip the Identifiers as they are "cheap". | ||
if (ctx.getClass() != SqlBaseParser.UnquoteIdentifierContext.class && | ||
ctx.getClass() != SqlBaseParser.QuoteIdentifierContext.class && | ||
ctx.getClass() != SqlBaseParser.BackQuotedIdentifierContext.class) { | ||
ctx.getClass() != SqlBaseParser.BackQuotedIdentifierContext.class && | ||
(insideIn == false || ctx.getClass() != SqlBaseParser.PrimaryExpressionContext.class)) { | ||
|
||
int currentDepth = depthCounts.putOrAdd(ctx.getClass().getSimpleName(), (short) 1, (short) 1); | ||
if (currentDepth > MAX_RULE_DEPTH) { | ||
throw new ParsingException(source(ctx), "SQL statement too large; " + | ||
|
@@ -240,12 +273,35 @@ public void enterEveryRule(ParserRuleContext ctx) { | |
|
||
@Override | ||
public void exitEveryRule(ParserRuleContext ctx) { | ||
// Avoid having negative numbers | ||
if (depthCounts.containsKey(ctx.getClass().getSimpleName())) { | ||
depthCounts.putOrAdd(ctx.getClass().getSimpleName(), (short) 0, (short) -1); | ||
if (inDetected(ctx)) { | ||
insideIn = false; | ||
} | ||
|
||
decrementCounter(ctx); | ||
super.exitEveryRule(ctx); | ||
} | ||
|
||
ObjectShortHashMap<String> depthCounts() { | ||
return depthCounts; | ||
} | ||
|
||
private void decrementCounter(ParserRuleContext ctx) { | ||
String className = ctx.getClass().getSimpleName(); | ||
String classNameToDecrement = ENTER_EXIT_RULE_MAPPING.getOrDefault(className, className); | ||
|
||
// Avoid having negative numbers | ||
if (depthCounts.containsKey(classNameToDecrement)) { | ||
depthCounts.putOrAdd(classNameToDecrement, (short) 0, (short) -1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the scenario of negative counter even possible? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, because currently some rules are not encountered on enter but only on exit. |
||
} | ||
} | ||
|
||
private boolean inDetected(ParserRuleContext ctx) { | ||
if (ctx.getParent() != null && ctx.getParent().getClass() == SqlBaseParser.PredicateContext.class) { | ||
SqlBaseParser.PredicateContext pc = (SqlBaseParser.PredicateContext) ctx.getParent(); | ||
return pc.kind != null && pc.kind.getType() == SqlBaseParser.IN; | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
private static final BaseErrorListener ERROR_LISTENER = new BaseErrorListener() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,13 @@ | |
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MatchQueryPredicate; | ||
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.MultiMatchQueryPredicate; | ||
import org.elasticsearch.xpack.sql.expression.predicate.fulltext.StringQueryPredicate; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.BooleanExpressionContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryPrimaryDefaultContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.QueryTermContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.StatementDefaultContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionContext; | ||
import org.elasticsearch.xpack.sql.parser.SqlBaseParser.ValueExpressionDefaultContext; | ||
import org.elasticsearch.xpack.sql.plan.logical.Filter; | ||
import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan; | ||
import org.elasticsearch.xpack.sql.plan.logical.OrderBy; | ||
|
@@ -254,6 +261,40 @@ public void testLimitToPreventStackOverflowFromLargeComplexSubselectTree() { | |
e.getMessage()); | ||
} | ||
|
||
public void testLimitStackOverflowForInAndLiteralsIsNotApplied() { | ||
new SqlParser().createStatement("SELECT * FROM t WHERE a IN(" + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only the parser being invoked is enough here? Can you check that at least that a minimal plan is created correctly given the query? Also, there is already a method that creates the statement given a query String: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test is only to check that we don't hit the limit, but I can add more assertions. |
||
Joiner.on(",").join(nCopies(100_000, "a + b")) + ")"); | ||
} | ||
|
||
public void testDecrementOfDepthCounter() { | ||
SqlParser.CircuitBreakerListener cbl = new SqlParser.CircuitBreakerListener(); | ||
StatementContext sc = new StatementContext(); | ||
QueryTermContext qtc = new QueryTermContext(); | ||
ValueExpressionContext vec = new ValueExpressionContext(); | ||
BooleanExpressionContext bec = new BooleanExpressionContext(); | ||
|
||
cbl.enterEveryRule(sc); | ||
cbl.enterEveryRule(sc); | ||
cbl.enterEveryRule(qtc); | ||
cbl.enterEveryRule(qtc); | ||
cbl.enterEveryRule(qtc); | ||
cbl.enterEveryRule(vec); | ||
cbl.enterEveryRule(bec); | ||
cbl.enterEveryRule(bec); | ||
|
||
cbl.exitEveryRule(new StatementDefaultContext(sc)); | ||
cbl.exitEveryRule(new StatementDefaultContext(sc)); | ||
cbl.exitEveryRule(new QueryPrimaryDefaultContext(qtc)); | ||
cbl.exitEveryRule(new QueryPrimaryDefaultContext(qtc)); | ||
cbl.exitEveryRule(new ValueExpressionDefaultContext(vec)); | ||
cbl.exitEveryRule(new SqlBaseParser.BooleanDefaultContext(bec)); | ||
|
||
assertEquals(0, cbl.depthCounts().get(SqlBaseParser.StatementContext.class.getSimpleName())); | ||
assertEquals(1, cbl.depthCounts().get(SqlBaseParser.QueryTermContext.class.getSimpleName())); | ||
assertEquals(0, cbl.depthCounts().get(SqlBaseParser.ValueExpressionContext.class.getSimpleName())); | ||
assertEquals(1, cbl.depthCounts().get(SqlBaseParser.BooleanExpressionContext.class.getSimpleName())); | ||
} | ||
|
||
private LogicalPlan parseStatement(String sql) { | ||
return new SqlParser().createStatement(sql); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you
import static
all these classes to make the code a bit less crowded?