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>
453 lines
12 KiB
C
453 lines
12 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.
|
|
static int
|
|
sock_to_fd(struct socket *s)
|
|
{
|
|
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;
|
|
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)
|
|
return -EAFNOSUPPORT;
|
|
|
|
// SOCK_CLOEXEC / SOCK_NONBLOCK ride in the high bits of `type`. Neither
|
|
// is honoured yet, but masking them off means a musl program that always
|
|
// sets SOCK_CLOEXEC still gets a socket instead of EINVAL.
|
|
type &= 0xFF;
|
|
if (domain == AF_UNIX && type != SOCK_STREAM)
|
|
return -EPROTOTYPE;
|
|
if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM)
|
|
return -EPROTOTYPE;
|
|
if (protocol != 0)
|
|
return -EPROTONOSUPPORT;
|
|
|
|
if ((s = socknew(domain, type)) == 0)
|
|
return -ENFILE;
|
|
|
|
if ((fd = sock_to_fd(s)) < 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);
|
|
}
|
|
|
|
SYSCALL_DEFINE(accept)
|
|
{
|
|
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)) < 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(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;
|
|
type &= 0xFF;
|
|
if (type != SOCK_STREAM)
|
|
return -EPROTOTYPE;
|
|
|
|
if ((r = sockpair(&a, &b)) < 0)
|
|
return r;
|
|
|
|
if ((fd0 = sock_to_fd(a)) < 0) {
|
|
sockclose(a);
|
|
sockclose(b);
|
|
return fd0;
|
|
}
|
|
if ((fd1 = sock_to_fd(b)) < 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);
|
|
}
|
|
|
|
// getsockopt/setsockopt: accepted and ignored. A server that sets SO_REUSEADDR
|
|
// and treats failure as fatal is common enough to be worth not failing.
|
|
SYSCALL_DEFINE(setsockopt) { return 0; }
|
|
|
|
SYSCALL_DEFINE(getsockopt)
|
|
{
|
|
addr_t uval, ulen;
|
|
int zero = 0, len = 0;
|
|
|
|
argaddr(3, &uval);
|
|
argaddr(4, &ulen);
|
|
if (uval && ulen) {
|
|
if (get_user(len, ulen) == 0 && len >= (int)sizeof(int)) {
|
|
put_user(zero, uval);
|
|
len = sizeof(int);
|
|
put_user(len, ulen);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|