-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Open
Description
What happened?
list.__repr__ holds a read-lock on the backing vector while rendering elements. If any element's __repr__ mutates the same list (e.g., append), the re-entrant call blocks on the write-lock while __repr__ waits for the element to finish, producing a hard deadlock instead of a Python-level exception.
Proof of Concept:
class Evil:
def __repr__(self):
lst.append(1)
return "evil"
lst = [Evil()]
print(lst)Affected Versions
| RustPython Version | Status | Exit Code |
|---|---|---|
Python 3.13.0alpha (heads/main-dirty:21300f689, Dec 13 2025, 22:16:49) [RustPython 0.4.0 with rustc 1.90.0-nightly (11ad40bb8 2025-06-28)] |
Deadlock | 124 |
Vulnerable Code
fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
if zelf.__len__() == 0 {
return Ok("[]".to_owned());
}
if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) {
let iter = zelf.borrow_vec().iter(); // holds a read lock on elements for the whole render
return collection_repr(None, "[", "]", iter, vm); // re-enters element __repr__ while lock is held
}
Ok("[...]".to_owned())
}
pub(crate) fn collection_repr<'a, I>(
class_name: Option<&str>,
prefix: &str,
suffix: &str,
iter: I,
vm: &VirtualMachine,
) -> PyResult<String>
where
I: Iterator<Item = &'a PyObjectRef>,
{
let mut repr = String::new();
let mut parts_iter = iter.map(|o| o.repr(vm)); // re-enters Python-level __repr__ for each element
repr.push_str(
parts_iter
.next()
.transpose()?
.expect("this is not called for empty collection")
.as_str(),
);
// ...
}
#[pymethod]
pub(crate) fn append(&self, x: PyObjectRef) {
self.borrow_vec_mut().push(x); // attempts write lock while read lock from repr_str is held -> deadlock
}Rust Output
Program hangs forever
CPython Output
[evil, 1]
Metadata
Metadata
Assignees
Labels
No labels