SchOS/cmd/socktest/main.c
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

861 lines
26 KiB
C

// socktest -- prove that Unix domain sockets carry data between processes.
//
// Every part of a socket is invisible from a return value. bind() returning 0
// proves nothing; a socket layer that accepts every call and drops every byte
// would pass a test built on return codes alone. So each check here has one
// process write and a *different* process read, and compares the bytes.
//
// socktest run every check
//
// Prints one line per check and a final verdict a script can grep for.
#include "types.h"
#include "user.h"
#include "socket.h"
#include "stat.h"
#include "fcntl.h"
static int failures;
static void
puts_(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void*)s, n);
}
static void
putn_(int v)
{
char buf[12];
int i = 0, neg = 0;
if (v < 0) { neg = 1; v = -v; }
if (v == 0) buf[i++] = '0';
while (v) { buf[i++] = '0' + (v % 10); v /= 10; }
if (neg) buf[i++] = '-';
while (i--) write(1, &buf[i], 1);
}
static void
check(int ok, const char *what)
{
puts_(ok ? " [ok] " : " FAIL: ");
puts_(what);
puts_("\n");
if (!ok) failures++;
}
static int
streq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == *b;
}
// Fill a sockaddr_un and return the length to pass to bind/connect: the
// family plus the path plus its NUL, which is what musl computes and what the
// kernel side is written to expect.
// Every bind() here is preceded by an unlink(), which is not tidiness: a bound
// socket is a *file* now, and it outlives the process that bound it. Rebinding
// a path that already has a node is EADDRINUSE, exactly as on Linux, so every
// real server removes the stale one first. Before this change the name lived
// only in the kernel's socket table and vanished when the process did, so a
// test could bind the same path on every run and never notice.
static int
mkaddr(struct sockaddr_un *sa, const char *path)
{
int i = 0;
for (i = 0; i < UNIX_PATH_MAX - 1 && path[i]; i++)
sa->sun_path[i] = path[i];
sa->sun_path[i] = '\0';
sa->sun_family = AF_UNIX;
return (int)sizeof(sa->sun_family) + i + 1;
}
// ---------------------------------------------------------------------------
// 1. A round trip over a named socket, between two processes.
// ---------------------------------------------------------------------------
static void
test_roundtrip(void)
{
struct sockaddr_un sa;
int len;
unlink("/tmp/socktest"); // a stale node from an earlier run
len = mkaddr(&sa, "/tmp/socktest");
int srv, pid, cli, n;
char buf[64];
puts_("named socket, two processes:\n");
srv = socket(AF_UNIX, SOCK_STREAM, 0);
check(srv >= 0, "socket() returns a descriptor");
if (srv < 0) return;
check(bind(srv, (struct sockaddr *)&sa, len) == 0, "bind() to /tmp/socktest");
check(listen(srv, 4) == 0, "listen()");
// A second bind to the same name must be refused, or two servers can
// silently steal each other's clients.
{
int dup_ = socket(AF_UNIX, SOCK_STREAM, 0);
check(bind(dup_, (struct sockaddr *)&sa, len) < 0,
"a second bind to the same name is refused");
close(dup_);
}
pid = fork();
if (pid == 0) {
// Child: the client. Connects, sends a question, reads the answer.
int c = socket(AF_UNIX, SOCK_STREAM, 0);
char reply[64];
int r;
close(srv);
if (connect(c, (struct sockaddr *)&sa, len) < 0) {
puts_(" FAIL: child connect()\n");
exit();
}
write(c, "ping", 4);
r = read(c, reply, sizeof(reply) - 1);
if (r < 0) r = 0;
reply[r] = '\0';
if (!streq(reply, "pong"))
puts_(" FAIL: child did not receive pong\n");
close(c);
exit();
}
cli = accept(srv, 0, 0);
check(cli >= 0, "accept() returns a connection");
if (cli < 0) { close(srv); wait(); return; }
n = read(cli, buf, sizeof(buf) - 1);
if (n < 0) n = 0;
buf[n] = '\0';
check(streq(buf, "ping"), "server read the client's bytes");
check(write(cli, "pong", 4) == 4, "server replied on the same socket");
close(cli);
close(srv);
wait();
}
// ---------------------------------------------------------------------------
// 2. Errors that must actually be errors.
// ---------------------------------------------------------------------------
static void
test_errors(void)
{
struct sockaddr_un sa;
int len;
unlink("/tmp/nobody-listening"); // a stale node from an earlier run
len = mkaddr(&sa, "/tmp/nobody-listening");
int s;
puts_("error paths:\n");
s = socket(AF_UNIX, SOCK_STREAM, 0);
check(connect(s, (struct sockaddr *)&sa, len) < 0,
"connect() to a name nobody bound fails");
close(s);
// This used to assert that AF_INET was *refused*, which was true when this
// file was written and stopped being true the day AF_INET landed. It kept
// passing anyway, and the reason is worth keeping: socktest runs seconds
// after boot, DHCP had not bound yet, and inet_new() refused every socket
// until it did -- reported as ENFILE. So the check saw "< 0" and called it
// a pass for a reason that had nothing to do with what it was testing.
//
// Two lessons, both already written down elsewhere in this tree and both
// learned again here: a test that asserts a *failure* has to say which
// failure, and a green suite is not evidence until something has checked
// that it can go red.
//
// What is left are the refusals that are unconditionally true, whatever the
// link is doing.
check(socket(99, SOCK_STREAM, 0) < 0, "an unknown address family is refused");
check(socket(AF_INET, SOCK_STREAM, 99) < 0,
"AF_INET with a nonsense protocol is refused");
check(socket(AF_UNIX, SOCK_DGRAM, 0) < 0, "SOCK_DGRAM is refused, not faked");
// read/write on a plain file descriptor must not be mistaken for a socket,
// and socket calls on one must be refused.
check(listen(1, 4) < 0, "listen() on a non-socket fd fails");
}
// ---------------------------------------------------------------------------
// 3. End-of-file. A reader whose peer closes must be told, not left hanging.
// ---------------------------------------------------------------------------
static void
test_eof(void)
{
struct sockaddr_un sa;
int len;
unlink("/tmp/socktest-eof"); // a stale node from an earlier run
len = mkaddr(&sa, "/tmp/socktest-eof");
int srv, cli, pid, n;
char buf[16];
puts_("end of file:\n");
srv = socket(AF_UNIX, SOCK_STREAM, 0);
bind(srv, (struct sockaddr *)&sa, len);
listen(srv, 4);
pid = fork();
if (pid == 0) {
int c = socket(AF_UNIX, SOCK_STREAM, 0);
close(srv);
connect(c, (struct sockaddr *)&sa, len);
write(c, "x", 1);
close(c); // the peer goes away with the byte still unread
exit();
}
cli = accept(srv, 0, 0);
n = read(cli, buf, sizeof(buf));
check(n == 1, "the byte sent before close() is still delivered");
n = read(cli, buf, sizeof(buf));
check(n == 0, "the next read returns 0 rather than blocking");
close(cli);
close(srv);
wait();
}
// ---------------------------------------------------------------------------
// 4. socketpair: bidirectional, no name, no listener.
// ---------------------------------------------------------------------------
static void
test_socketpair(void)
{
int sv[2], n;
char buf[16];
puts_("socketpair:\n");
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
check(0, "socketpair() succeeds");
return;
}
check(1, "socketpair() succeeds");
write(sv[0], "a", 1);
n = read(sv[1], buf, sizeof(buf));
check(n == 1 && buf[0] == 'a', "0 -> 1 carries a byte");
write(sv[1], "b", 1);
n = read(sv[0], buf, sizeof(buf));
check(n == 1 && buf[0] == 'b', "1 -> 0 carries a byte, on the same pair");
close(sv[0]);
close(sv[1]);
}
// ---------------------------------------------------------------------------
// 5. A message longer than the socket buffer, so the writer has to block and
// the reader has to drain in more than one go.
// ---------------------------------------------------------------------------
#define BIG 12000
static void
test_big(void)
{
int sv[2], pid, got = 0, ok = 1, i;
static char out[BIG], in[BIG];
puts_("a transfer larger than the buffer:\n");
for (i = 0; i < BIG; i++)
out[i] = (char)('A' + (i % 26));
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
check(0, "socketpair() for the big transfer");
return;
}
pid = fork();
if (pid == 0) {
int sent = 0, r;
close(sv[1]);
while (sent < BIG) {
r = write(sv[0], out + sent, BIG - sent);
if (r <= 0) break;
sent += r;
}
close(sv[0]);
exit();
}
close(sv[0]);
while (got < BIG) {
int r = read(sv[1], in + got, BIG - got);
if (r <= 0) break;
got += r;
}
close(sv[1]);
wait();
check(got == BIG, "every byte arrived");
if (got != BIG) {
puts_(" got ");
putn_(got);
puts_(" of ");
putn_(BIG);
puts_("\n");
}
for (i = 0; i < got; i++)
if (in[i] != out[i]) { ok = 0; break; }
check(ok, "and in the right order");
}
// ---------------------------------------------------------------------------
// Socket options.
//
// Both calls used to succeed unconditionally: setsockopt was `return 0;` and
// getsockopt wrote a zero for whatever was asked. So the only check that proves
// anything is a *round trip* -- set a value, read it back, and see the value
// rather than a zero -- plus one option that must be refused. A stub passes
// "does setsockopt return 0"; it cannot pass both of these at once.
// ---------------------------------------------------------------------------
#define T_SOL_SOCKET 1
#define T_SO_REUSEADDR 2
#define T_SO_TYPE 3
#define T_SO_RCVBUF 8
#define T_SO_NOSUCH 0x7ffe // not an option Linux defines either
static void
test_sockopt(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
int val, len;
puts_("-- socket options\n");
if (fd < 0) { check(0, "socket()"); return; }
// SO_TYPE must report what this socket actually is, not zero.
val = -1; len = sizeof(val);
check(getsockopt(fd, T_SOL_SOCKET, T_SO_TYPE, &val, &len) == 0 &&
val == SOCK_STREAM, "getsockopt SO_TYPE says SOCK_STREAM");
// The round trip. A stub that ignores the set and zeroes the get fails here.
val = 1;
check(setsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, sizeof(val)) == 0,
"setsockopt SO_REUSEADDR accepted");
val = -1; len = sizeof(val);
check(getsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, &len) == 0 &&
val == 1, "getsockopt SO_REUSEADDR reads back 1");
// And back off again, so the answer is not just "always non-zero".
val = 0;
setsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, sizeof(val));
val = -1; len = sizeof(val);
check(getsockopt(fd, T_SOL_SOCKET, T_SO_REUSEADDR, &val, &len) == 0 &&
val == 0, "and reads back 0 once cleared");
// A real buffer size, not zero.
val = -1; len = sizeof(val);
check(getsockopt(fd, T_SOL_SOCKET, T_SO_RCVBUF, &val, &len) == 0 && val > 0,
"getsockopt SO_RCVBUF is a real size");
// The refusal. This is the half that a permissive stub cannot fake.
val = 1;
check(setsockopt(fd, T_SOL_SOCKET, T_SO_NOSUCH, &val, sizeof(val)) < 0,
"setsockopt of an unknown option is refused");
len = sizeof(val);
check(getsockopt(fd, T_SOL_SOCKET, T_SO_NOSUCH, &val, &len) < 0,
"getsockopt of an unknown option is refused");
close(fd);
}
// ---------------------------------------------------------------------------
// SOCK_NONBLOCK, end to end.
//
// Checking that socket() accepts the flag proves nothing -- it was accepted
// before, and discarded. What proves it is a read that *returns* on a socket
// with nothing in it, where a blocking socket would sleep forever.
// ---------------------------------------------------------------------------
#define T_SOCK_NONBLOCK 0x800
#define T_F_GETFL 3
#define T_O_NONBLOCK 04000
static void
test_nonblock(void)
{
int sv[2];
char c;
puts_("-- SOCK_NONBLOCK\n");
if (socketpair(AF_UNIX, SOCK_STREAM | T_SOCK_NONBLOCK, 0, sv) < 0) {
check(0, "socketpair(SOCK_NONBLOCK)");
return;
}
check((fcntl(sv[0], T_F_GETFL, 0) & T_O_NONBLOCK) != 0,
"fcntl(F_GETFL) reports O_NONBLOCK");
// Nothing has been written, so a blocking read would never return. If this
// test hangs, the flag was dropped -- which is exactly what it used to be.
check(read(sv[0], &c, 1) < 0, "read on an empty non-blocking socket returns");
// And it still carries data, so the flag did not break the socket.
write(sv[1], "x", 1);
c = 0;
check(read(sv[0], &c, 1) == 1 && c == 'x', "and still delivers a byte");
close(sv[0]);
close(sv[1]);
}
// The user library has no string.h; these three are all this file needs.
static void
memset_(void *p, int c, int n)
{
char *d = p;
while (n-- > 0) *d++ = (char)c;
}
static void
memmove_(void *dst, const void *src, int n)
{
char *d = dst;
const char *s = src;
while (n-- > 0) *d++ = *s++;
}
static int
strneq_(const char *a, const char *b, int n)
{
while (n-- > 0 && *a == *b && *a) { a++; b++; }
return n < 0 || *a == *b;
}
// ---------------------------------------------------------------------------
// 8. SCM_RIGHTS: a descriptor crossing between processes.
//
// docs/plan/foundations.md item 1, docs/plan/hybrid.md mechanism 1. The thing
// being proved is not that recvmsg returns a number -- it is that the number
// names the *same open file* the other process had, which is why every check
// below moves data through the descriptor rather than inspecting it.
//
// The descriptor passed is always a pipe end, for one reason: a pipe announces
// its own lifetime. A read of a pipe whose every write end is closed returns 0,
// and that turns "was the kernel's reference released" -- which userspace
// cannot otherwise see -- into a value a test can compare.
// ---------------------------------------------------------------------------
#define T_F_SETFL 4
// Send `fd` with one byte of data on it.
static int
sendfd(int sock, int fd, char tag)
{
struct msghdr m;
struct iovec iv;
char cbuf[CMSG_SPACE(sizeof(int))];
struct cmsghdr *c;
memset_(&m, 0, sizeof(m));
memset_(cbuf, 0, sizeof(cbuf));
iv.iov_base = &tag;
iv.iov_len = 1;
m.msg_iov = &iv;
m.msg_iovlen = 1;
m.msg_control = cbuf;
m.msg_controllen = sizeof(cbuf);
c = (struct cmsghdr *)cbuf;
c->cmsg_len = CMSG_LEN(sizeof(int));
c->cmsg_level = SOL_SOCKET;
c->cmsg_type = SCM_RIGHTS;
memmove_(CMSG_DATA(c), &fd, sizeof(int));
return sendmsg(sock, &m, 0);
}
// Receive into `fds`, up to `want` of them. Returns the byte count; *ngot is
// how many descriptors arrived and *flags the kernel's msg_flags.
static int
recvfd(int sock, char *tag, int *fds, int want, int *ngot, int *flags)
{
struct msghdr m;
struct iovec iv;
char cbuf[CMSG_SPACE(SCM_MAXFD * sizeof(int))];
struct cmsghdr *c;
int n;
memset_(&m, 0, sizeof(m));
memset_(cbuf, 0, sizeof(cbuf));
iv.iov_base = tag;
iv.iov_len = 1;
m.msg_iov = &iv;
m.msg_iovlen = 1;
m.msg_control = cbuf;
// CMSG_LEN, not CMSG_SPACE: the trailing padding only matters if another
// header follows, and sizing by it would leave room for a descriptor the
// caller did not ask for -- which is exactly what makes a truncation test
// pass by accident.
m.msg_controllen = CMSG_LEN(want * sizeof(int));
*ngot = 0;
n = recvmsg(sock, &m, 0);
if (flags)
*flags = m.msg_flags;
if (n < 0)
return n;
c = CMSG_FIRSTHDR(&m);
if (c && c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS) {
int k = (int)((c->cmsg_len - CMSG_LEN(0)) / sizeof(int));
for (*ngot = 0; *ngot < k && *ngot < want; (*ngot)++)
memmove_(&fds[*ngot], CMSG_DATA(c) + *ngot * sizeof(int), sizeof(int));
}
return n;
}
// Is every write end of this pipe gone? Non-blocking, so a descriptor that
// leaked reports itself instead of hanging the test.
static int
pipe_at_eof(int rfd)
{
char c;
fcntl(rfd, T_F_SETFL, T_O_NONBLOCK);
return read(rfd, &c, 1) == 0;
}
static void
test_fdpass(void)
{
int sv[2], p[2], pid, got = 0, fds[SCM_MAXFD], n;
char tag = 0, buf[32];
puts_("SCM_RIGHTS:\n");
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) {
check(0, "socketpair() and pipe() for the descriptor tests");
return;
}
pid = fork();
if (pid == 0) {
close(sv[0]);
write(p[1], "through a passed descriptor", 27);
sendfd(sv[1], p[0], 'F');
// Every process-held reference is gone from here on. If the kernel is
// not holding one of its own, the parent receives a dead descriptor.
close(p[0]);
close(p[1]);
close(sv[1]);
exit();
}
close(sv[1]);
close(p[0]);
close(p[1]);
wait();
n = recvfd(sv[0], &tag, fds, SCM_MAXFD, &got, 0);
check(n == 1 && tag == 'F', "recvmsg returns the byte the descriptor rode on");
check(got == 1, "and one descriptor with it");
if (got == 1) {
n = read(fds[0], buf, sizeof(buf));
check(n == 27 && strneq_(buf, "through a passed descriptor", 27),
"the descriptor is the sender's pipe, and still has its data");
close(fds[0]);
}
close(sv[0]);
}
// Two messages, each carrying a descriptor, read with a buffer big enough for
// both. A read that ran past the second message's first byte would hand back
// one message's descriptors with two messages' bytes, and nothing in the
// result would say so -- which is why the kernel stops the read at the
// boundary instead.
static void
test_fdpass_boundary(void)
{
int sv[2], a[2], b[2], got = 0, fds[SCM_MAXFD], n, flags = 0;
char tag = 0;
struct msghdr m;
struct iovec iv;
char big[8];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 ||
pipe(a) < 0 || pipe(b) < 0) {
check(0, "sockets and pipes for the boundary test");
return;
}
sendfd(sv[1], a[0], '1');
sendfd(sv[1], b[0], '2');
// Ask for eight bytes. Two are available.
memset_(&m, 0, sizeof(m));
iv.iov_base = big;
iv.iov_len = sizeof(big);
m.msg_iov = &iv;
m.msg_iovlen = 1;
n = recvmsg(sv[0], &m, 0);
check(n == 1, "a read spanning two messages stops at the second's boundary");
n = recvfd(sv[0], &tag, fds, SCM_MAXFD, &got, &flags);
check(n == 1 && tag == '2' && got == 1,
"so the next read starts there, and gets that message's descriptor");
if (got == 1)
close(fds[0]);
close(sv[0]); close(sv[1]);
close(a[0]); close(a[1]); close(b[0]); close(b[1]);
}
// A plain read(2) of a message carrying descriptors. There is nowhere to put
// them, so they are closed -- the alternative is the kernel holding references
// for a receiver that has proved it is not looking.
static void
test_fdpass_dropped(void)
{
int sv[2], p[2];
char c;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) {
check(0, "sockets and a pipe for the dropped-descriptor test");
return;
}
sendfd(sv[1], p[1], 'D');
close(p[1]); // the kernel's reference is the last one
check(read(sv[0], &c, 1) == 1 && c == 'D',
"read(2) of a message carrying a descriptor returns its bytes");
check(pipe_at_eof(p[0]),
"and closes the descriptor rather than holding it for nobody");
close(p[0]); close(sv[0]); close(sv[1]);
}
// A message with descriptors that is never received at all. The socket is
// closed with them still queued, and the kernel holds the only references --
// so if close does not drain the queue they are unreachable and permanent.
static void
test_fdpass_orphaned(void)
{
int sv[2], p[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) {
check(0, "sockets and a pipe for the orphaned-descriptor test");
return;
}
sendfd(sv[1], p[1], 'O');
close(p[1]);
close(sv[0]); // never received
close(sv[1]);
check(pipe_at_eof(p[0]),
"closing a socket closes the descriptors still queued on it");
close(p[0]);
}
// More descriptors than the receiver left room for. The ones that do not fit
// are closed, and MSG_CTRUNC is how the caller is told it happened.
static void
test_fdpass_truncated(void)
{
int sv[2], a[2], b[2], got = 0, fds[SCM_MAXFD], flags = 0;
char tag = 0;
struct msghdr m;
struct iovec iv;
char cbuf[CMSG_SPACE(2 * sizeof(int))];
struct cmsghdr *c;
int two[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 ||
pipe(a) < 0 || pipe(b) < 0) {
check(0, "sockets and pipes for the truncation test");
return;
}
// One message, two descriptors.
memset_(&m, 0, sizeof(m));
memset_(cbuf, 0, sizeof(cbuf));
tag = 'T';
iv.iov_base = &tag;
iv.iov_len = 1;
m.msg_iov = &iv;
m.msg_iovlen = 1;
m.msg_control = cbuf;
m.msg_controllen = sizeof(cbuf);
c = (struct cmsghdr *)cbuf;
c->cmsg_len = CMSG_LEN(2 * sizeof(int));
c->cmsg_level = SOL_SOCKET;
c->cmsg_type = SCM_RIGHTS;
two[0] = a[1];
two[1] = b[1];
memmove_(CMSG_DATA(c), two, sizeof(two));
check(sendmsg(sv[1], &m, 0) == 1, "sendmsg carries two descriptors at once");
close(a[1]);
close(b[1]);
// Receive with room for one.
tag = 0;
recvfd(sv[0], &tag, fds, 1, &got, &flags);
check(got == 1, "a receiver with room for one gets one");
check((flags & MSG_CTRUNC) != 0, "and MSG_CTRUNC says the rest were dropped");
check(pipe_at_eof(b[0]) || pipe_at_eof(a[0]),
"the descriptor that did not fit was closed, not leaked");
if (got == 1)
close(fds[0]);
close(a[0]); close(b[0]); close(sv[0]); close(sv[1]);
}
// Descriptors with no bytes to ride on. Delivery pins them to the position of
// a first byte, so a message with no bytes has no position at which anyone
// could ever be handed them; saying so beats accepting it and losing them.
static void
test_fdpass_nodata(void)
{
int sv[2], p[2];
struct msghdr m;
char cbuf[CMSG_SPACE(sizeof(int))];
struct cmsghdr *c;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(p) < 0) {
check(0, "sockets and a pipe for the no-data test");
return;
}
memset_(&m, 0, sizeof(m));
memset_(cbuf, 0, sizeof(cbuf));
m.msg_control = cbuf;
m.msg_controllen = sizeof(cbuf);
c = (struct cmsghdr *)cbuf;
c->cmsg_len = CMSG_LEN(sizeof(int));
c->cmsg_level = SOL_SOCKET;
c->cmsg_type = SCM_RIGHTS;
memmove_(CMSG_DATA(c), &p[1], sizeof(int));
check(sendmsg(sv[1], &m, 0) < 0,
"sendmsg refuses descriptors with no data to carry them");
close(p[1]);
check(pipe_at_eof(p[0]),
"and does not keep the reference it took while trying");
close(p[0]); close(sv[0]); close(sv[1]);
}
// ---------------------------------------------------------------------------
// 15. A bound socket is a file.
//
// docs/plan/foundations.md item 3, in its own words: bind() a socket, then
// stat() and `ls -l` the path and see an `s`, then unlink() it.
//
// The reason it matters is not tidiness. A Wayland client finds its compositor
// by bind()ing a real path and libwayland locks the file beside it; a name
// table in the kernel can express neither. These checks are what a name table
// cannot pass -- every one of them asks the *filesystem* about the socket,
// not the socket layer.
// ---------------------------------------------------------------------------
static void
test_fsnames(void)
{
struct sockaddr_un sa;
struct stat st;
int len, sv, c;
puts_("a bound socket is a file:\n");
unlink("/tmp/socktest-fs"); // from a previous run
sv = socket(AF_UNIX, SOCK_STREAM, 0);
len = mkaddr(&sa, "/tmp/socktest-fs");
check(sv >= 0, "socket()");
check(bind(sv, (struct sockaddr *)&sa, len) == 0, "bind creates the path");
check(stat("/tmp/socktest-fs", &st) == 0, "and it can be stat()ed");
check(st.type == T_SOCK, "as a socket, not a regular file");
// Binding the same path twice is EADDRINUSE, and it has to come from the
// directory -- a socket file left by a crashed server must refuse a
// rebind for the same reason a running one does.
{
int s2 = socket(AF_UNIX, SOCK_STREAM, 0);
check(s2 >= 0 && bind(s2, (struct sockaddr *)&sa, len) < 0,
"a second bind to the same path is refused");
if (s2 >= 0) close(s2);
}
// Connecting to a path that is not a socket must be refused rather than
// matched by name. A name table could not tell the difference.
{
struct sockaddr_un sb;
int lb = mkaddr(&sb, "/tmp/socktest-notasock");
int fd = open("/tmp/socktest-notasock", O_CREATE | O_RDWR);
if (fd >= 0) close(fd);
c = socket(AF_UNIX, SOCK_STREAM, 0);
check(c >= 0 && connect(c, (struct sockaddr *)&sb, lb) < 0,
"connecting to a regular file is refused");
if (c >= 0) close(c);
unlink("/tmp/socktest-notasock");
}
check(unlink("/tmp/socktest-fs") == 0, "unlink removes it");
check(stat("/tmp/socktest-fs", &st) < 0, "and it is gone from the filesystem");
// The path is free again. On Linux the old socket keeps working for
// anyone already connected; it is simply no longer findable by name.
{
int s3 = socket(AF_UNIX, SOCK_STREAM, 0);
check(s3 >= 0 && bind(s3, (struct sockaddr *)&sa, len) == 0,
"and the path is free to bind again");
if (s3 >= 0) close(s3);
unlink("/tmp/socktest-fs");
}
close(sv);
}
int
main(void)
{
puts_("socktest: unix domain sockets\n");
test_roundtrip();
test_errors();
test_eof();
test_socketpair();
test_big();
test_sockopt();
test_nonblock();
test_fdpass();
test_fdpass_boundary();
test_fdpass_dropped();
test_fdpass_orphaned();
test_fdpass_truncated();
test_fdpass_nodata();
test_fsnames();
if (failures == 0)
puts_("socktest: ALL CHECKS PASSED\n");
else {
puts_("socktest: ");
putn_(failures);
puts_(" CHECK(S) FAILED\n");
}
exit();
}