{"record":{"id":"d6144d28b8144376","repo":"pydantic/monty","slug":"heapentries-get-data-already-freed","errorCode":null,"errorMessage":"HeapEntries::get - data already freed","messagePattern":"HeapEntries::get - data already freed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/monty/src/heap/stable_heap.rs","lineNumber":105,"sourceCode":"    #[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>> {\n        assert!(id.index() < self.len.get(), \"StableHeap::entry - {id:?} out of bounds\");\n        let (page_idx, slot_idx) = Self::page_slot_indices(id);","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/heap/stable_heap.rs#L87-L123","documentation":"`StableHeap::get` panics with \"HeapEntries::get - data already freed\" (crates/monty/src/heap/stable_heap.rs:105) when the slot at the given HeapId is within bounds but has been freed: `entry.as_ref()` returns None because `StableHeapEntry::free` took the value. `get` deliberately panics rather than expose dangling free slots, whose indices can be invalidated by subsequent `allocate` reuse.","triggerScenarios":"Calling `heap.entries.get(id)` (or `Heap::get`) with an id whose entry was already freed via `dec_ref` reaching zero, cycle collection (`collect_cycles`), or `StableHeapEntry::free`; also using an id captured before collection ran.","commonSituations":"Test or debug code holding a stale HeapId across a `collect_cycles()`/`dec_ref` call and then reading the entry; interpreter bugs that double-drop a value (two `dec_ref` calls on one reference) freeing the slot early; iterating saved ids after a snapshot restore where ids are remapped.","solutions":["Confirm the id is still live before access: `heap.entries.iter().any(|(other, _)| other == id)` (see the test helper `is_alive` at crates/monty/src/heap/mod.rs:2311).","Audit refcounting on the failing path — the slot freed early, so look for a missing `clone_with_heap` or an extra `dec_ref`/`drop_with` on that value.","Use `StableHeap::entry(id)` (returns Option) instead of `get` in code that must tolerate freed slots.","If the id was captured before `collect_cycles` or a snapshot restore, re-fetch it afterwards; freed ids are never valid again."],"exampleFix":"// before\nlet data = heap.entries.get(id); // panics if freed\n// after\nassert!(is_alive(&heap, id), \"entry was freed\");\nlet data = heap.entries.get(id);","handlingStrategy":"validation","validationCode":"let alive = heap.entries.iter().any(|(other, _)| other == id);\nassert!(alive, \"HeapId {id:?} already freed before access\");","typeGuard":"fn is_alive(heap: &Heap, id: HeapId) -> bool {\n    heap.entries.iter().any(|(other, _)| other == id)\n}","tryCatchPattern":"// get() panics by design; tolerate absence via entry():\nmatch heap.entries.entry(id) {\n    Some(mut slot) => { /* use slot */ }\n    None => { /* entry freed — re-fetch or skip */ }\n}","preventionTips":["Treat HeapIds as invalidated after collect_cycles, dec_ref-to-zero, and snapshot restore — always re-validate before use.","Audit clone_with_heap/dec_ref pairs on any path that frees an entry unexpectedly (double-dec_ref is the usual culprit).","In tolerant code paths use `entry(id)` (Option) instead of `get(id)` which panics on freed slots.","Keep a test helper like `is_alive` handy and assert liveness right before inspecting entries in tests."],"tags":["rust","panic","heap","use-after-free","refcounting"],"backgroundTag":"use-after-free","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"}