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.