Skip to content

Segfault: Py_DECREF(NULL) in PyContextVar_Set (context.c:367) #151773

Closed

Description

@prakashsellathurai

Crash report

What happened?

Segfault: Py_DECREF(NULL) in PyContextVar_Set (context.c:367)

PyContextVar_Set doesn't NULL-check token_new(); under OOM the contextvar_set() error path then Py_DECREFs the NULL token.

AI Disclaimer: this gist was drafted by Claude Code, which also generated the reduced reproducer.

Crash report

PyContextVar_Set does not NULL-check the result of token_new(). Under memory pressure token_new() returns NULL; if the following contextvar_set() also fails, the error path runs Py_DECREF(tok) on the NULL token and segfaults.

Reproducer

import contextvars
from _testcapi import set_nomemory, remove_mem_hooks
cv = contextvars.ContextVar("x")
for start in range(16):            # crashes at start=2
    set_nomemory(start, 0)
    try:
        try:
            cv.set(object())
        finally:
            remove_mem_hooks()
    except MemoryError:
        pass

A single set_nomemory(0, 0); cv.set(object()) does not crash — PyContextVar_Set's own early context_get() fails first and returns cleanly. The crash needs a budget where the early allocations succeed but token_new() and contextvar_set() then fail, hence the short sweep.

Backtrace

#0 _Py_atomic_load_uint32_relaxed
#1 Py_DECREF              Include/refcount.h:345    (op == 0x0)
#2 PyContextVar_Set       Python/context.c:367      (Py_DECREF(tok), tok == NULL)

Root cause

Python/context.c, PyContextVar_Set (L346):

    PyContextToken *tok = token_new(ctx, var, old_val);  /* L363: may return NULL, UNCHECKED */
    Py_XDECREF(old_val);
    if (contextvar_set(var, val)) {                      /* L366: fails under OOM (_PyHamt_Assoc -> NULL) */
        Py_DECREF(tok);                                  /* L367: tok == NULL -> SIGSEGV */
        return NULL;
    }
    return (PyObject *)tok;

token_new() allocates (PyObject_GC_New) and can return NULL on failure, but tok is not checked before the Py_DECREF(tok) on the contextvar_set() failure path. (On the success path the function also returns tok unchecked — harmless NULL-with-exception, but the modification still happened.)

Suggested fix

    PyContextToken *tok = token_new(ctx, var, old_val);
    Py_XDECREF(old_val);
    if (tok == NULL) {
        return NULL;
    }

(or Py_XDECREF(tok) on the error path).

Notes

Found by OOM-injection fuzzing (set_nomemory). Same pattern as gh-146080 (Py_DECREF(NULL) in an _ssl.c error label). Not free-threading-specific: reproduces on free-threaded and default (GIL) builds, debug and release. (A _py_warnings/ContextVar vehicle reaches this only when context_aware_warnings is enabled — the default on free-threaded builds — but the C bug is build-agnostic.)

Versions

  • main (3.16.0a0); reproduced on free-threaded debug+ASan, free-threaded release, and GIL release builds.

Part of python/cpython#151763 — an umbrella tracking 35 OOM-related crash findings.

CPython versions tested on:

3.16, CPython main branch

Operating systems tested on:

Windows

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

Python 3.9.23 (main, Jun 4 2025, 08:55:39) [GCC 13.3.0]

Linked PRs

Activity

  1. zangjiucheng commented on Jun 20, 2026

    @zangjiucheng
    Contributor

    Hey @prakashsellathurai

    I can reproduce this crash on the Linux distribution, and the suggested fix makes it work and LGTM!! (But wait for core dev people to come in to review)

    I will suggest the following test for the feature PR:

      # Python built with Py_TRACE_REFS fail with a fatal error in
      # _PyRefchain_Trace() on memory allocation error.
      @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build')
      @unittest.skipUnless(support.check_impl_detail(cpython=True), 'CPython only')
      def test_contextvar_set_oom(self):
          # gh-151773: PyContextVar_Set() called token_new(), which can return
          # NULL on OOM. The NULL was not checked, so a subsequent failure in
          # contextvar_set() ran Py_DECREF(NULL) and crashed. Scan the memory
          # allocation indexes around token_new() to make sure setting a
          # ContextVar under OOM raises MemoryError instead of segfaulting.
          _testcapi = import_helper.import_module('_testcapi')
          var = contextvars.ContextVar('test_contextvar_set_oom', default=None)
          for i in range(1, 20):
              try:
                  _testcapi.set_nomemory(i, i + 1)
                  var.set(42)
              except MemoryError:
                  pass
              finally:
                  _testcapi.remove_mem_hooks()

    Please take a look at this make sure, thx :)

  2. picnixz commented on Jun 20, 2026

    @picnixz
    Member

    No need for such test. We now decided not to test MemoryError issues in general. As long as we have a reproducer that is verified by core dev with a before/after check it's enough. Otherwise, we'll need to change constants sometimes.

  3. zangjiucheng commented on Jun 20, 2026

    @zangjiucheng
    Contributor

    Oh, kk thanks for updating :)

  4. added a commit that references this issue on Jun 22, 2026
  5. added a commit that references this issue on Jun 23, 2026
  6. added 3 commits that reference this issue on Jun 23, 2026
  7. added a commit that references this issue on Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions