Skip to content

Commit 8fd287b

Browse files
ZackerySpytzerlend-aaslandencukouserhiy-storchaka
authored
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 <[email protected]> Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 42b90cf commit 8fd287b

File tree

5 files changed

+101
-11
lines changed

5 files changed

+101
-11
lines changed

Doc/library/mmap.rst

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ update the underlying file.
4848

4949
To map anonymous memory, -1 should be passed as the fileno along with the length.
5050

51-
.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT[, offset])
51+
.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT, offset=0)
5252

5353
**(Windows version)** Maps *length* bytes from the file specified by the
5454
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
7171

7272
.. audit-event:: mmap.__new__ fileno,length,access,offset mmap.mmap
7373

74-
.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, access=ACCESS_DEFAULT[, offset])
74+
.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, \
75+
access=ACCESS_DEFAULT, offset=0, *, trackfd=True)
7576
:noindex:
7677
7778
**(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
102103
defaults to 0. *offset* must be a multiple of :const:`ALLOCATIONGRANULARITY`
103104
which is equal to :const:`PAGESIZE` on Unix systems.
104105

106+
If *trackfd* is ``False``, the file descriptor specified by *fileno* will
107+
not be duplicated, and the resulting :class:`!mmap` object will not
108+
be associated with the map's underlying file.
109+
This means that the :meth:`~mmap.mmap.size` and :meth:`~mmap.mmap.resize`
110+
methods will fail.
111+
This mode is useful to limit the number of open file descriptors.
112+
105113
To ensure validity of the created memory mapping the file specified
106114
by the descriptor *fileno* is internally automatically synchronized
107115
with the physical backing store on macOS.
108116

117+
.. versionchanged:: 3.13
118+
The *trackfd* parameter was added.
119+
109120
This example shows a simple way of using :class:`~mmap.mmap`::
110121

111122
import mmap
@@ -254,9 +265,12 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
254265

255266
.. method:: resize(newsize)
256267

257-
Resizes the map and the underlying file, if any. If the mmap was created
258-
with :const:`ACCESS_READ` or :const:`ACCESS_COPY`, resizing the map will
259-
raise a :exc:`TypeError` exception.
268+
Resizes the map and the underlying file, if any.
269+
270+
Resizing a map created with *access* of :const:`ACCESS_READ` or
271+
:const:`ACCESS_COPY`, will raise a :exc:`TypeError` exception.
272+
Resizing a map created with with *trackfd* set to ``False``,
273+
will raise a :exc:`ValueError` exception.
260274

261275
**On Windows**: Resizing the map will raise an :exc:`OSError` if there are other
262276
maps against the same named file. Resizing an anonymous map (ie against the

Doc/whatsnew/3.13.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ mmap
254254
that can be used where it requires a file-like object with seekable and
255255
the :meth:`~mmap.mmap.seek` method return the new absolute position.
256256
(Contributed by Donghee Na and Sylvie Liberman in :gh:`111835`.)
257+
* :class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is ``False``,
258+
the file descriptor specified by *fileno* will not be duplicated.
259+
(Contributed by Zackery Spytz and Petr Viktorin in :gh:`78502`.)
257260

258261
opcode
259262
------

Lib/test/test_mmap.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from test.support.import_helper import import_module
55
from test.support.os_helper import TESTFN, unlink
66
import unittest
7+
import errno
78
import os
89
import re
910
import itertools
@@ -266,6 +267,62 @@ def test_access_parameter(self):
266267
self.assertRaises(TypeError, m.write_byte, 0)
267268
m.close()
268269

270+
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
271+
def test_trackfd_parameter(self):
272+
size = 64
273+
with open(TESTFN, "wb") as f:
274+
f.write(b"a"*size)
275+
for close_original_fd in True, False:
276+
with self.subTest(close_original_fd=close_original_fd):
277+
with open(TESTFN, "r+b") as f:
278+
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
279+
if close_original_fd:
280+
f.close()
281+
self.assertEqual(len(m), size)
282+
with self.assertRaises(OSError) as err_cm:
283+
m.size()
284+
self.assertEqual(err_cm.exception.errno, errno.EBADF)
285+
with self.assertRaises(ValueError):
286+
m.resize(size * 2)
287+
with self.assertRaises(ValueError):
288+
m.resize(size // 2)
289+
self.assertEqual(m.closed, False)
290+
291+
# Smoke-test other API
292+
m.write_byte(ord('X'))
293+
m[2] = ord('Y')
294+
m.flush()
295+
with open(TESTFN, "rb") as f:
296+
self.assertEqual(f.read(4), b'XaYa')
297+
self.assertEqual(m.tell(), 1)
298+
m.seek(0)
299+
self.assertEqual(m.tell(), 0)
300+
self.assertEqual(m.read_byte(), ord('X'))
301+
302+
self.assertEqual(m.closed, True)
303+
self.assertEqual(os.stat(TESTFN).st_size, size)
304+
305+
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
306+
def test_trackfd_neg1(self):
307+
size = 64
308+
with mmap.mmap(-1, size, trackfd=False) as m:
309+
with self.assertRaises(OSError):
310+
m.size()
311+
with self.assertRaises(ValueError):
312+
m.resize(size // 2)
313+
self.assertEqual(len(m), size)
314+
m[0] = ord('a')
315+
assert m[0] == ord('a')
316+
317+
@unittest.skipIf(os.name != 'nt', 'trackfd only fails on Windows')
318+
def test_no_trackfd_parameter_on_windows(self):
319+
# 'trackffd' is an invalid keyword argument for this function
320+
size = 64
321+
with self.assertRaises(TypeError):
322+
mmap.mmap(-1, size, trackfd=True)
323+
with self.assertRaises(TypeError):
324+
mmap.mmap(-1, size, trackfd=False)
325+
269326
def test_bad_file_desc(self):
270327
# Try opening a bad file descriptor...
271328
self.assertRaises(OSError, mmap.mmap, -2, 4096)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:class:`mmap.mmap` now has a *trackfd* parameter on Unix; if it is
2+
``False``, the file descriptor specified by *fileno* will not be duplicated.

Modules/mmapmodule.c

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ typedef struct {
117117

118118
#ifdef UNIX
119119
int fd;
120+
_Bool trackfd;
120121
#endif
121122

122123
PyObject *weakreflist;
@@ -393,6 +394,13 @@ is_resizeable(mmap_object *self)
393394
"mmap can't resize with extant buffers exported.");
394395
return 0;
395396
}
397+
#ifdef UNIX
398+
if (!self->trackfd) {
399+
PyErr_SetString(PyExc_ValueError,
400+
"mmap can't resize with trackfd=False.");
401+
return 0;
402+
}
403+
#endif
396404
if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT))
397405
return 1;
398406
PyErr_Format(PyExc_TypeError,
@@ -1154,7 +1162,7 @@ is 0, the maximum length of the map is the current size of the file,\n\
11541162
except that if the file is empty Windows raises an exception (you cannot\n\
11551163
create an empty mapping on Windows).\n\
11561164
\n\
1157-
Unix: mmap(fileno, length[, flags[, prot[, access[, offset]]]])\n\
1165+
Unix: mmap(fileno, length[, flags[, prot[, access[, offset[, trackfd]]]]])\n\
11581166
\n\
11591167
Maps length bytes from the file specified by the file descriptor fileno,\n\
11601168
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)
12211229
off_t offset = 0;
12221230
int fd, flags = MAP_SHARED, prot = PROT_WRITE | PROT_READ;
12231231
int devzero = -1;
1224-
int access = (int)ACCESS_DEFAULT;
1232+
int access = (int)ACCESS_DEFAULT, trackfd = 1;
12251233
static char *keywords[] = {"fileno", "length",
12261234
"flags", "prot",
1227-
"access", "offset", NULL};
1235+
"access", "offset", "trackfd", NULL};
12281236

