Skip to content

Commit 646671a

Browse files
Copilotcsm10495
andcommitted
Fix infinite generator hanging on extended slice assignment (gh-146268)
When assigning an arbitrary iterable to an extended slice (step != 1), use bounded iteration to collect at most slicelength + 1 items instead of eagerly consuming the entire iterable via PySequence_Fast. This prevents infinite iterators from hanging indefinitely. The slice length is recomputed after consuming the iterable to handle pathological iterators that mutate the list during iteration. Co-authored-by: csm10495 <5749838+csm10495@users.noreply.github.com> Agent-Logs-Url: https://github.com/csm10495/cpython/sessions/996cabbe-7dc6-4faa-b95b-31907ef41b20
1 parent 83360b5 commit 646671a

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

Lib/test/list_tests.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,49 @@ def test_slice_assign_iterator(self):
202202
x[:] = reversed(range(3))
203203
self.assertEqual(x, self.type2test([2, 1, 0]))
204204

205+
def test_extended_slice_assign_infinite_iterator(self):
206+
# gh-146268: assigning an infinite iterator to an extended slice
207+
# (step != 1) should raise ValueError, not hang indefinitely.
208+
def infinite_gen():
209+
while True:
210+
yield "foo"
211+
212+
a = self.type2test(range(4))
213+
with self.assertRaises(ValueError):
214+
a[::2] = infinite_gen()
215+
# list should be unchanged after the failed assignment
216+
self.assertEqual(a, self.type2test(range(4)))
217+
218+
a = self.type2test(range(10))
219+
with self.assertRaises(ValueError):
220+
a[::3] = infinite_gen()
221+
self.assertEqual(a, self.type2test(range(10)))
222+
223+
# Negative step with infinite iterator
224+
a = self.type2test(range(6))
225+
with self.assertRaises(ValueError):
226+
a[::-2] = infinite_gen()
227+
self.assertEqual(a, self.type2test(range(6)))
228+
229+
def test_extended_slice_assign_iterator(self):
230+
# Assigning a finite iterator with the correct length to an
231+
# extended slice should work.
232+
a = self.type2test(range(10))
233+
a[::2] = iter(range(5))
234+
self.assertEqual(a, self.type2test([0, 1, 1, 3, 2, 5, 3, 7, 4, 9]))
235+
236+
# Too few items from an iterator
237+
a = self.type2test(range(10))
238+
with self.assertRaises(ValueError):
239+
a[::2] = iter(range(3))
240+
self.assertEqual(a, self.type2test(range(10)))
241+
242+
# Too many items from an iterator
243+
a = self.type2test(range(10))
244+
with self.assertRaises(ValueError):
245+
a[::2] = iter(range(10))
246+
self.assertEqual(a, self.type2test(range(10)))
247+
205248
def test_delslice(self):
206249
a = self.type2test([0, 1])
207250
del a[1:2]

Objects/listobject.c

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3827,6 +3827,62 @@ list_ass_subscript_lock_held(PyObject *_self, PyObject *item, PyObject *value)
38273827
seq = list_slice_lock_held((PyListObject *)value, 0,
38283828
Py_SIZE(value));
38293829
}
3830+
else if (step != 1 &&
3831+
!PyList_CheckExact(value) &&
3832+
!PyTuple_CheckExact(value))
3833+
{
3834+
/* For extended slices (step != 1) with arbitrary iterables,
3835+
use bounded iteration to avoid hanging on infinite
3836+
iterators (gh-146268). We compute a preliminary slice
3837+
length to cap the number of items we collect. The real
3838+
slice length is recomputed afterwards because the
3839+
iterable's __next__ may mutate the list. */
3840+
Py_ssize_t tmp_start = start, tmp_stop = stop;
3841+
Py_ssize_t slicelength_bound = adjust_slice_indexes(
3842+
self, &tmp_start, &tmp_stop, step);
3843+
3844+
PyObject *it = PyObject_GetIter(value);
3845+
if (it == NULL) {
3846+
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
3847+
PyErr_SetString(PyExc_TypeError,
3848+
"must assign iterable "
3849+
"to extended slice");
3850+
}
3851+
return -1;
3852+
}
3853+
Py_ssize_t alloc = slicelength_bound + 1;
3854+
if (alloc <= 0) {
3855+
/* Overflow or zero-length slice; still collect at
3856+
least 1 item so the size check can detect a
3857+
non-empty iterable. */
3858+
alloc = 1;
3859+
}
3860+
seq = PyList_New(alloc);
3861+
if (seq == NULL) {
3862+
Py_DECREF(it);
3863+
return -1;
3864+
}
3865+
Py_ssize_t j;
3866+
for (j = 0; j < alloc; j++) {
3867+
PyObject *v = PyIter_Next(it);
3868+
if (v == NULL) {
3869+
if (PyErr_Occurred()) {
3870+
/* Discard unfilled slots before decref */
3871+
Py_SET_SIZE(seq, j);
3872+
Py_DECREF(seq);
3873+
Py_DECREF(it);
3874+
return -1;
3875+
}
3876+
break;
3877+
}
3878+
PyList_SET_ITEM(seq, j, v);
3879+
}
3880+
Py_DECREF(it);
3881+
/* Shrink to the number of items actually collected */
3882+
if (j < alloc) {
3883+
Py_SET_SIZE(seq, j);
3884+
}
3885+
}
38303886
else {
38313887
seq = PySequence_Fast(value,
38323888
"must assign iterable "

0 commit comments

Comments
 (0)