Skip to content

Fix UdpSocket.recv-from truncating datagrams larger than 4096 bytes - #12

Merged
hellerve merged 1 commit into
masterfrom
claude/fix-udp-recv-truncation
Jul 13, 2026
Merged

Fix UdpSocket.recv-from truncating datagrams larger than 4096 bytes#12
hellerve merged 1 commit into
masterfrom
claude/fix-udp-recv-truncation

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Problem

UdpSocket.recv-from allocated a fixed 4096-byte receive buffer:

(let-do [buf (the (Array Byte) (Array.allocate 4096)) ...

recvfrom fills at most buf->capacity bytes 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->len to the actual byte count returned by recvfrom, so the returned array still reports the true length — it just no longer caps at 4096. No C logic change is needed; recvfrom already reads up to buf->capacity, which now tracks the larger allocation.

Also included

  • UdpSocket.local-port — mirrors the existing TcpListener.local-port (a one-line sockaddr_port(&u->bound) accessor over the address bind already captures via getsockname). This lets a socket bound to port 0 report its OS-assigned port, which the UDP suite below relies on to avoid a hard-coded port.
  • First UDP test suite (test/udp_test.carp) — a loopback round-trip (bind to port 0, send-to self, 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 -c and angler clean on the changed files; gendocs.carp regenerates (docs/UdpSocket.html updated for local-port and the reworded recv-from).

Note on CI

The new suite should be wired into .github/workflows/ci.yml next to the others:

carp -b test/udp_test.carp   # in "Build test targets"
carp -x test/udp_test.carp   # in "Run tests"

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).

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.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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- calls buf_ensure(buf, SOCK_BUF_SIZE) where SOCK_BUF_SIZE is 4096 (common.h:16), and buf_ensure only grows (never shrinks). So the old 4096-byte Carp allocation was never enlarged, and recvfrom capped at buf->capacity = 4096 → silent truncation. The new 65535 allocation makes buf_ensure a no-op and recvfrom reads the whole datagram; udp_socket.h:54 sets buf->len = (int)n so 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-port cases 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/, so test/udp_test.carp isn'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-from call regardless of datagram size. This is the correct, standard approach for datagram sockets (the size is unknown in advance and recvfrom truncates), 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.

@hellerve
hellerve merged commit 840a040 into master Jul 13, 2026
2 checks passed
@hellerve
hellerve deleted the claude/fix-udp-recv-truncation branch July 13, 2026 03:37
@carpentry-agent carpentry-agent Bot mentioned this pull request Jul 19, 2026
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