Skip to content

Commit 1f5fe99

Browse files
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. (cherry picked from commit 4a66615) Co-authored-by: Dennis Sweeney <[email protected]>
1 parent 8b8673f commit 1f5fe99

File tree

3 files changed

+226
-8
lines changed

3 files changed

+226
-8
lines changed

Lib/test/test_set.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,6 +1766,192 @@ def __eq__(self, o):
17661766
s = {0}
17671767
s.update(other)
17681768

1769+
1770+
class TestOperationsMutating:
1771+
"""Regression test for bpo-46615"""
1772+
1773+
constructor1 = None
1774+
constructor2 = None
1775+
1776+
def make_sets_of_bad_objects(self):
1777+
class Bad:
1778+
def __eq__(self, other):
1779+
if not enabled:
1780+
return False
1781+
if randrange(20) == 0:
1782+
set1.clear()
1783+
if randrange(20) == 0:
1784+
set2.clear()
1785+
return bool(randrange(2))
1786+
def __hash__(self):
1787+
return randrange(2)
1788+
# Don't behave poorly during construction.
1789+
enabled = False
1790+
set1 = self.constructor1(Bad() for _ in range(randrange(50)))
1791+
set2 = self.constructor2(Bad() for _ in range(randrange(50)))
1792+
# Now start behaving poorly
1793+
enabled = True
1794+
return set1, set2
1795+
1796+
def check_set_op_does_not_crash(self, function):
1797+
for _ in range(100):
1798+
set1, set2 = self.make_sets_of_bad_objects()
1799+
try:
1800+
function(set1, set2)
1801+
except RuntimeError as e:
1802+
# Just make sure we don't crash here.
1803+
self.assertIn("changed size during iteration", str(e))
1804+
1805+
1806+
class TestBinaryOpsMutating(TestOperationsMutating):
1807+
1808+
def test_eq_with_mutation(self):
1809+
self.check_set_op_does_not_crash(lambda a, b: a == b)
1810+
1811+
def test_ne_with_mutation(self):
1812+
self.check_set_op_does_not_crash(lambda a, b: a != b)
1813+
1814+
def test_lt_with_mutation(self):
1815+
self.check_set_op_does_not_crash(lambda a, b: a < b)
1816+
1817+
def test_le_with_mutation(self):
1818+
self.check_set_op_does_not_crash(lambda a, b: a <= b)
1819+
1820+
def test_gt_with_mutation(self):
1821+
self.check_set_op_does_not_crash(lambda a, b: a > b)
1822+
1823+
def test_ge_with_mutation(self):
1824+
self.check_set_op_does_not_crash(lambda a, b: a >= b)
1825+
1826+
def test_and_with_mutation(self):
1827+
self.check_set_op_does_not_crash(lambda a, b: a & b)
1828+
1829+
def test_or_with_mutation(self):
1830+
self.check_set_op_does_not_crash(lambda a, b: a | b)
1831+
1832+
def test_sub_with_mutation(self):
1833+
self.check_set_op_does_not_crash(lambda a, b: a - b)
1834+
1835+
def test_xor_with_mutation(self):
1836+
self.check_set_op_does_not_crash(lambda a, b: a ^ b)
1837+
1838+
def test_iadd_with_mutation(self):
1839+
def f(a, b):
1840+
a &= b
1841+
self.check_set_op_does_not_crash(f)
1842+
1843+
def test_ior_with_mutation(self):
1844+
def f(a, b):
1845+
a |= b
1846+
self.check_set_op_does_not_crash(f)
1847+
1848+
def test_isub_with_mutation(self):
1849+
def f(a, b):
1850+
a -= b
1851+
self.check_set_op_does_not_crash(f)
1852+
1853+
def test_ixor_with_mutation(self):
1854+
def f(a, b):
1855+
a ^= b
1856+
self.check_set_op_does_not_crash(f)
1857+
1858+
def test_iteration_with_mutation(self):
1859+
def f1(a, b):
1860+
for x in a:
1861+
pass
1862+
for y in b:
1863+
pass
1864+
def f2(a, b):
1865+
for y in b:
1866+
pass
1867+
for x in a:
1868+
pass
1869+
def f3(a, b):
1870+
for x, y in zip(a, b):
1871+
pass
1872+
self.check_set_op_does_not_crash(f1)
1873+
self.check_set_op_does_not_crash(f2)
1874+
self.check_set_op_does_not_crash(f3)
1875+
1876+
1877+
class TestBinaryOpsMutating_Set_Set(TestBinaryOpsMutating, unittest.TestCase):
1878+
constructor1 = set
1879+
constructor2 = set
1880+
1881+
class TestBinaryOpsMutating_Subclass_Subclass(TestBinaryOpsMutating, unittest.TestCase):
1882+
constructor1 = SetSubclass
1883+
constructor2 = SetSubclass
1884+
1885+
class TestBinaryOpsMutating_Set_Subclass(TestBinaryOpsMutating, unittest.TestCase):
1886+
constructor1 = set
1887+
constructor2 = SetSubclass
1888+
1889+
class TestBinaryOpsMutating_Subclass_Set(TestBinaryOpsMutating, unittest.TestCase):
1890+
constructor1 = SetSubclass
1891+
constructor2 = set
1892+
1893+
1894+
class TestMethodsMutating(TestOperationsMutating):
1895+
1896+
def test_issubset_with_mutation(self):
1897+
self.check_set_op_does_not_crash(set.issubset)
1898+
1899+
def test_issuperset_with_mutation(self):
1900+
self.check_set_op_does_not_crash(set.issuperset)
1901+
1902+
def test_intersection_with_mutation(self):
1903+
self.check_set_op_does_not_crash(set.intersection)
1904+
1905+
def test_union_with_mutation(self):
1906+
self.check_set_op_does_not_crash(set.union)
1907+
1908+
def test_difference_with_mutation(self):
1909+
self.check_set_op_does_not_crash(set.difference)
1910+
1911+
def test_symmetric_difference_with_mutation(self):
1912+
self.check_set_op_does_not_crash(set.symmetric_difference)
1913+
1914+
def test_isdisjoint_with_mutation(self):
1915+
self.check_set_op_does_not_crash(set.isdisjoint)
1916+
1917+
def test_difference_update_with_mutation(self):
1918+
self.check_set_op_does_not_crash(set.difference_update)
1919+
1920+
def test_intersection_update_with_mutation(self):
1921+
self.check_set_op_does_not_crash(set.intersection_update)
1922+
1923+
def test_symmetric_difference_update_with_mutation(self):
1924+
self.check_set_op_does_not_crash(set.symmetric_difference_update)
1925+
1926+
def test_update_with_mutation(self):
1927+
self.check_set_op_does_not_crash(set.update)
1928+
1929+
1930+
class TestMethodsMutating_Set_Set(TestMethodsMutating, unittest.TestCase):
1931+
constructor1 = set
1932+
constructor2 = set
1933+
1934+
class TestMethodsMutating_Subclass_Subclass(TestMethodsMutating, unittest.TestCase):
1935+
constructor1 = SetSubclass
1936+
constructor2 = SetSubclass
1937+
1938+
class TestMethodsMutating_Set_Subclass(TestMethodsMutating, unittest.TestCase):
1939+
constructor1 = set
1940+
constructor2 = SetSubclass
1941+
1942+
class TestMethodsMutating_Subclass_Set(TestMethodsMutating, unittest.TestCase):
1943+
constructor1 = SetSubclass
1944+
constructor2 = set
1945+
1946+
class TestMethodsMutating_Set_Dict(TestMethodsMutating, unittest.TestCase):
1947+
constructor1 = set
1948+
constructor2 = dict.fromkeys
1949+
1950+
class TestMethodsMutating_Set_List(TestMethodsMutating, unittest.TestCase):
1951+
constructor1 = set
1952+
constructor2 = list
1953+
1954+
17691955
# Application tests (based on David Eppstein's graph recipes ====================================
17701956

