memfd: a shared memory object you reach by descriptor
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>
This commit is contained in:
parent
c98bdf0019
commit
a79dfac32b
13 changed files with 624 additions and 16 deletions
|
|
@ -141,6 +141,41 @@ arrangement as `schfs-objtest`, and for the same reason: slab lists, free-list
|
|||
threading and whether pages actually come back are all things that can be wrong
|
||||
without a kernel to be wrong in.
|
||||
|
||||
### Shared memory objects
|
||||
|
||||
`shmget`/`shmat`/`shmdt` are System V shared memory: segments of `kalloc`'d
|
||||
pages, reference counted, mapped through a VMA with `no_free` set so that
|
||||
`freevm()` does not hand another process's pages to `kfree`.
|
||||
|
||||
`memfd_create(2)` is the same machinery reached by **descriptor** instead of by
|
||||
key. That difference is the whole 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 what a Wayland buffer crosses
|
||||
on, and it is why descriptor passing (`SCM_RIGHTS`) had to be built first.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `memfd_create(name, flags)` | a new segment of size 0, and a descriptor owning one reference |
|
||||
| `ftruncate(fd, n)` | gives it a size — growing allocates zeroed pages, shrinking frees from the end |
|
||||
| `mmap(…, MAP_SHARED, fd, off)` | maps the segment's existing pages, so both mappers get the same physical memory |
|
||||
| `read`/`write`/`lseek`/`fstat` | work, so it is a file and not a token only `mmap` understands |
|
||||
|
||||
Three things are refused rather than faked, and each refusal is 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 lets it through and faults the pages in.
|
||||
There is no fault handler here that could, so the range is rejected.
|
||||
- **Shrinking while mapped.** Linux truncates the pages out from under the
|
||||
mapper. Here a freed page would simply stay mapped to memory somebody else
|
||||
now owns.
|
||||
|
||||
Writable `MAP_SHARED` of a **real file** is still `ENODEV`. That needs a page
|
||||
cache, it is a separate project, and nothing in 1.0 asks for it — an anonymous
|
||||
object has no such contract to break, which is exactly why `memfd` is the
|
||||
narrow answer.
|
||||
|
||||
### What the fixed tables actually cost
|
||||
|
||||
The standing claim was that every fixed-size table in the kernel exists to
|
||||
|
|
|
|||
Loading…
Reference in a new issue