From c031f60599cfdf4816cef57ace42ddeaba6f7375 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Mon, 13 Jul 2026 05:14:41 +0200 Subject: [PATCH] Fix UdpSocket.recv-from truncating datagrams larger than 4096 bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/UdpSocket.html | 23 ++++++++++++++++++++- src/udp_socket.carp | 11 ++++++++-- src/udp_socket.h | 2 ++ test/udp_test.carp | 49 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 test/udp_test.carp diff --git a/docs/UdpSocket.html b/docs/UdpSocket.html index 96cf096..16466f3 100644 --- a/docs/UdpSocket.html +++ b/docs/UdpSocket.html @@ -122,6 +122,27 @@

+
+ +

+ local-port +

+
+
+ external +
+

+ (Fn [(Ref UdpSocket a)] Int) +

+ + + +

+

returns the port the socket is bound to. Useful +after binding to port 0 to discover the OS-assigned port.

+ +

+

@@ -157,7 +178,7 @@

(recv-from sock)

-

receives data and the sender's address. +

receives a datagram and the sender's address. Returns (Result (Pair (Array Byte) String) String) where the string is the sender's address in ip:port format.

diff --git a/src/udp_socket.carp b/src/udp_socket.carp index 87dcc34..e5885cf 100644 --- a/src/udp_socket.carp +++ b/src/udp_socket.carp @@ -47,11 +47,14 @@ Returns `(Result UdpSocket String)`.") (let [n (send-to- sock host port data)] (if (= n -1) (Result.Error (System.error-text)) (Result.Success n)))) - (doc recv-from "receives data and the sender's address. + (doc recv-from "receives a datagram and the sender's address. Returns `(Result (Pair (Array Byte) String) String)` where the string is the sender's address in `ip:port` format.") (defn recv-from [sock] - (let-do [buf (the (Array Byte) (Array.allocate 4096)) + ; A UDP datagram can carry up to 65535 bytes; size the buffer to the + ; maximum so a large datagram is received whole instead of truncated + ; (recvfrom silently discards whatever overflows the buffer). + (let-do [buf (the (Array Byte) (Array.allocate 65535)) sender @"" port 0 n (recv- sock &buf &sender &port)] @@ -65,6 +68,10 @@ the sender's address in `ip:port` format.") (doc set-timeout "sets read and write timeouts in seconds.") (register set-timeout (Fn [&UdpSocket Int] ())) + (doc local-port "returns the port the socket is bound to. Useful +after binding to port `0` to discover the OS-assigned port.") + (register local-port (Fn [&UdpSocket] Int)) + (register copy (Fn [&UdpSocket] UdpSocket)) (implements copy UdpSocket.copy) diff --git a/src/udp_socket.h b/src/udp_socket.h index c5a588e..edc3e3a 100644 --- a/src/udp_socket.h +++ b/src/udp_socket.h @@ -33,6 +33,8 @@ UdpSocket UdpSocket_bind_(String* host, int port) { int UdpSocket_fd_(UdpSocket* u) { return u->fd; } +int UdpSocket_local_MINUS_port(UdpSocket* u) { return sockaddr_port(&u->bound); } + int UdpSocket_send_MINUS_to_(UdpSocket* u, String* host, int port, Array* data) { struct sockaddr_storage dest; socklen_t dest_len; diff --git a/test/udp_test.carp b/test/udp_test.carp new file mode 100644 index 0000000..0e0003c --- /dev/null +++ b/test/udp_test.carp @@ -0,0 +1,49 @@ +(load "Test.carp") +(load "../socket.carp") +(use Test) + +; Sends `size` bytes (all 0x41) to a loopback UDP socket bound to an +; OS-assigned port and reports whether exactly that many bytes come +; back unchanged. This is the regression check for datagrams larger +; than the old fixed 4096-byte receive buffer, which recvfrom silently +; truncated. +(defn udp-echoes-intact? [size] + (match (UdpSocket.bind "127.0.0.1" 0) + (Result.Error _) false + (Result.Success sock) + (let-do [port (UdpSocket.local-port &sock) + payload (Array.replicate size &65b) + ok false] + (UdpSocket.set-timeout &sock 2) + (match (UdpSocket.send-to &sock "127.0.0.1" port &payload) + (Result.Success _) + (match (UdpSocket.recv-from &sock) + (Result.Success p) + (let [got (Pair.a &p)] + (set! ok + (and (= size (Array.length got)) + (= 65b @(Array.unsafe-nth got (- size 1)))))) + (Result.Error _) ()) + (Result.Error _) ()) + (UdpSocket.close sock) + ok))) + +(deftest test + (assert-true test + (match (UdpSocket.bind "127.0.0.1" 0) + (Result.Success sock) + (let-do [p (UdpSocket.local-port &sock)] (UdpSocket.close sock) (> p 0)) + (Result.Error _) false) + "local-port reports the OS-assigned port after binding to 0") + + (assert-true test + (udp-echoes-intact? 16) + "recv-from returns a small datagram intact") + + (assert-true test + (udp-echoes-intact? 5000) + "recv-from returns a 5000-byte datagram whole (was truncated to 4096)") + + (assert-true test + (udp-echoes-intact? 60000) + "recv-from returns a 60000-byte datagram whole"))