From febaa7569385eda4c74ddbf7710414847c8b6c28 Mon Sep 17 00:00:00 2001 From: Paulo Cabral Sanz Date: Fri, 14 Aug 2026 21:13:11 -0300 Subject: [PATCH 1/2] test: crash after VALUE_SEP then inline must not leak the blob apply_record only writes the valsep sidecar on VALUE_SEP, so an inline overwrite leaves the hash. Reopen then sweep_orphans keeps the blob. This fails on current main. --- crates/engine/src/log/mod.rs | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/crates/engine/src/log/mod.rs b/crates/engine/src/log/mod.rs index 4a73c8d..dc08089 100644 --- a/crates/engine/src/log/mod.rs +++ b/crates/engine/src/log/mod.rs @@ -1882,6 +1882,56 @@ mod value_sep_tests { assert_eq!(log.values.blob_count(), 1, "only v2's blob remains"); }); } + + /// Crash after VALUE_SEP then inline overwrite: both records are in the + /// unsealed active file. Replay must clear the sidecar the same way the + /// live write path does, or `sweep_orphans` keeps the large blob and the + /// next seal writes the stale hash. + #[test] + fn crash_after_large_then_small_must_not_leak_blob() { + run(async { + let dir = TempDir::new().unwrap(); + let path = dir.path().to_path_buf(); + let cfg = LogConfig { + rotate_threshold: 1 << 30, + fanout: 8, + value_sep_threshold: 4096, + }; + { + let log = NamespaceLog::open(path.clone(), cfg).await.unwrap(); + let big = vec![0xABu8; 64 * 1024]; + log.put_full(key(0), &big, &[], None).await.unwrap(); + log.put_full(key(0), b"inline-after-large", &[], None) + .await + .unwrap(); + assert_eq!( + log.index.borrow().valsep(b"k00000"), + None, + "live path clears valsep on inline overwrite" + ); + assert_eq!(log.values.blob_count(), 0); + } + + let log = NamespaceLog::open(path, cfg).await.unwrap(); + let entry = *log + .index + .borrow() + .get(b"k00000") + .expect("inline key must survive reopen"); + let (v, _) = log.read_value(entry).await.unwrap(); + assert_eq!(&v[..], b"inline-after-large"); + assert_eq!( + log.index.borrow().valsep(b"k00000"), + None, + "replay must clear valsep on inline overwrite" + ); + assert_eq!( + log.values.blob_count(), + 0, + "stale VALUE_SEP blob must be swept" + ); + }); + } } #[cfg(test)] From 55c509cf578a8ebaa88a03b8e40dffa70f469869 Mon Sep 17 00:00:00 2001 From: Paulo Cabral Sanz Date: Fri, 14 Aug 2026 21:17:40 -0300 Subject: [PATCH 2/2] fix: apply_record clears valsep on inline overwrite Match the live write path: a full inline record sets valsep to None so crash replay + sweep_orphans does not resurrect the previous VALUE_SEP blob. --- crates/engine/src/log/recover.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/engine/src/log/recover.rs b/crates/engine/src/log/recover.rs index 11a4e75..34425af 100644 --- a/crates/engine/src/log/recover.rs +++ b/crates/engine/src/log/recover.rs @@ -276,5 +276,10 @@ fn apply_record( "value-separated record without a 16-byte hash; ignoring sidecar entry" ); } + } else { + // Match the live write path (`apply_valsep_insert`): an inline + // overwrite must drop the previous VALUE_SEP hash so reopen + + // sweep_orphans does not resurrect the blob. + index.set_valsep(&key_bytes, None); } }