ldls: the plan for running someone else's drivers

docs/plan/ldls.md. SchOS cannot write a driver for every device that exists
and no project can, so the only route to the hardware support an established
system has is to run Linux's. hybrid.md built the mechanisms for this and said
so; vgpud proved them. This is what they were for.

Five rules, and everything else is an implementation of them:

  1. Nothing GPL ever enters an image we build. The driver is the user's,
     obtained by them, loaded on their machine.
  2. It runs in a process, never in the kernel. Legally a module linked into
     our kernel is the strongest derivative-work argument that exists, and
     practically this is foreign code that must not be able to take the
     machine. FreeBSD has shipped from this position for a decade.
  3. Facts, not expression. A symbol's signature and a struct's offsets are
     facts a compiled driver depends on; comments, macro bodies and inline
     bodies are expression and are never carried over. Provenance recorded per
     file. Flagged in the doc as wanting real legal advice before shipping.
  4. One kernel version at a time, pinned and named, because Linux says in
     writing that it has no stable in-kernel ABI.
  5. The unresolved-symbol list is the specification. Load a real module and
     let the loader say what is missing rather than guessing the surface.

Four pieces: an ELF relocatable loader (ours, clean, no licence question), the
KPI shim (the actual work), /dev/pci as the back end (built), and one new
kernel interface -- a netdev whose backend is a process, which is the exact
analogue of display_ops and is four vtable entries wide.

First target is 8139too rather than r8169, and the reason is decisive: QEMU
emulates rtl8139 and does not emulate r8169, so the obvious choice cannot be
tested at all. 8139too is also smaller, needs no firmware, and uses the thin
mii helpers instead of dragging in phylib. r8169 is what proves it was general,
afterwards and much more cheaply.

The "what it will not do" section carries the honest list, including the two
that are not yet answered: no EXPORT_SYMBOL_GPL story, and no IOMMU -- which
for foreign drivers is a materially worse problem than for our own, and is the
strongest argument for one this project has produced.

hybrid.md is closed and leaves the register; ldls.md takes the slot, which is
the freeze rule working rather than being bent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-14 01:51:30 +02:00
commit 5263150d3d
5 changed files with 237 additions and 3 deletions

View file

@ -95,9 +95,17 @@ The galling ones. The work exists, is tested, and nothing uses it.
|---|---|
| **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** |
| ~~**`hybrid.md`**~~ | **Closed.** All four mechanisms built; H1, H2 and H3 done. H4 — the USB eviction — is *withdrawn* rather than outstanding: USB is the bus peripherals arrive on, not a peripheral, and an eviction that has to leave an identical driver behind has moved nothing. See [hybrid.md](hybrid.md), "Decided: USB stays in the kernel". |
| **`ldls.md`** | Not started. The plan exists; L0, the loader, is the first thing that would. |
| **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. |
| **`debugger.md`** | D0 through D4 built and described in [../architecture/debugging.md](../architecture/debugging.md). Only the network transport is open — a userspace daemon bridging TCP to `/dev/dbg`, which is deliberately last. |
**The row LDLS spends.** `hybrid.md` closed above — H3 built, H4 withdrawn —
and [ldls.md](ldls.md) takes the slot it freed. Worth being honest that this is
the largest thing ever to enter this register, and that it is **explicitly not
part of 1.0**: nothing in the acceptance test needs a foreign driver. It is
here because the mechanisms it uses are finished and idle, and because the
alternative to it is writing a driver for every device that exists.
**The row the debugger spent.** Adding one requires closing one, and *A released
PCI device* closed when H2 landed. It is worth saying plainly that the debugger
is not a capability of the OS the way every other row here is: nothing in it

View file

