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();
+    }
+  }
+}

Reply via email to