Skip to content

Commit 4a66615

Browse files
authored
bpo-46615: Don't crash when set operations mutate the sets (GH-31120)
Ensure strong references are acquired whenever using `set_next()`. Added randomized test cases for `__eq__` methods that sometimes mutate sets when called.
1 parent 2049469 commit 4a66615

File tree

3 files changed

+226
-8
lines changed

3 files changed

+226
-8
lines changed

Lib/test/test_set.py

+186
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,192 @@ def __eq__(self, o):
18151815
s = {0}
18161816
s.update(other)
18171817

1818+
1819+
class TestOperationsMutating:
1820+
"""Regression test for bpo-46615"""
1821+
1822+
constructor1 = None
1823+
constructor2 = None
1824+
1825+
def make_sets_of_bad_objects(self):
1826+
class Bad:
1827+
def __eq__(self, other):
1828+
if not enabled:
1829+
return False
1830+
if randrange(20) == 0:
1831+
set1.clear()
1832+
if randrange(20) == 0:
1833+
set2.clear()
1834+
return bool(randrange(2))
1835+
def __hash__(self):
1836+
return randrange(2)
1837+
# Don't behave poorly during construction.
1838+
enabled = False
1839+
set1 = self.constructor1(Bad() for _ in range(randrange(50)))
1840+
set2 = self.constructor2(Bad() for _ in range(randrange(50)))
1841+
# Now start behaving poorly
1842+
enabled = True
1843+
return set1, set2
1844+
1845+
def check_set_op_does_not_crash(self, function):
1846+
for _ in range(100):
1847+
set1, set2 = self.make_sets_of_bad_objects()
1848+
try:
1849+
function(set1, set2)
1850+
except RuntimeError as e:
1851+
# Just make sure we don't crash here.
1852+
self.assertIn("changed size during iteration", str(e))
1853+
1854+
1855+
class TestBinaryOpsMutating(TestOperationsMutating):
1856+
1857+
def test_eq_with_mutation(self):
1858+
self.check_set_op_does_not_crash(lambda a, b: a == b)
1859+
1860+
def test_ne_with_mutation(self):
1861+
self.check_set_op_does_not_crash(lambda a, b: a != b)
1862+
1863+
def test_lt_with_mutation(self):
1864+
self.check_set_op_does_not_crash(lambda a, b: a < b)
1865+
1866+
def test_le_with_mutation(self):
1867+
self.check_set_op_does_not_crash(lambda a, b: a <= b)
1868+
1869+
def test_gt_with_mutation(self):
1870+
self.check_set_op_does_not_crash(lambda a, b: a > b)
1871+
1872+
def test_ge_with_mutation(self):
1873+
self.check_set_op_does_not_crash(lambda a, b: a >= b)
1874+
1875+
def test_and_with_mutation(self):
1876+
self.check_set_op_does_not_crash(lambda a, b: a & b)
1877+
1878+
def test_or_with_mutation(self):
1879+
self.check_set_op_does_not_crash(lambda a, b: a | b)
1880+
1881+
def test_sub_with_mutation(self):
1882+
self.check_set_op_does_not_crash(lambda a, b: a - b)
1883+
1884+
def test_xor_with_mutation(self):
1885+
self.check_set_op_does_not_crash(lambda a, b: a ^ b)
1886+
1887+
def test_iadd_with_mutation(self):
1888+
def f(a, b):
1889+
a &= b
1890+
self.check_set_op_does_not_crash(f)
1891+
1892+
def test_ior_with_mutation(self):
1893+
def f(a, b):
1894+
a |= b
1895+
self.check_set_op_does_not_crash(f)
1896+
1897+
def test_isub_with_mutation(self):
1898+
def f(a, b):
1899+
a -= b
1900+
self.check_set_op_does_not_crash(f)
1901+
1902+
def test_ixor_with_mutation(self):
1903+
def f(a, b):
1904+
a ^= b
1905+
self.check_set_op_does_not_crash(f)
1906+
1907+
def test_iteration_with_mutation(self):
1908+
def f1(a, b):
1909+
for x in a:
1910+
pass
1911+
for y in b:
1912+
pass
1913+
def f2(a, b):
1914+
for y in b:
1915+
pass
1916+
for x in a:
1917+
pass
1918+
def f3(a, b):
1919+
for x, y in zip(a, b):
1920+
pass
1921+
self.check_set_op_does_not_crash(f1)
1922+
self.check_set_op_does_not_crash(f2)
1923+
self.check_set_op_does_not_crash(f3)
1924+
1925+
1926+
class TestBinaryOpsMutating_Set_Set(TestBinaryOpsMutating, unittest.TestCase):
1927+
constructor1 = set
1928+
constructor2 = set
1929+
1930+
class TestBinaryOpsMutating_Subclass_Subclass(TestBinaryOpsMutating, unittest.TestCase):
1931+
constructor1 = SetSubclass
1932+
constructor2 = SetSubclass
1933+
1934+
class TestBinaryOpsMutating_Set_Subclass(TestBinaryOpsMutating, unittest.TestCase):
1935+
constructor1 = set
1936+
constructor2 = SetSubclass
1937+
1938+
class TestBinaryOpsMutating_Subclass_Set(TestBinaryOpsMutating, unittest.TestCase):
1939+
constructor1 = SetSubclass
1940+
constructor2 = set
1941+
1942+
1943+
class TestMethodsMutating(TestOperationsMutating):
1944+
1945+
def test_issubset_with_mutation(self):
1946+
self.check_set_op_does_not_crash(set.issubset)
1947+
1948+
def test_issuperset_with_mutation(self):
1949+
self.check_set_op_does_not_crash(set.issuperset)
1950+
1951+
def test_intersection_with_mutation(self):
1952+
self.check_set_op_does_not_crash(set.intersection)
1953+
1954+
def test_union_with_mutation(self):
1955+
self.check_set_op_does_not_crash(set.union)
1956+
1957+
def test_difference_with_mutation(self):
1958+
self.check_set_op_does_not_crash(set.difference)
1959+
1960+
def test_symmetric_difference_with_mutation(self):
1961+
self.check_set_op_does_not_crash(set.symmetric_difference)
1962+
1963+
def test_isdisjoint_with_mutation(self):
1964+
self.check_set_op_does_not_crash(set.isdisjoint)
1965+
1966+
def test_difference_update_with_mutation(self):
1967+
self.check_set_op_does_not_crash(set.difference_update)
1968+
1969+
def test_intersection_update_with_mutation(self):
1970+
self.check_set_op_does_not_crash(set.intersection_update)
1971+
1972+
def test_symmetric_difference_update_with_mutation(self):
1973+
self.check_set_op_does_not_crash(set.symmetric_difference_update)
1974+
1975+
def test_update_with_mutation(self):
1976+
self.check_set_op_does_not_crash(set.update)
1977+
1978+
1979+
class TestMethodsMutating_Set_Set(TestMethodsMutating, unittest.TestCase):
1980+
constructor1 = set
1981+
constructor2 = set
1982+
1983+
class TestMethodsMutating_Subclass_Subclass(TestMethodsMutating, unittest.TestCase):
1984+
constructor1 = SetSubclass
1985+
constructor2 = SetSubclass
1986+
1987+
class TestMethodsMutating_Set_Subclass(TestMethodsMutating, unittest.TestCase):
1988+
constructor1 = set
1989+
constructor2 = SetSubclass
1990+
1991+
class TestMethodsMutating_Subclass_Set(TestMethodsMutating, unittest.TestCase):
1992+
constructor1 = SetSubclass
1993+
constructor2 = set
1994+
1995+
class TestMethodsMutating_Set_Dict(TestMethodsMutating, unittest.TestCase):
1996+
constructor1 = set
1997+
constructor2 = dict.fromkeys
1998+
1999+
class TestMethodsMutating_Set_List(TestMethodsMutating, unittest.TestCase):
2000+
constructor1 = set
2001+
constructor2 = list
2002+
2003+
18182004
# Application tests (based on David Eppstein's graph recipes ====================================
18192005

