From 0a415d260e41cea003b7523929c625fe249feb4c Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 17 Oct 2026 22:18:09 +0200 Subject: [PATCH] Remove the shared_cache fuzz target --- README.md | 2 +- fuzz/Cargo.toml | 9 +- .../shared_cache/all-siblings-correct-key | Bin 64 -> 0 bytes fuzz/corpus/shared_cache/many-siblings | Bin 656 -> 0 bytes .../shared_cache/mixed-correct-and-wrong | Bin 323 -> 0 bytes fuzz/corpus/shared_cache/no-siblings | 2 - .../shared_cache/one-sibling-correct-key | Bin 212 -> 0 bytes .../corpus/shared_cache/one-sibling-wrong-key | Bin 1581 -> 0 bytes fuzz/corpus/shared_cache/sibling-has-ops | Bin 804 -> 0 bytes fuzz/examples/gen_shared_cache_seeds.rs | 81 -------- fuzz/fuzz_targets/shared_cache.rs | 5 - fuzz/src/db.rs | 7 - fuzz/src/ffi.rs | 1 - fuzz/src/input.rs | 45 ----- fuzz/src/lib.rs | 7 +- fuzz/src/run.rs | 179 ++++-------------- fuzz/tests/replay.rs | 27 +-- 17 files changed, 50 insertions(+), 315 deletions(-) delete mode 100644 fuzz/corpus/shared_cache/all-siblings-correct-key delete mode 100644 fuzz/corpus/shared_cache/many-siblings delete mode 100644 fuzz/corpus/shared_cache/mixed-correct-and-wrong delete mode 100644 fuzz/corpus/shared_cache/no-siblings delete mode 100644 fuzz/corpus/shared_cache/one-sibling-correct-key delete mode 100644 fuzz/corpus/shared_cache/one-sibling-wrong-key delete mode 100644 fuzz/corpus/shared_cache/sibling-has-ops delete mode 100644 fuzz/examples/gen_shared_cache_seeds.rs delete mode 100755 fuzz/fuzz_targets/shared_cache.rs diff --git a/README.md b/README.md index 8e53be7..d9a792c 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ SQLite3MC is MIT licensed. The amalgamation also carries public-domain code (SQL A daily workflow in the [repository](https://github.com/LucaCappelletti94/sqlite3mc-src) opens a pull request for each new SQLite3MC release, taking the archive's checksum only from the release's Sigstore-signed `SHA256SUMS`. CI re-runs `upgrade.sh`, which checks that signature again before trusting the pinned checksum, to prove the vendored bytes match the pinned release. It also compiles the packaged sources natively and through `sqlite-wasm-rs`, and checks that each build opens the other's files in every cipher, under Node and in headless Chrome and Firefox, on OPFS too. -CI also runs SQLite3MC's own tests, a rekey of every cipher and SQLite's TCL suite through the shipped amalgamation, once plainly and once under the address and undefined-behaviour sanitizers, with every expected failure named and explained. Six fuzz targets in `fuzz/` run on every pull request and daily through `ClusterFuzzLite`: a self-consistency check across the six native cipher schemes, and dedicated targets for rekeying, `journal_mode=WAL`, `ATTACH DATABASE`, the `sqlite3_backup_*` API, and shared-cache mode. +CI also runs SQLite3MC's own tests, a rekey of every cipher and SQLite's TCL suite through the shipped amalgamation, once plainly and once under the address and undefined-behaviour sanitizers, with every expected failure named and explained. Five fuzz targets in `fuzz/` run on every pull request and daily through `ClusterFuzzLite`: a self-consistency check across the six native cipher schemes, and dedicated targets for rekeying, `journal_mode=WAL`, `ATTACH DATABASE` and the `sqlite3_backup_*` API. Shared-cache mode is left out, since SQLite discourages it and SQLite3MC supports it only as legacy ([#290](https://github.com/utelle/SQLite3MultipleCiphers/issues/290)). diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 6fdd5f7..f1ebe76 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -3,7 +3,7 @@ name = "sqlite3mc-src-fuzz" version = "0.0.0" edition = "2021" publish = false -description = "Fuzzing SQLite3MC's own cipher schemes for self-consistency, its rekey support, its WAL journal mode, its ATTACH DATABASE support, its backup API, and its shared-cache mode" +description = "Fuzzing SQLite3MC's own cipher schemes for self-consistency, its rekey support, its WAL journal mode, its ATTACH DATABASE support, and its backup API" [package.metadata] cargo-fuzz = true @@ -51,13 +51,6 @@ test = false doc = false bench = false -[[bin]] -name = "shared_cache" -path = "fuzz_targets/shared_cache.rs" -test = false -doc = false -bench = false - # The crate's table, minus unsafe_code (FFI) and missing_docs and cargo (never published). [lints.rust] rust_2018_idioms = { level = "warn", priority = -1 } diff --git a/fuzz/corpus/shared_cache/all-siblings-correct-key b/fuzz/corpus/shared_cache/all-siblings-correct-key deleted file mode 100644 index 471ffcdb24d002f2d4160ab57f382479768391b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 zcmV-G0Kfmjbq3(i_rezJ;O_jzC`J!aPjB?yWbrISU56TMiU}5qBM=-jO-Y9(;JR6B WW-JC6RsoLV5l5xjM_lIM$9Mqw799=% diff --git a/fuzz/corpus/shared_cache/many-siblings b/fuzz/corpus/shared_cache/many-siblings deleted file mode 100644 index 510ffd4daca05cb5061a813795250f31661c65d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 656 zcmV;B0&o4*sd~WA<$&}V1E5s;3%JxHmtAc5OiOQx->(2Nj6e|aUS+%p+0h8RToWpg`dLknc6U7?j72hCZ*^{0b0& zBCD&BfMWB*>kvZ6hYTp~G+2a5vq+|ser46~)p65{mXTY1+8@X}68RM9{N0L|52*Y= z_o)sK`{{=u{yB?>GS@bhYur9lKl#68q4Z_m0|Q3$&lXOAuB+&aFI%^UBkf&cV?10G zHw%uCRw(X1BgYTx(xHaN5U9@tDNjZ_@<0f?25)|;+6otNhk_*X06|N%5~;t}{vg15 zCHLVD+pST;gdDB1o_s^gUWl^bFLD#ru`0n%yz}uEdnB-DoMXi~d_;b+-yh>?#YilaUG)L<2bF zx_S-vLY8!+|@2zS;kT9JoD+9Wg)){}{CO q`{ws*sAn9U9+Ruk$qz`VYm_PKL}p_j7d2mkCT|U=H17!!fyzL1T|c(~ diff --git a/fuzz/corpus/shared_cache/mixed-correct-and-wrong b/fuzz/corpus/shared_cache/mixed-correct-and-wrong deleted file mode 100644 index c4d9adb2a14ebf0cf1379a71cd6df8c40691afda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 323 zcmV-J0lfZi!Zq6R&2v8t2Sz87;x@siZZYmYNn9A}y5bC+AF{u)wwJqZga@0q`0B_8 zv5AMOGp!JX$VU0{mFy+jzW!KnbxddzK{+9C-yR{_f24&3ACPlni?zO=aJz5C;nKxC z_E2AZ$MDYJ{h9#(MWOMA1ugOfj4z|lTuCaPjJnrIDhG0x*`+geCt<8ixPwmJ(^z85^?%t zi1DF6eT+rZ03%-o7rr8!vDUK}C1gGGWzz+!QDyxDv(Bx~aBCWGrkt-!^&NQI$z*v* Vx$fXDlm;miq49HlR>2=|PPwcBr!igXH7Jr(O5(=uvRxmQ!Xa81szm+kKzEu{fCg2MGDux z{Rz0bu42@?4IADg&TeWEk>*7khKrmBP8B?gr75miRf&bk*j%}VG_M=nrleS_B0b~XM diff --git a/fuzz/corpus/shared_cache/one-sibling-wrong-key b/fuzz/corpus/shared_cache/one-sibling-wrong-key deleted file mode 100644 index 42266977aebd234fb24f9b2f4401121e26e53dc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1581 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 diff --git a/fuzz/examples/gen_shared_cache_seeds.rs b/fuzz/examples/gen_shared_cache_seeds.rs deleted file mode 100644 index ec9046a..0000000 --- a/fuzz/examples/gen_shared_cache_seeds.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Builds `corpus/shared_cache` the same way `gen_backup_seeds` builds `corpus/backup`: rejection -//! sampling through the real `Arbitrary` impl, keeping the first *clean* draw that lands on each -//! target parameter family. A draw that panics is never committed to `corpus/shared_cache` (it -//! would permanently fail the replay test); the first one found per family is saved to -//! `corpus/findings/` instead, which nothing replays, and the search continues past it for a -//! clean representative. - -use std::path::Path; - -use sqlite3mc_src_fuzz::{ - run_shared_cache, - seedgen::{run_search, DivergencePolicy, Family, Rng}, - SharedCacheCase, SiblingKey, -}; - -const fn has_content(case: &SharedCacheCase) -> bool { - !case.main_ops.is_empty() -} - -const fn shared(key: &SiblingKey) -> bool { - matches!(key, SiblingKey::Shared) -} - -fn families() -> &'static [Family] { - &[ - Family { - name: "no-siblings", - matches: |c| c.siblings.is_empty(), - }, - Family { - name: "one-sibling-correct-key", - matches: |c| c.siblings.len() == 1 && shared(&c.siblings[0].key) && has_content(c), - }, - Family { - name: "one-sibling-wrong-key", - matches: |c| c.siblings.len() == 1 && !shared(&c.siblings[0].key) && has_content(c), - }, - Family { - name: "many-siblings", - matches: |c| c.siblings.len() >= 2 && has_content(c), - }, - Family { - name: "sibling-has-ops", - matches: |c| c.siblings.iter().any(|s| !s.ops.is_empty()) && has_content(c), - }, - Family { - name: "all-siblings-correct-key", - matches: |c| { - !c.siblings.is_empty() - && c.siblings.iter().all(|s| shared(&s.key)) - && has_content(c) - }, - }, - Family { - name: "mixed-correct-and-wrong", - matches: |c| { - c.siblings.len() >= 2 - && c.siblings.iter().any(|s| shared(&s.key)) - && c.siblings.iter().any(|s| !shared(&s.key)) - && has_content(c) - }, - }, - ] -} - -fn main() { - let dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/shared_cache")); - let findings_dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/findings")); - run_search( - families(), - Rng::new(0x5C00_0000_5C00_0001), - dir, - 8000, - 60_000, - run_shared_cache, - &DivergencePolicy::QuarantineTo { - dir: findings_dir, - prefix: "shared-cache-", - }, - ); -} diff --git a/fuzz/fuzz_targets/shared_cache.rs b/fuzz/fuzz_targets/shared_cache.rs deleted file mode 100755 index 93619e8..0000000 --- a/fuzz/fuzz_targets/shared_cache.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![no_main] - -libfuzzer_sys::fuzz_target!(|case: sqlite3mc_src_fuzz::SharedCacheCase| { - sqlite3mc_src_fuzz::run_shared_cache(&case); -}); diff --git a/fuzz/src/db.rs b/fuzz/src/db.rs index 0ad724c..138a7bf 100644 --- a/fuzz/src/db.rs +++ b/fuzz/src/db.rs @@ -53,13 +53,6 @@ pub(crate) enum Param<'a> { Blob(&'a [u8]), } -/// `sqlite3_enable_shared_cache`: process-wide, so every connection opened after the call -/// defaults to shared-cache mode. -pub(crate) fn enable_shared_cache(enable: bool) -> Result<(), Code> { - // SAFETY: takes a plain `c_int`, touches no pointers, safe to call at any time. - check(unsafe { ffi::sqlite3_enable_shared_cache(c_int::from(enable)) }) -} - /// An open connection, closed on drop. pub(crate) struct Connection { db: NonNull, diff --git a/fuzz/src/ffi.rs b/fuzz/src/ffi.rs index ce60110..3999928 100644 --- a/fuzz/src/ffi.rs +++ b/fuzz/src/ffi.rs @@ -91,7 +91,6 @@ unsafe extern "C" { ) -> *mut Backup; pub(crate) fn sqlite3_backup_step(p: *mut Backup, n_page: c_int) -> c_int; pub(crate) fn sqlite3_backup_finish(p: *mut Backup) -> c_int; - pub(crate) fn sqlite3_enable_shared_cache(enable: c_int) -> c_int; /// SQLite3MC's own entry points, absent from upstream SQLite. pub(crate) fn sqlite3mc_config(db: *mut Sqlite3, param: *const c_char, value: c_int) -> c_int; pub(crate) fn sqlite3mc_config_cipher( diff --git a/fuzz/src/input.rs b/fuzz/src/input.rs index d19bf9e..b5978b3 100644 --- a/fuzz/src/input.rs +++ b/fuzz/src/input.rs @@ -590,48 +590,3 @@ pub struct BackupCase { /// steps of it. pub dest_ops: Vec, } - -/// Connections beyond this, opened to the same file under shared-cache mode, are ignored. -pub(crate) const MAX_SIBLINGS: usize = 3; - -/// The cipher and key a [`SharedConnection`] stages and keys with. -#[derive(Debug, Clone, PartialEq, Eq, Arbitrary)] -pub enum SiblingKey { - /// [`SharedCacheCase::written`], the one key shared-cache mode supports per file. - Shared, - /// A configuration of its own, almost certainly not [`SharedCacheCase::written`]. - Own(NativeConfig), -} - -/// One sibling connection, opened to the same file `main` already created and populated under -/// [`SharedCacheCase::written`]'s key, under shared-cache mode. -#[derive(Debug, Clone, PartialEq, Arbitrary)] -pub struct SharedConnection { - /// The cipher and key this connection stages and keys with. - pub key: SiblingKey, - /// Workload attempted on this connection after opening, at most [`MAX_OPS`] steps of it, - /// tolerated to fail when this connection's own key turned out wrong. - pub ops: Vec, -} - -/// One shared-cache execution: a main connection creates and populates a database, then up to -/// [`MAX_SIBLINGS`] further connections open the *same* file under shared-cache mode. -/// -/// Shared-cache connections to one file share one pager and one codec, so SQLite3MC supports a -/// single key and cipher configuration per file, and a sibling keying it differently replaces -/// the codec under `main` (`utelle/SQLite3MultipleCiphers` issue #281). -/// -/// Oracle: when every sibling keys with [`SiblingKey::Shared`], `main`'s own reads must keep -/// succeeding throughout. Unless a sibling ran a workload after one keyed the file differently, -/// and so wrote through the replaced codec, the file itself must still read back cleanly under -/// `written` once every connection closes, the same way the other harnesses' own "nothing -/// corrupts the file at rest" oracle does. -#[derive(Debug, Clone, PartialEq, Arbitrary)] -pub struct SharedCacheCase { - /// `main`'s own cipher and key, the file's one true key. - pub written: NativeConfig, - /// Workload `main` runs before any sibling connects, at most [`MAX_OPS`] steps of it. - pub main_ops: Vec, - /// Sibling connections to the same file, at most [`MAX_SIBLINGS`] of them. - pub siblings: Vec, -} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 9248adf..304d7dd 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -1,6 +1,5 @@ //! Fuzzing SQLite3MC's own cipher schemes for self-consistency, its rekey support, its WAL -//! journal mode, its `ATTACH DATABASE` support, its `sqlite3_backup_*` API, and its shared-cache -//! mode. +//! journal mode, its `ATTACH DATABASE` support, and its `sqlite3_backup_*` API. #![expect( clippy::redundant_pub_crate, @@ -15,6 +14,6 @@ pub mod seedgen; pub use input::{ Algorithm, AttachCase, BackupCase, Checkpoint, Cipher, NativeCase, NativeKey, Op, RekeyAction, - RekeyCase, SharedCacheCase, SiblingKey, Table, WalCase, + RekeyCase, Table, WalCase, }; -pub use run::{run_attach, run_backup, run_native, run_rekey, run_shared_cache, run_wal}; +pub use run::{run_attach, run_backup, run_native, run_rekey, run_wal}; diff --git a/fuzz/src/run.rs b/fuzz/src/run.rs index cdeb6ce..cae61f5 100644 --- a/fuzz/src/run.rs +++ b/fuzz/src/run.rs @@ -15,8 +15,8 @@ use crate::{ ffi::{SQLITE_DONE, SQLITE_OPEN_CREATE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE}, input::{ Algorithm, AttachCase, AttachedDb, BackupCase, Cell, Cipher, Column, NativeCase, - NativeConfig, NativeKey, Op, PageSize, RekeyAction, RekeyCase, SharedCacheCase, SiblingKey, - Table, WalCase, MAX_ATTACHED, MAX_ATTACH_ROWS, MAX_FLIPS, MAX_OPS, MAX_SIBLINGS, + NativeConfig, NativeKey, Op, PageSize, RekeyAction, RekeyCase, Table, WalCase, + MAX_ATTACHED, MAX_ATTACH_ROWS, MAX_FLIPS, MAX_OPS, }, }; @@ -594,89 +594,6 @@ fn attach( .is_ok() } -/// Runs `case` against SQLite3MC's handling of shared-cache mode. -/// -/// A main connection creates and populates a database, then up to [`MAX_SIBLINGS`] further -/// connections open the *same* file under shared-cache mode, each keying it with `main`'s own -/// configuration or one of its own. -/// -/// # Panics -/// -/// When SQLite3MC refuses a setting or step the harness generates as valid, when `main`'s own -/// read fails after siblings keyed with the same configuration connect, or when the file itself -/// fails to read back cleanly under its one true key once every connection closes. A sibling -/// workload run after another sibling keyed the file differently writes through that sibling's -/// codec (`utelle/SQLite3MultipleCiphers` issue #281), so such a run skips the file check. -pub fn run_shared_cache(case: &SharedCacheCase) { - static SERIAL: Mutex<()> = Mutex::new(()); - let _serial = begin_run(&SERIAL); - - crate::db::enable_shared_cache(true).expect("SQLite3MC accepts sqlite3_enable_shared_cache(1)"); - - let dir = WORKDIR.as_path(); - let path = dir.join("shared-cache.db"); - remove(&path); - - let cipher = case.written.cipher; - let main = open_native(&path, &case.written, "shared-cache database"); - if let Err(code) = main.execute(c"CREATE TABLE t0(i INTEGER, r REAL, t TEXT, b BLOB)", &[]) { - panic!( - "SQLite3MC refused to create main's own {} table: {code:?}", - cipher.name() - ); - } - run_workload(&main, "SQLite3MC", &case.main_ops, case); - - let mut siblings = Vec::with_capacity(case.siblings.len().min(MAX_SIBLINGS)); - let mut one_key = true; - let mut foreign_write = false; - for sibling in case.siblings.iter().take(MAX_SIBLINGS) { - let Ok(connection) = - Connection::open(&c_path(&path), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) - else { - continue; - }; - let config = match &sibling.key { - SiblingKey::Shared => &case.written, - SiblingKey::Own(config) => { - one_key &= *config == case.written; - config - } - }; - let _ = configure_native(&connection, config); - foreign_write |= !one_key && !sibling.ops.is_empty(); - run_workload_tolerant(&connection, &sibling.ops); - siblings.push(connection); - } - - if one_key { - if let Err(code) = main.execute(c"SELECT count(*) FROM sqlite_schema", &[]) { - panic!( - "SQLite3MC's main connection could not read its own {} shared-cache database \ - after {} sibling connection(s) keyed the same file identically for {case:#?}: \ - {code:?}", - cipher.name(), - siblings.len(), - ); - } - } - - drop(siblings); - drop(main); - - if !foreign_write { - let own = dump_native(&path, &case.written); - assert!( - clean_read(&own), - "SQLite3MC corrupted its own {} shared-cache database file for {case:#?}: {}", - cipher.name(), - describe(own.iter().rev().take(3)), - ); - } - - remove(&path); -} - /// A per-process directory on tmpfs where there is one. static WORKDIR: LazyLock = LazyLock::new(|| { let shm = Path::new("/dev/shm"); @@ -948,97 +865,83 @@ pub(crate) fn run_workload( label: &str, ops: &[Op], context: &D, -) { - run_workload_with( - connection, - ops, - |connection, text, params, op| match connection.execute(&sql(text), params) { - Ok(()) => true, - Err(code) => panic!("{label} failed {op:?} ({text}) with {code:?} for {context:#?}"), - }, - ); -} - -/// Runs `ops` against an already-open, already-configured connection, silently stopping at the -/// first statement that fails instead of panicking. For a connection whose own key or cipher may -/// be wrong on purpose, where failure is expected and not itself a defect. -pub(crate) fn run_workload_tolerant(connection: &Connection, ops: &[Op]) { - run_workload_with(connection, ops, |connection, text, params, _op| { - connection.execute(&sql(text), params).is_ok() - }); -} - -/// The shared dispatch `run_workload`/`run_workload_tolerant` both turn `ops` into SQL through; -/// `run` reports whether its statement succeeded, which decides whether dependent state (a table -/// now existing, a transaction now open) advances or the op is treated as never having happened. -fn run_workload_with( - connection: &Connection, - ops: &[Op], - mut run: impl FnMut(&Connection, &str, &[Param<'_>], &Op) -> bool, ) { let mut created = [false; 3]; // Tables created inside a transaction disappear again on rollback. let mut before_transaction: Option<[bool; 3]> = None; + let run = |text: &str, params: &[Param<'_>], op: &Op| { + if let Err(code) = connection.execute(&sql(text), params) { + panic!("{label} failed {op:?} ({text}) with {code:?} for {context:#?}"); + } + }; for op in ops.iter().take(MAX_OPS) { let exists = |table: Table| created[table.index()]; match op { Op::Create(table) => { let t = TABLE_NAMES[table.index()]; - let text = - format!("CREATE TABLE IF NOT EXISTS {t}(i INTEGER, r REAL, t TEXT, b BLOB)"); - if run(connection, &text, &[], op) { - created[table.index()] = true; - } + run( + &format!("CREATE TABLE IF NOT EXISTS {t}(i INTEGER, r REAL, t TEXT, b BLOB)"), + &[], + op, + ); + created[table.index()] = true; } Op::Index(table, column) if exists(*table) => { let (t, c) = (TABLE_NAMES[table.index()], column_name(*column)); - let text = format!("CREATE INDEX IF NOT EXISTS {t}_{c} ON {t}({c})"); - run(connection, &text, &[], op); + run( + &format!("CREATE INDEX IF NOT EXISTS {t}_{c} ON {t}({c})"), + &[], + op, + ); } Op::Insert(table, cells) if exists(*table) => { let owned = cells.each_ref().map(Owned::new); let params = owned.each_ref().map(Owned::param); let t = TABLE_NAMES[table.index()]; - let text = format!("INSERT INTO {t}(i, r, t, b) VALUES (?1, ?2, ?3, ?4)"); - run(connection, &text, ¶ms, op); + run( + &format!("INSERT INTO {t}(i, r, t, b) VALUES (?1, ?2, ?3, ?4)"), + ¶ms, + op, + ); } Op::Update(table, rowid, column, cell) if exists(*table) => { let owned = Owned::new(cell); let (t, c) = (TABLE_NAMES[table.index()], column_name(*column)); let params = [owned.param(), Param::Integer(i64::from(*rowid))]; - let text = format!("UPDATE {t} SET {c} = ?1 WHERE rowid = ?2"); - run(connection, &text, ¶ms, op); + run( + &format!("UPDATE {t} SET {c} = ?1 WHERE rowid = ?2"), + ¶ms, + op, + ); } Op::Delete(table, rowid) if exists(*table) => { let t = TABLE_NAMES[table.index()]; - let text = format!("DELETE FROM {t} WHERE rowid = ?1"); - run(connection, &text, &[Param::Integer(i64::from(*rowid))], op); - } - Op::Vacuum if before_transaction.is_none() => { - run(connection, "VACUUM", &[], op); + run( + &format!("DELETE FROM {t} WHERE rowid = ?1"), + &[Param::Integer(i64::from(*rowid))], + op, + ); } + Op::Vacuum if before_transaction.is_none() => run("VACUUM", &[], op), Op::Begin if before_transaction.is_none() => { - if run(connection, "BEGIN", &[], op) { - before_transaction = Some(created); - } + run("BEGIN", &[], op); + before_transaction = Some(created); } Op::Commit if before_transaction.is_some() => { - if run(connection, "COMMIT", &[], op) { - before_transaction = None; - } + run("COMMIT", &[], op); + before_transaction = None; } Op::Rollback => { if let Some(saved) = before_transaction.take() { - if run(connection, "ROLLBACK", &[], op) { - created = saved; - } + run("ROLLBACK", &[], op); + created = saved; } } _ => {} } } if before_transaction.is_some() { - run(connection, "COMMIT", &[], &Op::Commit); + run("COMMIT", &[], &Op::Commit); } } diff --git a/fuzz/tests/replay.rs b/fuzz/tests/replay.rs index ee64f37..a230dbb 100644 --- a/fuzz/tests/replay.rs +++ b/fuzz/tests/replay.rs @@ -1,11 +1,11 @@ //! Replays the committed corpora for self-consistency across the native cipher schemes, rekey -//! round-trips, WAL-mode crash tolerance, `ATTACH DATABASE` cross-database isolation, the backup -//! API's compatibility guard, and shared-cache mode, without libFuzzer. +//! round-trips, WAL-mode crash tolerance, `ATTACH DATABASE` cross-database isolation, and the +//! backup API's compatibility guard, without libFuzzer. use arbitrary::{Arbitrary, Unstructured}; use sqlite3mc_src_fuzz::{ - run_attach, run_backup, run_native, run_rekey, run_shared_cache, run_wal, AttachCase, - BackupCase, NativeCase, RekeyCase, SharedCacheCase, WalCase, + run_attach, run_backup, run_native, run_rekey, run_wal, AttachCase, BackupCase, NativeCase, + RekeyCase, WalCase, }; #[test] @@ -102,22 +102,3 @@ fn committed_backup_corpus_never_reports_success_for_an_unreadable_destination() run_backup(&case); } } - -#[test] -fn committed_shared_cache_corpus_keeps_the_main_connection_reading() { - let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/shared_cache"); - let mut seeds: Vec<_> = std::fs::read_dir(dir) - .expect("the committed corpus exists") - .map(|entry| entry.expect("the corpus is readable").path()) - .collect(); - seeds.sort(); - assert!(!seeds.is_empty(), "{dir} holds no seeds"); - for seed in seeds { - let bytes = std::fs::read(&seed).expect("seeds are readable"); - // libFuzzer's `fuzz_target!` decodes the same way, taking the rest of the input last. - let case = SharedCacheCase::arbitrary_take_rest(Unstructured::new(&bytes)) - .unwrap_or_else(|e| panic!("{} does not decode: {e}", seed.display())); - eprintln!("{}", seed.display()); - run_shared_cache(&case); - } -}