Fix UdpSocket.recv-from truncating datagrams larger than 4096 bytes - #12
Conversation
recv-from allocated a fixed 4096-byte receive buffer, and recvfrom fills at most buf->capacity before silently discarding the rest of the datagram. Any UDP datagram larger than 4096 bytes was therefore truncated with no error — silent data loss. Size the buffer to the maximum UDP payload (65535 bytes) so a datagram is received whole. Also add UdpSocket.local-port (mirroring TcpListener.local-port) so a socket bound to port 0 can report its OS-assigned port, and add the repo's first UDP test suite: a loopback round-trip that verifies small, over-4096, and 60000-byte datagrams all arrive intact.
There was a problem hiding this comment.
Build & Tests
Checked out and built locally (carp v0.6.0, armhf Linux). The full library compiles clean (carp -b test/tcp_test.carp OK), carp-fmt -c and angler are clean on both changed .carp files. The new test/udp_test.carp passes 4/4 locally. CI is green (ubuntu + macos).
I verified the fix is both real and necessary, and that the new test genuinely bites the old behavior:
- Traced the C path:
recv-callsbuf_ensure(buf, SOCK_BUF_SIZE)whereSOCK_BUF_SIZEis 4096 (common.h:16), andbuf_ensureonly grows (never shrinks). So the old 4096-byte Carp allocation was never enlarged, andrecvfromcapped atbuf->capacity= 4096 → silent truncation. The new 65535 allocation makesbuf_ensurea no-op andrecvfromreads the whole datagram;udp_socket.h:54setsbuf->len = (int)nso the returned array reports the true length. - To confirm the test isn't a no-op, I temporarily reverted the buffer to 4096 and reran: the 5000-byte and 60000-byte cases fail (truncated), while the 16-byte and
local-portcases pass. Restored to 65535 → all 4 pass. Solid regression coverage.
local-port is sound: it reads u->bound, which bind populates via getsockname (udp_socket.h:30), through the existing sockaddr_port helper — a faithful mirror of TcpListener.local-port. Name mangling and signatures check out.
Findings
No bugs found. Two non-blocking notes:
- The new suite is not yet wired into CI. As the PR description honestly states, the bot's App token can't modify
.github/workflows/, sotest/udp_test.carpisn't run by CI — the green checks only cover tcp/unix/poll. The two lines to add (carp -b/carp -x test/udp_test.carp) are given in the PR body; please add them at merge so the regression is guarded going forward. I ran the suite locally in the meantime. - 65535 bytes are allocated per
recv-fromcall regardless of datagram size. This is the correct, standard approach for datagram sockets (the size is unknown in advance andrecvfromtruncates), and matches the old code's pattern — noting it only for completeness, not as a concern.
Verdict: merge
Correct, minimal fix for a genuine silent-data-loss bug, with a real regression test and a useful local-port accessor. Only merge-time action is adding the two CI lines from the PR description.
Problem
UdpSocket.recv-fromallocated a fixed 4096-byte receive buffer:recvfromfills at mostbuf->capacitybytes and, for a datagram socket, silently discards whatever doesn't fit — the excess is lost, not left for the next read. So any UDP datagram larger than 4096 bytes was truncated with no error. UDP payloads routinely exceed that (up to ~65 KB), so this is silent data loss on a core operation.Reproduced on loopback before the fix: sending a 5000-byte datagram and reading it back yielded only 4096 bytes.
Fix
Size the receive buffer to the maximum UDP payload (65535 bytes) so a datagram is received whole. The C side already sets
buf->lento the actual byte count returned byrecvfrom, so the returned array still reports the true length — it just no longer caps at 4096. No C logic change is needed;recvfromalready reads up tobuf->capacity, which now tracks the larger allocation.Also included
UdpSocket.local-port— mirrors the existingTcpListener.local-port(a one-linesockaddr_port(&u->bound)accessor over the addressbindalready captures viagetsockname). This lets a socket bound to port0report its OS-assigned port, which the UDP suite below relies on to avoid a hard-coded port.test/udp_test.carp) — a loopback round-trip (bind to port 0,send-toself,recv-from) asserting that 16-byte, 5000-byte, and 60000-byte datagrams all arrive with their exact length and content. The two large cases fail against the old 4096-byte buffer and pass with the fix; I verified both directions.Verification
carp -x test/udp_test.carp→ 4/4 pass.carp-fmt -candanglerclean on the changed files;gendocs.carpregenerates (docs/UdpSocket.html updated forlocal-portand the rewordedrecv-from).Note on CI
The new suite should be wired into
.github/workflows/ci.ymlnext to the others:I left the workflow file out of this PR because the bot's GitHub App token isn't allowed to modify
.github/workflows/— please add those two lines when merging (or let me know and I'll provide a patch).