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

  1. 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).
  2. 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.
  3. Use `StableHeap::entry(id)` (returns Option) instead of `get` in code that must tolerate freed slots.
  4. 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

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


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/d6144d28b8144376. Report an issue: GitHub.