diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25106f1f..c9b5c3d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1057,6 +1057,9 @@ jobs: - name: sockaddr_check (tcp_listen_at, sock_bind, getsockname, getpeername, fd_last_errno) if: ${{ !cancelled() && matrix.group == 'runtime' }} run: sh scripts/sockaddr_check.sh + - name: datagram_check (addressed UDP on C and LLVM) + if: ${{ !cancelled() && matrix.group == 'runtime' }} + run: sh scripts/datagram_check.sh # v0.1.677: the system log. syslog(3) takes varargs and openlog(3) keeps # the ident pointer, so the runtime makes both calls; the message goes diff --git a/RUNBOOK.md b/RUNBOOK.md index 53d6906e..84d24a7c 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -262,7 +262,7 @@ This covers direct source entries and the runner's `apt-mirrors.txt` or Repository suites, components, signing keys and other mirrors are preserved. The setup action's opam cache remains enabled. Compare actual setup step durations and cache-hit messages when measuring changes: a cache hit does not -avoid the action's system-package installation. All 187 gates and all four +avoid the action's system-package installation. All 188 gates and all four build configurations remain required. ## Binary releases diff --git a/docs/stdlib-reference.md b/docs/stdlib-reference.md index cb1e5c9e..e9bce11c 100644 --- a/docs/stdlib-reference.md +++ b/docs/stdlib-reference.md @@ -264,6 +264,8 @@ and comes out as the text `getnameinfo(3)` writes, and the family comes out as a | `sock_getopt` | `int -> int -> int -> str` | `fd level name`: `getsockopt(2)` (v0.1.678). The numbers are the platform's (`SOL_SOCKET` is `0xffff` on macOS and `1` on Linux; ask `sys_os`). The option's bytes as lowercase hex, up to 256 bytes — an int is `01000000` on both hosts, and a flag reads back as nonzero (`1` on Linux, the option's own bit on macOS). `""` with errno kept | | `sock_setopt` | `int -> int -> int -> str -> int` | `fd level name hex`: `setsockopt(2)` with those bytes. `0`, or `-1` with errno kept (`EINVAL` for hex that is not whole bytes). RISC-V: memu translates the guest's Linux numbers to a macOS host's | | `sock_send` | `int -> int -> int -> int -> int` | `fd p len flags`: `send(2)` of the arena's bytes with the platform's `MSG_*` flags (v0.1.682; `MSG_OOB` is `1` on both hosts). The count, or `-1` with errno kept. RISC-V: memu translates the guest's Linux `MSG_EOR`, `MSG_DONTWAIT` and `MSG_NOSIGNAL` for a macOS host | +| `sock_sendto` | `int -> int -> int -> int -> str -> int -> int` | `fd p len flags host port`: send one datagram from the byte arena to a numeric IPv4/IPv6 address in the socket's family, preserving its binding and connected peer. Count, `-1` with errno, or `-2` with errno zero for an invalid numeric destination. C and LLVM only | +| `sock_recvfrom` | `int -> int -> int -> int -> str` | `fd p cap flags`: receive one datagram into the arena and return `"count family port host"`, using the same numeric address format as `sock_local_addr`. `""` with errno on failure; empty datagrams still carry a sender. Zero capacity is allowed, but a host result without sender metadata is reported as `""` with `EAGAIN`, not a successful packet. The count is the syscall result; with `MSG_TRUNC` it may exceed `cap`, so read at most `min(count, cap)` arena bytes. C and LLVM only | | `fd_except_ready` | `int -> int` | Is an exceptional condition — TCP's out-of-band byte — pending on `fd` now (v0.1.682)? `select(2)`'s third set with no wait: `1`, `0`, or `-1` with errno kept (`EINVAL` past `FD_SETSIZE`). Not `poll`: Darwin's does not report out-of-band data as `POLLPRI`. Darwin also calls a device such as `/dev/null` exceptional | | `sock_local_addr` | `int -> str` | `getsockname(2)` as `"
"` — `"inet 5000 127.0.0.1"`, `"inet6 5000 ::1"`, `"unix 0 "` (the path last: it may hold spaces), `"other 0 "`. `""` on failure | | `sock_peer_addr` | `int -> str` | `getpeername(2)`, the same text; `""` with `ENOTCONN` for a socket with no peer | diff --git a/lib/codegen_c.ml b/lib/codegen_c.ml index 47decb1a..a414b473 100644 --- a/lib/codegen_c.ml +++ b/lib/codegen_c.ml @@ -7228,7 +7228,7 @@ let native_ffi_names = length; the option's bytes cross as hex (an int option is 01000000) *) "sock_getopt"; "sock_setopt"; (* v0.1.682: send(2) with its flags (MSG_OOB), the bytes from the arena *) - "sock_send"; + "sock_send"; "sock_sendto"; "sock_recvfrom"; (* v0.1.550: resource limits, scheduling priority and advisory file locks. Each is out of an `extern`'s reach for its own reason: getrlimit and setrlimit move the limits through a `struct rlimit *`; getpriority and @@ -8149,6 +8149,59 @@ let native_ffi_runtime ~tls ~midi ~window ~audio ~filestat ~fdio ~proclimit = " if (getpeername((int)fd, (struct sockaddr*)&ss, &sl) != 0) return __sock_none(errno);"; " return __sock_text((struct sockaddr*)&ss, sl);"; "}"; + "#include "; + "/* Addressed datagrams keep sockaddr layout in the host runtime. Addresses"; + " are numeric; name/service resolution belongs to the caller. The descriptor"; + " is never replaced or connected, so its binding and peer remain intact. */"; + "static long long sock_sendto(long long fd, long long p, long long len, long long flags, const char* host, long long port) {"; + " if (fd < 0 || fd > INT_MAX) { __fd_errno = EBADF; return -1; }"; + " if (p < 0 || p > __MEM_CAP || len < 0 || len > __MEM_CAP - p ||"; + " flags < INT_MIN || flags > INT_MAX || port < 0 || port > 65535) {"; + " __fd_errno = EINVAL; return -1;"; + " }"; + " struct sockaddr_storage ss; socklen_t sl = sizeof ss;"; + " memset(&ss, 0, sizeof ss);"; + " if (getsockname((int)fd, (struct sockaddr*)&ss, &sl) != 0) { __fd_errno = errno; return -1; }"; + " struct addrinfo hints, *res; char service[16];"; + " memset(&hints, 0, sizeof hints);"; + " hints.ai_family = ss.ss_family; hints.ai_socktype = SOCK_DGRAM;"; + " hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;"; + " snprintf(service, sizeof service, \"%lld\", port);"; + " if (!host || !host[0] || getaddrinfo(host, service, &hints, &res) != 0) {"; + " __fd_errno = 0; return -2;"; + " }"; + " ssize_t n = sendto((int)fd, __mem + p, (size_t)len, (int)flags, res->ai_addr, res->ai_addrlen);"; + " int err = errno;"; + " freeaddrinfo(res);"; + " __fd_errno = n < 0 ? err : 0;"; + " return n < 0 ? -1 : (long long)n;"; + "}"; + "/* Payload goes to the caller's arena. The result is \"count family port host\";"; + " an empty string means failure (fd_last_errno). Zero-byte packets still"; + " return an address. MSG_TRUNC may report more than cap: only min(count, cap)"; + " bytes in the arena are valid. No process-global last-sender slot is used. */"; + "static char* sock_recvfrom(long long fd, long long p, long long cap, long long flags) {"; + " if (fd < 0 || fd > INT_MAX) return __sock_none(EBADF);"; + " if (p < 0 || p > __MEM_CAP || cap < 0 || cap > __MEM_CAP - p ||"; + " flags < INT_MIN || flags > INT_MAX) return __sock_none(EINVAL);"; + " struct sockaddr_storage ss; socklen_t sl = sizeof ss;"; + " memset(&ss, 0, sizeof ss);"; + " ssize_t n = recvfrom((int)fd, __mem + p, (size_t)cap, (int)flags, (struct sockaddr*)&ss, &sl);"; + " if (n < 0) return __sock_none(errno);"; + " /* Darwin may return zero bytes and no sender for a zero-capacity"; + " receive with no queued datagram. Do not fabricate a successful"; + " packet; match the would-block/timeout result of a nonzero receive. */"; + " if (cap == 0 && sl == 0) return __sock_none(EAGAIN);"; + " char* address = __sock_text((struct sockaddr*)&ss, sl);"; + " if (!address[0]) return address;"; + " char out[1280];"; + " int len = snprintf(out, sizeof out, \"%lld %s\", (long long)n, address);"; + " if (len < 0 || (size_t)len >= sizeof out) return __sock_none(EOVERFLOW);"; + " char* result = __lang_str_alloc(__lang_current_region, (size_t)len);"; + " memcpy(result, out, (size_t)len + 1);"; + " __fd_errno = 0;"; + " return result;"; + "}"; "/* tcp_accept: the connected client fd, or a coded failure -- the same"; " codes tcp_read has used since v0.1.226 (-1 would-block, -2 gone,"; " -3 other), so a nonblocking accept can tell \"no one is waiting\""; diff --git a/scripts/ci_gate_check.rb b/scripts/ci_gate_check.rb index 88721716..e4b702da 100644 --- a/scripts/ci_gate_check.rb +++ b/scripts/ci_gate_check.rb @@ -10,7 +10,7 @@ class Invalid < StandardError; end # Update deliberately when adding or removing checks. A deleted step must # not silently reduce the coverage while every remaining group is green. - EXPECTED_CHECKS = 187 + EXPECTED_CHECKS = 188 PREFLIGHT = "tool_preflight (a gate that did not run is not a gate that passed)" FULL_TESTS = "Run tests (full, with the wasm toolchain present)" CHECKOUT = "Check out the dogfood repositories" diff --git a/scripts/datagram_check.sh b/scripts/datagram_check.sh new file mode 100644 index 00000000..4ed908c3 --- /dev/null +++ b/scripts/datagram_check.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Addressed UDP, flags, source metadata, bounds and errno on both native backends. +set -eu +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MERE="$ROOT/_build/default/bin/mere.exe" +CC="${CC:-clang}" +T=$(mktemp -d) +trap 'rm -rf "$T"' EXIT +P="$ROOT/test/sockaddr/datagram_probe.mere" +"$MERE" -c "$P" > "$T/probe.c" +"$CC" -O2 -w -pthread "$T/probe.c" -o "$T/c" -lm +"$MERE" -ll "$P" > "$T/probe.ll" +"$MERE" --ffi-c "$P" > "$T/ffi.c" +"$CC" -O2 -w -pthread "$T/probe.ll" "$T/ffi.c" -o "$T/llvm" -lm +printf '%s\n' 'datagrams: IPv4 and IPv6 passed' > "$T/expected" +for backend in c llvm; do + sh "$ROOT/scripts/bounded.sh" 15 "$T/$backend" > "$T/$backend.out" 2>&1 || { + cat "$T/$backend.out" + exit 1 + } + diff -u "$T/expected" "$T/$backend.out" + echo "datagrams: $backend passed" +done diff --git a/test/sockaddr/datagram_probe.mere b/test/sockaddr/datagram_probe.mere new file mode 100644 index 00000000..83c063f9 --- /dev/null +++ b/test/sockaddr/datagram_probe.mere @@ -0,0 +1,114 @@ +// Real loopback packets, including two destinations on one bound socket. +// All ports come from the kernel. No public network or DNS service is used. +extern fn socket: int -> int -> int -> int; +extern fn sock_bind: int -> str -> int -> int; +extern fn sock_local_addr: int -> str; +extern fn sock_peer_addr: int -> str; +extern fn sock_sendto: int -> int -> int -> int -> str -> int -> int; +extern fn sock_recvfrom: int -> int -> int -> int -> str; +extern fn udp_open: str -> int -> int; +extern fn udp_send: int -> int -> int -> int; +extern fn tcp_close: int -> int; +extern fn tcp_set_timeout: int -> int -> int; +extern fn io_set_nonblocking: int -> int; +extern fn fd_last_errno: unit -> int; +extern fn io_poll_new: int -> int; +extern fn io_poll_add: int -> int -> int -> int; +extern fn io_poll_wait: int -> int -> int; +extern fn io_poll_del: int -> int -> int; +extern fn mem_alloc: int -> int; +extern fn mem_set_u8: int -> int -> int -> int; +extern fn mem_to_str: int -> int -> str; + +let check = fn (label: str) -> fn (ok: bool) -> + if ok then () else (let _ = print ("FAIL " ++ label) in exit 1); +let rec nth = fn (xs: str list) -> fn (i: int) -> + match xs with Nil -> "" | Cons (v, rest) -> if i == 0 then v else nth rest (i - 1); +let port = fn (s: str) -> int_of_str (nth (str_split s " ") 1); +let packet = fn (n: int) -> fn (addr: str) -> str_of_int n ++ " " ++ addr; + +let run = fn (family: int) -> fn (host: str) -> + let a = socket family 2 0 in + let b = socket family 2 0 in + let c = socket family 2 0 in + let _ = check "open" (a >= 0 && b >= 0 && c >= 0) in + let _ = check "bind" (sock_bind a host 0 == 0 && sock_bind b host 0 == 0 && sock_bind c host 0 == 0) in + let _ = tcp_set_timeout a 1000 in + let _ = tcp_set_timeout b 1000 in + let _ = tcp_set_timeout c 1000 in + let aa = sock_local_addr a in + let ba = sock_local_addr b in + let ca = sock_local_addr c in + let p = mem_alloc 16 in + let q = mem_alloc 16 in + // Darwin can report a successful zero-capacity recvfrom without a packet. + // A timeout/error must stay distinguishable from a genuine empty datagram. + let _ = tcp_set_timeout b 100 in + let _ = check "no packet at zero capacity" (str_eq (sock_recvfrom b q 0 0) "" && fd_last_errno () > 0) in + let _ = check "no packet at positive capacity" (str_eq (sock_recvfrom b q 1 0) "" && fd_last_errno () > 0) in + let _ = tcp_set_timeout b 1000 in + let poll = io_poll_new 0 in + let _ = io_poll_add poll b 1 in + let _ = mem_set_u8 p 0 65 in + let _ = mem_set_u8 p 1 0 in + let _ = mem_set_u8 p 2 255 in + let data = mem_to_str p 3 in + let _ = check "send binary" (sock_sendto a p 3 0 host (port ba) == 3 && fd_last_errno () == 0) in + let _ = check "peek sender" (str_eq (sock_recvfrom b q 16 2) (packet 3 aa)) in + let _ = check "peek binary" (str_eq (mem_to_str q 3) data) in + let _ = check "receive after peek" (str_eq (sock_recvfrom b q 16 0) (packet 3 aa)) in + let _ = check "receive binary" (str_eq (mem_to_str q 3) data && fd_last_errno () == 0) in + let _ = check "second destination" (sock_sendto a p 0 0 host (port ca) == 0) in + let _ = check "empty packet has sender" (str_eq (sock_recvfrom c q 16 0) (packet 0 aa)) in + // A queued packet may still be consumed with zero capacity, with a valid + // sender and without touching payload storage. Poll removes arrival races. + let _ = mem_set_u8 q 0 90 in + let _ = check "send for zero capacity" (sock_sendto a p 3 0 host (port ba) == 3) in + let _ = check "zero-capacity packet ready" (io_poll_wait poll 1000 > 0) in + let _ = check "zero-capacity sender" (str_eq (sock_recvfrom b q 0 0) (packet 0 aa) && fd_last_errno () == 0) in + let _ = check "zero capacity preserves storage" (str_eq (mem_to_str q 1) "Z") in + let _ = check "zero capacity consumes packet" (io_poll_wait poll 0 == 0) in + let _ = check "send empty for zero capacity" (sock_sendto a p 0 0 host (port ba) == 0) in + let _ = check "empty zero-capacity packet ready" (io_poll_wait poll 1000 > 0) in + let _ = check "empty zero-capacity sender" (str_eq (sock_recvfrom b q 0 0) (packet 0 aa) && fd_last_errno () == 0) in + let _ = check "empty zero-capacity packet consumed" (io_poll_wait poll 0 == 0) in + let _ = io_poll_del poll b in + let _ = check "binding unchanged" (str_eq (sock_local_addr a) aa) in + let _ = check "no connected peer" (str_eq (sock_peer_addr a) "" && fd_last_errno () > 0) in + let _ = check "send before truncation" (sock_sendto a p 3 0 host (port ba) == 3) in + let _ = check "truncate packet" (str_eq (sock_recvfrom b q 1 0) (packet 1 aa)) in + let _ = check "truncated byte" (str_eq (mem_to_str q 1) "A") in + let _ = check "nonblocking" (io_set_nonblocking b == 0) in + let _ = check "packet remainder discarded" (str_eq (sock_recvfrom b q 16 0) "" && fd_last_errno () > 0) in + let would_block = fd_last_errno () in + let _ = check "nonblocking zero capacity without packet" (str_eq (sock_recvfrom b q 0 0) "" && fd_last_errno () == would_block) in + let _ = check "hostname refused without lookup" (sock_sendto a p 1 0 "localhost" (port ba) == 0 - 2 && fd_last_errno () == 0) in + let _ = check "bad port" (sock_sendto a p 1 0 host 65536 == 0 - 1 && fd_last_errno () > 0) in + let _ = check "negative buffer" (sock_sendto a (0 - 1) 1 0 host (port ba) == 0 - 1 && fd_last_errno () > 0) in + let _ = check "oversized buffer" (sock_sendto a p 16777216 0 host (port ba) == 0 - 1 && fd_last_errno () > 0) in + let _ = check "negative receive capacity" (str_eq (sock_recvfrom b q (0 - 1) 0) "" && fd_last_errno () > 0) in + let _ = check "receive beyond arena" (str_eq (sock_recvfrom b 16777216 1 0) "" && fd_last_errno () > 0) in + let d = udp_open host (port ca) in + let _ = check "connect" (d >= 0) in + let da = sock_local_addr d in + let peer = sock_peer_addr d in + let sent = sock_sendto d p 3 0 host (port ba) in + // Darwin refuses sendto on a connected datagram socket; Linux permits it. + let _ = (if str_eq (sys_os ()) "darwin" then + check "connected send refused" (sent == 0 - 1 && fd_last_errno () > 0) + else (let _ = check "connected send to alternate" (sent == 3) in + check "alternate source" (str_eq (sock_recvfrom b q 16 0) (packet 3 da)))) in + let _ = check "connected peer unchanged" (str_eq (sock_peer_addr d) peer && str_eq (sock_local_addr d) da) in + let _ = check "connected default destination" (udp_send d p 3 == 3) in + let _ = check "default source" (str_eq (sock_recvfrom c q 16 0) (packet 3 da)) in + let _ = tcp_close d in + let _ = tcp_close a in + let _ = check "closed send" (sock_sendto a p 1 0 host (port ba) == 0 - 1 && fd_last_errno () > 0) in + let _ = check "closed receive" (str_eq (sock_recvfrom a q 16 0) "" && fd_last_errno () > 0) in + let _ = tcp_close b in + let _ = tcp_close c in + (); + +let _ = run 2 "127.0.0.1"; +let _ = run (if str_eq (sys_os ()) "darwin" then 30 else 10) "::1"; +print "datagrams: IPv4 and IPv6 passed"