Skip to content

Conversation

@efimov-mikhail
Copy link
Member

@efimov-mikhail efimov-mikhail commented Oct 31, 2025

@efimov-mikhail efimov-mikhail marked this pull request as draft October 31, 2025 14:46
@efimov-mikhail efimov-mikhail added needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes labels Oct 31, 2025
@efimov-mikhail efimov-mikhail marked this pull request as ready for review October 31, 2025 15:27
sergey-miryanov
sergey-miryanov previously approved these changes Nov 1, 2025
Copy link
Contributor

@sergey-miryanov sergey-miryanov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracer handles Python calls. If a thread's locals contain an object with a Python callback (e.g., a __del__ finalizer), then tracer will handle the call.

When our thread is finalizing, the locals are cleared, as are all objects within them. In this case, the finalizer of our object's __del__ will be called and handled by the tracer. This leads to the recreation of thread locals (threading_local_key and threading_local_sentinel), which leak because we mistakenly believe that the locals were cleared and will not be recreated.

In this PR, we clear the locals after clearing the tracers, so any callbacks from finalizers will not be handled, and the locals cannot be recreated.

@efimov-mikhail
Copy link
Member Author

efimov-mikhail commented Nov 1, 2025

Thanks for the review, Sergey!

But I have to say that I've discovered one even more difficult case without tracing at all:

import _thread
import threading

local = _thread._local()

class ClassWithDel:
    def __del__(self):
        getattr(local, 'c', None)

def thread_func():
    local.c = ClassWithDel()

t = threading.Thread(target=thread_func)
t.start()
t.join()

There's a leak with ASAN since finalizer of local.c object recreates localsdict for this thread.

CC @mpage @colesbury @ambv as author and reviewers of #121655.

Btw, we can merge this as is and create another issue for this case if this will be clearer.

@colesbury
Copy link
Contributor

This does not look like the right approach to me. I think you will end up playing whac-a-mole where each time you "fix" one leak you will introduce other bugs. You need a robust approach to dealing with finalizers like the GC has.

@efimov-mikhail
Copy link
Member Author

efimov-mikhail commented Nov 1, 2025

Thanks for the comment, @colesbury .
I'd like to stress that this PR doesn't introduce new bugs, it just doesn't fix all related issues in the current state. There's a leak with this code snippet on main.

And I'll be grateful for some piece of advice, if possible.

@colesbury
Copy link
Contributor

I think you just haven't yet found the new bugs that this PR introduces. The original bug report was only found via fuzzing. I suspect that if you keep fuzzing you will find different ones with this PR.

The problems is that we have code in the form:

Py_CLEAR(tstate->var1);
Py_CLEAR(tstate->var2);
...
Py_CLEAR(tstate->varN);

The underlying problem is that when you clear tstate->var2 you may execute Python code that re-initializes tstate->var2 or variables cleared earlier like tstate->var1. If you move around the order of clearing, you change the possible bugs.

For example, if you move tstate->threading_local_key to the end, you now have potential leaks where thread local variable destructors set things like tstate->asyncio_running_loop

@efimov-mikhail
Copy link
Member Author

In general, I agree with you, swapping clear function calls isn't good way of fixing anything.
I have some new thoughts on this topic. I'll continue to find better solution.

@efimov-mikhail efimov-mikhail marked this pull request as draft November 1, 2025 15:18
@efimov-mikhail efimov-mikhail changed the title gh-140798: Fix memory leak with threading.local when tracing is active gh-140798: Fix memory leak with threading.local when finalizer resurrects thread_local_key Nov 2, 2025
@efimov-mikhail efimov-mikhail changed the title gh-140798: Fix memory leak with threading.local when finalizer resurrects thread_local_key gh-140798: Fix memory leak with threading.local when finalizer resurrects threading_local_key Nov 2, 2025
@efimov-mikhail efimov-mikhail marked this pull request as ready for review November 2, 2025 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants