SchOS/kernel/sys/sys_net.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

583 lines
17 KiB
C

// Socket syscalls.
//
// The kernel side is in kernel/net/unix.c; this file is only the translation
// between a syscall's user-supplied `struct sockaddr` and the path string the
// socket layer actually wants, plus the file-descriptor bookkeeping.
//
// Two families now. AF_UNIX is answered by kernel/net/unix.c, AF_INET by
// kernel/net/inet.c on top of lwIP; the socket layer dispatches on the domain
// the socket was created with, so this file's only extra job is to read the
// right shape of sockaddr out of user memory. AF_INET6 is still refused with
// EAFNOSUPPORT rather than EINVAL, because a program that probes for IPv6 and
// falls back to IPv4 needs to tell "not here" from "you passed nonsense".
#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 "syscall.h"
#include "uaccess.h"
#include "errno.h"
#include "socket.h"
// Pull the sun_path out of a user `struct sockaddr_un`, NUL-terminated.
//
// The length the caller gives is checked rather than trusted: musl passes
// `offsetof(sockaddr_un, sun_path) + strlen(path) + 1`, which is shorter than
// the full struct, so reading sizeof(struct sockaddr_un) from user memory
// would fault on a path that sits near the end of a page.
static int
fetch_sun_path(addr_t uaddr, int ulen, char *out)
{
struct sockaddr_un sa;
int plen;
if (ulen < (int)sizeof(sa.sun_family) || ulen > (int)sizeof(sa))
return -EINVAL;
memset(&sa, 0, sizeof(sa));
if (copy_from_user(&sa, uaddr, (uint64)ulen) < 0)
return -EFAULT;
if (sa.sun_family == AF_INET)
return -EAFNOSUPPORT;
if (sa.sun_family != AF_UNIX)
return -EAFNOSUPPORT;
plen = ulen - (int)sizeof(sa.sun_family);
if (plen <= 0)
return -EINVAL;
if (plen > UNIX_PATH_MAX - 1)
plen = UNIX_PATH_MAX - 1;
memmove(out, sa.sun_path, (uint)plen);
out[plen] = '\0';
// A NUL inside the copied region is fine -- it just ends the path early,
// which is what a caller passing the full struct length does.
out[UNIX_PATH_MAX - 1] = '\0';
return 0;
}
// Wrap an established socket in a file and a descriptor. Consumes the
// socket's reference on success; on failure the caller still owns it.
//
// `sflags` carries SOCK_CLOEXEC and SOCK_NONBLOCK, which socket(), socketpair()
// and accept4() all take and all used to discard.
//
// Discarding them is not harmless in either direction. A dropped SOCK_CLOEXEC
// leaks the descriptor across every exec the process makes -- so a server that
// forks a child holds its listening socket open in it, and the port stays bound
// after the server dies. A dropped SOCK_NONBLOCK is worse: the caller believes
// it has a non-blocking socket, does not select() first, and blocks forever in
// what it thinks is a poll loop.
//
// Both are one line each, and they were masked off with a comment saying they
// were not honoured -- which is exactly the class of quiet lie the rest of this
// file has just been cleaned of.
static int
sock_to_fd(struct socket *s, int sflags)
{
struct file *f;
int fd;
if ((f = filealloc()) == 0)
return -ENFILE;
if ((fd = fdalloc(f)) < 0) {
f->ref = 0;
return -EMFILE;
}
f->type = FD_SOCKET;
f->sock = s;
f->readable = 1;
f->writable = 1;
f->off = 0;
f->nonblock = (sflags & SOCK_NONBLOCK) != 0;
if (sflags & SOCK_CLOEXEC)
myproc()->ofile_flags[fd] |= FD_CLOEXEC;
return fd;
}
// Resolve a descriptor to the socket behind it.
static int
argsock(int n, struct socket **ps)
{
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;
return 0;
}
SYSCALL_DEFINE(socket)
{
int domain, type, protocol;
struct socket *s;
int fd;
argint(0, &domain);
argint(1, &type);
argint(2, &protocol);
if (domain != AF_UNIX && domain != AF_INET && domain != AF_NETLINK)
return -EAFNOSUPPORT;
// SOCK_CLOEXEC / SOCK_NONBLOCK ride in the high bits of `type`. Split them
// off rather than discarding them: the type check below wants the low byte,
// and sock_to_fd wants the flags.
int sflags = type & (SOCK_CLOEXEC | SOCK_NONBLOCK);
if (type & ~(0xFF | SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= 0xFF;
if (domain == AF_UNIX && type != SOCK_STREAM)
return -EPROTOTYPE;
if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM)
return -EPROTOTYPE;
// AF_NETLINK takes SOCK_RAW or SOCK_DGRAM and its own protocol numbering;
// socknew checks both, because which protocols exist is the socket layer's
// business rather than this one's.
if (domain == AF_NETLINK && type != SOCK_RAW && type != SOCK_DGRAM)
return -EPROTOTYPE;
// Protocol 0 means "the default for this type". The one non-default we
// support is a ping socket -- SOCK_DGRAM with IPPROTO_ICMP, Linux's way of
// letting an unprivileged process do the one thing raw sockets were used
// for. Anything else is still refused rather than quietly given a socket
// that speaks a different protocol than the caller asked for.
if (protocol != 0) {
if (!(domain == AF_INET && type == SOCK_DGRAM &&
protocol == IPPROTO_ICMP))
return -EPROTONOSUPPORT;
}
// ENETDOWN rather than ENFILE when the stack itself is not running: the
// old blanket ENFILE said "file table overflow" for a machine with no
// network, which is a long way from the truth and a longer way from the
// fix.
if (domain == AF_INET && !net_is_ready())
return -ENETDOWN;
if ((s = socknew(domain, type, protocol)) == 0)
return -ENFILE;
if ((fd = sock_to_fd(s, sflags)) < 0) {
sockclose(s);
return fd;
}
return fd;
}
// Pull an address and port out of a user `struct sockaddr_in`.
//
// Both come back in network byte order and stay that way: lwIP wants them that
// way, so converting here and back again would be two swaps that cancel.
static int
fetch_sin(addr_t uaddr, int ulen, uint *addr, ushort *port)
{
struct sockaddr_in sa;
if (ulen < (int)sizeof(sa))
return -EINVAL;
if (copy_from_user(&sa, uaddr, sizeof(sa)) < 0)
return -EFAULT;
if (sa.sin_family != AF_INET)
return -EAFNOSUPPORT;
*addr = sa.sin_addr;
*port = sa.sin_port;
return 0;
}
// Network byte order to host, for a port. The kernel is little-endian and the
// wire is big-endian; lwIP's own ntohs is not reachable from here without
// dragging its headers into the syscall layer.
static ushort
port_from_net(ushort p)
{
return (ushort)(((p & 0xFF) << 8) | ((p >> 8) & 0xFF));
}
SYSCALL_DEFINE(bind)
{
struct socket *s;
addr_t uaddr;
int ulen, r;
char path[UNIX_PATH_MAX];
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &uaddr);
argint(2, &ulen);
if (sock_domain(s) == AF_INET) {
uint ip;
ushort port;
if ((r = fetch_sin(uaddr, ulen, &ip, &port)) < 0)
return r;
return sockbind_in(s, ip, port_from_net(port));
}
if ((r = fetch_sun_path(uaddr, ulen, path)) < 0)
return r;
return sockbind(s, path);
}
SYSCALL_DEFINE(listen)
{
struct socket *s;
int backlog, r;
if ((r = argsock(0, &s)) < 0)
return r;
argint(1, &backlog);
return socklisten(s, backlog);
}
SYSCALL_DEFINE(connect)
{
struct socket *s;
addr_t uaddr;
int ulen, r;
char path[UNIX_PATH_MAX];
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &uaddr);
argint(2, &ulen);
if (sock_domain(s) == AF_INET) {
uint ip;
ushort port;
if ((r = fetch_sin(uaddr, ulen, &ip, &port)) < 0)
return r;
return sockconnect_in(s, ip, port_from_net(port));
}
if ((r = fetch_sun_path(uaddr, ulen, path)) < 0)
return r;
return sockconnect(s, path);
}
// accept(2) and accept4(2) share everything but the flags word.
//
// They are two table entries rather than one with an alias, and the reason is
// the argument count: accept takes three, so a handler that read a fourth
// register would be reading whatever the caller happened to leave there and
// would invent a SOCK_NONBLOCK out of stack litter. That is precisely the bug
// the old `SYSCALL_ALIAS(288, accept)` would have grown the moment the flags
// started being honoured.
static addr_t
do_accept(int sflags)
{
struct socket *s, *c;
addr_t uaddr, ulenp;
int r, fd;
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &uaddr);
argaddr(2, &ulenp);
if ((c = sockaccept(s)) == 0)
return -EINVAL;
if ((fd = sock_to_fd(c, sflags)) < 0) {
sockclose(c);
return fd;
}
// The peer address of a Unix socket that never bound a name is the empty
// one: family AF_UNIX and a zero-length path. Reporting it is optional
// (both pointers may be null) but a caller that asks must not be left
// with an uninitialised struct it then prints.
if (uaddr && ulenp) {
struct sockaddr_un sa;
int len = (int)sizeof(sa.sun_family);
int want = 0;
memset(&sa, 0, sizeof(sa));
sa.sun_family = AF_UNIX;
safestrcpy(sa.sun_path, sockname(c), UNIX_PATH_MAX);
len += (int)strlen(sa.sun_path) + 1;
if (get_user(want, ulenp) < 0)
return fd; // the fd is already live; do not fail it
if (want > len)
want = len;
if (want > 0)
copy_to_user(uaddr, &sa, (uint64)want);
put_user(len, ulenp);
}
return fd;
}
SYSCALL_DEFINE(accept) { return do_accept(0); }
SYSCALL_DEFINE(accept4)
{
int flags = 0;
argint(3, &flags);
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
return do_accept(flags);
}
SYSCALL_DEFINE(socketpair)
{
int domain, type, protocol;
addr_t usv;
struct socket *a, *b;
int fd0, fd1, r;
argint(0, &domain);
argint(1, &type);
argint(2, &protocol);
argaddr(3, &usv);
if (domain != AF_UNIX)
return -EAFNOSUPPORT;
int sflags = type & (SOCK_CLOEXEC | SOCK_NONBLOCK);
type &= 0xFF;
if (type != SOCK_STREAM)
return -EPROTOTYPE;
if ((r = sockpair(&a, &b)) < 0)
return r;
if ((fd0 = sock_to_fd(a, sflags)) < 0) {
sockclose(a);
sockclose(b);
return fd0;
}
if ((fd1 = sock_to_fd(b, sflags)) < 0) {
myproc()->ofile[fd0] = 0;
sockclose(a);
sockclose(b);
return fd1;
}
int sv[2] = { fd0, fd1 };
if (copy_to_user(usv, sv, sizeof(sv)) < 0) {
myproc()->ofile[fd0] = 0;
myproc()->ofile[fd1] = 0;
sockclose(a);
sockclose(b);
return -EFAULT;
}
return 0;
}
// Host byte order to network, for a port on its way back to userland. The
// inverse of port_from_net above, and the same two lines: a sockaddr always
// carries the port big-endian, whichever direction it is travelling.
static ushort
port_to_net(ushort p)
{
return (ushort)(((p & 0xFF) << 8) | ((p >> 8) & 0xFF));
}
// send/recv with no flags are read/write, and sendto/recvfrom are those plus an
// address. Implementing them here rather than leaving them ENOSYS saves every
// ported program from being rewritten to use read() and write().
SYSCALL_DEFINE(sendto)
{
struct socket *s;
addr_t ubuf, uaddr;
int len, flags, ulen, r;
uint ip = 0;
ushort port = 0;
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &ubuf);
argint(2, &len);
argint(3, &flags);
argaddr(4, &uaddr);
argint(5, &ulen);
if (len < 0)
return -EINVAL;
char *p = (char *)user_ptr(ubuf, (uint64)len);
if (len && !p)
return -EFAULT;
// A null address is send(), not an error: the socket already knows where
// it is pointed, which for TCP is the only possibility anyway.
if (uaddr) {
if ((r = fetch_sin(uaddr, ulen, &ip, &port)) < 0)
return r;
port = port_from_net(port);
}
return socksendto(s, p, len, ip, port);
}
SYSCALL_DEFINE(recvfrom)
{
struct socket *s;
addr_t ubuf, uaddr, ulenp;
int len, flags, r;
uint ip = 0;
ushort port = 0;
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &ubuf);
argint(2, &len);
argint(3, &flags);
argaddr(4, &uaddr);
argaddr(5, &ulenp);
if (len < 0)
return -EINVAL;
char *p = (char *)user_ptr(ubuf, (uint64)len);
if (len && !p)
return -EFAULT;
r = sockrecvfrom(s, p, len, &ip, &port);
if (r < 0 || uaddr == 0)
return r;
// The address is reported only for the family that has one to report. A
// Unix socket leaves ip at zero, and the honest answer there is a length of
// zero rather than a sockaddr full of nothing that a caller would read as
// 0.0.0.0:0 and believe.
int 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 = (int)sizeof(sa);
// Truncated rather than refused if the caller's buffer is smaller,
// which is what accept() and getsockname() do everywhere: the length
// written back tells them what they got.
int cap = alen;
if (ulenp && get_user(cap, ulenp) < 0)
return -EFAULT;
if (cap > alen)
cap = alen;
if (cap > 0 && copy_to_user(uaddr, &sa, (uint64)cap) < 0)
return -EFAULT;
}
if (ulenp && copy_to_user(ulenp, &alen, sizeof(alen)) < 0)
return -EFAULT;
return r;
}
// shutdown(2). Half-close is not modelled -- there is one buffer per
// direction and no per-direction flag -- so SHUT_WR and SHUT_RDWR both mean
// "the peer sees end-of-file", which is the behaviour programs depend on.
SYSCALL_DEFINE(sockshutdown)
{
struct socket *s;
int how, r;
if ((r = argsock(0, &s)) < 0)
return r;
argint(1, &how);
return sock_shutdown(s, how);
}
// setsockopt / getsockopt.
//
// These both used to succeed unconditionally -- setsockopt was `return 0;` and
// getsockopt wrote a zero for every option asked about. The comment said it was
// so that a server setting SO_REUSEADDR and treating failure as fatal would not
// fail.
//
// That trade is the wrong way round, and it is worth being precise about why,
// because "accept and ignore" looks like the kind thing to do. A caller that
// gets ENOPROTOOPT knows the option did not take effect and can decide what to
// do. A caller that gets success *changes its behaviour on the strength of it*:
// it sets SO_RCVTIMEO and then blocks forever in a read it believes will time
// out; it sets TCP_NODELAY and reports latency numbers that measure Nagle. The
// failure is silent, remote from the call, and indistinguishable from a bug in
// the protocol stack.
//
// So the rule here is the one the rest of this kernel already follows: do it,
// or say you did not. The four options below are genuinely applied to lwIP's
// pcb; the readable ones report the truth; everything else is ENOPROTOOPT.
//
// SO_REUSEADDR is honoured rather than refused precisely because of the
// original comment's concern -- it is the one option a server really does set
// and really does check.
// Fetch the int a setsockopt call is passing. Every option handled here is an
// int; anything wanting a struct (SO_RCVTIMEO and friends) is refused above.
static int
opt_in(addr_t uval, int ulen, int *out)
{
if (!uval || ulen < (int)sizeof(int))
return -EINVAL;
return get_user(*out, uval) < 0 ? -EFAULT : 0;
}
// Hand one int back, and report how many bytes that was.
static int
opt_out(addr_t uval, addr_t ulenp, int val)
{
int want = 0;
if (!uval || !ulenp)
return -EFAULT;
if (get_user(want, ulenp) < 0)
return -EFAULT;
if (want < (int)sizeof(int))
return -EINVAL;
if (put_user(val, uval) < 0)
return -EFAULT;
want = sizeof(int);
return put_user(want, ulenp) < 0 ? -EFAULT : 0;
}
SYSCALL_DEFINE(setsockopt)
{
struct socket *s;
addr_t uval;
int level, name, ulen, val, r;
if ((r = argsock(0, &s)) < 0)
return r;
argint(1, &level);
argint(2, &name);
argaddr(3, &uval);
argint(4, &ulen);
if ((r = opt_in(uval, ulen, &val)) < 0)
return r;
return sockopt_set(s, level, name, val);
}
SYSCALL_DEFINE(getsockopt)
{
struct socket *s;
addr_t uval, ulenp;
int level, name, val = 0, r;
if ((r = argsock(0, &s)) < 0)
return r;
argint(1, &level);
argint(2, &name);
argaddr(3, &uval);
argaddr(4, &ulenp);
if ((r = sockopt_get(s, level, name, &val)) < 0)
return r;
return opt_out(uval, ulenp, val);
}