SchOS/kernel/net/unix.c
Nexoniarz f395c39704 net: AF_NETLINK, so getifaddrs() works for everything and not just one program
fastfetch reported "getifaddrs(&ifAddrStruct) failed", and the right answer to
that is not a patch to fastfetch. Asking what interfaces a machine has is
something every networked program does, and on Linux there is exactly one way:
a NETLINK_ROUTE socket. musl implements getifaddrs() over it, so a libc call
that looks like a syscall is really a small protocol conversation -- and without
this side of it every caller fails the same way.

kernel/net/netlink.c is a responder rather than a general socket family. No
event stream, no multicast groups, nothing subscribes to link state: a request
arrives, the whole answer is generated into the socket's buffer, and reads drain
it. That shape is chosen, not a shortcut -- getifaddrs sends a dump and reads
until NLMSG_DONE with MSG_DONTWAIT, so generating at send time means a read
never blocks and the non-blocking flag is honoured for free.

RTM_GETLINK returns one RTM_NEWLINK per interface with IFLA_IFNAME and
IFLA_ADDRESS; RTM_GETADDR returns one RTM_NEWADDR per address with IFA_ADDRESS,
IFA_LOCAL and IFA_LABEL. Anything else gets a bare NLMSG_DONE, because the
caller's next move is a read either way and an unexpected error message is worse
than nothing to read.

The data is not netlink's. Interfaces come from the netdev class and addresses
from lwIP's netifs, exactly as /proc/netif reads them; nothing in the file knows
what an e1000 is. Interface indices matter more than they look -- getifaddrs
matches address records to link records by index, so the two dumps have to
agree. Loopback is 1, netdevs follow from 2.

Verified by reading it back through musl rather than by inspecting the bytes:

    lo     flags=0x49    fam=-1
    eth0   flags=0x1043  fam=17                    (AF_PACKET, the MAC)
    lo     flags=0x49    fam=2  addr=127.0.0.1  mask=255.0.0.0
    eth0   flags=0x1043  fam=2  addr=10.0.2.15  mask=255.255.255.0

Correct flags, addresses and netmasks, and the link records pair with the
address records.

One thing this does not close: fastfetch's Local IP module defaults to
default-route-only, which calls ffNetifGetDefaultRouteIfName() and reads
/proc/net/route. SchOS has no routing table and no such file, so the module
still shows nothing -- now for a reason that is about routing rather than about
enumeration. That is the next file to provide.

socktest still passes; the AF_UNIX and AF_INET paths are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-12 19:23:34 +02:00

714 lines
20 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);
}
// ---------------------------------------------------------------------------
// Socket options
//
// Only the options that are genuinely honoured appear here; everything else is
// answered ENOPROTOOPT by the switch's default. That is deliberate and it is a
// change of policy: both calls used to succeed unconditionally, which is the
// worst of the three possible answers. A caller told "no" adapts; a caller told
// "yes" falsely proceeds on the strength of it -- setting SO_RCVTIMEO and then
// blocking forever in a read it believes will time out.
//
// The flags are recorded here as well as pushed into lwIP so that getsockopt
// has one answer rather than two, and so that an AF_UNIX socket -- which has no
// pcb and for which none of these mean anything -- can still give back what was
// set, as POSIX requires.
int
sockopt_set(struct socket *s, int level, int name, int val)
{
int r = 0;
acquire(&stab.lock);
if (level == SOL_SOCKET) {
switch (name) {
case SO_REUSEADDR: s->o_reuseaddr = !!val; break;
case SO_KEEPALIVE: s->o_keepalive = !!val; break;
case SO_BROADCAST: s->o_broadcast = !!val; break;
// Accepted and left at the size we have. Not the same lie as before:
// Linux is explicitly free to clamp these, and sockopt_get below reports
// the real figure rather than the requested one.
case SO_SNDBUF:
case SO_RCVBUF: release(&stab.lock); return 0;
default: release(&stab.lock); return -ENOPROTOOPT;
}
} else if (level == IPPROTO_TCP && name == TCP_NODELAY) {
if (s->type != SOCK_STREAM) { release(&stab.lock); return -EINVAL; }
s->o_nodelay = !!val;
} else {
release(&stab.lock);
return -ENOPROTOOPT;
}
int inet = (s->domain == AF_INET);
void *h = s->inet;
release(&stab.lock);
// Outside the table lock: inet_setopt takes the lwIP lock, which is a
// sleeplock, and the same ordering rule the rest of this file follows.
if (inet && h)
r = inet_setopt(h, level, name, val);
return r;
}
int
sockopt_get(struct socket *s, int level, int name, int *out)
{
int r = 0;
acquire(&stab.lock);
if (level == IPPROTO_TCP && name == TCP_NODELAY) {
*out = s->o_nodelay;
} else if (level != SOL_SOCKET) {
r = -ENOPROTOOPT;
} else {
switch (name) {
case SO_TYPE: *out = s->type; break;
case SO_ACCEPTCONN: *out = (s->state == SS_LISTENING); break;
case SO_REUSEADDR: *out = s->o_reuseaddr; break;
case SO_KEEPALIVE: *out = s->o_keepalive; break;
case SO_BROADCAST: *out = s->o_broadcast; break;
// The real buffer, not a number somebody asked for.
case SO_SNDBUF:
case SO_RCVBUF: *out = SOCKBUF; break;
// An honest zero rather than the blanket one it replaces: connect() here
// is blocking and reports failure as its own return value, so there is no
// deferred error for SO_ERROR to carry. It becomes a real field the day a
// non-blocking connect exists.
case SO_ERROR: *out = 0; break;
default: r = -ENOPROTOOPT; break;
}
}
release(&stab.lock);
return r;
}
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);
// Netlink never blocks: the answer was generated when the request was sent,
// so an empty buffer means the caller has already drained this dump rather
// than that more is coming. Blocking here would hang a getifaddrs() that
// asked one question too many.
if(s->domain == AF_NETLINK){
acquire(&stab.lock);
uint have = s->nwrite - s->nread;
if(have == 0){
release(&stab.lock);
return -EAGAIN;
}
if((uint)n > have)
n = (int)have;
memmove(addr, s->data + s->nread, (uint)n);
s->nread += (uint)n;
release(&stab.lock);
return 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);
// A netlink request is answered immediately, into this socket's own buffer
// rather than a peer's -- there is no peer. Doing the work here is what lets
// the read that follows never block, which matters because musl reads with
// MSG_DONTWAIT and treats a short read as the end of the dump.
if(s->domain == AF_NETLINK){
acquire(&stab.lock);
s->nread = s->nwrite = 0;
int got = netlink_request(addr, (uint)n, s->data, SOCKBUF);
if(got > 0)
s->nwrite = (uint)got;
release(&stab.lock);
return n; /* the request was consumed whole */
}
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, int proto)
{
struct socket *s;
if(domain != AF_UNIX && domain != AF_INET && domain != AF_NETLINK)
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;
// AF_NETLINK carries whole messages and musl asks for SOCK_RAW; SOCK_DGRAM is
// accepted too because Linux does. NETLINK_ROUTE is the only protocol served
// -- see kernel/net/netlink.c, which answers RTM_GETLINK and RTM_GETADDR and
// nothing else.
if(domain == AF_NETLINK){
if(type != SOCK_RAW && type != SOCK_DGRAM)
return 0;
if(proto != NETLINK_ROUTE)
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, proto);
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);
}