SchOS/include/socket.h
Nexoniarz 3f13989883 unix: a bound socket is a file
Gate 1, item 3, and the last of foundations.md.

bind() creates a T_SOCK node at the path; connect() resolves the path and looks
for the socket bound to **that inode**. Matching on the inode rather than on
the path *string* is the part that matters: the name used to live only in the
socket table, so two spellings of one path missed each other, a relative path
never matched an absolute one, and there was nothing to stat() or unlink().
An inode is the thing a path resolves *to*, so connect() can walk the path
however it likes and arrive at the same socket.

    # ls -l /tmp
    srwxr-xr-x 1 root root 0 2026-09-22 08:04 socktest

The node is a marker, not the binding. It holds no data and the live
association stays in the socket table, so a socket file left by a crash is
what Linux leaves behind -- a dead file that connect() refuses until something
binds again -- and unlink()ing a live socket's path does not disturb anyone
already connected.

EADDRINUSE comes from the *directory* rather than from a scan of the table,
deliberately: a stale socket file has to refuse a rebind for the same reason a
running server's does, or two servers both believe they own it. The creation
lives in sys_bind() and not in unix.c, because that is the layer with both --
kernel/net/unix.c knows about sockets and has no business calling create().

T_SOCK is a compat addition to SchFS by the rule in schfs.h: a driver that does
not know SCHFS_T_SOCK maps it to a zero-length regular file, so nothing goes
stale and nothing becomes unreadable.

cmd/socktest section 15 asserts the plan's own wording -- bind, stat, see an
`s`, unlink -- plus the two a name table could never fail: a second bind to a
live path is refused, and connecting to a path that is a *regular file* is
refused rather than matched by name.

**And the semantics being real broke socktest on its second run**, correctly:
the socket files persisted and every bind() hit EADDRINUSE. Every real server
unlinks before binding; socktest now does too, which is the change the new
behaviour demands rather than a workaround for it.

Chasing that turned up something worse. `socktest` reported rc=1 with every
check passing, and so did every other first-party program:

  - **lib/start.S called main and threw %eax away.** sys_exit() reads the
    status from %rdi and nothing put it there, so every program exited with
    whatever that register happened to hold. Every test suite ends in
    `return failures ? 1 : 0` and none of it reached the shell.
  - **exit(void) passed no status either**, for the same reason: the
    declaration takes no argument, so %rdi was the caller's litter.

`if socktest; then` works now, and a failing command reports 1 -- checked both
ways, because a fix that makes everything return 0 would look identical from
the success side.

Also factored: unlink_path() out of sys_unlink, so a failed bind can take its
own node back out without going through the syscall entry, and create_sock()
as a named door rather than exporting create() with an arbitrary type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 10:06:10 +02:00

357 lines
13 KiB
C

