The second of the two things the stack owed. recvfrom accepted the address arguments and ignored them, so every caller was told nothing about where a datagram came from -- which for UDP is most of the point: a server that cannot see the sender cannot reply to it. Fixing it properly meant fixing something worse that was sitting underneath. UDP payloads went into the same byte ring TCP uses, with no record of where one datagram ended and the next began, so two datagrams that arrived together came back from one read as one. That is a bug whose appearance depends on timing and whose blame lands on the sender. Datagrams now carry a header alongside the bytes: source address, source port, length. Sixteen of them per socket, in a ring beside the existing one. Whole buffers would have been 16 sockets x 16 datagrams x an MTU of static memory for something almost always empty; only the boundaries need keeping, and those are eight bytes each. A datagram too big for the buffer it is read into is truncated and the remainder discarded, rather than left in the ring to be delivered as the head of the next one. One that arrives with the queue full is dropped whole -- that is UDP's own contract, and half a datagram in the ring would corrupt every one after. An empty queue and a zero-length datagram are kept apart, because a sender really can send zero bytes and the right answer to that is 0 rather than "nothing has arrived yet". sendto grew the mirror image: a destination address on a datagram socket, which is what a server replying to whoever just wrote to it needs. On TCP a destination is ignored rather than refused -- the connection decides where the bytes go, and programs pass one out of habit. For a stream, recvfrom's source is read from the pcb: it is the peer, and it is the same answer for every byte. Verified: bootcheck -Net now covers UDP too, 45 [ok] and no failures. The address check is real rather than cosmetic -- nettest deliberately leaves its sockaddr uninitialised, so a kernel that filled in nothing would print stack rubbish instead of 10.0.2.2 and the port the responder is actually on. The boundaries are checked by byte count: the responder sends 20 bytes and then 16, and the guest reports reading 20 and then 16. Treated as a stream the first read would have taken all 36. Left for later, and deliberately: accept() still does not report the peer's address, and getsockname/getpeername do not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
589 lines
15 KiB
C
589 lines
15 KiB
C
// Unix domain sockets.
|
|
//
|
|
// A connected pair is two buffers, not one. Writing to a socket appends to
|
|
// its *peer's* buffer and reading drains its own, which is what makes a
|
|
// socket bidirectional where a pipe is not, without needing two of anything.
|
|
//
|
|
// **One lock covers the whole table**, rather than a lock per socket. A pipe
|
|
// gets away with a lock per pipe because every operation touches exactly one
|
|
// object; a socket operation touches two, and two-object locking needs an
|
|
// ordering rule that every path obeys or it deadlocks. Worse, the wakeup
|
|
// races are subtle: a reader that tests `peer_closed` under its own lock while
|
|
// the closer sets it under the peer's has no interlock at all, and sleeps
|
|
// through the wakeup that was meant for it. With one lock the condition and
|
|
// the sleep are atomic with respect to every writer of that condition, which
|
|
// is the property sleep/wakeup actually requires. Thirty-two sockets carrying
|
|
// GUI messages will not notice the contention.
|
|
//
|
|
// **Names are held in a table here, not in the filesystem.** A real Unix
|
|
// socket appears in a directory listing as a special file, and binding one
|
|
// creates that node. Doing that properly means a new inode type threaded
|
|
// through both filesystems, iget, dirlookup and stat -- a VFS change, not a
|
|
// socket change. The table gives every behaviour that matters: a client
|
|
// finds a server by a name it agreed in advance, which is the whole job.
|
|
// What is lost is `ls -l /tmp/schcom` showing an `s`, and `unlink()` of a
|
|
// socket path failing with ENOENT. Both are worth fixing when the VFS next
|
|
// gets opened up; neither blocks anything.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "mmu.h"
|
|
#include "proc.h"
|
|
#include "fs.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "file.h"
|
|
#include "posix.h"
|
|
#include "errno.h"
|
|
|
|
#define _KERNEL_SOCKET_INTERNALS
|
|
#include "socket.h"
|
|
|
|
static struct {
|
|
struct spinlock lock;
|
|
struct socket sock[NSOCK];
|
|
} stab;
|
|
|
|
void
|
|
sockinit(void)
|
|
{
|
|
initlock(&stab.lock, "socktab");
|
|
}
|
|
|
|
// Caller holds stab.lock.
|
|
static struct socket *
|
|
alloc_locked(int type)
|
|
{
|
|
struct socket *s;
|
|
|
|
for(s = stab.sock; s < stab.sock + NSOCK; s++){
|
|
if(s->state == SS_FREE && s->ref == 0){
|
|
memset(s, 0, sizeof(*s));
|
|
s->ref = 1;
|
|
s->type = type;
|
|
s->state = SS_UNBOUND;
|
|
return s;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Find a socket listening on this name. Caller holds stab.lock.
|
|
static struct socket *
|
|
find_listener(const char *name)
|
|
{
|
|
struct socket *s;
|
|
|
|
for(s = stab.sock; s < stab.sock + NSOCK; s++)
|
|
if(s->state == SS_LISTENING && strncmp(s->name, name, UNIX_PATH_MAX) == 0)
|
|
return s;
|
|
return 0;
|
|
}
|
|
|
|
// Detach s from its peer and tell the peer about it. Caller holds stab.lock.
|
|
static void
|
|
orphan_peer(struct socket *s)
|
|
{
|
|
struct socket *peer = s->peer;
|
|
|
|
if(peer == 0)
|
|
return;
|
|
peer->peer_closed = 1;
|
|
peer->peer = 0;
|
|
s->peer = 0;
|
|
|
|
// Both channels, because the peer may be blocked reading (waiting for data
|
|
// that will now never come) or writing (waiting for room in a buffer nobody
|
|
// will ever drain).
|
|
wakeup(&peer->nread);
|
|
wakeup(&peer->nwrite);
|
|
}
|
|
|
|
void
|
|
sockclose(struct socket *s)
|
|
{
|
|
int i;
|
|
|
|
if(s == 0)
|
|
return;
|
|
|
|
// An AF_INET socket's state lives in inet.c behind the lwIP lock, so its
|
|
// teardown happens there. The table entry below is freed either way.
|
|
if(s->domain == AF_INET){
|
|
acquire(&stab.lock);
|
|
int last = (--s->ref <= 0);
|
|
if(last)
|
|
s->state = SS_FREE;
|
|
release(&stab.lock);
|
|
if(last && s->inet){
|
|
inet_close(s->inet);
|
|
s->inet = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
s->nqueue = 0;
|
|
|
|
s->state = SS_FREE;
|
|
s->ref = 0;
|
|
wakeup(&s->nqueue); // an accept() blocked on this listener
|
|
release(&stab.lock);
|
|
}
|
|
|
|
void
|
|
sockdup(struct socket *s)
|
|
{
|
|
acquire(&stab.lock);
|
|
s->ref++;
|
|
release(&stab.lock);
|
|
}
|
|
|
|
const char *
|
|
sockname(struct socket *s)
|
|
{
|
|
return s->name;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Data
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int
|
|
sockread(struct socket *s, char *addr, int n)
|
|
{
|
|
int i;
|
|
|
|
if(n < 0)
|
|
return -EINVAL;
|
|
if(s->domain == AF_INET)
|
|
return inet_read(s->inet, addr, n);
|
|
|
|
acquire(&stab.lock);
|
|
while(s->nread == s->nwrite && !s->peer_closed){
|
|
if(myproc()->killed || s->state == SS_FREE){
|
|
release(&stab.lock);
|
|
return -EINTR;
|
|
}
|
|
sleep(&s->nread, &stab.lock);
|
|
}
|
|
|
|
for(i = 0; i < n; i++){
|
|
if(s->nread == s->nwrite)
|
|
break;
|
|
addr[i] = s->data[s->nread++ % SOCKBUF];
|
|
}
|
|
|
|
// Waking the writer matters even when nothing was read: it may be blocked
|
|
// on a full buffer that this call has just made room in.
|
|
wakeup(&s->nwrite);
|
|
release(&stab.lock);
|
|
return i;
|
|
}
|
|
|
|
int
|
|
sockwrite(struct socket *s, char *addr, int n)
|
|
{
|
|
struct socket *dst;
|
|
int i;
|
|
|
|
if(n < 0)
|
|
return -EINVAL;
|
|
if(s->domain == AF_INET)
|
|
return inet_write(s->inet, addr, n);
|
|
|
|
acquire(&stab.lock);
|
|
|
|
// Data goes into the peer's buffer, because that is where the peer will
|
|
// look for it. Same contract as a pipe with no reader: a signal, then an
|
|
// error. A process that writes to a dead connection and is not told about
|
|
// it spins forever believing it is making progress.
|
|
if(s->peer == 0){
|
|
release(&stab.lock);
|
|
sigsend(myproc()->pid, SIGPIPE);
|
|
return -EPIPE;
|
|
}
|
|
|
|
for(i = 0; i < n; i++){
|
|
// Re-read every iteration: the peer can go away while we sleep below.
|
|
dst = s->peer;
|
|
while(dst && dst->nwrite == dst->nread + SOCKBUF){
|
|
if(myproc()->killed){
|
|
release(&stab.lock);
|
|
return i > 0 ? i : -EINTR;
|
|
}
|
|
wakeup(&dst->nread);
|
|
sleep(&dst->nwrite, &stab.lock);
|
|
dst = s->peer;
|
|
}
|
|
if(dst == 0){
|
|
release(&stab.lock);
|
|
sigsend(myproc()->pid, SIGPIPE);
|
|
return i > 0 ? i : -EPIPE; // a short write is still a write
|
|
}
|
|
dst->data[dst->nwrite++ % SOCKBUF] = addr[i];
|
|
}
|
|
|
|
wakeup(&s->peer->nread);
|
|
release(&stab.lock);
|
|
return i;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Connection setup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct socket *
|
|
socknew(int domain, int type)
|
|
{
|
|
struct socket *s;
|
|
|
|
if(domain != AF_UNIX && domain != AF_INET)
|
|
return 0;
|
|
// AF_UNIX is stream-only here; AF_INET has datagrams too, and inet_new is
|
|
// what decides whether it can serve the type asked for.
|
|
if(domain == AF_UNIX && type != SOCK_STREAM)
|
|
return 0;
|
|
|
|
acquire(&stab.lock);
|
|
s = alloc_locked(type);
|
|
if(s)
|
|
s->domain = domain;
|
|
release(&stab.lock);
|
|
if(s == 0)
|
|
return 0;
|
|
|
|
if(domain == AF_INET){
|
|
// Outside the table lock: inet_new takes the lwIP lock, which is a
|
|
// sleeplock, and sleeping while holding a spinlock is a panic.
|
|
s->inet = inet_new(type);
|
|
if(s->inet == 0){
|
|
sockclose(s);
|
|
return 0;
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// A connected pair with no name and no listener, for socketpair(2).
|
|
int
|
|
sockpair(struct socket **pa, struct socket **pb)
|
|
{
|
|
struct socket *a, *b;
|
|
|
|
acquire(&stab.lock);
|
|
if((a = alloc_locked(SOCK_STREAM)) == 0){
|
|
release(&stab.lock);
|
|
return -ENFILE;
|
|
}
|
|
if((b = alloc_locked(SOCK_STREAM)) == 0){
|
|
a->state = SS_FREE;
|
|
a->ref = 0;
|
|
release(&stab.lock);
|
|
return -ENFILE;
|
|
}
|
|
a->peer = b; a->state = SS_CONNECTED;
|
|
b->peer = a; b->state = SS_CONNECTED;
|
|
release(&stab.lock);
|
|
|
|
*pa = a;
|
|
*pb = b;
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
sockbind(struct socket *s, const char *path)
|
|
{
|
|
struct socket *other;
|
|
|
|
if(path[0] == '\0')
|
|
return -EINVAL;
|
|
|
|
acquire(&stab.lock);
|
|
if(s->state != SS_UNBOUND){
|
|
release(&stab.lock);
|
|
return -EINVAL;
|
|
}
|
|
for(other = stab.sock; other < stab.sock + NSOCK; other++){
|
|
if(other != s && other->state >= SS_BOUND &&
|
|
strncmp(other->name, path, UNIX_PATH_MAX) == 0){
|
|
release(&stab.lock);
|
|
return -EADDRINUSE;
|
|
}
|
|
}
|
|
safestrcpy(s->name, path, UNIX_PATH_MAX);
|
|
s->state = SS_BOUND;
|
|
release(&stab.lock);
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
socklisten(struct socket *s, int backlog)
|
|
{
|
|
if(s->domain == AF_INET)
|
|
return inet_listen(s->inet, backlog);
|
|
|
|
(void)backlog; // the queue is a fixed NBACKLOG either way
|
|
|
|
acquire(&stab.lock);
|
|
if(s->state != SS_BOUND && s->state != SS_LISTENING){
|
|
release(&stab.lock);
|
|
return -EINVAL;
|
|
}
|
|
s->state = SS_LISTENING;
|
|
release(&stab.lock);
|
|
return 0;
|
|
}
|
|
|
|
// Returns the server-side socket of a new connection with a reference already
|
|
// held, or 0 on error.
|
|
struct socket *
|
|
sockaccept(struct socket *s)
|
|
{
|
|
struct socket *c;
|
|
int i;
|
|
|
|
if(s->domain == AF_INET){
|
|
// The connection lwIP hands back needs a table entry of its own, because
|
|
// everything above this layer works in `struct socket` and knows nothing
|
|
// about lwIP.
|
|
void *h = inet_accept(s->inet);
|
|
if(h == 0)
|
|
return 0;
|
|
acquire(&stab.lock);
|
|
c = alloc_locked(s->type);
|
|
if(c){
|
|
c->domain = AF_INET;
|
|
c->state = SS_CONNECTED;
|
|
}
|
|
release(&stab.lock);
|
|
if(c == 0){
|
|
inet_close(h);
|
|
return 0;
|
|
}
|
|
c->inet = h;
|
|
return c;
|
|
}
|
|
|
|
acquire(&stab.lock);
|
|
if(s->state != SS_LISTENING){
|
|
release(&stab.lock);
|
|
return 0;
|
|
}
|
|
|
|
while(s->nqueue == 0){
|
|
if(myproc()->killed || s->state != SS_LISTENING){
|
|
release(&stab.lock);
|
|
return 0;
|
|
}
|
|
sleep(&s->nqueue, &stab.lock);
|
|
}
|
|
|
|
c = s->queue[0];
|
|
for(i = 0; i + 1 < s->nqueue; i++)
|
|
s->queue[i] = s->queue[i+1];
|
|
s->nqueue--;
|
|
release(&stab.lock);
|
|
|
|
return c;
|
|
}
|
|
|
|
int
|
|
sockconnect(struct socket *s, const char *path)
|
|
{
|
|
struct socket *srv, *peer;
|
|
|
|
acquire(&stab.lock);
|
|
if(s->state != SS_UNBOUND && s->state != SS_BOUND){
|
|
release(&stab.lock);
|
|
return -EINVAL;
|
|
}
|
|
srv = find_listener(path);
|
|
if(srv == 0){
|
|
release(&stab.lock);
|
|
return -ECONNREFUSED;
|
|
}
|
|
if(srv->nqueue >= NBACKLOG){
|
|
release(&stab.lock);
|
|
return -EAGAIN;
|
|
}
|
|
|
|
// Every connection needs a socket at each end. The listener is not one of
|
|
// them -- it stays listening, and accept() hands back this new one.
|
|
peer = alloc_locked(s->type);
|
|
if(peer == 0){
|
|
release(&stab.lock);
|
|
return -ENFILE;
|
|
}
|
|
|
|
peer->peer = s;
|
|
peer->state = SS_CONNECTED;
|
|
s->peer = peer;
|
|
s->state = SS_CONNECTED;
|
|
|
|
srv->queue[srv->nqueue++] = peer;
|
|
wakeup(&srv->nqueue); // an accept() may be blocked on this
|
|
|
|
release(&stab.lock);
|
|
|
|
// Deliberately does *not* wait for accept(). On Unix a queued connection is
|
|
// already established: the client may write, and the bytes sit in the
|
|
// server-side buffer until somebody accepts and reads them. Blocking here
|
|
// instead would deadlock any server that connects to itself, and would make
|
|
// a busy server's backlog useless.
|
|
return 0;
|
|
}
|
|
|
|
// shutdown(2). There is one buffer per direction and no per-direction flag,
|
|
// so a half-close is approximated: SHUT_RD stops this end reading anything
|
|
// further, and SHUT_WR/SHUT_RDWR give the peer end-of-file. What programs
|
|
// actually use shutdown for -- "I have finished sending, read until EOF and
|
|
// then reply" -- works; what is missing is the ability to keep reading after
|
|
// SHUT_WR, since the connection is torn down rather than half-closed.
|
|
int
|
|
sock_shutdown(struct socket *s, int how)
|
|
{
|
|
acquire(&stab.lock);
|
|
if(s->state != SS_CONNECTED){
|
|
release(&stab.lock);
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
if(how == SHUT_RD || how == SHUT_RDWR){
|
|
s->peer_closed = 1;
|
|
wakeup(&s->nread);
|
|
}
|
|
if(how == SHUT_WR || how == SHUT_RDWR)
|
|
orphan_peer(s);
|
|
release(&stab.lock);
|
|
return 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Readiness, for poll/select
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int
|
|
sockreadable(struct socket *s)
|
|
{
|
|
if(s->domain == AF_INET)
|
|
return inet_readable(s->inet);
|
|
if(s->state == SS_LISTENING)
|
|
return s->nqueue > 0;
|
|
return s->nread != s->nwrite || s->peer_closed;
|
|
}
|
|
|
|
// The peer is gone and everything it sent has been read.
|
|
int
|
|
sock_hup(struct socket *s)
|
|
{
|
|
if(s->domain == AF_INET)
|
|
return inet_hup(s->inet);
|
|
|
|
if(s->state == SS_LISTENING)
|
|
return 0;
|
|
return s->peer == 0 && s->nread == s->nwrite;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* AF_INET addressing */
|
|
/* */
|
|
/* Separate entry points rather than an overloaded sockbind, because */
|
|
/* the two families do not name the same kind of thing: one takes a */
|
|
/* path, the other an address and a port. The syscall layer picks by */
|
|
/* the family in the sockaddr the caller passed. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
int
|
|
sockbind_in(struct socket *s, uint addr, ushort port)
|
|
{
|
|
if(s->domain != AF_INET)
|
|
return -EAFNOSUPPORT;
|
|
|
|
int r = inet_bind(s->inet, addr, port);
|
|
if(r == 0){
|
|
acquire(&stab.lock);
|
|
s->state = SS_BOUND;
|
|
release(&stab.lock);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
int
|
|
sockconnect_in(struct socket *s, uint addr, ushort port)
|
|
{
|
|
if(s->domain != AF_INET)
|
|
return -EAFNOSUPPORT;
|
|
|
|
int r = inet_connect(s->inet, addr, port);
|
|
if(r == 0){
|
|
acquire(&stab.lock);
|
|
s->state = SS_CONNECTED;
|
|
release(&stab.lock);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// Which family a socket belongs to. An accessor because kernel/sys/sys_net.c is
|
|
// the translation layer and deliberately does not see inside struct socket --
|
|
// it works in descriptors and user addresses, not in the socket's guts.
|
|
int
|
|
sock_domain(struct socket *s)
|
|
{
|
|
return s ? s->domain : 0;
|
|
}
|
|
|
|
// recvfrom and sendto, which differ from read and write only in carrying an
|
|
// address. Both live here rather than in sys_net.c for the same reason as
|
|
// sock_domain: the syscall layer must not know which side of the socket union
|
|
// is live.
|
|
//
|
|
// `port` comes back in host byte order, matching what sockbind_in and
|
|
// sockconnect_in take. The syscall layer converts on the way out, in the one
|
|
// place that also builds the sockaddr.
|
|
int
|
|
sockrecvfrom(struct socket *s, char *buf, int n, uint *addr, ushort *port)
|
|
{
|
|
if(n < 0)
|
|
return -EINVAL;
|
|
if(s->domain == AF_INET)
|
|
return inet_recvfrom(s->inet, buf, n, addr, port);
|
|
|
|
// A Unix socket has no address of this shape. Saying so by leaving the
|
|
// caller's address untouched, rather than inventing one, is what lets
|
|
// recvfrom report an address length of zero -- which is exactly what a
|
|
// program reading from an unnamed peer expects.
|
|
return sockread(s, buf, n);
|
|
}
|
|
|
|
int
|
|
socksendto(struct socket *s, char *buf, int n, uint addr, ushort port)
|
|
{
|
|
if(n < 0)
|
|
return -EINVAL;
|
|
if(s->domain == AF_INET)
|
|
return inet_sendto(s->inet, buf, n, addr, port);
|
|
if(addr)
|
|
return -EAFNOSUPPORT; // an AF_INET destination on an AF_UNIX socket
|
|
return sockwrite(s, buf, n);
|
|
}
|