Skip to content

Commit 0f7671c

Browse files
authored
[3.12] gh-110395: invalidate open kqueues after fork (GH-110517) (#111745)
* [3.12] gh-110395: invalidate open kqueues after fork (GH-110517) Invalidate open select.kqueue instances after fork as the fd will be invalid in the child. (cherry picked from commit a6c1c04) Co-authored-by: Davide Rizzo <[email protected]> * move assert to after the child dying this is in `main` via https://github.com/python/cpython/pull/111816/files
1 parent 3bd8b74 commit 0f7671c

File tree

3 files changed

+163
-7
lines changed

3 files changed

+163
-7
lines changed

Lib/test/test_kqueue.py

+18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import select
77
import socket
8+
from test import support
89
import time
910
import unittest
1011

@@ -256,6 +257,23 @@ def test_fd_non_inheritable(self):
256257
self.addCleanup(kqueue.close)
257258
self.assertEqual(os.get_inheritable(kqueue.fileno()), False)
258259

260+
@support.requires_fork()
261+
def test_fork(self):
262+
# gh-110395: kqueue objects must be closed after fork
263+
kqueue = select.kqueue()
264+
if (pid := os.fork()) == 0:
265+
try:
266+
self.assertTrue(kqueue.closed)
267+
with self.assertRaisesRegex(ValueError, "closed kqueue"):
268+
kqueue.fileno()
269+
except:
270+
os._exit(1)
271+
finally:
272+
os._exit(0)
273+
else:
274+
support.wait_process(pid, exitcode=0)
275+
self.assertFalse(kqueue.closed) # child done, we're still open.
276+
259277

260278
if __name__ == "__main__":
261279
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Ensure that :func:`select.kqueue` objects correctly appear as closed in
2+
forked children, to prevent operations on an invalid file descriptor.

Modules/selectmodule.c

+143-7
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,16 @@
1414

1515
#include "Python.h"
1616
#include "pycore_fileutils.h" // _Py_set_inheritable()
17+
#include "pycore_import.h" // _PyImport_GetModuleAttrString()
18+
#include "pycore_time.h" // _PyTime_t
1719
#include "structmember.h" // PyMemberDef
1820

21+
#include <stdbool.h>
22+
#include <stddef.h> // offsetof()
23+
#ifndef MS_WINDOWS
24+
# include <unistd.h> // close()
25+
#endif
26+
1927
#ifdef HAVE_SYS_DEVPOLL_H
2028
#include <sys/resource.h>
2129
#include <sys/devpoll.h>
@@ -70,13 +78,26 @@ extern void bzero(void *, int);
7078
# define POLLPRI 0
7179
#endif
7280

81+
#ifdef HAVE_KQUEUE
82+
// Linked list to track kqueue objects with an open fd, so
83+
// that we can invalidate them at fork;
84+
typedef struct _kqueue_list_item {
85+
struct kqueue_queue_Object *obj;
86+
struct _kqueue_list_item *next;
87+
} _kqueue_list_item, *_kqueue_list;
88+
#endif
89+
7390
typedef struct {
7491
PyObject *close;
7592
PyTypeObject *poll_Type;
7693
PyTypeObject *devpoll_Type;
7794
PyTypeObject *pyEpoll_Type;
95+
#ifdef HAVE_KQUEUE
7896
PyTypeObject *kqueue_event_Type;
7997
PyTypeObject *kqueue_queue_Type;
98+
_kqueue_list kqueue_open_list;
99+
bool kqueue_tracking_initialized;
100+
#endif
80101
} _selectstate;
81102

82103
static struct PyModuleDef selectmodule;
@@ -1749,7 +1770,7 @@ typedef struct {
17491770

17501771
#define kqueue_event_Check(op, state) (PyObject_TypeCheck((op), state->kqueue_event_Type))
17511772

1752-
typedef struct {
1773+
typedef struct kqueue_queue_Object {
17531774
PyObject_HEAD
17541775
SOCKET kqfd; /* kqueue control fd */
17551776
} kqueue_queue_Object;
@@ -1935,13 +1956,116 @@ kqueue_queue_err_closed(void)
19351956
return NULL;
19361957
}
19371958

1959+
static PyObject*
1960+
kqueue_tracking_after_fork(PyObject *module) {
1961+
_selectstate *state = get_select_state(module);
1962+
_kqueue_list_item *item = state->kqueue_open_list;
1963+
state->kqueue_open_list = NULL;
1964+
while (item) {
1965+
// Safety: we hold the GIL, and references are removed from this list
1966+
// before the object is deallocated.
1967+
kqueue_queue_Object *obj = item->obj;
1968+
assert(obj->kqfd != -1);
1969+
obj->kqfd = -1;
1970+
_kqueue_list_item *next = item->next;
1971+
PyMem_Free(item);
1972+
item = next;
1973+
}
1974+
Py_RETURN_NONE;
1975+
}
1976+
1977+
static PyMethodDef kqueue_tracking_after_fork_def = {
1978+
"kqueue_tracking_after_fork", (PyCFunction)kqueue_tracking_after_fork,
1979+
METH_NOARGS, "Invalidate open select.kqueue objects after fork."
1980+
};
1981+
1982+
static void
1983+
kqueue_tracking_init(PyObject *module) {
1984+
_selectstate *state = get_select_state(module);
1985+
assert(state->kqueue_open_list == NULL);
1986+
// Register a callback to invalidate kqueues with open fds after fork.
1987+
PyObject *register_at_fork = NULL, *cb = NULL, *args = NULL,
1988+
*kwargs = NULL, *result = NULL;
1989+
register_at_fork = _PyImport_GetModuleAttrString("posix",
1990+
"register_at_fork");
1991+
if (register_at_fork == NULL) {
1992+
goto finally;
1993+
}
1994+
cb = PyCFunction_New(&kqueue_tracking_after_fork_def, module);
1995+
if (cb == NULL) {
1996+
goto finally;
1997+
}
1998+
args = PyTuple_New(0);
1999+
assert(args != NULL);
2000+
kwargs = Py_BuildValue("{sO}", "after_in_child", cb);
2001+
if (kwargs == NULL) {
2002+
goto finally;
2003+
}
2004+
result = PyObject_Call(register_at_fork, args, kwargs);
2005+
2006+
finally:
2007+
if (PyErr_Occurred()) {
2008+
// There are a few reasons registration can fail, especially if someone
2009+
// touched posix.register_at_fork. But everything else still works so
2010+
// instead of raising we issue a warning and move along.
2011+
PyObject *exc = PyErr_GetRaisedException();
2012+
PyObject *exctype = (PyObject*)Py_TYPE(exc);
2013+
PyErr_WarnFormat(PyExc_RuntimeWarning, 1,
2014+
"An exception of type %S was raised while registering an "
2015+
"after-fork handler for select.kqueue objects: %S", exctype, exc);
2016+
Py_DECREF(exc);
2017+
}
2018+
Py_XDECREF(register_at_fork);
2019+
Py_XDECREF(cb);
2020+
Py_XDECREF(args);
2021+
Py_XDECREF(kwargs);
2022+
Py_XDECREF(result);
2023+
state->kqueue_tracking_initialized = true;
2024+
}
2025+
2026+
static int
2027+
kqueue_tracking_add(_selectstate *state, kqueue_queue_Object *self) {
2028+
if (!state->kqueue_tracking_initialized) {
2029+
kqueue_tracking_init(PyType_GetModule(Py_TYPE(self)));
2030+
}
2031+
assert(self->kqfd >= 0);
2032+
_kqueue_list_item *item = PyMem_New(_kqueue_list_item, 1);
2033+
if (item == NULL) {
2034+
PyErr_NoMemory();
2035+
return -1;
2036+
}
2037+
item->obj = self;
2038+
item->next = state->kqueue_open_list;
2039+
state->kqueue_open_list = item;
2040+
return 0;
2041+
}
2042+
2043+
static void
2044+
kqueue_tracking_remove(_selectstate *state, kqueue_queue_Object *self) {
2045+
_kqueue_list *listptr = &state->kqueue_open_list;
2046+
while (*listptr != NULL) {
2047+
_kqueue_list_item *item = *listptr;
2048+
if (item->obj == self) {
2049+
*listptr = item->next;
2050+
PyMem_Free(item);
2051+
return;
2052+
}
2053+
listptr = &item->next;
2054+
}
2055+
// The item should be in the list when we remove it,
2056+
// and it should only be removed once at close time.
2057+
assert(0);
2058+
}
2059+
19382060
static int
19392061
kqueue_queue_internal_close(kqueue_queue_Object *self)
19402062
{
19412063
int save_errno = 0;
19422064
if (self->kqfd >= 0) {
19432065
int kqfd = self->kqfd;
19442066
self->kqfd = -1;
2067+
_selectstate *state = _selectstate_by_type(Py_TYPE(self));
2068+
kqueue_tracking_remove(state, self);
19452069
Py_BEGIN_ALLOW_THREADS
19462070
if (close(kqfd) < 0)
19472071
save_errno = errno;
@@ -1982,6 +2106,13 @@ newKqueue_Object(PyTypeObject *type, SOCKET fd)
19822106
return NULL;
19832107
}
19842108
}
2109+
2110+
_selectstate *state = _selectstate_by_type(type);
2111+
if (kqueue_tracking_add(state, self) < 0) {
2112+
Py_DECREF(self);
2113+
return NULL;
2114+
}
2115+
19852116
return (PyObject *)self;
19862117
}
19872118

@@ -2012,13 +2143,11 @@ select_kqueue_impl(PyTypeObject *type)
20122143
}
20132144

