Skip to content
Merged
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
8 changes: 8 additions & 0 deletions Doc/library/mmap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
with :const:`ACCESS_READ` or :const:`ACCESS_COPY`, resizing the map will
raise a :exc:`TypeError` exception.

**On Windows**: Resizing the map will raise an :exc:`OSError` if there are other
maps against the same named file. Resizing an anonymous map (ie against the
pagefile) will silently create a new map with the original data copied over
up to the length of the new size.

.. versionchanged:: 3.11
Correctly fails if attempting to resize when another map is held
Allows resize against an anonymous map on Windows

.. method:: rfind(sub[, start[, end]])

Expand Down
76 changes: 75 additions & 1 deletion Lib/test/test_mmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import re
import itertools
import random
import socket
import sys
import weakref
Expand Down Expand Up @@ -707,7 +708,6 @@ def test_write_returning_the_number_of_bytes_written(self):
self.assertEqual(mm.write(b"yz"), 2)
self.assertEqual(mm.write(b"python"), 6)

@unittest.skipIf(os.name == 'nt', 'cannot resize anonymous mmaps on Windows')
def test_resize_past_pos(self):
m = mmap.mmap(-1, 8192)
self.addCleanup(m.close)
Expand Down Expand Up @@ -796,6 +796,80 @@ def test_madvise(self):
self.assertEqual(m.madvise(mmap.MADV_NORMAL, 0, 2), None)
self.assertEqual(m.madvise(mmap.MADV_NORMAL, 0, size), None)

@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_up_when_mapped_to_pagefile(self):
"""If the mmap is backed by the pagefile ensure a resize up can happen
and that the original data is still in place
"""
start_size = PAGESIZE
new_size = 2 * start_size
data = bytes(random.getrandbits(8) for _ in range(start_size))

m = mmap.mmap(-1, start_size)
m[:] = data
m.resize(new_size)
self.assertEqual(len(m), new_size)
self.assertEqual(m[:start_size], data[:start_size])

@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_down_when_mapped_to_pagefile(self):
"""If the mmap is backed by the pagefile ensure a resize down up can happen
and that a truncated form of the original data is still in place
"""
start_size = PAGESIZE
new_size = start_size // 2
data = bytes(random.getrandbits(8) for _ in range(start_size))

m = mmap.mmap(-1, start_size)
m[:] = data
m.resize(new_size)
self.assertEqual(len(m), new_size)
self.assertEqual(m[:new_size], data[:new_size])

@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_fails_if_mapping_held_elsewhere(self):
"""If more than one mapping is held against a named file on Windows, neither
mapping can be resized
"""
start_size = 2 * PAGESIZE
reduced_size = PAGESIZE

f = open(TESTFN, 'wb+')
f.truncate(start_size)
try:
m1 = mmap.mmap(f.fileno(), start_size)
m2 = mmap.mmap(f.fileno(), start_size)
with self.assertRaises(OSError):
m1.resize(reduced_size)
with self.assertRaises(OSError):
m2.resize(reduced_size)
m2.close()
m1.resize(reduced_size)
self.assertEqual(m1.size(), reduced_size)
self.assertEqual(os.stat(f.fileno()).st_size, reduced_size)
finally:
f.close()

@unittest.skipUnless(os.name == 'nt', 'requires Windows')
def test_resize_succeeds_with_error_for_second_named_mapping(self):
"""If a more than one mapping exists of the same name, none of them can
be resized: they'll raise an Exception and leave the original mapping intact
"""
start_size = 2 * PAGESIZE
reduced_size = PAGESIZE
tagname = "TEST"
Copy link
Contributor

@eryksun eryksun Oct 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry to have missed this before you merged. Naming kernel objects with generic names like "foo" and "TEST" is a bad idea because the namespace is shared by every process in the current session. It's a serious problem in all of the tests that use tagname. If the name is an existing Section object (i.e. file mapping), CreateFileMapping() will succeed with the last error set to ERROR_ALREADY_EXISTS. In many cases, the Section object likely refers to an unrelated file, but we can't avoid this, or even know about the problem, because mmap doesn't currently support exclusive ("x") creation. This and a few other cases really need to be addressed in the constructor, but that's a separate issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @eryksun -- good catch. Funnily enough, I'm normally a little over-obsessive with test names, generating them randomly to descrease any chance of a "lucky name". This time I've gone the other way! I'll run up a quick patch to fix this.

data_length = 8
data = bytes(random.getrandbits(8) for _ in range(data_length))

m1 = mmap.mmap(-1, start_size, tagname=tagname)
m2 = mmap.mmap(-1, start_size, tagname=tagname)
m1[:data_length] = data
self.assertEqual(m2[:data_length], data)
with self.assertRaises(OSError):
m1.resize(reduced_size)
self.assertEqual(m1.size(), start_size)
self.assertEqual(m1[:data_length], data)
self.assertEqual(m2[:data_length], data)

class LargeMmapTests(unittest.TestCase):

Expand Down
129 changes: 95 additions & 34 deletions Modules/mmapmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

