python / python/cpython

LIST_APPEND and SET_ADD opcodes safety in Free Threading

オープン
#152,288 コメント 5 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

topic-free-threading
主要言語
Python
スター
77.2k
フォーク
35.9k
PR マージ指標
PR 指標を取得中

説明

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.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

まず、提供されたスレッド付きの再現プログラムを動作確認に使い、SET_ADD と LIST_APPEND を _PySet_AddTakeRef() および set_add_entry_takeref() まで追跡します。thread-sanitized build を含む free-threaded build で実行し、critical section を省略しても安全かどうかを判断します。安全性の根拠が確立され、必要な説明または修正がすべて網羅されれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
backend
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
静か
明瞭さ
おおむね明確
初心者へのやさしさ
48/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。