python / python/cpython

Race condition in `FileIO` under free-threading

Open
#151,707 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

extension-modules topic-IO type-bug
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in Modules/_io/fileio.c at _io_FileIO_read_impl, _io_FileIO_write_impl, portable_lseek, and internal_close, then review linked PR gh-151708. Reproduce with the provided concurrent read/close script on a free-threaded build and run it under ThreadSanitizer; done means the reported data race and unexpected errors no longer occur.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, python
Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.