Skip to content

gh-153297: Fix data race on assigning __name__ and __qualname__ to functions on FT builds#153335

Open
sobolevn wants to merge 6 commits into
python:mainfrom
sobolevn:issue-153297
Open

gh-153297: Fix data race on assigning __name__ and __qualname__ to functions on FT builds#153335
sobolevn wants to merge 6 commits into
python:mainfrom
sobolevn:issue-153297

Conversation

@sobolevn

@sobolevn sobolevn commented Jul 8, 2026

Copy link
Copy Markdown
Member

@sobolevn sobolevn requested a review from colesbury July 8, 2026 13:03
@sobolevn

sobolevn commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

13

1 test failed:
    test_hashlib

failure is not related. Refs #153201

@kumaraditya303

Copy link
Copy Markdown
Contributor

I think there are places where these are read without critical section so it doesn't fix all the cases.

@sobolevn sobolevn requested a review from markshannon as a code owner July 11, 2026 13:10
@sobolevn

Copy link
Copy Markdown
Member Author

Yes, like here :(

// Objects/genobject.c
assert(func->func_name != NULL);
gen->gi_name = Py_NewRef(func->func_name);

It now uses a critical section on func.
And the second fix is for function.__code__, which also now uses critical sections, when accessing __name__.

But, initialize_locals in ceval.c seem safe? I am not sure if I should touch it in any way.

@devdanzin

devdanzin commented Jul 11, 2026

Copy link
Copy Markdown
Member

This PR adds critical sections to __name__, __qualname__, and __code__ — but func_set_defaults / func_get_defaults (and func_set_kwdefaults / func_get_kwdefaults) have the identical Py_XSETREF-setter / Py_NewRef-getter pattern and don't appear to be covered. On a free-threaded build, racing f.__defaults__ read/write segfaults the same way (via a negative refcount in the debug build; func_set_defaults in release):

import sys, threading, time
assert not sys._is_gil_enabled()
class M: pass
def target(): pass
target.__defaults__ = (M(),)
stop = False
def churn():
    while not stop:
        target.__defaults__ = (M(),)
def read():
    while not stop:
        d = target.__defaults__; del d
ts  = [threading.Thread(target=churn) for _ in range(4)]
ts += [threading.Thread(target=read) for _ in range(8)]
for t in ts: t.start()
time.sleep(5); stop = True
for t in ts: t.join()

Debug FT: Python/brc.c:62 _Py_NegativeRefcount: object has negative ref count. Release FT: SIGSEGV in func_set_defaults (funcobject.c) ← PyObject_SetAttr. Worth extending this PR (or a follow-up) to __defaults__/__kwdefaults__ so the whole getsetlist is covered in one pass.

ETA: __annotations__ is also structurally unprotected and not touched by this PR (its getter goes through the lock-free lazy func_get_annotation_dict + func_annotate), but I did not manage to reproduce a crash via __annotations__ in 5 runs the way __defaults__/__kwdefaults__ do. The lazy-annotate path evidently has a narrower window. Flagging it as structurally-suspect but unconfirmed, in case it's worth covering defensively while touching this area.

Investigation and draft by Claude Code (Opus 4.8).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants