The set libwayland's event loop is made of, in kernel/sys/sys_event.c: four kinds of descriptor with no inode, FD_EVENT, that read() drains and poll() reports on. They were table entries returning ENOSYS that read as done. epoll registers (descriptor, file) pairs, so a closed descriptor leaves the set; level- and edge-triggered, EPOLLONESHOT. Waiting is poll()'s, a tick at a time. The old eventfd and signalfd numbers (284, 282) were aliased to the new calls and read flags from a garbage register; they are their own entries now, with epoll_create and epoll_pwait. cmd/evtest checks each, then all four in one set. The plans are brought up to date: everything Wayland was waiting on is built except setsockopt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
199 lines
9.4 KiB
Markdown
199 lines
9.4 KiB
Markdown
# Foundations
|
||
|
||
The kernel work that [wayland.md](wayland.md) and [hybrid.md](hybrid.md) both
|
||
stop at. **This is the critical path**, and it is the reason those two are one
|
||
plan rather than two.
|
||
|
||
The insight is small and it decides the schedule: the substrate for passing a
|
||
buffer to a compositor and the substrate for handing a device to a driver process
|
||
are the same substrate. Descriptor passing and shared memory, built once.
|
||
|
||
---
|
||
|
||
## What is actually in the tree
|
||
|
||
Measured against the source, not against a README. Each of these was checked
|
||
before anything below was planned around it:
|
||
|
||
| | |
|
||
|---|---|
|
||
| ~~`sendmsg`/`recvmsg`, and therefore **`SCM_RIGHTS`**~~ | **Built.** See item 1 below. |
|
||
| Writable `MAP_SHARED` of a file | **Refused** — `kernel/sys/sys_mem.c:131` returns `-ENODEV`. |
|
||
| `memfd_create` | Does not exist. |
|
||
| `AF_UNIX` names | In a table in `kernel/net/unix.c`, not in the filesystem. |
|
||
| `setsockopt` | `return 0;` unconditionally — `kernel/sys/sys_net.c:436`. |
|
||
| `exec` | `ET_EXEC` only. No `PT_INTERP`, no `ET_DYN`. |
|
||
|
||
> **`projects/schcom/README.md` said `SCM_RIGHTS` was done while nothing in the
|
||
> kernel implemented it** — the most load-bearing wrong line in the tree, with an
|
||
> entire GUI plan drawn on top of it. The line is now true, which is not the same
|
||
> as it having been right, and the README says so.
|
||
|
||
The `MAP_SHARED` refusal is worth defending rather than fixing carelessly. The
|
||
code returns `-ENODEV` *honestly*: the contract of a writable shared mapping is
|
||
that stores land in the file and other mappers see them, and there is no page
|
||
cache that could make that true. Quietly handing back a private copy would be the
|
||
same lie in a new place.
|
||
|
||
---
|
||
|
||
## The six items
|
||
|
||
In dependency order. The first three are what Wayland cannot start without.
|
||
All but item 4 are built.
|
||
|
||
### 1. `sendmsg` / `recvmsg` with `SCM_RIGHTS` — **done**
|
||
|
||
Not optional and not substitutable. `wl_shm_create_pool` passes a **file
|
||
descriptor**; there is no path in the Wayland protocol that passes an integer
|
||
instead.
|
||
|
||
The `handle_kind` field in `projects/schcom/proto.h` was good foresight and it
|
||
cannot help here — *our* protocol is the flexible one, and Wayland's is not.
|
||
|
||
Built as described: `struct msghdr`/`cmsghdr` in Linux's layout, ancillary data
|
||
queued alongside the byte stream in `kernel/net/unix.c`, descriptor installation
|
||
into the receiver's file table, and the three ways a message can end — received,
|
||
received without room, never received — each releasing the kernel's reference.
|
||
Fourteen checks in `cmd/socktest`, every one of them moving data through the
|
||
descriptor rather than inspecting it. Documented in
|
||
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
|
||
|
||
The delivery rule is the part worth knowing before building on it: **descriptors
|
||
are pinned to the stream position of the first byte of the message that carried
|
||
them**, and a read that has not reached that position is cut short so it stops
|
||
before it. That is what keeps "which message did these belong to" answerable.
|
||
|
||
**This was also item 1 of the hybrid work**, where it is how a driver process is
|
||
handed the device it is allowed to touch.
|
||
|
||
Two bugs fell out of building it, neither in the new code:
|
||
|
||
- `sockpair()` never set the socket's domain, so **every `socketpair()` socket
|
||
claimed domain 0**. Invisible for as long as the data paths asked only whether
|
||
a socket was `AF_INET` or `AF_NETLINK` — zero answers "neither" correctly.
|
||
`SCM_RIGHTS` asks "is this `AF_UNIX`", and zero is wrong for that one.
|
||
- `pipe_poll()` reports `POLLHUP` but not `POLLIN` for a pipe at end of file, so
|
||
a **non-blocking read of a pipe whose writers have all gone returned `EAGAIN`
|
||
forever** instead of 0. A program that polls and reads would spin waiting for
|
||
an end that had already happened. `sockreadable()` always had the equivalent
|
||
condition; the pipe path did not.
|
||
|
||
### 2. A shareable memory object — **done**
|
||
|
||
`memfd_create`, plus `mmap(MAP_SHARED)` on it genuinely working between two
|
||
processes. Described in
|
||
[../architecture/kernel.md](../architecture/kernel.md#shared-memory-objects);
|
||
asserted by `cmd/memfdtest`, seventeen checks including the one that matters —
|
||
a descriptor sent over a socket, mapped by a receiver that has closed every
|
||
inherited copy, with stores visible in both directions.
|
||
|
||
Narrower than a general page cache, and sufficient. An anonymous shared object
|
||
needs no file behind it, which is exactly why `memfd` exists on Linux at all —
|
||
and it means this can be built on the shared-memory machinery already in
|
||
`kernel/sys/sys_mem.c` (`shmget`/`shmat`/`shmdt` work, and `struct vma` already
|
||
carries `shmid` and a `no_free` flag for pages that are not ours to release).
|
||
|
||
The general answer — a page cache, and writable `MAP_SHARED` of a real file —
|
||
stays owed. It is a larger project and nothing on this plan needs it.
|
||
|
||
### 3. `AF_UNIX` sockets in the filesystem namespace — **done**
|
||
|
||
Clients find a Wayland compositor by `bind()`ing `$XDG_RUNTIME_DIR/wayland-0` as
|
||
a real path, and libwayland `flock()`s the lock file beside it. A name table can
|
||
express neither.
|
||
|
||
`bind()` creates a `T_SOCK` node and `connect()` resolves the path and matches
|
||
the socket bound to **that inode** — not to a string, which is what made two
|
||
spellings of one path miss each other. `ls -l` shows `srwxr-xr-x`, `stat()`
|
||
reports `S_IFSOCK`, and `unlink()` removes it. Asserted by `cmd/socktest`,
|
||
section 15. `flock()` is still not implemented; see below.
|
||
|
||
`kernel/net/unix.c`'s own header comment already predicted this:
|
||
|
||
> *"What is lost is `ls -l /tmp/schcom` showing an `s`, and `unlink()` of a socket
|
||
> path failing with ENOENT. Both are worth fixing when the VFS next gets opened
|
||
> up; neither blocks anything."*
|
||
|
||
They block something now. This is that moment, and it is a VFS change rather than
|
||
a socket change: a socket inode type threaded through `iget`, `dirlookup` and
|
||
`stat`, plus `flock`.
|
||
|
||
### 4. `setsockopt` that does not lie
|
||
|
||
At minimum `SO_PASSCRED` and `SO_SNDBUF`. Returning 0 for an option you ignored
|
||
is worse than `ENOPROTOOPT`, because the caller adjusts its behaviour on the
|
||
strength of the lie.
|
||
|
||
Small, and grouped here because it is in the same file as item 3 and will be
|
||
found by the same testing.
|
||
|
||
### 5. Dynamic linking — **done**
|
||
|
||
`PT_INTERP` and `ET_DYN` in `kernel/fs/exec.c`. A program that names an
|
||
interpreter gets it loaded at the bottom of the mmap area and started instead,
|
||
with `AT_PHDR`, `AT_PHENT`, `AT_PHNUM`, `AT_BASE` and `AT_ENTRY` to find the
|
||
program by; a PIE program is placed where a fixed one would be. The interpreter
|
||
is musl's own: `ports/musl` now builds `libc.so` as well, shipped as
|
||
`/lib/ld-musl-x86_64.so.1`, which is libc and loader in one file.
|
||
|
||
The kernel half that was missing besides `exec` was `mmap(MAP_FIXED)` of a
|
||
file. It handed back zeroed pages whatever the descriptor said, and kept no
|
||
record of them for `fork` — and a dynamic linker maps every library segment
|
||
after the first exactly that way.
|
||
|
||
Every port still links `-static`, on purpose: this makes the other kind
|
||
possible, it does not convert anything. `cmd/dynhello` is the test — a
|
||
program against `libc.so` and a library of its own, checking TLS, the heap and
|
||
a forked child that still has its libraries.
|
||
|
||
`mprotect` is accepted and not enforced, so RELRO is not read-only yet.
|
||
|
||
### 6. The event descriptors — **done**
|
||
|
||
`epoll`, `eventfd`, `timerfd` and `signalfd`, in `kernel/sys/sys_event.c`.
|
||
libwayland's event loop is all four in one epoll set, which is why they arrive
|
||
together; not in the original list because they were stubs that looked like
|
||
syscalls, returning `ENOSYS` from `table.inc` entries that read as done.
|
||
|
||
They wait the way `poll()` does, a tick at a time, so the latency is 10 ms.
|
||
`cmd/evtest` checks each one, then all four in one set, level- and
|
||
edge-triggered.
|
||
|
||
Building it found a signal bug: a default-fatal signal killed on arrival even
|
||
when it was blocked. A blocked signal now stays pending, which is the contract
|
||
`signalfd` is built on.
|
||
|
||
---
|
||
|
||
## Verification
|
||
|
||
Each item gets a test before the thing that needs it is built, because a
|
||
substrate that is subtly wrong fails inside the consumer and looks like the
|
||
consumer's bug.
|
||
|
||
- **Item 1:** two processes, one passes an open descriptor, the other reads
|
||
through it. Plus the leak case: a sender that exits with descriptors in flight.
|
||
- **Item 2:** two processes map the same `memfd`, one writes, the other sees it.
|
||
- **Item 3:** `bind()` a socket, then `stat()` and `ls -l` the path and see an
|
||
`s`, then `unlink()` it.
|
||
- **Items 1–3 together:** a third test that does all of it at once, because that
|
||
combination is what Wayland actually performs and the three passing separately
|
||
does not prove it.
|
||
|
||
These belong in `bootcheck` as a new switch, following the `-Sock` model.
|
||
|
||
---
|
||
|
||
## What this does not deliver
|
||
|
||
- **No page cache**, so writable `MAP_SHARED` of a real file stays refused.
|
||
- **No `dlopen`** until item 5 is done, which means no Mesa and no third-party
|
||
shared libraries.
|
||
- **`SO_*` coverage is the two options named**, not the full set.
|
||
- **No credential passing** (`SCM_CREDENTIALS`) — only descriptors. Nothing on
|
||
this plan needs it, and a compositor that wants to know who connected will.
|
||
- **No `flock()`.** libwayland locks `wayland-0.lock` beside the socket to stop
|
||
two compositors claiming one display. The socket itself is now a real path,
|
||
so the lock file can be created and opened; what is missing is the advisory
|
||
lock, which is a separate mechanism and has no other caller yet.
|