17711957
def powerset(U):
Lines changed: 1 addition & 0 deletions
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

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,17 +1204,21 @@ set_intersection(PySetObject *so, PyObject *other)
12041204
while (set_next((PySetObject *)other, &pos, &entry)) {
12051205
key = entry->key;
12061206
hash = entry->hash;
1207+
Py_INCREF(key);
12071208
rv = set_contains_entry(so, key, hash);
12081209
if (rv < 0) {
12091210
Py_DECREF(result);
1211+
Py_DECREF(key);
12101212
return NULL;
12111213
}
12121214
if (rv) {
12131215
if (set_add_entry(result, key, hash)) {
12141216
Py_DECREF(result);
1217+
Py_DECREF(key);
12151218
return NULL;
12161219
}
12171220
}
1221+
Py_DECREF(key);
12181222
}
12191223
return (PyObject *)result;
12201224
}
@@ -1354,11 +1358,16 @@ set_isdisjoint(PySetObject *so, PyObject *other)
13541358
other = tmp;
13551359
}
13561360
while (set_next((PySetObject *)other, &pos, &entry)) {
1357-
rv = set_contains_entry(so, entry->key, entry->hash);
1358-
if (rv < 0)
1361+
PyObject *key = entry->key;
1362+
Py_INCREF(key);
1363+
rv = set_contains_entry(so, key, entry->hash);
1364+
Py_DECREF(key);
1365+
if (rv < 0) {
13591366
return NULL;
1360-
if (rv)
1367+
}
1368+
if (rv) {
13611369
Py_RETURN_FALSE;
1370+
}
13621371
}
13631372
Py_RETURN_TRUE;
13641373
}
@@ -1417,11 +1426,16 @@ set_difference_update_internal(PySetObject *so, PyObject *other)
14171426
Py_INCREF(other);
14181427
}
14191428

