Skip to content

Race in linecache under free-threading #133253

Closed
@vfdev-5

Description

@vfdev-5

Bug report

Bug description:

There is a time of check to time of use race under free-threading in linecache.py implementation:

cpython/Lib/linecache.py

Lines 128 to 129 in ed039b8

if filename in cache:
if len(cache[filename]) != 1:

Here are two reproducers: one with linecache and second with traceback (a similar usage was originally seen in JAX CI)

  1. Reproducer with linecache
import linecache
import concurrent.futures
import threading


if __name__ == "__main__":
    num_workers = 20
    num_runs = 100

    values = range(10, 15)
    for i in values:
        with open(f"test_{i}.py", "w") as h:
            h.write("import time\n")
            h.write("import system\n")

    barrier = threading.Barrier(num_workers)

    def closure():
        barrier.wait()
        for _ in range(num_runs):
            for name in values:
                linecache.getline(f"test_{name}.py", 1)

    with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
        futures = []
        for i in range(num_workers):
            futures.append(executor.submit(closure))
        assert len(list(f.result() for f in futures)) == num_workers

This gives:

Traceback (most recent call last):
  File "/project/playground/cpython_checks/linecache_race/repro.py", line 28, in <module>
    assert len(list(f.result() for f in futures)) == num_workers
               ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/project/playground/cpython_checks/linecache_race/repro.py", line 28, in <genexpr>
    assert len(list(f.result() for f in futures)) == num_workers
                    ~~~~~~~~^^
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/_base.py", line 443, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/_base.py", line 395, in __get_result
    raise self._exception
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/thread.py", line 86, in run
    result = ctx.run(self.task)
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/thread.py", line 73, in run
    return fn(*args, **kwargs)
  File "/project/playground/cpython_checks/linecache_race/repro.py", line 22, in closure
    linecache.getline(f"test_{name}.py", 1)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/cpython-tsan/lib/python3.14t/linecache.py", line 26, in getline
    lines = getlines(filename, module_globals)
  File "/tmp/cpython-tsan/lib/python3.14t/linecache.py", line 39, in getlines
    return cache[filename][2]
           ~~~~~^^^^^^^^^^
KeyError: 'test_11.py'
  1. Reproducer with traceback:
import traceback
import concurrent.futures
import threading


if __name__ == "__main__":
    num_workers = 20
    num_runs = 100

    barrier = threading.Barrier(num_workers)

    def closure():
        # import test_10

        barrier.wait()
        for _ in range(num_runs):
            try:
                raise RuntimeError("STOP")
            except RuntimeError as e:
                tb = traceback.extract_stack(e.__traceback__.tb_frame)

    with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
        futures = []
        for i in range(num_workers):
            futures.append(executor.submit(closure))
        assert len(list(f.result() for f in futures)) == num_workers

Gives the output:

Traceback (most recent call last):
  File "/project/playground/cpython_checks/linecache_race/repro_traceback.py", line 36, in closure
    raise RuntimeError("STOP")
RuntimeError: STOP

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/project/playground/cpython_checks/linecache_race/repro_traceback.py", line 44, in <module>
    assert len(list(f.result() for f in futures)) == num_workers
               ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/project/playground/cpython_checks/linecache_race/repro_traceback.py", line 44, in <genexpr>
    assert len(list(f.result() for f in futures)) == num_workers
                    ~~~~~~~~^^
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/_base.py", line 443, in result
    return self.__get_result()
           ~~~~~~~~~~~~~~~~~^^
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/_base.py", line 395, in __get_result
    raise self._exception
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/thread.py", line 86, in run
    result = ctx.run(self.task)
  File "/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/thread.py", line 73, in run
    return fn(*args, **kwargs)
  File "/project/playground/cpython_checks/linecache_race/repro_traceback.py", line 38, in closure
    tb = traceback.extract_stack(e.__traceback__.tb_frame)
  File "/tmp/cpython-tsan/lib/python3.14t/traceback.py", line 264, in extract_stack
    stack = StackSummary.extract(walk_stack(f), limit=limit)
  File "/tmp/cpython-tsan/lib/python3.14t/traceback.py", line 457, in extract
    return klass._extract_from_extended_frame_gen(
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        extended_frame_gen(), limit=limit, lookup_lines=lookup_lines,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        capture_locals=capture_locals)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/cpython-tsan/lib/python3.14t/traceback.py", line 508, in _extract_from_extended_frame_gen
    f.line
  File "/tmp/cpython-tsan/lib/python3.14t/traceback.py", line 377, in line
    self._set_lines()
    ~~~~~~~~~~~~~~~^^
  File "/tmp/cpython-tsan/lib/python3.14t/traceback.py", line 355, in _set_lines
    line = linecache.getline(self.filename, lineno).rstrip()
           ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/cpython-tsan/lib/python3.14t/linecache.py", line 26, in getline
    lines = getlines(filename, module_globals)
  File "/tmp/cpython-tsan/lib/python3.14t/linecache.py", line 42, in getlines
    return updatecache(filename, module_globals)
  File "/tmp/cpython-tsan/lib/python3.14t/linecache.py", line 129, in updatecache
    if len(cache[filename]) != 1:
           ~~~~~^^^^^^^^^^
KeyError: '/tmp/cpython-tsan/lib/python3.14t/concurrent/futures/thread.py'

Context: failure in JAX CI, https://github.com/jax-ml/jax/actions/runs/14770168501/job/41468833801#step:18:4022
@hawkinsp

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions