{"record":{"id":"c0d6f59f650f8b0b","repo":"pydantic/monty","slug":"stableheap-get-id-out-of-bounds","errorCode":null,"errorMessage":"StableHeap::get - {id:?} out of bounds","messagePattern":"StableHeap::get - (.+?) out of bounds","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/monty/src/heap/stable_heap.rs","lineNumber":103,"sourceCode":"\n    /// Returns the total number of initialized slots (including freed ones).\n    #[inline]\n    pub fn len(&self) -> usize {\n        self.len.get()\n    }\n\n    /// Returns a shared reference to the entry at `index`.\n    ///\n    /// # Panics\n    /// Panics if `index >= len`, or if the slot is freed.\n    #[inline]\n    #[track_caller]\n    pub fn get(&self, id: HeapId) -> &T {\n        // SAFETY: [DH] - this call panics rather than expose free slots which could be invalidated\n        // by calls to `.allocate()`.\n        let slot = unsafe { self.slot_at(id) };\n        let Some(entry) = slot else {\n            panic!(\"StableHeap::get - {id:?} out of bounds\");\n        };\n        entry.as_ref().expect(\"HeapEntries::get - data already freed\")\n    }\n\n    /// Returns a mutable reference to the entry at `index`. Entries can also be\n    /// freed via the returned `StableHeapEntry`'s `free` method.\n    ///\n    /// Does *not* go through [`Self::entry_ptr`]: that path derives its pointer via\n    /// `&self` so it only carries Shared / SharedReadOnly provenance under SB/TB,\n    /// and dereferencing it as `&mut` is UB. Instead this method takes the safe\n    /// `&mut self` → `pages.get_mut()` route, producing a `&mut Option<T>` with\n    /// Unique provenance suitable for `StableHeapEntry::free`'s `value.take()`.\n    ///\n    /// # Panics\n    /// Panics if `index >= len`.\n    #[inline]\n    #[track_caller]\n    pub fn entry(&mut self, id: HeapId) -> Option<StableHeapEntry<'_, T>> {","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/heap/stable_heap.rs#L85-L121","documentation":"`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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: raw HeapId held across a call that may free the entry\nlet id: HeapId = make_id(vm);\nother_op(vm)?; // may dec_ref `id`\nlet v = vm.heap.get(id); // panic\n// after: keep ownership in a Value managed by a guard\nlet v = vm.heap.get(id);\ndefer_drop!(v, vm);\nother_op(vm)?;\nuse_value(v);","handlingStrategy":"validation","validationCode":"// In VM code: never keep a bare HeapId across operations that can free it.\n// Guard with an owning Value and defer_drop!:\nlet value = vm.heap.get(id);\ndefer_drop!(value, vm);\n// safe to use `value` across fallible calls now","typeGuard":"// No runtime type check exists; the guard is ownership discipline:\nfn is_alive<T>(heap: &StableHeap<T>, id: HeapId) -> bool {\n    heap.contains(id) // if available; otherwise track ids you still own\n}","tryCatchPattern":"// This is a panic, not a Result — cannot be caught. Detect the known\n// valid-after-error pitfall instead:\nmatch monty.run(code, limits) {\n    Err(e @ ResourceError(..)) => {\n        // heap is invalid here — do NOT inspect remaining values; discard ctx\n        discard(e);\n    }\n    other => other,\n}","preventionTips":["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"],"tags":["panic","internal","heap","use-after-free","refcount"],"backgroundTag":"index-out-of-bounds","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"}