diff --git a/crates/file_store/src/entry_iter.rs b/crates/file_store/src/entry_iter.rs index 8b284f1814..ac6a789767 100644 --- a/crates/file_store/src/entry_iter.rs +++ b/crates/file_store/src/entry_iter.rs @@ -51,7 +51,25 @@ where let pos_before_read = self.db_file.stream_position()?; match bincode_options().deserialize_from(&mut self.db_file) { - Ok(changeset) => Ok(Some(changeset)), + Ok(changeset) => { + let pos_after_read = self.db_file.stream_position()?; + if pos_after_read == pos_before_read { + // The deserialized type consumed zero bytes (e.g. `()`). + // The file offset will never advance, so stop iterating + // to avoid an infinite loop. + // + // This only covers a changeset that itself decodes to zero + // bytes. A collection of zero-sized elements with a corrupt + // length prefix (e.g. `Vec<()>`) loops inside the decoder + // and never returns here, so it is not caught by this check + // or by a byte-counting size limit. `postcard` has the same + // hole, so the migration tracked in #2258 does not remove + // the need for this check. + self.finished = true; + return Ok(None); + } + Ok(Some(changeset)) + } Err(e) => { self.finished = true; let pos_after_read = self.db_file.stream_position()?; diff --git a/crates/file_store/tests/test_zero_width.rs b/crates/file_store/tests/test_zero_width.rs new file mode 100644 index 0000000000..a8e6803a3f --- /dev/null +++ b/crates/file_store/tests/test_zero_width.rs @@ -0,0 +1,43 @@ +use bdk_file_store::Store; +use std::time::Duration; + +const MAGIC: &[u8] = b"bdk_test_magic"; + +#[test] +fn load_terminates_on_zero_width_changeset() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("db"); + let mut bytes = MAGIC.to_vec(); + bytes.push(0xff); // any trailing byte + std::fs::write(&path, &bytes).unwrap(); + + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let result = Store::<()>::load(MAGIC, &path); + let _ = tx.send(result.is_ok()); + }); + // `()` decodes as zero bytes, so the file offset never advances and `load` never reaches EOF. + assert!( + rx.recv_timeout(Duration::from_secs(5)).is_ok(), + "load did not terminate" + ); +} + +#[test] +fn load_terminates_on_empty_zero_width_store() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("db"); + // No entries at all, just the magic bytes. `()` still decodes at EOF, so this hangs + // without the offset check even though the file is well formed. + std::fs::write(&path, MAGIC).unwrap(); + + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let _ = tx.send(Store::<()>::load(MAGIC, &path).map(|(_, changeset)| changeset)); + }); + let changeset = rx + .recv_timeout(Duration::from_secs(5)) + .expect("load did not terminate") + .expect("an empty store is not an error"); + assert!(changeset.is_none(), "an empty store has no changeset"); +}