L4 was listed as "free -- H2 built all of this" and never actually tested,
which is a claim rather than a result. Tested now, and it holds:
ldlsd running ping 2/2; holds index 7 and DMA slots 3 and 4
killed interface DOWN, device released, bus mastering
cleared, DMA reclaimed -- back to vgpud's 3 blocks
ping fails honestly: sendto: -5
a second ldlsd ping 3/3, and the arena hands back the same addresses
pciuser_release_all() and netuser_release_all() sit beside each other in
exit() for the same reason, and that they apply unchanged to a foreign driver
is the whole point of having built them against our own.
With that, every milestone on ldls.md is done, so it leaves docs/plan/ for
docs/architecture/ -- the rule docs/plan/README.md states and the one that
keeps the two directories meaning different things. The new document describes
what is, not what was planned: the five rules, the four pieces, what it costs
(polling, no stack protector, locks that are no-ops), that it can be killed,
and a "what it does not do" that leads with one driver, one kernel version,
tried once.
Sixteen source files pointed at docs/plan/ldls.md and would have been dangling
the moment it moved. All repointed, and the milestone references -- which the
architecture doc deliberately does not have -- rewritten to name sections
instead, so a reader lands somewhere real.
The register row closes with it. hybrid.md freed a slot, ldls.md took it, and
it is now free again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
232 lines
10 KiB
Markdown
232 lines
10 KiB
Markdown
# LDLS — running somebody else's drivers
|
|
|
|
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 Linux's
|
|
drivers, and LDLS is what does that.
|
|
|
|
It works. `8139too` — an unmodified GPL kernel module — loads into a SchOS
|
|
process, drives QEMU's rtl8139 through `/dev/pci`, and carries packets:
|
|
|
|
```
|
|
# ldlsd /lib/modules/8139too.ko /lib/modules/btf &
|
|
kpi: the driver read a MAC off the hardware: 52:54:00:ab:cd:ef
|
|
ndo_open returned 0
|
|
carrying frames. the interface is ldls0.
|
|
|
|
# ping 10.9.0.2 4
|
|
64 bytes from 10.9.0.2: icmp_seq=1 time=30 ms
|
|
...
|
|
4 packets transmitted, 4 received, 0% packet loss
|
|
```
|
|
|
|
`10.9.0.0/24` is reachable only through `ldls0`. The address on it came from
|
|
DHCP, over the same driver.
|
|
|
|
---
|
|
|
|
## The rules it is built on
|
|
|
|
Five, 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. SchOS
|
|
ships the layer and never the thing it loads. `.gitignore` refuses `*.ko`,
|
|
`make ldls-stage` is the one command that puts a module into a local image and
|
|
is deliberately not part of `all`, and `make ldls-unstage` takes it out again.
|
|
|
|
[../licensing.md](../licensing.md) §6 is the whole argument. The short form is
|
|
that GPL obligations attach to *distribution*, we distribute Apache-2.0 code
|
|
containing no GPL, and the combination happens in a user process on the user's
|
|
machine — so no combined work is distributed by anyone.
|
|
|
|
### 2. It runs in a process, never in the kernel
|
|
|
|
A module linked into our kernel is the strongest derivative-work argument that
|
|
exists. A module combined with our shim inside a user process is a different
|
|
thing at arm's length — the position FreeBSD has shipped from for a decade.
|
|
|
|
And practically: this is foreign code. A driver we did not write, cannot fully
|
|
audit and did not choose the quality of should not be able to take the machine
|
|
down.
|
|
|
|
### 3. Facts, not expression
|
|
|
|
A symbol's signature and a struct's field offsets are facts a compiled driver
|
|
depends on. Comments, macro bodies and implementations are expression and are
|
|
never carried over. Every file in `ldls/kpi/` records where its shape came
|
|
from.
|
|
|
|
Mostly the question does not arise, because of rule 3½:
|
|
|
|
### 3½. The layouts are not in our source at all
|
|
|
|
A kernel built with `CONFIG_DEBUG_INFO_BTF` publishes
|
|
`/sys/kernel/btf/vmlinux`: every struct layout and every function prototype, as
|
|
generated type data describing the kernel that is installed. `ldls/btf.c` reads
|
|
it at load time, on the user's machine, from the user's own kernel.
|
|
|
|
```
|
|
$ ldls-types /sys/kernel/btf/vmlinux --need
|
|
pci_driver probe offset 16 size 8
|
|
pci_dev resource offset 1000 size 1088
|
|
resource start offset 0 size 8
|
|
all present
|
|
```
|
|
|
|
Nothing there is ours to have transcribed, and it is right for the kernel
|
|
actually present rather than one pinned by guessing.
|
|
|
|
### 4. One kernel version at a time
|
|
|
|
Linux states in writing that it has no stable in-kernel ABI. BTF mostly
|
|
dissolves this, because it describes the kernel that is there — but a kernel
|
|
without BTF needs hand-written layouts for a named version, and that is when
|
|
this rule bites.
|
|
|
|
### 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. Every symbol LDLS does not implement still *resolves*, to a stub that
|
|
reports itself and returns zero, so the driver carries on and the next thing it
|
|
wants becomes visible. The work queue keeps writing itself.
|
|
|
|
---
|
|
|
|
## The four pieces
|
|
|
|
### The loader — `ldls/ko.c`
|
|
|
|
A `.ko` is an ELF64 relocatable object. Index the sections, place the
|
|
allocatable ones, resolve the undefined symbols, apply the relocations. Written
|
|
from the ELF-64 gABI and the x86-64 psABI, both published specifications, so
|
|
there is no licence question in it at all.
|
|
|
|
Measured over a stock 6.18.46 tree: **2,440 modules, every one lays out and
|
|
relocates to completion**, five relocation types, nothing unhandled. That sweep
|
|
is also what retired the fixed section table — `nvidia-modeset` has 7,069
|
|
sections against a median of 50 — so the caller supplies it and there is no
|
|
limit to run into.
|
|
|
|
It is **freestanding**: no libc, no allocation, no I/O. The same source builds
|
|
as a host tool, where a format failure costs a second, and as the SchOS-side
|
|
host. That is the arrangement `mkschfs` and the kernel already share.
|
|
|
|
#### Two things it has to fix in the object
|
|
|
|
**The `%gs` stack guard.** Modules are built `-mstack-protector-guard-reg=gs`,
|
|
so the canary is fetched through the GS segment — an ABI between the kernel and
|
|
its own per-CPU area that a host which is not that kernel cannot provide.
|
|
`ko_defuse_gs()` rewrites those references to `xor %reg, %reg`. There are two
|
|
forms: the prologue *loads* the guard with `mov`, the epilogue *compares* it
|
|
with `sub` before a `jne`. `xor` is right for both — it zeroes the canary and
|
|
sets ZF, so the branch falls through as a passing check does. It checks every
|
|
site before patching any and refuses the whole operation on anything it does
|
|
not recognise.
|
|
|
|
**Where it is loaded.** `-mcmodel=kernel` means `R_X86_64_32S` relocations, so
|
|
the object has to live where a signed 32-bit absolute address reaches. A
|
|
process's mmap area starts at `0x40000000`, which is inside that window; on the
|
|
host harness `MAP_32BIT` reaches it from the other end.
|
|
|
|
### The KPI — `ldls/kpi/`
|
|
|
|
The Linux symbols, implemented. Register access to a mapped BAR; the PCI calls
|
|
to `/dev/pci`; allocation to `mmap` and `PCIIOC_DMA`; `register_netdev` to
|
|
`/dev/netdev`. Organised under Linux's own header names so a reader who knows
|
|
where something lives in Linux knows where it is here.
|
|
|
|
`kpi_io_ok()` bounds every `ioread`/`iowrite` against the mapping `pci_iomap`
|
|
returned. A stray access would otherwise be a page fault with no context, and
|
|
with no IOMMU a hosted driver's wild writes have to be caught early or not at
|
|
all.
|
|
|
|
### The back end — `/dev/pci`
|
|
|
|
Already built, for `vgpud`, before any of this existed.
|
|
[hybrid.md](../plan/hybrid.md) insisted the interface be about *PCI devices*
|
|
rather than about a driver model, on the argument that an interface whose first
|
|
consumers are native drivers is a general mechanism rather than a hole shaped
|
|
like something foreign. A Linux driver's `pci_iomap`, `dma_alloc_coherent` and
|
|
`request_irq` land on it unaltered, which is that argument being collected.
|
|
|
|
### The class bridge — `/dev/netdev`
|
|
|
|
The one piece the kernel had to grow. `netdev_ops` is four entries —
|
|
`transmit`, `receive`, `mac`, `link_up` — so a process claims the device,
|
|
supplies a MAC, and `read()`/`write()` carry the frames. `/dev/pci` with the
|
|
arrows reversed: that hands a device *to* a process, this lets a process *be*
|
|
one. See [devices.md](devices.md).
|
|
|
|
lwIP never learns that the thing behind the vtable is not a driver.
|
|
|
|
---
|
|
|
|
## What it costs
|
|
|
|
**It polls.** A Linux driver expects its interrupt handler to be called from an
|
|
interrupt. `/dev/pci` does deliver them — by blocking `read()`, which needs a
|
|
thread `ldlsd` does not have — so the driver's NAPI poll is called from a loop
|
|
instead. The same work, in a context LDLS can provide, at the cost of latency
|
|
and a spinning process.
|
|
|
|
**The stack protector is gone**, per `ko_defuse_gs()` above. Foreign code, no
|
|
IOMMU, one hardening measure fewer.
|
|
|
|
**A lock is a no-op.** In one process with no preemption inside the driver
|
|
there is nobody to race with. That is correct right up until a driver has an
|
|
interrupt thread, and it is the deepest unfixed assumption in the layer.
|
|
|
|
---
|
|
|
|
## It can be killed
|
|
|
|
The whole H2 machinery applies to a foreign driver unchanged, which was the
|
|
point of having built it against our own:
|
|
|
|
| | |
|
|
|---|---|
|
|
| `ldlsd` running | `ping` works; it holds the device and two DMA blocks |
|
|
| killed | interface **down**, device released, bus mastering cleared, **DMA reclaimed** |
|
|
| `ping` | fails honestly — `sendto: -5` |
|
|
| a second `ldlsd` | `ping` works again, and the arena hands back the same addresses |
|
|
|
|
`pciuser_release_all()` and `netuser_release_all()` sit beside each other in
|
|
`exit()` for the same reason: a process holding a device is one that may crash.
|
|
|
|
---
|
|
|
|
## What it does not do
|
|
|
|
- **One driver, one kernel version, tried once.** `8139too` on 6.18.46 against
|
|
QEMU's rtl8139. Nothing here has been tried on a second driver, a second
|
|
kernel, or real silicon, and until it has, "LDLS runs Linux drivers" is a
|
|
claim rather than a description.
|
|
- **No interrupts**, per the polling note above.
|
|
- **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; for *foreign*
|
|
drivers the distinction between bug and intent is one we are in no position
|
|
to make. This is the strongest argument for an IOMMU the project has
|
|
produced.
|
|
- **It relies on SchOS having no NX.** Every mapping is executable because
|
|
`EFER.NXE` is never set. If that is ever fixed — and it should be — `ldlsd`
|
|
needs a way to ask for executable memory, and `mmap` does not offer one.
|
|
- **Nothing is ever freed.** A hosted driver's allocations last as long as the
|
|
process, which is a leak in the strict sense and the right trade for a
|
|
program that will be killed to restart it.
|
|
- **BTF has to come from somewhere.** SchOS has no `/sys/kernel/btf/vmlinux`,
|
|
so the user brings it with the module, from the kernel the module was built
|
|
for. A kernel built without `CONFIG_DEBUG_INFO_BTF` has neither.
|
|
- **No config-space writes.** `/dev/pci` has none, deliberately — a write is
|
|
how a BAR gets moved out from under the kernel — so a driver that needs one
|
|
is refused loudly rather than quietly succeeded.
|
|
- **No module-to-module dependencies, no firmware loading, no sysfs, no
|
|
unload.** `request_firmware` fails, `module_exit` is never called, and the
|
|
way to stop a driver is to kill the process.
|
|
- **Some symbols are still logging stubs.** Deliberate, per rule 5 — but a
|
|
driver reaching one gets zero and carries on, and `memcpy` proved how quiet
|
|
that can be: it was a stub while the receive path was being brought up, so
|
|
every frame arrived as the right *length* of zeroes and nothing reported an
|
|
error. **A stub that returns zero is not safe for a function whose whole job
|
|
is a side effect.**
|