From 91d4f4a0038c6ff6d14912c369b427c0cf449905 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 11 May 2026 21:01:38 -0700 Subject: [PATCH 1/4] 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/4] 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/4] 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) From 977fad471c8d3493b492b08630e045dfe1d9804c Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 9 Jun 2026 15:09:03 -0700 Subject: [PATCH 4/4] fix(socketcan): create poll objects once in __init__, move high-fd test into test_socketcan Signed-off-by: Sai Asish Y --- can/interfaces/socketcan/socketcan.py | 19 +++--- test/test_socketcan.py | 49 +++++++++++++++ test/test_socketcan_high_fd.py | 91 --------------------------- 3 files changed, 59 insertions(+), 100 deletions(-) delete mode 100644 test/test_socketcan_high_fd.py diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 2770a9f9c..2e17ff50f 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -741,6 +741,14 @@ def __init__( self._task_id_guard = threading.Lock() self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20 + # poll() avoids select.select()'s ValueError for fds >= FD_SETSIZE. + # One poller per direction so a writable socket cannot wake up a + # blocking receive and concurrent send/recv share no state. + self._poll_in = select.poll() + self._poll_in.register(self.socket, select.POLLIN) + self._poll_out = select.poll() + self._poll_out.register(self.socket, select.POLLOUT) + # set the local_loopback parameter try: self.socket.setsockopt( @@ -815,11 +823,8 @@ def shutdown(self) -> None: def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: try: - # 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)) - ready_receive_sockets = poller.poll(timeout_ms) + ready_receive_sockets = self._poll_in.poll(timeout_ms) except OSError as error: # something bad happened (e.g. the interface went down) raise can.CanOperationError( @@ -859,13 +864,9 @@ def send(self, msg: Message, timeout: float | None = None) -> None: time_left = timeout data = build_can_frame(msg) - # poll() avoids select.select()'s ValueError for fds >= FD_SETSIZE - poller = select.poll() - poller.register(self.socket, select.POLLOUT) - while time_left >= 0: # Wait for write availability - ready = poller.poll(max(0, int(time_left * 1000))) + ready = self._poll_out.poll(int(time_left * 1000)) if not ready: # Timeout break diff --git a/test/test_socketcan.py b/test/test_socketcan.py index 9d042f425..beeb1031a 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -5,6 +5,7 @@ """ import ctypes +import select import struct import sys import unittest @@ -26,6 +27,7 @@ build_bcm_transmit_header, build_bcm_tx_delete_header, build_bcm_update_header, + build_can_frame, ) from .config import IS_LINUX, IS_PYPY, TEST_INTERFACE_SOCKETCAN @@ -391,5 +393,52 @@ def test_pypy_socketcan_support(self): ) +@unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") +class SocketCANHighFdTest(unittest.TestCase): + """SocketcanBus must work with socket fds above FD_SETSIZE, see #2053.""" + + HIGH_FD = 2048 + + def setUp(self): + patcher_poll = patch("can.interfaces.socketcan.socketcan.select.poll") + patcher_create = patch("can.interfaces.socketcan.socketcan.create_socket") + patcher_bind = patch("can.interfaces.socketcan.socketcan.bind_socket") + self.mock_poller = patcher_poll.start().return_value + self.mock_socket = patcher_create.start().return_value + patcher_bind.start() + self.addCleanup(patcher_poll.stop) + self.addCleanup(patcher_create.stop) + self.addCleanup(patcher_bind.stop) + + self.mock_socket.fileno.return_value = self.HIGH_FD + self.bus = can.Bus(interface="socketcan", channel="can0") + self.addCleanup(self.bus.shutdown) + + def test_send_high_fd(self): + self.mock_poller.poll.return_value = [(self.HIGH_FD, select.POLLOUT)] + msg = can.Message(arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6, 7, 8]) + frame = build_can_frame(msg) + self.mock_socket.send.return_value = len(frame) + + self.bus.send(msg) + + self.mock_socket.send.assert_called_once_with(frame) + + def test_recv_high_fd(self): + self.mock_poller.poll.return_value = [(self.HIGH_FD, select.POLLIN)] + expected_msg = can.Message( + arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6, 7, 8], timestamp=1000.0 + ) + with patch( + "can.interfaces.socketcan.socketcan.capture_message" + ) as mock_capture: + mock_capture.return_value = expected_msg + msg = self.bus.recv(timeout=1.0) + + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x123) + mock_capture.assert_called_once_with(self.mock_socket, False) + + if __name__ == "__main__": unittest.main() diff --git a/test/test_socketcan_high_fd.py b/test/test_socketcan_high_fd.py deleted file mode 100644 index 84bdd0586..000000000 --- a/test/test_socketcan_high_fd.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/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()