From d7684e82b2f716201f115376e2829a203c8a7070 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 18:56:05 +0000 Subject: [PATCH] Fix panic on out-of-bounds SSTable sparse index offsets A corrupt or stale .meta file (or a truncated .tbl after a partial write) could contain index offsets past the end of the data file. SsTable::get sliced the mmap/raw buffer with that offset directly, panicking and crashing the server on the next read of the affected key. Treat an out-of-range offset as a stale index and fall back to scanning the table from the start, so lookups stay correct without panicking. https://claude.ai/code/session_01CJXWjB9ERGgV6B9mRqrdM9 --- src/sstable.rs | 11 +++++++--- tests/sstable_test.rs | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/sstable.rs b/src/sstable.rs index 3890a81..d865d05 100644 --- a/src/sstable.rs +++ b/src/sstable.rs @@ -181,13 +181,17 @@ impl SsTable { if !self.zone_map.contains(key) || !self.bloom.may_contain(key) { return Ok(None); } - let offset = self.seek_offset(key); + let offset = self.seek_offset(key) as usize; if let Some(root) = storage.local_path() { let path = root.join(&self.path); let file = File::open(path)?; // memory-map the file for efficient slicing let mmap = unsafe { MmapOptions::new().map(&file)? }; - if let Some(enc) = Self::scan_from(&mmap[offset as usize..], key) { + // The sparse index comes from a separately stored meta file; an + // offset beyond the data file means the index is stale or + // corrupt, so fall back to scanning the whole table. + let slice = mmap.get(offset..).unwrap_or(&mmap[..]); + if let Some(enc) = Self::scan_from(slice, key) { let val = STANDARD .decode(enc) .map_err(std::io::Error::other)?; @@ -197,7 +201,8 @@ impl SsTable { } // fallback to reading entire file from storage let raw = storage.get(&self.path).await?; - if let Some(enc) = Self::scan_from(&raw[offset as usize..], key) { + let slice = raw.get(offset..).unwrap_or(&raw[..]); + if let Some(enc) = Self::scan_from(slice, key) { let val = STANDARD .decode(enc) .map_err(std::io::Error::other)?; diff --git a/tests/sstable_test.rs b/tests/sstable_test.rs index b3fa8b1..a8d753e 100644 --- a/tests/sstable_test.rs +++ b/tests/sstable_test.rs @@ -23,3 +23,51 @@ async fn sstable_roundtrip() { .collect(); assert_eq!(keys, vec!["a", "b", "c"]); } + +#[tokio::test] +async fn out_of_bounds_index_offset_does_not_panic() { + let dir = tempfile::tempdir().unwrap(); + let storage = LocalStorage::new(dir.path()); + let entries = vec![ + ("a".to_string(), b"1".to_vec()), + ("b".to_string(), b"2".to_vec()), + ]; + let mut table = SsTable::create("table", &entries, &storage).await.unwrap(); + // Simulate a corrupt/stale sparse index pointing past the end of the + // data file. Lookups must not panic and must still find existing keys + // by falling back to a full scan. + table.index = vec![("a".to_string(), u64::MAX), ("b".to_string(), 1 << 40)]; + assert_eq!(table.get("a", &storage).await.unwrap(), Some(b"1".to_vec())); + assert_eq!(table.get("b", &storage).await.unwrap(), Some(b"2".to_vec())); + assert_eq!(table.get("zz", &storage).await.unwrap(), None); +} + +#[tokio::test] +async fn out_of_bounds_index_offset_remote_storage_does_not_panic() { + struct NoLocal(LocalStorage); + + #[async_trait::async_trait] + impl Storage for NoLocal { + async fn put(&self, path: &str, data: Vec) -> Result<(), cass::storage::StorageError> { + self.0.put(path, data).await + } + async fn get(&self, path: &str) -> Result, cass::storage::StorageError> { + self.0.get(path).await + } + async fn append(&self, path: &str, data: &[u8]) -> Result<(), cass::storage::StorageError> { + self.0.append(path, data).await + } + async fn list(&self, prefix: &str) -> Result, cass::storage::StorageError> { + self.0.list(prefix).await + } + } + + let dir = tempfile::tempdir().unwrap(); + // Hide the local path so `get` exercises the remote-storage fallback. + let storage = NoLocal(LocalStorage::new(dir.path())); + let entries = vec![("a".to_string(), b"1".to_vec())]; + let mut table = SsTable::create("table", &entries, &storage).await.unwrap(); + table.index = vec![("a".to_string(), u64::MAX)]; + assert_eq!(table.get("a", &storage).await.unwrap(), Some(b"1".to_vec())); + assert_eq!(table.get("zz", &storage).await.unwrap(), None); +}