@ -24,7 +24,7 @@ Taken together, because they turn out to need the same foundation.
| | |
|---|---|
| **[wayland.md](wayland.md)** | The GUI stack becomes Wayland. SchWM and SchDE are **deleted** — actually gone from the tree, not marked; Schcom survives and changes its wire format. |
| **[hybrid.md](hybrid.md)** | The kernel becomes hybrid — drivers move out into userspace processes, starting with the GPU. **Partly built**: all four mechanisms exist and the GPU driver already runs outside the kernel. |
| ~~**hybrid.md**~~ | **Built.** All four mechanisms exist, the GPU driver runs outside the kernel, it survives being killed, and the display mode can be changed from a process. H4 (USB) was *withdrawn* rather than left open — USB is the bus peripherals arrive on, not a peripheral. |
| ~~init.md~~ | **Built.** PID 1 is now a role the kernel refills; the description moved to [../architecture/init.md](../architecture/init.md). |
And the one they all wait on:
@ -39,6 +39,12 @@ And the one that says which parts of the above are actually being worked on:
|---|---|
| **[1.0.md](1.0.md)** | The finish line. Four gates, and everything else explicitly deferred. |
And the one the hybrid mechanisms were built for:
| | |
|---|---|
| **[ldls.md](ldls.md)** | The Linux Driver Layer for SchOS. Someone else's drivers, in a process, reached through `/dev/pci` — because no project can write a driver for every device that exists. **Not started.** |
And one that is a tool rather than a capability:
| | |
@ -99,8 +105,9 @@ requirements, and building the interface against it is what keeps it a general
mechanism rather than a hole shaped like something foreign. The compositor is
still a demanding consumer — it is no longer the *only* one available.
Four mechanisms, all built. One eviction done, one to go, then the foreign
driver host.
Four mechanisms, all built. One eviction done, the second withdrawn with its
reasons recorded, and the foreign driver host — [ldls.md](ldls.md) — is what
they were for.
### 4. `wayland.md` — with the GPU driver either side of the seam

214
docs/plan/ldls.md Normal file
View file

