Skip to content

Commit 1b9dc4e

Browse files
authored
Detect list mutation during sort even when list length is unchanged (RustPython#7432)
* Detect list mutation during sort even when list length is unchanged * Use mutation counter instead of capacity check for sort mutation detection The capacity heuristic missed mutations when `clear()` reset capacity to 0 via `mem::take`. An AtomicU32 counter on PyList, incremented in `borrow_vec_mut()`, reliably detects all mutations during sort. * Hold write guard during sort mutation counter reads * Fix list mutation counter race in `borrow_vec_mut`
1 parent 4141e74 commit 1b9dc4e

2 files changed

Lines changed: 18 additions & 6 deletions

File tree

Lib/test/test_sort.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ def test_small_stability(self):
153153

154154
class TestBugs(unittest.TestCase):
155155

156-
@unittest.skip("TODO: RUSTPYTHON; figure out how to detect sort mutation that doesn't change list length")
157156
def test_bug453523(self):
158157
# bug 453523 -- list.sort() crasher.
159158
# If this fails, the most likely outcome is a core dump.
@@ -170,7 +169,6 @@ def __lt__(self, other):
170169
L = [C() for i in range(50)]
171170
self.assertRaises(ValueError, L.sort)
172171

173-
@unittest.expectedFailure # TODO: RUSTPYTHON; figure out how to detect sort mutation that doesn't change list length
174172
def test_undetected_mutation(self):
175173
# Python 2.4a1 did not always detect mutation
176174
memorywaster = []

crates/vm/src/builtins/list.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ use alloc::fmt;
3030
use core::cell::Cell;
3131
use core::ops::DerefMut;
3232
use core::ptr::NonNull;
33+
use core::sync::atomic::{AtomicU32, Ordering};
3334

3435
#[pyclass(module = false, name = "list", unhashable = true, traverse = "manual")]
3536
#[derive(Default)]
3637
pub struct PyList {
3738
elements: PyRwLock<Vec<PyObjectRef>>,
39+
mutation_counter: AtomicU32,
3840
}
3941

4042
impl fmt::Debug for PyList {
@@ -48,6 +50,7 @@ impl From<Vec<PyObjectRef>> for PyList {
4850
fn from(elements: Vec<PyObjectRef>) -> Self {
4951
Self {
5052
elements: PyRwLock::new(elements),
53+
mutation_counter: AtomicU32::new(0),
5154
}
5255
}
5356
}
@@ -135,7 +138,9 @@ impl PyList {
135138
}
136139

137140
pub fn borrow_vec_mut(&self) -> PyRwLockWriteGuard<'_, Vec<PyObjectRef>> {
138-
self.elements.write()
141+
let guard = self.elements.write();
142+
self.mutation_counter.fetch_add(1, Ordering::Relaxed);
143+
guard
139144
}
140145

141146
fn repeat(&self, n: isize, vm: &VirtualMachine) -> PyResult<PyRef<Self>> {
@@ -391,12 +396,21 @@ impl PyList {
391396
// replace list contents with [] for duration of sort.
392397
// this prevents keyfunc from messing with the list and makes it easy to
393398
// check if it tries to append elements to it.
394-
let mut elements = core::mem::take(self.borrow_vec_mut().deref_mut());
399+
let (mut elements, version_before) = {
400+
let mut guard = self.elements.write();
401+
let version_before = self.mutation_counter.load(Ordering::Relaxed);
402+
(core::mem::take(guard.deref_mut()), version_before)
403+
};
395404
let res = do_sort(vm, &mut elements, options.key, options.reverse);
396-
core::mem::swap(self.borrow_vec_mut().deref_mut(), &mut elements);
405+
let mutated = {
406+
let mut guard = self.elements.write();
407+
let mutated = self.mutation_counter.load(Ordering::Relaxed) != version_before;
408+
core::mem::swap(guard.deref_mut(), &mut elements);
409+
mutated
410+
};
397411
res?;
398412

399-
if !elements.is_empty() {
413+
if mutated {
400414
return Err(vm.new_value_error("list modified during sort"));
401415
}
402416

0 commit comments

Comments
 (0)