#pragma once
#include "types.h"
// Unix domain sockets.
//
// These exist for Schcom. A compositor and its clients have to exchange
// messages, and a pipe cannot do it: a pipe is one-directional, has no
// notion of a connection, and gives no way for a server to be *found* by a
// client that starts later. A socket with a name in the filesystem is
// exactly the missing piece.
//
// AF_INET arrived later, and exactly as planned: underneath the same syscalls
// rather than reshaping them. A socket carries its domain and every entry point
// dispatches on it -- kernel/net/unix.c for AF_UNIX, kernel/net/inet.c (lwIP)
// for AF_INET.
#define AF_UNIX 1
#define AF_LOCAL 1
#define AF_INET 2
// Linux's number, because a musl program passes Linux's constant. getifaddrs()
// is implemented over this, which is why it exists here: enumerating interfaces
// is something every networked program does and nothing else offers.
#define AF_NETLINK 16
#define PF_NETLINK AF_NETLINK
#define NETLINK_ROUTE 0
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3
// Flags ORed into socket()'s and accept4()'s `type`/`flags` argument. Linux's
// values, because a musl program passes Linux's constants.
#define SOCK_NONBLOCK 0x800 // O_NONBLOCK
#define SOCK_CLOEXEC 0x80000 // O_CLOEXEC
// setsockopt/getsockopt levels and names, Linux's numbering.
//
// Only what is listed here is accepted; anything else is answered with
// ENOPROTOOPT rather than a success it did not earn. See the comment above
// sys_setsockopt for why that is worth the risk of failing a caller that never
// checked before.
#define SOL_SOCKET 1
#define IPPROTO_ICMP 1
#define IPPROTO_TCP 6
#define IPPROTO_UDP 17
#define SO_REUSEADDR 2
#define SO_TYPE 3
#define SO_ERROR 4
#define SO_BROADCAST 6
#define SO_SNDBUF 7
#define SO_RCVBUF 8
#define SO_KEEPALIVE 9
#define SO_ACCEPTCONN 30
#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 is declared in two headers on purpose, and guarded so that
// including both is not an error.
//
// posix.h has it because readv/writev need it; this file has it because a
// userspace program including <socket.h> for sendmsg must get a complete
// struct msghdr without also having to know about a kernel header. Both
// definitions are identical and both are Linux's layout.
//
// The guard is not cosmetic. C23 permits redeclaring a struct with identical
// members and GCC 15 defaults to C23, so the duplicate was invisible on the
// gcc path -- while clang defaults to gnu17 and rejected it, which broke the
// one toolchain shell.nix exists to provide and which the UEFI loader needs.
#ifndef SCH_IOVEC_DEFINED
#define SCH_IOVEC_DEFINED
struct iovec {
void *iov_base;
uint64 iov_len;
};
#endif
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
#define SHUT_WR 1
#define SHUT_RDWR 2
struct sockaddr_un {
uint16 sun_family;
char sun_path[UNIX_PATH_MAX];
};
// The generic form syscalls take. Same shape as Linux's, so a musl-linked
// program's struct lines up byte for byte.
struct sockaddr {
uint16 sa_family;
char sa_data[UNIX_PATH_MAX];
};
// The AF_INET address. Same layout as Linux's, so a musl-linked program's
// struct lines up byte for byte. sin_port and sin_addr are network byte order
// and stay that way: lwIP wants them so, and converting on the way in and out
// again would be two swaps that cancel.
struct sockaddr_in {
uint16 sin_family;
uint16 sin_port;
uint32 sin_addr;
char sin_zero[8];
};
// ---------------------------------------------------------------------------
// rtnetlink, enough of it for getifaddrs(3)
//
// Every structure below is Linux's, byte for byte, because musl parses what it
// is handed with Linux's own headers. They are not ours to design.
//
// What musl actually does (src/network/netlink.c) is the whole specification
// this has to meet: open PF_NETLINK/SOCK_RAW/NETLINK_ROUTE, send an
// nlmsghdr+rtgenmsg asking RTM_GETLINK with NLM_F_DUMP, read replies until
// NLMSG_DONE, then do it again for RTM_GETADDR on the same socket.
// ---------------------------------------------------------------------------
struct nlmsghdr {
uint32 nlmsg_len;
uint16 nlmsg_type;
uint16 nlmsg_flags;
uint32 nlmsg_seq;
uint32 nlmsg_pid;
};
struct rtgenmsg { uchar rtgen_family; };
struct ifinfomsg {
uchar ifi_family;
uchar __ifi_pad;
uint16 ifi_type; // ARPHRD_*
int32 ifi_index;
uint32 ifi_flags; // IFF_*
uint32 ifi_change;
};
struct ifaddrmsg {
uchar ifa_family;
uchar ifa_prefixlen;
uchar ifa_flags;
uchar ifa_scope;
uint32 ifa_index;
};
struct rtattr {
uint16 rta_len;
uint16 rta_type;
};
#define NLMSG_ERROR 2
#define NLMSG_DONE 3
#define NLM_F_REQUEST 0x001
#define NLM_F_MULTI 0x002
#define NLM_F_DUMP 0x300
#define RTM_NEWLINK 16
#define RTM_GETLINK 18
#define RTM_NEWADDR 20
#define RTM_GETADDR 22
#define IFLA_ADDRESS 1
#define IFLA_IFNAME 3
#define IFA_ADDRESS 1
#define IFA_LOCAL 2
#define IFA_LABEL 3
#define ARPHRD_ETHER 1
#define ARPHRD_LOOPBACK 772
#define IFF_UP 0x1
#define IFF_LOOPBACK 0x8
#define IFF_RUNNING 0x40
#define IFF_BROADCAST 0x2
#define IFF_MULTICAST 0x1000
// Both are four-byte aligned on every architecture Linux supports.
#define NLMSG_ALIGN(n) (((n) + 3) & ~3)
#define RTA_ALIGN(n) (((n) + 3) & ~3)
#ifdef _KERNEL_SOCKET_INTERNALS
#define NSOCK 32 // sockets in the system
#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,
SS_BOUND,
SS_LISTENING,
SS_CONNECTED,
};
// Every field here is protected by the socket table's single lock in
// kernel/net/unix.c -- there is deliberately no per-socket lock; see the
// comment at the top of that file for why.
struct socket {
int ref;
int type;
int state;
int domain; // AF_UNIX or AF_INET
// For AF_INET: the lwIP-backed socket in kernel/net/inet.c. Opaque here so
// that socket.h does not have to drag lwIP's headers into every file that
// wants a struct socket.
void *inet;
char name[UNIX_PATH_MAX]; // the bound path, for a listener and getsockname
// The filesystem node bind() created, by identity rather than by name.
//
// Matching on the *path string* was what made these sockets invisible to the
// filesystem: two spellings of one path missed each other, a relative path
// never matched an absolute one, and there was nothing to stat() or
// unlink(). An inode is the thing a path resolves *to*, so connect() can
// walk the path however it likes and still arrive at the same socket.
//
// bind_dev 0 means "no node": a socket bound before this existed, or one
// whose file has been unlinked. The socket stays bound and stays usable by
// anyone already connected; it is simply no longer findable, which is
// exactly what Linux does.
uint bind_dev;
uint64 bind_ino;
// A connected socket's peer. Writing to a socket appends to the *peer's*
// buffer, which is what makes one buffer per socket enough for a
// bidirectional connection.
struct socket *peer;
int peer_closed;
char data[SOCKBUF];
uint nread, nwrite;
// Connections waiting to be accepted, on a listener.
struct socket *queue[NBACKLOG];
int nqueue;
// Socket options that are genuinely honoured. Kept here rather than only in
// lwIP's pcb because getsockopt has to answer for an AF_UNIX socket too, and
// because a flag set before bind() has to survive until there is a pcb to
// apply it to.
int o_reuseaddr;
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