Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
3 changes: 3 additions & 0 deletions fuzz/corpus/findings/backup-rc4-small-page-short-read
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@·žÕjìxã¹––Ò ´á|Ž
1\Âì9h® &€4áëêÌ¢Nx ¥ƒ*‡n·žÕjìxã¹––Ò ´á|Ž
PJ[áº;iÑå2'Z Ú .|¿W¦ŒF½zsýj¦)yIÏúÛÜO$ÃéHªáìD
Expand Down
Binary file not shown.
3 changes: 3 additions & 0 deletions fuzz/corpus/shared_cache/all-siblings-correct-key
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ªÞÔ'æL7à®HpM½[/äý¢QséΦꐈxâC½,§› ý ‡Û³¾nR’F=D—_6Ìp
º+×Hšæ¨%¶ˆ‹}¡$ˆ®þqúŸí˜·¥‚è%܁—yýÉÆF |˜¡¤2Jy‘X1·YÅNµmÙµäYVï$€Á0³Ü®ýíÇCÑû9Ù¡<übW”K^Ô~[N@æËD—vˆ³*Ãá©Ë„½]b¬áºÒH·&xþßô¬#WLIgYøXŽ$"€ÿI¦}d[BÖȇœä—…<6!¥»nÄ€`Æò['Ç¿Wgžƒ ¾õ·l6\çÃï[’>¢®£HWh®íÛXüôcPŸTטA
K›ª¹ÏH˜sqio‡G6äÀ¢t•x—2ªâvꌳL|°Ï
Expand Down
1 change: 1 addition & 0 deletions fuzz/corpus/shared_cache/many-siblings
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
¾{Ó9ƒAo–'ñ§$¬6*6yƒÉhuøÄáÉ,ë¶\ý‡!SÞó¡M.r‚HÓŸ‚±p3;ƹPm-ü!ñ\C‡¡úIfÉÚ²æø¡«×&º×ª‘½¨ˆáüíö4Îòfg³ä`þ¶­»¼HjÇ oq<Ê^xðƒlHIèáYÚ7'¯4GÐ1îÔ‰ìôªc‚„Ÿ¨]-mVñEÈpëš2é_«nÒ6•ùÝ7›'< ÄØ0|mèÍI®°…J~rNò.ÊD½EðXèÞFJØôñ
Binary file added fuzz/corpus/shared_cache/mixed-correct-and-wrong
Binary file not shown.
Binary file added fuzz/corpus/shared_cache/one-sibling-correct-key
Binary file not shown.
Binary file added fuzz/corpus/shared_cache/one-sibling-wrong-key
Binary file not shown.
Binary file added fuzz/corpus/shared_cache/sibling-has-ops
Binary file not shown.
20 changes: 10 additions & 10 deletions fuzz/examples/gen_shared_cache_seeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ use std::path::Path;
use sqlite3mc_src_fuzz::{
run_shared_cache,
seedgen::{run_search, DivergencePolicy, Family, Rng},
SharedCacheCase,
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<SharedCacheCase>] {
&[
Family {
Expand All @@ -25,15 +29,11 @@ fn families() -> &'static [Family<SharedCacheCase>] {
},
Family {
name: "one-sibling-correct-key",
matches: |c| {
c.siblings.len() == 1 && c.siblings[0].config == c.written && has_content(c)
},
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 && c.siblings[0].config != c.written && has_content(c)
},
matches: |c| c.siblings.len() == 1 && !shared(&c.siblings[0].key) && has_content(c),
},
Family {
name: "many-siblings",
Expand All @@ -47,16 +47,16 @@ fn families() -> &'static [Family<SharedCacheCase>] {
name: "all-siblings-correct-key",
matches: |c| {
!c.siblings.is_empty()
&& c.siblings.iter().all(|s| s.config == c.written)
&& 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| s.config == c.written)
&& c.siblings.iter().any(|s| s.config != c.written)
&& c.siblings.iter().any(|s| shared(&s.key))
&& c.siblings.iter().any(|s| !shared(&s.key))
&& has_content(c)
},
},
Expand Down
74 changes: 60 additions & 14 deletions fuzz/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,39 @@ impl<'a> Arbitrary<'a> for NativeConfig {
}
}

