This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 9c5e22f35 Differential Fuzzing (1/3): Check `SQLiteDialect` against 
SQLite's own parser (#2562)
9c5e22f35 is described below

commit 9c5e22f359024ff7e729429cf8e898394a29c2d9
Author: Luca Cappelletti <[email protected]>
AuthorDate: Wed Sep 23 21:05:08 2026 +0000

    Differential Fuzzing (1/3): Check `SQLiteDialect` against SQLite's own 
parser (#2562)
---
 docs/fuzzing.md                          |   2 +
 fuzz/Cargo.toml                          |   8 ++
 fuzz/fuzz_targets/fuzz_sqlite_accepts.rs | 149 +++++++++++++++++++++++++++++++
 3 files changed, 159 insertions(+)

diff --git a/docs/fuzzing.md b/docs/fuzzing.md
index f0512690d..1714e06cd 100644
--- a/docs/fuzzing.md
+++ b/docs/fuzzing.md
@@ -31,6 +31,8 @@ There are two targets. `fuzz_parse_sql` parses the input with 
every dialect.
 `fuzz_parse_roundtrip` additionally re-parses the SQL rendered by `Display` 
and fails when a
 rendered statement no longer parses.
 
+`fuzz_sqlite_accepts` compiles the input with a bundled SQLite, without 
running it, and fails when SQLite accepts SQL that `SQLiteDialect` rejects. 
Name resolution errors such as a missing table still count as accepted once 
SQLite has read the statement to its end.
+
 ClusterFuzzLite runs continuous fuzzing. Every pull request fuzzes for 10 
minutes in
 `code-change` mode, a daily batch job grows the shared corpus stored on the
 `clusterfuzzlite` branch, and a daily prune compacts it.
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index ed69281a1..4242bf8ce 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -26,6 +26,7 @@ cargo-fuzz = true
 
 [dependencies]
 libfuzzer-sys = "0.4"
+libsqlite3-sys = { version = "0.38", features = ["bundled"] }
 sqlparser = { path = ".." }
 
 # Prevent this from interfering with workspaces
@@ -45,3 +46,10 @@ path = "fuzz_targets/fuzz_parse_roundtrip.rs"
 test = false
 doc = false
 bench = false
+
+[[bin]]
+name = "fuzz_sqlite_accepts"
+path = "fuzz_targets/fuzz_sqlite_accepts.rs"
+test = false
+doc = false
+bench = false
diff --git a/fuzz/fuzz_targets/fuzz_sqlite_accepts.rs 
b/fuzz/fuzz_targets/fuzz_sqlite_accepts.rs
new file mode 100644
index 000000000..7d45b685e
--- /dev/null
+++ b/fuzz/fuzz_targets/fuzz_sqlite_accepts.rs
@@ -0,0 +1,149 @@
+// 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.
+
+#![no_main]
+
+use libfuzzer_sys::fuzz_target;
+use libsqlite3_sys as ffi;
+use sqlparser::dialect::SQLiteDialect;
+use sqlparser::parser::{Parser, ParserError};
+use std::ffi::{c_int, CStr};
+use std::ptr;
+
+/// Errors SQLite raises while resolving names, after the grammar accepted the 
statement.
+const RESOLUTION_ERRORS: &[&[u8]] = &[
+    b"no such table: ",
+    b"no such column: ",
+    b"no such function: ",
+    b"no such index: ",
+    b"no such collation sequence: ",
+    b"no such module: ",
+    b"no such window: ",
+    b"unknown database ",
+    b"wrong number of arguments to function ",
+    b"ambiguous column name: ",
+    b"misuse of aggregate",
+    b"misuse of window function ",
+    b"no tables specified",
+];
+
+struct Db(*mut ffi::sqlite3);
+
+impl Db {
+    fn open() -> Self {
+        let mut db = ptr::null_mut();
+        // SAFETY: the filename is NUL terminated and `db` is a valid out 
pointer.
+        let rc = unsafe {
+            ffi::sqlite3_open_v2(
+                c":memory:".as_ptr(),
+                &mut db,
+                ffi::SQLITE_OPEN_READWRITE | ffi::SQLITE_OPEN_CREATE,
+                ptr::null(),
+            )
+        };
+        assert_eq!(
+            rc,
+            ffi::SQLITE_OK,
+            "cannot open an in-memory SQLite database"
+        );
+        Self(db)
+    }
+
+    /// Compiles, without running, the first statement of `sql` and returns 
SQLite's error message.
+    fn prepare_error(&self, sql: &[u8]) -> Option<Vec<u8>> {
+        let len = c_int::try_from(sql.len()).expect("fuzz input fits a c_int");
+        let mut stmt = ptr::null_mut();
+        // SAFETY: SQLite reads at most `len` bytes of `sql`, and `stmt` is 
finalized before return.
+        let rc = unsafe {
+            let rc = ffi::sqlite3_prepare_v2(
+                self.0,
+                sql.as_ptr().cast(),
+                len,
+                &mut stmt,
+                ptr::null_mut(),
+            );
+            ffi::sqlite3_finalize(stmt);
+            rc
+        };
+        if rc == ffi::SQLITE_OK {
+            return None;
+        }
+        // SAFETY: sqlite3_errmsg never returns NULL, and the message stays 
valid until the next call on this handle.
+        let message = unsafe { CStr::from_ptr(ffi::sqlite3_errmsg(self.0)) };
+        Some(message.to_bytes().to_vec())
+    }
+
+    fn accepts(&self, statement: &[u8]) -> bool {
+        let Some(error) = self.prepare_error(statement) else {
+            return true;
+        };
+        if !RESOLUTION_ERRORS.iter().any(|e| error.starts_with(e)) {
+            return false;
+        }
+        // An error raised mid-statement leaves the tail unparsed and survives 
a trailing syntax error.
+        let mut probe = 
statement.strip_suffix(b";").unwrap_or(statement).to_vec();
+        probe.extend_from_slice(b"\n)");
+        self.prepare_error(&probe).as_deref() == Some(br#"near ")": syntax 
error"#)
+    }
+}
+
+impl Drop for Db {
+    fn drop(&mut self) {
+        // SAFETY: every statement is finalized, so the handle closes.
+        unsafe { ffi::sqlite3_close(self.0) };
+    }
+}
+
+/// Splits `sql` into statements the way the SQLite shell does, each keeping 
its `;`.
+fn statements(sql: &str) -> Vec<&str> {
+    let mut statements = Vec::new();
+    let mut prefix = Vec::with_capacity(sql.len() + 1);
+    let mut start = 0;
+    for (end, _) in sql.match_indices(';') {
+        prefix.clear();
+        prefix.extend_from_slice(&sql.as_bytes()[start..=end]);
+        prefix.push(0);
+        // SAFETY: `prefix` ends with its only NUL because the caller rejects 
inputs holding one.
+        if unsafe { ffi::sqlite3_complete(prefix.as_ptr().cast()) } != 0 {
+            statements.push(&sql[start..=end]);
+            start = end + 1;
+        }
+    }
+    statements.push(&sql[start..]);
+    statements
+}
+
+fuzz_target!(|sql: &str| {
+    // SQLite stops reading at a NUL, sqlparser does not.
+    if sql.contains('\0') {
+        return;
+    }
+    let db = Db::open();
+    if !statements(sql).iter().all(|s| db.accepts(s.as_bytes())) {
+        return;
+    }
+    match Parser::parse_sql(&SQLiteDialect {}, sql) {
+        Ok(_) | Err(ParserError::RecursionLimitExceeded) => {}
+        Err(err) => {
+            // SAFETY: sqlite3_libversion returns a static NUL terminated 
string.
+            let version = unsafe { CStr::from_ptr(ffi::sqlite3_libversion()) };
+            panic!(
+                "SQLite {version:?} accepts SQL the SQLite dialect rejects\n  
sql: {sql:?}\n  error: {err}"
+            )
+        }
+    }
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to