Describe the bug
Store::append (crates/file_store/src/store.rs) serializes the changeset at the file handle's current cursor. The file is opened without append mode and append neither seeks to the end nor takes a lock, so the cursor of each Store reflects the end of the file at the time that handle was loaded. When two handles for the same file exist (e.g. two processes, or a second Store::load while the first is still in use), an append through one handle is overwritten by a later append through the other. If the entries have equal size the file stays fully decodable, so the earlier changeset is lost silently.
#2258 appears to address this by seeking to the end of the file before each write.
This issue was found by AI.
To Reproduce
Add crates/file_store/tests/test_stale_handle.rs and run cargo test -p bdk_file_store --test test_stale_handle:
use bdk_file_store::Store;
use std::collections::BTreeSet;
const MAGIC: &[u8] = b"bdk_test_magic";
type ChangeSet = BTreeSet<String>;
#[test]
fn append_through_second_handle_keeps_earlier_append() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("db");
let mut first = Store::<ChangeSet>::create(MAGIC, &path).unwrap();
first.append(&ChangeSet::from(["initial".to_string()])).unwrap();
// Second handle opened while the file ends after "initial".
let (mut second, _) = Store::<ChangeSet>::load(MAGIC, &path).unwrap();
first.append(&ChangeSet::from(["first".to_string()])).unwrap();
second.append(&ChangeSet::from(["other".to_string()])).unwrap();
drop((first, second));
let (_, recovered) = Store::<ChangeSet>::load(MAGIC, &path).unwrap();
let expected = ChangeSet::from(["initial".into(), "first".into(), "other".into()]);
assert_eq!(recovered, Some(expected));
}
Current output:
assertion `left == right` failed
left: Some({"initial", "other"})
right: Some({"first", "initial", "other"})
Expected behavior
A changeset that was successfully appended should not be lost by a later append through another handle to the same file.
Describe the bug
Store::append(crates/file_store/src/store.rs) serializes the changeset at the file handle's current cursor. The file is opened without append mode andappendneither seeks to the end nor takes a lock, so the cursor of eachStorereflects the end of the file at the time that handle was loaded. When two handles for the same file exist (e.g. two processes, or a secondStore::loadwhile the first is still in use), an append through one handle is overwritten by a later append through the other. If the entries have equal size the file stays fully decodable, so the earlier changeset is lost silently.#2258 appears to address this by seeking to the end of the file before each write.
This issue was found by AI.
To Reproduce
Add
crates/file_store/tests/test_stale_handle.rsand runcargo test -p bdk_file_store --test test_stale_handle:Current output:
Expected behavior
A changeset that was successfully appended should not be lost by a later append through another handle to the same file.