@@ -1524,3 +1524,86 @@ def test_multiple_params() -> None:
15241524 # Different parameter name than loop variable
15251525 funcs2 = uses_multiple_params_different_name(["a", "b"], "!")
15261526 assert [f() for f in funcs2] == ["a!", "b!"]
1527+
1528+ [case testConcurrentFirstCallWithKeywordArgs]
1529+ # Regression test for a free-threading data race in argument parser
1530+ # initialization. The first call to a compiled function with keyword arguments
1531+ # lazily initializes a static CPyArg_Parser and pushes it onto a global list.
1532+ # When many threads race that first call on a free-threaded build, the
1533+ # initialization and list insertion must be synchronized, or the runtime hits
1534+ # a failed assertion (parser->next == NULL) and aborts.
1535+ import sys
1536+ import threading
1537+
1538+ # A pool of distinct functions, none of which is called before the concurrent
1539+ # test. Each function's first call lazily initializes a static CPyArg_Parser,
1540+ # and the test races that initialization across threads. Keyword arguments
1541+ # force the call through the slow path that runs parser_init. Using many
1542+ # functions gives many independent races per run, so the test reliably triggers
1543+ # the bug on an unfixed free-threaded build instead of depending on a single
1544+ # narrow timing window.
1545+ def g0(a: int, b: int, c: int) -> int: return a + b + c
1546+ def g1(a: int, b: int, c: int) -> int: return a + b + c
1547+ def g2(a: int, b: int, c: int) -> int: return a + b + c
1548+ def g3(a: int, b: int, c: int) -> int: return a + b + c
1549+ def g4(a: int, b: int, c: int) -> int: return a + b + c
1550+ def g5(a: int, b: int, c: int) -> int: return a + b + c
1551+ def g6(a: int, b: int, c: int) -> int: return a + b + c
1552+ def g7(a: int, b: int, c: int) -> int: return a + b + c
1553+ def g8(a: int, b: int, c: int) -> int: return a + b + c
1554+ def g9(a: int, b: int, c: int) -> int: return a + b + c
1555+ def g10(a: int, b: int, c: int) -> int: return a + b + c
1556+ def g11(a: int, b: int, c: int) -> int: return a + b + c
1557+ def g12(a: int, b: int, c: int) -> int: return a + b + c
1558+ def g13(a: int, b: int, c: int) -> int: return a + b + c
1559+ def g14(a: int, b: int, c: int) -> int: return a + b + c
1560+ def g15(a: int, b: int, c: int) -> int: return a + b + c
1561+ def g16(a: int, b: int, c: int) -> int: return a + b + c
1562+ def g17(a: int, b: int, c: int) -> int: return a + b + c
1563+ def g18(a: int, b: int, c: int) -> int: return a + b + c
1564+ def g19(a: int, b: int, c: int) -> int: return a + b + c
1565+ def g20(a: int, b: int, c: int) -> int: return a + b + c
1566+ def g21(a: int, b: int, c: int) -> int: return a + b + c
1567+ def g22(a: int, b: int, c: int) -> int: return a + b + c
1568+ def g23(a: int, b: int, c: int) -> int: return a + b + c
1569+ def g24(a: int, b: int, c: int) -> int: return a + b + c
1570+ def g25(a: int, b: int, c: int) -> int: return a + b + c
1571+ def g26(a: int, b: int, c: int) -> int: return a + b + c
1572+ def g27(a: int, b: int, c: int) -> int: return a + b + c
1573+ def g28(a: int, b: int, c: int) -> int: return a + b + c
1574+ def g29(a: int, b: int, c: int) -> int: return a + b + c
1575+
1576+ FUNCS = [g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, g10, g11, g12, g13, g14,
1577+ g15, g16, g17, g18, g19, g20, g21, g22, g23, g24, g25, g26, g27,
1578+ g28, g29]
1579+
1580+ def is_gil_disabled() -> bool:
1581+ return hasattr(sys, "_is_gil_enabled") and not sys._is_gil_enabled()
1582+
1583+ def test_concurrent_first_call() -> None:
1584+ if not is_gil_disabled():
1585+ # The race can only happen without the GIL.
1586+ return
1587+
1588+ num_threads = 16
1589+ barrier = threading.Barrier(num_threads)
1590+ errors: list[str] = []
1591+
1592+ def run() -> None:
1593+ # Line up all threads, then let them race freely through the list. The
1594+ # first call to each function lazily initializes its parser, so every
1595+ # function is a fresh race under real parallel pressure.
1596+ barrier.wait()
1597+ try:
1598+ for fn in FUNCS:
1599+ assert fn(a=1, b=2, c=3) == 6
1600+ except BaseException as e:
1601+ errors.append(repr(e))
1602+
1603+ threads = [threading.Thread(target=run) for _ in range(num_threads)]
1604+ for t in threads:
1605+ t.start()
1606+ for t in threads:
1607+ t.join()
1608+
1609+ assert not errors, errors
0 commit comments