{"record":{"id":"7927c4d049320d79","repo":"pydantic/monty","slug":"heapptr-entry-slot-has-been-freed","errorCode":null,"errorMessage":"HeapPtr::entry: slot has been freed","messagePattern":"HeapPtr::entry: slot has been freed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/monty/src/heap/mod.rs","lineNumber":653,"sourceCode":"    /// so a `HeapPtr` cannot be reborrowed under a different reader scope.\n    brand: PhantomData<fn(&'a ()) -> &'a ()>,\n}\n\nimpl<'a> HeapPtr<'a> {\n    /// Returns the live [`HeapEntry`] this pointer refers to, panicking if the slot\n    /// has been freed.\n    ///\n    /// All `HeapEntry` fields are interior-mutable — `refcount`/`readers`/`color`\n    /// via `Cell` and `data` via `UnsafeCell` — so callers can mutate them through\n    /// the returned `&HeapEntry` without ever needing `&mut HeapEntry`. That's\n    /// what makes a `&self`-derived `HeapPtr` (with Shared provenance) sound to\n    /// dereference: we never derive `&mut` from it, so the SB/TB rules permit\n    /// interior mutation via the embedded `Cell`/`UnsafeCell`.\n    ///\n    /// Use [`Self::try_entry`] for code paths that may legitimately encounter\n    /// freed slots (e.g. linear scans over `0..heap.entries.len()`).\n    pub fn entry<'r>(self, reader: &'r HeapReader<'a>) -> &'r HeapEntry {\n        self.try_entry(reader).expect(\"HeapPtr::entry: slot has been freed\")\n    }\n\n    /// Returns the [`HeapEntry`] this pointer refers to, or `None` if the slot is\n    /// currently freed.\n    ///\n    /// Use where a freed slot is part of the expected state (linear scans, root\n    /// reseeds, etc.). Mutation paths (cycle collector mark/scan inner loops) should\n    /// prefer [`Self::entry`] so that an unexpectedly-freed entry surfaces as a\n    /// loud panic rather than a silent skip.\n    pub(crate) fn try_entry<'r>(self, _reader: &'r HeapReader<'a>) -> Option<&'r HeapEntry> {\n        // SAFETY:\n        //  - The invariant `'a` on `_reader` matches this pointer's brand, which is\n        //    only settable inside `HeapReader::with`. That guarantees same-heap\n        //    origin: a `HeapPtr<'a>` from a different reader scope cannot satisfy\n        //    this signature.\n        //  - `StableHeap::entry_ptr` only returns pointers to initialized slots, so\n        //    the `Option<HeapEntry>` behind the pointer is always a valid place.\n        //  - The `&HeapReader` borrow excludes any `&mut HeapReader` op that could","sourceCodeStart":635,"sourceCodeEnd":671,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/heap/mod.rs#L635-L671","documentation":"`HeapPtr::entry` panics when the heap slot it points at is currently freed; the doc comment directs code that can legitimately see freed slots (linear scans over `0..heap.entries.len()`) to `try_entry` instead. Panicking here means a `HeapPtr` outlived its entry's lifetime — a use-after-free of a heap object. Root cause is almost always a refcount/drop ordering bug upstream.","triggerScenarios":"Calling `entry()` on a `HeapPtr` whose slot was freed by a prior `dec_ref`/cycle-collection pass; holding a `HeapPtr` across an operation that allocates/frees (which `HeapPtr` is supposed to prevent); GC code touching a collected object.","commonSituations":"New heap types missing entries in `py_dec_ref_ids` (or double-pushing ids), causing premature frees; iteration code holding a `HeapRead` while calling dropping operations; cycle collector changes that free entries still referenced.","solutions":["Switch code that may observe freed slots to `try_entry` and handle `None` explicitly.","Find who freed the entry early: check `py_dec_ref_ids` pushes every owned id exactly once and `DropWithContext` releases owned ids on all branches.","Ensure no `HeapPtr`/`HeapRead` is retained across `allocate` calls that can free slots — re-read by `HeapId` after mutating operations.","Run with `--features memory-model-checks` to pinpoint the refcount violation."],"exampleFix":"// before\nlet ptr = heap.read(id).as_list();\nptr.append(vm, item)?;               // append allocates, may free `ptr`'s slot\nlet data = ptr.entry(reader);        // panics: slot freed\n// after\nlet ptr = heap.read(id).as_list();\nptr.append(vm, item)?;               // frees/mutates through owned handles only\n// do not re-derive entry from a stale HeapPtr; re-read by id if needed\nlet ptr2 = heap.read(id);\nlet data = ptr2.entry(reader);","handlingStrategy":"type-guard","validationCode":"// in scans where a freed slot is possible\nif let Some(entry) = ptr.try_entry(reader) { /* use entry */ }","typeGuard":"fn is_live<'a>(ptr: HeapPtr<'a>, reader: &HeapReader<'a>) -> bool { ptr.try_entry(reader).is_some() }","tryCatchPattern":null,"preventionTips":["Prefer try_entry wherever freed slots are legitimate (linear scans).","Never hold a HeapPtr across allocating/freeing operations; re-read by id.","Audit py_dec_ref_ids to push each owned id exactly once.","Exercise GC changes with memory-model-checks."],"tags":["rust","heap","use-after-free","panic","gc"],"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"}