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
248 lines
8.3 KiB
C
248 lines
8.3 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// memfdtest -- two processes map the same memfd, one writes, the other sees it.
|
|
//
|
|
// That sentence is docs/architecture/kernel.md's own acceptance criterion for
|
|
// item 2, and it is the check a permissive implementation cannot pass: an
|
|
// mmap that quietly returns private zeroed pages satisfies every return code
|
|
// here and fails the one assertion that matters, because the reader is a
|
|
// *different process* and the pages would not be the same pages.
|
|
//
|
|
// The lifecycle is the one a Wayland client performs, in order:
|
|
//
|
|
// fd = memfd_create("buf", 0);
|
|
// ftruncate(fd, size);
|
|
// p = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
|
|
//
|
|
// and the reason it had to wait for SCM_RIGHTS is that the *other* process
|
|
// gets the same object by being handed the descriptor, not by naming it.
|
|
|
|
#include "core/types.h"
|
|
#include "uapi/user.h"
|
|
#include "uapi/fcntl.h"
|
|
#include "uapi/stat.h"
|
|
#include "uapi/posix.h"
|
|
#include "net/socket.h"
|
|
|
|
static int failures;
|
|
|
|
static void
|
|
puts_(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s[n]) n++;
|
|
write(1, (void *)s, n);
|
|
}
|
|
|
|
// mmap returns -errno in the pointer on this system's own ABI -- musl turns
|
|
// that into MAP_FAILED, but a first-party binary sees the raw value. Anything
|
|
// in the top page is an error, not an address.
|
|
static int
|
|
mmap_failed(void *p)
|
|
{
|
|
return (uint64)p >= (uint64)-4096;
|
|
}
|
|
|
|
static void
|
|
check(const char *what, int ok)
|
|
{
|
|
puts_(ok ? "[ok] " : "[FAIL] ");
|
|
puts_(what);
|
|
puts_("\n");
|
|
if (!ok) failures++;
|
|
}
|
|
|
|
#define SIZE 8192
|
|
|
|
static void memset_(void *d, int c, int n) { char *p = d; while (n-- > 0) *p++ = (char)c; }
|
|
static void memmove_(void *d, const void *s, int n)
|
|
{ char *a = d; const char *b = s; while (n-- > 0) *a++ = *b++; }
|
|
|
|
// Send one descriptor over a socket, the way a Wayland client hands its buffer
|
|
// to the compositor. Same shape as cmd/socktest's helper; kept local so this
|
|
// test stands on its own.
|
|
static int
|
|
sendfd(int sock, int fd)
|
|
{
|
|
struct msghdr m;
|
|
struct iovec iv;
|
|
char cbuf[CMSG_SPACE(sizeof(int))];
|
|
struct cmsghdr *c;
|
|
char tag = 'x';
|
|
|
|
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);
|
|
}
|
|
|
|
static int
|
|
recvfd(int sock)
|
|
{
|
|
struct msghdr m;
|
|
struct iovec iv;
|
|
char cbuf[CMSG_SPACE(sizeof(int))];
|
|
struct cmsghdr *c;
|
|
char tag = 0;
|
|
int got = -1;
|
|
|
|
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 = CMSG_LEN(sizeof(int));
|
|
if (recvmsg(sock, &m, 0) < 0)
|
|
return -1;
|
|
c = CMSG_FIRSTHDR(&m);
|
|
if (c && c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS)
|
|
memmove_(&got, CMSG_DATA(c), sizeof(int));
|
|
return got;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
int fd, pfd[2], st = 0;
|
|
char *p;
|
|
struct stat sb;
|
|
|
|
puts_("memfdtest: a shareable memory object\n\n");
|
|
|
|
fd = memfd_create("test-buffer", 0);
|
|
check("memfd_create returns a descriptor", fd >= 0);
|
|
if (fd < 0) { puts_("\nmemfdtest: FAILED\n"); return 1; }
|
|
|
|
// A fresh memfd is empty. Mapping it before it has a size must fail --
|
|
// there are no pages yet, and handing back anonymous ones would be the
|
|
// lie this whole object exists to avoid.
|
|
check("a new memfd has size 0",
|
|
fstat(fd, &sb) == 0 && sb.size == 0);
|
|
check("mapping a memfd that has no size yet is refused",
|
|
mmap_failed(mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)));
|
|
|
|
check("ftruncate gives it a size", ftruncate(fd, SIZE) == 0);
|
|
check("and fstat reports it back",
|
|
fstat(fd, &sb) == 0 && sb.size == SIZE);
|
|
|
|
p = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
check("mmap(MAP_SHARED) returns a mapping", !mmap_failed(p));
|
|
if (mmap_failed(p)) { puts_("\nmemfdtest: FAILED\n"); return 1; }
|
|
|
|
// Readable and writable by this process at all, before anything about
|
|
// sharing is claimed.
|
|
p[0] = 'A';
|
|
p[SIZE - 1] = 'Z';
|
|
check("the mapping is writable and reads back",
|
|
p[0] == 'A' && p[SIZE - 1] == 'Z');
|
|
|
|
// MAP_PRIVATE is refused rather than silently copied.
|
|
check("MAP_PRIVATE on a memfd is refused",
|
|
mmap_failed(mmap(0, SIZE, PROT_READ, MAP_PRIVATE, fd, 0)));
|
|
|
|
// --- the assertion this test exists for ------------------------------
|
|
//
|
|
// fork(), then have the child map the *same* descriptor independently and
|
|
// write through its own mapping. The parent must see it through its own.
|
|
// A private mapping passes everything above and fails here.
|
|
if (pipe(pfd) < 0) { puts_("[FAIL] pipe\n"); return 1; }
|
|
|
|
int pid = fork();
|
|
if (pid == 0) {
|
|
char *q = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
char ok = mmap_failed(q) ? 0 : 1;
|
|
if (ok) {
|
|
// Prove the child sees what the parent wrote before the fork...
|
|
if (q[0] != 'A' || q[SIZE - 1] != 'Z')
|
|
ok = 0;
|
|
// ...then write something of its own, in a different place.
|
|
q[100] = 'C';
|
|
q[SIZE / 2] = 'M';
|
|
}
|
|
write(pfd[1], &ok, 1);
|
|
close(pfd[1]);
|
|
exit();
|
|
}
|
|
close(pfd[1]);
|
|
{
|
|
char ok = 0;
|
|
read(pfd[0], &ok, 1);
|
|
close(pfd[0]);
|
|
check("a second process maps the same fd and sees the parent's bytes",
|
|
ok == 1);
|
|
}
|
|
wait();
|
|
(void)st;
|
|
|
|
check("and the parent sees what the child wrote",
|
|
p[100] == 'C' && p[SIZE / 2] == 'M');
|
|
check("without disturbing anything else in the mapping",
|
|
p[0] == 'A' && p[SIZE - 1] == 'Z' && p[101] == 0);
|
|
|
|
// --- read()/write() reach the same bytes -------------------------------
|
|
{
|
|
char buf[4] = { 0, 0, 0, 0 };
|
|
lseek(fd, 100, 0 /* SEEK_SET */);
|
|
check("read() sees what mmap wrote",
|
|
read(fd, buf, 1) == 1 && buf[0] == 'C');
|
|
lseek(fd, 200, 0 /* SEEK_SET */);
|
|
buf[0] = 'W';
|
|
check("write() is visible through the mapping",
|
|
write(fd, buf, 1) == 1 && p[200] == 'W');
|
|
}
|
|
|
|
// --- shrinking while mapped is refused, not faked ----------------------
|
|
check("shrinking a mapped memfd is refused", ftruncate(fd, 4096) < 0);
|
|
check("growing it is allowed", ftruncate(fd, SIZE * 2) == 0);
|
|
|
|
// --- the combination this was all for ---------------------------------
|
|
//
|
|
// A forked child inherits the descriptor, which proves the *pages* are
|
|
// shared but not that the *object* travels. The Wayland case is an
|
|
// unrelated process that is handed the descriptor over a socket and has
|
|
// no other way to name it. So: send the memfd through a socketpair, and
|
|
// have the receiver close every inherited copy before it maps.
|
|
{
|
|
int sv[2], cpid, rp[2];
|
|
char ok = 0;
|
|
|
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0 || pipe(rp) < 0) {
|
|
check("socketpair for the descriptor-passing check", 0);
|
|
} else if ((cpid = fork()) == 0) {
|
|
int got;
|
|
char *q;
|
|
close(sv[0]); close(rp[0]);
|
|
got = recvfd(sv[1]);
|
|
// Close the inherited copy, so the only route to the object is
|
|
// the descriptor that arrived over the socket.
|
|
close(fd);
|
|
q = (got >= 0) ? mmap(0, SIZE, PROT_READ | PROT_WRITE,
|
|
MAP_SHARED, got, 0) : (char *)-1;
|
|
ok = (got >= 0 && !mmap_failed(q) && q[100] == 'C') ? 1 : 0;
|
|
if (ok) q[300] = 'S';
|
|
write(rp[1], &ok, 1);
|
|
close(rp[1]);
|
|
exit();
|
|
} else {
|
|
close(sv[1]); close(rp[1]);
|
|
sendfd(sv[0], fd);
|
|
read(rp[0], &ok, 1);
|
|
close(rp[0]); close(sv[0]);
|
|
wait();
|
|
check("a memfd passed over a socket maps in the receiver", ok == 1);
|
|
check("and the sender sees what the receiver wrote", p[300] == 'S');
|
|
}
|
|
}
|
|
|
|
close(fd);
|
|
|
|
puts_("\n");
|
|
puts_(failures ? "memfdtest: FAILED\n" : "memfdtest: all checks passed\n");
|
|
return failures ? 1 : 0;
|
|
}
|