python / python/cpython

Race condition in `FileIO` under free-threading

Đang mở
#151,707 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

extension-modules topic-IO type-bug
Ngôn ngữ chính
Python
Star
77.2k
Fork
35.9k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

Bug report

Bug description:

Several places in File IO (e.g. _io_FileIO_read_impl, _io_FileIO_write_impl, and portable_lseek) read self->fd with no synchronisation, while internal_close may write self->fd = -1 and call close(fd) with no lock either.

https://github.com/python/cpython/blob/d986124d83465190987357f987ee24bd7a817cac/Modules/_io/fileio.c#L122-L130

https://github.com/python/cpython/blob/d986124d83465190987357f987ee24bd7a817cac/Modules/_io/fileio.c#L878-L898

https://github.com/python/cpython/blob/d986124d83465190987357f987ee24bd7a817cac/Modules/_io/fileio.c#L959-L963

When a FileIO object is shared across threads, the following races can occur.

_io_FileIO_read_impl does:

if (self->fd < 0)          // check -- no lock
    return err_closed();
...
n = _Py_read(self->fd, ptr, size);   // use -- no lock

Between the check and the use, another thread can call close, which writes self->fd = -1 in internal_close. The reader then calls _Py_read(-1, ...), gets EBADF, and raises an unexpected OSError.

Alternatively, the file descriptor may also be reused in which case it would read from a different file than the one it "thinks".
This is also the case in portable_lseek which makes an explicit local copy:

int fd = self->fd;
...
lseek(fd, pos, whence);

Reproducer

import os
import threading
import tempfile

def test_concurrent_read_close() -> None:
    errors: list[str] = []
    lock = threading.Lock()

    with tempfile.NamedTemporaryFile(delete=False) as tf:
        path = tf.name
        tf.write(b"hello" * 1000)

    try:
        fio = open(path, "rb", buffering=0)  # FileIO

        def reader() -> None:
            for _ in range(5000):
                try:
                    fio.seek(0)
                    fio.read(10)
                except Exception as e:
                    with lock:
                        errors.append(str(e))

        def closer() -> None:
            for _ in range(100):
                try:
                    fio.close()
                except Exception:
                    pass

        threads = [
            *[threading.Thread(target=reader) for _ in range(4)],
            threading.Thread(target=closer),
        ]
        for t in threads: t.start()
        for t in threads: t.join()

        unexpected = [e for e in errors if "closed file" not in e]
        if unexpected:
            print(f"BUG: {unexpected[:3]}")
    finally:
        os.unlink(path)

for _ in range(20):
    test_concurrent_read_close()

Under TSAN (CPython main, free-threaded build):

