1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-05-15 05:42:23 -04:00

fix: add socket lock to prevent TX/RX race causing stream corruption on retransmits

The RX reader thread sets setblocking(0) and the TX writer (via aprslib
sendall) sets setblocking(1) on the same socket without synchronization.
This race condition causes partial writes where other stations' APRS-IS
stream data gets concatenated onto retransmitted packets.

Add a shared _socket_lock between send() and _socket_readlines() so the
socket blocking mode is never changed by one thread while the other is
mid-operation.
This commit is contained in:
Walter Boring 2026-05-14 16:55:57 -04:00
parent 9146ff76c9
commit 5cc918e5c2

View File

@ -44,6 +44,13 @@ class APRSLibClient(aprslib.IS):
select_timeout = 1
lock = threading.Lock()
# Shared lock between RX (reader) and TX (writer) to prevent
# socket state races. The reader sets setblocking(0) and the
# writer's sendall (in aprslib) sets setblocking(1). Without
# synchronization, these can race causing partial writes and
# stream corruption on retransmits.
_socket_lock = threading.Lock()
def stop(self):
self.thread_stop = True
LOG.warning('Shutdown Aprsdis client.')
@ -54,8 +61,15 @@ class APRSLibClient(aprslib.IS):
@wrapt.synchronized(lock)
def send(self, packet: core.Packet):
"""Send an APRS Message object."""
self.sendall(packet.raw)
"""Send an APRS Message object.
Uses _socket_lock to prevent racing with the reader thread's
setblocking(0) call. The upstream aprslib sendall() sets
setblocking(1) before writing, which can corrupt in-progress
recv() calls if unsynchronized.
"""
with self._socket_lock:
self.sendall(packet.raw)
def is_alive(self):
"""If the connection is alive or not."""
@ -141,7 +155,13 @@ class APRSLibClient(aprslib.IS):
continue
try:
short_buf = self.sock.recv(4096)
with self._socket_lock:
# Re-ensure non-blocking mode inside the lock.
# The send() path (via aprslib sendall) sets
# setblocking(1), so we must restore non-blocking
# before recv() to avoid blocking indefinitely.
self.sock.setblocking(0)
short_buf = self.sock.recv(4096)
# sock.recv returns empty if the connection drops
if not short_buf: