From fc4f631a1c3eb71ed2553df8408527d65d7a1145 Mon Sep 17 00:00:00 2001 From: Kandelo Agent Date: Fri, 5 Jun 2026 21:57:13 +0000 Subject: [PATCH 1/8] fix: align syscall_cp wasm32 signature (cherry picked from commit 45ada263980c39d9e67d649b5ff404d63d4014ca) --- scripts/build-musl.sh | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/scripts/build-musl.sh b/scripts/build-musl.sh index 421215a9b..f0eaef812 100755 --- a/scripts/build-musl.sh +++ b/scripts/build-musl.sh @@ -94,6 +94,54 @@ if [ -d "$OVERLAY_DIR/src" ]; then fi fi +# musl's src/internal/syscall.h uses syscall_arg_t for the public +# varargs syscall() path and also hard-codes it into the non-varargs +# __syscall_cp() cancellation-point prototype. On wasm32posix those +# two paths intentionally differ: +# +# - syscall_arg_t must remain long/i32 because syscall(long, ...) +# reads varargs with va_arg(ap, syscall_arg_t); widening that type +# would read past 32-bit caller arguments. +# - __syscall_cp() is not variadic and must use the same widened i64 +# slots as __syscallN so cancellation-point syscalls preserve +# 64-bit offsets/lengths and match libc/glue/channel_syscall.c's +# wasm function signature. +# +# Let arch/syscall_arch.h opt into a separate SYSCALL_CP_ARG_T while +# keeping upstream musl behavior for arches that do not define it. +python3 - "$MUSL_DIR/src/internal/syscall.h" <<'PY' +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +text = path.read_text() + +default_block = """#ifndef SYSCALL_CP_ARG_T +#define SYSCALL_CP_ARG_T syscall_arg_t +#endif + +""" +insert_after = """#endif + +""" +if "SYSCALL_CP_ARG_T" not in text: + marker = insert_after + "hidden long __syscall_ret" + if marker not in text: + raise SystemExit("build-musl: could not patch syscall.h: insertion marker not found") + text = text.replace(marker, insert_after + default_block + "hidden long __syscall_ret", 1) + +old_proto = """__syscall_cp(syscall_arg_t, syscall_arg_t, syscall_arg_t, syscall_arg_t, +\t syscall_arg_t, syscall_arg_t, syscall_arg_t)""" +new_proto = """__syscall_cp(SYSCALL_CP_ARG_T, SYSCALL_CP_ARG_T, SYSCALL_CP_ARG_T, SYSCALL_CP_ARG_T, +\t SYSCALL_CP_ARG_T, SYSCALL_CP_ARG_T, SYSCALL_CP_ARG_T)""" +if old_proto in text: + text = text.replace(old_proto, new_proto, 1) +elif new_proto not in text: + raise SystemExit("build-musl: could not patch syscall.h: __syscall_cp prototype not found") + +path.write_text(text) +PY + # Copy CRT overlay (e.g., Wasm-specific crt1.c with proper main signature) if [ -d "$OVERLAY_DIR/crt" ]; then cp -r "$OVERLAY_DIR/crt/"* "$MUSL_DIR/crt/" From e2d3f1878bbcfe6cc6e69c40caa9248ff096f8fd Mon Sep 17 00:00:00 2001 From: Kandelo Agent Date: Fri, 5 Jun 2026 22:01:42 +0000 Subject: [PATCH 2/8] fix: patch musl syscall_cp fallback signature (cherry picked from commit 817e410295394ff4b96ed511ffdb6bc6d55dd915) --- scripts/build-musl.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/scripts/build-musl.sh b/scripts/build-musl.sh index f0eaef812..582aaedea 100755 --- a/scripts/build-musl.sh +++ b/scripts/build-musl.sh @@ -142,6 +142,36 @@ elif new_proto not in text: path.write_text(text) PY +python3 - "$MUSL_DIR/src/thread/__syscall_cp.c" <<'PY' +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +text = path.read_text() + +replacements = { + """static long sccp(syscall_arg_t nr, + syscall_arg_t u, syscall_arg_t v, syscall_arg_t w, + syscall_arg_t x, syscall_arg_t y, syscall_arg_t z)""": + """static long sccp(SYSCALL_CP_ARG_T nr, + SYSCALL_CP_ARG_T u, SYSCALL_CP_ARG_T v, SYSCALL_CP_ARG_T w, + SYSCALL_CP_ARG_T x, SYSCALL_CP_ARG_T y, SYSCALL_CP_ARG_T z)""", + """long (__syscall_cp)(syscall_arg_t nr, + syscall_arg_t u, syscall_arg_t v, syscall_arg_t w, + syscall_arg_t x, syscall_arg_t y, syscall_arg_t z)""": + """long (__syscall_cp)(SYSCALL_CP_ARG_T nr, + SYSCALL_CP_ARG_T u, SYSCALL_CP_ARG_T v, SYSCALL_CP_ARG_T w, + SYSCALL_CP_ARG_T x, SYSCALL_CP_ARG_T y, SYSCALL_CP_ARG_T z)""", +} +for old, new in replacements.items(): + if old in text: + text = text.replace(old, new, 1) + elif new not in text: + raise SystemExit(f"build-musl: could not patch __syscall_cp.c pattern: {old.splitlines()[0]}") + +path.write_text(text) +PY + # Copy CRT overlay (e.g., Wasm-specific crt1.c with proper main signature) if [ -d "$OVERLAY_DIR/crt" ]; then cp -r "$OVERLAY_DIR/crt/"* "$MUSL_DIR/crt/" From d94865c1d427bb95f398bd329647f7591851891b Mon Sep 17 00:00:00 2001 From: Kandelo Agent Date: Fri, 12 Jun 2026 06:12:12 +0000 Subject: [PATCH 3/8] fix: provide route netlink loopback enumeration --- crates/kernel/src/fork.rs | 81 +++++++++ crates/kernel/src/socket.rs | 3 +- crates/kernel/src/syscalls.rs | 304 ++++++++++++++++++++++++++++++++++ crates/kernel/src/wasm_api.rs | 7 +- 4 files changed, 393 insertions(+), 2 deletions(-) diff --git a/crates/kernel/src/fork.rs b/crates/kernel/src/fork.rs index e8786ed31..e85065b21 100644 --- a/crates/kernel/src/fork.rs +++ b/crates/kernel/src/fork.rs @@ -682,6 +682,7 @@ pub fn serialize_fork_state(proc: &Process, buf: &mut [u8]) -> Result 0, SocketDomain::Inet => 1, SocketDomain::Inet6 => 2, + SocketDomain::Netlink => 3, })?; w.write_u32(match sock.sock_type { SocketType::Stream => 0, @@ -1046,6 +1047,7 @@ pub fn deserialize_fork_state(buf: &[u8], child_pid: u32) -> Result SocketDomain::Unix, 1 => SocketDomain::Inet, 2 => SocketDomain::Inet6, + 3 => SocketDomain::Netlink, _ => return Err(Errno::EINVAL), }; let sock_type = match r.read_u32()? { @@ -1497,6 +1499,85 @@ pub fn serialize_exec_state(proc: &Process, buf: &mut [u8]) -> Result 0, + SocketDomain::Inet => 1, + SocketDomain::Inet6 => 2, + SocketDomain::Netlink => 3, + })?; + w.write_u32(match sock.sock_type { + SocketType::Stream => 0, + SocketType::Dgram => 1, + })?; + w.write_u32(sock.protocol)?; + w.write_u32(match sock.state { + SocketState::Unbound => 0, + SocketState::Bound => 1, + SocketState::Listening => 2, + SocketState::Connected => 3, + SocketState::Closed => 4, + SocketState::Connecting => 4, + })?; + w.write_u32(sock.peer_idx.map(|v| v as u32).unwrap_or(0xFFFFFFFF))?; + w.write_u32(sock.recv_buf_idx.map(|v| v as u32).unwrap_or(0xFFFFFFFF))?; + w.write_u32(sock.send_buf_idx.map(|v| v as u32).unwrap_or(0xFFFFFFFF))?; + w.write_u32(if sock.shut_rd { 1 } else { 0 })?; + w.write_u32(if sock.shut_wr { 1 } else { 0 })?; + w.write_u32(sock.host_net_handle.map(|v| v as u32).unwrap_or(0xFFFFFFFF))?; + w.write_u32(sock.options.len() as u32)?; + for &(level, optname, value) in &sock.options { + w.write_u32(level)?; + w.write_u32(optname)?; + w.write_u32(value)?; + } + w.write_bytes(&sock.bind_addr)?; + w.write_u32(sock.bind_port as u32)?; + w.write_bytes(&sock.peer_addr)?; + w.write_u32(sock.peer_port as u32)?; + // Legacy per-process accept backlog is consume-once state. + // Shared listener backlog indices below preserve the actual + // POSIX accept queue for current stream listeners. + w.write_u32(0u32)?; + w.write_u32(if sock.global_pipes { 1 } else { 0 })?; + w.write_u32( + sock.shared_backlog_idx + .map(|v| v as u32) + .unwrap_or(0xFFFFFFFF), + )?; + match &sock.bind_path { + Some(p) => { + w.write_u32(p.len() as u32)?; + w.write_bytes(p)?; + } + None => { + w.write_u32(0xFFFFFFFF)?; + } + } + w.write_u32(sock.accept_wake_idx.unwrap_or(0xFFFFFFFF))?; + } + } + } + // ── Patch total_size ── let total = w.pos as u32; w.patch_u32(total_size_offset, total); diff --git a/crates/kernel/src/socket.rs b/crates/kernel/src/socket.rs index 4b3690dec..4b81d05c7 100644 --- a/crates/kernel/src/socket.rs +++ b/crates/kernel/src/socket.rs @@ -65,6 +65,7 @@ pub enum SocketDomain { Unix, Inet, Inet6, + Netlink, } /// Socket type. @@ -375,7 +376,6 @@ impl SocketInfo { shared_backlog_idx: None, accept_wake_idx: None, dgram_queue: Vec::new(), - global_pipes: true, ipv4_multicast_memberships: Vec::new(), netlink_queue: Vec::new(), global_pipes: true, @@ -418,6 +418,7 @@ impl SocketInfo { /// /// Discarded in the child: /// * `dgram_queue` — buffered UDP datagrams. +/// * `netlink_queue` — buffered netlink replies. /// * `oob_byte` — pending TCP out-of-band byte. /// * `listen_backlog` — pre-accepted AF_UNIX same-process connections. /// Indices reference other entries in this process's SocketTable; if diff --git a/crates/kernel/src/syscalls.rs b/crates/kernel/src/syscalls.rs index be2d3a9d7..913534048 100644 --- a/crates/kernel/src/syscalls.rs +++ b/crates/kernel/src/syscalls.rs @@ -2307,6 +2307,7 @@ pub fn sys_read( return Err(Errno::EAGAIN); } } + SocketDomain::Netlink => return sys_recv(proc, host, fd, buf, 0), } } FileType::TimerFd => { @@ -2708,6 +2709,7 @@ pub fn sys_write( return Err(Errno::EAGAIN); } } + SocketDomain::Netlink => return sys_send(proc, host, fd, buf, 0), } } FileType::EventFd => { @@ -6006,6 +6008,117 @@ pub fn sys_mprotect(_proc: &Process, _addr: usize, _len: usize, _prot: u32) -> R Ok(()) } +const AF_NETLINK: u32 = 16; +const SOCK_RAW: u32 = 3; +const NETLINK_ROUTE: u32 = 0; + +const NLMSG_DONE: u16 = 3; +const NLM_F_MULTI: u16 = 2; +const RTM_NEWLINK: u16 = 16; +const RTM_GETLINK: u16 = 18; +const RTM_NEWADDR: u16 = 20; +const RTM_GETADDR: u16 = 22; + +const IFI_LOOPBACK_INDEX: u32 = 1; +const IFI_LOOPBACK_FLAGS: u32 = 0x1 | 0x8 | 0x40; // IFF_UP | IFF_LOOPBACK | IFF_RUNNING +const ARPHRD_LOOPBACK: u16 = 772; + +const IFLA_IFNAME: u16 = 3; +const IFA_ADDRESS: u16 = 1; +const IFA_LOCAL: u16 = 2; +const IFA_LABEL: u16 = 3; + +#[inline] +fn align4_len(len: usize) -> usize { + (len + 3) & !3 +} + +#[inline] +fn push_u16(out: &mut Vec, v: u16) { + out.extend_from_slice(&v.to_le_bytes()); +} + +#[inline] +fn push_u32(out: &mut Vec, v: u32) { + out.extend_from_slice(&v.to_le_bytes()); +} + +fn push_rtattr(out: &mut Vec, attr_type: u16, data: &[u8]) { + let attr_len = 4 + data.len(); + push_u16(out, attr_len as u16); + push_u16(out, attr_type); + out.extend_from_slice(data); + while out.len() % 4 != 0 { + out.push(0); + } +} + +fn push_netlink_msg(out: &mut Vec, msg_type: u16, flags: u16, seq: u32, payload: &[u8]) { + let len = 16 + payload.len(); + push_u32(out, len as u32); + push_u16(out, msg_type); + push_u16(out, flags); + push_u32(out, seq); + push_u32(out, 0); // kernel port id + out.extend_from_slice(payload); + while out.len() % 4 != 0 { + out.push(0); + } +} + +fn push_loopback_link_msg(out: &mut Vec, seq: u32) { + let mut payload = Vec::new(); + payload.push(0); // AF_UNSPEC + payload.push(0); // pad + push_u16(&mut payload, ARPHRD_LOOPBACK); + push_u32(&mut payload, IFI_LOOPBACK_INDEX); + push_u32(&mut payload, IFI_LOOPBACK_FLAGS); + push_u32(&mut payload, 0xFFFF_FFFF); + push_rtattr(&mut payload, IFLA_IFNAME, b"lo\0"); + push_netlink_msg(out, RTM_NEWLINK, NLM_F_MULTI, seq, &payload); +} + +fn push_loopback_addr_msg(out: &mut Vec, seq: u32) { + let mut payload = Vec::new(); + payload.push(2); // AF_INET + payload.push(8); // /8 loopback route + payload.push(0); + payload.push(254); // RT_SCOPE_HOST + push_u32(&mut payload, IFI_LOOPBACK_INDEX); + push_rtattr(&mut payload, IFA_ADDRESS, &[127, 0, 0, 1]); + push_rtattr(&mut payload, IFA_LOCAL, &[127, 0, 0, 1]); + push_rtattr(&mut payload, IFA_LABEL, b"lo\0"); + push_netlink_msg(out, RTM_NEWADDR, NLM_F_MULTI, seq, &payload); +} + +fn netlink_done_msg(seq: u32) -> Vec { + let mut out = Vec::new(); + push_netlink_msg(&mut out, NLMSG_DONE, NLM_F_MULTI, seq, &[]); + out +} + +fn netlink_route_response(request: &[u8]) -> Result, Errno> { + if request.len() < 16 { + return Err(Errno::EINVAL); + } + let nlmsg_len = u32::from_le_bytes([request[0], request[1], request[2], request[3]]) as usize; + if nlmsg_len < 16 || nlmsg_len > request.len() { + return Err(Errno::EINVAL); + } + let msg_type = u16::from_le_bytes([request[4], request[5]]); + let seq = u32::from_le_bytes([request[8], request[9], request[10], request[11]]); + let mut out = Vec::new(); + match msg_type { + RTM_GETLINK => push_loopback_link_msg(&mut out, seq), + RTM_GETADDR => push_loopback_addr_msg(&mut out, seq), + _ => return Err(Errno::EOPNOTSUPP), + } + out.extend_from_slice(&netlink_done_msg(seq)); + let aligned = align4_len(out.len()); + out.resize(aligned, 0); + Ok(out) +} + /// Create a socket, returning the new fd. pub fn sys_socket( proc: &mut Process, @@ -6021,6 +6134,7 @@ pub fn sys_socket( AF_UNIX => SocketDomain::Unix, AF_INET => SocketDomain::Inet, AF_INET6 => SocketDomain::Inet6, + AF_NETLINK => SocketDomain::Netlink, _ => return Err(Errno::EAFNOSUPPORT), }; @@ -6028,6 +6142,7 @@ pub fn sys_socket( let stype = match base_type { SOCK_STREAM => SocketType::Stream, SOCK_DGRAM => SocketType::Dgram, + SOCK_RAW if dom == SocketDomain::Netlink && protocol == NETLINK_ROUTE => SocketType::Dgram, _ => return Err(Errno::EPROTOTYPE), }; @@ -7046,6 +7161,18 @@ pub fn sys_getsockname(proc: &Process, fd: i32, buf: &mut [u8]) -> Result { + let total_len = 12usize; // struct sockaddr_nl + let n = buf.len().min(total_len); + if n >= 2 { + buf[0] = AF_NETLINK as u8; + buf[1] = 0; + } + for b in buf.iter_mut().take(n).skip(2) { + *b = 0; + } + Ok(total_len) + } } } @@ -7075,6 +7202,7 @@ pub fn sys_getpeername(proc: &Process, fd: i32, buf: &mut [u8]) -> Result return Err(Errno::ENOTCONN), } match sock.domain { SocketDomain::Inet => Ok(write_sockaddr_in(buf, sock.peer_addr, sock.peer_port)), @@ -7086,6 +7214,7 @@ pub fn sys_getpeername(proc: &Process, fd: i32, buf: &mut [u8]) -> Result Err(Errno::ENOTCONN), } } @@ -7168,6 +7297,15 @@ pub fn sys_send( } let sock_idx = (-(ofd.host_handle + 1)) as usize; let sock = proc.sockets.get(sock_idx).ok_or(Errno::EBADF)?; + if sock.domain == SocketDomain::Netlink { + if sock.protocol != NETLINK_ROUTE { + return Err(Errno::EOPNOTSUPP); + } + let response = netlink_route_response(buf)?; + let sock = proc.sockets.get_mut(sock_idx).ok_or(Errno::EBADF)?; + sock.netlink_queue.push(response); + return Ok(buf.len()); + } if sock.sock_type == SocketType::Dgram { match sock.domain { SocketDomain::Inet => { @@ -7194,6 +7332,7 @@ pub fn sys_send( return Ok(buf.len()); } SocketDomain::Unix => {} + SocketDomain::Netlink => unreachable!(), } } if sock.state != SocketState::Connected { @@ -7240,6 +7379,7 @@ pub fn sys_send( } result } + SocketDomain::Netlink => Err(Errno::EOPNOTSUPP), } } @@ -7265,6 +7405,20 @@ pub fn sys_recv( } let sock_idx = (-(ofd.host_handle + 1)) as usize; let sock = proc.sockets.get(sock_idx).ok_or(Errno::EBADF)?; + if sock.domain == SocketDomain::Netlink { + let sock = proc.sockets.get_mut(sock_idx).ok_or(Errno::EBADF)?; + let Some(packet) = sock.netlink_queue.first() else { + return Err(Errno::EAGAIN); + }; + let n = buf.len().min(packet.len()); + buf[..n].copy_from_slice(&packet[..n]); + if n == packet.len() { + sock.netlink_queue.remove(0); + } else { + sock.netlink_queue[0].drain(..n); + } + return Ok(n); + } if sock.sock_type == SocketType::Dgram && matches!( sock.domain, @@ -7357,6 +7511,7 @@ pub fn sys_getsockopt(proc: &mut Process, fd: i32, level: u32, optname: u32) -> SocketDomain::Unix => AF_UNIX, SocketDomain::Inet => AF_INET, SocketDomain::Inet6 => AF_INET6, + SocketDomain::Netlink => AF_NETLINK, }), // Linux semantics: return cached errno and clear it. SO_ERROR => { @@ -7869,6 +8024,11 @@ pub fn sys_bind( sock.state = SocketState::Bound; Ok(()) } + SocketDomain::Netlink => { + let sock = proc.sockets.get_mut(sock_idx).ok_or(Errno::EBADF)?; + sock.state = SocketState::Bound; + Ok(()) + } } } @@ -8603,6 +8763,7 @@ pub fn sys_connect( Ok(()) } + SocketDomain::Netlink => Err(Errno::EOPNOTSUPP), } } @@ -8644,6 +8805,10 @@ pub fn sys_sendto( let sock_idx = (-(ofd.host_handle + 1)) as usize; let sock = proc.sockets.get(sock_idx).ok_or(Errno::EBADF)?; + if sock.domain == SocketDomain::Netlink { + return sys_send(proc, _host, fd, buf, _flags); + } + if addr.is_empty() { if sock.state == SocketState::Connected { return sys_send(proc, _host, fd, buf, _flags); @@ -8691,6 +8856,7 @@ pub fn sys_sendto( .ok_or(Errno::ECONNREFUSED)?; unix_dgram_send_to_sock(proc, sock_idx, peer_idx, buf) } + SocketDomain::Netlink => Err(Errno::EOPNOTSUPP), } } @@ -8716,6 +8882,11 @@ pub fn sys_recvfrom( let sock_idx = (-(ofd.host_handle + 1)) as usize; let sock = proc.sockets.get(sock_idx).ok_or(Errno::EBADF)?; + if sock.domain == SocketDomain::Netlink { + let n = sys_recv(proc, _host, fd, buf, _flags)?; + return Ok((n, 0)); + } + // For STREAM sockets, delegate to sys_recv (musl routes recv→recvfrom) if sock.sock_type == SocketType::Stream { let n = sys_recv(proc, _host, fd, buf, _flags)?; @@ -8742,6 +8913,7 @@ pub fn sys_recvfrom( d.src_addr6 == sock.peer_addr6 && d.src_port == sock.peer_port } SocketDomain::Unix => sock.peer_idx.map_or(true, |peer| d.src_sock_idx == Some(peer)), + SocketDomain::Netlink => false, } }); if datagram_idx.is_none() { @@ -8778,6 +8950,7 @@ pub fn sys_recvfrom( } 2 } + SocketDomain::Netlink => 0, }; } @@ -17688,6 +17861,137 @@ mod tests { unsafe { crate::unix_socket::global_unix_socket_registry() }.unregister(&resolved); } + #[test] + fn test_unix_stream_inherited_listener_accepts_parent_connection() { + let _lock = UNIX_REGISTRY_LOCK.lock().unwrap(); + use crate::process_table::ProcessTable; + use crate::spawn::{FileAction, SpawnAttrs}; + + let mut table = ProcessTable::new(); + let mut host = MockHostIO::new(); + let parent_pid = 9101; + table.create_process(parent_pid).unwrap(); + + let path = b"/tmp/inherited_listener_9101.sock"; + let resolved = { + let parent = table.get(parent_pid).unwrap(); + crate::path::resolve_path(path, &parent.cwd) + }; + unsafe { crate::unix_socket::global_unix_socket_registry() }.unregister(&resolved); + + let mut addr = [0u8; 110]; + addr[0] = 1; // AF_UNIX + addr[2..2 + path.len()].copy_from_slice(path); + let addrlen = 2 + path.len() + 1; + + let server_fd = { + let parent = table.get_mut(parent_pid).unwrap(); + let fd = sys_socket(parent, &mut host, 1, 1, 0).unwrap(); + sys_bind(parent, &mut host, fd, &addr[..addrlen]).unwrap(); + sys_listen(parent, &mut host, fd, 5).unwrap(); + fd + }; + + let dup_fd = { + let parent = table.get_mut(parent_pid).unwrap(); + let fd = sys_dup(parent, server_fd).unwrap(); + parent.fd_table.get_mut(server_fd).unwrap().fd_flags |= FD_CLOEXEC; + parent.fd_table.get_mut(fd).unwrap().fd_flags |= FD_CLOEXEC; + fd + }; + + let child_pid = table + .spawn_child( + parent_pid, + &[b"child".as_slice()], + &[], + &[FileAction::Dup2 { + srcfd: dup_fd, + fd: 0, + }], + &SpawnAttrs::empty(), + &mut host, + ) + .unwrap(); + + let client_fd = { + let parent = table.get_mut(parent_pid).unwrap(); + let fd = sys_socket(parent, &mut host, 1, 1, 0).unwrap(); + sys_connect(parent, &mut host, fd, &addr[..addrlen]).unwrap(); + fd + }; + + let accepted_fd = { + let child = table.get_mut(child_pid).unwrap(); + let mut pollfd = WasmPollFd { + fd: 0, + events: POLLIN, + revents: 0, + }; + assert_eq!( + sys_poll(child, &mut host, core::slice::from_mut(&mut pollfd), 0).unwrap(), + 1 + ); + assert_ne!(pollfd.revents & POLLIN, 0); + sys_accept(child, &mut host, 0).unwrap() + }; + + { + let parent = table.get_mut(parent_pid).unwrap(); + assert_eq!(sys_write(parent, &mut host, client_fd, b"ping").unwrap(), 4); + } + { + let child = table.get_mut(child_pid).unwrap(); + let mut buf = [0u8; 4]; + assert_eq!( + sys_read(child, &mut host, accepted_fd, &mut buf).unwrap(), + 4 + ); + assert_eq!(&buf, b"ping"); + } + + // Clean up + unsafe { crate::unix_socket::global_unix_socket_registry() }.unregister(&resolved); + } + + #[test] + fn test_netlink_route_reports_loopback_interface() { + let mut proc = Process::new(1); + let mut host = MockHostIO::new(); + let fd = sys_socket( + &mut proc, + &mut host, + AF_NETLINK, + SOCK_RAW | wasm_posix_shared::socket::SOCK_CLOEXEC, + NETLINK_ROUTE, + ) + .unwrap(); + + let mut req = Vec::new(); + push_u32(&mut req, 20); + push_u16(&mut req, RTM_GETLINK); + push_u16(&mut req, 0x301); // NLM_F_REQUEST | NLM_F_DUMP + push_u32(&mut req, 123); + push_u32(&mut req, 0); + req.push(0); // rtgenmsg.rtgen_family = AF_UNSPEC + req.extend_from_slice(&[0, 0, 0]); + assert_eq!(sys_send(&mut proc, &mut host, fd, &req, 0).unwrap(), req.len()); + + let mut buf = [0u8; 512]; + let n = sys_recv(&mut proc, &mut host, fd, &mut buf, 0).unwrap(); + assert!(n >= 16); + assert_eq!(u16::from_le_bytes([buf[4], buf[5]]), RTM_NEWLINK); + assert!(buf[..n].windows(3).any(|w| w == b"lo\0")); + + req[4..6].copy_from_slice(&RTM_GETADDR.to_le_bytes()); + req[8..12].copy_from_slice(&124u32.to_le_bytes()); + assert_eq!(sys_send(&mut proc, &mut host, fd, &req, 0).unwrap(), req.len()); + let n = sys_recv(&mut proc, &mut host, fd, &mut buf, 0).unwrap(); + assert!(n >= 16); + assert_eq!(u16::from_le_bytes([buf[4], buf[5]]), RTM_NEWADDR); + assert!(buf[..n].windows(4).any(|w| w == [127, 0, 0, 1])); + } + #[test] fn test_unix_stream_connect_pushes_accept_wakeup() { let _lock = UNIX_REGISTRY_LOCK.lock().unwrap(); diff --git a/crates/kernel/src/wasm_api.rs b/crates/kernel/src/wasm_api.rs index 6117aa24f..0729b116b 100644 --- a/crates/kernel/src/wasm_api.rs +++ b/crates/kernel/src/wasm_api.rs @@ -6336,6 +6336,7 @@ fn extract_scm_rights( SocketDomain::Unix => 0, SocketDomain::Inet => 1, SocketDomain::Inet6 => 2, + SocketDomain::Netlink => 3, }, sock_type: match sock.sock_type { SocketType::Stream => 0, @@ -6487,7 +6488,8 @@ fn install_scm_rights_fds( let domain = match sock_data.domain { 0 => SocketDomain::Unix, 1 => SocketDomain::Inet, - _ => SocketDomain::Inet6, + 2 => SocketDomain::Inet6, + _ => SocketDomain::Netlink, }; let sock_type = match sock_data.sock_type { 0 => SocketType::Stream, @@ -7154,6 +7156,9 @@ pub extern "C" fn kernel_accept4( addr_buf.copy_from_slice(&sa[..n]); addrlen_buf.copy_from_slice(&28u32.to_le_bytes()); } + crate::socket::SocketDomain::Netlink => { + addrlen_buf.copy_from_slice(&0u32.to_le_bytes()); + } } } } From 3a9a0db7e99a86ccf49c59bb4962a5309f44a2ff Mon Sep 17 00:00:00 2001 From: Kandelo Agent Date: Mon, 15 Jun 2026 02:39:15 +0000 Subject: [PATCH 4/8] fix: preserve offset on invalid lseek --- host/src/platform/node.ts | 3 +++ host/src/vfs/host-fs.ts | 5 +++++ host/test/node-host-vfs-only-metadata.test.ts | 21 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/host/src/platform/node.ts b/host/src/platform/node.ts index a4ca99629..c6a714da5 100644 --- a/host/src/platform/node.ts +++ b/host/src/platform/node.ts @@ -138,6 +138,9 @@ export class NodePlatformIO implements PlatformIO { default: throw new Error(`Invalid whence value: ${whence}`); } + if (newPos < 0) { + throw makeFsError("EINVAL", "negative seek offset"); + } this.fdPositions.set(handle, newPos); return newPos; } diff --git a/host/src/vfs/host-fs.ts b/host/src/vfs/host-fs.ts index 32427bbd2..562ceca1f 100644 --- a/host/src/vfs/host-fs.ts +++ b/host/src/vfs/host-fs.ts @@ -329,6 +329,11 @@ export class HostFileSystem implements FileSystemBackend { default: throw new Error(`Invalid whence value: ${whence}`); } + if (newPos < 0) { + const err = new Error("EINVAL: negative seek offset") as Error & { code: string }; + err.code = "EINVAL"; + throw err; + } this.fdPositions.set(handle, newPos); return newPos; } diff --git a/host/test/node-host-vfs-only-metadata.test.ts b/host/test/node-host-vfs-only-metadata.test.ts index f0d3bc77e..1a4843108 100644 --- a/host/test/node-host-vfs-only-metadata.test.ts +++ b/host/test/node-host-vfs-only-metadata.test.ts @@ -30,6 +30,8 @@ interface MetadataBackend { stat(path: string): { mode: number; uid: number; gid: number }; open(path: string, flags: number, mode: number): number; close(handle: number): number; + read(handle: number, buffer: Uint8Array, offset: number | null, length: number): number; + seek(handle: number, offset: number, whence: number): number; fstat(handle: number): { mode: number; uid: number; gid: number }; chmod(path: string, mode: number): void; chown(path: string, uid: number, gid: number): void; @@ -111,6 +113,25 @@ const backendFactories: Array<[string, () => BackendCase]> = [ ]; describe.each(backendFactories)("%s", (_name, makeCase) => { + it("rejects negative seek targets without changing the file offset", () => { + const c = makeCase(); + const native = c.nativePath("seek-file"); + writeFileSync(native, "abcdef"); + + const fd = c.backend.open(c.vfsPath("seek-file"), O_RDWR, 0); + try { + expect(c.backend.seek(fd, 2, 0 /* SEEK_SET */)).toBe(2); + expect(() => c.backend.seek(fd, -5, 1 /* SEEK_CUR */)).toThrow(/EINVAL/); + expect(c.backend.seek(fd, 0, 1 /* SEEK_CUR */)).toBe(2); + + const buf = new Uint8Array(1); + expect(c.backend.read(fd, buf, null, 1)).toBe(1); + expect(new TextDecoder().decode(buf)).toBe("c"); + } finally { + c.backend.close(fd); + } + }); + it("keeps path chmod/chown changes in VFS metadata only", () => { const c = makeCase(); const native = c.nativePath("path-file"); From b0b84ebee014dcb7ed0e642f3614dfbc969c002c Mon Sep 17 00:00:00 2001 From: Kandelo Agent Date: Tue, 16 Jun 2026 02:14:37 +0000 Subject: [PATCH 5/8] fix: interrupt select retries on caught signals (cherry picked from commit 7bbb5472253ef09217cf2553c1e7048d9923e8ff) --- host/src/kernel-worker.ts | 28 ++++++++++++++-- host/test/select-timeout-retry.test.ts | 46 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/host/src/kernel-worker.ts b/host/src/kernel-worker.ts index 81073e0e7..3d9de33a1 100644 --- a/host/src/kernel-worker.ts +++ b/host/src/kernel-worker.ts @@ -4407,7 +4407,17 @@ export class CentralizedKernelWorker { } } - this.dequeueSignalForDelivery(channel); + const deliveredSignal = this.dequeueSignalForDelivery(channel); + const getExitStatus = this.kernelInstance!.exports + .kernel_get_process_exit_status as ((pid: number) => number) | undefined; + if (getExitStatus && getExitStatus(channel.pid) >= 128) { + this.handleProcessTerminated(channel); + return; + } + if (deliveredSignal > 0) { + this.completeChannel(channel, SYS_SELECT, origArgs, undefined, -1, EINTR_ERRNO); + return; + } // EAGAIN retry for blocking select. Mirrors handlePselect6. if (retVal === -1 && errVal === EAGAIN) { @@ -4556,8 +4566,20 @@ export class CentralizedKernelWorker { } } - // Handle signal delivery - this.dequeueSignalForDelivery(channel); + // Handle signal delivery. POSIX select/pselect must return EINTR when a + // caught signal is delivered, even when the host-side retry loop would + // otherwise park the channel for EAGAIN readiness polling. + const deliveredSignal = this.dequeueSignalForDelivery(channel); + const getExitStatus = this.kernelInstance!.exports + .kernel_get_process_exit_status as ((pid: number) => number) | undefined; + if (getExitStatus && getExitStatus(channel.pid) >= 128) { + this.handleProcessTerminated(channel); + return; + } + if (deliveredSignal > 0) { + this.completeChannel(channel, SYS_PSELECT6, origArgs, undefined, -1, EINTR_ERRNO); + return; + } // Handle EAGAIN retry for blocking select if (retVal === -1 && errVal === EAGAIN) { diff --git a/host/test/select-timeout-retry.test.ts b/host/test/select-timeout-retry.test.ts index 752920a47..d94361e00 100644 --- a/host/test/select-timeout-retry.test.ts +++ b/host/test/select-timeout-retry.test.ts @@ -140,6 +140,52 @@ describe("centralized select/pselect timeout retries", () => { expect(worker.pendingPollRetries.size).toBe(0); }); + it("interrupts host-side pselect6 retry when a handler signal is pending", () => { + const kernelMemory = createSharedMemory(); + const processMemory = createSharedMemory(); + const scratchOffset = 128; + const handleChannel = vi.fn(() => { + const kernelView = new DataView(kernelMemory.buffer, scratchOffset); + kernelView.setBigInt64(CH_RETURN, -1n, true); + kernelView.setUint32(CH_ERRNO, 11, true); + return 0; + }); + const worker = createWorkerHarness({ + kernel_handle_channel: handleChannel, + kernel_get_process_exit_status: () => 0, + }); + worker.kernelMemory = kernelMemory; + worker.scratchOffset = scratchOffset; + worker.dequeueSignalForDelivery = vi.fn(() => 10); + + const channel = createChannel(42, processMemory); + worker.processes = new Map([ + [42, { pid: 42, memory: processMemory, channels: [channel], ptrWidth: 4 }], + ]); + worker.activeChannels = [channel]; + + const readfdsPtr = 1024; + const tsPtr = 2048; + const processView = new DataView(processMemory.buffer); + processView.setUint8(readfdsPtr, 1); + processView.setBigInt64(tsPtr, 1n, true); + processView.setBigInt64(tsPtr + 8, 0n, true); + + const origArgs = [1, readfdsPtr, 0, 0, tsPtr, 0]; + worker.handlePselect6(channel, origArgs); + + expect(worker.dequeueSignalForDelivery).toHaveBeenCalledWith(channel); + expect(worker.completeChannel).toHaveBeenCalledWith( + channel, + expect.any(Number), + origArgs, + undefined, + -1, + 4, + ); + expect(worker.pendingSelectRetries.size).toBe(0); + }); + it("merges disjoint MAP_SHARED writes from live processes", () => { const worker = createWorkerHarness({}); const mem1 = createSharedMemory(); From 53ea15487584bf0c307bf1aa067fcdac8b77076a Mon Sep 17 00:00:00 2001 From: Kandelo Agent Date: Tue, 16 Jun 2026 03:57:20 +0000 Subject: [PATCH 6/8] fix: implement times syscall for php fpm tests (cherry picked from commit ad467cdcc92a83def3cd2ffb041283d5267073ec) --- abi/snapshot.json | 4 ++ crates/shared/src/lib.rs | 5 ++ host/src/generated/abi.ts | 2 + host/src/kernel-worker.ts | 44 ++++++++++++++++ host/test/select-timeout-retry.test.ts | 69 ++++++++++++++++++++++++++ 5 files changed, 124 insertions(+) diff --git a/abi/snapshot.json b/abi/snapshot.json index 87ae45000..2a30cdddb 100644 --- a/abi/snapshot.json +++ b/abi/snapshot.json @@ -5123,6 +5123,10 @@ "name": "GetRobustList", "number": 262 }, + { + "name": "Times", + "number": 270 + }, { "name": "Mknod", "number": 271 diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index e1c5eee34..71fb8d8f7 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -1216,6 +1216,7 @@ pub mod abi { pub const SYS_STATX: u32 = 260; pub const SYS_SET_ROBUST_LIST: u32 = 261; pub const SYS_GET_ROBUST_LIST: u32 = 262; + pub const SYS_TIMES: u32 = 270; pub const SYS_MKNOD: u32 = 271; pub const SYS_MKNODAT: u32 = 272; pub const SYS_MSYNC: u32 = 278; @@ -1379,6 +1380,10 @@ pub mod abi { name: "GetRobustList", number: SYS_GET_ROBUST_LIST, }, + AbiSyscallNumber { + name: "Times", + number: SYS_TIMES, + }, AbiSyscallNumber { name: "Mknod", number: SYS_MKNOD, diff --git a/host/src/generated/abi.ts b/host/src/generated/abi.ts index 515696b9c..18a8a1ca4 100644 --- a/host/src/generated/abi.ts +++ b/host/src/generated/abi.ts @@ -287,6 +287,7 @@ export const ABI_SYSCALLS = { Statx: 260, SetRobustList: 261, GetRobustList: 262, + Times: 270, Mknod: 271, Mknodat: 272, Msync: 278, @@ -498,6 +499,7 @@ export const ABI_SYSCALL_NAMES: Record = { 260: "statx", 261: "set_robust_list", 262: "get_robust_list", + 270: "times", 271: "mknod", 272: "mknodat", 278: "msync", diff --git a/host/src/kernel-worker.ts b/host/src/kernel-worker.ts index 3d9de33a1..ec3bd8104 100644 --- a/host/src/kernel-worker.ts +++ b/host/src/kernel-worker.ts @@ -202,6 +202,8 @@ const SYS_RECVMSG = ABI_SYSCALLS.Recvmsg; const SYS_ACCEPT = ABI_SYSCALLS.Accept; const SYS_ACCEPT4 = ABI_SYSCALLS.Accept4; const SYS_CONNECT = ABI_SYSCALLS.Connect; +const SYS_TIMES = ABI_SYSCALLS.Times; +const CLK_TCK = 100; const MSG_DONTWAIT = 0x0040; @@ -2234,6 +2236,11 @@ export class CentralizedKernelWorker { return; } + if (syscallNr === SYS_TIMES) { + this.handleTimes(channel, origArgs, logging ? logEntry : undefined); + return; + } + // --- Futex: must operate on process memory, not kernel memory --- // The kernel's host_futex_wake/wait imports use kernel memory, but futex // addresses are in process memory. Intercept here and handle directly. @@ -4160,6 +4167,43 @@ export class CentralizedKernelWorker { return false; } + private handleTimes(channel: ChannelInfo, origArgs: number[], logEntry?: string): void { + const tmsPtr = origArgs[0]; + const ptrWidth = this.getPtrWidth(channel.pid); + const structSize = ptrWidth === 8 ? 32 : 16; + const processMem = new Uint8Array(channel.memory.buffer); + + if (tmsPtr !== 0) { + if (!Number.isInteger(tmsPtr) || tmsPtr < 0 || tmsPtr + structSize > processMem.length) { + if (logEntry) console.error(logEntry + " = -1 (EFAULT)"); + this.completeChannel(channel, SYS_TIMES, origArgs, undefined, -1, EFAULT); + return; + } + + // Kandelo does not yet account guest CPU time separately. Match the + // existing getrusage baseline by returning zero user/system ticks while + // still providing a valid tms struct. + const view = new DataView(channel.memory.buffer, tmsPtr, structSize); + if (ptrWidth === 8) { + for (let off = 0; off < structSize; off += 8) { + view.setBigInt64(off, 0n, true); + } + } else { + for (let off = 0; off < structSize; off += 4) { + view.setInt32(off, 0, true); + } + } + } + + const nowMs = typeof performance !== "undefined" && typeof performance.now === "function" + ? performance.now() + : Date.now(); + const ticks = Math.floor((nowMs * CLK_TCK) / 1000); + if (logEntry) console.error(logEntry + ` = ${ticks}`); + this.dequeueSignalForDelivery(channel); + this.completeChannel(channel, SYS_TIMES, origArgs, undefined, ticks, 0); + } + /** * Complete a sleep syscall, checking for pending signals first. * POSIX: sleep interrupted by signal returns EINTR. diff --git a/host/test/select-timeout-retry.test.ts b/host/test/select-timeout-retry.test.ts index d94361e00..9883f83fa 100644 --- a/host/test/select-timeout-retry.test.ts +++ b/host/test/select-timeout-retry.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { + ABI_SYSCALLS, CH_ERRNO, CH_RETURN, } from "../src/generated/abi"; @@ -186,6 +187,64 @@ describe("centralized select/pselect timeout retries", () => { expect(worker.pendingSelectRetries.size).toBe(0); }); + it("does not truncate multi-kilobyte exec environment strings", () => { + const worker = createWorkerHarness({}); + const processMemory = createSharedMemory(); + const mem = new Uint8Array(processMemory.buffer); + const view = new DataView(processMemory.buffer); + const arrayPtr = 1024; + const stringPtr = 4096; + const value = `FPM_SOCKETS=${"x".repeat(9000)}`; + mem.set(new TextEncoder().encode(value), stringPtr); + mem[stringPtr + value.length] = 0; + view.setUint32(arrayPtr, stringPtr, true); + view.setUint32(arrayPtr + 4, 0, true); + + expect(worker.readStringArrayFromProcess(mem, arrayPtr, 4)).toEqual([value]); + }); + + it("implements times(2) with ptr-width aware tms output", () => { + const worker = createWorkerHarness({}); + const processMemory = createSharedMemory(); + const processView = new DataView(processMemory.buffer); + const channel = createChannel(42, processMemory); + worker.processes = new Map([ + [42, { pid: 42, memory: processMemory, channels: [channel], ptrWidth: 4 }], + ]); + worker.activeChannels = [channel]; + + const tms32 = 1024; + worker.handleTimes(channel, [tms32, 0, 0, 0, 0, 0]); + + expect(worker.completeChannel).toHaveBeenCalledWith( + channel, + ABI_SYSCALLS.Times, + [tms32, 0, 0, 0, 0, 0], + undefined, + expect.any(Number), + 0, + ); + for (let off = 0; off < 16; off += 4) { + expect(processView.getInt32(tms32 + off, true)).toBe(0); + } + + const worker64 = createWorkerHarness({}); + const processMemory64 = createSharedMemory(); + const processView64 = new DataView(processMemory64.buffer); + const channel64 = createChannel(43, processMemory64); + worker64.processes = new Map([ + [43, { pid: 43, memory: processMemory64, channels: [channel64], ptrWidth: 8 }], + ]); + worker64.activeChannels = [channel64]; + + const tms64 = 2048; + worker64.handleTimes(channel64, [tms64, 0, 0, 0, 0, 0]); + + for (let off = 0; off < 32; off += 8) { + expect(processView64.getBigInt64(tms64 + off, true)).toBe(0n); + } + }); + it("merges disjoint MAP_SHARED writes from live processes", () => { const worker = createWorkerHarness({}); const mem1 = createSharedMemory(); @@ -282,6 +341,16 @@ function createWorkerHarness(exports: Record): any { dequeueSignalForDelivery: vi.fn(), bindKernelTidForChannel: vi.fn(), assertKernelStackContext: vi.fn(), + assertKernelStackStage: vi.fn(), + assertKernelStackBaseline: vi.fn(), + isKernelStackTraceEnabled: vi.fn(() => false), + synchronizeSharedMappingsForSyscallBoundary: vi.fn(), + synchronizeSysvShmMappingsForSyscallBoundary: vi.fn(), + flushSharedMappingsBeforeFileSyscall: vi.fn(), + handleSharedMappingsAfterFileSyscall: vi.fn(), + toKernelPtr(value: number | bigint): number { + return Number(value); + }, }); } From c6a257c6088d93df8e84d7fd2681720ac7dda0e5 Mon Sep 17 00:00:00 2001 From: Kandelo Agent Date: Tue, 16 Jun 2026 05:50:23 +0000 Subject: [PATCH 7/8] test: remove stale sortix pthread xfail (cherry picked from commit bc13ad8631a2cd71f620b0e311a120b7752a100c) --- scripts/run-sortix-tests.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/run-sortix-tests.sh b/scripts/run-sortix-tests.sh index 1dadac835..9235d01d5 100755 --- a/scripts/run-sortix-tests.sh +++ b/scripts/run-sortix-tests.sh @@ -38,9 +38,6 @@ INCLUDE_EXPECTED_FAIL=( BASIC_EXPECTED_FAIL=( "devctl/posix_devctl" # device control (Sortix/2024, not in musl) - "pthread/pthread_condattr_setpshared" # cross-process MAP_SHARED|MAP_ANONYMOUS memory - # not supported on wasm (pthread primitives ARE - # supported — see crates/kernel/src/pshared.rs) "pthread/pthread_attr_setinheritsched" # priority scheduling not supported "strings/ffsll" # wasm32 test bug (long vs long long) # aio/aio_cancel was flaky (FAIL once, XPASS next run) — left From 771abb8e7f2ff65601cbc0709b6f4ccb77cc3bd2 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Fri, 19 Jun 2026 08:29:17 -0400 Subject: [PATCH 8/8] Declare bzip2 for pure dev-shell xtask linking --- flake.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 8e680e438..32a906c08 100644 --- a/flake.nix +++ b/flake.nix @@ -88,7 +88,8 @@ # mkconfig, cpython itself, file's # magic-build, etc. # flex/bison — bash, m4, mariadb (yacc-style parsers) - # xz — extracting .tar.xz tarballs (sed, m4, …) + # bzip2/xz — xtask/package extraction links libbz2/liblzma + # for .tar.bz2/.tar.xz source archives. # patch — applying *.patch files (mariadb, ruby) # gh — only used by stage-pr-staging release lookup pkgs.curl @@ -96,6 +97,7 @@ pkgs.python3 pkgs.flex pkgs.bison + pkgs.bzip2 pkgs.xz pkgs.gnupatch pkgs.gh