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

  1. Audit the code path that produced the id: ensure owned `HeapId`s are released exactly once via `py_dec_ref_ids` or `DropWithContext`.
  2. Use `defer_drop!`/`DropGuard` instead of manual `drop_with` on branching paths so no value is used after drop.
  3. Never store `HeapId` across snapshot/restore or between heaps; re-resolve values from the owning heap.
  4. 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

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


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