From 917c9f673b8fdc076f47464a6d65fd0b9f23799f Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Tue, 13 Jan 2026 18:59:17 +0900 Subject: [PATCH] Fix dict race condition --- crates/vm/src/builtins/dict.rs | 18 ++++++++++++++++++ crates/vm/src/dict_inner.rs | 16 ++++++++++++++++ crates/vm/src/vm/mod.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index 43f7100e963..d1adb8a066d 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -62,6 +62,24 @@ impl PyDict { &self.entries } + /// Returns all keys as a Vec, atomically under a single read lock. + /// Thread-safe: prevents "dictionary changed size during iteration" errors. + pub fn keys_vec(&self) -> Vec { + self.entries.keys() + } + + /// Returns all values as a Vec, atomically under a single read lock. + /// Thread-safe: prevents "dictionary changed size during iteration" errors. + pub fn values_vec(&self) -> Vec { + self.entries.values() + } + + /// Returns all items as a Vec, atomically under a single read lock. + /// Thread-safe: prevents "dictionary changed size during iteration" errors. + pub fn items_vec(&self) -> Vec<(PyObjectRef, PyObjectRef)> { + self.entries.items() + } + // Used in update and ior. pub(crate) fn merge_object(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { let casted: Result, _> = other.downcast_exact(vm); diff --git a/crates/vm/src/dict_inner.rs b/crates/vm/src/dict_inner.rs index d57f8be0fe7..1d9fe8403ab 100644 --- a/crates/vm/src/dict_inner.rs +++ b/crates/vm/src/dict_inner.rs @@ -553,6 +553,22 @@ impl Dict { .collect() } + pub fn values(&self) -> Vec { + self.read() + .entries + .iter() + .filter_map(|v| v.as_ref().map(|v| v.value.clone())) + .collect() + } + + pub fn items(&self) -> Vec<(PyObjectRef, T)> { + self.read() + .entries + .iter() + .filter_map(|v| v.as_ref().map(|v| (v.key.clone(), v.value.clone()))) + .collect() + } + pub fn try_fold_keys(&self, init: Acc, f: Fold) -> PyResult where Fold: FnMut(Acc, &PyObject) -> PyResult, diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 7708b5a9176..8233df43a2b 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -20,7 +20,11 @@ use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, builtins::{ PyBaseExceptionRef, PyDict, PyDictRef, PyInt, PyList, PyModule, PyStr, PyStrInterned, - PyStrRef, PyTypeRef, code::PyCode, pystr::AsPyStr, tuple::PyTuple, + PyStrRef, PyTypeRef, + code::PyCode, + dict::{PyDictItems, PyDictKeys, PyDictValues}, + pystr::AsPyStr, + tuple::PyTuple, }, codecs::CodecsRegistry, common::{hash::HashSecret, lock::PyMutex, rc::PyRc}, @@ -808,6 +812,29 @@ impl VirtualMachine { } else if cls.is(self.ctx.types.list_type) { list_borrow = value.downcast_ref::().unwrap().borrow_vec(); &list_borrow + } else if cls.is(self.ctx.types.dict_keys_type) { + // Atomic snapshot of dict keys - prevents race condition during iteration + let keys = value.downcast_ref::().unwrap().dict.keys_vec(); + return keys.into_iter().map(func).collect(); + } else if cls.is(self.ctx.types.dict_values_type) { + // Atomic snapshot of dict values - prevents race condition during iteration + let values = value + .downcast_ref::() + .unwrap() + .dict + .values_vec(); + return values.into_iter().map(func).collect(); + } else if cls.is(self.ctx.types.dict_items_type) { + // Atomic snapshot of dict items - prevents race condition during iteration + let items = value + .downcast_ref::() + .unwrap() + .dict + .items_vec(); + return items + .into_iter() + .map(|(k, v)| func(self.ctx.new_tuple(vec![k, v]).into())) + .collect(); } else { return self.map_py_iter(value, func); };