Skip to content

Commit 9ca549e

Browse files
authored
Merge pull request #258 from thisisthepy/feat/n12-borrow-guard
Test: borrowed field read mutant (N-12, #193)
2 parents ec69848 + 96f082e commit 9ca549e

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

‎python-multiplatform-ksp/src/test/python/test_free_threaded.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)