From 9b2961d1ed80c889d7a2855690a25f7b11defa54 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Thu, 4 Jun 2026 19:35:42 +0800 Subject: [PATCH 1/3] fix: memory leak in dropping uninserted guard --- src/shard.rs | 1 + src/sync.rs | 19 +++++++++++++++++++ src/sync_placeholder.rs | 7 +++++++ 3 files changed, 27 insertions(+) diff --git a/src/shard.rs b/src/shard.rs index bf01c02..e4a6ec0 100644 --- a/src/shard.rs +++ b/src/shard.rs @@ -224,6 +224,7 @@ impl CacheShard Lifecycle for DefaultLifecycle { #[cfg(test)] mod tests { use super::*; + use crate::shard::SharedPlaceholder as _; use std::{ sync::{Arc, Barrier}, thread, @@ -1763,4 +1764,22 @@ mod tests { drop(guards); assert_eq!(cache.get(&2), None); } + + #[test] + fn test_guard_leak() { + let cache: Cache = Cache::new(8); + let guard1 = match cache.get_value_or_guard(&1, None) { + GuardResult::Guard(g) => g, + _ => panic!("expected guard"), + }; + let idx1 = guard1.shared().idx(); + drop(guard1); + let guard2 = match cache.get_value_or_guard(&1, None) { + GuardResult::Guard(g) => g, + _ => panic!("expected guard"), + }; + let idx2 = guard2.shared().idx(); + drop(guard2); + assert_eq!(idx1, idx2); + } } diff --git a/src/sync_placeholder.rs b/src/sync_placeholder.rs index 8f09d11..5da45e2 100644 --- a/src/sync_placeholder.rs +++ b/src/sync_placeholder.rs @@ -93,6 +93,13 @@ pub struct PlaceholderGuard<'a, Key, Val, We, B, L> { inserted: bool, } +#[cfg(test)] +impl<'a, Key, Val, We, B, L> PlaceholderGuard<'a, Key, Val, We, B, L> { + pub fn shared(&self) -> &SharedPlaceholder { + &self.shared + } +} + #[derive(Debug)] enum Waiter { Thread { From 458cc4ceb5e2d3edcd26da2660aac7dcd938a0ef Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Thu, 4 Jun 2026 23:50:57 +0800 Subject: [PATCH 2/3] fix --- src/shard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shard.rs b/src/shard.rs index e4a6ec0..c493371 100644 --- a/src/shard.rs +++ b/src/shard.rs @@ -223,8 +223,8 @@ impl CacheShard Date: Thu, 4 Jun 2026 22:47:50 +0200 Subject: [PATCH 3/3] Add regression tests for placeholder slot reuse on guard drop Cover the two documented cases where a placeholder is removed/replaced while a guard is still outstanding (overwrite insert, and remove + slot reuse). Both panicked under the original unconditional entries.remove and pass with the fix that only frees the slot when the placeholder is still present. --- src/sync.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/sync.rs b/src/sync.rs index 405b91d..8810ec2 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1782,4 +1782,37 @@ mod tests { drop(guard2); assert_eq!(idx1, idx2); } + + // A real insert overwrites the placeholder in place, reusing its slab slot as + // a Resident. Dropping the now-stale guard must not free that slot, otherwise + // the live entry is evicted while the map still references it. + #[test] + fn test_guard_drop_after_overwrite_insert() { + let cache: Cache = Cache::new(8); + let guard = match cache.get_value_or_guard(&1, None) { + GuardResult::Guard(g) => g, + _ => panic!("expected guard"), + }; + cache.insert(1, 100); + assert_eq!(cache.get(&1), Some(100)); + drop(guard); + assert_eq!(cache.get(&1), Some(100)); + } + + // A remove frees the placeholder's slab slot, which a later insert reuses for a + // different key. Dropping the original guard must not free that slot again, or + // it evicts the unrelated key. + #[test] + fn test_guard_drop_after_remove_and_reuse() { + let cache: Cache = Cache::new(8); + let guard = match cache.get_value_or_guard(&1, None) { + GuardResult::Guard(g) => g, + _ => panic!("expected guard"), + }; + cache.remove(&1); + cache.insert(2, 222); + assert_eq!(cache.get(&2), Some(222)); + drop(guard); + assert_eq!(cache.get(&2), Some(222)); + } }