1420-
while (set_next((PySetObject *)other, &pos, &entry))
1421-
if (set_discard_entry(so, entry->key, entry->hash) < 0) {
1429+
while (set_next((PySetObject *)other, &pos, &entry)) {
1430+
PyObject *key = entry->key;
1431+
Py_INCREF(key);
1432+
if (set_discard_entry(so, key, entry->hash) < 0) {
14221433
Py_DECREF(other);
1434+
Py_DECREF(key);
14231435
return -1;
14241436
}
1437+
Py_DECREF(key);
1438+
}
14251439

14261440
Py_DECREF(other);
14271441
} else {
@@ -1512,17 +1526,21 @@ set_difference(PySetObject *so, PyObject *other)
15121526
while (set_next(so, &pos, &entry)) {
15131527
key = entry->key;
15141528
hash = entry->hash;
1529+
Py_INCREF(key);
15151530
rv = _PyDict_Contains_KnownHash(other, key, hash);
15161531
if (rv < 0) {
15171532
Py_DECREF(result);
1533+
Py_DECREF(key);
15181534
return NULL;
15191535
}
15201536
if (!rv) {
15211537
if (set_add_entry((PySetObject *)result, key, hash)) {
15221538
Py_DECREF(result);
1539+
Py_DECREF(key);
15231540
return NULL;
15241541
}
15251542
}
1543+
Py_DECREF(key);
15261544
}
15271545
return result;
15281546
}
@@ -1531,17 +1549,21 @@ set_difference(PySetObject *so, PyObject *other)
15311549
while (set_next(so, &pos, &entry)) {
15321550
key = entry->key;
15331551
hash = entry->hash;
1552+
Py_INCREF(key);
15341553
rv = set_contains_entry((PySetObject *)other, key, hash);
15351554
if (rv < 0) {
15361555
Py_DECREF(result);
1556+
Py_DECREF(key);
15371557
return NULL;
15381558
}
15391559
if (!rv) {
15401560
if (set_add_entry((PySetObject *)result, key, hash)) {
15411561
Py_DECREF(result);
1562+
Py_DECREF(key);
15421563
return NULL;
15431564
}
15441565
}
1566+
Py_DECREF(key);
15451567
}
15461568
return result;
15471569
}
@@ -1638,17 +1660,21 @@ set_symmetric_difference_update(PySetObject *so, PyObject *other)
16381660
while (set_next(otherset, &pos, &entry)) {
16391661
key = entry->key;
16401662
hash = entry->hash;
1663+
Py_INCREF(key);
16411664
rv = set_discard_entry(so, key, hash);
16421665
if (rv < 0) {
16431666
Py_DECREF(otherset);
1667+
Py_DECREF(key);
16441668
return NULL;
16451669
}
16461670
if (rv == DISCARD_NOTFOUND) {
16471671
if (set_add_entry(so, key, hash)) {
16481672
Py_DECREF(otherset);
1673+
Py_DECREF(key);
16491674
return NULL;
16501675
}
16511676
}
1677+
Py_DECREF(key);
16521678
}
16531679
Py_DECREF(otherset);
16541680
Py_RETURN_NONE;
@@ -1723,11 +1749,16 @@ set_issubset(PySetObject *so, PyObject *other)
17231749
Py_RETURN_FALSE;
17241750

17251751
while (set_next(so, &pos, &entry)) {
1726-
rv = set_contains_entry((PySetObject *)other, entry->key, entry->hash);
1727-
if (rv < 0)
1752+
PyObject *key = entry->key;
1753+
Py_INCREF(key);
1754+
rv = set_contains_entry((PySetObject *)other, key, entry->hash);
1755+
Py_DECREF(key);
1756+
if (rv < 0) {
17281757
return NULL;
1729-
if (!rv)
1758+
}
1759+
if (!rv) {
17301760
Py_RETURN_FALSE;
1761+
}
17311762
}
17321763
Py_RETURN_TRUE;
17331764
}

0 commit comments

Comments
 (0)