Skip to content

Commit d7c4856

Browse files
committed
fix(catalog): use scan_prefix instead of 0xFF-bounded range scans
Prefix scans over catalog rows built their end bound by appending a single 0xFF byte to the start key. For a realm or segment id whose first byte is itself 0xFF, that end bound sorts before (or equal to) some in-range keys, silently dropping rows from segment listing, stats, and rekey migration. One such migration failed with an unverifiable-header corruption. Replace every appended-sentinel range scan with scan_prefix, which bounds correctly regardless of key content.
1 parent ad75d83 commit d7c4856

7 files changed

Lines changed: 47 additions & 30 deletions

File tree

src/compaction/helpers.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ pub(super) async fn list_all_segments_inner<V: Vfs + Clone>(
6060
pager.page_size(),
6161
);
6262
let start = vec![CatalogRowKind::Segment as u8];
63-
let mut end = start.clone();
64-
end.push(0xFF);
65-
let rows = tree.collect_range(&start, &end).await?;
63+
let rows = tree.scan_prefix(&start).await?;
6664
let mut out = Vec::with_capacity(rows.len());
6765
for (_k, v) in rows {
6866
let meta = Catalog::decode_segment_meta(&v)?;
@@ -88,9 +86,7 @@ pub(super) async fn find_segment_name_inner<V: Vfs + Clone>(
8886
pager.page_size(),
8987
);
9088
let start = vec![CatalogRowKind::Segment as u8];
91-
let mut end = start.clone();
92-
end.push(0xFF);
93-
let rows = tree.collect_range(&start, &end).await?;
89+
let rows = tree.scan_prefix(&start).await?;
9490
for (k, v) in rows {
9591
let meta = Catalog::decode_segment_meta(&v)?;
9692
if meta.segment_id == *segment_id && k.len() > 17 {

src/txn/db/misc.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,7 @@ impl<V: Vfs + Clone> Db<V> {
161161
);
162162

163163
let seg_start = vec![CatalogRowKind::Segment as u8];
164-
let mut seg_end = seg_start.clone();
165-
seg_end.push(0xFF);
166-
let seg_rows = tree
167-
.collect_range(&seg_start, &seg_end)
168-
.await
169-
.unwrap_or_default();
164+
let seg_rows = tree.scan_prefix(&seg_start).await.unwrap_or_default();
170165
let seg_count = u32::try_from(seg_rows.len()).unwrap_or(u32::MAX);
171166
let seg_bytes: u64 = seg_rows
172167
.iter()

src/txn/db/rekey/segments.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,7 @@ impl<V: Vfs + Clone> Db<V> {
348348
}
349349
let tree = self.rekey_catalog_tree(state);
350350
let start = vec![CatalogRowKind::RekeySegmentProgress as u8];
351-
let mut end = start.clone();
352-
end.push(0xFF);
353-
let rows = tree.collect_range(&start, &end).await?;
351+
let rows = tree.scan_prefix(&start).await?;
354352
let mut out = BTreeMap::new();
355353
for (key, value) in rows {
356354
if key.len() != 17 {
@@ -371,9 +369,7 @@ impl<V: Vfs + Clone> Db<V> {
371369
}
372370
let tree = self.rekey_catalog_tree(state);
373371
let start = vec![CatalogRowKind::Segment as u8];
374-
let mut end = start.clone();
375-
end.push(0xFF);
376-
tree.collect_range(&start, &end)
372+
tree.scan_prefix(&start)
377373
.await?
378374
.into_iter()
379375
.map(|(key, value)| {

src/txn/db/segment.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ impl<V: Vfs + Clone> Db<V> {
7272
self.page_size,
7373
);
7474
let start = Catalog::segment_key(realm, prefix.as_bytes())?;
75-
let mut end = start.clone();
76-
end.push(0xFF);
77-
let rows = tree.collect_range(&start, &end).await?;
75+
let rows = tree.scan_prefix(&start).await?;
7876
let mut out = Vec::with_capacity(rows.len());
7977
for (_k, v) in rows {
8078
let meta = Catalog::decode_segment_meta(&v)?;
@@ -296,9 +294,7 @@ impl<V: Vfs + Clone> Db<V> {
296294
self.page_size,
297295
);
298296
let start = vec![CatalogRowKind::Segment as u8];
299-
let mut end = start.clone();
300-
end.push(0xFF);
301-
let rows = tree.collect_range(&start, &end).await?;
297+
let rows = tree.scan_prefix(&start).await?;
302298
let mut out = Vec::with_capacity(rows.len());
303299
for (_k, v) in rows {
304300
let meta = Catalog::decode_segment_meta(&v)?;

src/txn/read/txn.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,7 @@ impl<'db, V: Vfs + Clone> ReadTxn<'db, V> {
153153
}
154154
let tree = self.catalog_tree();
155155
let start = Catalog::segment_key(self.db.realm_id, prefix.as_bytes())?;
156-
let mut end = start.clone();
157-
end.push(0xFF);
158-
let rows = tree.collect_range(&start, &end).await?;
156+
let rows = tree.scan_prefix(&start).await?;
159157
let mut out = Vec::with_capacity(rows.len());
160158
for (_k, v) in rows {
161159
out.push(Catalog::decode_segment_meta(&v)?);

src/txn/write/txn.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,7 @@ impl<'db, V: Vfs + Clone> WriteTxn<'db, V> {
323323
let mut prefix = Vec::with_capacity(17);
324324
prefix.push(0x01u8); // CatalogRowKind::Segment
325325
prefix.extend_from_slice(&realm.0);
326-
let mut end = prefix.clone();
327-
end.push(0xFF);
328-
let rows = self.catalog_tree.collect_range(&prefix, &end).await?;
326+
let rows = self.catalog_tree.scan_prefix(&prefix).await?;
329327
let mut committed: u64 = 0;
330328
for (_, v) in rows {
331329
let meta = Catalog::decode_segment_meta(&v)?;

tests/segment_basic.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,41 @@ async fn read_txn_pins_catalog_snapshot() {
308308
let page = reader.read_page(1).await.unwrap();
309309
assert!(page.starts_with(b"v1"));
310310
}
311+
312+
/// Catalog segment rows are keyed `[0x01, realm(16), name]`. A "kind-prefix"
313+
/// scan (start `[0x01]`, as used by the live-segment stats) that bounded its end
314+
/// with an appended `0xFF` byte stopped at `[0x01, 0xFF]` (exclusive) and thus
315+
/// dropped every row of a realm whose first byte is `0xFF`. The scan must bound
316+
/// by prefix (`scan_prefix`), not by an appended sentinel. The same boundary
317+
/// defect made rekey migration of a segment whose random `segment_id` began with
318+
/// `0xFF` fail with an unverifiable-header corruption.
319+
#[tokio::test(flavor = "current_thread")]
320+
async fn segments_under_high_byte_realm_are_listed_and_counted() {
321+
let realm = RealmId::new([0xFF; 16]);
322+
let db = Db::open_internal(MemVfs::new(), [9u8; 32], PAGE, realm)
323+
.await
324+
.unwrap();
325+
326+
let mut w = db
327+
.create_segment(realm, SegmentKind::Unspecified)
328+
.await
329+
.unwrap();
330+
w.append_page(SegmentPageKind::Data, b"high-realm")
331+
.await
332+
.unwrap();
333+
let meta = w.seal().await.unwrap();
334+
{
335+
let mut t = db.begin_write().await.unwrap();
336+
t.link_segment("engine.idx", &meta).await.unwrap();
337+
t.commit().await.unwrap();
338+
}
339+
340+
let rx = db.begin_read().await.unwrap();
341+
let listed = rx.list_segments("").await.unwrap();
342+
assert_eq!(
343+
listed.len(),
344+
1,
345+
"segment hidden from list_segments under 0xFF realm"
346+
);
347+
assert_eq!(db.stats().await.unwrap().segments_live, 1);
348+
}

0 commit comments

Comments
 (0)