From 2250db32c3abad231ee3a46ffdf643308174cd31 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 16 Sep 2026 10:13:34 +0800 Subject: [PATCH 1/2] fix(sync): copy dynamic SQLite control arguments --- powersync/src/sync/download/sync_iteration.rs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/powersync/src/sync/download/sync_iteration.rs b/powersync/src/sync/download/sync_iteration.rs index 20d6785..2d240f5 100644 --- a/powersync/src/sync/download/sync_iteration.rs +++ b/powersync/src/sync/download/sync_iteration.rs @@ -220,22 +220,47 @@ enum PowerSyncControlArgument { impl PowerSyncControlArgument { fn bind_to(&self, stmt: &ManagedStmt, index: i32) -> Result<(), ResultCode> { - // We use Destructor::STATIC here which is technically not safe, but fine since we'll always - // drop the statement before the control argument. match self { PowerSyncControlArgument::Null => stmt.bind_null(index), PowerSyncControlArgument::StaticString(str) => { stmt.bind_text(index, str, Destructor::STATIC) } - PowerSyncControlArgument::String(str) => stmt.bind_text(index, str, Destructor::STATIC), + PowerSyncControlArgument::String(str) => { + stmt.bind_text(index, str, Destructor::TRANSIENT) + } PowerSyncControlArgument::Bytes(bytes) => { - stmt.bind_blob(index, bytes, Destructor::STATIC) + stmt.bind_blob(index, bytes, Destructor::TRANSIENT) } }?; Ok(()) } } +#[cfg(all(test, feature = "rusqlite"))] +mod tests { + use std::hint::black_box; + + use super::*; + + #[test] + fn binding_dynamic_control_arguments_copies_the_payload() { + let connection = rusqlite::Connection::open_in_memory().unwrap(); + let connection = SqliteConnection::from(connection); + let stmt = connection.prepare("SELECT ?").unwrap(); + let expected = "a".repeat(4096); + + PowerSyncControlArgument::String(expected.clone()) + .bind_to(&stmt, 1) + .unwrap(); + + let overwrite = "b".repeat(4096); + black_box(&overwrite); + + assert_eq!(stmt.step().unwrap(), ResultCode::ROW); + assert_eq!(stmt.column_text(0).unwrap(), expected); + } +} + #[derive(Debug, Serialize)] pub struct StartDownloadIteration { pub parameters: serde_json::Value, From b0c1606ccfdd1764aa056606e04800d97ca435ca Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 16 Sep 2026 16:19:43 +0800 Subject: [PATCH 2/2] fix(sync): scope SQLite control argument lifetime --- powersync/src/sync/download/sync_iteration.rs | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/powersync/src/sync/download/sync_iteration.rs b/powersync/src/sync/download/sync_iteration.rs index 2d240f5..ebc6537 100644 --- a/powersync/src/sync/download/sync_iteration.rs +++ b/powersync/src/sync/download/sync_iteration.rs @@ -193,9 +193,10 @@ impl DownloadEvent { let (op, arg) = self.into_powersync_control_argument(); stmt.bind_text(1, op, Destructor::STATIC)?; - arg.bind_to(&stmt, 2)?; + // SAFETY: `arg` remains alive until after `stmt` is explicitly dropped below. + unsafe { arg.bind_to(&stmt, 2)? }; - if let ResultCode::ROW = stmt.step()? { + let instructions = if let ResultCode::ROW = stmt.step()? { let instructions = stmt.column_text(0).map_err(|_| { PowerSyncError::argument_error("Could not read powersync_control instructions") })?; @@ -203,7 +204,10 @@ impl DownloadEvent { serde_json::from_str(instructions)? } else { panic!("Expected a row") // Can't happen, scalar select - } + }; + + drop(stmt); + instructions }; tx.commit()?; @@ -219,48 +223,24 @@ enum PowerSyncControlArgument { } impl PowerSyncControlArgument { - fn bind_to(&self, stmt: &ManagedStmt, index: i32) -> Result<(), ResultCode> { + /// # Safety + /// + /// The argument must outlive `stmt`. + unsafe fn bind_to(&self, stmt: &ManagedStmt, index: i32) -> Result<(), ResultCode> { match self { PowerSyncControlArgument::Null => stmt.bind_null(index), PowerSyncControlArgument::StaticString(str) => { stmt.bind_text(index, str, Destructor::STATIC) } - PowerSyncControlArgument::String(str) => { - stmt.bind_text(index, str, Destructor::TRANSIENT) - } + PowerSyncControlArgument::String(str) => stmt.bind_text(index, str, Destructor::STATIC), PowerSyncControlArgument::Bytes(bytes) => { - stmt.bind_blob(index, bytes, Destructor::TRANSIENT) + stmt.bind_blob(index, bytes, Destructor::STATIC) } }?; Ok(()) } } -#[cfg(all(test, feature = "rusqlite"))] -mod tests { - use std::hint::black_box; - - use super::*; - - #[test] - fn binding_dynamic_control_arguments_copies_the_payload() { - let connection = rusqlite::Connection::open_in_memory().unwrap(); - let connection = SqliteConnection::from(connection); - let stmt = connection.prepare("SELECT ?").unwrap(); - let expected = "a".repeat(4096); - - PowerSyncControlArgument::String(expected.clone()) - .bind_to(&stmt, 1) - .unwrap(); - - let overwrite = "b".repeat(4096); - black_box(&overwrite); - - assert_eq!(stmt.step().unwrap(), ResultCode::ROW); - assert_eq!(stmt.column_text(0).unwrap(), expected); - } -} - #[derive(Debug, Serialize)] pub struct StartDownloadIteration { pub parameters: serde_json::Value,