Repository navigation
std.ipc: two-way socket channel with descriptor passing - #3490
Merged
Merged
Conversation
Reshaped std.ipc to support bidirectional Unix domain stream socket channels and passing open file descriptors across process boundaries on Linux, FreeBSD, and macOS. - std.os: added os.socketpair() and os.run_socket() to spawn children with a connected AF_UNIX stream socket pair on fd 3. - std.ipc: added parent_channel_kind(), binary-safe read(), send_fd(), and recv_fd(), while preserving parent_channel(), write(), and write_close() for backward compatibility with pipe channels. - Enforced all safety rules: single descriptor per message with leak cleanup, receipt close-on-exec (MSG_CMSG_CLOEXEC and fcntl fallback), SIGPIPE resistance (MSG_NOSIGNAL and SO_NOSIGPIPE), short write loop with EINTR retries, refusal of empty payloads, and descriptive errors. - Capability gate: added std.ipc under the 'os' capability group for --emit=lib compilation. - Added comprehensive unit spec in std/ipc/test_ipc.ae and integration test suite in tests/integration/std_ipc_socket/. Co-authored-by: paul-hammant <82182+paul-hammant@users.noreply.github.com>
Reworks Jules's draft (paul-hammant#46): - Remove the test-only externs from the shipped library (two-descriptor sender, async sender thread, socket-buffer and cloexec probes). The hostile peer is now a C program the integration script builds, and close-on-exec is tested by whether a child inherits the descriptor. - aether_socketpair_cloexec moves to runtime/utils/aether_spawn.c and takes the fork lock where SOCK_CLOEXEC is missing (macOS), as aether_pipe_cloexec does. - MSG_NOSIGNAL falls back to 0 (macOS relies on SO_NOSIGPIPE); sockets are compiled out where there are no processes (WASI). - send_fd finishes a partial sendmsg rather than failing after the descriptor has gone; recv_fd aligns its control buffer, closes every descriptor of a refused message, names each refusal, and uses fcntl only where MSG_CMSG_CLOEXEC is missing. - Errors use a per-thread buffer and strerror_r. - std.ipc imports std.io rather than std.net, so --with=os alone admits it under --emit=lib; emit_lib_banned covers it. - Restore the comments the draft deleted (run_pipe's contract, the Windows rationale, the module docs) and extend them to sockets. - Spec rewritten on std.spec; runs on Windows too. New C test forces short and interrupted sends with signals, which a blocking 1 MB transfer never produces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS a message carrying more descriptors than recv_fd's control buffer held came back truncated (MSG_CTRUNC) with cmsg_len still claiming all of them, so recv_fd read descriptor numbers past the buffer and closed whatever the stack named: std_ipc_socket's ten-descriptor case closed the probe's own stdout and exited 1 with nothing printed. The kernel also installs every descriptor of a truncated message, so the two it hid stayed open. recv_fd now counts only the descriptors inside the control data it received, and makes room for 256, more than one message can carry (Linux's SCM_MAX_FD is 253; macOS refuses past 254), so nothing is truncated and every refused descriptor is closed. The hostile peer's ten descriptors are refused as more than one. aether_spawn.c includes <sys/socket.h> and makes a socket pair only where AETHER_HAS_NETWORKING: the embedded Cortex-M4 build has no sockets, and aether_socketpair_cloexec fails there with ENOSYS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
std.ipcgoes from a one-way child-to-parent pipe to a two-way socket channel that can also pass open file descriptors, on Linux, FreeBSD and macOS. It is the transport for running untrusted code in a locked-down child that cannot open files itself: the parent checks a request, opens the file, and passes the descriptor across. Message framing (CBOR) and the higher layers are later work.Jules drafted it on the fork (paul-hammant#46, merged here unchanged); the second commit reshapes it.
API
os.run_socket(prog, argv, env) -> (fd, pid, err): likerun_pipe, through the same spawn path, but the channel is one end of anAF_UNIXstream pair at fd 3, withAETHER_IPC_FD=3andAETHER_IPC_KIND=socket.os.socketpair() -> (a, b, err): a pair within one process. Both ends close-on-exec.ipc.parent_channel_kind():"pipe","socket"or"".ipc.read(ch, max): one binary-safe read;("", "")at end of stream.ipc.send_fd(ch, fd, payload)/ipc.recv_fd(ch, max).ipc.writenow writes every byte itself rather than throughstd.net.fd_write.parent_channel(),write_close,os.run_pipeandos.run_pipe_drain_and_waitare unchanged (skirae and aeb use them), andstd_ipc_roundtrip/std_ipc_bash_chainpass as they were.Under
--emit=lib,std.ipcnow sits under--with=os. Before, it was only rejected because it importedstd.net, so it needed--with=net. It now importsstd.ioforfd_close, soosis the only capability it needs.What the receiver can rely on, and the test that fails without each
MSG_CTRUNCcheckMSG_CMSG_CLOEXECSOCK_CLOEXECon the pairMSG_NOSIGNALipc_write_finishes_sends_signals_cut_short--emit=libgatestd.ipc→osentryemit_lib_banned:std.ipcacceptedRule 4 needed its own test. A blocking send never comes back short by itself, so the 1 MB transfer passes with or without the loop. The C test makes the kernel return early: a helper thread signals the sending thread while its 1 MB send is blocked on a stalled reader. That produces both a short return and EINTR.
Changes from Jules's draft
Test hooks removed from the shipped library. Four test-only externs were exported from
std.ipcand built intolibaether:test_ipc_send_two_fds,ipc_test_send_async_raw(a detached pthread),ipc_set_sock_buf_rawandipc_get_cloexec_raw. They are gone:Fork race.
aether_socketpair_cloexecmoved toruntime/utils/aether_spawn.c. On macOS (noSOCK_CLOEXEC) it now holds the fork lock acrosssocketpair+fcntl, asaether_pipe_cloexecdoes. The draft's fallback had no lock, so a concurrent fork could copy the pair into an unrelated child.macOS build.
MSG_NOSIGNALwas used unguarded. macOS doesn't define it; it now falls back to 0, andSO_NOSIGPIPE(set on both ends) covers macOS.WASI build. WASI has descriptors but no sockets, and the draft's code didn't compile there (it broke
wasi_panic_link,wasm_emit_lib_exports,size_modeandemit_csrc_cross). Without processes,writeandreadare plain descriptor I/O andsend_fd/recv_fdreport that they are unavailable.send_fdpartial send. The draft returned an error whensendmsgsent only part of the payload. The descriptor has already gone with the first byte by then, so the rest is now sent as plain bytes.recv_fd.fcntlfallback runs only whereMSG_CMSG_CLOEXECis missing, so the Linux test of rule 2 can fail.Errors. Errors used a shared
static charbuffer andstrerror. They now use a per-thread buffer andstrerror_r, named "<call>: <reason>".os.socketpair's error was a barestrerror.ipc.write. It triessendonce per call and switches towriteonENOTSOCK; the draft made two syscalls per chunk on a pipe.Windows.
ipc.writeandipc.readremain plain CRT descriptor I/O there; the draft madewriteunsupported.Deleted comments restored. The draft removed a lot of documentation:
os_run_pipe_rawcontract and its deadlock note;ipc_parent_channel_raw;send-is-reserved naming note and the usage example inmodule.ae;They are back, extended to cover sockets.
Spec. The spec now uses
std.specinstead ofexit(1)asserts. It runs on Windows too, checking that each socket entry point reports itself unsupported. The draft's "length preserved" check compared two strings that would both have been truncated; the bytes are now checked past the zero byte.Checked
std_ipc_roundtrip,std_ipc_bash_chain,test_os_run_supervised,test_wait_status_signalledandtest_fd_read_intopass.make test-ae: 2060 passed. The 6 failures were the four WASI tests above (fixed and rerun) pluswindows_crt_symbolsandhttp_server_h2's 50-stream curl stress, which fail the same way on main on this machine.ae fmt,check-docs,check-changelog,check-tests,check-warnings,check-raw-calls,check-extern-widths,check-standalone,check-examples,check-crypto-wipe,check-archive-exports-nozlib,make docs,make test.Not checked locally: macOS and FreeBSD (CI covers macOS). A control message of another kind (rule 1's third case) has no test. Linux only delivers credentials to a receiver that has set
SO_PASSCRED, and the one portable sender for it is FreeBSD'sSCM_CREDS.🤖 Generated with Claude Code