Skip to content
Closed
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
20 changes: 19 additions & 1 deletion crates/file_store/src/entry_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down
43 changes: 43 additions & 0 deletions crates/file_store/tests/test_zero_width.rs
Original file line number Diff line number Diff line change
@@ -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");
}