@@ -627,6 +627,56 @@ def hammer():
627627 assert stale > 0 , "the mutant (no dict critical section) was not caught"
628628
629629
630+ @pytest .mark .race_mutant
631+ def test_mutant_with_a_borrowed_field_read_is_caught (built , tmp_path ):
632+ """#193 (SPEC N-12): a field read of a shared object must take a strong reference
633+ (PyMember_GetOne). A borrowed read (load the slot, take the reference later) lets a writer
634+ replace the slot and free the old value in between; the concurrent FieldGet test must see it."""
635+ result , path , _ = built
636+ h = (cbuild .RUNTIME_DIR / "tp_runtime.h" ).read_text ()
637+ good = " return PyMember_GetOne((const char *)obj, m);\n "
638+ assert good in h
639+ racy = (" PyObject *v = *(PyObject *volatile *)((char *)obj + m->offset);\n "
640+ " for (volatile int i = 0; i < 2000; i++) { }\n "
641+ " return Py_NewRef(v);\n " )
642+ d = tmp_path / "rt"
643+ d .mkdir ()
644+ (d / "tp_runtime.h" ).write_text (h .replace (good , racy ))
645+ so = cbuild .build (cgen .generate (result .module , path ), "ftmod" , tmp_path / "mut" , runtime_dir = d )
646+ body = """
647+ cell = m.Cell((0, 0), None)
648+ stop = False
649+ bad = []
650+ def writer():
651+ i = 0
652+ while not stop:
653+ m.seta(cell, (i, [i]))
654+ i += 1
655+ def reader():
656+ for _ in range(20000):
657+ v = m.geta(cell)
658+ if type(v) is not tuple or len(v) != 2:
659+ bad.append(1)
660+ wt = threading.Thread(target=writer); wt.start()
661+ errs = run_threads([reader] * 3)
662+ stop = True
663+ wt.join()
664+ print(json.dumps({"bad": len(bad), "errs": len(errs)}))
665+ """
666+ assert run_child (result .extension , body ) == {"bad" : 0 , "errs" : 0 } # the real read is sound
667+ caught = False
668+ for _ in range (3 ):
669+ try :
670+ r = run_child (so , body )
671+ except AssertionError : # the child crashed on a freed object: caught
672+ caught = True
673+ break
674+ if r ["bad" ] or r ["errs" ]:
675+ caught = True
676+ break
677+ assert caught , "the mutant (borrowed field read) was not caught"
678+
679+
630680# --- ThreadSanitizer (best effort) --------------------------------------------------------------------
631681# The interpreter is not instrumented, so TSan sees races between two accesses made by generated C or
632682# the header (the epoch, the class state) but NOT a missing list lock (the list is written by CPython).
0 commit comments