From d59b229afe0431b2f68107e857666cae6e57e005 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Tue, 10 Jun 2025 16:52:45 +1200 Subject: [PATCH 01/15] gh-135335: flush stdout/stderr in forkserver after preloading modules If a preloaded module writes to stdout or stderr, and the stream is buffered, child processes will inherit the buffered data after forking. Attempt to prevent this by flushing the streams after preload. --- Lib/multiprocessing/forkserver.py | 4 ++++ Lib/test/_test_multiprocessing.py | 23 +++++++++++++++++++ Lib/test/mp_preload_flush.py | 7 ++++++ ...-06-10-21-42-04.gh-issue-135335.WnUqb_.rst | 2 ++ 4 files changed, 36 insertions(+) create mode 100644 Lib/test/mp_preload_flush.py create mode 100644 Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst diff --git a/Lib/multiprocessing/forkserver.py b/Lib/multiprocessing/forkserver.py index 681af2610e9b37..c91891ff162c2d 100644 --- a/Lib/multiprocessing/forkserver.py +++ b/Lib/multiprocessing/forkserver.py @@ -222,6 +222,10 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None, except ImportError: pass + # gh-135335: flush stdout/stderr in case any of the preloaded modules + # wrote to them, otherwise children might inherit buffered data + util._flush_std_streams() + util._close_stdin() sig_r, sig_w = os.pipe() diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 75f31d858d3306..a008dc7bae6073 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6801,6 +6801,29 @@ def test_child_sys_path(self): self.assertEqual(child_sys_path[1:], sys.path[1:]) self.assertIsNone(import_error, msg=f"child could not import {self._mod_name}") + def test_std_streams_flushed_after_preload(self): + # gh-135335: Check fork server flushes standard streams after + # preloading modules + if multiprocessing.get_start_method() != "forkserver": + self.skipTest("forkserver specific test") + + # Create a test module in the temporary directory on the child's path + # TODO: This can all be simplified once gh-126631 is fixed and we can + # use __main__ instead of a module. + os.mkdir(os.path.join(self._temp_dir, 'a')) + with open(os.path.join(self._temp_dir, 'a', '__init__.py'), "w") as f: + f.write('''if 1: + import sys + print('stdout', file=sys.stdout) + print('stderr', file=sys.stderr)''') + + name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py') + env = {'PYTHONPATH': ":".join(sys.path)} + rc, out, err = test.support.script_helper.assert_python_ok(name, **env) + self.assertEqual(rc, 0) + self.assertEqual(out.decode().rstrip(), 'stdout') + self.assertEqual(err.decode().rstrip(), 'stderr') + class MiscTestCase(unittest.TestCase): def test__all__(self): diff --git a/Lib/test/mp_preload_flush.py b/Lib/test/mp_preload_flush.py new file mode 100644 index 00000000000000..6c58b9a4534c25 --- /dev/null +++ b/Lib/test/mp_preload_flush.py @@ -0,0 +1,7 @@ +import multiprocessing +if __name__ == '__main__': + multiprocessing.set_forkserver_preload(['a']) + for _ in range(2): + p = multiprocessing.Process() + p.start() + p.join() diff --git a/Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst b/Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst new file mode 100644 index 00000000000000..74a5022d4151f8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst @@ -0,0 +1,2 @@ +Flush ``stdout`` and ``stderr`` after preloading modules in the +:mod:`multiprocessing` ``forkserver``. From 552266f6a43117954cb0a67071a8ed61839c13d8 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Tue, 10 Jun 2025 22:59:30 +1200 Subject: [PATCH 02/15] Attempt to fix MacOS test failures ...or at least produce more useful output if they fail --- Lib/test/_test_multiprocessing.py | 5 ++++- Lib/test/mp_preload_flush.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index a008dc7bae6073..30d3335576f997 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6818,9 +6818,12 @@ def test_std_streams_flushed_after_preload(self): print('stderr', file=sys.stderr)''') name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py') - env = {'PYTHONPATH': ":".join(sys.path)} + env = {'PYTHONPATH': self._temp_dir} rc, out, err = test.support.script_helper.assert_python_ok(name, **env) self.assertEqual(rc, 0) + + # We want to see all the output if it isn't as expected + self.maxDiff = None self.assertEqual(out.decode().rstrip(), 'stdout') self.assertEqual(err.decode().rstrip(), 'stderr') diff --git a/Lib/test/mp_preload_flush.py b/Lib/test/mp_preload_flush.py index 6c58b9a4534c25..ef97935ff04b28 100644 --- a/Lib/test/mp_preload_flush.py +++ b/Lib/test/mp_preload_flush.py @@ -1,7 +1,12 @@ import multiprocessing +import sys + if __name__ == '__main__': + assert 'a' not in sys.modules multiprocessing.set_forkserver_preload(['a']) for _ in range(2): p = multiprocessing.Process() p.start() p.join() +else: + assert 'a' in sys.modules From 6e36eea29b59386a9b266970783408761fbef3f7 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Tue, 10 Jun 2025 23:26:51 +1200 Subject: [PATCH 03/15] Check stderr first to get details of assertion failures --- Lib/test/_test_multiprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 30d3335576f997..a4d251530bd456 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6824,8 +6824,8 @@ def test_std_streams_flushed_after_preload(self): # We want to see all the output if it isn't as expected self.maxDiff = None - self.assertEqual(out.decode().rstrip(), 'stdout') self.assertEqual(err.decode().rstrip(), 'stderr') + self.assertEqual(out.decode().rstrip(), 'stdout') class MiscTestCase(unittest.TestCase): From 33f9956909b8faff5a0d22d1c6d0bc2a0dc8bd2e Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Thu, 12 Jun 2025 13:53:51 +1200 Subject: [PATCH 04/15] Further minor diagnostic improvements upon test failure --- Lib/test/_test_multiprocessing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index a4d251530bd456..886b35711c8b45 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6820,6 +6820,9 @@ def test_std_streams_flushed_after_preload(self): name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py') env = {'PYTHONPATH': self._temp_dir} rc, out, err = test.support.script_helper.assert_python_ok(name, **env) + if rc: + support.print_warning("preload flush test failed with stderr:") + support.print_warning(err.decode()) self.assertEqual(rc, 0) # We want to see all the output if it isn't as expected From 9944968baede34b265d2dd0bbed39e4471e90c34 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Mon, 16 Jun 2025 16:07:08 +1200 Subject: [PATCH 05/15] Set start method explicitly to "forkserver" so it works on MacOS (and if the defaults change elsewhere in the future) --- Lib/test/_test_multiprocessing.py | 2 +- Lib/test/mp_preload_flush.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 886b35711c8b45..cbb31f706da0bd 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6815,7 +6815,7 @@ def test_std_streams_flushed_after_preload(self): f.write('''if 1: import sys print('stdout', file=sys.stdout) - print('stderr', file=sys.stderr)''') + print('stderr', file=sys.stderr)\n''') name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py') env = {'PYTHONPATH': self._temp_dir} diff --git a/Lib/test/mp_preload_flush.py b/Lib/test/mp_preload_flush.py index ef97935ff04b28..27f87b4629bf31 100644 --- a/Lib/test/mp_preload_flush.py +++ b/Lib/test/mp_preload_flush.py @@ -3,6 +3,7 @@ if __name__ == '__main__': assert 'a' not in sys.modules + multiprocessing.set_start_method('forkserver') multiprocessing.set_forkserver_preload(['a']) for _ in range(2): p = multiprocessing.Process() From 1523e744ecea76a83dec62e53ca269652b6c7274 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Tue, 17 Jun 2025 22:55:40 +1200 Subject: [PATCH 06/15] Address reviewer feedback: introduce some intermediate variables and use a not-completely-awful name for the module to test. --- Lib/test/_test_multiprocessing.py | 15 +++++++++------ Lib/test/mp_preload_flush.py | 7 ++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index cbb31f706da0bd..98e1e359d39938 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6810,12 +6810,15 @@ def test_std_streams_flushed_after_preload(self): # Create a test module in the temporary directory on the child's path # TODO: This can all be simplified once gh-126631 is fixed and we can # use __main__ instead of a module. - os.mkdir(os.path.join(self._temp_dir, 'a')) - with open(os.path.join(self._temp_dir, 'a', '__init__.py'), "w") as f: - f.write('''if 1: - import sys - print('stdout', file=sys.stdout) - print('stderr', file=sys.stderr)\n''') + dirname = os.path.join(self._temp_dir, 'preloaded_module') + init_name = os.path.join(dirname, '__init__.py') + os.mkdir(dirname) + with open(os.path.join(init_name), "w") as f: + cmd = '''if 1: + import sys + print('stdout', file=sys.stdout) + print('stderr', file=sys.stderr)\n''' + f.write(cmd) name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py') env = {'PYTHONPATH': self._temp_dir} diff --git a/Lib/test/mp_preload_flush.py b/Lib/test/mp_preload_flush.py index 27f87b4629bf31..53359b8c8e0a47 100644 --- a/Lib/test/mp_preload_flush.py +++ b/Lib/test/mp_preload_flush.py @@ -1,13 +1,14 @@ import multiprocessing import sys +modname = 'preloaded_module' if __name__ == '__main__': - assert 'a' not in sys.modules + assert modname not in sys.modules multiprocessing.set_start_method('forkserver') - multiprocessing.set_forkserver_preload(['a']) + multiprocessing.set_forkserver_preload([modname]) for _ in range(2): p = multiprocessing.Process() p.start() p.join() else: - assert 'a' in sys.modules + assert modname in sys.modules From 2125574c24a94fef29af3b78748ab8a5a74c4ad5 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Tue, 17 Jun 2025 23:27:56 +1200 Subject: [PATCH 07/15] Update Lib/test/_test_multiprocessing.py Co-authored-by: Mikhail Efimov --- Lib/test/_test_multiprocessing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 98e1e359d39938..ffc2679cdcbfa2 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6817,7 +6817,8 @@ def test_std_streams_flushed_after_preload(self): cmd = '''if 1: import sys print('stdout', file=sys.stdout) - print('stderr', file=sys.stderr)\n''' + print('stderr', file=sys.stderr) + ''' f.write(cmd) name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py') From db298cb557f67e2859c6fbdf50994c842fc3a835 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Tue, 17 Jun 2025 23:42:30 +1200 Subject: [PATCH 08/15] Additional review feedback: add comment and align ordering --- Lib/test/_test_multiprocessing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index ffc2679cdcbfa2..cd7e6c98f9be62 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6816,8 +6816,8 @@ def test_std_streams_flushed_after_preload(self): with open(os.path.join(init_name), "w") as f: cmd = '''if 1: import sys - print('stdout', file=sys.stdout) print('stderr', file=sys.stderr) + print('stdout', file=sys.stdout) ''' f.write(cmd) @@ -6829,7 +6829,9 @@ def test_std_streams_flushed_after_preload(self): support.print_warning(err.decode()) self.assertEqual(rc, 0) - # We want to see all the output if it isn't as expected + # We want to see all the output if it isn't as expected. + # Check stderr first, as it is more likely to be useful to see in the + # event of a failure. self.maxDiff = None self.assertEqual(err.decode().rstrip(), 'stderr') self.assertEqual(out.decode().rstrip(), 'stdout') From f9bab0abd45a23c31f50b9a1b502d6c95ddf30da Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Wed, 18 Jun 2025 00:44:10 +1200 Subject: [PATCH 09/15] Update Lib/test/mp_preload_flush.py Co-authored-by: Victor Stinner --- Lib/test/mp_preload_flush.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/mp_preload_flush.py b/Lib/test/mp_preload_flush.py index 53359b8c8e0a47..22df265a86ca3b 100644 --- a/Lib/test/mp_preload_flush.py +++ b/Lib/test/mp_preload_flush.py @@ -10,5 +10,5 @@ p = multiprocessing.Process() p.start() p.join() -else: - assert modname in sys.modules +elif modname not in sys.modules: + raise AssertionError(f'{modname!r} is not in sys.modules') From 4ffa1383f35d261c4856acc6c2a671b9ff34c1f8 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Wed, 18 Jun 2025 00:45:37 +1200 Subject: [PATCH 10/15] Show default diff on unexpected stdout/err --- Lib/test/_test_multiprocessing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index cd7e6c98f9be62..2c8ab71f48756c 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6829,10 +6829,8 @@ def test_std_streams_flushed_after_preload(self): support.print_warning(err.decode()) self.assertEqual(rc, 0) - # We want to see all the output if it isn't as expected. # Check stderr first, as it is more likely to be useful to see in the # event of a failure. - self.maxDiff = None self.assertEqual(err.decode().rstrip(), 'stderr') self.assertEqual(out.decode().rstrip(), 'stdout') From 4d57d74133b955a322b67450b2a56e702ec61936 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Wed, 18 Jun 2025 10:35:31 +1200 Subject: [PATCH 11/15] Update Lib/test/_test_multiprocessing.py Co-authored-by: Victor Stinner --- Lib/test/_test_multiprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 2c8ab71f48756c..da3c8020f2f653 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6813,7 +6813,7 @@ def test_std_streams_flushed_after_preload(self): dirname = os.path.join(self._temp_dir, 'preloaded_module') init_name = os.path.join(dirname, '__init__.py') os.mkdir(dirname) - with open(os.path.join(init_name), "w") as f: + with open(init_name, "w") as f: cmd = '''if 1: import sys print('stderr', file=sys.stderr) From e73ea349e6c64007afd6fe30d49deb76adeb6a91 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Wed, 18 Jun 2025 10:36:25 +1200 Subject: [PATCH 12/15] Update Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst Co-authored-by: Victor Stinner --- .../Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst b/Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst index 74a5022d4151f8..466ba0d232cd1f 100644 --- a/Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst +++ b/Misc/NEWS.d/next/Library/2025-06-10-21-42-04.gh-issue-135335.WnUqb_.rst @@ -1,2 +1,2 @@ -Flush ``stdout`` and ``stderr`` after preloading modules in the -:mod:`multiprocessing` ``forkserver``. +:mod:`multiprocessing`: Flush ``stdout`` and ``stderr`` after preloading +modules in the ``forkserver``. From 3f07ff76c04d266ea1b493088c996cefbf4b47de Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Wed, 18 Jun 2025 10:37:27 +1200 Subject: [PATCH 13/15] Remove redundant check --- Lib/test/_test_multiprocessing.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index da3c8020f2f653..282badb8a71f08 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6823,11 +6823,7 @@ def test_std_streams_flushed_after_preload(self): name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py') env = {'PYTHONPATH': self._temp_dir} - rc, out, err = test.support.script_helper.assert_python_ok(name, **env) - if rc: - support.print_warning("preload flush test failed with stderr:") - support.print_warning(err.decode()) - self.assertEqual(rc, 0) + _, out, err = test.support.script_helper.assert_python_ok(name, **env) # Check stderr first, as it is more likely to be useful to see in the # event of a failure. From da4df4416105e8fcf5f1992da28ec9c7b5526fbb Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Wed, 18 Jun 2025 21:31:41 +1200 Subject: [PATCH 14/15] Don't write an newline to avoid any implicit flushes that might happen --- Lib/test/_test_multiprocessing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 282badb8a71f08..a1259ff1d63d18 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6816,8 +6816,8 @@ def test_std_streams_flushed_after_preload(self): with open(init_name, "w") as f: cmd = '''if 1: import sys - print('stderr', file=sys.stderr) - print('stdout', file=sys.stdout) + print('stderr', end='', file=sys.stderr) + print('stdout', end='', file=sys.stdout) ''' f.write(cmd) From fdb30e11b691225536ff3216612f888ff6d76141 Mon Sep 17 00:00:00 2001 From: Duane Griffin Date: Wed, 18 Jun 2025 22:17:46 +1200 Subject: [PATCH 15/15] Be consistent and do not rely on asserts being enabled --- Lib/test/mp_preload_flush.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/mp_preload_flush.py b/Lib/test/mp_preload_flush.py index 22df265a86ca3b..3501554d366a21 100644 --- a/Lib/test/mp_preload_flush.py +++ b/Lib/test/mp_preload_flush.py @@ -3,7 +3,8 @@ modname = 'preloaded_module' if __name__ == '__main__': - assert modname not in sys.modules + if modname in sys.modules: + raise AssertionError(f'{modname!r} is not in sys.modules') multiprocessing.set_start_method('forkserver') multiprocessing.set_forkserver_preload([modname]) for _ in range(2):