From 7a7427bd6f01108ee7900562132ff9ac3a073db5 Mon Sep 17 00:00:00 2001 From: Matt Davis <6775756+nitzmahone@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:08:48 -0700 Subject: [PATCH] Fix post-fork deadlock from early Python writers like pydevd (#85296) (cherry picked from commit 1d1bbe3424a80a54ee6c0f260354d365e9d06e3f) --- .../fragments/post_fork_stdio_deadlock.yml | 2 ++ lib/ansible/utils/display.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/post_fork_stdio_deadlock.yml diff --git a/changelogs/fragments/post_fork_stdio_deadlock.yml b/changelogs/fragments/post_fork_stdio_deadlock.yml new file mode 100644 index 00000000000..fee63456240 --- /dev/null +++ b/changelogs/fragments/post_fork_stdio_deadlock.yml @@ -0,0 +1,2 @@ +bugfixes: + - display - Fix hang caused by early post-fork writers to stdout/stderr (e.g., pydevd) encountering an unreleased fork lock. diff --git a/lib/ansible/utils/display.py b/lib/ansible/utils/display.py index 85affd75a31..945b570cb20 100644 --- a/lib/ansible/utils/display.py +++ b/lib/ansible/utils/display.py @@ -17,6 +17,7 @@ from __future__ import annotations +import contextlib import dataclasses try: @@ -216,10 +217,22 @@ b_COW_PATHS = ( def _synchronize_textiowrapper(tio: t.TextIO, lock: threading.RLock): - # Ensure that a background thread can't hold the internal buffer lock on a file object - # during a fork, which causes forked children to hang. We're using display's existing lock for - # convenience (and entering the lock before a fork). + """ + This decorator ensures that the supplied RLock is held before invoking the wrapped methods. + It is intended to prevent background threads from holding the Python stdout/stderr buffer lock on a file object during a fork. + Since background threads are abandoned in child forks, locks they hold are orphaned in a locked state. + Attempts to acquire an orphaned lock in this state will block forever, effectively hanging the child process on stdout/stderr writes. + The shared lock is permanently disabled immediately after a fork. + This prevents hangs in early post-fork code (e.g., stdio writes from pydevd, coverage, etc.) before user code has resumed and released the lock. + """ + def _wrap_with_lock(f, lock): + def disable_lock(): + nonlocal lock + lock = contextlib.nullcontext() + + os.register_at_fork(after_in_child=disable_lock) + @wraps(f) def locking_wrapper(*args, **kwargs): with lock: