python / python/cpython

Race condition in `itertools.islice` under free-threading

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

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

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

説明

Bug report

Bug description:

islice_next reads and writes three fields -- lz->cnt, lz->next, and lz->it -- and do operations on them without a critical section.

https://github.com/python/cpython/blob/d986124d83465190987357f987ee24bd7a817cac/Modules/itertoolsmodule.c#L1628-L1661

Two threads calling next on the same islice object concurrently can race:

  • Both read lz->cnt = N < lz->next, both execute the skip-loop body for the same slot, advancing the underlying iterator twice for one step. In that case, the step arithmetic gets corrupted.
  • Both read lz->cnt = N < stop, both pass the stop check, both call iternext(it) and return an item -- the total number of yielded items exceeds stop.
  • There's a race on lz->cnt++ and lz->next += step: both threads can read the same value, then both can increment locally, and both store the same result. After that, the counter is permanently incorrect (such that subsequent skip/stop decisions use a wrong baseline).
  • One of the threads reaches exhaustion (goto empty) and calls Py_CLEAR(lz->it), which sets lz->it = NULL and DECREF's the iterator, which frees it (since each thread has ready it = lz->it. and not INCREF'd it). Another thread can have read lz->it before the clear and be in the middle of iternext(it) on the now-freed object in which case there is a use-after-free.

chain_next for example wraps its body in Py_BEGIN_CRITICAL_SECTION(op), which I believe is what islice should do as well.

https://github.com/python/cpython/blob/d986124d83465190987357f987ee24bd7a817cac/Modules/itertoolsmodule.c#L1937-L1945

Reproducer

import itertools
import threading

STOP = 100
NTHREADS = 8

data = iter(range(STOP + NTHREADS * 2))
sl = itertools.islice(data, STOP)

results: list[int] = []
lock = threading.Lock()

def consume() -> None:
    while True:
        v = next(sl, None)
        if v is None:
            break
        with lock:
            results.append(v)

threads = [threading.Thread(target=consume) for _ in range(NTHREADS)]
for t in threads: t.start()
for t in threads: t.join()

The reproducer, on a free-threaded build, gets reports like this one.

WARNING: ThreadSanitizer: data race (pid=49886)
  Read of size 8 at 0x00030275f8b0 by thread T2:
    #0 islice_next itertoolsmodule.c:1663 (python.exe:arm64+0x10042bce4)
    #1 builtin_next bltinmodule.c:1770 (python.exe:arm64+0x10028a900)
    #2 cfunction_vectorcall_FASTCALL methodobject.c:449 (python.exe:arm64+0x1001379dc)
    #3 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100090e80)
    #4 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090e80)
...
CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs
  • gh-151410

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

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

はじめの一歩

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

調査の方向性

Modules/itertoolsmodule.c の islice_next から開始し、chain_next のクリティカルセクション処理と比較してください。付属のマルチスレッド再現プログラムを free-threaded CPython ビルドで実行し、関連する itertools テストを調査してください。同時実行された next() 呼び出しによって報告された競合状態が発生せず、islice の stop 境界も超えなくなれば完了です。

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

評価

技術スタック
python
領域
backend
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
明確に書かれている
初心者へのやさしさ
35/100

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

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