pydantic/monty · critical
HeapPtr::entry: slot has been freed
Error message
HeapPtr::entry: slot has been freed
What it means
`HeapPtr::entry` panics when the heap slot it points at is currently freed; the doc comment directs code that can legitimately see freed slots (linear scans over `0..heap.entries.len()`) to `try_entry` instead. Panicking here means a `HeapPtr` outlived its entry's lifetime — a use-after-free of a heap object. Root cause is almost always a refcount/drop ordering bug upstream.
Source
Thrown at crates/monty/src/heap/mod.rs:653
/// so a `HeapPtr` cannot be reborrowed under a different reader scope.
brand: PhantomData<fn(&'a ()) -> &'a ()>,
}
impl<'a> HeapPtr<'a> {
/// Returns the live [`HeapEntry`] this pointer refers to, panicking if the slot
/// has been freed.
///
/// All `HeapEntry` fields are interior-mutable — `refcount`/`readers`/`color`
/// via `Cell` and `data` via `UnsafeCell` — so callers can mutate them through
/// the returned `&HeapEntry` without ever needing `&mut HeapEntry`. That's
/// what makes a `&self`-derived `HeapPtr` (with Shared provenance) sound to
/// dereference: we never derive `&mut` from it, so the SB/TB rules permit
/// interior mutation via the embedded `Cell`/`UnsafeCell`.
///
/// Use [`Self::try_entry`] for code paths that may legitimately encounter
/// freed slots (e.g. linear scans over `0..heap.entries.len()`).
pub fn entry<'r>(self, reader: &'r HeapReader<'a>) -> &'r HeapEntry {
self.try_entry(reader).expect("HeapPtr::entry: slot has been freed")
}
/// Returns the [`HeapEntry`] this pointer refers to, or `None` if the slot is
/// currently freed.
///
/// Use where a freed slot is part of the expected state (linear scans, root
/// reseeds, etc.). Mutation paths (cycle collector mark/scan inner loops) should
/// prefer [`Self::entry`] so that an unexpectedly-freed entry surfaces as a
/// loud panic rather than a silent skip.
pub(crate) fn try_entry<'r>(self, _reader: &'r HeapReader<'a>) -> Option<&'r HeapEntry> {
// SAFETY:
// - The invariant `'a` on `_reader` matches this pointer's brand, which is
// only settable inside `HeapReader::with`. That guarantees same-heap
// origin: a `HeapPtr<'a>` from a different reader scope cannot satisfy
// this signature.
// - `StableHeap::entry_ptr` only returns pointers to initialized slots, so
// the `Option<HeapEntry>` behind the pointer is always a valid place.
// - The `&HeapReader` borrow excludes any `&mut HeapReader` op that couldView on GitHub (pinned to adc986b362)
Solutions
- Switch code that may observe freed slots to `try_entry` and handle `None` explicitly.
- Find who freed the entry early: check `py_dec_ref_ids` pushes every owned id exactly once and `DropWithContext` releases owned ids on all branches.
- Ensure no `HeapPtr`/`HeapRead` is retained across `allocate` calls that can free slots — re-read by `HeapId` after mutating operations.
- Run with `--features memory-model-checks` to pinpoint the refcount violation.
Example fix
// before let ptr = heap.read(id).as_list(); ptr.append(vm, item)?; // append allocates, may free `ptr`'s slot let data = ptr.entry(reader); // panics: slot freed // after let ptr = heap.read(id).as_list(); ptr.append(vm, item)?; // frees/mutates through owned handles only // do not re-derive entry from a stale HeapPtr; re-read by id if needed let ptr2 = heap.read(id); let data = ptr2.entry(reader);
Defensive patterns
Strategy: type-guard
Validate before calling
// in scans where a freed slot is possible
if let Some(entry) = ptr.try_entry(reader) { /* use entry */ } Type guard
fn is_live<'a>(ptr: HeapPtr<'a>, reader: &HeapReader<'a>) -> bool { ptr.try_entry(reader).is_some() } Prevention
- Prefer try_entry wherever freed slots are legitimate (linear scans).
- Never hold a HeapPtr across allocating/freeing operations; re-read by id.
- Audit py_dec_ref_ids to push each owned id exactly once.
- Exercise GC changes with memory-model-checks.
When it happens
Trigger: Calling `entry()` on a `HeapPtr` whose slot was freed by a prior `dec_ref`/cycle-collection pass; holding a `HeapPtr` across an operation that allocates/frees (which `HeapPtr` is supposed to prevent); GC code touching a collected object.
Common situations: New heap types missing entries in `py_dec_ref_ids` (or double-pushing ids), causing premature frees; iteration code holding a `HeapRead` while calling dropping operations; cycle collector changes that free entries still referenced.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- HeapReader::read_ptr - id out of bounds
- already looked up
- HeapEntries::get - data already freed
- StableHeap::get - {id:?} out of bounds
- Heap::boundary_uuid: entry already freed
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/7927c4d049320d79.
Report an issue: GitHub.