diff --git a/crates/file_store/src/entry_iter.rs b/crates/file_store/src/entry_iter.rs index 8b284f1814..0ad58f933b 100644 --- a/crates/file_store/src/entry_iter.rs +++ b/crates/file_store/src/entry_iter.rs @@ -50,7 +50,34 @@ where } let pos_before_read = self.db_file.stream_position()?; - match bincode_options().deserialize_from(&mut self.db_file) { + // An entry can never be larger than the bytes remaining in the file. Bounding the + // deserializer by that amount makes a corrupt/oversized length prefix fail with + // `bincode::ErrorKind::SizeLimit` (mapped to `StoreError::Bincode` below) instead of + // attempting an unbounded allocation and panicking with `capacity overflow`. + // + // The factor of 9 accounts for how bincode charges reads: every length literal it + // decodes is charged a full `size_of::()` (8 bytes) even when the varint + // encoding occupies one byte on disk, and the payload length is charged again. A + // valid entry therefore charges at most `8 * bytes + bytes` and this bound can + // never reject a store that fits in the file. + let remaining = self + .db_file + .get_ref() + .metadata()? + .len() + .saturating_sub(pos_before_read); + // At end of file there is nothing left to decode; terminate cleanly without + // invoking the deserializer (with a zero limit it would report `SizeLimit` + // instead of `UnexpectedEof`, breaking the clean-EOF path below). + if remaining == 0 { + self.finished = true; + return Ok(None); + } + let limit = remaining.saturating_mul(9); + match bincode_options() + .with_limit(limit) + .deserialize_from(&mut self.db_file) + { Ok(changeset) => Ok(Some(changeset)), Err(e) => { self.finished = true; diff --git a/crates/file_store/tests/test_oversized_entry_len.rs b/crates/file_store/tests/test_oversized_entry_len.rs new file mode 100644 index 0000000000..76bdda0b4d --- /dev/null +++ b/crates/file_store/tests/test_oversized_entry_len.rs @@ -0,0 +1,43 @@ +use bdk_file_store::Store; +use std::collections::BTreeSet; + +const MAGIC: &[u8] = b"bdk_test_magic"; + +#[test] +fn load_returns_error_on_oversized_length_prefix() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("db"); + + let mut bytes = MAGIC.to_vec(); + bytes.push(1); // BTreeSet length: 1 element + bytes.push(253); // bincode varint tag: u64 follows + bytes.extend_from_slice(&u64::MAX.to_le_bytes()); // String length: u64::MAX + std::fs::write(&path, &bytes).unwrap(); + + let result = Store::>::load(MAGIC, &path); + assert!(result.is_err(), "load should fail with an error, not panic"); +} + +#[test] +fn valid_store_still_round_trips() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("db"); + + let mut store = Store::>::create(MAGIC, &path).unwrap(); + store + .append(&BTreeSet::from(["initial".to_string()])) + .unwrap(); + store + .append(&BTreeSet::from(["second".to_string()])) + .unwrap(); + drop(store); + + let (_, recovered) = Store::>::load(MAGIC, &path).unwrap(); + assert_eq!( + recovered, + Some(BTreeSet::from([ + "initial".to_string(), + "second".to_string() + ])) + ); +}