python / python/cpython

`functools.lru_cache` critical section locks `self` but `_LockHeld` dict APIs assert `self->cache` is locked

Đang mở
#148,180 5 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-free-threading type-crash
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ả

Crash report

What happened?

On a debug free-threaded build, functools.lru_cache crashes with a critical section assertion failure when gc.get_objects() is called between cache uses.

bounded_lru_cache_wrapper (line 1498) acquires Py_BEGIN_CRITICAL_SECTION(self), locking the lru_cache object's mutex. It then calls bounded_lru_cache_get_lock_held (line 1325) which calls _PyDict_GetItemRef_KnownHash_LockHeld on self->cache. The _LockHeld variant asserts that the dict's per-object mutex is held (_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(mp) at dictobject.c:1254), but only self's mutex is held — self and self->cache have different mutexes.

In 3.15 we have this abort message:

Assertion "(cs != ((void*)0) && cs->_cs_mutex == mutex)" failed: Critical section of object is not held

This was introduced in gh-131757 (PR #131758), which split the critical section to allow the cached function to execute concurrently. The same pattern was extended in gh-132641 (PR #133787) which added more _LockHeld calls following the same assumption.

On non-debug builds the assertion is compiled out and the code happens to work in practice (no actual contention in the reproducer), but the lock contract is violated.

Reproducer

Requires a debug free-threaded build (e.g., --with-pydebug --disable-gil).

from functools import lru_cache
import gc

@lru_cache(maxsize=32)
def get_pep(num):
    return type('', (), {})()

for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
    pep = get_pep(n)
    print(n, pep)

objs = gc.get_objects()
print(dir(get_pep))

for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
    pep = get_pep(n)
    print(n, pep)

Backtrace on 3.14 (trimmed):

python: ./Include/internal/pycore_critical_section.h:226: void _PyCriticalSection_AssertHeld(PyMutex *): Assertion `cs != NULL && cs->_cs_mutex == mutex' failed.
[...]
#5  _PyCriticalSection_AssertHeld (mutex=...) at pycore_critical_section.h:226
#6  _Py_dict_lookup (mp=...) at Objects/dictobject.c:1254
#7  _PyDict_GetItemRef_KnownHash_LockHeld (...) at Objects/dictobject.c:2420
#8  bounded_lru_cache_get_lock_held (...) at Modules/_functoolsmodule.c:1325
#9  bounded_lru_cache_wrapper (...) at Modules/_functoolsmodule.c:1499

Backtrace on 3.15:

./Include/internal/pycore_critical_section.h:249: _PyCriticalSection_AssertHeldObj: Assertion "(cs != ((void*)0) && cs->_cs_mutex == mutex)" failed: Critical section of object is not held
Enable tracemalloc to get the memory block allocation traceback

object address  : 0x7bffb670ccd0
object refcount : 2
object type     : 0x555556749840
object type name: dict
object repr     : {8: <functools._lru_list_elem object at 0x7bffb65701d0>, 290: <functools._lru_list_elem object at 0x7bffb6570240>, 308: <functools._lru_list_elem object at 0x7bffb65702b0>, 320: <functools._lru_list_elem object at 0x7bffb6570320>, 218: <functools._lru_list_elem object at 0x7bffb6570390>, 279: <functools._lru_list_elem object at 0x7bffb6570400>, 289: <functools._lru_list_elem object at 0x7bffb6570470>, 9991: <functools._lru_list_elem object at 0x7bffb65704e0>}

Fatal Python error: _PyObject_AssertFailed: _PyObject_AssertFailed
Python runtime state: initialized

Stack (most recent call first):
  File "/home/danzin/crashers/yifei_01.py", line 15 in <module>

Program received signal SIGABRT, Aborted.
Download failed: Invalid argument.  Continuing without source file ./nptl/./nptl/pthread_kill.c.
__pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
warning: 44     ./nptl/pthread_kill.c: No such file or directory
(gdb) bt
#0  __pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (threadid=<optimized out>, signo=6) at ./nptl/pthread_kill.c:89
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:100
#3  0x00007ffff7c45e2e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7c28888 in __GI_abort () at ./stdlib/abort.c:77
#5  0x000055555606dd54 in fatal_error_exit (status=status@entry=-1) at Python/pylifecycle.c:3389
#6  0x000055555606da1b in fatal_error (fd=fd@entry=2, header=header@entry=1, prefix=prefix@entry=0x555556405360 <__func__._PyObject_AssertFailed> "_PyObject_AssertFailed",
    msg=msg@entry=0x555556405360 <__func__._PyObject_AssertFailed> "_PyObject_AssertFailed", status=status@entry=-1) at Python/pylifecycle.c:3612
#7  0x0000555556069730 in _Py_FatalErrorFunc (func=0x555556405360 <__func__._PyObject_AssertFailed> "_PyObject_AssertFailed",
    msg=0x555556405360 <__func__._PyObject_AssertFailed> "_PyObject_AssertFailed") at Python/pylifecycle.c:3635
#8  0x00005555557fc43b in _PyObject_AssertFailed (obj=obj@entry=0x7bffb670ccd0, expr=<optimized out>, msg=<optimized out>, file=<optimized out>, line=line@entry=249,
    function=<optimized out>) at Objects/object.c:3259
#9  0x0000555555bb7711 in _PyCriticalSection_AssertHeldObj (op=0x7bffb670ccd0) at ./Include/internal/pycore_critical_section.h:247
#10 _Py_dict_lookup (mp=0x7bffb670ccd0, key=key@entry=0x55555693c4f8 <_PyRuntime+30776>, hash=hash@entry=8, value_addr=value_addr@entry=0x7fffffff9d00) at Objects/dictobject.c:1305
#11 0x0000555555bbbc61 in _PyDict_GetItemRef_KnownHash_LockHeld (op=0x196f39, key=0x196f39, key@entry=0x55555693c4f8 <_PyRuntime+30776>, hash=6, hash@entry=8,
    result=result@entry=0x7fffffff9e80) at Objects/dictobject.c:2480
#12 0x00005555562ed221 in bounded_lru_cache_get_lock_held (self=0x7bffb6826c90, args=0x7bffb69bb290, kwds=0x0, result=<optimized out>, key=<optimized out>, hash=<optimized out>)
    at ./Modules/_functoolsmodule.c:1416
#13 bounded_lru_cache_wrapper (self=0x7bffb6826c90, args=0x7bffb69bb290, kwds=0x0) at ./Modules/_functoolsmodule.c:1590
#14 0x0000555555ac6b5d in _PyObject_MakeTpCall (tstate=0x555556997d60 <_PyRuntime+405664>, callable=0x7bffb6826c90, args=0x7fffffffa048, nargs=1, keywords=0x0) at Objects/call.c:242
#15 0x0000555555ea9e3b in _Py_VectorCallInstrumentation_StackRefSteal (callable=..., arguments=arguments@entry=0x7e8ff6fe0290, total_args=total_args@entry=1, kwnames=kwnames@entry=...,
    call_instrumentation=false, frame=frame@entry=0x7e8ff6fe0220, this_instr=0x7bffb6405394, tstate=0x555556997d60 <_PyRuntime+405664>) at Python/ceval.c:775
#16 0x0000555555ee754d in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:1841
#17 0x0000555555ea8d04 in _PyEval_EvalFrame (tstate=0x555556997d60 <_PyRuntime+405664>, frame=0x7e8ff6fe0220, throwflag=0) at ./Include/internal/pycore_ceval.h:118
#18 _PyEval_Vector (tstate=tstate@entry=0x555556997d60 <_PyRuntime+405664>, func=func@entry=0x7bffb64c3e30, locals=locals@entry=0x7bffb670ca90, args=args@entry=0x0,
    argcount=argcount@entry=0, kwnames=kwnames@entry=0x0) at Python/ceval.c:2179
#19 0x0000555555ea80db in PyEval_EvalCode (co=co@entry=0x7bffb6405210, globals=globals@entry=0x7bffb670ca90, locals=locals@entry=0x7bffb670ca90) at Python/ceval.c:686
#20 0x00005555560c6ca0 in run_eval_code_obj (tstate=tstate@entry=0x555556997d60 <_PyRuntime+405664>, co=co@entry=0x7bffb6405210, globals=0x7bffb670ca90, locals=locals@entry=0x7bffb670ca90)
    at Python/pythonrun.c:1369
Possible fix

Either lock both objects:

// Line 1498, 1511: lock both self and self->cache
Py_BEGIN_CRITICAL_SECTION2(self, self->cache);

Or use the regular (self-locking) dict APIs instead of _LockHeld variants for the get path.

The initial crash and reproducer were found by @zhuyifei1999. Root cause analysis and issue draft done with assistance from Claude Code.

CPython versions tested on:

3.15, CPython main branch, 3.14

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.15.0a7+ free-threading build (heads/main:4d0e8ee649c, Mar 29 2026, 23:20:29) [Clang 21.1.2 (2ubuntu6)]

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/_functoolsmodule.c tại bounded_lru_cache_get_lock_held và bounded_lru_cache_wrapper, đặc biệt là các dòng được báo cáo 1325 và 1498. Tái hiện lỗi bằng một bản build free-threaded debug sử dụng script được cung cấp, sau đó kiểm tra contract của critical section đối với self và self->cache. Công việc được xem là hoàn tất khi reproducer không còn kích hoạt assertion và hành vi của lru_cache vẫn đúng.

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
backend
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
45/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.