SchOS/kernel/net/unix.c
Nexoniarz c0806d1b64 net: a TCP/IP stack, and AF_INET on the sockets that were already there
lwIP 2.2.0, fetched by ports/lwip and compiled straight into the kernel.
Not written from scratch, and the reason is not the packet formats: it is
congestion control, retransmission, window management and twenty years of
corner cases that only show up against a real peer. Not FreeBSD's
netinet either, though the licence would have allowed it -- that stack is
not a library, it reaches into mbufs, epoch(9), netisr, VNET and uma(9),
so adopting it means porting FreeBSD's kernel infrastructure first. lwIP
was written for exactly this situation: a small kernel with a raw
Ethernet driver and a port layer of a few hundred lines.

It is kernel code, so nothing is installed into the sysroot and there is
no second build system: the top-level Makefile compiles the files it
wants against our port headers. The file list is explicit rather than a
wildcard, because lwIP ships IPv6, 6LoWPAN, PPP, SLIP, DNS-SD and a dozen
apps in the same tree, and building what is not configured yields either
dead code or a link error naming a port function nobody meant to write.
Its sources are unmodified and its warnings are silenced for its files
only -- patching them would mean maintaining a fork.

The port layer is ours: a lock, a clock, the netif that hands frames to
e1000, lwipopts.h, and a shim/ directory of tiny headers so lwIP's
<string.h> and friends resolve to the kernel's functions instead of the
host's, whose prototypes disagree and would otherwise be found on the
default include path.

NO_SYS=1, so lwIP has no threads of its own and one kernel thread feeds
it. A thread rather than the interrupt handler, because a call into lwIP
can reach the driver, the driver can sleep, and an interrupt handler
cannot -- so the interrupt still only clears the cause register and the
thread polls. kthread_create is new and generally useful: a process with
a kernel-only page directory, no files, no cwd, no parent, dispatched
from forkret, which can therefore sleep and be preempted like anything
else. DHCP obtains an address at boot and the console says which one.

AF_INET went in exactly where phase 2 said it would. socknew dispatches
on the domain, kernel/net/inet.c implements the internet side on lwIP's
raw API, and not one syscall changed shape: bind, connect, listen,
accept, read and write reach the same descriptors they always did. TCP
and UDP both work. cmd/nettest opens a connection, writes, and reads the
answer back.

Two things are owed and named here so they are not discovered later:
recvfrom does not yet report the source address, and the socket paths
poll on the timer tick instead of sleeping on the socket. Both are
follow-ups, not redesigns.

NOTICE carries lwIP's BSD-3 text. Clause 3 is no-endorsement, not the
old advertising clause, so it sits with Apache-2.0 without difficulty.

Verified: the tree builds clean, and the headless boot check passes with
27 [ok] and no failures with the stack compiled in and the thread
running -- which is the part that would break first if netinit deadlocked
or the thread starved anything. An end-to-end connection test belongs in
that suite and is the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:02:48 +02:00

554 lines
14 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;
}