Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Lib/multiprocessing/reduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ class DupHandle(object):
'''Picklable wrapper for a handle.'''
def __init__(self, handle, access, pid=None):
if pid is None:
popen = context.get_spawning_popen()
if popen is not None:
# gh-154208: The child process is already running, so
# duplicate the handle straight into it. Stealing the
# handle (below) would leak the duplicate held by this
# process if the child died before it could steal it.
# This mirrors the fix made for the pipe used to send
# the child its parameters -- see bpo-33929.
self._handle = _winapi.DuplicateHandle(
_winapi.GetCurrentProcess(),
handle, popen.sentinel, access, False, 0)
self._access = access
self._pid = popen.pid
return
# We just duplicate the handle in the current process and
# let the receiving process steal the handle.
pid = os.getpid()
Expand Down
65 changes: 65 additions & 0 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5733,6 +5733,71 @@ def test_neg_timeout(self):
a.close()
b.close()

#
# Issue 154208: handles duplicated for a child process must not leak in the
# parent if the child dies before it can make use of them
#

@unittest.skipUnless(WIN32, "skipped on non-Windows platforms")
class TestWindowsHandleLeak(unittest.TestCase):

@staticmethod
def handle_count():
import ctypes
from ctypes import wintypes

kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.GetCurrentProcess.argtypes = ()
kernel32.GetCurrentProcess.restype = wintypes.HANDLE
kernel32.GetProcessHandleCount.argtypes = (wintypes.HANDLE,
wintypes.LPDWORD)
kernel32.GetProcessHandleCount.restype = wintypes.BOOL

# Pseudo-handle: it must not be closed
hproc = kernel32.GetCurrentProcess()
count = wintypes.DWORD()
if not kernel32.GetProcessHandleCount(hproc, ctypes.byref(count)):
raise ctypes.WinError(ctypes.get_last_error())
return count.value

@staticmethod
def _sleep(conn):
time.sleep(support.LONG_TIMEOUT)

def spawn_and_terminate(self):
# Kill the child before it gets a chance to unpickle its arguments:
# the handles duplicated for it are then never claimed.
reader, writer = multiprocessing.Pipe(duplex=False)
try:
proc = multiprocessing.Process(target=self._sleep, args=(reader,))
proc.start()
try:
proc.terminate()
proc.join()
finally:
proc.close()
finally:
reader.close()
writer.close()

def test_no_handle_leak_if_child_is_terminated(self):
# Warm up: the first iterations import modules and populate caches,
# which opens handles that are unrelated to this test.
for _ in range(3):
self.spawn_and_terminate()
gc.collect()

before = self.handle_count()
for _ in range(10):
self.spawn_and_terminate()
gc.collect()
leaked = self.handle_count() - before

# Before the fix, each iteration leaked the handle duplicated for
# the child, so the count grew without bound.
self.assertLessEqual(leaked, 4)


#
# Issue 14151: Test invalid family on invalid environment
#
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
On Windows, :mod:`multiprocessing` no longer leaks a handle in the parent
process for each handle passed to a child process which is terminated before
it can steal them. Handles are now duplicated directly into the child
process, so that they are released by the operating system if the child dies
early.
Loading