diff --git a/Lib/multiprocessing/shared_memory.py b/Lib/multiprocessing/shared_memory.py index 99a8ce3320ad4ee..e34434c95689094 100644 --- a/Lib/multiprocessing/shared_memory.py +++ b/Lib/multiprocessing/shared_memory.py @@ -116,7 +116,14 @@ def __init__(self, name=None, create=False, size=0, *, track=True): size = stats.st_size self._mmap = mmap.mmap(self._fd, size) except OSError: - self.unlink() + self.close() + # Only destroy the shared memory block if we created it in + # this call. When attaching to a block created by another + # process we must not unlink another owner's memory. The + # block has not been registered with the resource tracker + # yet, so unlink it directly to avoid a spurious UNREGISTER. + if create: + _posixshmem.shm_unlink(self._name) raise if self._track: resource_tracker.register(self._name, "shared_memory") diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 115a187a8a85882..cf887d4130e10ba 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -4615,6 +4615,44 @@ class OptionalAttachSharedMemory(shared_memory.SharedMemory): sms.close() + @unittest.skipUnless(shared_memory._USE_POSIX, + "POSIX shared memory required") + def test_shared_memory_attach_failure_preserves_block(self): + # A failure to mmap while *attaching* to an existing shared memory + # block (create=False) must not unlink (destroy) the block, which + # is owned by another party. + name = self._new_shm_name('test_attach_fail') + owner = shared_memory.SharedMemory(name, create=True, size=1024) + self.addCleanup(owner.unlink) + self.addCleanup(owner.close) + + with unittest.mock.patch( + 'multiprocessing.shared_memory.mmap.mmap', + side_effect=OSError("simulated mmap failure")): + with self.assertRaises(OSError): + shared_memory.SharedMemory(name) # attach must not unlink + + # The owner's block must still be attachable. + attached = shared_memory.SharedMemory(name) + self.addCleanup(attached.close) + self.assertGreaterEqual(attached.size, 1024) + + @unittest.skipUnless(shared_memory._USE_POSIX, + "POSIX shared memory required") + def test_shared_memory_create_failure_cleans_up(self): + # A failure to mmap while *creating* a block must destroy the + # just-created block so that it is not leaked. + name = self._new_shm_name('test_create_fail') + with unittest.mock.patch( + 'multiprocessing.shared_memory.mmap.mmap', + side_effect=OSError("simulated mmap failure")): + with self.assertRaises(OSError): + shared_memory.SharedMemory(name, create=True, size=1024) + + # The half-created block must not survive. + with self.assertRaises(FileNotFoundError): + shared_memory.SharedMemory(name) + def test_shared_memory_recreate(self): # Test if shared memory segment is created properly, # when _make_filename returns an existing shared memory segment name diff --git a/Misc/NEWS.d/next/Library/2026-07-07-19-30-33.gh-issue-153279.xxY4MA.rst b/Misc/NEWS.d/next/Library/2026-07-07-19-30-33.gh-issue-153279.xxY4MA.rst new file mode 100644 index 000000000000000..e4c3924b05b201e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-07-19-30-33.gh-issue-153279.xxY4MA.rst @@ -0,0 +1,6 @@ +Fix :class:`multiprocessing.shared_memory.SharedMemory` destroying another +process's shared memory block when attaching to it fails. A failure to +:func:`~mmap.mmap` while attaching (``create=False``) no longer unlinks the +block, which is owned by another process; the file descriptor is simply +closed. The block is now unlinked only when it was created in the failing +call.