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
29 changes: 28 additions & 1 deletion crates/file_store/src/entry_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u64>()` (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;
Expand Down
43 changes: 43 additions & 0 deletions crates/file_store/tests/test_oversized_entry_len.rs
Original file line number Diff line number Diff line change
@@ -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::<BTreeSet<String>>::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::<BTreeSet<String>>::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::<BTreeSet<String>>::load(MAGIC, &path).unwrap();
assert_eq!(
recovered,
Some(BTreeSet::from([
"initial".to_string(),
"second".to_string()
]))
);
}