18202006
def powerset(U):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When iterating over sets internally in ``setobject.c``, acquire strong references to the resulting items from the set. This prevents crashes in corner-cases of various set operations where the set gets mutated.

Objects/setobject.c

+39-8
Original file line numberDiff line numberDiff line change
@@ -1205,17 +1205,21 @@ set_intersection(PySetObject *so, PyObject *other)
12051205
while (set_next((PySetObject *)other, &pos, &entry)) {
12061206
key = entry->key;
12071207
hash = entry->hash;
1208+
Py_INCREF(key);
12081209
rv = set_contains_entry(so, key, hash);
12091210
if (rv < 0) {
12101211
Py_DECREF(result);
1212+
Py_DECREF(key);
12111213
return NULL;
12121214
}
12131215
if (rv) {
12141216
if (set_add_entry(result, key, hash)) {
12151217
Py_DECREF(result);
1218+
Py_DECREF(key);
12161219
return NULL;
12171220
}
12181221
}
1222+
Py_DECREF(key);
12191223
}
12201224
return (PyObject *)result;
12211225
}
@@ -1355,11 +1359,16 @@ set_isdisjoint(PySetObject *so, PyObject *other)
13551359
other = tmp;
13561360
}
13571361
while (set_next((PySetObject *)other, &pos, &entry)) {
1358-
rv = set_contains_entry(so, entry->key, entry->hash);
1359-
if (rv < 0)
1362+
PyObject *key = entry->key;
1363+
Py_INCREF(key);
1364+
rv = set_contains_entry(so, key, entry->hash);
1365+
Py_DECREF(key);
1366+
if (rv < 0) {
13601367
return NULL;
1361-
if (rv)
1368+
}
1369+
if (rv) {
13621370
Py_RETURN_FALSE;
1371+
}
13631372
}
13641373
Py_RETURN_TRUE;
13651374
}
@@ -1418,11 +1427,16 @@ set_difference_update_internal(PySetObject *so, PyObject *other)
14181427
Py_INCREF(other);
14191428
}
14201429

