From 9e6d8dd4e33f3a499a56ed6488384dcb8bfb13aa Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 13 Sep 2026 19:49:37 +0200 Subject: [PATCH] sockets: a descriptor can cross between processes sendmsg/recvmsg with SCM_RIGHTS. Item 1 of docs/plan/foundations.md and mechanism 1 of docs/plan/hybrid.md, which are the same primitive seen from two directions: Wayland's wl_shm_create_pool passes a file descriptor with no integer-shaped alternative, and handing a driver process the device it is allowed to touch is the same act. Both plans stopped here. Linux's structures byte for byte -- msghdr, cmsghdr, the CMSG_ macros -- because the programs that need this are not ours. libwayland and toybox both call recvmsg with Linux's headers and would not notice a friendlier door. The delivery rule is the whole design. A batch of descriptors is pinned to the stream position of the first byte of the message that carried it, and that has two halves: a reader arriving at that position is handed them, and a reader that has not reached it is stopped SHORT of it. Leave out the second half and a program asking for 4096 bytes swallows three messages and is handed one message's descriptors, with nothing in the result saying which. socktest checks exactly that: two one-byte messages, an eight-byte read, one byte back. The kernel holds a reference to every descriptor in flight, so a sender may close its own copy the moment sendmsg returns. Giving that reference back is the part that leaks if it is not written down, and there are three ways a message ends: - received with room installed in the receiver's table - received without room, or by plain read(2) closed; MSG_CTRUNC for the first case - never received, socket closed closed by sockclose The middle row looks like a choice and is not: holding kernel references for a receiver that has proved it is not looking is a leak with extra steps. No garbage collector. A socket descriptor sent over that same socket and never received holds a reference to the socket that holds the reference to it, and neither close reaches zero. Linux carries several hundred lines for this. Nothing on either plan passes a socket over a socket -- Wayland passes memory, the driver work passes devices -- so it is a bounded leak that is documented rather than a silent one. Fourteen checks in cmd/socktest, and every one moves data through the descriptor rather than inspecting it. The descriptor passed is always a pipe end, because a pipe announces its own lifetime: a read of a pipe whose writers are all gone returns 0, which turns "was the kernel's reference released" -- otherwise invisible from userspace -- into a value a test can compare. Two bugs fell out, neither in the new code: - sockpair() never set the socket's domain, so every socketpair() socket claimed domain 0. Invisible while the data paths asked only whether a socket was AF_INET or AF_NETLINK, since zero answers "neither" correctly. SCM_RIGHTS asks "is this AF_UNIX", and zero is wrong for that one. - pipe_poll() reports POLLHUP but not POLLIN at end of file, so a non-blocking read of a pipe whose writers had all gone returned EAGAIN forever instead of 0 -- a program that polls and reads spins waiting for an end that already happened. sockreadable() always had the equivalent condition; the pipe path did not. Found because the test helper for "is this descriptor closed" is a non-blocking read of a pipe. projects/schcom/README.md said SCM_RIGHTS was done for a long time while nothing in the kernel implemented it, and an entire GUI plan was drawn on that line. It is true now, which is not the same as it having been right, and the README says so. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- cmd/socktest/main.c | 347 ++++++++++++++++++++++++++++++ docs/architecture/kernel.md | 2 - docs/architecture/networking.md | 64 +++++- docs/architecture/syscalls.md | 5 +- docs/plan/foundations.md | 42 +++- docs/plan/hybrid.md | 18 +- include/defs.h | 3 + include/socket.h | 113 ++++++++++ include/user.h | 4 + kernel/fs/file.c | 8 +- kernel/net/unix.c | 186 ++++++++++++++-- kernel/sys/sys_net.c | 361 ++++++++++++++++++++++++++++++++ kernel/sys/table.inc | 4 + lib/usys.S | 2 + projects/schcom/README.md | 18 +- 15 files changed, 1134 insertions(+), 43 deletions(-) diff --git a/cmd/socktest/main.c b/cmd/socktest/main.c index 6be9122..ed513ad 100644 --- a/cmd/socktest/main.c +++ b/cmd/socktest/main.c @@ -402,6 +402,347 @@ test_nonblock(void) close(sv[1]); } + + +// The user library has no string.h; these three are all this file needs. +static void +memset_(void *p, int c, int n) +{ + char *d = p; + while (n-- > 0) *d++ = (char)c; +} + +static void +memmove_(void *dst, const void *src, int n) +{ + char *d = dst; + const char *s = src; + while (n-- > 0) *d++ = *s++; +} + +static int +strneq_(const char *a, const char *b, int n) +{ + while (n-- > 0 && *a == *b && *a) { a++; b++; } + return n < 0 || *a == *b; +} + +// --------------------------------------------------------------------------- +// 8. SCM_RIGHTS: a descriptor crossing between processes. +// +// docs/plan/foundations.md item 1, docs/plan/hybrid.md mechanism 1. The thing +// being proved is not that recvmsg returns a number -- it is that the number +// names the *same open file* the other process had, which is why every check +// below moves data through the descriptor rather than inspecting it. +// +// The descriptor passed is always a pipe end, for one reason: a pipe announces +// its own lifetime. A read of a pipe whose every write end is closed returns 0, +// and that turns "was the kernel's reference released" -- which userspace +// cannot otherwise see -- into a value a test can compare. +// --------------------------------------------------------------------------- +#define T_F_SETFL 4 + +// Send `fd` with one byte of data on it. +static int +sendfd(int sock, int fd, char tag) +{ + struct msghdr m; + struct iovec iv; + char cbuf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr *c; + + memset_(&m, 0, sizeof(m)); + memset_(cbuf, 0, sizeof(cbuf)); + + iv.iov_base = &tag; + iv.iov_len = 1; + m.msg_iov = &iv; + m.msg_iovlen = 1; + m.msg_control = cbuf; + m.msg_controllen = sizeof(cbuf); + + c = (struct cmsghdr *)cbuf; + c->cmsg_len = CMSG_LEN(sizeof(int)); + c->cmsg_level = SOL_SOCKET; + c->cmsg_type = SCM_RIGHTS; + memmove_(CMSG_DATA(c), &fd, sizeof(int)); + + return sendmsg(sock, &m, 0); +} + +// Receive into `fds`, up to `want` of them. Returns the byte count; *ngot is +// how many descriptors arrived and *flags the kernel's msg_flags. +static int +recvfd(int sock, char *tag, int *fds, int want, int *ngot, int *flags) +{ + struct msghdr m; + struct iovec iv; + char cbuf[CMSG_SPACE(SCM_MAXFD * sizeof(int))]; + struct cmsghdr *c; + int n; + + memset_(&m, 0, sizeof(m)); + memset_(cbuf, 0, sizeof(cbuf)); + + iv.iov_base = tag; + iv.iov_len = 1; + m.msg_iov = &iv; + m.msg_iovlen = 1; + m.msg_control = cbuf; + // CMSG_LEN, not CMSG_SPACE: the trailing padding only matters if another + // header follows, and sizing by it would leave room for a descriptor the + // caller did not ask for -- which is exactly what makes a truncation test + // pass by accident. + m.msg_controllen = CMSG_LEN(want * sizeof(int)); + + *ngot = 0; + n = recvmsg(sock, &m, 0); + if (flags) + *flags = m.msg_flags; + if (n < 0) + return n; + + c = CMSG_FIRSTHDR(&m); + if (c && c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS) { + int k = (int)((c->cmsg_len - CMSG_LEN(0)) / sizeof(int)); + for (*ngot = 0; *ngot < k && *ngot < want; (*ngot)++) + memmove_(&fds[*ngot], CMSG_DATA(c) + *ngot * sizeof(int), sizeof(int)); + } + return n; +} + +// Is every write end of this pipe gone? Non-blocking, so a descriptor that +// leaked reports itself instead of hanging the test. +static int +pipe_at_eof(int rfd) +{ + char c; + fcntl(rfd, T_F_SETFL, T_O_NONBLOCK); + return read(rfd, &c, 1) == 0; +} + +static void +test_fdpass(void) +{ + int sv[2], p[2], pid, got = 0, fds[SCM_MAXFD], n; + char tag = 0, buf[32]; + + puts_("SCM_RIGHTS:\n"); + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) { + check(0, "socketpair() and pipe() for the descriptor tests"); + return; + } + + pid = fork(); + if (pid == 0) { + close(sv[0]); + write(p[1], "through a passed descriptor", 27); + sendfd(sv[1], p[0], 'F'); + // Every process-held reference is gone from here on. If the kernel is + // not holding one of its own, the parent receives a dead descriptor. + close(p[0]); + close(p[1]); + close(sv[1]); + exit(); + } + + close(sv[1]); + close(p[0]); + close(p[1]); + wait(); + + n = recvfd(sv[0], &tag, fds, SCM_MAXFD, &got, 0); + check(n == 1 && tag == 'F', "recvmsg returns the byte the descriptor rode on"); + check(got == 1, "and one descriptor with it"); + + if (got == 1) { + n = read(fds[0], buf, sizeof(buf)); + check(n == 27 && strneq_(buf, "through a passed descriptor", 27), + "the descriptor is the sender's pipe, and still has its data"); + close(fds[0]); + } + close(sv[0]); +} + +// Two messages, each carrying a descriptor, read with a buffer big enough for +// both. A read that ran past the second message's first byte would hand back +// one message's descriptors with two messages' bytes, and nothing in the +// result would say so -- which is why the kernel stops the read at the +// boundary instead. +static void +test_fdpass_boundary(void) +{ + int sv[2], a[2], b[2], got = 0, fds[SCM_MAXFD], n, flags = 0; + char tag = 0; + struct msghdr m; + struct iovec iv; + char big[8]; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || + pipe(a) < 0 || pipe(b) < 0) { + check(0, "sockets and pipes for the boundary test"); + return; + } + + sendfd(sv[1], a[0], '1'); + sendfd(sv[1], b[0], '2'); + + // Ask for eight bytes. Two are available. + memset_(&m, 0, sizeof(m)); + iv.iov_base = big; + iv.iov_len = sizeof(big); + m.msg_iov = &iv; + m.msg_iovlen = 1; + n = recvmsg(sv[0], &m, 0); + check(n == 1, "a read spanning two messages stops at the second's boundary"); + + n = recvfd(sv[0], &tag, fds, SCM_MAXFD, &got, &flags); + check(n == 1 && tag == '2' && got == 1, + "so the next read starts there, and gets that message's descriptor"); + if (got == 1) + close(fds[0]); + + close(sv[0]); close(sv[1]); + close(a[0]); close(a[1]); close(b[0]); close(b[1]); +} + +// A plain read(2) of a message carrying descriptors. There is nowhere to put +// them, so they are closed -- the alternative is the kernel holding references +// for a receiver that has proved it is not looking. +static void +test_fdpass_dropped(void) +{ + int sv[2], p[2]; + char c; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) { + check(0, "sockets and a pipe for the dropped-descriptor test"); + return; + } + + sendfd(sv[1], p[1], 'D'); + close(p[1]); // the kernel's reference is the last one + + check(read(sv[0], &c, 1) == 1 && c == 'D', + "read(2) of a message carrying a descriptor returns its bytes"); + check(pipe_at_eof(p[0]), + "and closes the descriptor rather than holding it for nobody"); + + close(p[0]); close(sv[0]); close(sv[1]); +} + +// A message with descriptors that is never received at all. The socket is +// closed with them still queued, and the kernel holds the only references -- +// so if close does not drain the queue they are unreachable and permanent. +static void +test_fdpass_orphaned(void) +{ + int sv[2], p[2]; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) { + check(0, "sockets and a pipe for the orphaned-descriptor test"); + return; + } + + sendfd(sv[1], p[1], 'O'); + close(p[1]); + close(sv[0]); // never received + close(sv[1]); + + check(pipe_at_eof(p[0]), + "closing a socket closes the descriptors still queued on it"); + close(p[0]); +} + +// More descriptors than the receiver left room for. The ones that do not fit +// are closed, and MSG_CTRUNC is how the caller is told it happened. +static void +test_fdpass_truncated(void) +{ + int sv[2], a[2], b[2], got = 0, fds[SCM_MAXFD], flags = 0; + char tag = 0; + struct msghdr m; + struct iovec iv; + char cbuf[CMSG_SPACE(2 * sizeof(int))]; + struct cmsghdr *c; + int two[2]; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || + pipe(a) < 0 || pipe(b) < 0) { + check(0, "sockets and pipes for the truncation test"); + return; + } + + // One message, two descriptors. + memset_(&m, 0, sizeof(m)); + memset_(cbuf, 0, sizeof(cbuf)); + tag = 'T'; + iv.iov_base = &tag; + iv.iov_len = 1; + m.msg_iov = &iv; + m.msg_iovlen = 1; + m.msg_control = cbuf; + m.msg_controllen = sizeof(cbuf); + c = (struct cmsghdr *)cbuf; + c->cmsg_len = CMSG_LEN(2 * sizeof(int)); + c->cmsg_level = SOL_SOCKET; + c->cmsg_type = SCM_RIGHTS; + two[0] = a[1]; + two[1] = b[1]; + memmove_(CMSG_DATA(c), two, sizeof(two)); + check(sendmsg(sv[1], &m, 0) == 1, "sendmsg carries two descriptors at once"); + close(a[1]); + close(b[1]); + + // Receive with room for one. + tag = 0; + recvfd(sv[0], &tag, fds, 1, &got, &flags); + check(got == 1, "a receiver with room for one gets one"); + check((flags & MSG_CTRUNC) != 0, "and MSG_CTRUNC says the rest were dropped"); + check(pipe_at_eof(b[0]) || pipe_at_eof(a[0]), + "the descriptor that did not fit was closed, not leaked"); + + if (got == 1) + close(fds[0]); + close(a[0]); close(b[0]); close(sv[0]); close(sv[1]); +} + +// Descriptors with no bytes to ride on. Delivery pins them to the position of +// a first byte, so a message with no bytes has no position at which anyone +// could ever be handed them; saying so beats accepting it and losing them. +static void +test_fdpass_nodata(void) +{ + int sv[2], p[2]; + struct msghdr m; + char cbuf[CMSG_SPACE(sizeof(int))]; + struct cmsghdr *c; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) { + check(0, "sockets and a pipe for the no-data test"); + return; + } + + memset_(&m, 0, sizeof(m)); + memset_(cbuf, 0, sizeof(cbuf)); + m.msg_control = cbuf; + m.msg_controllen = sizeof(cbuf); + c = (struct cmsghdr *)cbuf; + c->cmsg_len = CMSG_LEN(sizeof(int)); + c->cmsg_level = SOL_SOCKET; + c->cmsg_type = SCM_RIGHTS; + memmove_(CMSG_DATA(c), &p[1], sizeof(int)); + + check(sendmsg(sv[1], &m, 0) < 0, + "sendmsg refuses descriptors with no data to carry them"); + close(p[1]); + check(pipe_at_eof(p[0]), + "and does not keep the reference it took while trying"); + + close(p[0]); close(sv[0]); close(sv[1]); +} + int main(void) { @@ -414,6 +755,12 @@ main(void) test_big(); test_sockopt(); test_nonblock(); + test_fdpass(); + test_fdpass_boundary(); + test_fdpass_dropped(); + test_fdpass_orphaned(); + test_fdpass_truncated(); + test_fdpass_nodata(); if (failures == 0) puts_("socktest: ALL CHECKS PASSED\n"); diff --git a/docs/architecture/kernel.md b/docs/architecture/kernel.md index 85fc5f0..9930137 100644 --- a/docs/architecture/kernel.md +++ b/docs/architecture/kernel.md @@ -195,8 +195,6 @@ allocation over 8 MB mapped straight into a neighbouring live mapping. - **No demand paging and no page cache.** `mmap` of a file *reads the whole thing in* at map time. Writable `MAP_SHARED` of a file is therefore refused outright rather than faked — see [../plan/foundations.md](../plan/foundations.md). -- **No `sendmsg`/`recvmsg`, so no `SCM_RIGHTS`.** File descriptors cannot be - passed between processes. - **No dynamic linking.** `exec` handles `ET_EXEC` only: no `PT_INTERP`, no `ET_DYN`, no shared libraries, no `dlopen`. - **No threads.** `clone` exists but does not share an address space; diff --git a/docs/architecture/networking.md b/docs/architecture/networking.md index 82bcce9..33c0a4e 100644 --- a/docs/architecture/networking.md +++ b/docs/architecture/networking.md @@ -176,6 +176,53 @@ server by a name agreed in advance — and what is lost is `ls -l` showing an `s and `unlink()` of a socket path working. Both want a VFS change rather than a socket change. +### Descriptor passing + +`sendmsg`/`recvmsg` with `SCM_RIGHTS`, which is the one thing a socket can carry +that a pipe cannot: a reference to an object the kernel already knows, rather +than a copy of its contents. Wayland's `wl_shm_create_pool` passes a descriptor +and has no integer-shaped alternative, and +[hybrid.md](../plan/hybrid.md)'s mechanism 1 is how a driver process is handed +the device it is allowed to touch. Both wanted the same primitive. + +**A batch of descriptors is pinned to the stream position of the first byte of +the message that carried it.** That single rule is the whole delivery model, and +it has two halves that are easy to get half-right: + +- a reader that arrives at that position is handed the descriptors, and +- a reader that has not reached it yet is **stopped short of it**. + +Without the second half, a program that asked for 4096 bytes would swallow three +messages in one call and be handed one message's descriptors, with nothing in +the result saying which message they belonged to. `socktest` checks exactly +this: two messages, each one byte and one descriptor, read with an eight-byte +buffer, and the first read must return one byte. + +The kernel holds a reference on every descriptor in flight, so a sender may +close its own copy the moment `sendmsg` returns. That reference is also the +thing that has to be given back, and there are three ways a message ends: + +| | | +|---|---| +| received by `recvmsg` with room | installed in the receiver's table | +| received by `recvmsg` without room, or by plain `read(2)` | **closed**, and `MSG_CTRUNC` set for the first case | +| never received — the socket is closed | **closed** by `sockclose` | + +The second row is the one that looks like a choice and is not. Holding kernel +references for a receiver that has already proved it is not looking is a leak +with extra steps, and it is what Linux does for the same reason. + +Descriptors need at least one byte to ride on; `sendmsg` refuses a message of +pure ancillary data with `EINVAL` rather than accepting it and pinning the +descriptors to a position that never arrives. + +**There is no garbage collector.** An AF_UNIX socket descriptor, sent over that +same socket and never received, holds a reference to the socket that holds the +reference to it, and neither close can reach zero. Linux carries several hundred +lines to break these cycles. Nothing on either plan passes a socket over a +socket — Wayland passes memory, the driver work passes devices — so this is a +known and bounded leak (thirty-two sockets) rather than a silent one. + ### AF_INET `kernel/net/inet.c`, on lwIP's **raw** API rather than lwIP's own sockets layer, @@ -296,11 +343,18 @@ be fixed before `recvfrom` could report a source address at all. ## What it does not do -- **`ping` is a first-party program**, `cmd/ping`, not toybox's. toybox's reads - replies with `recvmsg(2)` and asks for the TTL through an `IP_RECVTTL` control - message, and neither exists — `recvmsg` is item 1 of - [../plan/foundations.md](../plan/foundations.md), where it is wanted for - `SCM_RIGHTS` anyway. Ours has no `-I`, no flood mode and no TTL display. +- **`ping` is a first-party program**, `cmd/ping`, not toybox's. `recvmsg` now + exists, so half of what stopped toybox's is gone; what remains is + `IP_RECVTTL`, an `IPPROTO_IP` control message that would have to be generated + on the receive path rather than merely carried. Ours has no `-I`, no flood + mode and no TTL display. +- **`SCM_RIGHTS` is the only control message.** `SCM_CREDENTIALS` is refused + with `EINVAL` rather than ignored: a caller told its credentials were passed + would have the receiver read a uid out of uninitialised memory and act on it. +- **A message may carry eight descriptors, and a socket eight such messages.** + Past that `sendmsg` answers `ENOBUFS`. The cap exists because the kernel holds + a reference to each one until it is received; a sender eight unreceived + messages ahead of its receiver is not rescued by a ninth slot. - **No resolver reachable from userland.** lwIP's DNS client is compiled but nothing exposes it, so `ping` takes a dotted quad. `localhost` is special-cased in the program rather than resolved. diff --git a/docs/architecture/syscalls.md b/docs/architecture/syscalls.md index acbc180..9073970 100644 --- a/docs/architecture/syscalls.md +++ b/docs/architecture/syscalls.md @@ -113,8 +113,9 @@ field by field, each through its own translation, or use `copyin`/`copyout`. ## What it does not do -- **No `sendmsg`/`recvmsg`**, and therefore no ancillary data and no - `SCM_RIGHTS`. +- **`sendmsg`/`recvmsg` carry `SCM_RIGHTS` and nothing else.** + `SCM_CREDENTIALS` is refused rather than ignored. See + [networking.md](networking.md#descriptor-passing). - **No `memfd_create`.** - **No `getsockname` or `getpeername`**, and `accept()` does not report an AF_INET peer's address. diff --git a/docs/plan/foundations.md b/docs/plan/foundations.md index 7e4f165..3d6098a 100644 --- a/docs/plan/foundations.md +++ b/docs/plan/foundations.md @@ -17,16 +17,17 @@ before anything below was planned around it: | | | |---|---| -| `sendmsg`/`recvmsg`, and therefore **`SCM_RIGHTS`** | **Do not exist.** Not in `kernel/sys/table.inc`, not anywhere in `kernel/`. | +| ~~`sendmsg`/`recvmsg`, and therefore **`SCM_RIGHTS`**~~ | **Built.** See item 1 below. | | Writable `MAP_SHARED` of a file | **Refused** — `kernel/sys/sys_mem.c:131` returns `-ENODEV`. | | `memfd_create` | Does not exist. | | `AF_UNIX` names | In a table in `kernel/net/unix.c`, not in the filesystem. | | `setsockopt` | `return 0;` unconditionally — `kernel/sys/sys_net.c:436`. | | `exec` | `ET_EXEC` only. No `PT_INTERP`, no `ET_DYN`. | -> **`projects/schcom/README.md` says `SCM_RIGHTS` is done. That line is wrong**, -> and it is the most load-bearing wrong line in the tree — an entire GUI plan was -> drawn on top of it. It is corrected as part of this work. +> **`projects/schcom/README.md` said `SCM_RIGHTS` was done while nothing in the +> kernel implemented it** — the most load-bearing wrong line in the tree, with an +> entire GUI plan drawn on top of it. The line is now true, which is not the same +> as it having been right, and the README says so. The `MAP_SHARED` refusal is worth defending rather than fixing carelessly. The code returns `-ENODEV` *honestly*: the contract of a writable shared mapping is @@ -39,8 +40,9 @@ same lie in a new place. ## The five items In dependency order. The first three are what Wayland cannot start without. +Item 1 is built; items 2 and 3 are what a buffer still cannot cross on. -### 1. `sendmsg` / `recvmsg` with `SCM_RIGHTS` +### 1. `sendmsg` / `recvmsg` with `SCM_RIGHTS` — **done** Not optional and not substitutable. `wl_shm_create_pool` passes a **file descriptor**; there is no path in the Wayland protocol that passes an integer @@ -49,14 +51,34 @@ instead. The `handle_kind` field in `projects/schcom/proto.h` was good foresight and it cannot help here — *our* protocol is the flexible one, and Wayland's is not. -Needs: `struct msghdr`/`cmsghdr` handling, ancillary data queued alongside the -byte stream in `kernel/net/unix.c`, descriptor installation into the receiver's -file table, and correct behaviour when a message carrying descriptors is never -received (they must be closed, not leaked). +Built as described: `struct msghdr`/`cmsghdr` in Linux's layout, ancillary data +queued alongside the byte stream in `kernel/net/unix.c`, descriptor installation +into the receiver's file table, and the three ways a message can end — received, +received without room, never received — each releasing the kernel's reference. +Fourteen checks in `cmd/socktest`, every one of them moving data through the +descriptor rather than inspecting it. Documented in +[../architecture/networking.md](../architecture/networking.md#descriptor-passing). -**This is also item 1 of the hybrid work**, where it is how a driver process is +The delivery rule is the part worth knowing before building on it: **descriptors +are pinned to the stream position of the first byte of the message that carried +them**, and a read that has not reached that position is cut short so it stops +before it. That is what keeps "which message did these belong to" answerable. + +**This was also item 1 of the hybrid work**, where it is how a driver process is handed the device it is allowed to touch. +Two bugs fell out of building it, neither in the new code: + +- `sockpair()` never set the socket's domain, so **every `socketpair()` socket + claimed domain 0**. Invisible for as long as the data paths asked only whether + a socket was `AF_INET` or `AF_NETLINK` — zero answers "neither" correctly. + `SCM_RIGHTS` asks "is this `AF_UNIX`", and zero is wrong for that one. +- `pipe_poll()` reports `POLLHUP` but not `POLLIN` for a pipe at end of file, so + a **non-blocking read of a pipe whose writers have all gone returned `EAGAIN` + forever** instead of 0. A program that polls and reads would spin waiting for + an end that had already happened. `sockreadable()` always had the equivalent + condition; the pipe path did not. + ### 2. A shareable memory object `memfd_create`, plus `mmap(MAP_SHARED)` on it genuinely working between two diff --git a/docs/plan/hybrid.md b/docs/plan/hybrid.md index 5504853..16b007b 100644 --- a/docs/plan/hybrid.md +++ b/docs/plan/hybrid.md @@ -157,7 +157,8 @@ re-fires for ever, drowning the e1000 that shares it. The fix is one line, is that the storm detector built for history/kernel.md section 13 named the line, the rate and the neighbour without anyone having to look. -**The control channel (mechanism 1) is what remains.** +**All four mechanisms now exist.** What remains on this plan is the second +eviction and then the foreign driver host. ### One thing the interface is deliberately not @@ -175,11 +176,20 @@ version that survives a licensing conversation. Four, and only the fourth is large. -### 1. A control channel +### 1. A control channel — **built** `AF_UNIX` plus `SCM_RIGHTS` — item 1 of [foundations.md](foundations.md), which -the Wayland work builds first. A descriptor is how a driver process is handed the -device it is allowed to touch. +the Wayland work also needed first. A descriptor is how a driver process is +handed the device it is allowed to touch. + +Built as `sendmsg`/`recvmsg` in Linux's shape, because the programs that will +use it are not ours: libwayland and toybox both call `recvmsg` with Linux's +structures and would not notice a friendlier door. Descriptors are pinned to the +stream position of the first byte of their message, and the kernel holds a +reference to each until it is received — or closes it when the socket is dropped +with the message still queued, which is the case that leaks if nobody writes it +down. See +[../architecture/networking.md](../architecture/networking.md#descriptor-passing). **Do not build Mach ports.** The port model is worth its cost when there is a naming and capability story to hang on it; there is not one yet, and inventing it diff --git a/include/defs.h b/include/defs.h index e2cd78b..207fdae 100644 --- a/include/defs.h +++ b/include/defs.h @@ -424,6 +424,9 @@ void sockclose(struct socket*); void sockdup(struct socket*); int sockread(struct socket*, char*, int); int sockwrite(struct socket*, char*, int); +// The same two, carrying descriptors. See kernel/net/unix.c. +int sockread_fds(struct socket*, char*, int, struct file**, int*); +int sockwrite_fds(struct socket*, char*, int, struct file**, int); int sockbind(struct socket*, const char *path); int socklisten(struct socket*, int backlog); struct socket* sockaccept(struct socket*); diff --git a/include/socket.h b/include/socket.h index feddad5..ca87433 100644 --- a/include/socket.h +++ b/include/socket.h @@ -55,6 +55,90 @@ #define TCP_NODELAY 1 +// --------------------------------------------------------------------------- +// Ancillary data -- sendmsg/recvmsg, and SCM_RIGHTS +// +// A descriptor is the only thing that can be passed between two processes that +// is not a number: it names an object the kernel already knows, and handing it +// over is the difference between "here is a buffer" and "here is a *copy* of a +// buffer". Wayland's wl_shm_create_pool passes one and has no integer-shaped +// alternative; docs/plan/hybrid.md's mechanism 1 is this and nothing else. +// +// Every structure below is Linux's, byte for byte, because a musl-linked +// program builds them with Linux's headers and we do not get a vote. The +// offsets that matter on x86-64 are, for msghdr: +// +// 0 msg_name 8 msg_namelen(+4 pad) 16 msg_iov 24 msg_iovlen +// 32 msg_control 40 msg_controllen 48 msg_flags +// +// -- 56 bytes total, which is what musl's int-typed msg_iovlen plus a pad word +// adds up to as well. +// --------------------------------------------------------------------------- + +#define MSG_OOB 0x0001 +#define MSG_PEEK 0x0002 +#define MSG_DONTROUTE 0x0004 +#define MSG_CTRUNC 0x0008 // some ancillary data was dropped +#define MSG_TRUNC 0x0020 +#define MSG_DONTWAIT 0x0040 +#define MSG_EOR 0x0080 +#define MSG_WAITALL 0x0100 +#define MSG_NOSIGNAL 0x4000 +#define MSG_CMSG_CLOEXEC 0x40000000 + +// The only control message that exists here. SCM_CREDENTIALS is deliberately +// absent rather than stubbed: a caller told it succeeded would read a uid out +// of uninitialised memory and act on it. +#define SCM_RIGHTS 1 + +// The most descriptors one message may carry. A cap has to exist -- the kernel +// holds a reference to every one of them until it is received -- and saying so +// here means a sender can size its control buffer against the same number the +// kernel will enforce. +#define SCM_MAXFD 8 + +struct iovec { + void *iov_base; + uint64 iov_len; +}; + +struct msghdr { + void *msg_name; // struct sockaddr *, or 0 + uint32 msg_namelen; + uint32 __pad1; + struct iovec *msg_iov; + uint64 msg_iovlen; + void *msg_control; // struct cmsghdr *, or 0 + uint64 msg_controllen; + int msg_flags; // out on recvmsg: MSG_CTRUNC +}; + +struct cmsghdr { + uint64 cmsg_len; // CMSG_LEN(payload), NOT including padding + int cmsg_level; // SOL_SOCKET + int cmsg_type; // SCM_RIGHTS +}; + +// cmsg_len counts the header plus the payload; CMSG_SPACE additionally counts +// the padding to the next header. Conflating the two is the classic way to +// build a control buffer the kernel then rejects, which is why both exist. +#define CMSG_ALIGN(n) (((uint64)(n) + sizeof(uint64) - 1) & ~(uint64)(sizeof(uint64) - 1)) +#define CMSG_LEN(n) (CMSG_ALIGN(sizeof(struct cmsghdr)) + (uint64)(n)) +#define CMSG_SPACE(n) (CMSG_ALIGN(sizeof(struct cmsghdr)) + CMSG_ALIGN(n)) +#define CMSG_DATA(c) ((unsigned char *)((struct cmsghdr *)(c) + 1)) + +#define CMSG_FIRSTHDR(m) \ + ((m)->msg_control && (m)->msg_controllen >= sizeof(struct cmsghdr) \ + ? (struct cmsghdr *)(m)->msg_control \ + : (struct cmsghdr *)0) + +#define CMSG_NXTHDR(m, c) \ + ((c)->cmsg_len < sizeof(struct cmsghdr) || \ + (uint64)((char *)(c) + CMSG_ALIGN((c)->cmsg_len) + sizeof(struct cmsghdr) \ + - (char *)(m)->msg_control) > (m)->msg_controllen \ + ? (struct cmsghdr *)0 \ + : (struct cmsghdr *)((char *)(c) + CMSG_ALIGN((c)->cmsg_len))) + #define UNIX_PATH_MAX 108 #define SHUT_RD 0 @@ -164,6 +248,29 @@ struct rtattr { #define SOCKBUF 4096 // receive buffer per connected socket #define NBACKLOG 8 +// Descriptors in flight. +// +// A batch is pinned to the stream position of the first byte of the message +// that carried it, and that is the whole delivery rule: a reader that arrives +// at that position gets the descriptors, and a reader that has not reached it +// yet is stopped *short* of it rather than allowed to read past. Without the +// second half a program that read 4096 bytes in one call would swallow three +// messages and be handed one message's descriptors, with no way to tell which. +// +// Eight batches in flight is not a guess: a sender that has eight unreceived +// messages carrying descriptors and keeps going is not going to be rescued by +// a ninth slot, and ENOBUFS says so where a larger array would only move the +// same failure further away. +#define NSCM 8 // messages carrying descriptors, in flight + +struct file; + +struct scm_fds { + uint pos; // stream position of the message's first byte + int n; + struct file *f[SCM_MAXFD]; // references held by the kernel, not by anyone +}; + enum sockstate { SS_FREE = 0, SS_UNBOUND, @@ -209,6 +316,12 @@ struct socket { int o_keepalive; int o_broadcast; int o_nodelay; + + // Descriptors sent to this socket and not yet received. They live on the + // *receiving* side, next to the bytes they were sent with, because that is + // the only side that can say when they have been delivered. + struct scm_fds scm[NSCM]; + int nscm; }; #endif diff --git a/include/user.h b/include/user.h index 55718ce..42e7497 100644 --- a/include/user.h +++ b/include/user.h @@ -148,6 +148,10 @@ int recvfrom(int fd, void *buf, int len, int flags, struct sockaddr*, int*); int sockshutdown(int fd, int how); int setsockopt(int fd, int level, int opt, const void *val, int len); int getsockopt(int fd, int level, int opt, void *val, int *len); +// The only calls that can move a descriptor between processes (SCM_RIGHTS). +struct msghdr; +int sendmsg(int fd, const struct msghdr *msg, int flags); +int recvmsg(int fd, struct msghdr *msg, int flags); // --- ulib.c --- int stat(const char*, struct stat*); diff --git a/kernel/fs/file.c b/kernel/fs/file.c index 42a0ce5..ee862a6 100644 --- a/kernel/fs/file.c +++ b/kernel/fs/file.c @@ -118,7 +118,13 @@ fileread(struct file *f, char *addr, int n) // this is a check of the same condition their sleep loops wait on, made one // level up. Writes still block -- fcntl documents that. if(f->type == FD_PIPE){ - if(f->nonblock && !(pipe_poll(f->pipe, f->readable, f->writable) & POLLIN)) + // POLLHUP counts as readable here, and leaving it out was a bug: a pipe + // whose writers have all gone is not "nothing yet", it is end of file, and + // the answer to a read is 0. Told EAGAIN instead, a caller that polls and + // reads spins forever waiting for an end that has already happened. + // sockreadable() has always included the equivalent condition. + if(f->nonblock && + !(pipe_poll(f->pipe, f->readable, f->writable) & (POLLIN | POLLHUP))) return -EAGAIN; return piperead(f->pipe, addr, n); } diff --git a/kernel/net/unix.c b/kernel/net/unix.c index 16eb44f..e02367c 100644 --- a/kernel/net/unix.c +++ b/kernel/net/unix.c @@ -45,6 +45,10 @@ static struct { struct socket sock[NSOCK]; } stab; +// Defined with the rest of the descriptor-passing code below; declared here +// because sockclose is the one caller that cannot be moved after it. +static void scm_drain(struct socket *s); + void sockinit(void) { @@ -123,27 +127,46 @@ sockclose(struct socket *s) return; } + struct socket *q[NBACKLOG]; + int nq; + acquire(&stab.lock); if(--s->ref > 0){ release(&stab.lock); return; } - orphan_peer(s); - // A listener that goes away takes its unaccepted connections with it. Each // queued entry is the *server* side of a connection whose client is already // live and may be writing into it; that client has to be told, or it hangs // on a socket that will never be read. - for(i = 0; i < s->nqueue; i++){ - struct socket *q = s->queue[i]; - if(q == 0) - continue; - orphan_peer(q); - q->state = SS_FREE; - q->ref = 0; - } + // + // Taking the backlog out here rather than walking it below is what lets the + // drain happen with the lock dropped. At this point the reference count is + // zero and the state is not yet SS_FREE, so nothing can find these sockets + // and alloc_locked will not reuse them either. + nq = s->nqueue; + for(i = 0; i < nq; i++) + q[i] = s->queue[i]; s->nqueue = 0; + release(&stab.lock); + + // Descriptors that were sent to these sockets and never received have to be + // closed rather than leaked -- the kernel is holding the only reference. + scm_drain(s); + for(i = 0; i < nq; i++) + if(q[i]) + scm_drain(q[i]); + + acquire(&stab.lock); + orphan_peer(s); + for(i = 0; i < nq; i++){ + if(q[i] == 0) + continue; + orphan_peer(q[i]); + q[i]->state = SS_FREE; + q[i]->ref = 0; + } s->state = SS_FREE; s->ref = 0; @@ -247,15 +270,76 @@ sockname(struct socket *s) return s->name; } +// --------------------------------------------------------------------------- +// Descriptor passing +// +// docs/plan/hybrid.md mechanism 1, and item 1 of foundations.md. The kernel +// holds a reference on every descriptor in flight, which is what makes the +// sender free to close its own copy the instant sendmsg returns -- and what +// makes dropping a socket with descriptors still queued a thing that has to be +// handled rather than a thing that happens to work. +// +// **What this does not have is a garbage collector.** A descriptor for an +// AF_UNIX socket, sent over that same socket and never received, holds a +// reference to the socket that holds the reference to it; neither side's close +// can reach zero and the pair leaks until reboot. Linux carries several hundred +// lines to break exactly these cycles. Nothing on either plan passes a socket +// over a socket -- Wayland passes memory and the driver work passes devices -- +// so this is a known, bounded leak (thirty-two sockets) and not a silent one. +// --------------------------------------------------------------------------- + +// Close every descriptor still queued on s. +// +// Called with the socket already unreachable but *not yet marked free*, and +// without stab.lock: fileclose can sleep. Popping one at a time keeps the +// kernel stack out of it -- a snapshot of every batch would be half a page. +static void +scm_drain(struct socket *s) +{ + struct file *f; + int i, j; + + for(;;){ + f = 0; + acquire(&stab.lock); + for(i = 0; i < s->nscm && f == 0; i++) + for(j = 0; j < s->scm[i].n; j++) + if(s->scm[i].f[j]){ + f = s->scm[i].f[j]; + s->scm[i].f[j] = 0; + break; + } + if(f == 0){ + s->nscm = 0; + release(&stab.lock); + return; + } + release(&stab.lock); + fileclose(f); + } +} + // --------------------------------------------------------------------------- // Data // --------------------------------------------------------------------------- +// Read bytes, and the descriptors that came with them. +// +// `fout` is an array of SCM_MAXFD slots, or 0. **Zero is not "ignore them"**: +// a plain read(2) of a message carrying descriptors *closes* them, because the +// alternative is holding kernel references for a receiver that has already +// proved it is not looking. That is what Linux does and it is the only choice +// that cannot leak. int -sockread(struct socket *s, char *addr, int n) +sockread_fds(struct socket *s, char *addr, int n, struct file **fout, int *nfout) { + struct file *taken[SCM_MAXFD]; + int got = 0; int i; + if(nfout) + *nfout = 0; + if(n < 0) return -EINVAL; if(s->domain == AF_INET) @@ -289,6 +373,24 @@ sockread(struct socket *s, char *addr, int n) sleep(&s->nread, &stab.lock); } + // A message carrying descriptors is a boundary. This read either starts + // exactly on one -- and takes them -- or is cut short so that it stops + // before it, leaving the next read to start there. + if(s->nscm > 0){ + if(s->scm[0].pos == s->nread){ + got = s->scm[0].n; + for(i = 0; i < got; i++) + taken[i] = s->scm[0].f[i]; + for(i = 1; i < s->nscm; i++) + s->scm[i - 1] = s->scm[i]; + s->nscm--; + } + // Positions strictly increase -- a message with descriptors carries at + // least one byte -- so after taking one the next is genuinely ahead. + if(s->nscm > 0 && (uint)n > s->scm[0].pos - s->nread) + n = (int)(s->scm[0].pos - s->nread); + } + for(i = 0; i < n; i++){ if(s->nread == s->nwrite) break; @@ -299,17 +401,44 @@ sockread(struct socket *s, char *addr, int n) // on a full buffer that this call has just made room in. wakeup(&s->nwrite); release(&stab.lock); + + // Outside the lock, because fileclose can sleep. + if(fout){ + for(int j = 0; j < got; j++) + fout[j] = taken[j]; + if(nfout) + *nfout = got; + } else { + for(int j = 0; j < got; j++) + fileclose(taken[j]); + } return i; } int -sockwrite(struct socket *s, char *addr, int n) +sockread(struct socket *s, char *addr, int n) +{ + return sockread_fds(s, addr, n, 0, 0); +} + +// Write bytes, optionally handing `nf` descriptors over with them. +// +// The kernel takes its own reference on each before it gets here, so the +// sender may close its copies as soon as this returns. A return of *any* +// positive count means the descriptors were queued; a negative return means +// they were not, and are still the caller's to close. There is no third case, +// which is deliberate -- the attach happens under the same lock hold as the +// first byte, so no reader can slip between them. +int +sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf) { struct socket *dst; int i; if(n < 0) return -EINVAL; + if(nf > 0 && (s->domain != AF_UNIX || n == 0)) + return -EINVAL; if(s->domain == AF_INET) return inet_write(s->inet, addr, n); @@ -339,6 +468,13 @@ sockwrite(struct socket *s, char *addr, int n) return -EPIPE; } + // Refuse early and cleanly rather than part-way through the bytes. A sender + // that is this far ahead of its receiver is not helped by a partial message. + if(nf > 0 && s->peer->nscm >= NSCM){ + release(&stab.lock); + return -ENOBUFS; + } + for(i = 0; i < n; i++){ // Re-read every iteration: the peer can go away while we sleep below. dst = s->peer; @@ -356,6 +492,20 @@ sockwrite(struct socket *s, char *addr, int n) sigsend(myproc()->pid, SIGPIPE); return i > 0 ? i : -EPIPE; // a short write is still a write } + // The descriptors ride at the position of the first byte, pinned here -- + // between the room check above and the store below, with the lock held + // throughout, so the position cannot be read past before it exists. + if(i == 0 && nf > 0){ + if(dst->nscm >= NSCM){ + release(&stab.lock); + return -ENOBUFS; + } + struct scm_fds *b = &dst->scm[dst->nscm++]; + b->pos = dst->nwrite; + b->n = nf; + for(int j = 0; j < nf; j++) + b->f[j] = f[j]; + } dst->data[dst->nwrite++ % SOCKBUF] = addr[i]; } @@ -364,6 +514,12 @@ sockwrite(struct socket *s, char *addr, int n) return i; } +int +sockwrite(struct socket *s, char *addr, int n) +{ + return sockwrite_fds(s, addr, n, 0, 0); +} + // --------------------------------------------------------------------------- // Connection setup // --------------------------------------------------------------------------- @@ -427,6 +583,12 @@ sockpair(struct socket **pa, struct socket **pb) release(&stab.lock); return -ENFILE; } + // The domain was never set here, and nothing noticed for as long as every + // data path asked only whether it was AF_INET or AF_NETLINK -- a socketpair + // socket answered "neither" by being zero. SCM_RIGHTS asks the other + // question, "is this AF_UNIX", and zero is the wrong answer to that one. + a->domain = AF_UNIX; + b->domain = AF_UNIX; a->peer = b; a->state = SS_CONNECTED; b->peer = a; b->state = SS_CONNECTED; release(&stab.lock); diff --git a/kernel/sys/sys_net.c b/kernel/sys/sys_net.c index 2c8bd0e..0138655 100644 --- a/kernel/sys/sys_net.c +++ b/kernel/sys/sys_net.c @@ -581,3 +581,364 @@ SYSCALL_DEFINE(getsockopt) return r; return opt_out(uval, ulenp, val); } + +// --------------------------------------------------------------------------- +// sendmsg / recvmsg +// +// These exist for SCM_RIGHTS. Everything else they do -- scatter/gather over an +// iovec, an address on the side -- sendto and recvfrom already did, and the +// reason to build the general call rather than a `sendfd()` is that the +// programs that need it are not ours: libwayland and toybox's ping both call +// recvmsg with Linux's structures and would not notice a friendlier door. +// +// docs/plan/foundations.md item 1, and docs/plan/hybrid.md mechanism 1. +// --------------------------------------------------------------------------- + +// Linux caps an iovec at UIO_MAXIOV = 1024. Thirty-two is the cap here for one +// reason: each entry is validated and copied individually, and a caller that +// needs more than thirty-two separate buffers in one message is describing a +// problem this kernel has not met. EMSGSIZE is Linux's answer for too many. +#define SCH_IOV_MAX 32 + +// The control buffer is copied onto the kernel stack, and that stack is 4 KB +// (param.h, KSTACKSIZE). 256 bytes holds CMSG_SPACE(SCM_MAXFD * 4) six times +// over; anything larger on the *send* side is a caller with a plan we do not +// need to support. +#define SCH_CMSG_MAX 256 + +// Resolve a descriptor to both the socket and the open file description -- +// recvmsg needs the second because O_NONBLOCK lives there, not on the socket. +static int +argsockfile(int n, struct socket **ps, struct file **pf) +{ + int fd; + struct file *f; + + if (argint(n, &fd) < 0) + return -EBADF; + if (fd < 0 || fd >= NOFILE || !(f = myproc()->ofile[fd])) + return -EBADF; + if (f->type != FD_SOCKET) + return -ENOTSOCK; + *ps = f->sock; + *pf = f; + return 0; +} + +static int +fetch_msghdr(addr_t umsg, struct msghdr *m) +{ + if (umsg == 0 || copy_from_user(m, umsg, sizeof(*m)) < 0) + return -EFAULT; + if (m->msg_iovlen > SCH_IOV_MAX) + return -EMSGSIZE; + return 0; +} + +// One iovec, validated. Returns the byte count, 0 for an empty entry that the +// caller should skip, or negative on a bad pointer. +static int +fetch_iov(struct msghdr *m, uint64 i, char **pp) +{ + struct iovec iv; + + if (copy_from_user(&iv, (addr_t)&m->msg_iov[i], sizeof(iv)) < 0) + return -EFAULT; + if (iv.iov_len == 0) + return 0; + if (iv.iov_len > 0x7fffffff) + return -EINVAL; + if (!(*pp = (char *)user_ptr((addr_t)iv.iov_base, iv.iov_len))) + return -EFAULT; + return (int)iv.iov_len; +} + +// Pull the descriptors out of a control buffer and take a kernel reference on +// each. On any failure the ones already taken are released -- a half-gathered +// message must not leave references behind for a call that returns an error. +static int +scm_gather(struct msghdr *m, struct file **fs, int *nfp) +{ + char buf[SCH_CMSG_MAX]; + uint64 off, payload, i; + struct cmsghdr *c; + struct file *f; + int ufd, r; + + *nfp = 0; + if (m->msg_control == 0 || m->msg_controllen < sizeof(struct cmsghdr)) + return 0; + if (m->msg_controllen > SCH_CMSG_MAX) + return -EINVAL; + if (copy_from_user(buf, (addr_t)m->msg_control, m->msg_controllen) < 0) + return -EFAULT; + + for (off = 0; off + sizeof(struct cmsghdr) <= m->msg_controllen; ) { + c = (struct cmsghdr *)(buf + off); + if (c->cmsg_len < CMSG_LEN(0) || c->cmsg_len > m->msg_controllen - off) { + r = -EINVAL; + goto fail; + } + if (c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS) { + payload = c->cmsg_len - CMSG_LEN(0); + for (i = 0; i + sizeof(int) <= payload; i += sizeof(int)) { + memmove(&ufd, CMSG_DATA(c) + i, sizeof(int)); + if (*nfp >= SCM_MAXFD) { + r = -EINVAL; + goto fail; + } + if (ufd < 0 || ufd >= NOFILE || !(f = myproc()->ofile[ufd])) { + r = -EBADF; + goto fail; + } + fs[(*nfp)++] = filedup(f); + } + } else if (c->cmsg_level == SOL_SOCKET) { + // SCM_CREDENTIALS and friends. Refused rather than ignored: a + // caller that asked to pass credentials and was told yes would + // believe the receiver got them. + r = -EINVAL; + goto fail; + } + off += CMSG_ALIGN(c->cmsg_len); + } + return 0; + +fail: + while (*nfp > 0) + fileclose(fs[--*nfp]); + return r; +} + +SYSCALL_DEFINE(sendmsg) +{ + struct socket *s; + struct file *file; + struct msghdr m; + struct file *fs[SCM_MAXFD]; + addr_t umsg; + uint64 i; + int flags, nf = 0, r; + long sent = 0; + uint ip = 0; + ushort port = 0; + int addressed = 0; + + if ((r = argsockfile(0, &s, &file)) < 0) + return r; + argaddr(1, &umsg); + argint(2, &flags); + if ((r = fetch_msghdr(umsg, &m)) < 0) + return r; + + if ((r = scm_gather(&m, fs, &nf)) < 0) + return r; + + // SCM_RIGHTS is an AF_UNIX facility and there is nothing to generalise: a + // descriptor has no meaning on the far side of a wire. + if (nf > 0 && sock_domain(s) != AF_UNIX) { + r = -EINVAL; + goto fail; + } + + if (m.msg_name && sock_domain(s) == AF_INET) { + if ((r = fetch_sin((addr_t)m.msg_name, (int)m.msg_namelen, &ip, &port)) < 0) + goto fail; + port = port_from_net(port); + addressed = 1; + } + + for (i = 0; i < m.msg_iovlen; i++) { + char *p; + int len, w; + + if ((len = fetch_iov(&m, i, &p)) < 0) { + r = len; + goto fail; + } + if (len == 0) + continue; + + if (addressed) + w = socksendto(s, p, len, ip, port); + else if (nf > 0) + w = sockwrite_fds(s, p, len, fs, nf); + else + w = sockwrite(s, p, len); + + if (w < 0) { + if (sent > 0) + break; // a short write is still a write + r = w; + goto fail; + } + // Any positive count means the descriptors are queued and no longer + // ours; see the contract above sockwrite_fds. + if (w > 0) + nf = 0; + sent += w; + if (w < len) + break; + } + + // Descriptors with no bytes to ride on. The delivery rule pins them to the + // position of a first byte that in this case never arrives, so there is no + // position at which a receiver could ever be handed them. + if (nf > 0) { + r = -EINVAL; + goto fail; + } + return (int)sent; + +fail: + while (nf > 0) + fileclose(fs[--nf]); + return r; +} + +SYSCALL_DEFINE(recvmsg) +{ + struct socket *s; + struct file *file; + struct msghdr m; + struct file *fs[SCM_MAXFD]; + char cbuf[CMSG_SPACE(SCM_MAXFD * sizeof(int))]; + int ufd[SCM_MAXFD]; + addr_t umsg; + uint64 i, clen = 0; + int flags, nf = 0, mflags = 0, r; + long got = 0; + uint ip = 0; + ushort port = 0; + + if ((r = argsockfile(0, &s, &file)) < 0) + return r; + argaddr(1, &umsg); + argint(2, &flags); + if ((r = fetch_msghdr(umsg, &m)) < 0) + return r; + + // O_NONBLOCK on the description and MSG_DONTWAIT on the call mean the same + // thing and are both honoured, because a program that polls and then reads + // uses whichever one its library happened to pick. + if ((file->nonblock || (flags & MSG_DONTWAIT)) && !sockreadable(s)) + return -EAGAIN; + + for (i = 0; i < m.msg_iovlen; i++) { + char *p; + int len, rd; + + if ((len = fetch_iov(&m, i, &p)) < 0) + return len; + if (len == 0) + continue; + + // Only the first read may block, and it stops for good once a message + // carrying descriptors has been taken: the next iovec would be filled + // from the message *after* it, and the caller has no way to say where + // one ended and the next began. + if (got > 0 && (nf > 0 || !sockreadable(s))) + break; + + if (sock_domain(s) == AF_UNIX) + rd = sockread_fds(s, p, len, fs, &nf); + else if (m.msg_name && sock_domain(s) == AF_INET) + rd = sockrecvfrom(s, p, len, &ip, &port); + else + rd = sockread(s, p, len); + + if (rd < 0) { + if (got > 0) + break; + return rd; + } + got += rd; + if (rd < len) + break; + } + + if (nf > 0) { + int room = 0, installed = 0, j; + uint64 writelen; + + if (m.msg_control && m.msg_controllen >= CMSG_LEN(sizeof(int))) + room = (int)((m.msg_controllen - CMSG_LEN(0)) / sizeof(int)); + if (room > nf) + room = nf; + + // How much will actually be written. CMSG_SPACE includes the padding + // to where a *next* header would start, and there is no next header -- + // so when the caller's buffer is not big enough for that padding the + // message still fits without it. Writing CMSG_SPACE unconditionally + // would overrun a buffer sized with CMSG_LEN, which is legal. + writelen = CMSG_SPACE(room * sizeof(int)); + if (writelen > m.msg_controllen) + writelen = CMSG_LEN(room * sizeof(int)); + + // Check the control buffer is writable *before* installing anything. + // A descriptor that has been put in the table and then reported to a + // caller who never learned its number is unclosable. + if (room > 0 && !user_ptr((addr_t)m.msg_control, writelen)) + room = 0; + + for (j = 0; j < room; j++) { + int d = fdalloc(fs[j]); + if (d < 0) + break; + if (flags & MSG_CMSG_CLOEXEC) + myproc()->ofile_flags[d] |= FD_CLOEXEC; + ufd[installed++] = d; + } + + // Whatever did not fit is closed here and nowhere else: the kernel + // holds the last reference, and a receiver with no room is still a + // delivery. MSG_CTRUNC is how the caller finds out. + for (j = installed; j < nf; j++) + fileclose(fs[j]); + if (installed < nf) + mflags |= MSG_CTRUNC; + + if (installed > 0) { + struct cmsghdr *c = (struct cmsghdr *)cbuf; + memset(cbuf, 0, sizeof(cbuf)); // pad bytes, not kernel stack + c->cmsg_len = CMSG_LEN(installed * sizeof(int)); + c->cmsg_level = SOL_SOCKET; + c->cmsg_type = SCM_RIGHTS; + memmove(CMSG_DATA(c), ufd, installed * sizeof(int)); + clen = CMSG_SPACE(installed * sizeof(int)); + if (clen > m.msg_controllen) + clen = CMSG_LEN(installed * sizeof(int)); + if (copy_to_user((addr_t)m.msg_control, cbuf, clen) < 0) + return -EFAULT; // ruled out above, but not assumed + } + } + + // The address, for the family that has one. Same rule as recvfrom: a Unix + // socket reports a length of zero rather than a sockaddr full of nothing. + if (m.msg_name) { + uint32 alen = 0; + if (sock_domain(s) == AF_INET) { + struct sockaddr_in sa; + memset(&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_addr = ip; + sa.sin_port = port_to_net(port); + alen = (uint32)sizeof(sa); + if (alen > m.msg_namelen) + alen = m.msg_namelen; + if (alen && copy_to_user((addr_t)m.msg_name, &sa, alen) < 0) + return -EFAULT; + } + if (copy_to_user(umsg + __builtin_offsetof(struct msghdr, msg_namelen), + &alen, sizeof(alen)) < 0) + return -EFAULT; + } + + if (copy_to_user(umsg + __builtin_offsetof(struct msghdr, msg_controllen), + &clen, sizeof(clen)) < 0) + return -EFAULT; + if (copy_to_user(umsg + __builtin_offsetof(struct msghdr, msg_flags), + &mflags, sizeof(mflags)) < 0) + return -EFAULT; + return (int)got; +} diff --git a/kernel/sys/table.inc b/kernel/sys/table.inc index 722a4d1..6210d02 100644 --- a/kernel/sys/table.inc +++ b/kernel/sys/table.inc @@ -140,6 +140,10 @@ SYSCALL(192, 55, getsockopt) // takes three arguments, so reading a fourth register from an accept() caller // reads whatever it last left there and invents a flag out of stack litter. SYSCALL(198, 288, accept4) +// sendmsg/recvmsg exist for the one thing only they can do: carry a descriptor +// from one process to another (SCM_RIGHTS). docs/plan/foundations.md item 1. +SYSCALL(199, 46, sendmsg) +SYSCALL(200, 47, recvmsg) // --- time --- SYSCALL( 28, 37, alarm) diff --git a/lib/usys.S b/lib/usys.S index d696293..ca0966e 100644 --- a/lib/usys.S +++ b/lib/usys.S @@ -117,6 +117,8 @@ SYSCALL(sockshutdown) SYSCALL(socketpair) SYSCALL(setsockopt) SYSCALL(getsockopt) +SYSCALL(sendmsg) +SYSCALL(recvmsg) SYSCALL(mount) SYSCALL(getdents64) diff --git a/projects/schcom/README.md b/projects/schcom/README.md index 1f558f0..4108a50 100644 --- a/projects/schcom/README.md +++ b/projects/schcom/README.md @@ -54,7 +54,7 @@ make schcom-test # boots it and checks the pixels | Framebuffer geometry via the fbdev ioctls | done | | Unix domain sockets | done | | Raw input events — key down/up with modifiers | done | -| `SCM_RIGHTS` | **not implemented** — there is no `sendmsg`/`recvmsg` at all | +| `SCM_RIGHTS` | done — `sendmsg`/`recvmsg`, see [`docs/architecture/networking.md`](../../docs/architecture/networking.md#descriptor-passing) | | Mode setting and double buffering | partial — virtio-gpu binds, no modeset API yet | ## Design notes @@ -90,9 +90,13 @@ id passed as an integer, where Wayland passes a file descriptor over `SCM_RIGHTS`. `struct schcom_surface` carries a `handle_kind` field precisely so that switching would be a new value rather than a protocol break. -**That field cannot save us, and this README used to claim `SCM_RIGHTS` was -done.** It is not — there is no `sendmsg`/`recvmsg` in the kernel at all. And -Wayland's `wl_shm_create_pool` passes a descriptor with no alternative form, so -*our* protocol being flexible is not the flexibility that is needed. Adding -descriptor passing is item 1 of -[`docs/plan/foundations.md`](../../docs/plan/foundations.md). +**That field could not have saved us.** Wayland's `wl_shm_create_pool` passes a +descriptor with no alternative form, so *our* protocol being the flexible one +was never the flexibility that was needed — and for a long time this README +claimed `SCM_RIGHTS` was done when there was no `sendmsg`/`recvmsg` in the +kernel at all. An entire GUI plan was drawn on top of that line. + +It is true now. Item 1 of +[`docs/plan/foundations.md`](../../docs/plan/foundations.md) is built and +tested; what a buffer still needs is item 2, a shareable memory object, because +a descriptor for a System V segment is not a thing that exists.