Skip to content

common: fail RPC receive when the peer closes mid-payload - #113

Open
transmogrify-cell wants to merge 1 commit into
pothosware:masterfrom
transmogrify-cell:rpc-payload-eof
Open

transmogrify-cell wants to merge 1 commit into
pothosware:masterfrom
transmogrify-cell:rpc-payload-eof

Conversation

@transmogrify-cell

@transmogrify-cell transmogrify-cell commented Nov 9, 2026 •

Copy link
Copy Markdown

SoapyRPCUnpacker::recv() reads the payload in a loop that only treats a negative recv() result as an error:

ret = _sock.recv(_message+bytesReceived, toRecv);
if (ret < 0) throw ...;
bytesReceived += ret;

If the server closes the connection after sending the header but before the whole payload has arrived, recv() returns 0 on every call. bytesReceived never advances, and the client spins at 100% CPU forever. For device calls this happens while holding _mutex, so every other call on that device blocks too. This PR throws a runtime_error when recv() returns 0, as the header receive already does with MSG_WAITALL.

It only matters when the server goes away part-way through sending a reply, which in practice means a reply longer than one TCP segment. It is a hang rather than a crash, so it's rare but hard to diagnose. The change is independent of #112: it touches a different file, and that PR doesn't cover this path.

Reproducing

A fake server that sends a valid header announcing a 4096-byte packet, then 8 payload bytes, then FIN:

import socket, struct, sys, threading, time
ls = socket.socket(); ls.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ls.bind(("127.0.0.1", int(sys.argv[1]))); ls.listen(4)
def serve(c):
    c.recv(65536)
    c.sendall(struct.pack("!4sII", b"SRPC", 0x000400, 4096) + b"\0" * 8)
    c.shutdown(socket.SHUT_WR); time.sleep(30)
while True:
    threading.Thread(target=serve, args=(ls.accept()[0],), daemon=True).start()

The client side is a one-line program calling SoapySDR::Device::make("driver=remote,remote=tcp://127.0.0.1:PORT"):

  • master: never returns, at 100% CPU. A stack sample shows it in SoapyRPCUnpacker::recv() → SoapyRPCSocket::recv() (SoapyRPCUnpacker.cpp:107), reached via findRemote(). I killed it after 12 s.
  • this branch: fails immediately with SoapyRemote::find(tcp://127.0.0.1:…) -- transact FAIL: SoapyRPCUnpacker::recv(payload) FAIL: connection closed by peer, and make() throws.

Testing

  • Tested with SoapySDR 0.8.1 on macOS arm64 (Apple clang 17).
  • Regression check against a real SoapySDRServer serving a fake RX device, over the default UDP stream: 100 activate/deactivate cycles at 250 kS/s plus setFrequency. Behaviour was identical to master: no errors, and samples kept flowing.
  • No SDR hardware involved. The change is transport-only.

Disclosure: I found and tested this while looking into gqrx-sdr/gqrx#1445. That issue is fixed by #112, not by this PR.

SoapyRPCUnpacker::recv() reads the payload in a loop and only treats a
negative recv() result as an error. When the server closes the connection
after the header but before the whole payload has arrived, recv() keeps
returning 0, bytesReceived never advances and the loop spins forever at
full CPU, holding the device mutex. Every other call on that device then
blocks as well.

Throw a runtime_error when recv() returns 0, like the header receive
already does, so the caller sees the dead link instead of hanging.
@transmogrify-cell

transmogrify-cell commented Nov 10, 2026 •

Copy link
Copy Markdown
Author

@AsciiWolf could you take a look pls? It's a 6-line change in SoapyRPCUnpacker::recv(), independent of #112. The CI workflows are waiting for maintainer approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant