Gate 1, item 2, and the thing a Wayland buffer crosses on.
memfd_create(2) is the same segment machinery shmget() already had -- pages,
refcounts, no_free VMAs, the fork and exit accounting -- reached by
**descriptor** rather than by System V key. That difference is the entire
point and the reason it exists on Linux at all: two processes share memory by
passing the fd over a socket, so neither has to agree on a path or race to
invent a key. It is also why SCM_RIGHTS had to be item 1.
docs/plan/foundations.md proposed exactly this and it was right: what memfd
adds on top of shmget is a segment that starts empty and is sized by
ftruncate(), and a file that owns one reference to it. FD_MEMFD, and
read/write/lseek/fstat all learn about it -- a descriptor that refuses read()
is a surprise nobody needs.
Three things are refused rather than faked, each the honest version of
something this kernel cannot do:
- MAP_PRIVATE on a memfd. Every caller is sharing on purpose; a private
mapping of an anonymous object is an expensive MAP_ANONYMOUS.
- Mapping past the end. Linux faults those pages in; there is no handler
here that could.
- Shrinking while mapped. Linux truncates the pages out from under the
mapper; here a freed page would stay mapped to memory somebody else now
owns.
Writable MAP_SHARED of a *real file* is still ENODEV. That needs a page cache,
and an anonymous object has no such contract to break -- which is precisely
why memfd is the narrow answer.
**cmd/memfdtest**, seventeen checks, written so a permissive implementation
cannot pass: an mmap that quietly returned private zeroed pages satisfies every
return code and fails the one assertion that matters, because the reader is a
different process. It ends with the combination the plan singled out -- a memfd
sent over a socketpair to a receiver that closes every inherited copy first,
maps only the descriptor that arrived, reads what the sender wrote and writes
back where the sender can see it.
Two things were found by writing that test, both of which had been refusing
everything that was not FD_INODE:
- sys_fstat() never reached filestat(), so the memfd branch there was
unreachable from the syscall that needs it. It also refused pipes and
sockets, and returned -1 (which musl reads as EPERM) rather than an errno.
- sys_lseek() the same, and SEEK_END needs a length that is not in an inode.
Verified: 40 consecutive runs leave MemFree at exactly the value it started
at -- 936656 kB before and after -- so nothing leaks across creation, dual
mapping, descriptor passing and exit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
248 lines
8.3 KiB
C
248 lines
8.3 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// memfdtest -- two processes map the same memfd, one writes, the other sees it.
|
|
//
|
|
// That sentence is docs/plan/foundations.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 "types.h"
|
|
#include "user.h"
|
|
#include "fcntl.h"
|
|
#include "stat.h"
|
|
#include "posix.h"
|
|
#include "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;
|
|
}
|