Skip to content

Commit 4ea7fd9

Browse files
committed
Drop old PyObjectRef outside type lock to prevent deadlock
Dropping values inside with_type_lock can trigger weakref callbacks, which may access attributes (LOAD_ATTR specialization) and re-acquire the non-reentrant type mutex, causing deadlock. Return old values from lock closures so they drop after lock release.
1 parent ea2d66e commit 4ea7fd9

File tree

7 files changed

+94
-93
lines changed

7 files changed

+94
-93
lines changed

.github/workflows/cron-ci.yaml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212
name: Periodic checks/tasks
1313

1414
env:
15-
CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,ssl-rustls,jit
15+
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit,host_env
1616
PYTHON_VERSION: "3.14.3"
1717

1818
jobs:
@@ -35,7 +35,7 @@ jobs:
3535
python-version: ${{ env.PYTHON_VERSION }}
3636
- run: sudo apt-get update && sudo apt-get -y install lcov
3737
- name: Run cargo-llvm-cov with Rust tests.
38-
run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,encodings,ssl-rustls,jit
38+
run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit,host_env
3939
- name: Run cargo-llvm-cov with Python snippets.
4040
run: python scripts/cargo-llvm-cov.py
4141
continue-on-error: true
@@ -48,7 +48,7 @@ jobs:
4848
if: ${{ github.event_name != 'pull_request' }}
4949
uses: codecov/codecov-action@v5
5050
with:
51-
file: ./codecov.lcov
51+
files: ./codecov.lcov
5252

