diff --git a/Lib/test/test_devpoll.py b/Lib/test/test_devpoll.py index 85e0accb611b1d..5951f8817ab1c1 100644 --- a/Lib/test/test_devpoll.py +++ b/Lib/test/test_devpoll.py @@ -2,6 +2,7 @@ # Initial tests are copied as is from "test_poll.py" +import errno import os import random import select @@ -112,6 +113,15 @@ def test_close(self): self.assertRaises(ValueError, devpoll.register, fd, select.POLLIN) self.assertRaises(ValueError, devpoll.unregister, fd) + def test_close_error(self): + # gh-146205: close() should raise OSError if underlying fd is invalid + devpoll = select.devpoll() + fd = devpoll.fileno() + os.close(fd) + with self.assertRaises(OSError) as cm: + devpoll.close() + self.assertEqual(cm.exception.errno, errno.EBADF) + def test_fd_non_inheritable(self): devpoll = select.devpoll() self.addCleanup(devpoll.close) diff --git a/Lib/test/test_epoll.py b/Lib/test/test_epoll.py index c94946a6ae6b7c..5e6a4ab0166a86 100644 --- a/Lib/test/test_epoll.py +++ b/Lib/test/test_epoll.py @@ -259,6 +259,15 @@ def test_close(self): self.assertRaises(ValueError, epoll.register, fd, select.EPOLLIN) self.assertRaises(ValueError, epoll.unregister, fd) + def test_close_error(self): + # gh-146205: close() should raise OSError if underlying fd is invalid + epoll = select.epoll() + fd = epoll.fileno() + os.close(fd) + with self.assertRaises(OSError) as cm: + epoll.close() + self.assertEqual(cm.exception.errno, errno.EBADF) + def test_fd_non_inheritable(self): epoll = select.epoll() self.addCleanup(epoll.close) diff --git a/Lib/test/test_kqueue.py b/Lib/test/test_kqueue.py index d2ab45c4a5b1ea..2cf99be9e2c3ba 100644 --- a/Lib/test/test_kqueue.py +++ b/Lib/test/test_kqueue.py @@ -254,6 +254,15 @@ def test_close(self): # operations must fail with ValueError("I/O operation on closed ...") self.assertRaises(ValueError, kqueue.control, None, 4) + def test_close_error(self): + # gh-146205: close() should raise OSError if underlying fd is invalid + kqueue = select.kqueue() + fd = kqueue.fileno() + os.close(fd) + with self.assertRaises(OSError) as cm: + kqueue.close() + self.assertEqual(cm.exception.errno, errno.EBADF) + def test_fd_non_inheritable(self): kqueue = select.kqueue() self.addCleanup(kqueue.close) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-20-12-52-55.gh-issue-146205.M4yKdf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-20-12-52-55.gh-issue-146205.M4yKdf.rst new file mode 100644 index 00000000000000..e9d95cdf836dba --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-20-12-52-55.gh-issue-146205.M4yKdf.rst @@ -0,0 +1,2 @@ +Fixed a bug where :meth:`select.epoll.close`, :meth:`select.kqueue.close`, +and :meth:`select.devpoll.close` silently ignored errors. diff --git a/Modules/selectmodule.c b/Modules/selectmodule.c index 137bf2ca55bbf8..4dd544c6ee8d34 100644 --- a/Modules/selectmodule.c +++ b/Modules/selectmodule.c @@ -1118,8 +1118,9 @@ static PyObject * select_devpoll_close_impl(devpollObject *self) /*[clinic end generated code: output=26b355bd6429f21b input=408fde21a377ccfb]*/ { - errno = devpoll_internal_close(self); - if (errno < 0) { + int err = devpoll_internal_close(self); + if (err != 0) { + errno = err; PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -1446,8 +1447,9 @@ static PyObject * select_epoll_close_impl(pyEpoll_Object *self) /*[clinic end generated code: output=ee2144c446a1a435 input=f626a769192e1dbe]*/ { - errno = pyepoll_internal_close(self); - if (errno < 0) { + int err = pyepoll_internal_close(self); + if (err != 0) { + errno = err; PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -2263,8 +2265,9 @@ static PyObject * select_kqueue_close_impl(kqueue_queue_Object *self) /*[clinic end generated code: output=d1c7df0b407a4bc1 input=6d763c858b17b690]*/ { - errno = kqueue_queue_internal_close(self); - if (errno < 0) { + int err = kqueue_queue_internal_close(self); + if (err != 0) { + errno = err; PyErr_SetFromErrno(PyExc_OSError); return NULL; }