GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — `list.remove` is not atomic for non trivial `__eq__` comparisons · Issue #148259 · python/cpython

list.remove is not atomic for non trivial __eq__ comparisons #148259

New issue
Copy link
New issue
Copy link
Open
Open
list.remove is not atomic for non trivial __eq__ comparisons#148259
Copy link
Labels
interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtriagedThe issue has been accepted as valid by a triager.The issue has been accepted as valid by a triager.type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@KowalskiThomas
KowalskiThomas
opened on Apr 8, 2026
Issue body actions

Bug report

Bug

I found a race condition on the free-threaded build at 72eca2af59043c78647b0e6be3777a947ea9ef0f happening in list.remove.

The long story short is if whatever PyObject_RichCompareBool does releases the GIL, the critical section no longer protects the list's state from other threads, which may then mutate the list and result in the wrong item being removed later.

This is the current code for list.remove with comments around the problematic logic.

static PyObject *
list_remove_impl(PyListObject *self, PyObject *value)
{
    Py_ssize_t i;
    for (i = 0; i < Py_SIZE(self); i++) {
        // read the contents under lock
        PyObject *obj = self->ob_item[i];
        Py_INCREF(obj);
        // this may release the GIL, suspending the critical section
        int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
        Py_DECREF(obj);
        // it is possible that obj (which we want to remove) isn't at
        // position i anymore as other threads may have mutated 
        // the list...
        if (cmp > 0) {
            // ... but we still remove it!
            if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0)
                Py_RETURN_NONE;
            return NULL;
        }
    }
    ...
}

I'm not sure whether a crash can occur from that race condition (e.g. i is the position of the last element in the list and another thread pops the last element), but regardless I think there's a correctness issue with that race condition.

It's a bit hard to come up with a reproducer due to the race condition happening under pretty specific circumstances but I can try to make (or generate) one if needed.

Possible fixes

The simplest thing I can think of is re-comparing ob_item[i] with obj after PyObject_RichCompareBool has returned to make sure the state of the list, at least as far as this specific item's position is concerned, hasn't changed, and raising a RuntimeError if not.

