Sockets

rusty-sockets wraps blocking std::net TCP, UDP, and listener sockets and hands the JavaScript host opaque u64 handle ids instead of raw file descriptors. It caps how many handles can be open, enforces accept timeouts, and survives a poisoned registry lock. It is a std-based shim, not an epoll/kqueue reactor.

A network-exposed TCP listener has to survive more than the happy path: ten thousand connections arriving at once, a peer resetting mid-write, the accept loop hitting a permanent error. Those are the job, not edge cases. Cruft ships the sockets crate (rusty-sockets) where mio or tokio net would stand, and this page covers what it does under pressure.

Alpha (Cruft 0.0.10). This crate replaces the mio / tokio net dependency with a handle-based std::net shim. It has not been externally audited and has near-zero interop testing against real-world load. Do not rely on it in production or against adversarial input.

A handle registry over blocking std::net

The crate wraps blocking std::net::TcpListener / TcpStream / UdpSocket and hands the JS host opaque u64 handle ids instead of raw fds. It is not an epoll/kqueue reactor multiplexing thousands of sockets on a few threads. The host stores ids in a JS-side map; the crate keeps the OS socket alive in a global registry keyed by those ids. Passing ids instead of fds is deliberate: the OS reuses fd numbers, and a bare fd crossing the FFI boundary is a use-after-close waiting to happen.

There is one concession to async: listener_bind_async runs the accept loop on a background std::thread and streams accepted-connection events through an mpsc channel that the host drains with listener_poll. That is the std-only equivalent of a reactor, a background accept thread per listener, which does not multiplex many connections on a few threads.

The whole crate is zero unsafe and zero FFI: there is no unsafe token in the source. Everything is safe Rust over the standard library.

Connection flooding and fd growth

A listener that inserts a registry entry per accepted connection with no ceiling is a denial-of-service vector. That was the crate's most serious early problem: unbounded inserts, plus a per-I/O try_clone() that dup()d the fd on nearly every accept/read/write, transiently doubling fd consumption.

Both are bounded and enforced now:

  • The registry carries hard caps: MAX_HANDLES = 4096 and MAX_STREAM_HANDLES = 2048. put() is fallible and returns SocketError::ResourceLimit when a cap is hit; sync accept, nonblocking accept, connect, UDP bind, and async listener creation all route through it.
  • TCP handles are stored as Arc<TcpListener> / Arc<TcpStream>, so accept/read/write/try_read/set_nonblocking clone Rust ownership instead of dup()ing an OS fd on every call. The fd-doubling is gone.
  • The async accept loop reports cap backpressure without registering a new stream, and breaks after one fatal accept error instead of emitting an infinite 10ms error stream.

The stream-cap refusal path is tested without consuming thousands of OS fds.

Fixed contract bugs in the surface

Three surface bugs are fixed:

  • listener_set_accept_timeout(ms) used to discard ms and do nothing. Listener handles now carry an accept_timeout field, listener_set_accept_timeout mutates it, and listener_accept enforces it with a bounded nonblocking accept loop that restores blocking mode before returning.
  • The non-Unix raw-fd path overloaded WrongKind to also mean "unsupported platform". There is now a distinct SocketError::UnsupportedPlatform.
  • stream_raw_fd / listener_raw_fd used to claim the fd "stays valid until close", a contract nothing enforced (closing the handle lets the OS reuse the fd number, so an external reactor then operates on the wrong connection). The documented contract is now the true caller-owned one: unregister before handle_close, and do not assume raw-fd validity after close. The race lives in the consumer, not in memory safety.

The single global registry lock

Every socket call locks a single OnceLock<Mutex<Registry>>. Older code unwrapped with .expect("registry poisoned") at many sites, so one panic under the lock would poison the mutex and take down all networking in the process. Access now routes through lock_registry() and lock_rx(), which recover a poisoned mutex with into_inner() instead of panicking; a poisoned global registry still serves a normal listener_bind / handle_close. This recovers from poisoning; the single global lock stays. The lock is still one process-wide mutex, and its critical sections are what keep the design tolerable.

Test coverage of the error paths

Early tests were happy-path loopback only, with no UDP coverage. Coverage now includes nonblocking-read WouldBlock, UDP bind/send/try-recv loopback plus empty-recv WouldBlock and bad-address write error, UDP wrong-kind errors, and the enforced accept timeout. UDP is no longer entirely untested.

Still not covered, and therefore still unproven for robustness: peer-reset (RST) mid-I/O, partial-write looping under a full socket buffer, real fd-exhaustion / EMFILE soak, and any concurrency-race or stress test. These are follow-on hardening, not proven-good behavior. Loopback and a poisoned-lock unit test are not a flood.

Limitations

  • It is a blocking std::net shim. There is no epoll/kqueue multiplexing. Scaling is thread-per-listener (async) plus blocking I/O per handle. tokio / mio exist because that model does not scale to C10k, and sockets does not either.
  • The maturity gap versus mio / tokio net is enormous. Those crates carry years of production exposure, cross-platform readiness plumbing, and a battle-tested reactor. sockets is one small file at Cruft 0.0.10, its tests mostly loopback. The caps and contract fixes closed the release-blocking problems; they did not buy maturity.
  • The single global registry mutex remains a single global lock. Poisoning is now survived, but contention and the process-wide chokepoint are architectural, not fixed.
  • The raw-fd escape hatch is a genuine fd-reuse race in the consumer's hands. It is now documented accurately. That is progress, not a safety guarantee.
  • No adversarial, RST, partial-write, or fd-pressure soak testing exists. The tests cover the specific fixes; they do not prove the crate holds under real hostile load.