{"record":{"id":"c6b0e96383135d33","repo":"pydantic/monty","slug":"dict-keys-view-must-always-reference-a-dict","errorCode":null,"errorMessage":"dict_keys view must always reference a dict","messagePattern":"dict_keys view must always reference a dict","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/monty/src/types/dict_view.rs","lineNumber":66,"sourceCode":"\nimpl DictKeysView {\n    /// Creates a new keys view over an existing dictionary heap entry.\n    #[must_use]\n    pub fn new(dict_id: HeapId) -> Self {\n        Self { dict_id }\n    }\n\n    /// Returns the underlying dictionary heap id.\n    #[must_use]\n    pub fn dict_id(self) -> HeapId {\n        self.dict_id\n    }\n}\n\nimpl<'h> HeapRead<'h, DictKeysView> {\n    fn dict(&self, vm: &mut VM<'h>) -> HeapObjectRead<'h, Dict> {\n        let HeapReadOutput::Dict(dict) = vm.heap.read(self.get(vm.heap).dict_id) else {\n            panic!(\"dict_keys view must always reference a dict\");\n        };\n        dict\n    }\n\n    /// Compares this keys view to a mutable set using set membership semantics.\n    pub(crate) fn eq_set(&self, other: &HeapRead<'h, Set>, vm: &mut VM<'h>) -> RunResult<bool> {\n        dict_keys_eq_set_like(\n            &self.dict(vm),\n            other.get(vm.heap).len(),\n            |key, vm| other.contains(key, vm),\n            vm,\n        )\n    }\n\n    /// Compares this keys view to a frozenset using set membership semantics.\n    pub(crate) fn eq_frozenset(&self, other: &HeapRead<'h, FrozenSet>, vm: &mut VM<'h>) -> RunResult<bool> {\n        dict_keys_eq_set_like(\n            &self.dict(vm),","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/types/dict_view.rs#L48-L84","documentation":"The HeapRead-based DictKeysView accessor reads the backing dictionary from the heap and panics if the id does not resolve to a Dict entry. As with the trait version, this is unreachable by construction and signals heap corruption or an id-reuse bug under the safe HeapReader API.","triggerScenarios":"Any keys-view operation (eq_set, iteration, len) when the view's stored dict_id resolves to a non-Dict HeapReadOutput — i.e. the dict was freed and its id recycled while the view handle was live.","commonSituations":"Memory-model CI after refcount changes; debugging reports of wrong-typed heap reads.","solutions":["Fix the refcount path that lets the dict be freed while the view exists (views must count as readers/refs).","Verify the view's dict_id is incremented at construction and released via py_dec_ref_ids.","Run the relevant test binary with `--features memory-model-checks` to catch the stale-id path."],"exampleFix":"// before\nlet view = DictKeysView { dict_id }; // borrowed id\n// after\nheap.inc_ref(dict_id);\nlet view = DictKeysView { dict_id }; // owned, cleaned up via py_dec_ref_ids","handlingStrategy":"type-guard","validationCode":"// check the read resolves to a Dict before further use\nif !matches!(vm.heap.read(view.get(vm.heap).dict_id), HeapReadOutput::Dict(_)) { return Err(ViewError::stale_dict()); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Views must participate in refcounting via py_dec_ref_ids","Increment the dict refcount when constructing a view from a borrowed id","Exercise dict views under --features memory-model-checks after heap changes"],"tags":["rust","heap","internal-invariant"],"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-14T16:17:12.679Z"}