impl NativeConfig {
/// Whether `other` lays pages out exactly like `self`, every setting but the key equal.
#[must_use]
pub fn same_layout(&self, other: &Self) -> bool {
let Self {
cipher,
legacy,
page_size,
plaintext_header_size,
aegis_algorithm_256,
key: _,
} = self;
*cipher == other.cipher
&& *legacy == other.legacy
&& *page_size == other.page_size
&& *plaintext_header_size == other.plaintext_header_size
&& *aegis_algorithm_256 == other.aegis_algorithm_256
}

/// Whether this configuration encrypts page 1's header bytes 16 to 23, so nothing on disk
/// tells SQLite the page size before the key is set: `rc4` always, and `aes128cbc`,
/// `aes256cbc` and `chacha20` in `legacy` mode unless a plaintext header covers them.
#[must_use]
pub fn hides_page_size(&self) -> bool {
let whole_page_one = match self.cipher {
Cipher::Rc4 => true,
Cipher::Aes128 | Cipher::Aes256 | Cipher::ChaCha20 => self.legacy == Some(true),
Cipher::Ascon128 | Cipher::Aegis => false,
};
whole_page_one && self.plaintext_header_size.unwrap_or(0) < 24
}
}

/// One native-scheme execution: write, round-trip, a wrong key, and, where authenticated, damage.
#[derive(Debug, Clone, PartialEq, Arbitrary)]
pub struct NativeCase {
Expand Down Expand Up @@ -704,11 +737,14 @@ impl<'a> Arbitrary<'a> for PageSize {
/// One `sqlite3_backup` execution: writes a source, optionally pre-populates a destination with
/// unrelated content, then runs `sqlite3_backup_init`/`_step`/`_finish` between them.
///
/// Oracle: if SQLite3MC's own compatibility guard (`sqlite3mcIsBackupSupported`, called from
/// `sqlite3_backup_init`) lets the pair through, and every step reports success, the destination
/// must reopen cleanly with its own true key; a refused pair (`backup_init` returning `NULL`) is
/// not a defect, since this harness exists to find exactly the cases where that guard *should*
/// have refused the pair but did not.
/// SQLite3MC's own compatibility guard (`sqlite3mcIsBackupSupported`, called from
/// `sqlite3_backup_init`) lets pairs disagreeing on reserved bytes through, so a backup between them
/// corrupts the destination (`upstream/sqlite3mc-backup-guard-ignores-reserve.md`).
///
/// Oracle: when [`BackupCase::src`] and [`BackupCase::dest`] are laid out identically, every
/// cipher setting but the key and the `PRAGMA page_size` equal, and every step reports success,
/// the destination must reopen cleanly with its own true key. A refused pair (`backup_init`
/// returning `NULL`) is never a defect.
#[derive(Debug, Clone, PartialEq, Arbitrary)]
pub struct BackupCase {
/// The source database's own cipher and key.
Expand All @@ -729,13 +765,21 @@ pub struct BackupCase {
/// 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, almost certainly not
/// [`SharedCacheCase::written`].
pub config: NativeConfig,
/// 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<Op>,
Expand All @@ -744,13 +788,15 @@ pub struct SharedConnection {
/// 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.
///
/// Each sibling has its own cipher and key, almost certainly not matching `main`'s.
/// 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: `main` never changes its own key; its own reads must keep succeeding throughout,
/// regardless of what any sibling connection does to the file it shares with it, correct key,
/// wrong key, or a different cipher entirely. Also checked after every connection closes: the
/// file itself must still read back cleanly under `written`, the same way the other harnesses'
/// own "nothing corrupts the file at rest" oracle does.
/// 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.
Expand Down
2 changes: 1 addition & 1 deletion fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ pub mod seedgen;

pub use input::{
Algorithm, AttachCase, BackupCase, Case, Checkpoint, Cipher, Config, Key, NativeCase,
NativeKey, Op, RekeyAction, RekeyCase, SharedCacheCase, Side, Table, WalCase,
NativeKey, Op, RekeyAction, RekeyCase, SharedCacheCase, SiblingKey, Side, Table, WalCase,
};
pub use run::{run, run_attach, run_backup, run_native, run_rekey, run_shared_cache, run_wal};
86 changes: 58 additions & 28 deletions fuzz/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use crate::{
input::{
Algorithm, AttachCase, AttachedDb, BackupCase, Case, Cell, Cipher, Column, Config, Key,
NativeCase, NativeConfig, NativeKey, Op, PageSize, RekeyAction, RekeyCase, SharedCacheCase,
Side, Table, WalCase, MAX_ATTACHED, MAX_ATTACH_ROWS, MAX_FLIPS, MAX_OPS, MAX_SIBLINGS,
SiblingKey, Side, Table, WalCase, MAX_ATTACHED, MAX_ATTACH_ROWS, MAX_FLIPS, MAX_OPS,
MAX_SIBLINGS,
},
};

Expand Down Expand Up @@ -551,15 +552,20 @@ fn exercise_attached(
/// own), each with its own cipher, key, and `PRAGMA page_size`, then backs the source up into the
/// destination. When `sqlite3_backup_init`'s own compatibility guard (`sqlite3mcIsBackupSupported`)
/// refuses the pair, there is nothing further to check: that is the guard working as intended.
/// When it lets the pair through and the backup reports full success, the destination must still
/// reopen cleanly with its own true key: `sqlite3_backup_step`/`_finish` reporting success for a
/// destination that cannot actually be read back is exactly the defect `utelle/SQLite3MultipleCiphers`
/// issue #158 described, and exactly what this harness exists to catch a recurrence of.
/// That guard still lets pairs disagreeing on reserved bytes through, and such a backup corrupts the
/// destination (`upstream/sqlite3mc-backup-guard-ignores-reserve.md`). So only a pair laid out
/// identically, every cipher setting but the key and the `PRAGMA page_size` equal, has to reopen
/// cleanly with the destination's own true key after the backup reports full success. A failure
/// there is the defect `utelle/SQLite3MultipleCiphers` issue #158 described, and exactly what this
/// harness exists to catch a recurrence of. The one identically laid out pair exempt is a
/// destination that hides its page size and is sized below 4096-byte pages by `PRAGMA page_size`
/// alone, which SQLite3MC cannot reopen at all, backup or not
/// (`upstream/sqlite3mc-rc4-short-read-skips-decryption.md`).
///
/// # Panics
///
/// When SQLite3MC refuses a setting or step the harness generates as valid, or a backup the guard
/// allowed and that reported full success leaves an unreadable destination.
/// When SQLite3MC refuses a setting or step the harness generates as valid, or a backup between
/// two identically laid out databases reported full success and left an unreadable destination.
pub fn run_backup(case: &BackupCase) {
static SERIAL: Mutex<()> = Mutex::new(());
let _serial = begin_run(&SERIAL, case);
Expand All @@ -576,7 +582,14 @@ pub fn run_backup(case: &BackupCase) {
if let Some(job) = dest.backup_init(c"main", &src, c"main") {
let step_rc = job.step(-1);
let finish_result = job.finish();
if step_rc == SQLITE_DONE && finish_result.is_ok() {
if step_rc == SQLITE_DONE
&& finish_result.is_ok()
&& case.src_page_size == case.dest_page_size
&& case.src.same_layout(&case.dest)
&& !(case.dest.hides_page_size()
&& case.dest.page_size.is_none()
&& case.dest_page_size.is_some_and(|size| size.get() < 4096))
{
drop(src);
drop(dest);
let dump = dump_native(&dest_path, &case.dest);
Expand Down Expand Up @@ -653,13 +666,16 @@ fn attach(
/// 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 with their own cipher and key.
/// 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 a sibling connects despite `main` never changing its own key, or when the
/// file itself fails to read back cleanly under its one true key once every connection closes.
/// 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, case);
Expand All @@ -673,7 +689,7 @@ pub fn run_shared_cache(case: &SharedCacheCase) {

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)", &[]) {
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()
Expand All @@ -682,6 +698,8 @@ pub fn run_shared_cache(case: &SharedCacheCase) {
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(
&SQLITE3MC,
Expand All @@ -690,31 +708,43 @@ pub fn run_shared_cache(case: &SharedCacheCase) {
) else {
continue;
};
let _ = configure_native(&connection, &sibling.config);
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 let Err(code) = main.execute(c"SELECT count(*) FROM sqlite_schema", &[]) {
panic!(
"SQLite3MC's main connection, still keyed with its own correct {} key, could not \
read its own shared-cache database after {} sibling connection(s) opened the same \
file for {case:#?}: {code:?}",
cipher.name(),
siblings.len(),
);
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);

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)),
);
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);
}
Expand Down
Loading