gh-78502: Add a trackfd parameter to mmap.mmap() (GH-25425)

If *trackfd* is False, the file descriptor specified by *fileno*
will not be duplicated.

Co-authored-by: Erlend E. Aasland <erlend@python.org>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Zackery Spytz 2024-01-15 23:51:46 -08:00 committed by GitHub
parent 42b90cf0d6
commit 8fd287b18f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 11 deletions

View file

@ -48,7 +48,7 @@ update the underlying file.
To map anonymous memory, -1 should be passed as the fileno along with the length. To map anonymous memory, -1 should be passed as the fileno along with the length.
.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT[, offset]) .. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT, offset=0)
**(Windows version)** Maps *length* bytes from the file specified by the **(Windows version)** Maps *length* bytes from the file specified by the
file handle *fileno*, and creates a mmap object. If *length* is larger file handle *fileno*, and creates a mmap object. If *length* is larger
@ -71,7 +71,8 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
.. audit-event:: mmap.__new__ fileno,length,access,offset mmap.mmap .. audit-event:: mmap.__new__ fileno,length,access,offset mmap.mmap
.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, access=ACCESS_DEFAULT[, offset]) .. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, \
access=ACCESS_DEFAULT, offset=0, *, trackfd=True)
:noindex: :noindex:
**(Unix version)** Maps *length* bytes from the file specified by the file **(Unix version)** Maps *length* bytes from the file specified by the file
@ -102,10 +103,20 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
defaults to 0. *offset* must be a multiple of :const:`ALLOCATIONGRANULARITY` defaults to 0. *offset* must be a multiple of :const:`ALLOCATIONGRANULARITY`
which is equal to :const:`PAGESIZE` on Unix systems. which is equal to :const:`PAGESIZE` on Unix systems.
If *trackfd* is ``False``, the file descriptor specified by *fileno* will
not be duplicated, and the resulting :class:`!mmap` object will not
be associated with the map's underlying file.
This means that the :meth:`~mmap.mmap.size` and :meth:`~mmap.mmap.resize`
methods will fail.
This mode is useful to limit the number of open file descriptors.
To ensure validity of the created memory mapping the file specified To ensure validity of the created memory mapping the file specified
by the descriptor *fileno* is internally automatically synchronized by the descriptor *fileno* is internally automatically synchronized
with the physical backing store on macOS. with the physical backing store on macOS.
.. versionchanged:: 3.13
The *trackfd* parameter was added.
This example shows a simple way of using :class:`~mmap.mmap`:: This example shows a simple way of using :class:`~mmap.mmap`::
import mmap import mmap
@ -254,9 +265,12 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
.. method:: resize(newsize) .. method:: resize(newsize)
Resizes the map and the underlying file, if any. If the mmap was created Resizes the map and the underlying file, if any.
with :const:`ACCESS_READ` or :const:`ACCESS_COPY`, resizing the map will
raise a :exc:`TypeError` exception. Resizing a map created with *access* of :const:`ACCESS_READ` or
:const:`ACCESS_COPY`, will raise a :exc:`TypeError` exception.
Resizing a map created with with *trackfd* set to ``False``,
will raise a :exc:`ValueError` exception.
**On Windows**: Resizing the map will raise an :exc:`OSError` if there are other **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 maps against the same named file. Resizing an anonymous map (ie against the

View file

@ -254,6 +254,9 @@ mmap
that can be used where it requires a file-like object with seekable and that can be used where it requires a file-like object with seekable and
the :meth:`~mmap.mmap.seek` method return the new absolute position. the :meth:`~mmap.mmap.seek` method return the new absolute position.
(Contributed by Donghee Na and Sylvie Liberman in :gh:`111835`.) (Contributed by Donghee Na and Sylvie Liberman in :gh:`111835`.)
* :class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is ``False``,
the file descriptor specified by *fileno* will not be duplicated.
(Contributed by Zackery Spytz and Petr Viktorin in :gh:`78502`.)
opcode opcode
------ ------

View file

@ -4,6 +4,7 @@
from test.support.import_helper import import_module from test.support.import_helper import import_module
from test.support.os_helper import TESTFN, unlink from test.support.os_helper import TESTFN, unlink
import unittest import unittest
import errno
import os import os
import re import re
import itertools import itertools
@ -266,6 +267,62 @@ def test_access_parameter(self):
self.assertRaises(TypeError, m.write_byte, 0) self.assertRaises(TypeError, m.write_byte, 0)
m.close() m.close()
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
def test_trackfd_parameter(self):
size = 64
with open(TESTFN, "wb") as f:
f.write(b"a"*size)
for close_original_fd in True, False:
with self.subTest(close_original_fd=close_original_fd):
with open(TESTFN, "r+b") as f:
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
if close_original_fd:
f.close()
self.assertEqual(len(m), size)
with self.assertRaises(OSError) as err_cm:
m.size()
self.assertEqual(err_cm.exception.errno, errno.EBADF)
with self.assertRaises(ValueError):
m.resize(size * 2)
with self.assertRaises(ValueError):
m.resize(size // 2)
self.assertEqual(m.closed, False)
# Smoke-test other API
m.write_byte(ord('X'))
m[2] = ord('Y')
m.flush()
with open(TESTFN, "rb") as f:
self.assertEqual(f.read(4), b'XaYa')
self.assertEqual(m.tell(), 1)
m.seek(0)
self.assertEqual(m.tell(), 0)
self.assertEqual(m.read_byte(), ord('X'))
self.assertEqual(m.closed, True)
self.assertEqual(os.stat(TESTFN).st_size, size)
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
def test_trackfd_neg1(self):
size = 64
with mmap.mmap(-1, size, trackfd=False) as m:
with self.assertRaises(OSError):
m.size()
with self.assertRaises(ValueError):
m.resize(size // 2)
self.assertEqual(len(m), size)
m[0] = ord('a')
assert m[0] == ord('a')
@unittest.skipIf(os.name != 'nt', 'trackfd only fails on Windows')
def test_no_trackfd_parameter_on_windows(self):
# 'trackffd' is an invalid keyword argument for this function
size = 64
with self.assertRaises(TypeError):
mmap.mmap(-1, size, trackfd=True)
with self.assertRaises(TypeError):
mmap.mmap(-1, size, trackfd=False)
def test_bad_file_desc(self): def test_bad_file_desc(self):
# Try opening a bad file descriptor... # Try opening a bad file descriptor...
self.assertRaises(OSError, mmap.mmap, -2, 4096) self.assertRaises(OSError, mmap.mmap, -2, 4096)

View file

@ -0,0 +1,2 @@
:class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is
``False``, the file descriptor specified by *fileno* will not be duplicated.

View file

@ -117,6 +117,7 @@ typedef struct {
#ifdef UNIX #ifdef UNIX
int fd; int fd;
_Bool trackfd;
#endif #endif
PyObject *weakreflist; PyObject *weakreflist;
@ -393,6 +394,13 @@ is_resizeable(mmap_object *self)
"mmap can't resize with extant buffers exported."); "mmap can't resize with extant buffers exported.");
return 0; return 0;
} }
#ifdef UNIX
if (!self->trackfd) {
PyErr_SetString(PyExc_ValueError,
"mmap can't resize with trackfd=False.");
return 0;
}
#endif
if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT)) if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT))
return 1; return 1;
PyErr_Format(PyExc_TypeError, PyErr_Format(PyExc_TypeError,
@ -1154,7 +1162,7 @@ is 0, the maximum length of the map is the current size of the file,\n\
except that if the file is empty Windows raises an exception (you cannot\n\ except that if the file is empty Windows raises an exception (you cannot\n\
create an empty mapping on Windows).\n\ create an empty mapping on Windows).\n\
\n\ \n\
Unix: mmap(fileno, length[, flags[, prot[, access[, offset]]]])\n\ Unix: mmap(fileno, length[, flags[, prot[, access[, offset[, trackfd]]]]])\n\
\n\ \n\
Maps length bytes from the file specified by the file descriptor fileno,\n\ Maps length bytes from the file specified by the file descriptor fileno,\n\
and returns a mmap object. If length is 0, the maximum length of the map\n\ and returns a mmap object. If length is 0, the maximum length of the map\n\
@ -1221,15 +1229,17 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
off_t offset = 0; off_t offset = 0;
int fd, flags = MAP_SHARED, prot = PROT_WRITE | PROT_READ; int fd, flags = MAP_SHARED, prot = PROT_WRITE | PROT_READ;
int devzero = -1; int devzero = -1;
int access = (int)ACCESS_DEFAULT; int access = (int)ACCESS_DEFAULT, trackfd = 1;
static char *keywords[] = {"fileno", "length", static char *keywords[] = {"fileno", "length",
"flags", "prot", "flags", "prot",
"access", "offset", NULL}; "access", "offset", "trackfd", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|iii" _Py_PARSE_OFF_T, keywords, if (!PyArg_ParseTupleAndKeywords(args, kwdict,
"in|iii" _Py_PARSE_OFF_T "$p", keywords,
&fd, &map_size, &flags, &prot, &fd, &map_size, &flags, &prot,
&access, &offset)) &access, &offset, &trackfd)) {
return NULL; return NULL;
}
if (map_size < 0) { if (map_size < 0) {
PyErr_SetString(PyExc_OverflowError, PyErr_SetString(PyExc_OverflowError,
"memory mapped length must be positive"); "memory mapped length must be positive");
@ -1325,6 +1335,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
m_obj->weakreflist = NULL; m_obj->weakreflist = NULL;
m_obj->exports = 0; m_obj->exports = 0;
m_obj->offset = offset; m_obj->offset = offset;
m_obj->trackfd = trackfd;
if (fd == -1) { if (fd == -1) {
m_obj->fd = -1; m_obj->fd = -1;
/* Assume the caller wants to map anonymous memory. /* Assume the caller wants to map anonymous memory.
@ -1350,13 +1361,16 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
} }
#endif #endif
} }
else { else if (trackfd) {
m_obj->fd = _Py_dup(fd); m_obj->fd = _Py_dup(fd);
if (m_obj->fd == -1) { if (m_obj->fd == -1) {
Py_DECREF(m_obj); Py_DECREF(m_obj);
return NULL; return NULL;
} }
} }
else {
m_obj->fd = -1;
}
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS
m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset); m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset);