Skip to content

Commit a6c1c04

Browse files
authored
pythongh-110395: invalidate open kqueues after fork (python#110517)
Invalidate open select.kqueue instances after fork as the fd will be invalid in the child.
1 parent cd6b2ce commit a6c1c04

File tree

3 files changed

+157
-7
lines changed

3 files changed

+157
-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+
self.assertFalse(kqueue.closed)
275+
support.wait_process(pid, exitcode=0)
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

+137-7
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
#include "Python.h"
1616
#include "pycore_fileutils.h" // _Py_set_inheritable()
17+
#include "pycore_import.h" // _PyImport_GetModuleAttrString()
1718
#include "pycore_time.h" // _PyTime_t
1819

20+
#include <stdbool.h>
1921
#include <stddef.h> // offsetof()
2022
#ifndef MS_WINDOWS
2123
# include <unistd.h> // close()
@@ -75,13 +77,26 @@ extern void bzero(void *, int);
7577
# define POLLPRI 0
7678
#endif
7779

80+
#ifdef HAVE_KQUEUE
81+
// Linked list to track kqueue objects with an open fd, so
82+
// that we can invalidate them at fork;
83+
typedef struct _kqueue_list_item {
84+
struct kqueue_queue_Object *obj;
85+
struct _kqueue_list_item *next;
86+
} _kqueue_list_item, *_kqueue_list;
87+
#endif
88+
7889
typedef struct {
7990
PyObject *close;
8091
PyTypeObject *poll_Type;
8192
PyTypeObject *devpoll_Type;
8293
PyTypeObject *pyEpoll_Type;
94+
#ifdef HAVE_KQUEUE
8395
PyTypeObject *kqueue_event_Type;
8496
PyTypeObject *kqueue_queue_Type;
97+
_kqueue_list kqueue_open_list;
98+
bool kqueue_tracking_initialized;
99+
#endif
85100
} _selectstate;
86101

87102
static struct PyModuleDef selectmodule;
@@ -1754,7 +1769,7 @@ typedef struct {
17541769

17551770
#define kqueue_event_Check(op, state) (PyObject_TypeCheck((op), state->kqueue_event_Type))
17561771

1757-
typedef struct {
1772+
typedef struct kqueue_queue_Object {
17581773
PyObject_HEAD
17591774
SOCKET kqfd; /* kqueue control fd */
17601775
} kqueue_queue_Object;
@@ -1940,13 +1955,116 @@ kqueue_queue_err_closed(void)
19401955
return NULL;
19411956
}
19421957

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

@@ -2017,13 +2142,11 @@ select_kqueue_impl(PyTypeObject *type)
20172142
}
20182143

20192144
static void
2020-
kqueue_queue_dealloc(kqueue_queue_Object *self)
2145+
kqueue_queue_finalize(kqueue_queue_Object *self)
20212146
{
2022-
PyTypeObject* type = Py_TYPE(self);
2147+
PyObject* error = PyErr_GetRaisedException();
20232148
kqueue_queue_internal_close(self);
2024-
freefunc kqueue_free = PyType_GetSlot(type, Py_tp_free);
2025-
kqueue_free((PyObject *)self);
2026-
Py_DECREF((PyObject *)type);
2149+
PyErr_SetRaisedException(error);
20272150
}
20282151

20292152
/*[clinic input]
@@ -2357,11 +2480,11 @@ static PyMethodDef kqueue_queue_methods[] = {
23572480
};
23582481

23592482
static PyType_Slot kqueue_queue_Type_slots[] = {
2360-
{Py_tp_dealloc, kqueue_queue_dealloc},
23612483
{Py_tp_doc, (void*)select_kqueue__doc__},
23622484
{Py_tp_getset, kqueue_queue_getsetlist},
23632485
{Py_tp_methods, kqueue_queue_methods},
23642486
{Py_tp_new, select_kqueue},
2487+
{Py_tp_finalize, kqueue_queue_finalize},
23652488
{0, 0},
23662489
};
23672490

@@ -2406,8 +2529,11 @@ _select_traverse(PyObject *module, visitproc visit, void *arg)
24062529
Py_VISIT(state->poll_Type);
24072530
Py_VISIT(state->devpoll_Type);
24082531
Py_VISIT(state->pyEpoll_Type);
2532+
#ifdef HAVE_KQUEUE
24092533
Py_VISIT(state->kqueue_event_Type);
24102534
Py_VISIT(state->kqueue_queue_Type);
2535+
// state->kqueue_open_list only holds borrowed refs
2536+
#endif
24112537
return 0;
24122538
}
24132539

@@ -2420,8 +2546,10 @@ _select_clear(PyObject *module)
24202546
Py_CLEAR(state->poll_Type);
24212547
Py_CLEAR(state->devpoll_Type);
24222548
Py_CLEAR(state->pyEpoll_Type);
2549+
#ifdef HAVE_KQUEUE
24232550
Py_CLEAR(state->kqueue_event_Type);
24242551
Py_CLEAR(state->kqueue_queue_Type);
2552+
#endif
24252553
return 0;
24262554
}
24272555

@@ -2570,6 +2698,8 @@ _select_exec(PyObject *m)
25702698
} while (0)
25712699

25722700
#ifdef HAVE_KQUEUE
2701+
state->kqueue_open_list = NULL;
2702+
25732703
state->kqueue_event_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
25742704
m, &kqueue_event_Type_spec, NULL);
25752705
if (state->kqueue_event_Type == NULL) {

0 commit comments

Comments
 (0)