From e69864861e3db80c87872625942c2a17f2c4987e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:22:24 -0500 Subject: [PATCH 1/2] isotp: fix soft socket .select() drops ObjectPipe, causing sr1() to hang in threaded mode The select() method was filtering out ObjectPipe instances (like the sniffer's close_pipe) from its return value. This prevented the sniffer's stop mechanism from working correctly in threaded mode - when sniffer.stop() sent to close_pipe, the select() method would unblock but not return the close_pipe, so the sniffer loop couldn't detect the stop signal and had to rely on continue_sniff timing, causing hangs under load. The fix includes close_pipe (ObjectPipe) instances in the select return value, so the sniffer loop properly detects the stop signal via the 'if s is close_pipe: break' check. Added two new tests: - sr1 timeout with threaded=True (no response scenario) - sr1 timeout with threaded=True and background CAN traffic The new "ISOTPSoftSocket select returns control ObjectPipe" test directly verifies that ISOTPSoftSocket.select() passes through ready ObjectPipe instances (e.g. the sniffer's close_pipe). This test deterministically FAILS without the fix and PASSES with it. The integration tests (sr1 timeout with threaded=True) are kept for end-to-end coverage but the race window is too narrow on Linux with TestSocket to reliably trigger the bug. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Ben Gardiner --- scapy/contrib/isotp/isotp_soft_socket.py | 12 ++- test/contrib/isotp_soft_socket.uts | 96 ++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py index 4182d445336..6ccb73665b8 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -202,8 +202,8 @@ def recv(self, x=0xffff, **kwargs): return msg @staticmethod - def select(sockets, remain=None): - # type: (List[SuperSocket], Optional[float]) -> List[SuperSocket] + def select(sockets, remain=None): # type: ignore[override] + # type: (List[Union[SuperSocket, ObjectPipe[Any]]], Optional[float]) -> List[Union[SuperSocket, ObjectPipe[Any]]] # noqa: E501 """This function is called during sendrecv() routine to wait for sockets to be ready to receive """ @@ -214,8 +214,12 @@ def select(sockets, remain=None): ready_pipes = select_objects(obj_pipes, remain) - return [x for x in sockets if isinstance(x, ISOTPSoftSocket) and - not x.closed and x.impl.rx_queue in ready_pipes] + result: List[Union[SuperSocket, ObjectPipe[Any]]] = [ + x for x in sockets if isinstance(x, ISOTPSoftSocket) and + not x.closed and x.impl.rx_queue in ready_pipes] + result += [x for x in sockets if isinstance(x, ObjectPipe) and + x in ready_pipes] + return result class TimeoutScheduler: diff --git a/test/contrib/isotp_soft_socket.uts b/test/contrib/isotp_soft_socket.uts index 2e7bdfaeccf..0f0a3992fda 100644 --- a/test/contrib/isotp_soft_socket.uts +++ b/test/contrib/isotp_soft_socket.uts @@ -954,6 +954,102 @@ with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as s assert rx2 is None += ISOTPSoftSocket select returns control ObjectPipe + +from scapy.automaton import ObjectPipe as _ObjectPipe + +close_pipe = _ObjectPipe("control_socket") +close_pipe.send(None) + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, 0x123, 0x321) as sock: + result = ISOTPSoftSocket.select([sock, close_pipe], remain=0) + +assert close_pipe in result + +close_pipe.close() + += ISOTPSoftSocket select returns control ObjectPipe alongside ready rx_queue + +from scapy.automaton import ObjectPipe as _ObjectPipe + +close_pipe = _ObjectPipe("control_socket") +close_pipe.send(None) + +with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, 0x641, 0x241) as sock: + sock.impl.rx_queue.send((b'\x62\xF1\x90\x41\x42\x43', 0.0)) + result = ISOTPSoftSocket.select([sock, close_pipe], remain=0) + +assert close_pipe in result +assert sock in result + +close_pipe.close() + += ISOTPSoftSocket sr1 SF request with MF response threaded + +from threading import Thread + +request = ISOTP(b'\x22\xF1\x90') +response_data = b'\x62\xF1\x90' + b'\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50' +response_msg = ISOTP(response_data) + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x641, 0x241) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x241, 0x641) as sock_rx: + isocan_rx.pair(isocan_tx) + def responder(): + sniffed = sock_rx.sniff(count=1, timeout=5) + if sniffed: + sock_rx.send(response_msg) + resp_thread = Thread(target=responder, daemon=True) + resp_thread.start() + time.sleep(0.1) + rx = sock_tx.sr1(request, timeout=5, verbose=False, threaded=True) + resp_thread.join(timeout=3) + +assert rx is not None +assert rx.data == response_data + += ISOTPSoftSocket sr1 timeout with threaded=True + +from threading import Thread, Event +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x321, 0x123) as sock_rx: + isocan_rx.pair(isocan_tx) + start = time.time() + rx2 = sock_tx.sr1(msg, timeout=3, verbose=False, threaded=True) + elapsed = time.time() - start + +assert rx2 is None +assert elapsed < 5 + += ISOTPSoftSocket sr1 timeout with threaded=True and background traffic + +from threading import Thread, Event +msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') + +with TestSocket(CAN) as isocan_tx, ISOTPSoftSocket(isocan_tx, 0x123, 0x321) as sock_tx, \ + TestSocket(CAN) as isocan_rx, ISOTPSoftSocket(isocan_rx, 0x321, 0x123) as sock_rx: + isocan_rx.pair(isocan_tx) + stop_traffic = Event() + def bg_traffic(): + while not stop_traffic.is_set(): + try: + isocan_rx.send(CAN(identifier=0x456, data=dhex("01 02 03"))) + except Exception: + break + time.sleep(0.01) + traffic_thread = Thread(target=bg_traffic, daemon=True) + traffic_thread.start() + start = time.time() + rx2 = sock_tx.sr1(msg, timeout=3, verbose=False, threaded=True) + elapsed = time.time() - start + stop_traffic.set() + traffic_thread.join(timeout=2) + +assert rx2 is None +assert elapsed < 5 + = ISOTPSoftSocket sniff msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') From cc5b3062406927f766ec2aa7d1df93a61feba172 Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Fri, 27 Feb 2026 10:39:49 -0500 Subject: [PATCH 2/2] isotp: fix potential cause of intermittent test failures where soft socket is garbage collected --- scapy/contrib/isotp/isotp_soft_socket.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scapy/contrib/isotp/isotp_soft_socket.py b/scapy/contrib/isotp/isotp_soft_socket.py index 6ccb73665b8..5cfb1689308 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -166,7 +166,8 @@ def __init__(self, def close(self): # type: () -> None if not self.closed: - self.impl.close() + if hasattr(self, "impl"): + self.impl.close() self.closed = True def failure_analysis(self):