1421-
while (set_next((PySetObject *)other, &pos, &entry))
1422-
if (set_discard_entry(so, entry->key, entry->hash) < 0) {
1430+
while (set_next((PySetObject *)other, &pos, &entry)) {
1431+
PyObject *key = entry->key;
1432+
Py_INCREF(key);
1433+
if (set_discard_entry(so, key, entry->hash) < 0) {
14231434
Py_DECREF(other);
1435+
Py_DECREF(key);
14241436
return -1;
14251437
}
1438+
Py_DECREF(key);
1439+
}
14261440

14271441
Py_DECREF(other);
14281442
} else {
@@ -1513,17 +1527,21 @@ set_difference(PySetObject *so, PyObject *other)
15131527
while (set_next(so, &pos, &entry)) {
15141528
key = entry->key;
15151529
hash = entry->hash;
1530+
Py_INCREF(key);
15161531
rv = _PyDict_Contains_KnownHash(other, key, hash);
15171532
if (rv < 0) {
15181533
Py_DECREF(result);
1534+
Py_DECREF(key);
15191535
return NULL;
15201536
}
15211537
if (!rv) {
15221538
if (set_add_entry((PySetObject *)result, key, hash)) {
15231539
Py_DECREF(result);
1540+
Py_DECREF(key);
15241541
return NULL;
15251542
}
15261543
}
1544+
Py_DECREF(key);
15271545
}
15281546
return result;
15291547
}
@@ -1532,17 +1550,21 @@ set_difference(PySetObject *so, PyObject *other)
15321550
while (set_next(so, &pos, &entry)) {
15331551
key = entry->key;
15341552
hash = entry->hash;
1553+
Py_INCREF(key);
15351554
rv = set_contains_entry((PySetObject *)other, key, hash);
15361555
if (rv < 0) {
15371556
Py_DECREF(result);
1557+
Py_DECREF(key);
15381558
return NULL;
15391559
}
15401560
if (!rv) {
15411561
if (set_add_entry((PySetObject *)result, key, hash)) {
15421562
Py_DECREF(result);
1563+
Py_DECREF(key);
15431564
return NULL;
15441565
}
15451566
}
1567+
Py_DECREF(key);
15461568
}
15471569
return result;
15481570
}
@@ -1639,17 +1661,21 @@ set_symmetric_difference_update(PySetObject *so, PyObject *other)
16391661
while (set_next(otherset, &pos, &entry)) {
16401662
key = entry->key;
16411663
hash = entry->hash;
1664+
Py_INCREF(key);
16421665
rv = set_discard_entry(so, key, hash);
16431666
if (rv < 0) {
16441667
Py_DECREF(otherset);
1668+
Py_DECREF(key);
16451669
return NULL;
16461670
}
16471671
if (rv == DISCARD_NOTFOUND) {
16481672
if (set_add_entry(so, key, hash)) {
16491673
Py_DECREF(otherset);
1674+
Py_DECREF(key);
16501675
return NULL;
16511676
}
16521677
}
1678+
Py_DECREF(key);
16531679
}
16541680
Py_DECREF(otherset);
16551681
Py_RETURN_NONE;
@@ -1724,11 +1750,16 @@ set_issubset(PySetObject *so, PyObject *other)
17241750
Py_RETURN_FALSE;
17251751

17261752
while (set_next(so, &pos, &entry)) {
1727-
rv = set_contains_entry((PySetObject *)other, entry->key, entry->hash);
1728-
if (rv < 0)
1753+
PyObject *key = entry->key;
1754+
Py_INCREF(key);
1755+
rv = set_contains_entry((PySetObject *)other, key, entry->hash);
1756+
Py_DECREF(key);
1757+
if (rv < 0) {
17291758
return NULL;
1730-
if (!rv)
1759+
}
1760+
if (!rv) {
17311761
Py_RETURN_FALSE;
1762+
}
17321763
}
17331764
Py_RETURN_TRUE;
17341765
}

0 commit comments

Comments
 (0)