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

  1. 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
  2. Verify every clone path calls `clone_with_heap` (inc_ref) and every drop path calls `drop_with` exactly once
  3. Remember heap state is invalid after a ResourceError — discard the execution context rather than touching remaining values
  4. 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

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


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