|
7 | 7 | from test.support import check_free_after_iterating, ALWAYS_EQ, NEVER_EQ
|
8 | 8 | import pickle
|
9 | 9 | import collections.abc
|
| 10 | +import functools |
| 11 | +import contextlib |
| 12 | +import builtins |
10 | 13 |
|
11 | 14 | # Test result of triple loop (too big to inline)
|
12 | 15 | TRIPLETS = [(0, 0, 0), (0, 0, 1), (0, 0, 2),
|
@@ -91,6 +94,12 @@ def __call__(self):
|
91 | 94 | raise IndexError # Emergency stop
|
92 | 95 | return i
|
93 | 96 |
|
| 97 | +class EmptyIterClass: |
| 98 | + def __len__(self): |
| 99 | + return 0 |
| 100 | + def __getitem__(self, i): |
| 101 | + raise StopIteration |
| 102 | + |
94 | 103 | # Main test suite
|
95 | 104 |
|
96 | 105 | class TestCase(unittest.TestCase):
|
@@ -238,6 +247,78 @@ def test_mutating_seq_class_exhausted_iter(self):
|
238 | 247 | self.assertEqual(list(empit), [5, 6])
|
239 | 248 | self.assertEqual(list(a), [0, 1, 2, 3, 4, 5, 6])
|
240 | 249 |
|
| 250 | + def test_reduce_mutating_builtins_iter(self): |
| 251 | + # This is a reproducer of issue #101765 |
| 252 | + # where iter `__reduce__` calls could lead to a segfault or SystemError |
| 253 | + # depending on the order of C argument evaluation, which is undefined |
| 254 | + |
| 255 | + # Backup builtins |
| 256 | + builtins_dict = builtins.__dict__ |
| 257 | + orig = {"iter": iter, "reversed": reversed} |
| 258 | + |
| 259 | + def run(builtin_name, item, sentinel=None): |
| 260 | + it = iter(item) if sentinel is None else iter(item, sentinel) |
| 261 | + |
| 262 | + class CustomStr: |
| 263 | + def __init__(self, name, iterator): |
| 264 | + self.name = name |
| 265 | + self.iterator = iterator |
| 266 | + def __hash__(self): |
| 267 | + return hash(self.name) |
| 268 | + def __eq__(self, other): |
| 269 | + # Here we exhaust our iterator, possibly changing |
| 270 | + # its `it_seq` pointer to NULL |
| 271 | + # The `__reduce__` call should correctly get |
| 272 | + # the pointers after this call |
| 273 | + list(self.iterator) |
| 274 | + return other == self.name |
| 275 | + |
| 276 | + # del is required here |
| 277 | + # to not prematurely call __eq__ from |
| 278 | + # the hash collision with the old key |
| 279 | + del builtins_dict[builtin_name] |
| 280 | + builtins_dict[CustomStr(builtin_name, it)] = orig[builtin_name] |
| 281 | + |
| 282 | + return it.__reduce__() |
| 283 | + |
| 284 | + types = [ |
| 285 | + (EmptyIterClass(),), |
| 286 | + (bytes(8),), |
| 287 | + (bytearray(8),), |
| 288 | + ((1, 2, 3),), |
| 289 | + (lambda: 0, 0), |
| 290 | + (tuple[int],) # GenericAlias |
| 291 | + ] |
| 292 | + |
| 293 | + try: |
| 294 | + run_iter = functools.partial(run, "iter") |
| 295 | + # The returned value of `__reduce__` should not only be valid |
| 296 | + # but also *empty*, as `it` was exhausted during `__eq__` |
| 297 | + # i.e "xyz" returns (iter, ("",)) |
| 298 | + self.assertEqual(run_iter("xyz"), (orig["iter"], ("",))) |
| 299 | + self.assertEqual(run_iter([1, 2, 3]), (orig["iter"], ([],))) |
| 300 | + |
| 301 | + # _PyEval_GetBuiltin is also called for `reversed` in a branch of |
| 302 | + # listiter_reduce_general |
| 303 | + self.assertEqual( |
| 304 | + run("reversed", orig["reversed"](list(range(8)))), |
| 305 | + (iter, ([],)) |
| 306 | + ) |
| 307 | + |
| 308 | + for case in types: |
| 309 | + self.assertEqual(run_iter(*case), (orig["iter"], ((),))) |
| 310 | + finally: |
| 311 | + # Restore original builtins |
| 312 | + for key, func in orig.items(): |
| 313 | + # need to suppress KeyErrors in case |
| 314 | + # a failed test deletes the key without setting anything |
| 315 | + with contextlib.suppress(KeyError): |
| 316 | + # del is required here |
| 317 | + # to not invoke our custom __eq__ from |
| 318 | + # the hash collision with the old key |
| 319 | + del builtins_dict[key] |
| 320 | + builtins_dict[key] = func |
| 321 | + |
241 | 322 | # Test a new_style class with __iter__ but no next() method
|
242 | 323 | def test_new_style_iter_class(self):
|
243 | 324 | class IterClass(object):
|
|
0 commit comments