From 25809dc7a3acc37411776d79f526512088e466c3 Mon Sep 17 00:00:00 2001 From: forhappy Date: Wed, 14 Oct 2026 21:09:14 -0700 Subject: [PATCH] fix(sqlite): upgrade bundled WAL safety Use SQLite 3.51.3 to fix WAL-reset corruption. Preserve checked unsigned bindings and reject hook installation failures before SQL. Backport to the exact Cellule revision pinned by Crab. --- Cargo.lock | 45 ++++++++++++++----- Cargo.toml | 2 +- crates/cellule-ltx/docs/safety.md | 7 +++ .../cellule-ltx/tests/cell/roots/lifecycle.rs | 6 +-- .../cellule-runtime/src/primitives/sql/mod.rs | 21 +++++---- .../cellule-runtime/tests/primitives/sql.rs | 22 +++++++++ 6 files changed, 78 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b512761a..ef78a8df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,9 +678,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" @@ -838,9 +838,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "foldhash", ] @@ -853,11 +853,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -1239,9 +1239,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libsqlite3-sys" -version = "0.32.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -1962,11 +1962,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + [[package]] name = "rusqlite" -version = "0.34.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ "bitflags", "fallible-iterator", @@ -1974,6 +1984,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -2346,6 +2357,18 @@ dependencies = [ "der", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 6b7e13d8..61531cf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ futures-util = "0.3" object_store = { version = "0.14.1", default-features = false } prost = "0.13" reqwest = { version = "0.12", default-features = false } -rusqlite = { version = "0.34", features = ["bundled"] } +rusqlite = { version = "0.39", features = ["bundled", "fallible_uint"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tempfile = "3" diff --git a/crates/cellule-ltx/docs/safety.md b/crates/cellule-ltx/docs/safety.md index 7898dd4e..c45810c6 100644 --- a/crates/cellule-ltx/docs/safety.md +++ b/crates/cellule-ltx/docs/safety.md @@ -4,6 +4,13 @@ LTX CRC64 protects file structure and rolling database state; it is not an authenticator. Cell roots and objects carry BLAKE3 digests. The embedding service authenticates the authority record that selects a root. +Managed databases use bundled SQLite 3.51.3 through rusqlite 0.39. This includes +SQLite's [WAL-reset race fix](https://sqlite.org/releaselog/3_51_3.html), required +when writer and checkpoint connections overlap. The `fallible_uint` feature +preserves range-checked unsigned SQL bindings used by the runtime. Embedders +sharing rusqlite types must select the same compatible dependency version; +LTX capture, durability, and recovery contracts remain unchanged. + | Invariant | Effect | | --- | --- | | First segment is a full snapshot | No delta-only restore. | diff --git a/crates/cellule-ltx/tests/cell/roots/lifecycle.rs b/crates/cellule-ltx/tests/cell/roots/lifecycle.rs index 4bf50d73..d4098116 100644 --- a/crates/cellule-ltx/tests/cell/roots/lifecycle.rs +++ b/crates/cellule-ltx/tests/cell/roots/lifecycle.rs @@ -49,11 +49,7 @@ async fn read_only_roots_keep_exact_snapshots_without_materializing_pages() { .unwrap(); assert_eq!(value, 1); connection.pragma_update(None, "query_only", false).unwrap(); - assert!( - connection - .is_readonly(rusqlite::DatabaseName::Main) - .unwrap() - ); + assert!(connection.is_readonly(rusqlite::MAIN_DB).unwrap()); assert!( connection .execute("UPDATE counter SET value=99", []) diff --git a/crates/cellule-runtime/src/primitives/sql/mod.rs b/crates/cellule-runtime/src/primitives/sql/mod.rs index f373af4e..d2a9b71d 100644 --- a/crates/cellule-runtime/src/primitives/sql/mod.rs +++ b/crates/cellule-runtime/src/primitives/sql/mod.rs @@ -102,8 +102,7 @@ pub(crate) fn write_blob( "SQL blob write exceeds 1 MiB", )?; } - let mut blob = - transaction.blob_open(rusqlite::DatabaseName::Main, table, column, row_id, false)?; + let mut blob = transaction.blob_open(rusqlite::MAIN_DB, table, column, row_id, false)?; let written = blob.write_at(bytes, offset); let closed = blob.close(); written?; @@ -117,7 +116,7 @@ fn execute_batch( mode: AccessMode, ) -> Result> { validate_batch(batch)?; - let _authorizer = AuthorizerGuard::install(connection, mode); + let _authorizer = AuthorizerGuard::install(connection, mode)?; let mut result_sets = Vec::with_capacity(batch.statements.len()); let mut row_count = 0_usize; let mut result_bytes = 0_usize; @@ -299,20 +298,26 @@ struct AuthorizerGuard<'a> { } impl<'a> AuthorizerGuard<'a> { - fn install(connection: &'a Connection, mode: AccessMode) -> Self { + fn install(connection: &'a Connection, mode: AccessMode) -> Result { let authorizer: fn(AuthContext<'_>) -> Authorization = match mode { AccessMode::ReadOnly => authorize_read, AccessMode::ReadWrite => authorize_write, }; - connection.authorizer(Some(authorizer)); - Self { connection } + // Borrowed handles reject hook installation. No application SQL may + // run until authorization is installed on this exact connection. + connection.authorizer(Some(authorizer))?; + Ok(Self { connection }) } } impl Drop for AuthorizerGuard<'_> { fn drop(&mut self) { - self.connection - .authorizer(None::) -> Authorization>); + if let Err(error) = self + .connection + .authorizer(None::) -> Authorization>) + { + tracing::error!(%error, "SQL authorizer cleanup failed"); + } } } diff --git a/crates/cellule-runtime/tests/primitives/sql.rs b/crates/cellule-runtime/tests/primitives/sql.rs index e5ca386b..ca07973c 100644 --- a/crates/cellule-runtime/tests/primitives/sql.rs +++ b/crates/cellule-runtime/tests/primitives/sql.rs @@ -315,6 +315,28 @@ fn typed_batch_mutates_and_materializes_in_order() { transaction.commit().unwrap(); } +#[test] +fn borrowed_sql_connection_cannot_bypass_the_authorizer() { + let connection = connection(); + // SAFETY: The owner outlives this borrowed wrapper; access is sequential, + // and dropping the wrapper does not close the owner's SQLite handle. + let borrowed = unsafe { Connection::from_handle(connection.handle()) }.unwrap(); + let result = sql_query_batch( + &borrowed, + &SqlBatch { + statements: vec![statement( + "SELECT commit_sequence FROM sys_meta", + Vec::new(), + )], + }, + ); + assert!(matches!( + result, + Err(cellule_runtime::Error::Sqlite(error)) + if error.sqlite_error_code() == Some(cellule_ltx::rusqlite::ErrorCode::ApiMisuse) + )); +} + #[test] fn authorizer_blocks_runtime_tables_and_indirect_trigger_or_view_access() { let mut connection = connection();