This commit is contained in:
Levy A.
2025-03-19 17:10:23 -03:00
parent 269857d66c
commit 0013c93fa5
3 changed files with 147 additions and 57 deletions

15
fuzz/Cargo.lock generated
View File

@@ -614,6 +614,7 @@ version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
@@ -642,7 +643,7 @@ dependencies = [
[[package]]
name = "limbo_core"
version = "0.0.16"
version = "0.0.17"
dependencies = [
"built",
"cfg_block",
@@ -679,14 +680,16 @@ dependencies = [
[[package]]
name = "limbo_ext"
version = "0.0.16"
version = "0.0.17"
dependencies = [
"chrono",
"getrandom 0.3.1",
"limbo_macros",
]
[[package]]
name = "limbo_macros"
version = "0.0.16"
version = "0.0.17"
dependencies = [
"proc-macro2",
"quote",
@@ -695,7 +698,7 @@ dependencies = [
[[package]]
name = "limbo_sqlite3_parser"
version = "0.0.16"
version = "0.0.17"
dependencies = [
"bitflags",
"cc",
@@ -714,7 +717,7 @@ dependencies = [
[[package]]
name = "limbo_time"
version = "0.0.16"
version = "0.0.17"
dependencies = [
"chrono",
"limbo_ext",
@@ -726,7 +729,7 @@ dependencies = [
[[package]]
name = "limbo_uuid"
version = "0.0.16"
version = "0.0.17"
dependencies = [
"limbo_ext",
"mimalloc",

View File

@@ -1,7 +1,7 @@
[package]
name = "limbo-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
authors = ["the Limbo authors"]
publish = false
edition = "2021"
@@ -12,7 +12,7 @@ cargo-fuzz = true
libfuzzer-sys = "0.4"
arbitrary = { version = "1.4.1", features = ["derive"] }
limbo_core = { path = "../core" }
rusqlite = "0.34.0"
rusqlite = { version = "0.34.0", features = ["bundled"] }
# Prevent this from interfering with workspaces
[workspace]

View File

@@ -1,31 +1,67 @@
#![no_main]
use core::fmt;
use std::{num::NonZero, sync::Arc};
use std::{error::Error, num::NonZero, sync::Arc};
use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use libfuzzer_sys::{fuzz_target, Corpus};
use limbo_core::{OwnedValue, IO as _};
#[derive(Debug, Arbitrary)]
enum Binary {
Equal,
NotEqual,
macro_rules! str_enum {
($vis:vis enum $name:ident { $($variant:ident => $value:literal),*, }) => {
#[derive(PartialEq, Debug, Copy, Clone, Arbitrary)]
$vis enum $name {
$($variant),*
}
impl $name {
pub fn to_str(self) -> &'static str {
match self {
$($name::$variant => $value),*
}
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_str())
}
}
};
}
impl fmt::Display for Binary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Binary::Equal => "=",
Binary::NotEqual => "<>",
}
)
str_enum! {
enum Binary {
Equal => "=",
Is => "IS",
NotEqual => "<>",
GreaterThan => ">",
GreaterThanOrEqual => ">=",
LessThan => "<",
LessThanOrEqual => "<=",
RightShift => ">>",
LeftShift => "<<",
BitwiseAnd => "&",
BitwiseOr => "|",
And => "AND",
Or => "OR",
Add => "+",
Subtract => "-",
Multiply => "*",
Divide => "/",
Mod => "%",
Concat => "||",
}
}
#[derive(Debug, Arbitrary, Clone)]
str_enum! {
enum Unary {
Not => "~",
Negative => "-",
Positive => "+",
}
}
#[derive(Arbitrary, Debug, Clone)]
enum Value {
Null,
Integer(i64),
@@ -39,7 +75,13 @@ impl From<Value> for limbo_core::OwnedValue {
match value {
Value::Null => limbo_core::OwnedValue::Null,
Value::Integer(v) => limbo_core::OwnedValue::Integer(v),
Value::Real(v) => limbo_core::OwnedValue::Float(v),
Value::Real(v) => {
if v.is_nan() {
limbo_core::OwnedValue::Null
} else {
limbo_core::OwnedValue::Float(v)
}
}
Value::Text(v) => limbo_core::OwnedValue::from_text(&v),
Value::Blob(v) => limbo_core::OwnedValue::from_blob(v.to_owned()),
}
@@ -78,48 +120,82 @@ impl rusqlite::types::FromSql for Value {
enum Expr {
Value(Value),
Binary(Binary, Box<Expr>, Box<Expr>),
Unary(Unary, Box<Expr>),
}
fn to_sql(expr: &Expr) -> (String, Vec<Value>) {
match expr {
Expr::Value(value) => ("?".to_string(), vec![value.clone()]),
Expr::Binary(op, lhs, rhs) => {
let mut lhs = to_sql(lhs);
let mut rhs = to_sql(rhs);
lhs.1.append(&mut rhs.1);
(format!("({}) {op} ({})", lhs.0, rhs.0), lhs.1)
#[derive(Debug)]
struct Output {
query: String,
parameters: Vec<Value>,
depth: usize,
}
impl Expr {
pub fn lower(&self) -> Output {
match self {
Expr::Value(value) => Output {
query: "?".to_string(),
parameters: vec![value.clone()],
depth: 0,
},
Expr::Unary(op, expr) => {
let expr = expr.lower();
Output {
query: format!("{op} ({})", expr.query),
parameters: expr.parameters,
depth: expr.depth + 1,
}
}
Expr::Binary(op, lhs, rhs) => {
let mut lhs = lhs.lower();
let mut rhs = rhs.lower();
Output {
query: format!("({}) {op} ({})", lhs.query, rhs.query),
parameters: {
lhs.parameters.append(&mut rhs.parameters);
lhs.parameters
},
depth: lhs.depth.max(rhs.depth) + 1,
}
}
}
}
}
fn do_fuzz(expr: Expr) {
let (sql, params) = to_sql(&expr);
let sql = format!("select {}", &sql);
fn do_fuzz(expr: Expr) -> Result<Corpus, Box<dyn Error>> {
let expr = expr.lower();
let sql = format!("SELECT {}", expr.query);
let sqlite = rusqlite::Connection::open_in_memory().unwrap();
let io = Arc::new(limbo_core::MemoryIO::new());
let memory = limbo_core::Database::open_file(io.clone(), ":memory:", true).unwrap();
let limbo = memory.connect().unwrap();
let expected = sqlite
.query_row(&sql, rusqlite::params_from_iter(params.iter()), |row| {
row.get::<_, Value>(0)
})
.unwrap();
let mut stmt = limbo.prepare(sql).unwrap();
for (idx, value) in params.into_iter().enumerate() {
stmt.bind_at(NonZero::new(idx + 1).unwrap(), value.into())
// FIX: `limbo_core::translate::expr::translate_expr` causes a overflow if this is any higher.
if expr.depth > 153 {
return Ok(Corpus::Reject);
}
let value = 'value: {
let expected = {
let conn = rusqlite::Connection::open_in_memory()?;
conn.query_row(
&sql,
rusqlite::params_from_iter(expr.parameters.iter()),
|row| row.get::<_, Value>(0),
)?
};
let found = 'value: {
let io = Arc::new(limbo_core::MemoryIO::new());
let db = limbo_core::Database::open_file(io.clone(), ":memory:", true)?;
let conn = db.connect()?;
let mut stmt = conn.prepare(sql)?;
for (idx, value) in expr.parameters.iter().enumerate() {
stmt.bind_at(NonZero::new(idx + 1).unwrap(), value.clone().into())
}
loop {
use limbo_core::StepResult;
match stmt.step().unwrap() {
StepResult::IO => io.run_once().unwrap(),
match stmt.step()? {
StepResult::IO => io.run_once()?,
StepResult::Row => {
let row = stmt.row().unwrap();
assert_eq!(row.count(), 1);
assert_eq!(row.count(), 1, "expr: {:?}", expr);
break 'value row.get_value(0).clone();
}
_ => unreachable!(),
@@ -127,7 +203,18 @@ fn do_fuzz(expr: Expr) {
}
};
assert_eq!(OwnedValue::from(expected), value);
assert_eq!(
OwnedValue::from(expected.clone()),
found.clone(),
"with expression {:?} {}",
expr,
match (expected, found) {
(Value::Real(a), OwnedValue::Float(b)) => format!("float diff: {:?}", (a - b).abs()),
_ => "".to_string(),
}
);
Ok(Corpus::Keep)
}
fuzz_target!(|expr: Expr| do_fuzz(expr));
fuzz_target!(|expr: Expr| -> Corpus { do_fuzz(expr).unwrap_or(Corpus::Keep) });