python / python/cpython

LIST_APPEND and SET_ADD opcodes safety in Free Threading

Offen
#152,288 5 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

topic-free-threading
Vorherrschende Sprache
Python
Sterne
77.2k
Forks
35.9k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

Hi,

While reviewing PR gh-152273, I moved _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(so); check to set_add_entry_takeref() to check for bugs. I discovered that SET_ADD opcode calls _PySet_AddTakeRef() without getting the set critical section. That's surprising and IMO it would deserve adding a comment to explain why it's safe to omit locking here.

LIST_APPEND has a similar design (also omit locking).


The following code uses the evil gc.get_objects() function to call set.add() in a different thread while the main thread is building a set:

import dis
import gc
import threading

GLOBAL_SET = None

def worker():
    EVENT.wait()
    obj = GLOBAL_SET
    for i in range(100):
        # Call set.add() which uses the critical section
        obj.add(i)


MARKER = b"MARKER".decode()
EVENT = threading.Event()

class EvilHash:
    def __init__(self, hash_value):
        self.hash_value = hash_value

    def __hash__(self):
        global GLOBAL_SET, EVENT
        if GLOBAL_SET is None:
            # Invoke the evil gc.get_objects()!
            for obj in gc.get_objects():
                if isinstance(obj, set) and MARKER in obj:
                    GLOBAL_SET = obj
                    EVENT.set()
        return self.hash_value

    def __repr__(self):
        return f"EvilHash({self.hash_value})"

def func():
    build_set = {
        # Use *list so following items are added by SET_ADD opcode
        *[MARKER],
        # Added by SET_ADD which calls _PySet_AddTakeRef()
        # without the critical section
        EvilHash(0), EvilHash(1), EvilHash(2), EvilHash(3), EvilHash(4),
        EvilHash(5)}

    print(build_set)
    print("Length:", len(build_set))
    if len(build_set) != 107:
        raise Exception("race condition!")

thread = threading.Thread(target=worker)
thread.start()
func()
thread.join()
#dis.dis(func)

I expected the code to fail randomly, but so far I failed to trigger a race condition on Free Threading. Running the code on Python built with --with-thread-sanitizer doesn't show any warning.

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne damit, SET_ADD und LIST_APPEND durch _PySet_AddTakeRef() und set_add_entry_takeref() zu verfolgen, wobei der bereitgestellte Thread-Reproducer als Verhaltensprüfung dient. Führe ihn auf einem free-threaded build aus, einschließlich des thread-sanitized build, und ermittle, ob das Weglassen des critical section sicher ist. Als abgeschlossen gilt die Aufgabe, wenn die Begründung für die Sicherheit geklärt ist und jede erforderliche Erklärung oder Korrektur abgedeckt ist.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
backend
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Ruhig
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
48/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.