From 26afcf25682605a92587315370e39f3f3edad540 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 20 Oct 2026 23:38:34 -0700 Subject: [PATCH 1/2] gh-159534: Fix race condition in test_sampling_profiler Fix test_is_python_process_non_python_subprocess(). Wait until the forked process calls exec() before calling is_python_process(). Previously, is_python_build() returned True if exec() was not called yet in the forked process. --- .../test_sampling_profiler/test_children.py | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_children.py b/Lib/test/test_profiling/test_sampling_profiler/test_children.py index e64d917eedde56..03541635b2a741 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_children.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_children.py @@ -16,6 +16,7 @@ SHORT_TIMEOUT, reap_children, requires_remote_subprocess_debugging, + sleeping_retry, ) # Guard imports that require _remote_debugging module. @@ -84,29 +85,13 @@ def reader(): return result[0] -def _wait_for_process_ready(proc, timeout): - # Wait for a subprocess to be ready using polling instead of fixed sleep. - # Returns True if process is ready, False if it exited or timeout. - deadline = time.time() + timeout - poll_interval = 0.01 - - while time.time() < deadline: - if proc.poll() is not None: - return False - - try: - if sys.platform == "linux": - if os.path.exists(f"/proc/{proc.pid}/exe"): - return True - else: - return True - except OSError: - pass - - time.sleep(poll_interval) - poll_interval = min(poll_interval * 2, 0.1) - - return proc.poll() is None +def _wait_for_process_exec(proc, name, timeout): + # Wait until a child process calls exec() to replace the forked process + # executable. Raise AssertionError on timeout. + for _ in sleeping_retry(timeout, max_delay=0.1): + exe = os.readlink(f"/proc/{proc.pid}/exe") + if os.path.basename(exe) == name: + break @unittest.skipIf( @@ -634,17 +619,14 @@ def test_is_python_process_non_python_subprocess(self): """Test that a non-Python subprocess is not detected as Python.""" # Start a non-Python subprocess (sleep command) proc = subprocess.Popen( - ["sleep", "10"], + ["sleep", "60"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) try: - # Wait for process to be ready using polling - self.assertTrue( - _wait_for_process_ready(proc, SHORT_TIMEOUT), - f"Process {proc.pid} should be ready within {SHORT_TIMEOUT}s", - ) + if sys.platform == "linux": + _wait_for_process_exec(proc, "sleep", SHORT_TIMEOUT) self.assertFalse( is_python_process(proc.pid), From 2ae125116978a0a0f6eed03eef95beb6d3230ba7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 21 Oct 2026 11:33:03 -0500 Subject: [PATCH 2/2] Remove redundant sys.platform check --- .../test_profiling/test_sampling_profiler/test_children.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_children.py b/Lib/test/test_profiling/test_sampling_profiler/test_children.py index 03541635b2a741..4b27c6dbb1896c 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_children.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_children.py @@ -625,8 +625,7 @@ def test_is_python_process_non_python_subprocess(self): ) try: - if sys.platform == "linux": - _wait_for_process_exec(proc, "sleep", SHORT_TIMEOUT) + _wait_for_process_exec(proc, "sleep", SHORT_TIMEOUT) self.assertFalse( is_python_process(proc.pid),