==================
WARNING: ThreadSanitizer: data race (pid=77467)
  Read of size 4 at 0x000302acd320 by thread T1:
    #0 _io_FileIO_read_impl fileio.c:878 (python.exe:arm64+0x1004053cc)
    #1 _io_FileIO_read fileio.c.h:353 (python.exe:arm64+0x1004053cc)
    #2 method_vectorcall_FASTCALL_KEYWORDS_METHOD descrobject.c:381 (python.exe:arm64+0x1000a8a78)
    #3 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100090e80)
    #4 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090e80)
    #5 _Py_VectorCall_StackRefSteal ceval.c:724 (python.exe:arm64+0x100290228)
    #6 _PyEval_EvalFrameDefault generated_cases.c.h:4362 (python.exe:arm64+0x10029b364)
    #7 _PyEval_EvalFrame pycore_ceval.h:122 (python.exe:arm64+0x10028fe5c)
    #8 _PyEval_Vector ceval.c:2134 (python.exe:arm64+0x10028fe5c)
    #9 _PyFunction_Vectorcall call.c (python.exe:arm64+0x1000914bc)
    #10 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100092c3c)
    #11 _PyObject_VectorcallPrepend call.c:855 (python.exe:arm64+0x100092c3c)
    #12 method_vectorcall classobject.c:55 (python.exe:arm64+0x100095fdc)
    #13 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x1002dce38)
    #14 context_run context.c:728 (python.exe:arm64+0x1002dce38)
    #15 method_vectorcall_FASTCALL_KEYWORDS descrobject.c:421 (python.exe:arm64+0x1000a8644)
    #16 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100090e80)
    #17 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090e80)
    #18 _Py_VectorCallInstrumentation_StackRefSteal ceval.c:766 (python.exe:arm64+0x100290968)
    #19 _PyEval_EvalFrameDefault generated_cases.c.h:1846 (python.exe:arm64+0x100295ff8)
    #20 _PyEval_EvalFrame pycore_ceval.h:122 (python.exe:arm64+0x10028fe5c)
    #21 _PyEval_Vector ceval.c:2134 (python.exe:arm64+0x10028fe5c)
    #22 _PyFunction_Vectorcall call.c (python.exe:arm64+0x1000914bc)
    #23 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100092c3c)
    #24 _PyObject_VectorcallPrepend call.c:855 (python.exe:arm64+0x100092c3c)
    #25 method_vectorcall classobject.c:55 (python.exe:arm64+0x100095fdc)
    #26 _PyVectorcall_Call call.c:273 (python.exe:arm64+0x10009112c)
    #27 _PyObject_Call call.c:348 (python.exe:arm64+0x10009112c)
    #28 PyObject_Call call.c:373 (python.exe:arm64+0x1000911a4)
    #29 thread_run _threadmodule.c:388 (python.exe:arm64+0x10044df04)
    #30 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10038a12c)

  Previous write of size 4 at 0x000302acd320 by thread T5:
    #0 internal_close fileio.c:128 (python.exe:arm64+0x100406bd8)
    #1 _io_FileIO_close_impl fileio.c:187 (python.exe:arm64+0x10040627c)
    #2 _io_FileIO_close fileio.c.h:34 (python.exe:arm64+0x10040627c)
    #3 method_vectorcall_FASTCALL_KEYWORDS_METHOD descrobject.c:381 (python.exe:arm64+0x1000a8a78)
    #4 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100090e80)
    #5 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090e80)
    #6 _Py_VectorCallInstrumentation_StackRefSteal ceval.c:766 (python.exe:arm64+0x100290968)
    #7 _PyEval_EvalFrameDefault generated_cases.c.h:1846 (python.exe:arm64+0x100295ff8)
    #8 _PyEval_EvalFrame pycore_ceval.h:122 (python.exe:arm64+0x10028fe5c)
    #9 _PyEval_Vector ceval.c:2134 (python.exe:arm64+0x10028fe5c)
    #10 _PyFunction_Vectorcall call.c (python.exe:arm64+0x1000914bc)
    #11 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100092c3c)
    #12 _PyObject_VectorcallPrepend call.c:855 (python.exe:arm64+0x100092c3c)
    #13 method_vectorcall classobject.c:55 (python.exe:arm64+0x100095fdc)
    #14 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x1002dce38)
    #15 context_run context.c:728 (python.exe:arm64+0x1002dce38)
    #16 method_vectorcall_FASTCALL_KEYWORDS descrobject.c:421 (python.exe:arm64+0x1000a8644)
    #17 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100090e80)
    #18 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090e80)
    #19 _Py_VectorCallInstrumentation_StackRefSteal ceval.c:766 (python.exe:arm64+0x100290968)
    #20 _PyEval_EvalFrameDefault generated_cases.c.h:1846 (python.exe:arm64+0x100295ff8)
    #21 _PyEval_EvalFrame pycore_ceval.h:122 (python.exe:arm64+0x10028fe5c)
    #22 _PyEval_Vector ceval.c:2134 (python.exe:arm64+0x10028fe5c)
    #23 _PyFunction_Vectorcall call.c (python.exe:arm64+0x1000914bc)
    #24 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100092c3c)
    #25 _PyObject_VectorcallPrepend call.c:855 (python.exe:arm64+0x100092c3c)
    #26 method_vectorcall classobject.c:55 (python.exe:arm64+0x100095fdc)
    #27 _PyVectorcall_Call call.c:273 (python.exe:arm64+0x10009112c)
    #28 _PyObject_Call call.c:348 (python.exe:arm64+0x10009112c)
    #29 PyObject_Call call.c:373 (python.exe:arm64+0x1000911a4)
    #30 thread_run _threadmodule.c:388 (python.exe:arm64+0x10044df04)
    #31 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10038a12c)

  Thread T1 (tid=19700764, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64+0x31d84)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100389300)
    #2 PyThread_start_joinable_thread thread_pthread.h:323 (python.exe:arm64+0x100389138)
    #3 ThreadHandle_start _threadmodule.c:475 (python.exe:arm64+0x10044dd10)
    #4 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x10044d7dc)
    #5 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x10044c844)
    #6 cfunction_call methodobject.c:564 (python.exe:arm64+0x100138744)
    #7 _PyObject_MakeTpCall call.c:242 (python.exe:arm64+0x10009030c)
    #8 _PyObject_VectorcallTstate pycore_call.h:142 (python.exe:arm64+0x100090f14)
    #9 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090f14)
    #10 _Py_VectorCall_StackRefSteal ceval.c:724 (python.exe:arm64+0x100290228)
    #11 _PyEval_EvalFrameDefault generated_cases.c.h:3325 (python.exe:arm64+0x10029964c)
    #12 _PyEval_EvalFrame pycore_ceval.h:122 (python.exe:arm64+0x10028fa34)
    #13 _PyEval_Vector ceval.c:2134 (python.exe:arm64+0x10028fa34)
    #14 PyEval_EvalCode ceval.c:677 (python.exe:arm64+0x10028fa34)
    #15 run_eval_code_obj pythonrun.c:1369 (python.exe:arm64+0x100366a70)
    #16 run_mod pythonrun.c:1472 (python.exe:arm64+0x1003667cc)
    #17 pyrun_file pythonrun.c:1296 (python.exe:arm64+0x100361d38)
    #18 _PyRun_SimpleFileObject pythonrun.c:518 (python.exe:arm64+0x100361d38)
    #19 _PyRun_AnyFileObject pythonrun.c:81 (python.exe:arm64+0x1003614c8)
    #20 pymain_run_file_obj main.c:411 (python.exe:arm64+0x1003a3328)
    #21 pymain_run_file main.c:430 (python.exe:arm64+0x1003a3328)
    #22 pymain_run_python main.c:715 (python.exe:arm64+0x1003a2658)
    #23 Py_RunMain main.c:796 (python.exe:arm64+0x1003a2658)
    #24 pymain_main main.c:826 (python.exe:arm64+0x1003a2bd0)
    #25 Py_BytesMain main.c:850 (python.exe:arm64+0x1003a2cd0)
    #26 main python.c:15 (python.exe:arm64+0x100000a78)

  Thread T5 (tid=19700768, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64+0x31d84)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100389300)
    #2 PyThread_start_joinable_thread thread_pthread.h:323 (python.exe:arm64+0x100389138)
    #3 ThreadHandle_start _threadmodule.c:475 (python.exe:arm64+0x10044dd10)
    #4 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x10044d7dc)
    #5 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x10044c844)
    #6 cfunction_call methodobject.c:564 (python.exe:arm64+0x100138744)
    #7 _PyObject_MakeTpCall call.c:242 (python.exe:arm64+0x10009030c)
    #8 _PyObject_VectorcallTstate pycore_call.h:142 (python.exe:arm64+0x100090f14)
    #9 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090f14)
    #10 _Py_VectorCall_StackRefSteal ceval.c:724 (python.exe:arm64+0x100290228)
    #11 _PyEval_EvalFrameDefault generated_cases.c.h:3528 (python.exe:arm64+0x100299d1c)
    #12 _PyEval_EvalFrame pycore_ceval.h:122 (python.exe:arm64+0x10028fa34)
    #13 _PyEval_Vector ceval.c:2134 (python.exe:arm64+0x10028fa34)
    #14 PyEval_EvalCode ceval.c:677 (python.exe:arm64+0x10028fa34)
    #15 run_eval_code_obj pythonrun.c:1369 (python.exe:arm64+0x100366a70)
    #16 run_mod pythonrun.c:1472 (python.exe:arm64+0x1003667cc)
    #17 pyrun_file pythonrun.c:1296 (python.exe:arm64+0x100361d38)
    #18 _PyRun_SimpleFileObject pythonrun.c:518 (python.exe:arm64+0x100361d38)
    #19 _PyRun_AnyFileObject pythonrun.c:81 (python.exe:arm64+0x1003614c8)
    #20 pymain_run_file_obj main.c:411 (python.exe:arm64+0x1003a3328)
    #21 pymain_run_file main.c:430 (python.exe:arm64+0x1003a3328)
    #22 pymain_run_python main.c:715 (python.exe:arm64+0x1003a2658)
    #23 Py_RunMain main.c:796 (python.exe:arm64+0x1003a2658)
    #24 pymain_main main.c:826 (python.exe:arm64+0x1003a2bd0)
    #25 Py_BytesMain main.c:850 (python.exe:arm64+0x1003a2cd0)
    #26 main python.c:15 (python.exe:arm64+0x100000a78)

SUMMARY: ThreadSanitizer: data race fileio.c:878 in _io_FileIO_read_impl
==================
CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs
  • gh-151708

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu trong Modules/_io/fileio.c tại _io_FileIO_read_impl, _io_FileIO_write_impl, portable_lseek và internal_close, sau đó xem xét PR được liên kết gh-151708. Tái hiện bằng script đọc/đóng đồng thời được cung cấp trên một free-threaded build và chạy script dưới ThreadSanitizer; được xem là hoàn tất khi data race được báo cáo và các lỗi không mong muốn không còn xảy ra.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
c, python
Lĩnh vực
operating-systems
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
30/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.