From 74df1a473a6b5cbeb97b918e7d8ba1c98f545722 Mon Sep 17 00:00:00 2001 From: "Teppei.F" <37261985+T3pp31@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:56:32 +0900 Subject: [PATCH] Fix sniff timeout when libpcap select returns readable with no packets Use nonblock_recv() in timed sniff loops when the socket supports it, so a blocking pcap_next_ex cannot stall past the sniff timeout (#4590). Regression test simulates libpcap reporting the fd as readable while nonblock_recv returns no BPF-matched packet. Co-authored-by: Cursor --- scapy/contrib/isotp/isotp_utils.py | 7 ++++-- scapy/sendrecv.py | 6 ++++- scapy/sessions.py | 14 ++++++++--- test/regression.uts | 39 ++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/scapy/contrib/isotp/isotp_utils.py b/scapy/contrib/isotp/isotp_utils.py index da1d5e73ab9..5ea1eb1c962 100644 --- a/scapy/contrib/isotp/isotp_utils.py +++ b/scapy/contrib/isotp/isotp_utils.py @@ -345,11 +345,14 @@ def __init__(self, *args, **kwargs): basecls=kwargs.pop("basecls", ISOTP)) super(ISOTPSession, self).__init__(*args, **kwargs) - def recv(self, sock: SuperSocket) -> Iterator[Packet]: + def recv(self, sock: SuperSocket, nonblock: bool = False) -> Iterator[Packet]: """ Will be called by sniff() to ask for a packet """ - pkt = sock.recv() + if nonblock and hasattr(sock, "nonblock_recv"): + pkt = sock.nonblock_recv() + else: + pkt = sock.recv() if not pkt: return self.m.feed(pkt) diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 3b119a1e905..b3787f75215 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -1336,7 +1336,11 @@ def stop_cb(): # The session object is passed the socket to call recv() on, # and may perform additional processing (ip defrag, etc.) try: - packets = session.recv(s) + packets = session.recv( + s, + nonblock=timeout is not None and + hasattr(s, "nonblock_recv"), + ) # A session can return multiple objects for p in packets: if lfilter and not lfilter(p): diff --git a/scapy/sessions.py b/scapy/sessions.py index 3c58dc2c6a3..df7302fd14f 100644 --- a/scapy/sessions.py +++ b/scapy/sessions.py @@ -51,11 +51,14 @@ def process(self, pkt: Packet) -> Optional[Packet]: return self.supersession.process(pkt) return pkt - def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: + def recv(self, sock: 'SuperSocket', nonblock: bool = False) -> Iterator[Packet]: """ Will be called by sniff() to ask for a packet """ - pkt = sock.recv() + if nonblock and hasattr(sock, "nonblock_recv"): + pkt = sock.nonblock_recv() + else: + pkt = sock.recv() if not pkt: return pkt = self.process(pkt) @@ -407,11 +410,14 @@ def process(self, return pkt return None - def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: + def recv(self, sock: 'SuperSocket', nonblock: bool = False) -> Iterator[Packet]: """ Will be called by sniff() to ask for a packet """ - pkt = sock.recv(stop_dissection_after=self.stop_dissection_after) + if nonblock and hasattr(sock, "nonblock_recv"): + pkt = sock.nonblock_recv() + else: + pkt = sock.recv(stop_dissection_after=self.stop_dissection_after) # Now handle TCP reassembly if self.app: while pkt is not None: diff --git a/test/regression.uts b/test/regression.uts index 3c055f831f7..b775d5aa14f 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -2061,6 +2061,45 @@ o.send(REFPACKET) pkts = sniff(opened_socket=[o], timeout=3) assert len(pkts) == 10 += sniff timeout with select-ready but empty pcap socket (#4590) + +import time +from scapy.automaton import ObjectPipe +from scapy.supersocket import SuperSocket + +class PcapLikeSocket(SuperSocket): + def __init__(self): + self.ins = ObjectPipe(name="pcap_like_socket") + self.outs = None + self.recv_called = False + + def recv(self, x=65535, **kwargs): + self.recv_called = True + time.sleep(60) + return None + + def nonblock_recv(self, x=65535): + return None + + @staticmethod + def select(sockets, remain=None): + return list(sockets) + + def close(self): + if self.closed: + return + self.closed = True + self.ins.close() + +sock = PcapLikeSocket() +t0 = time.monotonic() +pkts = sniff(opened_socket=sock, timeout=1, count=1) +elapsed = time.monotonic() - t0 +assert not sock.recv_called, "blocking recv must not be used with timeout" +assert elapsed < 3, "sniff should stop on timeout, got %.1fs" % elapsed +assert len(pkts) == 0 +sock.close() + = GH issue 3306 ~ netaccess needs_root