README: rewritten around what runs (Linux software, Xorg, apk, Linux's drivers), the honest gaps, measured numbers. plan/: 1.0.md is the finish line, where each clause of the acceptance test stands, and the register (one failing clause: a root that survives a reboot); built plans (foundations, hybrid, alpine, xorg) and the decided-against one (wayland) are gone, their content lives in architecture/; roadmap.md is only what is still open. guides: building/layout/testing/adding-a-driver (Linux code may be used). history: four entries (ls -lR, TLB shootdowns, MDIO 0x02460246, netlink MSG_TRUNC). Website: cards, stack, download page, docs index. Code comments that pointed at deleted plan docs point at the architecture docs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
861 lines
26 KiB
C
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 "core/types.h"
|
|
#include "uapi/user.h"
|
|
#include "net/socket.h"
|
|
#include "uapi/stat.h"
|
|
#include "uapi/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/architecture/kernel.md item 1, docs/architecture/devices.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/architecture/kernel.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();
|
|
}
|