Skip to content
Open
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
70 changes: 66 additions & 4 deletions crates/kernel/src/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7425,7 +7425,8 @@ pub fn sys_connect(
// until the TCP handshake either completes or errors. EAGAIN
// surfaces while still in flight.
let net_handle = sock_idx as i32;
if sock.state != SocketState::Connecting {
let was_connecting = sock.state == SocketState::Connecting;
if !was_connecting {
host.host_net_connect(net_handle, &ip, port)?;
let sock = proc.sockets.get_mut(sock_idx).ok_or(Errno::EBADF)?;
sock.state = SocketState::Connecting;
Expand All @@ -7437,7 +7438,18 @@ pub fn sys_connect(
sock.state = SocketState::Connected;
Ok(())
}
Err(Errno::EAGAIN) => Err(Errno::EAGAIN),
// Handshake still in flight. POSIX reports EINPROGRESS on the
// first (non-completing) call and EALREADY on subsequent
// calls while connecting — never EAGAIN. The host layer maps
// these to a blocking retry for blocking sockets and returns
// them to userspace for non-blocking sockets.
Err(Errno::EAGAIN) => {
if was_connecting {
Err(Errno::EALREADY)
} else {
Err(Errno::EINPROGRESS)
}
}
Err(e) => {
let sock = proc.sockets.get_mut(sock_idx).ok_or(Errno::EBADF)?;
sock.state = SocketState::Closed;
Expand Down Expand Up @@ -11086,6 +11098,11 @@ mod tests {
/// Override for `gl_submit`'s return value (0 = success, negative
/// = errno). Defaults to 0.
gl_submit_rc: i32,
/// When true, model an external TCP connect whose handshake is still in
/// flight: `host_net_connect` succeeds (kickoff) and
/// `host_net_connect_status` reports EAGAIN. Defaults to false, which
/// keeps the historical "connect refused" behavior.
net_connect_in_flight: bool,
}

impl MockHostIO {
Expand All @@ -11109,6 +11126,7 @@ mod tests {
gl_unbind_calls: Vec::new(),
gbm_bo_bind_rc: 0,
gl_submit_rc: 0,
net_connect_in_flight: false,
}
}

Expand Down Expand Up @@ -11469,10 +11487,18 @@ mod tests {
_addr: &[u8],
_port: u16,
) -> Result<(), Errno> {
Err(Errno::ECONNREFUSED)
if self.net_connect_in_flight {
Ok(())
} else {
Err(Errno::ECONNREFUSED)
}
}
fn host_net_connect_status(&mut self, _handle: i32) -> Result<(), Errno> {
Err(Errno::ECONNREFUSED)
if self.net_connect_in_flight {
Err(Errno::EAGAIN)
} else {
Err(Errno::ECONNREFUSED)
}
}
fn host_net_send(
&mut self,
Expand Down Expand Up @@ -17657,6 +17683,42 @@ mod tests {
assert_eq!(from_addr[0], 2); // AF_INET
}

#[test]
fn test_nonblocking_connect_reports_einprogress_then_ealready() {
// A non-blocking external TCP connect whose handshake cannot complete
// synchronously must report EINPROGRESS on the first call and EALREADY
// while still in flight — never EAGAIN (which POSIX/Linux do not use for
// connect). Async clients gate on `errno == EINPROGRESS`.
use wasm_posix_shared::socket::*;
let mut proc = Process::new(1);
let mut host = MockHostIO::new();
host.net_connect_in_flight = true; // handshake never completes

let fd = sys_socket(&mut proc, &mut host, AF_INET, SOCK_STREAM, 0).unwrap();

// sockaddr_in for a non-loopback, non-virtual destination (203.0.113.5:80)
// so connect takes the external host-backed path.
let mut addr = [0u8; 16];
addr[0] = 2; // AF_INET
let port_be = 80u16.to_be_bytes();
addr[2] = port_be[0];
addr[3] = port_be[1];
addr[4] = 203;
addr[5] = 0;
addr[6] = 113;
addr[7] = 5;

let first = sys_connect(&mut proc, &mut host, fd, &addr).unwrap_err();
assert_eq!(first, Errno::EINPROGRESS, "first connect should be EINPROGRESS");

let second = sys_connect(&mut proc, &mut host, fd, &addr).unwrap_err();
assert_eq!(
second,
Errno::EALREADY,
"in-flight connect should be EALREADY, not EAGAIN"
);
}

// ── Threading tests ──────────────────────────────────────────────

#[test]
Expand Down
2 changes: 1 addition & 1 deletion docs/posix-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ shortcuts.
| `bind()` | Partial | AF_UNIX paths, AF_INET TCP host-backed bind/listen, and AF_INET UDP in-kernel bind for INADDR_ANY, loopback, and broadcast addresses. The browser local virtual-network backend supports AF_INET TCP/UDP binds between attached Kandelo machines. UDP ephemeral ports, getsockname, bind conflicts, and SO_REUSEADDR conflict cases are covered by Sortix UDP tests. |
| `listen()` | Partial | AF_INET TCP delegates to the active HostIO networking backend, including Node `net` and the browser local virtual-network backend. Datagram listen rejects as unsupported. AF_UNIX stream listen remains EOPNOTSUPP. |
| `accept()` / `accept4()` | Partial | AF_INET TCP delegates to the active HostIO networking backend and returns connected sockets. Datagram accept rejects as unsupported. SOCK_NONBLOCK and SOCK_CLOEXEC flags on accept4. |
| `connect()` | Partial | AF_UNIX SOCK_DGRAM connects to a bit-bucket pattern. AF_INET TCP is host-backed and works over Node external TCP or the browser local virtual-network backend. AF_INET UDP connect stores the peer, auto-binds an ephemeral local port when needed, filters receives to the connected peer, and supports AF_UNSPEC unconnect. UDP loopback and local virtual routes are supported; external raw UDP routes return ENETUNREACH without a HostIO UDP proxy/backend. |
| `connect()` | Partial | AF_UNIX SOCK_DGRAM connects to a bit-bucket pattern. AF_INET TCP is host-backed and works over Node external TCP or the browser local virtual-network backend. A non-blocking TCP connect whose handshake cannot complete synchronously reports `EINPROGRESS` on the first call and `EALREADY` while still in flight (blocking connects wait for completion). AF_INET UDP connect stores the peer, auto-binds an ephemeral local port when needed, filters receives to the connected peer, and supports AF_UNSPEC unconnect. UDP loopback and local virtual routes are supported; external raw UDP routes return ENETUNREACH without a HostIO UDP proxy/backend. |
| `send()` / `recv()` | Partial | Unix domain streams, AF_INET TCP, and connected AF_INET UDP. TCP send/recv works over Node external TCP and the local virtual-network backend. UDP preserves datagram boundaries and supports MSG_PEEK/MSG_DONTWAIT through recvfrom. MSG_NOSIGNAL is honored for stream/broken-pipe paths. |
| `sendto()` / `recvfrom()` | Partial | AF_INET UDP loopback and local virtual-network send/receive, connected and unconnected sendto, source address reporting, and connected receive filtering are implemented. External raw UDP routes return ENETUNREACH and are narrowly xfailed in the Sortix UDP suite. |
| `setsockopt()` / `getsockopt()` | Partial | SOL_SOCKET: SO_TYPE, SO_DOMAIN, SO_ERROR, SO_ACCEPTCONN, SO_RCVBUF, SO_SNDBUF readable; SO_REUSEADDR affects UDP bind conflicts; SO_KEEPALIVE, SO_LINGER, SO_RCVTIMEO, SO_SNDTIMEO, SO_BROADCAST accepted/stored. IPPROTO_TCP: TCP_NODELAY stored. |
Expand Down
23 changes: 23 additions & 0 deletions host/src/kernel-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ const FORK_BUF_SIZE = FORK_SAVE_BUFFER_SIZE;

/** Errno values */
const EAGAIN = 11;
const EALREADY = 114;
const EINPROGRESS = 115;
const ETIMEDOUT = 110;
const EINTR_ERRNO = 4;

Expand Down Expand Up @@ -2560,6 +2562,27 @@ export class CentralizedKernelWorker {
return;
}

// In-flight non-blocking connect: the kernel reports EINPROGRESS on the
// first call and EALREADY while the handshake is still pending. For a
// non-blocking socket these are the POSIX-correct results and are returned
// to the guest as-is; for a blocking socket they mean "keep waiting", so we
// route them into the same retry loop as EAGAIN.
if (
retVal === -1 &&
syscallNr === SYS_CONNECT &&
(errVal === EINPROGRESS || errVal === EALREADY)
) {
const isFdNonblock = this.kernelInstance!.exports.kernel_is_fd_nonblock as
((pid: number, fd: number) => number) | undefined;
const nonblock = isFdNonblock ? isFdNonblock(channel.pid, origArgs[0]) === 1 : false;
if (nonblock) {
this.completeChannel(channel, syscallNr, origArgs, SYSCALL_ARGS[syscallNr], -1, errVal);
} else {
this.handleBlockingRetry(channel, syscallNr, origArgs);
}
return;
}

// 2. Sleep syscalls: kernel returned success immediately, but we need
// to delay the response to simulate the sleep duration.
if (this.handleSleepDelay(channel, syscallNr, origArgs, retVal, errVal)) {
Expand Down
Loading