Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed in the completed Ubuntu 24.04 runtime-job log: this step executed successfully and reports both C and LLVM passing. Together with the earlier macOS checks, this validates the shared runtime and the zero-capacity regression on both supported host platforms.


# 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
Expand Down
2 changes: 1 addition & 1 deletion RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/stdlib-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `"<family> <port> <address>"` — `"inet 5000 127.0.0.1"`, `"inet6 5000 ::1"`, `"unix 0 <path>"` (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 |
Expand Down
55 changes: 54 additions & 1 deletion lib/codegen_c.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <limits.h>";
"/* 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);";

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verified: this resolves the previous zero-capacity finding on both native backends. Checking for missing sender metadata preserves successful zero-capacity receives of queued datagrams, while the original no-packet reproducer now returns an empty result with EAGAIN. The new regression also fails when this guard is removed.

" char* address = __sock_text((struct sockaddr*)&ss, sl);";
" if (!address[0]) return address;";
Comment on lines +8189 to +8196

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[Minor] Handle zero-capacity receives before reporting a datagram

cap == 0 passes validation, but on macOS a zero-capacity recvfrom can return n == 0 with sl == 0 even when no packet has been sent. The cleared sockaddr then becomes "other 0 ", and this helper reports "0 other 0 " with fd_last_errno() == 0. Callers therefore see a successful receive with invalid sender metadata rather than a timeout/error.

Reproduced on both C and LLVM with the existing extern declarations:

let fd = socket 2 2 0;
let _ = sock_bind fd "127.0.0.1" 0;
let _ = tcp_set_timeout fd 100;
let p = mem_alloc 1;
print (sock_recvfrom fd p 0 0)

No sender is needed. Changing the capacity to 1 correctly returns "" with EAGAIN after the timeout.

Please either reject zero receive capacity with EINVAL and document the positive-capacity requirement, or handle zero-capacity receives so that success requires an actual packet and valid sender metadata. Add a no-packet regression and retain coverage for genuine empty datagrams received with positive capacity.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7b101ac. sock_recvfrom now maps a zero-capacity result with no sender (sl == 0) to an empty result with EAGAIN, instead of constructing 0 other 0 . Zero capacity remains supported when the kernel actually supplies a packet and its sender.

Added IPv4/IPv6 regressions for no queued packet on timeout-configured and nonblocking sockets, plus queued empty and nonempty packets received at zero capacity. The latter checks verify sender identity, unchanged payload storage, and removal from the receive queue. Existing positive-capacity empty-datagram coverage remains in place.

The no-packet regression fails on the old implementation. The expanded gate passes on C and LLVM on macOS and C on Linux; the existing sockaddr gate passes all 116 checks.

" 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\"";
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci_gate_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
23 changes: 23 additions & 0 deletions scripts/datagram_check.sh
Original file line number Diff line number Diff line change
@@ -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
114 changes: 114 additions & 0 deletions test/sockaddr/datagram_probe.mere
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good regression coverage: sending to two destinations through the same bound socket and checking its binding and connected peer helps catch implementations that reconnect or replace the descriptor while adding addressed sends.

// 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"
Loading