Skip to content

Deliver loopback UDP datagrams across processes on the same machine#855

Open
brandonpayton wants to merge 1 commit into
mainfrom
fix/cross-process-loopback-udp
Open

Deliver loopback UDP datagrams across processes on the same machine#855
brandonpayton wants to merge 1 commit into
mainfrom
fix/cross-process-loopback-udp

Conversation

@brandonpayton

Copy link
Copy Markdown
Member

Contract: POSIX loopback is machine-scoped, not process-scoped

sendto(127.0.0.1:port) from one process must reach a socket bound to that port in another process on the same machine. Kandelo delivered loopback datagrams only within the sender's own process — udp_send_datagram holds a single &mut Process and skipped every endpoint whose pid != proc.pid. So cross-process loopback UDP was silently dropped: unconnected sendto returned success with the datagram lost, and connected sockets got a spurious ECONNREFUSED.

TCP already handled the analogous case (cross_process_loopback_connect, invoked from kernel_connect — this is how nginx→PHP-FPM works over loopback). UDP had no equivalent.

Fix

Add deliver_cross_process_loopback_udp (in syscalls.rs, table-agnostic and unit-testable), invoked from the kernel_sendto / kernel_send / kernel_sendmsg dispatch entry points after the same-process attempt. The dispatch layer owns the global process table, mirroring the existing TCP cross-process pattern; syscalls.rs stays table-agnostic.

  • Only engages for AF_INET DGRAM sockets whose effective destination is 127.x. Non-loopback (virtual-network 10.88.x) and same-process delivery are untouched.
  • A loopback UDP port binds in exactly one process, and endpoints owned by the sender are skipped, so this never double-delivers a datagram already queued same-process.
  • An unconnected sender auto-binds to INADDR_ANY; its source is reported as 127.0.0.1 so the receiver's recvfrom sees a loopback source, matching real semantics.

ABI

No change — no new kernel exports/imports or repr(C) types. scripts/check-abi-version.sh reports the snapshot in sync and ABI_VERSION/snapshot consistent.

Validation

  • cargo test -p kandelo --target aarch64-apple-darwin --lib964 pass, including new test_udp_loopback_cross_process, which binds a UDP socket in process B, delivers from process A, and reads it back via real sys_socket/sys_bind/sys_recvfrom across two Processes (asserting payload + loopback source addr/port).
  • scripts/check-abi-version.sh — snapshot in sync.
  • Same-process loopback UDP (test_udp_loopback) and non-loopback paths unchanged (helper is a no-op for them).

Follow-up (not in this PR): a host-level two-process integration test would exercise the guest sendto() syscall end-to-end; the dispatch wiring here mirrors the already-proven TCP cross_process_loopback_connect.

🤖 Generated with Claude Code

POSIX loopback is machine-scoped: `sendto(127.0.0.1:port)` from one process
must reach a socket bound to that port in another process on the same machine.
Kandelo delivered loopback datagrams only within the sender's own process
(`udp_send_datagram` holds a single `&mut Process` and skipped every endpoint
whose `pid != proc.pid`), so cross-process loopback UDP was silently dropped —
unconnected `sendto` returned success with the datagram lost, and connected
sockets got a spurious `ECONNREFUSED`. TCP already handled the analogous case
via `cross_process_loopback_connect`; UDP had no equivalent.

Add `deliver_cross_process_loopback_udp`, invoked from the `kernel_sendto`,
`kernel_send`, and `kernel_sendmsg` dispatch entry points after the
same-process attempt (the dispatch layer owns the global process table, mirroring
`cross_process_loopback_connect`). A loopback UDP port binds in exactly one
process, and endpoints owned by the sender are skipped, so this never
double-delivers a datagram already queued same-process.

No ABI change: no new kernel exports/imports or repr(C) types; snapshot
unchanged.

Validation: `cargo test -p kandelo --lib` (964 pass, incl. new
`test_udp_loopback_cross_process` exercising bind/deliver/recvfrom across two
processes); `scripts/check-abi-version.sh` reports snapshot in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brandonpayton

Copy link
Copy Markdown
Member Author

ABI 39 rebase review found correctness gaps that should be addressed before reusing this implementation:

  • The second routing pass runs after the existing process-local connected send has already cached ECONNREFUSED. A first datagram can be delivered cross-process while leaving a spurious SO_ERROR; the next connected send then fails. Destination selection, delivery, and connected-error state need one authoritative operation.
  • The uniqueness assumption is false with SO_REUSEADDR. The helper records owner PIDs and re-looks up a matching socket, so multiple bindings in one process can select/deliver to the first endpoint repeatedly instead of exactly once per eligible endpoint.
  • Routing should use the canonical effective destination produced by socket state, not a second raw-address interpretation in the dispatch layer.
  • The current unit test constructs two Process values directly; it does not prove two guest workers, blocked recv/poll wakeup, exit/rebind lifecycle, connected-twice/SO_ERROR, no-target behavior, or reuse-address delivery.

The original ABI 39 failure still reproduces exactly: server pid 100 bound 0.0.0.0:24123; client pid 101 sent to 127.0.0.1, retried 25 times, then timed out while the server remained blocked. The platform owner is still the machine-wide binding/process table, but this head should be rebased and redesigned rather than merged as-is. This PR also does not by itself enable util-linux logger: ABI 39 kernel_sendmsg consumes only iov[0], while logger sends header + body as multiple iovecs.

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