python / python/cpython

LIST_APPEND and SET_ADD opcodes safety in Free Threading

未關閉
#152,288 5 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

topic-free-threading
主要語言
Python
星號
77.2k
分支
36k
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. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

研究方向

首先透過 _PySet_AddTakeRef() 和 set_add_entry_takeref() 追蹤 SET_ADD 和 LIST_APPEND,並使用提供的執行緒 reproducer 作為行為檢查。在 free-threaded build 上執行它,包括 thread-sanitized build,並判定省略 critical section 是否安全。完成的標準是已建立安全性依據,並涵蓋任何必要的說明或修正。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
python
領域
backend
Issue 類型
缺陷
難度
4/5
預估耗時
3-5 天
活躍度
冷清
描述清晰度
基本清楚
新手友好度
48/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。