Alternatively, I looked at other containers and it seems like deque has a state field which is incremented every time a mutating operation is done on it; if the state value changes in an unexpected manner during mutations, it raises a RuntimeError (I don't think list has anything like that currently).

I already have a branch in my fork that implements the former "fix", but I'm also open to working on the latter if there's interest in it.

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

  • gh-148259: make list.remove atomic for non-trivial __eq__ comparison #148440

Activity

  1. KowalskiThomas
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Apr 8, 2026
  2. StanFromIreland
    added
    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
    topic-free-threading
    on Apr 8, 2026
  3. Shrey-N commented on Apr 10, 2026

    @Shrey-N
    Shrey-N
    on Apr 10, 2026
    Contributor
    More actions

    Hiya I have confirmed this race condition in the free threaded build. Your draft fix correctly identifies the need for re verification but introduces a reference leak for every element that doesn't match the search value.

    If you want I can open a PR shortly with a fix that handles the reference counting correctly across all paths and raises a RuntimeError if a mutation is detected

  4. KowalskiThomas commented on Apr 10, 2026

    @KowalskiThomas
    KowalskiThomas
    on Apr 10, 2026
    ContributorAuthor
    More actions

    Thanks for looking into it! I'll have a look at the leak, thanks for reporting.

    Reacted by Shrey Naithani
  5. Shrey-N commented on Apr 10, 2026

    @Shrey-N
    Shrey-N
    on Apr 10, 2026
    Contributor
    More actions

    No problem! If you're able to open your fork as a draft PR, I would be happy to jump in and help you refine the implementation and fix those leaks :)

  6. colesbury commented on Apr 10, 2026

    @colesbury
    colesbury
    on Apr 10, 2026
    Contributor
    More actions

    Thank you for the bug report. This is longstanding behavior of list.remove -- it's not specific to free threading. The PyObject_RichCompareBool() can release the GIL (for GIL-enabled Python) or the list's lock (in free threading). Either way, it's the same effect.

    @KowalskiThomas - please open a PR with the change from your branch. I'm generally hesitant to change longstanding behavior like this because it risks introducing regressions that are more troublesome than the original limitations, but this might be narrowly scoped enough.

    Reacted by Maurycy Pawłowski-Wieroński
  7. colesbury
    removed
    topic-free-threading
    on Apr 10, 2026
  8. colesbury
    changed the title [-]Race condition in `list.remove` under free-threading[/-] [+]`list.remove` is not atomic for non trivial `__eq__` comparisons[/+] on Apr 10, 2026
  9. KowalskiThomas commented on Apr 11, 2026

    @KowalskiThomas
    KowalskiThomas
    on Apr 11, 2026
    ContributorAuthor
    More actions

    Thanks, I'll open the PR after I get a chance to look into the leaked reference thingy (should be tomorrow!)

    I'm generally hesitant to change longstanding behavior like this because it risks introducing regressions that are more troublesome than the original limitations

    Just for my own interest, do you have examples? My understanding is that here, we either have the exception raised when the list is mutated while we iterate through it, or we have nothing at all happening but we end up with an incorrect result (silently).
    As far as I can tell, as a user I'd definitely prefer the exception (which is clear and actionable) over the very confusing bug -- am I missing something or is that case not a good example maybe?

    Thank you!

  10. picnixz
    added
    topic-free-threading
    triagedThe issue has been accepted as valid by a triager.The issue has been accepted as valid by a triager.
    and removed
    topic-free-threading
    on Apr 11, 2026
  11. KowalskiThomas commented on Apr 12, 2026

    @KowalskiThomas
    KowalskiThomas
    on Apr 12, 2026
    ContributorAuthor
    More actions

    @Shrey-N I checked multiple times, but I'm not seeing the reference leak you mentioned 🤔 On top of that, as far as I can tell, my changes have the same semantics memory-wise/refcount-wise than existing branches right after my added code, and I'd assume they don't have reference leaks. Could you confirm what the leak is?

  12. KowalskiThomas commented on Apr 12, 2026

    @KowalskiThomas
    KowalskiThomas
    on Apr 12, 2026
    ContributorAuthor
    More actions

    I opened issue #148442 which is a similar race condition -- posting about it here because I'm curious whether this would fall under the "might cause other problems" umbrella or not.

  13. colesbury commented on Apr 13, 2026

    @colesbury
    colesbury
    on Apr 13, 2026
    Contributor
    More actions

    Just for my own interest, do you have examples? My understanding is that here, we either have the exception raised when the list is mutated while we iterate through it, or we have nothing at all happening but we end up with an incorrect result (silently).
    As far as I can tell, as a user I'd definitely prefer the exception (which is clear and actionable) over the very confusing bug -- am I missing something or is that case not a good example maybe?

    For example, I think adding a mutation counter like deque's state field wouldn't be appropriate here because it would risk breaking existing code that happens to work. One thread calling list.remove() and another calling list.append() isn't subject to this problem, but would trigger modification errors with a mutation counter.

    The backwards compatibility concerns here typically trump "better" APIs or concerns about confusing bugs.

  14. KowalskiThomas commented on Apr 13, 2026

    @KowalskiThomas
    KowalskiThomas
    on Apr 13, 2026
    ContributorAuthor
    More actions

    Thanks a lot for the example, that makes a lot of sense to me 🙇

  15. serhiy-storchaka commented on Apr 15, 2026

    @serhiy-storchaka
    serhiy-storchaka
    on Apr 15, 2026
    Member
    More actions

    Lets look at precedence in set.remove, set.discard, dict.__delitem__, dict.pop. I wrote test for dict and set:

    • if the key/element was concurrently deleted, you get a KeyError
    • if the key/element was concurrently replaced with an equal key/element, that new key/element will be deleted
    • if the key/element was concurrently replaced with a non-equal key/element (but with the same hash), you get a KeyError and that new key/element will remain the dict/set

    It looks like if the found key/element was removed, the lookup starts again.

    If we are going to change the current list behavior, it should be made similar to dict and set, i.e. restart the search if the found item was removed during comparison. Although I am not sure that we should change it.

    The difference between the list and hash tables is that the order of a hash table after deletion or insertion is not predicable. You can see some pattern for small integer keys, but it will fail for large integers or for non-integer keys, or just in the next Python release.

  16. KowalskiThomas commented on Apr 17, 2026

    @KowalskiThomas
    KowalskiThomas
    on Apr 17, 2026
    ContributorAuthor
    More actions

    Thanks for looking into this and for the pointers! Definitely agree that behaviours should converge where possible across standard containers. The latest comments on the PR (#148440) go in that direction as well, suggesting this might be worth formalising as a PEP (which makes sense to me).

    For completeness' sake, I initially thought the RuntimeError might be the right thing to do because deque (which is more akin to a list than list is to dict/set):

    cpython/Modules/_collectionsmodule.c

    Lines 1476 to 1480 in db3e990

    if (start_state != deque->state) {
    PyErr_SetString(PyExc_IndexError,
    "deque mutated during iteration");
    return NULL;
    }

    But yeah, open to any options of course. As I pointed out in my PR comment, my point with this issue/PR is mostly to eliminate a possible behaviour that would be pretty confusing from a user's standpoint; I really don't have a strong opinion on how it should be removed (retrying, failing explicitly, or something else).

  17. serhiy-storchaka
    added
    topic-free-threading
    on Jul 3, 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)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtriagedThe issue has been accepted as valid by a triager.The issue has been accepted as valid by a triager.type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.