SchOS/docs/plan/foundations.md
Nexoniarz a4fbdb7437 epoll, eventfd, timerfd, signalfd
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
2026-10-24 10:59:52 +02:00

199 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.