20142145
static void
2015-
kqueue_queue_dealloc(kqueue_queue_Object *self)
2146+
kqueue_queue_finalize(kqueue_queue_Object *self)
20162147
{
2017-
PyTypeObject* type = Py_TYPE(self);
2148+
PyObject* error = PyErr_GetRaisedException();
20182149
kqueue_queue_internal_close(self);
2019-
freefunc kqueue_free = PyType_GetSlot(type, Py_tp_free);
2020-
kqueue_free((PyObject *)self);
2021-
Py_DECREF((PyObject *)type);
2150+
PyErr_SetRaisedException(error);
20222151
}
20232152

20242153
/*[clinic input]
@@ -2352,11 +2481,11 @@ static PyMethodDef kqueue_queue_methods[] = {
23522481
};
23532482

23542483
static PyType_Slot kqueue_queue_Type_slots[] = {
2355-
{Py_tp_dealloc, kqueue_queue_dealloc},
23562484
{Py_tp_doc, (void*)select_kqueue__doc__},
23572485
{Py_tp_getset, kqueue_queue_getsetlist},
23582486
{Py_tp_methods, kqueue_queue_methods},
23592487
{Py_tp_new, select_kqueue},
2488+
{Py_tp_finalize, kqueue_queue_finalize},
23602489
{0, 0},
23612490
};
23622491

@@ -2401,8 +2530,11 @@ _select_traverse(PyObject *module, visitproc visit, void *arg)
24012530
Py_VISIT(state->poll_Type);
24022531
Py_VISIT(state->devpoll_Type);
24032532
Py_VISIT(state->pyEpoll_Type);
2533+
#ifdef HAVE_KQUEUE
24042534
Py_VISIT(state->kqueue_event_Type);
24052535
Py_VISIT(state->kqueue_queue_Type);
2536+
// state->kqueue_open_list only holds borrowed refs
2537+
#endif
24062538
return 0;
24072539
}
24082540

@@ -2415,8 +2547,10 @@ _select_clear(PyObject *module)
24152547
Py_CLEAR(state->poll_Type);
24162548
Py_CLEAR(state->devpoll_Type);
24172549
Py_CLEAR(state->pyEpoll_Type);
2550+
#ifdef HAVE_KQUEUE
24182551
Py_CLEAR(state->kqueue_event_Type);
24192552
Py_CLEAR(state->kqueue_queue_Type);
2553+
#endif
24202554
return 0;
24212555
}
24222556

@@ -2550,6 +2684,8 @@ _select_exec(PyObject *m)
25502684
#endif /* HAVE_EPOLL */
25512685

25522686
#ifdef HAVE_KQUEUE
2687+
state->kqueue_open_list = NULL;
2688+
25532689
state->kqueue_event_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
25542690
m, &kqueue_event_Type_spec, NULL);
25552691
if (state->kqueue_event_Type == NULL) {

0 commit comments

Comments
 (0)