{"record":{"id":"0ecd2d9faeee53fb","repo":"pydantic/monty","slug":"heapreader-read-ptr-id-out-of-bounds","errorCode":null,"errorMessage":"HeapReader::read_ptr - id out of bounds","messagePattern":"HeapReader::read_ptr - id out of bounds","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/monty/src/heap/mod.rs","lineNumber":153,"sourceCode":"            },\n            data,\n        )\n    }\n}\n\nimpl<'a> HeapReader<'a> {\n    /// Resolves a `HeapId` to a stable, branded [`HeapPtr<'a>`] for its entry.\n    ///\n    /// The returned `HeapPtr` can be used for efficient repeated access to the same entry\n    /// without needing to re-index into the paged storage on every access.\n    ///\n    /// # Panics\n    ///\n    /// Panics if `id` is out of bounds.\n    pub(crate) fn read_ptr(&self, id: HeapId) -> HeapPtr<'a> {\n        // SAFETY: [DH] - `HeapPtr` prevents holding reference to freed slots across calls to allocate; it\n        // always hands out either live `&HeapData` or `None`, never `&Option<HeapData>`.\n        let slot = unsafe { self.heap.entries.slot_at(id) }.expect(\"HeapReader::read_ptr - id out of bounds\");\n        HeapPtr {\n            inner: NonNull::from(slot),\n            brand: PhantomData,\n        }\n    }\n\n    /// Indexes into the heap.\n    ///\n    /// Thin wrapper around [`HeapPtr::read`]: resolves `id` to a `HeapPtr` and\n    /// delegates the typed match/reader-count logic there. Panics if `id` is out\n    /// of bounds or the slot is currently freed.\n    pub fn read(&self, id: HeapId) -> HeapReadOutput<'a> {\n        self.read_ptr(id).read(id, self)\n    }\n\n    /// Reads `id` as the requested concrete payload type.\n    ///\n    /// Returns `None` when the live entry stores a different payload type.","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/heap/mod.rs#L135-L171","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet id: HeapId = value.into_id(); // raw id presumed borrowed\nsome_other_op(vm)?;              // may free the entry\nlet ptr = reader.read_ptr(id);   // panics: id freed/out of bounds\n// after\nlet value = vm.heap.get_iter(iter_ref); // keep an owning handle\ndefer_drop!(value, vm);\nsome_other_op(vm)?;\n// access through the guard-managed handle, not a raw id","handlingStrategy":"validation","validationCode":"// before dereferencing an internal HeapId in test/tooling code\nassert!(id.0 < heap.entries.len(), 'HeapId out of bounds before read_ptr');","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["rust","heap","use-after-free","panic","memory-safety"],"backgroundTag":"internal-invariant-violation","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}