This is an automated email from the ASF dual-hosted git repository.
jackietien pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new fe0d0f35c9d Fix OOM in SQL Parser ErrorHandler due to infinite loop in
ATN traversal (#17023)
fe0d0f35c9d is described below
commit fe0d0f35c9d1501cb71dba9f127d4acbfdcb81de
Author: Jackie Tien <[email protected]>
AuthorDate: Wed Jan 14 15:21:11 2026 +0800
Fix OOM in SQL Parser ErrorHandler due to infinite loop in ATN traversal
(#17023)
* Fix OOM in SQL Parser ErrorHandler due to infinite loop in ATN traversal
- Fix infinite recursion in by tracking visited states during ATN
traversal.
- Add to verify the fix and prevent regression.
- This resolves the OOM issue caused by invalid SQL queries like .
---
.gitignore | 3 +
.../plan/relational/sql/parser/ErrorHandler.java | 31 ++++++---
.../sql/ast/SqlParserErrorHandlerTest.java | 76 ++++++++++++++++++++++
3 files changed, 101 insertions(+), 9 deletions(-)
diff --git a/.gitignore b/.gitignore
index a489dd048da..2c19b1b3a2c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -124,3 +124,6 @@
iotdb-core/tsfile/src/main/antlr4/org/apache/tsfile/parser/gen/
.mvn/.gradle-enterprise/
.mvn/.develocity/
.run/
+
+# Relational Grammar ANTLR
+iotdb-core/relational-grammar/src/main/antlr4/org/apache/iotdb/db/relational/grammar/sql/.antlr/
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/ErrorHandler.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/ErrorHandler.java
index f2e653eec30..e663ac5bcf3 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/ErrorHandler.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/ErrorHandler.java
@@ -84,7 +84,8 @@ public class ErrorHandler extends BaseErrorListener {
Result result = analyze(e, parser);
- // pick the candidate tokens associated largest token index processed
(i.e., the path that
+ // pick the candidate tokens associated largest token index processed
(i.e., the
+ // path that
// consumed the most input)
String expected =
result.getExpected().stream().sorted().collect(Collectors.joining(", "));
@@ -209,10 +210,13 @@ public class ErrorHandler extends BaseErrorListener {
RuleStartState startState = atn.ruleToStartState[currentState.ruleIndex];
if (isReachable(currentState, startState)) {
- // We've been dropped inside a rule in a state that's reachable via
epsilon transitions.
+ // We've been dropped inside a rule in a state that's reachable via
epsilon
+ // transitions.
// This is,
- // effectively, equivalent to starting at the beginning (or
immediately outside) the rule.
- // In that case, backtrack to the beginning to be able to take
advantage of logic that
+ // effectively, equivalent to starting at the beginning (or
immediately outside)
+ // the rule.
+ // In that case, backtrack to the beginning to be able to take
advantage of
+ // logic that
// remaps
// some rules to well-known names for reporting purposes
currentState = startState;
@@ -238,6 +242,8 @@ public class ErrorHandler extends BaseErrorListener {
private boolean isReachable(ATNState target, RuleStartState from) {
Deque<ATNState> activeStates = new ArrayDeque<>();
activeStates.add(from);
+ Set<Integer> visited = new HashSet<>();
+ visited.add(from.stateNumber);
while (!activeStates.isEmpty()) {
ATNState current = activeStates.pop();
@@ -250,7 +256,10 @@ public class ErrorHandler extends BaseErrorListener {
Transition transition = current.transition(i);
if (transition.isEpsilon()) {
- activeStates.push(transition.target);
+ if (!visited.contains(transition.target.stateNumber)) {
+ activeStates.push(transition.target);
+ visited.add(transition.target.stateNumber);
+ }
}
}
}
@@ -336,13 +345,17 @@ public class ErrorHandler extends BaseErrorListener {
labels.complement(IntervalSet.of(Token.MIN_USER_TOKEN_TYPE,
atn.maxTokenType));
}
- // Surprisingly, TokenStream (i.e. BufferedTokenStream) may not
have loaded all the
+ // Surprisingly, TokenStream (i.e. BufferedTokenStream) may not
have loaded all
+ // the
// tokens from the
- // underlying stream. TokenStream.get() does not force tokens to
be buffered -- it just
+ // underlying stream. TokenStream.get() does not force tokens to
be buffered --
+ // it just
// returns what's
- // in the current buffer, or fail with an IndexOutOfBoundsError.
Since Antlr decided the
+ // in the current buffer, or fail with an IndexOutOfBoundsError.
Since Antlr
+ // decided the
// error occurred
- // within the current set of buffered tokens, stop when we reach
the end of the buffer.
+ // within the current set of buffered tokens, stop when we reach
the end of the
+ // buffer.
if (labels.contains(currentToken) && tokenIndex < stream.size() -
1) {
activeStates.push(new ParsingState(transition.target, tokenIndex
+ 1, false, parser));
} else {
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/SqlParserErrorHandlerTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/SqlParserErrorHandlerTest.java
new file mode 100644
index 00000000000..f17efb29616
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/SqlParserErrorHandlerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.queryengine.plan.relational.sql.ast;
+
+import
org.apache.iotdb.db.queryengine.plan.relational.sql.parser.ParsingException;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.parser.SqlParser;
+
+import org.junit.Test;
+
+import java.time.ZoneId;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class SqlParserErrorHandlerTest {
+
+ @Test
+ public void testParseLimitOffsetError() {
+ String sql = "select * from information_schema.queries offset 48 limit1";
+ SqlParser sqlParser = new SqlParser();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future<?> future =
+ executor.submit(
+ () -> {
+ try {
+ sqlParser.createStatement(sql, ZoneId.systemDefault(), null);
+ fail("Expected ParsingException to be thrown");
+ } catch (ParsingException e) {
+ assertTrue(e.getMessage().contains("mismatched input
'limit1'"));
+ }
+ });
+
+ try {
+ // The parsing should fail quickly. If it hangs (OOM), this timeout will
+ // trigger.
+ future.get(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ fail("Interrupted");
+ } catch (ExecutionException e) {
+ // If parsing exception propagates here (which it shouldn't as it's
caught in
+ // the task), handle it
+ if (e.getCause() instanceof ParsingException) {
+ assertTrue(e.getCause().getMessage().contains("mismatched input
'limit1'"));
+ } else {
+ fail("Unexpected exception: " + e.getCause());
+ }
+ } catch (TimeoutException e) {
+ fail("Parsing timed out - potential endless loop/OOM detected");
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+}