pydantic/monty · critical
StableHeap::get - {id:?} out of bounds
Error message
StableHeap::get - {id:?} out of bounds What it means
`StableHeap::get` indexes the paged heap arena by HeapId; this panic fires when the id is out of bounds or its slot was freed. It is a deliberate safety panic (rather than exposing freed/reused memory) meaning the caller holds a HeapId for an entry that no longer exists — an interpreter bug such as a use-after-free of a heap id, not a Python-level error.
Source
Thrown at crates/monty/src/heap/stable_heap.rs:103
/// Returns the total number of initialized slots (including freed ones).
#[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>> {View on GitHub (pinned to adc986b362)
Solutions
- Check whether the code path holds a raw HeapId across an operation that can free the entry; wrap it in an owning `Value` managed by `defer_drop!`/`DropGuard` instead
- Verify every clone path calls `clone_with_heap` (inc_ref) and every drop path calls `drop_with` exactly once
- Remember heap state is invalid after a ResourceError — discard the execution context rather than touching remaining values
- Reproduce under `--features memory-model-checks` to pinpoint the refcount leak or double-drop
Example fix
// before: raw HeapId held across a call that may free the entry let id: HeapId = make_id(vm); other_op(vm)?; // may dec_ref `id` let v = vm.heap.get(id); // panic // after: keep ownership in a Value managed by a guard let v = vm.heap.get(id); defer_drop!(v, vm); other_op(vm)?; use_value(v);
Defensive patterns
Strategy: validation
Validate before calling
// In VM code: never keep a bare HeapId across operations that can free it. // Guard with an owning Value and defer_drop!: let value = vm.heap.get(id); defer_drop!(value, vm); // safe to use `value` across fallible calls now
Type guard
// No runtime type check exists; the guard is ownership discipline:
fn is_alive<T>(heap: &StableHeap<T>, id: HeapId) -> bool {
heap.contains(id) // if available; otherwise track ids you still own
} Try / catch
// This is a panic, not a Result — cannot be caught. Detect the known
// valid-after-error pitfall instead:
match monty.run(code, limits) {
Err(e @ ResourceError(..)) => {
// heap is invalid here — do NOT inspect remaining values; discard ctx
discard(e);
}
other => other,
} Prevention
- Wrap locally owned HeapIds in Value and manage with defer_drop!/DropGuard
- Call clone_with_heap on every clone; drop_with exactly once per owned value
- Never touch heap objects after a ResourceError terminates execution
- Reproduce refcount bugs under the memory-model-checks feature
When it happens
Trigger: Calling `heap.get(id)` (or `heap.read(id)`) with a HeapId whose slot was freed via dec_ref, or never allocated; typically a double-drop, a missed inc_ref on a clone, or reading a value after a ResourceError terminated execution (where refcounts are explicitly not maintained).
Common situations: Continuing to use a VM/heap after a memory/time ResourceError; writing VM code that holds a raw HeapId across a call that can drop the entry; fuzzing refcount-sensitive programs.
Related errors
- list_extend: expected List on heap
- LoadCell: entry is not a Cell
- StoreCell: entry is not a Cell
- DeleteCell: entry is not a Cell
- HeapReader::read_ptr - id out of bounds
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/c0d6f59f650f8b0b.
Report an issue: GitHub.