This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-2562-2d577237d7aaebcf22c41f84b4d10d8b442f1949 in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
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]
