pydantic/monty · critical
HeapReader::read_ptr - id out of bounds
Error message
HeapReader::read_ptr - id out of bounds
What it means
`HeapReader::read_ptr` converts a `HeapId` into a `HeapPtr` and panics when `entries.slot_at(id)` returns `None`, i.e. the id is out of bounds of the heap's entry arena. Heap ids are internal, so this means a stale or invalid `HeapId` was used after the heap grew/shrank or a value was carried across heap instances. It is an internal invariant break, typically caused by a refcount/drop bug elsewhere.
Source
Thrown at crates/monty/src/heap/mod.rs:153
},
data,
)
}
}
impl<'a> HeapReader<'a> {
/// Resolves a `HeapId` to a stable, branded [`HeapPtr<'a>`] for its entry.
///
/// The returned `HeapPtr` can be used for efficient repeated access to the same entry
/// without needing to re-index into the paged storage on every access.
///
/// # Panics
///
/// Panics if `id` is out of bounds.
pub(crate) fn read_ptr(&self, id: HeapId) -> HeapPtr<'a> {
// SAFETY: [DH] - `HeapPtr` prevents holding reference to freed slots across calls to allocate; it
// always hands out either live `&HeapData` or `None`, never `&Option<HeapData>`.
let slot = unsafe { self.heap.entries.slot_at(id) }.expect("HeapReader::read_ptr - id out of bounds");
HeapPtr {
inner: NonNull::from(slot),
brand: PhantomData,
}
}
/// Indexes into the heap.
///
/// Thin wrapper around [`HeapPtr::read`]: resolves `id` to a `HeapPtr` and
/// delegates the typed match/reader-count logic there. Panics if `id` is out
/// of bounds or the slot is currently freed.
pub fn read(&self, id: HeapId) -> HeapReadOutput<'a> {
self.read_ptr(id).read(id, self)
}
/// Reads `id` as the requested concrete payload type.
///
/// Returns `None` when the live entry stores a different payload type.View on GitHub (pinned to adc986b362)
Solutions
- Audit the code path that produced the id: ensure owned `HeapId`s are released exactly once via `py_dec_ref_ids` or `DropWithContext`.
- Use `defer_drop!`/`DropGuard` instead of manual `drop_with` on branching paths so no value is used after drop.
- Never store `HeapId` across snapshot/restore or between heaps; re-resolve values from the owning heap.
- Run the failing case under `--features memory-model-checks` to surface the refcount bug.
Example fix
// before let id: HeapId = value.into_id(); // raw id presumed borrowed some_other_op(vm)?; // may free the entry let ptr = reader.read_ptr(id); // panics: id freed/out of bounds // after let value = vm.heap.get_iter(iter_ref); // keep an owning handle defer_drop!(value, vm); some_other_op(vm)?; // access through the guard-managed handle, not a raw id
Defensive patterns
Strategy: validation
Validate before calling
// before dereferencing an internal HeapId in test/tooling code assert!(id.0 < heap.entries.len(), 'HeapId out of bounds before read_ptr');
Prevention
- Own HeapIds in Value::Ref and manage with defer_drop!/DropGuard, never raw ids on branching paths.
- Release owned ids exactly once via py_dec_ref_ids or DropWithContext.
- Never carry HeapIds across heaps or snapshot boundaries.
- Run refcount-touching changes under --features memory-model-checks.
When it happens
Trigger: Dereferencing a `HeapId` obtained from a dropped/freed value; using an id from a different heap instance; refcount bugs that free an entry while a `Value::Ref` still holds its id; snapshot/restore mismatches giving ids from another heap.
Common situations: Developing new heap-stored types with incorrect `py_dec_ref_ids` ownership; introducing a use-after-drop in a new opcode; tests with `memory-model-checks` catching a leak or double-drop first.
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
- HeapPtr::entry: slot has been freed
- HeapEntries::get - data already 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/0ecd2d9faeee53fb.
Report an issue: GitHub.