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
45 changes: 34 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions crates/cellule-ltx/docs/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
6 changes: 1 addition & 5 deletions crates/cellule-ltx/tests/cell/roots/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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", [])
Expand Down
21 changes: 13 additions & 8 deletions crates/cellule-runtime/src/primitives/sql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand All @@ -117,7 +116,7 @@ fn execute_batch(
mode: AccessMode,
) -> Result<Vec<SqlResultSet>> {
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;
Expand Down Expand Up @@ -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<Self> {
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::<fn(AuthContext<'_>) -> Authorization>);
if let Err(error) = self
.connection
.authorizer(None::<fn(AuthContext<'_>) -> Authorization>)
{
tracing::error!(%error, "SQL authorizer cleanup failed");
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions crates/cellule-runtime/tests/primitives/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading