gex-kernel: inet over loopback with no network stack; SHUT_WR across a page; poll and recvmsg fixes

- socket(AF_INET) without a NetStack (the browser with no Wisp relay)
  returned EAFNOSUPPORT, so tests/wasm32/inet.c failed every check from
  bind on in run-browser.sh. It now gets `net::NoNet`: bind, listen,
  connect and accept over loopback work as natively; a connection out is
  ENETUNREACH.
- RemoteEnd (TCP between a page's processes, through the global kernel)
  had no shutdown_write, so shutdown(SHUT_WR) was a no-op and the far
  end's recv waited forever. It is the global kernel's shutdown, which is
  SHUT_WR's.
- poll: a negative fd is an unused slot (revents 0), not POLLNVAL. musl
  >=1.2.4 res_msend polls with fd -1 for its TCP slots; POLLNVAL there made
  every getaddrinfo wait out its 5 s timeout (wineboot's host-name lookup).
- recvmsg on an inet socket writes msg_name (the sender), as recvfrom does.

inet.c: checks for both; it passes natively and in run-browser.sh (x86).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_1896c5b302a54696aaa92c5f4047f20b
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-09 05:54:04 -04:00
1 parent 7c316bae4e
commit f43fba88f0
3 files changed
+65 -3

No files matched your search

+16
View File
@@ -72,6 +72,18 @@ pub trait NetStack: Send + Sync + std::fmt::Debug {
fn tcp(&self) -> Result<Arc<dyn Transport>, Errno>;
}
/// No network behind the kernel (the browser without a Wisp relay): inet
/// sockets still work over loopback -- bind, listen, connect and the
/// kernel's own DNS answers -- and a connection out is unreachable.
#[derive(Debug)]
pub struct NoNet;
impl NetStack for NoNet {
fn tcp(&self) -> Result<Arc<dyn Transport>, Errno> {
Err(Errno::ENETUNREACH)
}
}
// -- names ------------------------------------------------------------------
/// The private range the synthetic answers come from. 240.0.0.0/4 is reserved
@@ -309,6 +321,10 @@ impl Transport for RemoteEnd {
fn close(&self) {
(self.remote.close)(self.id);
}
/// The global kernel's shutdown is SHUT_WR's: the far end reads to its end.
fn shutdown_write(&self) {
(self.remote.shutdown)(self.id);
}
}
/// The global kernel's name for a TCP port (abstract, so no file appears).
+35 -3
View File
@@ -873,6 +873,15 @@ pub fn poll_revents(p: &PollFd) -> u16 {
const POLLIN: u16 = 0x001;
const POLLOUT: u16 = 0x004;
const POLLNVAL: u16 = 0x020;
// A negative descriptor is a slot the caller is not using: its events
// are ignored and it reports nothing, as on Linux. musl's resolver keeps
// one such slot per query for a TCP retry; reporting POLLNVAL there made
// every `ppoll` return at once with nothing to read, and the resolver
// spun until its 5 s timeout -- on every `getaddrinfo` (Wine's
// `wineboot` resolving the host name was 5 s of each session boot).
if p.fd < 0 {
return 0;
}
let Some(h) = &p.handle else {
return POLLNVAL;
};
@@ -5394,9 +5403,9 @@ impl Kernel {
let file = OpenFile::new(handle, "socket:[netlink]".to_string(), flags, true, true);
return self.fds.insert(file, ty & SOCK_CLOEXEC != 0);
}
let Some(net) = self.net.clone() else {
return Err(Errno::EAFNOSUPPORT);
};
// Without a network stack (the browser with no relay), loopback still
// works: Wine's in-process server and Winsock use it.
let net = self.net.clone().unwrap_or_else(|| Arc::new(net::NoNet) as Arc<dyn net::NetStack>);
if domain != AF_INET {
return Err(Errno::EAFNOSUPPORT);
}
@@ -6314,9 +6323,23 @@ impl Kernel {
break;
}
}
let is_unix = unix_sock.is_some();
let received = unix_sock.map(|s| s.take_received()).unwrap_or_default();
let written = self.write_scm_rights(m, control, controllen, received)?;
self.msg_wr(m, msg, 5, written)?;
// Where the datagram came from, as `recvfrom` says it. musl's
// resolver (1.2.4 on) reads its answers with `recvmsg` and drops any
// whose source is not a nameserver it asked: with the name left
// unwritten every answer was dropped, and each `getaddrinfo` waited
// out the resolver's 5 s timeout and failed.
let name = self.msg_rd(m, msg, 0)?;
if name != 0 && !is_unix {
if let Ok(sock) = self.socket_of(fd) {
let (ip, port) = sock.last_from().or_else(|| sock.peer()).unwrap_or((0, 0));
let namelen = msg + if self.compat { 4 } else { 8 };
self.sys_write_sockaddr(m, ip, port, name, namelen)?;
}
}
Ok(total)
}
@@ -6954,6 +6977,15 @@ mod tests {
use super::*;
use crate::vfs::TmpFs;
#[test]
fn poll_skips_negative_descriptors() {
let slot = |fd| PollFd { addr: 0, fd, events: 1, handle: None, writable: false };
// An unused slot reports nothing; a closed descriptor is POLLNVAL.
assert_eq!(poll_revents(&slot(-1)), 0);
assert_eq!(poll_revents(&slot(7)), 0x020);
assert!(!poll_ready(&[slot(-1), slot(-1)]));
}
fn setup() -> (Vec<u8>, Kernel) {
let mem = vec![0u8; 0x40000];
let mut k = Kernel::new(0x1000, 0x10000, "/prog".into());
+14
View File
@@ -59,6 +59,20 @@ int main(void)
ok(poll(&p, 1, 5000) == 1, "poll: a datagram");
len = sizeof b;
ok(recvfrom(u1, buf, sizeof buf, 0, (struct sockaddr *)&b, &len) == 5 && !memcmp(buf, "dgram", 5), "recvfrom");
{
/* What musl's resolver does: poll with unused (negative) slots,
* which report nothing, and recvmsg, which names the sender. */
struct pollfd ps[3] = {{.fd = -1}, {.fd = -1}, {.fd = u1, .events = POLLIN}};
struct sockaddr_in from = {0}, me = {0};
struct iovec io = {buf, sizeof buf};
struct msghdr mh = {.msg_name = &from, .msg_namelen = sizeof from, .msg_iov = &io, .msg_iovlen = 1};
socklen_t ml = sizeof me;
ok(sendto(u2, "again", 5, 0, (struct sockaddr *)&a, sizeof a) == 5, "sendto again");
ok(poll(ps, 3, 5000) == 1 && ps[0].revents == 0 && ps[2].revents == POLLIN, "poll: negative slots report nothing");
ok(recvmsg(u1, &mh, 0) == 5 && !memcmp(buf, "again", 5), "recvmsg");
ok(getsockname(u2, (struct sockaddr *)&me, &ml) == 0 && mh.msg_namelen == sizeof from
&& from.sin_family == AF_INET && from.sin_port == me.sin_port, "recvmsg names the sender");
}
close(u1); close(u2);
printf("%d failures\n", fails);
return fails != 0;