gh-135335: flush stdout/stderr in forkserver after preloading modules (#135338)

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.

Co-authored-by: Mikhail Efimov <efimov.mikhail@gmail.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Duane Griffin 2025-06-19 00:17:02 +12:00 committed by GitHub
parent 5f6ab92465
commit 9877d191f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 0 deletions

View file

@ -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()

View file

@ -6801,6 +6801,35 @@ class _TestSpawnedSysPath(BaseTestCase):
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.
dirname = os.path.join(self._temp_dir, 'preloaded_module')
init_name = os.path.join(dirname, '__init__.py')
os.mkdir(dirname)
with open(init_name, "w") as f:
cmd = '''if 1:
import sys
print('stderr', end='', file=sys.stderr)
print('stdout', end='', file=sys.stdout)
'''
f.write(cmd)
name = os.path.join(os.path.dirname(__file__), 'mp_preload_flush.py')
env = {'PYTHONPATH': self._temp_dir}
_, 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.
self.assertEqual(err.decode().rstrip(), 'stderr')
self.assertEqual(out.decode().rstrip(), 'stdout')
class MiscTestCase(unittest.TestCase):
def test__all__(self):

View file

@ -0,0 +1,15 @@
import multiprocessing
import sys
modname = 'preloaded_module'
if __name__ == '__main__':
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):
p = multiprocessing.Process()
p.start()
p.join()
elif modname not in sys.modules:
raise AssertionError(f'{modname!r} is not in sys.modules')

View file

@ -0,0 +1,2 @@
:mod:`multiprocessing`: Flush ``stdout`` and ``stderr`` after preloading
modules in the ``forkserver``.