#ifdef MS_WINDOWS
#include <windows.h>
#include <winternl.h>
static int
my_getpagesize(void)
{
Expand Down Expand Up @@ -376,14 +377,15 @@ is_resizeable(mmap_object *self)
{
if (self->exports > 0) {
PyErr_SetString(PyExc_BufferError,
"mmap can't resize with extant buffers exported.");
"mmap can't resize with extant buffers exported.");
return 0;
}
if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT))
return 1;
PyErr_Format(PyExc_TypeError,
"mmap can't resize a readonly or copy-on-write memory map.");
"mmap can't resize a readonly or copy-on-write memory map.");
return 0;

}


Expand Down Expand Up @@ -503,51 +505,110 @@ mmap_resize_method(mmap_object *self,
}

{
/*
To resize an mmap on Windows:

- Close the existing mapping
- If the mapping is backed to a named file:
unmap the view, clear the data, and resize the file
If the file can't be resized (eg because it has other mapped references
to it) then let the mapping be recreated at the original size and set
an error code so an exception will be raised.
- Create a new mapping of the relevant size to the same file
- Map a new view of the resized file
- If the mapping is backed by the pagefile:
copy any previous data into the new mapped area
unmap the original view which will release the memory
*/
#ifdef MS_WINDOWS
DWORD dwErrCode = 0;
DWORD off_hi, off_lo, newSizeLow, newSizeHigh;
/* First, unmap the file view */
UnmapViewOfFile(self->data);
self->data = NULL;
/* Close the mapping object */
DWORD error = 0, file_resize_error = 0;
char* old_data = self->data;
LARGE_INTEGER offset, max_size;
offset.QuadPart = self->offset;
max_size.QuadPart = self->offset + new_size;
/* close the file mapping */
CloseHandle(self->map_handle);
self->map_handle = NULL;
/* Move to the desired EOF position */
newSizeHigh = (DWORD)((self->offset + new_size) >> 32);
newSizeLow = (DWORD)((self->offset + new_size) & 0xFFFFFFFF);
off_hi = (DWORD)(self->offset >> 32);
off_lo = (DWORD)(self->offset & 0xFFFFFFFF);
SetFilePointer(self->file_handle,
newSizeLow, &newSizeHigh, FILE_BEGIN);
/* Change the size of the file */
SetEndOfFile(self->file_handle);
/* Create another mapping object and remap the file view */
/* if the file mapping still exists, it cannot be resized. */
if (self->tagname) {
self->map_handle = OpenFileMapping(FILE_MAP_WRITE, FALSE,
self->tagname);
if (self->map_handle) {
PyErr_SetFromWindowsErr(ERROR_USER_MAPPED_FILE);
return NULL;
}
} else {
self->map_handle = NULL;
}

/* if it's not the paging file, unmap the view and resize the file */
if (self->file_handle != INVALID_HANDLE_VALUE) {
if (!UnmapViewOfFile(self->data)) {
return PyErr_SetFromWindowsErr(GetLastError());
};
self->data = NULL;
/* resize the file */
if (!SetFilePointerEx(self->file_handle, max_size, NULL,
FILE_BEGIN) ||
!SetEndOfFile(self->file_handle)) {
/* resizing failed. try to remap the file */
file_resize_error = GetLastError();
new_size = max_size.QuadPart = self->size;
}
}

/* create a new file mapping and map a new view */
/* FIXME: call CreateFileMappingW with wchar_t tagname */
self->map_handle = CreateFileMapping(
self->file_handle,
NULL,
PAGE_READWRITE,
0,
0,
max_size.HighPart,
max_size.LowPart,
self->tagname);
if (self->map_handle != NULL) {
self->data = (char *) MapViewOfFile(self->map_handle,
FILE_MAP_WRITE,
off_hi,
off_lo,
new_size);

error = GetLastError();
if (error == ERROR_ALREADY_EXISTS) {
CloseHandle(self->map_handle);
self->map_handle = NULL;
}
else if (self->map_handle != NULL) {
self->data = MapViewOfFile(self->map_handle,
FILE_MAP_WRITE,
offset.HighPart,
offset.LowPart,
new_size);
if (self->data != NULL) {
/* copy the old view if using the paging file */
if (self->file_handle == INVALID_HANDLE_VALUE) {
memcpy(self->data, old_data,
self->size < new_size ? self->size : new_size);
if (!UnmapViewOfFile(old_data)) {
error = GetLastError();
}
}
self->size = new_size;
Py_RETURN_NONE;
} else {
dwErrCode = GetLastError();
}
else {
error = GetLastError();
CloseHandle(self->map_handle);
self->map_handle = NULL;
}
} else {
dwErrCode = GetLastError();
}
PyErr_SetFromWindowsErr(dwErrCode);
return NULL;

if (error) {
return PyErr_SetFromWindowsErr(error);
return NULL;
}
/* It's possible for a resize to fail, typically because another mapping
is still held against the same underlying file. Even if nothing has
failed -- ie we're still returning a valid file mapping -- raise the
error as an exception as the resize won't have happened
*/
if (file_resize_error) {
PyErr_SetFromWindowsErr(file_resize_error);
return NULL;
}
Py_RETURN_NONE;
#endif /* MS_WINDOWS */

#ifdef UNIX
Expand Down