@ -0,0 +1,214 @@
# LDLS — the Linux Driver Layer for SchOS
SchOS cannot write a driver for every device that exists. No project can. The
only route to the hardware support an established system has is to run someone
else's drivers, and in practice "someone else's drivers" means Linux's.
[hybrid.md](hybrid.md) built the mechanisms for this and said so in as many
words — MMIO, DMA, interrupts and config space to a process, with SchOS's own
drivers as the first consumers so the interface would be general rather than a
hole shaped like something foreign. `vgpud` proved it. **LDLS is what those
mechanisms were built for.**
---
## The rules
These are the spine. Everything below is an implementation of them, and
anything that violates one is wrong however well it works.
### 1. Nothing GPL ever enters an image we build
The driver is **the user's**, obtained by them, loaded on their machine. SchOS
ships the layer and never the thing it loads. This is not a new position — it
was decided on 2026-08-08 and is written up in
[../architecture/devices.md](../architecture/devices.md) and
[../licensing.md](../licensing.md) §2:
> Apache-2.0 code and GPL-2.0-only code cannot be combined into one
> distributable work. It is not a matter of taste or of how carefully the files
> are kept apart in the tree.
`RAMDISK_EXCLUDE` and `NOTPORTS` are the existing mechanisms that keep this
true mechanically rather than by intention, and LDLS must stay inside them.
### 2. It runs in a process, never in the kernel
Two independent reasons, and either alone would decide it.
**Legally**, a module linked into our kernel is the strongest derivative-work
argument that exists — it is the case the FSF has always made. A module
combined with our shim *inside a user process, on the user's machine, at their
direction* is a different thing at arm's length, and it is the position
FreeBSD has shipped from for about a decade: a BSD-licensed `linuxkpi` in base,
GPL `drm-kmod` installed by the user from ports.
**Practically**, this is foreign code. A driver we did not write, cannot fully
audit and did not choose the quality of, running with the machine's memory in
reach, is exactly the code that should not be able to take the machine down.
Crash isolation is not a nicety here; it is the reason.
### 3. Facts, not expression
The shim has to present Linux's internal API, and that API is expressed in GPL
headers. The line LDLS holds:
| | |
|---|---|
| A symbol exists, and takes these arguments | **fact** — reproduce it |
| A struct has these fields in this order at these offsets | **fact** — a compiled driver indexes them, so there is no other way to express it |
| The comment above it | **expression** — never |
| The macro body, the inline function body, the implementation | **expression** — never |
Every KPI file records where its shape came from. Not decoration: it is the
difference between a clean-room reimplementation and a transcription, and the
only time anyone will ask is the time it matters.
This is the same discipline [../licensing.md](../licensing.md) §5 already
describes — *"the xHCI, USB, PCI, fbdev and ELF specifications were all read
and implemented directly, which is why those drivers have no licence
entanglement at all"* — applied to a case where the specification is thinner.
> **Not legal advice, and it should be checked by someone whose job that is
> before anything ships.** The reasoning rests on interfaces being outside
> copyright (EU Software Directive Art. 1(2)) and on *Google v. Oracle* (2021),
> and neither is a blanket permission.
### 4. One kernel version at a time, pinned and named
Linux states in writing that it has no stable in-kernel ABI —
`Documentation/process/stable-api-nonsense.rst` exists to say so. Believing
them is cheaper than being surprised by them.
So LDLS targets **one version**, says which, and is re-pinned deliberately.
FreeBSD's LinuxKPI does exactly this per drm-kmod release. A layer that claims
to support "Linux drivers" without a version is claiming something nobody has
ever delivered.
### 5. The unresolved-symbol list is the specification
Do not guess the API surface. **Load a real module and let the loader say what
is missing.** That list is the work queue, it is exact, and it is available
before a single line of shim is written.
---
## What it is made of
Four pieces, of which three are ordinary and one is the project.
### The loader — ours, clean, and small
A `.ko` is a relocatable ELF64 object. Loading one is: lay out the allocatable
sections, apply the relocations, resolve the undefined symbols against a table,
and find the entry point. SchOS already parses ELF in `kernel/fs/exec.c`, for a
different object type but with the same plumbing.
There is no licence question here at all — ELF is a published specification and
so is the relocation set.
### The KPI — the shim, and the actual work
Our Apache-2.0 reimplementation of the Linux symbols a given driver needs,
organised under Linux's own header names so it is navigable: `kpi/slab.h`,
`kpi/pci.h`, `kpi/netdevice.h`. Rule 3 governs every file.
### The back end — already built
`/dev/pci`: `PCIIOC_INFO`, `PCIIOC_CLAIM`, `PCIIOC_CFG`, `mmap` of a BAR,
`PCIIOC_DMA`, `PCIIOC_IRQ`. A Linux driver's `pci_iomap`, `dma_alloc_coherent`
and `request_irq` land on these, which is the whole reason they were specified
in terms of PCI devices rather than in terms of drivers.
### The class bridge — the one new kernel interface
This is the piece that does not exist, and it is the interesting one.
A hosted driver produces packets in a process. SchOS's network stack consumes
them through `struct netdev_ops` in the kernel — `transmit`, `receive`, `mac`,
`link_up`, four entries and nothing else. So what is missing is a **netdev
whose backend is a process**: the kernel publishes the vtable, the four calls
are forwarded over a descriptor, and lwIP never learns that the driver is not
in the kernel.
It is the exact analogue of what `display_ops` was for the GPU, and
`netdev.h`'s own opening comment already anticipates the shape — *"the stack
should ask for the network interfaces and get whatever is bound … exactly as a
compositor asks for the display and does not care which driver answers."*
One device, one process, claimed and released, on the `/dev/pci` discipline.
---
## Milestones
| | | |
|---|---|---|
| **L0** | **The loader.** Parse a real `.ko`, lay it out, relocate it, and **print every unresolved symbol**. Nothing executes. The output is the measured API surface. | |
| **L1** | **Stubs.** Every symbol resolves, to a function that logs its name and stops. `init_module` runs and dies at the first real call — which yields the *call order*, the second thing you need and the other thing you cannot guess. | |
| **L2** | **The hardware.** `probe()` receives a working `struct pci_dev`, maps its BARs, allocates coherent DMA and registers an interrupt, all through `/dev/pci`. The driver reaches its device. | |
| **L3** | **A packet.** The process-backed netdev, wired to the driver's transmit and receive paths. **`ping` over a Linux driver.** This is the milestone that proves the whole idea and the one that cannot be skipped. | |
| **L4** | **Kill it.** The host process dies, the kernel clears bus mastering and reclaims the DMA, a new host starts and the interface comes back. | free — H2 built all of this |
L4 is listed for completeness rather than as work. `pciuser_stop()`,
`pciuser_release_all()` and `KeepAlive` already do it, and that they apply
unchanged to a foreign driver is the point of having built them against our
own.
### The first target: `8139too`, not `r8169`
The obvious choice is a Realtek `r8169`, because that silicon is everywhere.
It is the wrong first choice for one decisive reason: **QEMU cannot emulate
it**, so L0 through L3 would have no way to be tested, and a driver layer
developed against hardware you cannot run is a layer nobody can debug.
QEMU emulates `rtl8139`. `8139too` is small, self-contained, needs no firmware
blob, and uses the thin `mii` helpers rather than dragging in phylib — which
`r8169` does, and which is a substantial surface on its own.
So: **`8139too` to build the layer, `r8169` to prove it was general.** The
second is the interesting one and it is much cheaper once the first works.
This is the `/dev/pci` lesson applied backwards. There, a native driver was the
first consumer so the interface would not be shaped like something foreign.
Here the interface is fixed by Linux, and what is chosen instead is the
*smallest real consumer that can be tested* — because the risk is no longer
designing the wrong interface, it is building a hundred stubs nobody has ever
run.
---
## What it will not do
- **It is not a security boundary, and here that is worse than usual.** There
is no IOMMU, so a hosted driver can point its device at any physical address.
For SchOS's own drivers that isolates bugs and not intent, which
[hybrid.md](hybrid.md) says at length. For *foreign* drivers the distinction
between bug and intent is one we are in no position to make at all. This is
the single strongest argument for an IOMMU that this project has produced,
and it should be recorded as such rather than waved past.
- **No answer yet on `EXPORT_SYMBOL_GPL`.** Linux marks much of its internal
API GPL-only and modules carry `MODULE_LICENSE`. A layer that supplies those
symbols while asserting a licence it does not hold would be a deliberate
circumvention, and LDLS must not do that. What it should do instead —
refuse such modules, or simply not claim to be Linux and let the user decide
what they load on their own machine — is **undecided**, and is the first
thing to take real advice on.
- **No firmware loading.** `request_firmware` is a stub that fails. Many
drivers need a blob, most blobs are redistributable only under their own
terms, and that is a second licensing problem which is not this one.
- **No module-to-module dependencies.** One object, no `depmod`, no symbol
exports between hosted modules.
- **No sysfs, procfs or debugfs.** Drivers that only *report* through them will
work; drivers that are *configured* through them will not.
- **No unload.** `module_exit` is never called; the way to stop a driver is to
kill the process, which H2 made a supported operation rather than a failure.
- **No sleeping-context fidelity.** Linux drivers assume a kernel's rules about
what may sleep, what runs in interrupt context and what `spin_lock_irqsave`
costs. A process has different rules, and where they differ the shim will be
wrong in ways that show up as rare races rather than as failures to build.
This is the deepest technical risk in the whole plan.
- **It will not support "Linux drivers".** It will support *one driver, on one
kernel version, that has been tried*. Anything beyond that is a claim, and
the list of what has actually been run belongs in the architecture document
when there is one.