From b97da8930d25f633531f41e78249f30463b87856 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 4 Sep 2026 20:21:22 -0400 Subject: [PATCH 1/2] Sometimes notify_closing can be called on dead events --- newsfragments/3502.bugfix.rst | 1 + src/trio/_core/_io_kqueue.py | 8 +++++++- src/trio/_core/_tests/test_guest_mode.py | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 newsfragments/3502.bugfix.rst diff --git a/newsfragments/3502.bugfix.rst b/newsfragments/3502.bugfix.rst new file mode 100644 index 0000000000..04ea9853c0 --- /dev/null +++ b/newsfragments/3502.bugfix.rst @@ -0,0 +1 @@ +Ensure `trio.lowlevel.notify_closing` works even on events that have been deleted, e.g. because they were registered as one shot events. diff --git a/src/trio/_core/_io_kqueue.py b/src/trio/_core/_io_kqueue.py index 464ca457e1..faaf88af7d 100644 --- a/src/trio/_core/_io_kqueue.py +++ b/src/trio/_core/_io_kqueue.py @@ -280,7 +280,13 @@ def notify_closing(self, fd: int | _HasFileNo) -> None: if type(receiver) is _core.Task: event = select.kevent(fd, filter_, select.KQ_EV_DELETE) - self._kqueue.control([event], 0) + try: + self._kqueue.control([event], 0) + except OSError as e: + if e.errno == errno.ENOENT: + # the event isn't in kqueue + continue + raise exc = _core.ClosedResourceError("another task closed this fd") _core.reschedule(receiver, outcome.Error(exc)) del self._registered[key] diff --git a/src/trio/_core/_tests/test_guest_mode.py b/src/trio/_core/_tests/test_guest_mode.py index 743eddc846..7acbc0f082 100644 --- a/src/trio/_core/_tests/test_guest_mode.py +++ b/src/trio/_core/_tests/test_guest_mode.py @@ -753,3 +753,24 @@ async def trio_main() -> None: aiotrio_run(trio_main, host_uses_signal_set_wakeup_fd=True) assert record == {("asyncio", "asyncio", True), ("trio", "trio", True)} + + +def test_notify_closing_after_events() -> None: + # inspired by wrong repro in https://github.com/python-trio/trio/pull/3502 + # either the program should silently pass or wait_writable should fail. + pair = socket.socketpair() + for sock in pair: + sock.setblocking(False) + + async def trio_main(in_host: InHost) -> None: + in_host(uh_oh) + with contextlib.suppress(trio.ClosedResourceError): + await trio.lowlevel.wait_writable(pair[0]) # blocks + + def uh_oh() -> None: + # this will run after trio gets events but before they are processed + trio.lowlevel.notify_closing(pair[0]) + + trivial_guest_run(trio_main) + for sock in pair: + sock.close() From 71691639a8769d488632dbbf8af2a93d17a2bace Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sat, 5 Sep 2026 02:11:32 -0400 Subject: [PATCH 2/2] Add some pragmas, I guess --- src/trio/_core/_io_kqueue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/trio/_core/_io_kqueue.py b/src/trio/_core/_io_kqueue.py index faaf88af7d..80d0ce6296 100644 --- a/src/trio/_core/_io_kqueue.py +++ b/src/trio/_core/_io_kqueue.py @@ -283,10 +283,10 @@ def notify_closing(self, fd: int | _HasFileNo) -> None: try: self._kqueue.control([event], 0) except OSError as e: - if e.errno == errno.ENOENT: + if e.errno == errno.ENOENT: # pragma: no branch # the event isn't in kqueue continue - raise + raise # pragma: no cover exc = _core.ClosedResourceError("another task closed this fd") _core.reschedule(receiver, outcome.Error(exc)) del self._registered[key]