Skip to content

Commit

Permalink
pythongh-124872: Mark the thread's initial context as entered
Browse files Browse the repository at this point in the history
Starting with commit 843d28f
(temporarily reverted in d3c82b9 and
restored in commit bee112a), it is
now technically possible to access a thread's initial context created
by `context_get`.  Mark that context as entered so that developers
cannot push that context onto the thread's stack a second time, which
would cause a cycle.

(Even if the `CONTEXT_SWITCHED` event is removed, this is good
defensive practice, and the consistent treatment of all contexts on
the stack makes it easier to understand the code.)
  • Loading branch information
rhansen committed Oct 16, 2024
1 parent 8e7b2a1 commit 81d2a0d
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 9 deletions.
16 changes: 16 additions & 0 deletions Lib/test/test_capi/test_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,5 +659,21 @@ def test_exit_base_context(self):
ctx.run(lambda: None)
self.assertEqual(switches, [ctx, None])

def test_reenter_base_context(self):
_testcapi.clear_context_stack()
# contextvars.copy_context() creates the base context (via the
# context_get C function).
ctx = contextvars.copy_context()
with self.context_watcher(0) as switches:
ctx.run(lambda: None)
self.assertEqual(len(switches), 2)
self.assertEqual(switches[0], ctx)
base_ctx = switches[1]
self.assertIsNotNone(base_ctx)
self.assertIsNot(base_ctx, ctx)
with self.assertRaisesRegex(RuntimeError, 'already entered'):
base_ctx.run(lambda: None)


if __name__ == "__main__":
unittest.main()
22 changes: 13 additions & 9 deletions Python/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,9 @@ static inline void
context_switched(PyThreadState *ts)
{
ts->context_ver++;
// ts->context is used instead of context_get() because context_get() might
// throw if ts->context is NULL.
// ts->context is used instead of context_get() because if ts->context is
// NULL, context_get() will either call context_switched -- causing a
// double notification -- or throw.
notify_context_watchers(ts, Py_CONTEXT_SWITCHED, ts->context);
}

Expand Down Expand Up @@ -478,15 +479,18 @@ context_get(void)
{
PyThreadState *ts = _PyThreadState_GET();
assert(ts != NULL);
PyContext *current_ctx = (PyContext *)ts->context;
if (current_ctx == NULL) {
current_ctx = context_new_empty();
if (current_ctx == NULL) {
return NULL;
if (ts->context == NULL) {
PyContext *ctx = context_new_empty();
if (ctx != NULL && _PyContext_Enter(ts, (PyObject *)ctx)) {
Py_UNREACHABLE();
}
ts->context = (PyObject *)current_ctx;
assert(ts->context == (PyObject *)ctx);
Py_CLEAR(ctx); // _PyContext_Enter created its own ref.
}
return current_ctx;
// The current context may be NULL if the above context_new_empty() call
// failed.
assert(ts->context == NULL || PyContext_CheckExact(ts->context));
return (PyContext *)ts->context;
}

static int
Expand Down

0 comments on commit 81d2a0d

Please sign in to comment.