1229-
if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|iii" _Py_PARSE_OFF_T, keywords,
1237+
if (!PyArg_ParseTupleAndKeywords(args, kwdict,
1238+
"in|iii" _Py_PARSE_OFF_T "$p", keywords,
12301239
&fd, &map_size, &flags, &prot,
1231-
&access, &offset))
1240+
&access, &offset, &trackfd)) {
12321241
return NULL;
1242+
}
12331243
if (map_size < 0) {
12341244
PyErr_SetString(PyExc_OverflowError,
12351245
"memory mapped length must be positive");
@@ -1325,6 +1335,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
13251335
m_obj->weakreflist = NULL;
13261336
m_obj->exports = 0;
13271337
m_obj->offset = offset;
1338+
m_obj->trackfd = trackfd;
13281339
if (fd == -1) {
13291340
m_obj->fd = -1;
13301341
/* Assume the caller wants to map anonymous memory.
@@ -1350,13 +1361,16 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
13501361
}
13511362
#endif
13521363
}
1353-
else {
1364+
else if (trackfd) {
13541365
m_obj->fd = _Py_dup(fd);
13551366
if (m_obj->fd == -1) {
13561367
Py_DECREF(m_obj);
13571368
return NULL;
13581369
}
13591370
}
1371+
else {
1372+
m_obj->fd = -1;
1373+
}
13601374

13611375
Py_BEGIN_ALLOW_THREADS
13621376
m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset);

0 commit comments

Comments
 (0)