lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 278349715401e8c8c5d61405dcabffb9880e42e3
parent b096b3695614f3e7fbb2023d17723a48d6048dc7
Author: triesap <tyson@radroots.org>
Date:   Sat, 15 Aug 2026 19:08:54 +0000

service-sqlite: close transaction policy escape

Diffstat:
Mcrates/service_sqlite/README.md | 9+++++++++
Mcrates/service_sqlite/src/connection.rs | 145++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/service_sqlite/src/lib.rs | 1+
Mcrates/service_sqlite/src/migration.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Acrates/service_sqlite/src/statement_policy.rs | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/service_sqlite/tests/package_boundary.rs | 35+++++++++++++++++++++++++++++++----
6 files changed, 424 insertions(+), 78 deletions(-)

diff --git a/crates/service_sqlite/README.md b/crates/service_sqlite/README.md @@ -15,6 +15,15 @@ runner-owned. Writable host opening finishes every pending governed migration before returning, and read-only inspection opens only current migration and schema state. +Service-controlled SQL is screened before SQLite compilation through both the +borrowed transaction executor and migration callback executor. The closed +statement-control inventory is `PRAGMA`, `ATTACH`, `DETACH`, `BEGIN`, `COMMIT`, +`END`, `ROLLBACK`, `SAVEPOINT`, and `RELEASE`, regardless of case, whitespace, +comments, multiple statements, or prepared-query entry point. Rejection is +sticky for the transaction, so ignoring the immediate SQL error cannot permit +commit. Runner-owned setup remains private, and the complete connection policy +is revalidated before commit and before a connection can return to the pool. + Cancelling a host transaction before the runner enables outer commit quarantines its connection and leaves no authoritative transaction effect. A service-operation error is returned only after rollback is confirmed; an diff --git a/crates/service_sqlite/src/connection.rs b/crates/service_sqlite/src/connection.rs @@ -555,14 +555,14 @@ impl ServiceSqliteHost { } let operation_result = { - let database_control_rejected = Arc::new(AtomicBool::new(false)); + let statement_control_rejected = Arc::new(AtomicBool::new(false)); let mut executor = ServiceSqliteTransaction { connection: &mut transaction, - database_control_rejected: Arc::clone(&database_control_rejected), + statement_control_rejected: Arc::clone(&statement_control_rejected), }; - (operation(&mut executor).await, database_control_rejected) + (operation(&mut executor).await, statement_control_rejected) }; - let (operation_result, database_control_rejected) = operation_result; + let (operation_result, statement_control_rejected) = operation_result; if let Err(error) = self.pool.validate() { let operation_error = operation_result.err(); let permit = gate.permit_runner_rollback(); @@ -618,7 +618,7 @@ impl ServiceSqliteHost { &mut transaction, &gate, &initial_policy, - &database_control_rejected, + &statement_control_rejected, ) .await; let precommit = match precommit { @@ -801,12 +801,12 @@ impl ServiceSqliteHost { connection: &mut SqliteConnection, gate: &crate::transaction_control::TransactionControlGate, initial_policy: &crate::migration::MigrationConnectionPolicy, - database_control_rejected: &AtomicBool, + statement_control_rejected: &AtomicBool, ) -> Result<(), ServiceSqliteError> { self.pool.validate()?; crate::require_condition( !gate.control_violation_observed() - && !database_control_rejected.load(Ordering::Acquire), + && !statement_control_rejected.load(Ordering::Acquire), ServiceSqliteErrorKind::Open, )?; crate::migration::assert_governed_transaction(connection).await?; @@ -874,12 +874,12 @@ impl fmt::Debug for ServiceSqliteHost { /// ``` pub struct ServiceSqliteTransaction<'connection> { connection: &'connection mut SqliteConnection, - database_control_rejected: Arc<AtomicBool>, + statement_control_rejected: Arc<AtomicBool>, } struct RestrictedExecute<Q> { query: Q, - database_control_rejected: Arc<AtomicBool>, + statement_control_rejected: Arc<AtomicBool>, } impl<'query, Q> Execute<'query, Sqlite> for RestrictedExecute<Q> @@ -887,7 +887,7 @@ where Q: Execute<'query, Sqlite>, { fn sql(self) -> SqlStr { - restricted_sql(self.query.sql(), &self.database_control_rejected) + restricted_sql(self.query.sql(), &self.statement_control_rejected) } fn statement(&self) -> Option<&SqliteStatement> { @@ -905,21 +905,15 @@ where } } -fn restricted_sql(sql: SqlStr, database_control_rejected: &AtomicBool) -> SqlStr { - if contains_database_control(sql.as_str()) { - database_control_rejected.store(true, Ordering::Release); - SqlStr::from_static("RADROOTS_FORBIDDEN_DATABASE_CONTROL") +fn restricted_sql(sql: SqlStr, statement_control_rejected: &AtomicBool) -> SqlStr { + if crate::statement_policy::contains_forbidden_statement_control(sql.as_str()) { + statement_control_rejected.store(true, Ordering::Release); + SqlStr::from_static("RADROOTS_FORBIDDEN_STATEMENT_CONTROL") } else { sql } } -pub(crate) fn contains_database_control(sql: &str) -> bool { - sql.as_bytes() - .split(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_') - .any(|token| token.eq_ignore_ascii_case(b"attach") || token.eq_ignore_ascii_case(b"detach")) -} - impl fmt::Debug for ServiceSqliteTransaction<'_> { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("ServiceSqliteTransaction([redacted])") @@ -943,7 +937,7 @@ where { (&mut *self.connection).fetch_many(RestrictedExecute { query, - database_control_rejected: Arc::clone(&self.database_control_rejected), + statement_control_rejected: Arc::clone(&self.statement_control_rejected), }) } @@ -957,7 +951,7 @@ where { (&mut *self.connection).fetch_optional(RestrictedExecute { query, - database_control_rejected: Arc::clone(&self.database_control_rejected), + statement_control_rejected: Arc::clone(&self.statement_control_rejected), }) } @@ -970,7 +964,7 @@ where 'executor: 'e, { (&mut *self.connection).prepare_with( - restricted_sql(sql, &self.database_control_rejected), + restricted_sql(sql, &self.statement_control_rejected), parameters, ) } @@ -3279,24 +3273,17 @@ mod tests { #[cfg(any(target_os = "linux", target_os = "macos"))] #[tokio::test] - async fn transaction_control_policy_and_attachment_escapes_fail_closed() { - for (statement, expected_kind) in [ - ( - "INSERT INTO host_probe (value) VALUES (0); COMMIT", - ServiceSqliteTransactionErrorKind::RollbackFailed, - ), - ( - "ROLLBACK; BEGIN DEFERRED; INSERT INTO host_probe (value) VALUES (1)", - ServiceSqliteTransactionErrorKind::NotCommitted, - ), - ( - "PRAGMA trusted_schema=ON; INSERT INTO host_probe (value) VALUES (2)", - ServiceSqliteTransactionErrorKind::NotCommitted, - ), - ( - "ATTACH DATABASE ':memory:' AS extra; INSERT INTO host_probe (value) VALUES (3)", - ServiceSqliteTransactionErrorKind::NotCommitted, - ), + async fn complete_statement_control_inventory_is_sticky_and_fails_closed() { + for statement in [ + "INSERT INTO host_probe (value) VALUES (0); /* policy */ PrAgMa\ntrusted_schema=ON", + "INSERT INTO host_probe (value) VALUES (1); ATTACH DATABASE ':memory:' AS extra", + "INSERT INTO host_probe (value) VALUES (2); DETACH DATABASE extra", + "INSERT INTO host_probe (value) VALUES (3); BEGIN DEFERRED", + "INSERT INTO host_probe (value) VALUES (4); COMMIT", + "INSERT INTO host_probe (value) VALUES (5); END", + "INSERT INTO host_probe (value) VALUES (6); ROLLBACK", + "INSERT INTO host_probe (value) VALUES (7); SAVEPOINT escaped", + "INSERT INTO host_probe (value) VALUES (8); RELEASE SAVEPOINT escaped", ] { let (_root, _paths, _identity, _migrations, _schema, host) = initialized_host().await; let error = host @@ -3308,7 +3295,10 @@ mod tests { }) .await .expect_err("escape attempt must not commit"); - assert_eq!(error.kind(), expected_kind); + assert_eq!( + error.kind(), + ServiceSqliteTransactionErrorKind::NotCommitted + ); assert!(error.sqlite_error().is_some()); assert_eq!(row_count(&host).await, 0); } @@ -3338,6 +3328,58 @@ mod tests { #[cfg(any(target_os = "linux", target_os = "macos"))] #[tokio::test] + async fn prepared_query_policy_rejection_is_sticky_and_rolls_back_prior_work() { + let (_root, _paths, _identity, _migrations, _schema, host) = initialized_host().await; + let error = host + .transaction(|transaction| { + Box::pin(async move { + sqlx::query("INSERT INTO host_probe (value) VALUES (11)") + .execute(&mut *transaction) + .await + .expect("ordinary service statement"); + let _ = (&mut *transaction) + .prepare(SqlStr::from_static( + "SELECT 1; /* ignored */ PRAGMA trusted_schema=ON", + )) + .await; + Ok::<_, Infallible>(()) + }) + }) + .await + .expect_err("ignored prepared-query rejection must block commit"); + assert_eq!( + error.kind(), + ServiceSqliteTransactionErrorKind::NotCommitted + ); + assert_eq!(row_count(&host).await, 0); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[tokio::test] + async fn control_words_in_values_and_case_expressions_remain_available() { + let (_root, _paths, _identity, _migrations, _schema, host) = initialized_host().await; + let value = host + .transaction(|transaction| { + Box::pin(async move { + let value = sqlx::query_scalar::<_, String>( + "SELECT CASE WHEN 1 = 1 THEN 'commit' ELSE 'end' END", + ) + .fetch_one(&mut *transaction) + .await?; + sqlx::query("INSERT INTO host_probe (value) VALUES (12)") + .execute(&mut *transaction) + .await?; + Ok::<_, sqlx::Error>(value) + }) + }) + .await + .expect("ordinary expression commits"); + assert_eq!(value, "commit"); + assert_eq!(row_count(&host).await, 1); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[tokio::test] async fn attach_detach_is_rejected_before_it_can_create_external_state() { let (root, _paths, _identity, _migrations, _schema, host) = initialized_host().await; let external_database = root.path().join("forbidden-attachment.sqlite"); @@ -3481,25 +3523,6 @@ mod tests { ); } - #[test] - fn database_control_token_screen_is_closed_and_case_insensitive() { - for forbidden in [ - "ATTACH DATABASE 'x' AS extra", - "detach database extra", - "SELECT 1; /* ignored */ AtTaCh ':memory:' AS x", - "SELECT 'attach'", - ] { - assert!(contains_database_control(forbidden)); - } - for allowed in [ - "SELECT attachment FROM items", - "SELECT detached FROM items", - "SELECT COUNT(*) FROM host_probe", - ] { - assert!(!contains_database_control(allowed)); - } - } - #[cfg(any(target_os = "linux", target_os = "macos"))] #[tokio::test] async fn read_only_writes_roll_back_and_internal_pool_close_refuses_new_work() { diff --git a/crates/service_sqlite/src/lib.rs b/crates/service_sqlite/src/lib.rs @@ -24,6 +24,7 @@ mod restore; reason = "the sealed SQLx-handle adapter owns the missing SQLite online-backup calls" )] mod sqlite_native_backup; +mod statement_policy; mod status; mod transaction_control; diff --git a/crates/service_sqlite/src/migration.rs b/crates/service_sqlite/src/migration.rs @@ -630,13 +630,13 @@ pub type MigrationCallback = pub struct MigrationTransactionExecutor<'a> { connection: &'a mut SqliteConnection, - database_control_rejected: bool, + statement_control_rejected: bool, } impl MigrationTransactionExecutor<'_> { pub async fn execute(&mut self, sql: &'static str) -> Result<(), ServiceSqliteError> { - if crate::connection::contains_database_control(sql) { - self.database_control_rejected = true; + if crate::statement_policy::contains_forbidden_statement_control(sql) { + self.statement_control_rejected = true; return Err(ServiceSqliteError::new( crate::ServiceSqliteErrorKind::Migration, )); @@ -1084,7 +1084,7 @@ async fn execute_descriptor( ) -> Result<(), ServiceSqliteError> { let mut executor = MigrationTransactionExecutor { connection, - database_control_rejected: false, + statement_control_rejected: false, }; match descriptor.kind() { MigrationKind::Sql => { @@ -1102,7 +1102,7 @@ async fn execute_descriptor( assert_governed_transaction(executor.connection).await?; } } - if executor.database_control_rejected { + if executor.statement_control_rejected { return Err(migration_error(MigrationFailureKind::Execution)); } Ok(()) @@ -1738,13 +1738,13 @@ mod tests { } #[cfg(any(target_os = "linux", target_os = "macos"))] - fn ignored_attachment_callback<'a>( + fn ignored_statement_control_callback<'a>( executor: &'a mut MigrationTransactionExecutor<'_>, ) -> MigrationCallbackFuture<'a> { - let sql = IGNORED_ATTACHMENT_SQL + let sql = IGNORED_STATEMENT_CONTROL_SQL .lock() - .expect("attachment SQL mutex") - .expect("attachment SQL is installed"); + .expect("statement-control SQL mutex") + .expect("statement-control SQL is installed"); Box::pin(async move { let _ = executor.execute(sql).await; Ok(()) @@ -1755,7 +1755,7 @@ mod tests { static PENDING_CALLBACK_COUNT: AtomicUsize = AtomicUsize::new(0); #[cfg(any(target_os = "linux", target_os = "macos"))] - static IGNORED_ATTACHMENT_SQL: Mutex<Option<&'static str>> = Mutex::new(None); + static IGNORED_STATEMENT_CONTROL_SQL: Mutex<Option<&'static str>> = Mutex::new(None); #[cfg(any(target_os = "linux", target_os = "macos"))] async fn replace_with_permissive_ledger(connection: &mut SqliteConnection) { @@ -2387,6 +2387,81 @@ mod tests { #[cfg(any(target_os = "linux", target_os = "macos"))] #[tokio::test(flavor = "current_thread")] + async fn migration_sql_rejects_complete_statement_control_inventory_before_execution() { + for (name, statement) in [ + ( + "reject_pragma", + "CREATE TABLE leaked (id INTEGER); /* policy */ PrAgMa\ntrusted_schema=ON", + ), + ( + "reject_attach", + "CREATE TABLE leaked (id INTEGER); ATTACH ':memory:' AS escaped", + ), + ( + "reject_detach", + "CREATE TABLE leaked (id INTEGER); DETACH DATABASE escaped", + ), + ( + "reject_begin", + "CREATE TABLE leaked (id INTEGER); BEGIN DEFERRED", + ), + ("reject_commit", "CREATE TABLE leaked (id INTEGER); COMMIT"), + ( + "reject_end", + "CREATE TABLE leaked (id INTEGER); END TRANSACTION", + ), + ( + "reject_rollback", + "CREATE TABLE leaked (id INTEGER); ROLLBACK", + ), + ( + "reject_savepoint", + "CREATE TABLE leaked (id INTEGER); SAVEPOINT escaped", + ), + ( + "reject_release", + "CREATE TABLE leaked (id INTEGER); RELEASE SAVEPOINT escaped", + ), + ] { + let directory = tempfile::tempdir().unwrap(); + let database_path = directory.path().join("statement-control.sqlite"); + let mut connection = initialized_file_database(&database_path).await; + let catalog = MigrationCatalog::new([sql(2, name, statement)]).unwrap(); + let schema_catalog = unchanged_schema_catalog(&catalog); + let mut validate = || Ok(()); + let error = apply_governed_migrations( + &mut connection, + &catalog, + &schema_catalog, + MigrationAppliedAtUnixSeconds::new(1_800_000_000).unwrap(), + &build_identity(), + &[], + &mut validate, + ) + .await + .expect_err("statement-control migration must be rejected"); + assert_eq!(error.kind(), ServiceSqliteErrorKind::Migration); + assert_eq!(read_state_schema_version(&mut connection).await.unwrap(), 1); + assert!( + read_migration_history(&mut connection) + .await + .unwrap() + .is_empty() + ); + assert_eq!( + sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name = 'leaked'", + ) + .fetch_one(&mut connection) + .await + .unwrap(), + 0 + ); + } + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[tokio::test(flavor = "current_thread")] async fn migration_executor_rejects_transient_attachment_before_file_creation() { let directory = tempfile::tempdir().unwrap(); let database_path = directory.path().join("main.sqlite"); @@ -2434,7 +2509,9 @@ mod tests { ) .into_boxed_str(), ); - *IGNORED_ATTACHMENT_SQL.lock().expect("attachment SQL mutex") = Some(callback_sql); + *IGNORED_STATEMENT_CONTROL_SQL + .lock() + .expect("statement-control SQL mutex") = Some(callback_sql); let mut callback_connection = initialized_file_database(&callback_database_path).await; let callback_descriptor = MigrationDescriptor::callback( 2, @@ -2447,7 +2524,7 @@ mod tests { callback_descriptor.target_version(), callback_descriptor.name(), callback_descriptor.checksum(), - ignored_attachment_callback, + ignored_statement_control_callback, ); let callback_catalog = MigrationCatalog::new([callback_descriptor]).unwrap(); let callback_schema_catalog = unchanged_schema_catalog(&callback_catalog); @@ -2462,7 +2539,9 @@ mod tests { ) .await .expect_err("ignored callback ATTACH/DETACH must still fail the migration"); - *IGNORED_ATTACHMENT_SQL.lock().expect("attachment SQL mutex") = None; + *IGNORED_STATEMENT_CONTROL_SQL + .lock() + .expect("statement-control SQL mutex") = None; assert_eq!(callback_error.kind(), ServiceSqliteErrorKind::Migration); assert!(!callback_external_path.exists()); assert_eq!( diff --git a/crates/service_sqlite/src/statement_policy.rs b/crates/service_sqlite/src/statement_policy.rs @@ -0,0 +1,207 @@ +//! Private service-controlled SQL admission policy. + +#[derive(Clone, Copy)] +enum CreatePrefix { + None, + Create, + CreateTemp, + Other, +} + +pub(crate) fn contains_forbidden_statement_control(sql: &str) -> bool { + let bytes = sql.as_bytes(); + let mut cursor = 0; + let mut statement_start = true; + let mut create_prefix = CreatePrefix::None; + let mut trigger_definition = false; + let mut trigger_body = false; + let mut trigger_case_depth = 0_u32; + let mut trigger_end_seen = false; + + while cursor < bytes.len() { + let byte = bytes[cursor]; + if statement_start && bytes[cursor..].starts_with(&[0xef, 0xbb, 0xbf]) { + cursor += 3; + continue; + } + if byte.is_ascii_whitespace() { + cursor += 1; + continue; + } + if byte == b'-' && bytes.get(cursor + 1) == Some(&b'-') { + cursor += 2; + while cursor < bytes.len() && !matches!(bytes[cursor], b'\n' | b'\r') { + cursor += 1; + } + continue; + } + if byte == b'/' && bytes.get(cursor + 1) == Some(&b'*') { + cursor += 2; + while cursor + 1 < bytes.len() && !(bytes[cursor] == b'*' && bytes[cursor + 1] == b'/') + { + cursor += 1; + } + cursor = bytes.len().min(cursor + 2); + continue; + } + if matches!(byte, b'\'' | b'"' | b'`' | b'[') { + let terminator = if byte == b'[' { b']' } else { byte }; + statement_start = false; + create_prefix = CreatePrefix::Other; + cursor += 1; + while cursor < bytes.len() { + if bytes[cursor] == terminator { + if bytes.get(cursor + 1) == Some(&terminator) { + cursor += 2; + } else { + cursor += 1; + break; + } + } else { + cursor += 1; + } + } + continue; + } + if byte == b';' { + if trigger_definition && trigger_body && !trigger_end_seen { + cursor += 1; + continue; + } + statement_start = true; + create_prefix = CreatePrefix::None; + trigger_definition = false; + trigger_body = false; + trigger_case_depth = 0; + trigger_end_seen = false; + cursor += 1; + continue; + } + if !is_identifier(byte) { + statement_start = false; + create_prefix = CreatePrefix::Other; + cursor += 1; + continue; + } + + let start = cursor; + cursor += 1; + while cursor < bytes.len() && is_identifier(bytes[cursor]) { + cursor += 1; + } + let token = &bytes[start..cursor]; + + if trigger_definition { + if !trigger_body && token.eq_ignore_ascii_case(b"begin") { + trigger_body = true; + } else if trigger_body && token.eq_ignore_ascii_case(b"case") { + trigger_case_depth = trigger_case_depth.saturating_add(1); + } else if trigger_body && token.eq_ignore_ascii_case(b"end") { + if trigger_case_depth == 0 { + trigger_end_seen = true; + } else { + trigger_case_depth -= 1; + } + } + continue; + } + + if statement_start { + if is_forbidden(token) { + return true; + } + statement_start = false; + create_prefix = if token.eq_ignore_ascii_case(b"create") { + CreatePrefix::Create + } else { + CreatePrefix::Other + }; + continue; + } + + create_prefix = match create_prefix { + CreatePrefix::Create + if token.eq_ignore_ascii_case(b"temp") + || token.eq_ignore_ascii_case(b"temporary") => + { + CreatePrefix::CreateTemp + } + CreatePrefix::Create | CreatePrefix::CreateTemp + if token.eq_ignore_ascii_case(b"trigger") => + { + trigger_definition = true; + CreatePrefix::None + } + CreatePrefix::Create | CreatePrefix::CreateTemp => CreatePrefix::Other, + other => other, + }; + } + + false +} + +fn is_identifier(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} + +fn is_forbidden(token: &[u8]) -> bool { + [ + b"pragma".as_slice(), + b"attach", + b"detach", + b"begin", + b"commit", + b"end", + b"rollback", + b"savepoint", + b"release", + ] + .into_iter() + .any(|forbidden| token.eq_ignore_ascii_case(forbidden)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn statement_control_inventory_is_closed_and_case_insensitive() { + for forbidden in [ + "/* ignored before control */ PRAGMA trusted_schema = ON", + "\u{feff}PRAGMA trusted_schema = ON", + "ATTACH DATABASE 'x' AS extra", + "detach database extra", + " /* ignored */ BeGiN IMMEDIATE", + "SELECT 1; -- ignored\n CoMmIt", + "END TRANSACTION", + "ROLLBACK TO escaped", + "SAVEPOINT escaped", + "RELEASE SAVEPOINT escaped", + "CREATE TRIGGER audit_insert AFTER INSERT ON items BEGIN INSERT INTO audit_log (value) VALUES (NEW.value); END; /* after trigger */ COMMIT", + ] { + assert!(contains_forbidden_statement_control(forbidden)); + } + } + + #[test] + fn values_identifiers_comments_case_and_triggers_remain_available() { + for allowed in [ + "SELECT 'pragma attach detach begin commit end rollback savepoint release'", + "SELECT CASE WHEN value = 1 THEN 'commit' ELSE 'end' END FROM items", + "SELECT 1 /* PRAGMA ATTACH COMMIT */", + "CREATE TRIGGER audit_insert AFTER INSERT ON items BEGIN INSERT INTO audit_log (value) VALUES (CASE WHEN NEW.value = 1 THEN 'commit' ELSE 'end' END); END;", + "SELECT pragmatic FROM items", + "SELECT attachment FROM items", + "SELECT detached FROM items", + "SELECT beginner FROM items", + "SELECT committed FROM items", + "SELECT ending FROM items", + "SELECT rolled_back FROM items", + "SELECT savepoints FROM items", + "SELECT released FROM items", + "SELECT COUNT(*) FROM host_probe", + ] { + assert!(!contains_forbidden_statement_control(allowed)); + } + } +} diff --git a/crates/service_sqlite/tests/package_boundary.rs b/crates/service_sqlite/tests/package_boundary.rs @@ -33,6 +33,7 @@ const RESTORE_STAGE_SOURCE: &str = include_str!("../src/restore/stage.rs"); const SQLITE_NATIVE_BACKUP_SOURCE: &str = include_str!("../src/sqlite_native_backup.rs"); const STATUS_SOURCE: &str = include_str!("../src/status/mod.rs"); const DISK_SOURCE: &str = include_str!("../src/status/disk.rs"); +const STATEMENT_POLICY_SOURCE: &str = include_str!("../src/statement_policy.rs"); const TRANSACTION_CONTROL_SOURCE: &str = include_str!("../src/transaction_control.rs"); const WRITER_PROCESS_TEST_SOURCE: &str = include_str!("writer_authority.rs"); @@ -129,6 +130,7 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "restore", "sqlite_native_backup", "status", + "statement_policy", "transaction_control" ]) ); @@ -138,6 +140,11 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "borrowed `ServiceSqliteTransaction` executor", "transaction begin, commit, rollback, policy", "attached-database exclusion", + "Service-controlled SQL is screened before SQLite compilation", + "statement-control inventory is `PRAGMA`, `ATTACH`, `DETACH`, `BEGIN`, `COMMIT`,", + "`END`, `ROLLBACK`, `SAVEPOINT`, and `RELEASE`", + "sticky for the transaction", + "is revalidated before commit and before a connection can return to the pool", "Writable host opening finishes every pending governed migration", "read-only inspection opens only current migration and", "with raw database authority", @@ -786,8 +793,8 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "validate_callback_bindings", "advance_schema_version", "pub struct MigrationTransactionExecutor", - "contains_database_control", - "database_control_rejected", + "contains_forbidden_statement_control", + "statement_control_rejected", "SAVEPOINT radroots_migration_transaction_probe", "FROM pragma_database_list", "CASE WHEN typeof(name) = 'text'", @@ -812,8 +819,8 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "connection.close_on_drop()", "connection.trust()", "RestrictedExecute", - "contains_database_control", - "RADROOTS_FORBIDDEN_DATABASE_CONTROL", + "contains_forbidden_statement_control", + "RADROOTS_FORBIDDEN_STATEMENT_CONTROL", "OperationRolledBack", "RollbackFailed", "CommitOutcomeUnknown", @@ -833,6 +840,26 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { } for required in [ + "pub(crate) fn contains_forbidden_statement_control", + "b\"pragma\".as_slice()", + "b\"attach\"", + "b\"detach\"", + "b\"begin\"", + "b\"commit\"", + "b\"end\"", + "b\"rollback\"", + "b\"savepoint\"", + "b\"release\"", + "trigger_definition", + "trigger_case_depth", + ] { + assert!( + STATEMENT_POLICY_SOURCE.contains(required), + "statement-policy source is missing `{required}`" + ); + } + + for required in [ "set_commit_hook", "set_rollback_hook", "permit_outer_commit",