Gate 1, item 3, and the last of foundations.md.
bind() creates a T_SOCK node at the path; connect() resolves the path and looks
for the socket bound to **that inode**. Matching on the inode rather than on
the path *string* is the part that matters: the name used to live only in the
socket table, so two spellings of one path missed each other, a relative path
never matched an absolute one, and there was nothing to stat() or unlink().
An inode is the thing a path resolves *to*, so connect() can walk the path
however it likes and arrive at the same socket.
# ls -l /tmp
srwxr-xr-x 1 root root 0 2026-09-22 08:04 socktest
The node is a marker, not the binding. It holds no data and the live
association stays in the socket table, so a socket file left by a crash is
what Linux leaves behind -- a dead file that connect() refuses until something
binds again -- and unlink()ing a live socket's path does not disturb anyone
already connected.
EADDRINUSE comes from the *directory* rather than from a scan of the table,
deliberately: a stale socket file has to refuse a rebind for the same reason a
running server's does, or two servers both believe they own it. The creation
lives in sys_bind() and not in unix.c, because that is the layer with both --
kernel/net/unix.c knows about sockets and has no business calling create().
T_SOCK is a compat addition to SchFS by the rule in schfs.h: a driver that does
not know SCHFS_T_SOCK maps it to a zero-length regular file, so nothing goes
stale and nothing becomes unreadable.
cmd/socktest section 15 asserts the plan's own wording -- bind, stat, see an
`s`, unlink -- plus the two a name table could never fail: a second bind to a
live path is refused, and connecting to a path that is a *regular file* is
refused rather than matched by name.
**And the semantics being real broke socktest on its second run**, correctly:
the socket files persisted and every bind() hit EADDRINUSE. Every real server
unlinks before binding; socktest now does too, which is the change the new
behaviour demands rather than a workaround for it.
Chasing that turned up something worse. `socktest` reported rc=1 with every
check passing, and so did every other first-party program:
- **lib/start.S called main and threw %eax away.** sys_exit() reads the
status from %rdi and nothing put it there, so every program exited with
whatever that register happened to hold. Every test suite ends in
`return failures ? 1 : 0` and none of it reached the shell.
- **exit(void) passed no status either**, for the same reason: the
declaration takes no argument, so %rdi was the caller's litter.
`if socktest; then` works now, and a failing command reports 1 -- checked both
ways, because a fix that makes everything return 0 would look identical from
the success side.
Also factored: unlink_path() out of sys_unlink, so a failed bind can take its
own node back out without going through the syscall entry, and create_sock()
as a named door rather than exporting create() with an arbitrary type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
8 KiB
Markdown
171 lines
8 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 five items
|
||
|
||
In dependency order. The first three are what Wayland cannot start without.
|
||
Item 1 is built; items 2 and 3 are what a buffer still cannot cross on.
|
||
|
||
### 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
|
||
|
||
`PT_INTERP` and `ET_DYN` in `kernel/fs/exec.c`.
|
||
|
||
**Deferrable, and deferred.** Static binaries are enough to reach a working
|
||
desktop with software rendering. This becomes necessary when Mesa does — EGL
|
||
means `dlopen` means a loader — and that is a separate decision made later.
|
||
|
||
---
|
||
|
||
## 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.
|