From 00e58baf8b13c0c33de6f9a37af0962ca2f3c76c Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 20:59:20 +0200 Subject: [PATCH 1/2] File the socket2 fork's OwnedFd conversion that holds a table key, not a handle Found while designing #863. Read from the fork at the commit the lock names and the `toyos` branch's tip (1d9025096919, src/sys/toyos.rs), not run: `From for OwnedFd` (1237-1241) wraps the backend's own table key, counted up from 1000, in `OwnedFd::from_raw_fd`, and forgets the socket. The kernel reads the key as slot 1000 at generation 0 and up; the OwnedFd's drop is SYS_CLOSE of it, which ends a process with no live handle there (handle_fault, exit 139) and silently closes an unrelated handle in one that has one, while the socket's own handle and netstack socket leak. The other fd impls at 1225-1259 cross the same two numbers. No caller exists today: tokio's ToyOS TcpSocket drops its socket2 socket rather than convert it, and neither the tree nor the russh fork calls one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...-ownedfd-holds-a-table-key-not-a-handle.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md diff --git a/issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md b/issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md new file mode 100644 index 0000000000..bf0cb3bac4 --- /dev/null +++ b/issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md @@ -0,0 +1,58 @@ +--- +status: open +kind: defect +opened: 2026-10-12 +--- + +# The socket2 fork's `From for OwnedFd` holds a table key, not a handle + +Read from the fork at the commit the lock names and the `toyos` branch's tip, +`1d90250`, and not run. socket2's ToyOS backend (`src/sys/toyos.rs`) names a +socket by a key into a table of its own, counted up from 1000 (`NEXT_FD`, +`sockets`, `alloc_fd`, lines 320-329), and keeps the socket's kernel handle +inside the entry (`SocketState`, 331-364). Its `From for +OwnedFd` (1237-1241) hands `OwnedFd::from_raw_fd` that key, taken by +`socket_into_raw` (435-439), which forgets the socket. So: + +- The `OwnedFd` holds a number the kernel never gave the process. `RawHandle` + (`sdk/abi/src/handle.rs`) reads a key K as slot K mod 4096 at generation + K / 4096: the first socket's key, 1000, as slot 1000 at generation 0. +- What the socket held is never let go: its entry stays in the table, so its + `Drop` (408-421), which sends `tcp_close` and closes its connection's or + listener's handle, never runs while the process lives. +- The `OwnedFd`'s drop closes the key as a handle (`OwnedFd`'s `Drop`, + `rust/library/std/src/os/fd/owned.rs:241-244` at the gitlink's `c1b8813`). + The kernel looks it up in the process's table (`sys_close`, + `kernel/src/syscall/handles.rs`; `HandleTable::remove`, + `kernel/src/object/handle.rs`). A slot past the table, empty, or at another + generation is `BadHandle` or `Stale`, and `process::handle_fault` ends the + process with exit code 139; a process that never held more than 1000 handles + at once has no slot 1000. A process holding a live handle at that slot and + generation loses it silently, whatever object it names, and the next use of + that handle by its holder is `Stale` and ends the process there. + +The other fd impls cross the same two numbers. `AsFd`, `AsRawFd` (1225-1235) +and `IntoRawFd` (1243-1247) hand out the key as a descriptor. `From` +and `FromRawFd` (1249-1259) take a kernel handle as a key: it names no entry, +so every call on the socket is refused "invalid socket fd" and its drop closes +nothing and leaks the handle; or, where the handle's number is a live key, it +names another socket's entry, which its drop then closes. An unconnected socket +(`SocketState::Unconnected`) holds no kernel object at all, so it has no handle +to give. + +Nothing calls the conversion yet. On ToyOS socket2 is built only for the tokio +fork (`902fb97`). There `TcpSocket::connect` and `listen` drop their socket2 +socket instead of converting it (`tokio/src/net/tcp/socket.rs:838-843` and +`913-923`), and its ToyOS `IntoRawFd for TcpSocket` (1081-1086) passes the key +to whoever calls it. Nothing in the tree, `system/sshserver` included, or in +the russh fork (`389804d`) calls one. + +**Exit**: on ToyOS, `From for OwnedFd` and each other impl at +1225-1259 either names a kernel handle the process holds for that socket, or +does not compile, and the lock names the fork commit that does it. A root +`[patch.crates-io]` that no longer names `ToyOSOrg/socket2` (stage 4 of +`issues/the-ssh-server-is-toyos-own-and-russh-is-gone.md`) also meets this, +because upstream socket2 has no ToyOS backend. + +**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`; the +change is a commit on the socket2 fork's `toyos` branch. From 2daa11297e5637a67e1407daa54b93c65423d9b7 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 21:18:34 +0200 Subject: [PATCH 2/2] Drop the slot-1000 clause and name poll_connect's answer in the socket2 OwnedFd issue Review of #898 round 1, both NOTEs. The clause "a process that never held more than 1000 handles at once has no slot 1000" was false of the kernel: SYS_DUP2 (sys_dup2 -> HandleTable::install_at) installs at any slot below 4096 and grows the table to it, whatever the process holds, and libc's dup2 reaches it. The clause goes; the verdict around it (an empty slot is BadHandle, a live one is lost) stands without it. "every call on the socket is refused \"invalid socket fd\"" was false of the fork at 1d90250: tcp_keepalive_time and set_tcp_keepalive answer Ok without a lookup, and so do recvmsg and sendmsg with no buffers. poll_connect looks the number up but answers NotConnected. The sentence now covers only the calls that look the number up and names poll_connect's answer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...et2-forks-ownedfd-holds-a-table-key-not-a-handle.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md b/issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md index bf0cb3bac4..01e09aa86f 100644 --- a/issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md +++ b/issues/the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle.md @@ -26,17 +26,17 @@ OwnedFd` (1237-1241) hands `OwnedFd::from_raw_fd` that key, taken by `kernel/src/syscall/handles.rs`; `HandleTable::remove`, `kernel/src/object/handle.rs`). A slot past the table, empty, or at another generation is `BadHandle` or `Stale`, and `process::handle_fault` ends the - process with exit code 139; a process that never held more than 1000 handles - at once has no slot 1000. A process holding a live handle at that slot and + process with exit code 139. A process holding a live handle at that slot and generation loses it silently, whatever object it names, and the next use of that handle by its holder is `Stale` and ends the process there. The other fd impls cross the same two numbers. `AsFd`, `AsRawFd` (1225-1235) and `IntoRawFd` (1243-1247) hand out the key as a descriptor. `From` and `FromRawFd` (1249-1259) take a kernel handle as a key: it names no entry, -so every call on the socket is refused "invalid socket fd" and its drop closes -nothing and leaks the handle; or, where the handle's number is a live key, it -names another socket's entry, which its drop then closes. An unconnected socket +so every call on the socket that looks the number up is refused, "invalid +socket fd" or, from `poll_connect` (538-545), `NotConnected`, and its drop +closes nothing and leaks the handle; or, where the handle's number is a live +key, it names another socket's entry, which its drop then closes. An unconnected socket (`SocketState::Unconnected`) holds no kernel object at all, so it has no handle to give.