pydantic/monty · critical
HeapEntries::get - data already freed
Error message
HeapEntries::get - data already freed
What it means
`StableHeap::get` panics with "HeapEntries::get - data already freed" (crates/monty/src/heap/stable_heap.rs:105) when the slot at the given HeapId is within bounds but has been freed: `entry.as_ref()` returns None because `StableHeapEntry::free` took the value. `get` deliberately panics rather than expose dangling free slots, whose indices can be invalidated by subsequent `allocate` reuse.
Source
Thrown at crates/monty/src/heap/stable_heap.rs:105
#[inline]
pub fn len(&self) -> usize {
self.len.get()
}
/// Returns a shared reference to the entry at `index`.
///
/// # Panics
/// Panics if `index >= len`, or if the slot is freed.
#[inline]
#[track_caller]
pub fn get(&self, id: HeapId) -> &T {
// SAFETY: [DH] - this call panics rather than expose free slots which could be invalidated
// by calls to `.allocate()`.
let slot = unsafe { self.slot_at(id) };
let Some(entry) = slot else {
panic!("StableHeap::get - {id:?} out of bounds");
};
entry.as_ref().expect("HeapEntries::get - data already freed")
}
/// Returns a mutable reference to the entry at `index`. Entries can also be
/// freed via the returned `StableHeapEntry`'s `free` method.
///
/// Does *not* go through [`Self::entry_ptr`]: that path derives its pointer via
/// `&self` so it only carries Shared / SharedReadOnly provenance under SB/TB,
/// and dereferencing it as `&mut` is UB. Instead this method takes the safe
/// `&mut self` → `pages.get_mut()` route, producing a `&mut Option<T>` with
/// Unique provenance suitable for `StableHeapEntry::free`'s `value.take()`.
///
/// # Panics
/// Panics if `index >= len`.
#[inline]
#[track_caller]
pub fn entry(&mut self, id: HeapId) -> Option<StableHeapEntry<'_, T>> {
assert!(id.index() < self.len.get(), "StableHeap::entry - {id:?} out of bounds");
let (page_idx, slot_idx) = Self::page_slot_indices(id);View on GitHub (pinned to adc986b362)
Solutions
- Confirm the id is still live before access: `heap.entries.iter().any(|(other, _)| other == id)` (see the test helper `is_alive` at crates/monty/src/heap/mod.rs:2311).
- Audit refcounting on the failing path — the slot freed early, so look for a missing `clone_with_heap` or an extra `dec_ref`/`drop_with` on that value.
- Use `StableHeap::entry(id)` (returns Option) instead of `get` in code that must tolerate freed slots.
- If the id was captured before `collect_cycles` or a snapshot restore, re-fetch it afterwards; freed ids are never valid again.
Example fix
// before let data = heap.entries.get(id); // panics if freed // after assert!(is_alive(&heap, id), "entry was freed"); let data = heap.entries.get(id);
Defensive patterns
Strategy: validation
Validate before calling
let alive = heap.entries.iter().any(|(other, _)| other == id);
assert!(alive, "HeapId {id:?} already freed before access"); Type guard
fn is_alive(heap: &Heap, id: HeapId) -> bool {
heap.entries.iter().any(|(other, _)| other == id)
} Try / catch
// get() panics by design; tolerate absence via entry():
match heap.entries.entry(id) {
Some(mut slot) => { /* use slot */ }
None => { /* entry freed — re-fetch or skip */ }
} Prevention
- Treat HeapIds as invalidated after collect_cycles, dec_ref-to-zero, and snapshot restore — always re-validate before use.
- Audit clone_with_heap/dec_ref pairs on any path that frees an entry unexpectedly (double-dec_ref is the usual culprit).
- In tolerant code paths use `entry(id)` (Option) instead of `get(id)` which panics on freed slots.
- Keep a test helper like `is_alive` handy and assert liveness right before inspecting entries in tests.
When it happens
Trigger: Calling `heap.entries.get(id)` (or `Heap::get`) with an id whose entry was already freed via `dec_ref` reaching zero, cycle collection (`collect_cycles`), or `StableHeapEntry::free`; also using an id captured before collection ran.
Common situations: Test or debug code holding a stale HeapId across a `collect_cycles()`/`dec_ref` call and then reading the entry; interpreter bugs that double-drop a value (two `dec_ref` calls on one reference) freeing the slot early; iterating saved ids after a snapshot restore where ids are remapped.
Related errors
- HeapReader::read_ptr - id out of bounds
- HeapPtr::entry: slot has been freed
- StableHeap::get - {id:?} out of bounds
- Heap::boundary_uuid: entry already freed
- already looked up
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/d6144d28b8144376.
Report an issue: GitHub.