5353
testdata:
5454
name: Collect regression test data
@@ -170,12 +170,12 @@ jobs:
170170
- name: restructure generated files
171171
run: |
172172
cd ./target/criterion/reports
173-
find -type d -name cpython | xargs rm -rf
174-
find -type d -name rustpython | xargs rm -rf
175-
find -mindepth 2 -maxdepth 2 -name violin.svg | xargs rm -rf
176-
find -type f -not -name violin.svg | xargs rm -rf
177-
for file in $(find -type f -name violin.svg); do mv $file $(echo $file | sed -E "s_\./([^/]+)/([^/]+)/violin\.svg_./\1/\2.svg_"); done
178-
find -mindepth 2 -maxdepth 2 -type d | xargs rm -rf
173+
find . -type d -name cpython -print0 | xargs -0 rm -rf
174+
find . -type d -name rustpython -print0 | xargs -0 rm -rf
175+
find . -mindepth 2 -maxdepth 2 -name violin.svg -print0 | xargs -0 rm -rf
176+
find . -type f -not -name violin.svg -print0 | xargs -0 rm -rf
177+
find . -type f -name violin.svg -exec sh -c 'for file; do mv "$file" "$(echo "$file" | sed -E "s_\./([^/]+)/([^/]+)/violin\.svg_./\1/\2.svg_")"; done' _ {} +
178+
find . -mindepth 2 -maxdepth 2 -type d -print0 | xargs -0 rm -rf
179179
cd ..
180180
mv reports/* .
181181
rmdir reports

Lib/test/test_class.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ def assertNotOrderable(self, a, b):
614614
with self.assertRaises(TypeError):
615615
a >= b
616616

617+
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; AssertionError: 2582729627104 != 2582729622496")
617618
def testHashComparisonOfMethods(self):
618619
# Test comparison and hash of methods
619620
class A:

crates/vm/src/builtins/type.rs

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,18 +1562,19 @@ impl PyType {
15621562
drop(attrs);
15631563

15641564
let none = vm.ctx.none();
1565-
Ok(Self::with_type_lock(vm, || {
1565+
let (result, _prev) = Self::with_type_lock(vm, || {
15661566
let mut attrs = self.attributes.write();
15671567
if let Some(annotate) = attrs.get(annotate_key).cloned() {
1568-
return annotate;
1568+
return (annotate, None);
15691569
}
15701570
if let Some(annotate) = attrs.get(annotate_func_key).cloned() {
1571-
return annotate;
1571+
return (annotate, None);
15721572
}
15731573
self.modified_inner();
1574-
attrs.insert(annotate_func_key, none.clone());
1575-
none
1576-
}))
1574+
let prev = attrs.insert(annotate_func_key, none.clone());
1575+
(none, prev)
1576+
});
1577+
Ok(result)
15771578
}
15781579

15791580
#[pygetset(setter)]
@@ -1596,13 +1597,16 @@ impl PyType {
15961597
return Err(vm.new_type_error("__annotate__ must be callable or None"));
15971598
}
15981599

1599-
Self::with_type_lock(vm, || {
1600+
let _prev_values = Self::with_type_lock(vm, || {
16001601
self.modified_inner();
16011602
let mut attrs = self.attributes.write();
1602-
if !vm.is_none(&value) {
1603-
attrs.swap_remove(identifier!(vm, __annotations_cache__));
1604-
}
1605-
attrs.insert(identifier!(vm, __annotate_func__), value);
1603+
let removed = if !vm.is_none(&value) {
1604+
attrs.swap_remove(identifier!(vm, __annotations_cache__))
1605+
} else {
1606+
None
1607+
};
1608+
let prev = attrs.insert(identifier!(vm, __annotate_func__), value);
1609+
(removed, prev)
16061610
});
16071611

16081612
Ok(())
@@ -1665,20 +1669,21 @@ impl PyType {
16651669
vm.ctx.new_dict().into()
16661670
};
16671671

1668-
Ok(Self::with_type_lock(vm, || {
1672+
let (result, _prev) = Self::with_type_lock(vm, || {
16691673
let mut attrs = self.attributes.write();
16701674
if let Some(existing) = attrs.get(annotations_key).cloned()
16711675
&& !existing.class().is(vm.ctx.types.getset_type)
16721676
{
1673-
return existing;
1677+
return (existing, None);
16741678
}
16751679
if let Some(existing) = attrs.get(annotations_cache_key).cloned() {
1676-
return existing;
1680+
return (existing, None);
16771681
}
16781682
self.modified_inner();
1679-
attrs.insert(annotations_cache_key, annotations.clone());
1680-
annotations
1681-
}))
1683+
let prev = attrs.insert(annotations_cache_key, annotations.clone());
1684+
(annotations, prev)
1685+
});
1686+
Ok(result)
16821687
}
16831688

16841689
#[pygetset(setter)]
@@ -1694,44 +1699,42 @@ impl PyType {
16941699
)));
16951700
}
16961701

1697-
Self::with_type_lock(vm, || {
1702+
let _prev_values = Self::with_type_lock(vm, || {
16981703
self.modified_inner();
16991704
let mut attrs = self.attributes.write();
17001705
let has_annotations = attrs.contains_key(identifier!(vm, __annotations__));
17011706

1707+
let mut prev = Vec::new();
17021708
match value {
17031709
crate::function::PySetterValue::Assign(value) => {
17041710
let key = if has_annotations {
17051711
identifier!(vm, __annotations__)
17061712
} else {
17071713
identifier!(vm, __annotations_cache__)
17081714
};
1709-
attrs.insert(key, value);
1715+
prev.extend(attrs.insert(key, value));
17101716
if has_annotations {
1711-
attrs.swap_remove(identifier!(vm, __annotations_cache__));
1717+
prev.extend(attrs.swap_remove(identifier!(vm, __annotations_cache__)));
17121718
}
17131719
}
17141720
crate::function::PySetterValue::Delete => {
17151721
let removed = if has_annotations {
1716-
attrs
1717-
.swap_remove(identifier!(vm, __annotations__))
1718-
.is_some()
1722+
attrs.swap_remove(identifier!(vm, __annotations__))
17191723
} else {
1720-
attrs
1721-
.swap_remove(identifier!(vm, __annotations_cache__))
1722-
.is_some()
1724+
attrs.swap_remove(identifier!(vm, __annotations_cache__))
17231725
};
1724-
if !removed {
1726+
if removed.is_none() {
17251727
return Err(vm.new_attribute_error("__annotations__"));
17261728
}
1729+
prev.extend(removed);
17271730
if has_annotations {
1728-
attrs.swap_remove(identifier!(vm, __annotations_cache__));
1731+
prev.extend(attrs.swap_remove(identifier!(vm, __annotations_cache__)));
17291732
}
17301733
}
17311734
}
1732-
attrs.swap_remove(identifier!(vm, __annotate_func__));
1733-
attrs.swap_remove(identifier!(vm, __annotate__));
1734-
Ok(())
1735+
prev.extend(attrs.swap_remove(identifier!(vm, __annotate_func__)));
1736+
prev.extend(attrs.swap_remove(identifier!(vm, __annotate__)));
1737+
Ok(prev)
17351738
})?;
17361739

17371740
Ok(())
@@ -1765,11 +1768,12 @@ impl PyType {
17651768
#[pygetset(setter)]
17661769
fn set___module__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
17671770
self.check_set_special_type_attr(identifier!(vm, __module__), vm)?;
1768-
Self::with_type_lock(vm, || {
1771+
let _prev_values = Self::with_type_lock(vm, || {
17691772
self.modified_inner();
1770-
self.attributes
1771-
.write()
1772-
.insert(identifier!(vm, __module__), value);
1773+
let mut attributes = self.attributes.write();
1774+
let removed = attributes.swap_remove(identifier!(vm, __firstlineno__));
1775+
let prev = attributes.insert(identifier!(vm, __module__), value);
1776+
(removed, prev)
17731777
});
17741778
Ok(())
17751779
}
@@ -1896,9 +1900,9 @@ impl PyType {
18961900
match value {
18971901
PySetterValue::Assign(val) => {
18981902
self.check_set_special_type_attr(key, vm)?;
1899-
Self::with_type_lock(vm, || {
1903+
let _prev_value = Self::with_type_lock(vm, || {
19001904
self.modified_inner();
1901-
self.attributes.write().insert(key, val.into());
1905+
self.attributes.write().insert(key, val.into())
19021906
});
19031907
}
19041908
PySetterValue::Delete => {
@@ -1908,9 +1912,9 @@ impl PyType {
19081912
self.slot_name()
19091913
)));
19101914
}
1911-
Self::with_type_lock(vm, || {
1915+
let _prev_value = Self::with_type_lock(vm, || {
19121916
self.modified_inner();
1913-
self.attributes.write().shift_remove(&key);
1917+
self.attributes.write().shift_remove(&key)
19141918
});
19151919
}
19161920
}
@@ -2519,11 +2523,11 @@ impl Py<PyType> {
25192523
// Check if we can set this special type attribute
25202524
self.check_set_special_type_attr(identifier!(vm, __doc__), vm)?;
25212525

2522-
PyType::with_type_lock(vm, || {
2526+
let _prev_value = PyType::with_type_lock(vm, || {
25232527
self.modified_inner();
25242528
self.attributes
25252529
.write()
2526-
.insert(identifier!(vm, __doc__), value);
2530+
.insert(identifier!(vm, __doc__), value)
25272531
});
25282532

25292533
Ok(())
@@ -2586,14 +2590,17 @@ impl SetAttr for PyType {
25862590
}
25872591
let assign = value.is_assign();
25882592

2589-
Self::with_type_lock(vm, || {
2593+
// Drop old value OUTSIDE the type lock to avoid deadlock:
2594+
// dropping may trigger weakref callbacks → method calls →
2595+
// LOAD_ATTR specialization → version_for_specialization → type lock.
2596+
let _prev_value = Self::with_type_lock(vm, || {
25902597
// Invalidate inline caches before modifying attributes.
25912598
// This ensures other threads see the version invalidation before
25922599
// any attribute changes, preventing use-after-free of cached descriptors.
25932600
zelf.modified_inner();
25942601

25952602
if let PySetterValue::Assign(value) = value {
2596-
zelf.attributes.write().insert(attr_name, value);
2603+
Ok(zelf.attributes.write().insert(attr_name, value))
25972604
} else {
25982605
let prev_value = zelf.attributes.write().shift_remove(attr_name); // TODO: swap_remove applicable?
25992606
if prev_value.is_none() {
@@ -2603,8 +2610,8 @@ impl SetAttr for PyType {
26032610
attr_name,
26042611
)));
26052612
}
2613+
Ok(prev_value)
26062614
}
2607-
Ok(())
26082615
})?;
26092616

26102617
if attr_name.as_wtf8().starts_with("__") && attr_name.as_wtf8().ends_with("__") {

crates/vm/src/stdlib/_ctypes/base.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,11 +2554,10 @@ fn make_fields(
25542554
}
25552555

25562556
let new_descr = super::PyCField::new_from_field(fdescr, index, offset);
2557-
cls.as_object().set_attr(
2557+
cls.set_attr(
25582558
vm.ctx.intern_str(fname.as_wtf8()),
25592559
new_descr.to_pyobject(vm),
2560-
vm,
2561-
)?;
2560+
);
25622561
}
25632562

25642563
Ok(())
@@ -2597,11 +2596,10 @@ pub(super) fn make_anon_fields(cls: &Py<PyType>, vm: &VirtualMachine) -> PyResul
25972596

25982597
let mut new_descr = super::PyCField::new_from_field(descr, 0, 0);
25992598
new_descr.set_anonymous(true);
2600-
cls.as_object().set_attr(
2599+
cls.set_attr(
26012600
vm.ctx.intern_str(fname.as_wtf8()),
26022601
new_descr.to_pyobject(vm),
2603-
vm,
2604-
)?;
2602+
);
26052603

26062604
make_fields(cls, descr, descr.index, descr.offset, vm)?;
26072605
}

crates/vm/src/stdlib/_ctypes/structure.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -498,11 +498,7 @@ impl PyCStructType {
498498
};
499499

500500
// Set the CField as a class attribute
501-
cls.as_object().set_attr(
502-
vm.ctx.intern_str(name.clone()),
503-
c_field.to_pyobject(vm),
504-
vm,
505-
)?;
501+
cls.set_attr(vm.ctx.intern_str(name.clone()), c_field.to_pyobject(vm));
506502

507503
// Update tracking - don't advance offset for packed bitfields
508504
if field_advances_offset {

crates/vm/src/stdlib/_ctypes/union.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,7 @@ impl PyCUnionType {
312312
PyCField::new(name.clone(), field_type_ref, 0, size as isize, index)
313313
};
314314

315-
cls.as_object()
316-
.set_attr(vm.ctx.intern_str(name), c_field.to_pyobject(vm), vm)?;
315+
cls.set_attr(vm.ctx.intern_str(name), c_field.to_pyobject(vm));
317316
}
318317

319318
// Calculate total_align and aligned_size

0 commit comments

Comments
 (0)