From 91d4f4a0038c6ff6d14912c369b427c0cf449905 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 11 May 2026 21:01:38 -0700 Subject: [PATCH 1/3] fix(socketcan): support socket file descriptors over 1023 select.select() is limited by glibc's FD_SETSIZE (1024) and raises ValueError for higher fds even when the OS limit allows them. Switch SocketcanBus._recv_internal() and send() to select.poll(), which has no such limit. Fixes #2053. Signed-off-by: SAY-5 --- can/interfaces/socketcan/socketcan.py | 20 ++++-- test/test_socketcan_high_fd.py | 91 +++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 test/test_socketcan_high_fd.py diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 6dc856cbf..0a88a325a 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -815,9 +815,15 @@ def shutdown(self) -> None: def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: try: - # get all sockets that are ready (can be a list with a single value - # being self.socket or an empty list if self.socket is not ready) - ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) + # Wait for the socket to become readable. ``poll()`` is used in + # preference to ``select.select()`` because the latter is limited + # to file descriptors below ``FD_SETSIZE`` (1024 on glibc), and + # raises ``ValueError: filedescriptor out of range in select()`` + # for higher fds even when the OS limit allows them. + poller = select.poll() + poller.register(self.socket, select.POLLIN) + timeout_ms = -1 if timeout is None else max(0, int(timeout * 1000)) + ready_receive_sockets = poller.poll(timeout_ms) except OSError as error: # something bad happened (e.g. the interface went down) raise can.CanOperationError( @@ -857,9 +863,15 @@ def send(self, msg: Message, timeout: float | None = None) -> None: time_left = timeout data = build_can_frame(msg) + # ``poll()`` is used in preference to ``select.select()`` because the + # latter is limited to file descriptors below ``FD_SETSIZE`` (1024 on + # glibc) and raises ``ValueError`` for higher fds. + poller = select.poll() + poller.register(self.socket, select.POLLOUT) + while time_left >= 0: # Wait for write availability - ready = select.select([], [self.socket], [], time_left)[1] + ready = poller.poll(max(0, int(time_left * 1000))) if not ready: # Timeout break diff --git a/test/test_socketcan_high_fd.py b/test/test_socketcan_high_fd.py new file mode 100644 index 000000000..84bdd0586 --- /dev/null +++ b/test/test_socketcan_high_fd.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +""" +Regression tests for https://github.com/hardbyte/python-can/issues/2053. + +``SocketcanBus`` previously used ``select.select()``, which is limited by +glibc to file descriptors below ``FD_SETSIZE`` (1024) and raises +``ValueError: filedescriptor out of range in select()`` for higher fds. + +These tests verify that ``send()`` and ``recv()`` work with a socket whose +file descriptor exceeds 1023. +""" + +import unittest +from unittest.mock import MagicMock, patch + +import can +from can import Message +from can.interfaces.socketcan.socketcan import build_can_frame + +from .config import IS_LINUX + +HIGH_FD = 2048 + + +@unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") +class TestSocketcanHighFdLinux(unittest.TestCase): + """Verify SocketcanBus works when the underlying socket fd exceeds 1023.""" + + def setUp(self): + patcher_create = patch("can.interfaces.socketcan.socketcan.create_socket") + patcher_bind = patch("can.interfaces.socketcan.socketcan.bind_socket") + + self.mock_create_socket = patcher_create.start() + self.mock_bind_socket = patcher_bind.start() + + self.mock_socket = MagicMock() + self.mock_socket.fileno.return_value = HIGH_FD + self.mock_create_socket.return_value = self.mock_socket + + self.bus = can.Bus(interface="socketcan", channel="can0") + + self.addCleanup(patcher_create.stop) + self.addCleanup(patcher_bind.stop) + + def tearDown(self): + self.bus.shutdown() + + @patch("can.interfaces.socketcan.socketcan.select.poll") + def test_send_high_fd(self, mock_poll_factory): + """send() succeeds when the socket fd > 1023.""" + poller = MagicMock() + # ``poll()`` returns a non-empty list to signal write readiness. + poller.poll.return_value = [(HIGH_FD, 4)] + mock_poll_factory.return_value = poller + + msg = Message(arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6, 7, 8]) + frame_data = build_can_frame(msg) + self.mock_socket.send.return_value = len(frame_data) + + self.bus.send(msg) + + self.mock_socket.send.assert_called_once_with(frame_data) + poller.register.assert_called_once() + + @patch("can.interfaces.socketcan.socketcan.capture_message") + @patch("can.interfaces.socketcan.socketcan.select.poll") + def test_recv_high_fd(self, mock_poll_factory, mock_capture): + """recv() succeeds when the socket fd > 1023.""" + poller = MagicMock() + poller.poll.return_value = [(HIGH_FD, 1)] + mock_poll_factory.return_value = poller + + expected_msg = Message( + arbitration_id=0x123, + data=[1, 2, 3, 4, 5, 6, 7, 8], + channel="can0", + timestamp=1000.0, + ) + mock_capture.return_value = expected_msg + + msg = self.bus.recv(timeout=1.0) + + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x123) + self.assertEqual(msg.data, bytearray([1, 2, 3, 4, 5, 6, 7, 8])) + mock_capture.assert_called_once_with(self.mock_socket, False) + + +if __name__ == "__main__": + unittest.main() From e7d04a6a5df57a0e0d4ee002293a1fd35901d736 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 11 May 2026 21:02:06 -0700 Subject: [PATCH 2/3] docs: add towncrier news fragment for #2053 Signed-off-by: SAY-5 --- doc/changelog.d/2053.fixed.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/2053.fixed.rst diff --git a/doc/changelog.d/2053.fixed.rst b/doc/changelog.d/2053.fixed.rst new file mode 100644 index 000000000..fa24644da --- /dev/null +++ b/doc/changelog.d/2053.fixed.rst @@ -0,0 +1 @@ +``SocketcanBus`` now uses ``select.poll()`` instead of ``select.select()`` so that socket file descriptors above ``FD_SETSIZE`` (1024 on glibc) no longer raise ``ValueError: filedescriptor out of range in select()``. From 43dc1a5e5898bbfd72ab2db46619ce946f3c621a Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 12 May 2026 16:35:18 -0700 Subject: [PATCH 3/3] fix(socketcan): condense poll() comments to keep module under line limit --- can/interfaces/socketcan/socketcan.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 0a88a325a..2770a9f9c 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -815,11 +815,7 @@ def shutdown(self) -> None: def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: try: - # Wait for the socket to become readable. ``poll()`` is used in - # preference to ``select.select()`` because the latter is limited - # to file descriptors below ``FD_SETSIZE`` (1024 on glibc), and - # raises ``ValueError: filedescriptor out of range in select()`` - # for higher fds even when the OS limit allows them. + # poll() avoids select.select()'s ValueError for fds >= FD_SETSIZE poller = select.poll() poller.register(self.socket, select.POLLIN) timeout_ms = -1 if timeout is None else max(0, int(timeout * 1000)) @@ -863,9 +859,7 @@ def send(self, msg: Message, timeout: float | None = None) -> None: time_left = timeout data = build_can_frame(msg) - # ``poll()`` is used in preference to ``select.select()`` because the - # latter is limited to file descriptors below ``FD_SETSIZE`` (1024 on - # glibc) and raises ``ValueError`` for higher fds. + # poll() avoids select.select()'s ValueError for fds >= FD_SETSIZE poller = select.poll() poller.register(self.socket, select.POLLOUT)