From 6b2e216a9bfbd47220858a3bfa65998e51de7ee9 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Tue, 20 Jun 2023 17:39:03 -0700 Subject: [PATCH 1/3] Add some tests for the thread-safety of PEP 659 --- Lib/test/test_opcache.py | 512 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 511 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index 5281eb77c02d1b..b14d62fcde8610 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -1,3 +1,6 @@ +import dis +import threading +import types import unittest @@ -481,6 +484,513 @@ def f(x, y): f() +class TestRacesDoNotCrash(unittest.TestCase): + # Careful with these. Bigger numbers have a higher chance of catching bugs, + # but you can also burn through a *ton* of type/dict/function versions: + ITEMS = 1 << 10 + LOOPS = 1 << 2 + WARMUPS = 1 << 1 + WRITERS = 1 << 1 + + def assert_specialized(self, f, opname): + instructions = dis.get_instructions(f, adaptive=True) + opnames = {instruction.opname for instruction in instructions} + self.assertIn(opname, opnames) + + def assert_races_do_not_crash( + self, opname, get_items, read, write, *, check_items=False + ): + # This might need a few dozen loops in some cases: + for _ in range(self.LOOPS): + items = get_items() + # Reset: + if check_items: + for item in items: + item.__code__ = item.__code__.replace() + else: + read.__code__ = read.__code__.replace() + # Specialize: + for _ in range(self.WARMUPS): + read(items) + if check_items: + for item in items: + self.assert_specialized(item, opname) + else: + self.assert_specialized(read, opname) + # Create writers: + writers = [] + for _ in range(self.WRITERS): + writer = threading.Thread(target=write, args=[items]) + writers.append(writer) + # Run: + for writer in writers: + writer.start() + read(items) # BOOM! + for writer in writers: + writer.join() + + def test_binary_subscr_getitem(self): + def get_items(): + class C: + __getitem__ = lambda self, item: None + + items = [] + for _ in range(self.ITEMS): + item = C() + items.append(item) + return items + + def read(items): + for item in items: + try: + item[None] + except TypeError: + pass + + def write(items): + for item in items: + try: + del item.__getitem__ + except AttributeError: + pass + type(item).__getitem__ = lambda self, item: None + + opname = "BINARY_SUBSCR_GETITEM" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_binary_subscr_list_int(self): + def get_items(): + items = [] + for _ in range(self.ITEMS): + item = [None] + items.append(item) + return items + + def read(items): + for item in items: + try: + item[0] + except IndexError: + pass + + def write(items): + for item in items: + item.clear() + item.append(None) + + opname = "BINARY_SUBSCR_LIST_INT" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_for_iter_gen(self): + def get_items(): + def g(): + yield + yield + + items = [] + for _ in range(self.ITEMS): + item = g() + items.append(item) + return items + + def read(items): + for item in items: + try: + for _ in item: + break + except ValueError: + pass + + def write(items): + for item in items: + try: + for _ in item: + break + except ValueError: + pass + + opname = "FOR_ITER_GEN" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_for_iter_list(self): + def get_items(): + items = [] + for _ in range(self.ITEMS): + item = [None] + items.append(item) + return items + + def read(items): + for item in items: + for item in item: + break + + def write(items): + for item in items: + item.clear() + item.append(None) + + opname = "FOR_ITER_LIST" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_class(self): + def get_items(): + class C: + a = object() + + items = [] + for _ in range(self.ITEMS): + item = C + items.append(item) + return items + + def read(items): + for item in items: + try: + item.a + except AttributeError: + pass + + def write(items): + for item in items: + try: + del item.a + except AttributeError: + pass + item.a = object() + + opname = "LOAD_ATTR_CLASS" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_getattribute_overridden(self): + def get_items(): + class C: + __getattribute__ = lambda self, name: None + + items = [] + for _ in range(self.ITEMS): + item = C() + items.append(item) + return items + + def read(items): + for item in items: + try: + item.a + except AttributeError: + pass + + def write(items): + for item in items: + try: + del item.__getattribute__ + except AttributeError: + pass + type(item).__getattribute__ = lambda self, name: None + + opname = "LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_instance_value(self): + def get_items(): + class C: + pass + + items = [] + for _ in range(self.ITEMS): + item = C() + item.a = None + items.append(item) + return items + + def read(items): + for item in items: + item.a + + def write(items): + for item in items: + item.__dict__[None] = None + + opname = "LOAD_ATTR_INSTANCE_VALUE" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_method_lazy_dict(self): + def get_items(): + class C(Exception): + m = lambda self: None + + items = [] + for _ in range(self.ITEMS): + item = C() + items.append(item) + return items + + def read(items): + for item in items: + try: + item.m() + except AttributeError: + pass + + def write(items): + for item in items: + try: + del item.m + except AttributeError: + pass + type(item).m = lambda self: None + + opname = "LOAD_ATTR_METHOD_LAZY_DICT" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_method_no_dict(self): + def get_items(): + class C: + __slots__ = () + m = lambda self: None + + items = [] + for _ in range(self.ITEMS): + item = C() + items.append(item) + return items + + def read(items): + for item in items: + try: + item.m() + except AttributeError: + pass + + def write(items): + for item in items: + try: + del item.m + except AttributeError: + pass + type(item).m = lambda self: None + + opname = "LOAD_ATTR_METHOD_NO_DICT" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_method_with_values(self): + def get_items(): + class C: + m = lambda self: None + + items = [] + for _ in range(self.ITEMS): + item = C() + items.append(item) + return items + + def read(items): + for item in items: + try: + item.m() + except AttributeError: + pass + + def write(items): + for item in items: + try: + del item.m + except AttributeError: + pass + type(item).m = lambda self: None + + opname = "LOAD_ATTR_METHOD_WITH_VALUES" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_module(self): + def get_items(): + items = [] + for _ in range(self.ITEMS): + item = types.ModuleType("") + items.append(item) + return items + + def read(items): + for item in items: + try: + item.__name__ + except AttributeError: + pass + + def write(items): + for item in items: + d = item.__dict__.copy() + item.__dict__.clear() + item.__dict__.update(d) + + opname = "LOAD_ATTR_MODULE" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_property(self): + def get_items(): + class C: + a = property(lambda self: None) + + items = [] + for _ in range(self.ITEMS): + item = C() + items.append(item) + return items + + def read(items): + for item in items: + try: + item.a + except AttributeError: + pass + + def write(items): + for item in items: + try: + del type(item).a + except AttributeError: + pass + type(item).a = property(lambda self: None) + + opname = "LOAD_ATTR_PROPERTY" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_attr_with_hint(self): + def get_items(): + class C: + pass + + items = [] + for _ in range(self.ITEMS): + item = C() + item.__dict__ + item.a = None + items.append(item) + return items + + def read(items): + for item in items: + item.a + + def write(items): + for item in items: + item.__dict__[None] = None + + opname = "LOAD_ATTR_WITH_HINT" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_load_global_module(self): + def get_items(): + items = [] + for _ in range(self.ITEMS): + item = eval("lambda: x", {"x": None}) + items.append(item) + return items + + def read(items): + for item in items: + item() + + def write(items): + for item in items: + item.__globals__[None] = None + + opname = "LOAD_GLOBAL_MODULE" + self.assert_races_do_not_crash( + opname, get_items, read, write, check_items=True + ) + + def test_store_attr_instance_value(self): + def get_items(): + class C: + pass + + items = [] + for _ in range(self.ITEMS): + item = C() + items.append(item) + return items + + def read(items): + for item in items: + item.a = None + + def write(items): + for item in items: + item.__dict__[None] = None + + opname = "STORE_ATTR_INSTANCE_VALUE" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_store_attr_with_hint(self): + def get_items(): + class C: + pass + + items = [] + for _ in range(self.ITEMS): + item = C() + item.__dict__ + items.append(item) + return items + + def read(items): + for item in items: + item.a = None + + def write(items): + for item in items: + item.__dict__[None] = None + + opname = "STORE_ATTR_WITH_HINT" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_store_subscr_list_int(self): + def get_items(): + items = [] + for _ in range(self.ITEMS): + item = [None] + items.append(item) + return items + + def read(items): + for item in items: + try: + item[0] = None + except IndexError: + pass + + def write(items): + for item in items: + item.clear() + item.append(None) + + opname = "STORE_SUBSCR_LIST_INT" + self.assert_races_do_not_crash(opname, get_items, read, write) + + def test_unpack_sequence_list(self): + def get_items(): + items = [] + for _ in range(self.ITEMS): + item = [None] + items.append(item) + return items + + def read(items): + for item in items: + try: + [_] = item + except ValueError: + pass + + def write(items): + for item in items: + item.clear() + item.append(None) + + opname = "UNPACK_SEQUENCE_LIST" + self.assert_races_do_not_crash(opname, get_items, read, write) + + if __name__ == "__main__": - import unittest unittest.main() From 4de3724e44e5144ec580fe76ab37ee06291a72a4 Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Tue, 20 Jun 2023 17:43:39 -0700 Subject: [PATCH 2/3] make patchcheck --- Lib/test/test_opcache.py | 66 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index b14d62fcde8610..dbbc75be788fb4 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -539,7 +539,7 @@ class C: item = C() items.append(item) return items - + def read(items): for item in items: try: @@ -554,7 +554,7 @@ def write(items): except AttributeError: pass type(item).__getitem__ = lambda self, item: None - + opname = "BINARY_SUBSCR_GETITEM" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -565,7 +565,7 @@ def get_items(): item = [None] items.append(item) return items - + def read(items): for item in items: try: @@ -577,7 +577,7 @@ def write(items): for item in items: item.clear() item.append(None) - + opname = "BINARY_SUBSCR_LIST_INT" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -592,7 +592,7 @@ def g(): item = g() items.append(item) return items - + def read(items): for item in items: try: @@ -608,7 +608,7 @@ def write(items): break except ValueError: pass - + opname = "FOR_ITER_GEN" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -619,7 +619,7 @@ def get_items(): item = [None] items.append(item) return items - + def read(items): for item in items: for item in item: @@ -629,7 +629,7 @@ def write(items): for item in items: item.clear() item.append(None) - + opname = "FOR_ITER_LIST" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -643,7 +643,7 @@ class C: item = C items.append(item) return items - + def read(items): for item in items: try: @@ -658,7 +658,7 @@ def write(items): except AttributeError: pass item.a = object() - + opname = "LOAD_ATTR_CLASS" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -672,7 +672,7 @@ class C: item = C() items.append(item) return items - + def read(items): for item in items: try: @@ -687,7 +687,7 @@ def write(items): except AttributeError: pass type(item).__getattribute__ = lambda self, name: None - + opname = "LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -702,7 +702,7 @@ class C: item.a = None items.append(item) return items - + def read(items): for item in items: item.a @@ -710,7 +710,7 @@ def read(items): def write(items): for item in items: item.__dict__[None] = None - + opname = "LOAD_ATTR_INSTANCE_VALUE" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -724,7 +724,7 @@ class C(Exception): item = C() items.append(item) return items - + def read(items): for item in items: try: @@ -739,7 +739,7 @@ def write(items): except AttributeError: pass type(item).m = lambda self: None - + opname = "LOAD_ATTR_METHOD_LAZY_DICT" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -754,7 +754,7 @@ class C: item = C() items.append(item) return items - + def read(items): for item in items: try: @@ -769,7 +769,7 @@ def write(items): except AttributeError: pass type(item).m = lambda self: None - + opname = "LOAD_ATTR_METHOD_NO_DICT" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -783,7 +783,7 @@ class C: item = C() items.append(item) return items - + def read(items): for item in items: try: @@ -798,7 +798,7 @@ def write(items): except AttributeError: pass type(item).m = lambda self: None - + opname = "LOAD_ATTR_METHOD_WITH_VALUES" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -809,7 +809,7 @@ def get_items(): item = types.ModuleType("") items.append(item) return items - + def read(items): for item in items: try: @@ -822,7 +822,7 @@ def write(items): d = item.__dict__.copy() item.__dict__.clear() item.__dict__.update(d) - + opname = "LOAD_ATTR_MODULE" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -836,7 +836,7 @@ class C: item = C() items.append(item) return items - + def read(items): for item in items: try: @@ -851,7 +851,7 @@ def write(items): except AttributeError: pass type(item).a = property(lambda self: None) - + opname = "LOAD_ATTR_PROPERTY" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -867,7 +867,7 @@ class C: item.a = None items.append(item) return items - + def read(items): for item in items: item.a @@ -875,7 +875,7 @@ def read(items): def write(items): for item in items: item.__dict__[None] = None - + opname = "LOAD_ATTR_WITH_HINT" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -894,7 +894,7 @@ def read(items): def write(items): for item in items: item.__globals__[None] = None - + opname = "LOAD_GLOBAL_MODULE" self.assert_races_do_not_crash( opname, get_items, read, write, check_items=True @@ -910,7 +910,7 @@ class C: item = C() items.append(item) return items - + def read(items): for item in items: item.a = None @@ -918,7 +918,7 @@ def read(items): def write(items): for item in items: item.__dict__[None] = None - + opname = "STORE_ATTR_INSTANCE_VALUE" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -933,7 +933,7 @@ class C: item.__dict__ items.append(item) return items - + def read(items): for item in items: item.a = None @@ -941,7 +941,7 @@ def read(items): def write(items): for item in items: item.__dict__[None] = None - + opname = "STORE_ATTR_WITH_HINT" self.assert_races_do_not_crash(opname, get_items, read, write) @@ -952,7 +952,7 @@ def get_items(): item = [None] items.append(item) return items - + def read(items): for item in items: try: @@ -964,7 +964,7 @@ def write(items): for item in items: item.clear() item.append(None) - + opname = "STORE_SUBSCR_LIST_INT" self.assert_races_do_not_crash(opname, get_items, read, write) From f4d7f102fd2c95c37dbe8a6473dd37225edb64fe Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Thu, 22 Jun 2023 14:33:44 -0700 Subject: [PATCH 3/3] Numbers --- Lib/test/test_opcache.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py index dbbc75be788fb4..2f6f91ded248bb 100644 --- a/Lib/test/test_opcache.py +++ b/Lib/test/test_opcache.py @@ -487,10 +487,10 @@ def f(x, y): class TestRacesDoNotCrash(unittest.TestCase): # Careful with these. Bigger numbers have a higher chance of catching bugs, # but you can also burn through a *ton* of type/dict/function versions: - ITEMS = 1 << 10 - LOOPS = 1 << 2 - WARMUPS = 1 << 1 - WRITERS = 1 << 1 + ITEMS = 1000 + LOOPS = 4 + WARMUPS = 2 + WRITERS = 2 def assert_specialized(self, f, opname): instructions = dis.get_instructions(f, adaptive=True)