Bug description
PyContext_AddWatcher() and PyContext_ClearWatcher() are public in Python 3.14's and above free-threaded builds, but CPython's context-watcher registry is accessed without synchronization.
The public declarations are available unconditionally for CPython 3.14+ builds:
The per-interpreter registry consists of two ordinary, non-atomic fields:
All three operations access this shared state without a lock or atomic operations:
notify_context_watchers() reads the bitmask and callback pointers.
PyContext_AddWatcher() scans and writes the callback array, then performs a read-modify-write on the bitmask.
PyContext_ClearWatcher() reads and clears the callback slot, then performs another read-modify-write on the bitmask.
Notification is performed directly by the thread whose current context changes: context_switched() calls notify_context_watchers(), including from PyContext_Enter() and PyContext_Exit(). On a free-threaded interpreter, multiple attached threads in the same interpreter can therefore dispatch callbacks concurrently while another thread calls AddWatcher or ClearWatcher.
This creates CPython-internal data races, independently of whether the callback protects its own state:
| Concurrent operations |
Shared objects |
Possible result |
| AddWatcher / AddWatcher |
Callback slots and active bitmask |
Both callers can select the same empty slot, one callback can overwrite the other, and both can return the same watcher ID. Concurrent bitmask read-modify-writes can also lose an update. |
| AddWatcher / notification |
Callback slots and active bitmask |
Notification can observe an active bit without a safely published callback pointer, or otherwise observe inconsistent/stale registry state. |
| ClearWatcher / notification |
Callback slots and active bitmask |
Notification can snapshot an active bit and then load a callback slot which has become NULL. In a debug build this reaches assert(cb != NULL); in a release build it may attempt to call a NULL or stale function pointer. |
| ClearWatcher / ClearWatcher |
Callback slot and active bitmask |
Both callers can observe the slot as registered, and their unsynchronized reads/writes constitute a data race. |
| AddWatcher / ClearWatcher |
Callback slots and active bitmask |
A slot may be reused while another operation is clearing it, producing a mismatched callback/active-bit state. |
The same implementation is present on the current Python 3.14 branch.
There is also an API-lifetime question: PyContext_ClearWatcher() does not specify whether a callback already selected by another thread may begin or continue after ClearWatcher returns. Without that guarantee, an extension cannot know when it is safe to tear down state used by its callback. Separately, because notifications run on the switching threads, the documentation should say explicitly that a single PyContext_WatchCallback may be invoked concurrently by multiple threads in a free-threaded interpreter.
The context watcher documentation currently specifies registration, clearing, and exception handling, but does not describe any free-threading restrictions or concurrency guarantees.
For comparison:
A startup-only rule would avoid concurrent AddWatcher calls, but it would not by itself make ClearWatcher safe against notifications from active threads. Context switches are normal runtime operations, so external synchronization against internal notification is not generally available to an extension.
Expected behavior
Access to CPython's per-interpreter context-watcher registry should be data-race-free in free-threaded builds.
The API should also define:
- whether the same callback can execute concurrently on multiple threads;
- whether
PyContext_ClearWatcher() waits for, cancels, or permits already-selected/in-flight callbacks;
- when callback-owned state may safely be destroyed after clearing a watcher.
The implementation likely needs synchronization or an atomic publication/snapshot scheme for AddWatcher, ClearWatcher, and notification. Any solution should account for callback reentrancy and avoid holding a registry lock while calling extension callbacks.
At minimum, if concurrent registration or clearing cannot be supported, the documentation should state enforceable startup/shutdown restrictions. Documentation alone does not appear sufficient for ClearWatcher racing with notification unless the API also provides a usable lifetime rule.
CPython versions
- Python 3.14 branch at
e2ec020314487c3fea9807894172becb6c5f4577
- CPython main at
dbac03b411549c0f40fbb66127ae6f445027fcc8
This report is based on source inspection. The races are platform-independent C data races in the free-threaded build; I have not included a probabilistic crash reproducer.
Operating systems
All platforms using a free-threaded CPython build.
Related issues
CPython versions tested on:
3.14
Operating systems tested on:
macOS
Bug description
PyContext_AddWatcher()andPyContext_ClearWatcher()are public in Python 3.14's and above free-threaded builds, but CPython's context-watcher registry is accessed without synchronization.The public declarations are available unconditionally for CPython 3.14+ builds:
PyContext_WatchCallback,PyContext_AddWatcher(), andPyContext_ClearWatcher()The per-interpreter registry consists of two ordinary, non-atomic fields:
PyInterpreterState.context_watchers[CONTEXT_MAX_WATCHERS], an array of callback function pointersPyInterpreterState.active_context_watchers, auint8_tbitmask indicating which slots are activeAll three operations access this shared state without a lock or atomic operations:
notify_context_watchers()reads the bitmask and callback pointers.PyContext_AddWatcher()scans and writes the callback array, then performs a read-modify-write on the bitmask.PyContext_ClearWatcher()reads and clears the callback slot, then performs another read-modify-write on the bitmask.Notification is performed directly by the thread whose current context changes:
context_switched()callsnotify_context_watchers(), including fromPyContext_Enter()andPyContext_Exit(). On a free-threaded interpreter, multiple attached threads in the same interpreter can therefore dispatch callbacks concurrently while another thread calls AddWatcher or ClearWatcher.This creates CPython-internal data races, independently of whether the callback protects its own state:
assert(cb != NULL); in a release build it may attempt to call a NULL or stale function pointer.The same implementation is present on the current Python 3.14 branch.
There is also an API-lifetime question:
PyContext_ClearWatcher()does not specify whether a callback already selected by another thread may begin or continue after ClearWatcher returns. Without that guarantee, an extension cannot know when it is safe to tear down state used by its callback. Separately, because notifications run on the switching threads, the documentation should say explicitly that a singlePyContext_WatchCallbackmay be invoked concurrently by multiple threads in a free-threaded interpreter.The context watcher documentation currently specifies registration, clearing, and exception handling, but does not describe any free-threading restrictions or concurrency guarantees.
For comparison:
PyType_AddWatcher()to run at startup before spawning the first thread.A startup-only rule would avoid concurrent AddWatcher calls, but it would not by itself make ClearWatcher safe against notifications from active threads. Context switches are normal runtime operations, so external synchronization against internal notification is not generally available to an extension.
Expected behavior
Access to CPython's per-interpreter context-watcher registry should be data-race-free in free-threaded builds.
The API should also define:
PyContext_ClearWatcher()waits for, cancels, or permits already-selected/in-flight callbacks;The implementation likely needs synchronization or an atomic publication/snapshot scheme for AddWatcher, ClearWatcher, and notification. Any solution should account for callback reentrancy and avoid holding a registry lock while calling extension callbacks.
At minimum, if concurrent registration or clearing cannot be supported, the documentation should state enforceable startup/shutdown restrictions. Documentation alone does not appear sufficient for ClearWatcher racing with notification unless the API also provides a usable lifetime rule.
CPython versions
e2ec020314487c3fea9807894172becb6c5f4577dbac03b411549c0f40fbb66127ae6f445027fcc8This report is based on source inspection. The races are platform-independent C data races in the free-threaded build; I have not included a probabilistic crash reproducer.
Operating systems
All platforms using a free-threaded CPython build.
Related issues
CPython versions tested on:
3.14
Operating systems tested on:
macOS