ldls: the stubs, and the driver's own voice
docs/plan/ldls.md milestone L1. Every undefined symbol resolves to a
trampoline -- twenty generated bytes that load the symbol's index and jump to
a shared thunk, which saves the argument registers and reports the call. Then
init_module is simply called.
$ ldls-run 8139too.ko
module 8139too
license GPL
wants 94 symbols
calling init_module at 0x416fc020
1 _printk(0x416fb6c0, 0, 0)
| 8139too: 8139too Fast Ethernet driver 0.9.28
2 __pci_register_driver(0x416fb400, 0x416fff80, 0x416fbdbb)
init_module returned 0 after 2 stub call(s)
That second line is the driver's own banner, printed through a _printk that
is ours, from a string in the module's own .rodata that a relocation put
there. There is no stronger evidence that a load worked than the loaded thing
speaking.
Stubs return zero rather than stopping, which departs from what the milestone
originally said and does so deliberately: stopping at the first call tells you
one name, carrying on tells you the sequence, and the sequence is the thing a
symbol table cannot give you. A driver that then dereferences the zero faults,
and that is a result too -- it says how far it got before it needed the kernel
to have really done something.
The first run failed on neither of those. It failed on a relocation:
ldls-run: relocate: a signed 32-bit relocation does not fit
A module is compiled -mcmodel=kernel, so the compiler emits R_X86_64_32S for
anything it believes lives in the top 2 GB, and an ordinary mmap at 0x7f...
has nowhere to put it. Linux gives modules a region within 2 GB of the kernel
text; the harness uses MAP_32BIT to reach the same window from the other end.
On SchOS this costs nothing -- a process's mmap area starts at 0x40000000 and
is already inside it -- but it is a real constraint on where the host may load
a module, and a loader that did not check its own arithmetic would have found
it much later and much more confusingly.
Swept over 500 stock modules: 449 ran init to completion, 22 faulted on a
stub's zero, 29 have no init_module, and zero hit a loader error.
Recorded honestly in "what it will not do": running init is not working. Every
symbol is a stub, so what has been shown is that drivers ask for things in a
readable order, not that anything answers. Also recorded: every undefined
symbol in a module is NOTYPE, so the stubs cannot tell jiffies from
pci_iomap and both get a trampoline -- BTF distinguishes VAR from FUNC and
that is where the fix belongs rather than in a guess.
make ldls-check is 23 checks now, five of them L1's, all against
ldls/test/refmod.c which is ours and now has an init_module of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
901ae03a69
commit
7a7c7611e0
8 changed files with 636 additions and 10 deletions
|
|
@ -179,7 +179,7 @@ One device, one process, claimed and released, on the `/dev/pci` discipline.
|
|||
| | | |
|
||||
|---|---|---|
|
||||
| **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. | **done** — `ldls/ko.c`, `make ldls-check` |
|
||||
| **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. | |
|
||||
| **L1** | **Stubs.** Every symbol resolves, to a function that logs its name. `init_module` runs, which yields the *call order* — the second thing you need and the other thing you cannot guess. | **done** — `ldls/stub.c`, `ldls-run` |
|
||||
| **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 |
|
||||
|
|
@ -278,6 +278,75 @@ that cannot happen by accident.
|
|||
|
||||
---
|
||||
|
||||
## L1, and the driver's own voice
|
||||
|
||||
Every undefined symbol resolves to a trampoline — twenty generated bytes that
|
||||
load the symbol's index and jump to a shared thunk, which saves the argument
|
||||
registers and reports the call. `init_module` is then simply called.
|
||||
|
||||
```
|
||||
$ ldls-run 8139too.ko
|
||||
module 8139too
|
||||
license GPL
|
||||
wants 94 symbols
|
||||
loaded 33988 bytes at 0x416f8000, 94 stubs at 0x40377000
|
||||
|
||||
calling init_module at 0x416fc020
|
||||
|
||||
1 _printk(0x416fb6c0, 0, 0)
|
||||
| 8139too: 8139too Fast Ethernet driver 0.9.28
|
||||
2 __pci_register_driver(0x416fb400, 0x416fff80, 0x416fbdbb)
|
||||
|
||||
init_module returned 0 after 2 stub call(s)
|
||||
```
|
||||
|
||||
**That second line is the driver's own banner**, printed through a `_printk`
|
||||
that is ours, from a string in the module's own `.rodata` that a relocation put
|
||||
there. There is no stronger evidence that a load worked than the loaded thing
|
||||
speaking.
|
||||
|
||||
The stubs return zero rather than stopping, which is a deliberate departure
|
||||
from what this milestone originally said. Stopping at the first call tells you
|
||||
one name; carrying on tells you the *sequence*, and the sequence is the thing
|
||||
that cannot be read off a symbol table. A driver that then dereferences the
|
||||
zero faults, and that is a result too — it says exactly how far it got before
|
||||
it needed the kernel to have really done something.
|
||||
|
||||
### The constraint nobody would guess
|
||||
|
||||
The first run failed on a relocation, not on anything to do with stubs:
|
||||
|
||||
```
|
||||
ldls-run: relocate: a signed 32-bit relocation does not fit
|
||||
```
|
||||
|
||||
A module is compiled `-mcmodel=kernel`, so the compiler emits `R_X86_64_32S` —
|
||||
a **signed 32-bit absolute address** — for anything it believes lives in the
|
||||
top 2 GB. Load the object at an ordinary `mmap` address around `0x7f…` and
|
||||
those relocations have nowhere to go. Linux avoids this by giving modules a
|
||||
region within 2 GB of the kernel text; the harness uses `MAP_32BIT` to reach
|
||||
the same place from the other end.
|
||||
|
||||
**On SchOS this costs nothing** — a process's mmap area starts at `0x40000000`
|
||||
and is already inside the window — but it is a real constraint on where the
|
||||
LDLS host may put a module, and it would have been found much later and much
|
||||
more confusingly without a loader that checks its own arithmetic.
|
||||
|
||||
### What running 500 of them says
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `init_module` ran and returned | **449** |
|
||||
| faulted on a stub's zero | 22 |
|
||||
| no `init_module` at all | 29 |
|
||||
| **loader errors** | **0** |
|
||||
|
||||
The 22 are not failures of the loader; they are drivers that needed an
|
||||
allocation to come back non-null. The 29 without an entry point are mostly
|
||||
library modules that exist to be depended on.
|
||||
|
||||
---
|
||||
|
||||
## What it will not do
|
||||
|
||||
- **It is not a security boundary, and here that is worse than usual.** There
|
||||
|
|
@ -309,11 +378,20 @@ that cannot happen by accident.
|
|||
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.
|
||||
- **Loading is not running.** L0 proves 2,440 modules can be laid out and
|
||||
relocated. That says the *format* is handled and says nothing whatever about
|
||||
whether any of them would work: not one symbol is implemented, and the
|
||||
distance between "relocates" and "transmits a packet" is the entire rest of
|
||||
this plan.
|
||||
- **Running init is not working.** L0 proves 2,440 modules lay out and
|
||||
relocate; L1 proves 449 of 500 run their init to completion. Both say the
|
||||
*mechanism* is right and neither says a driver would function: every symbol
|
||||
is a stub that logs and returns zero, so what has been demonstrated is that
|
||||
drivers ask for things in a readable order, not that anything answers them.
|
||||
The distance from here to a packet is the rest of this plan.
|
||||
- **The stubs cannot tell code from data.** Every undefined symbol in a module
|
||||
is `NOTYPE` in the ELF symbol table, so `jiffies` and `pci_iomap` are
|
||||
indistinguishable and both get a trampoline. A driver that *reads* `jiffies`
|
||||
reads trampoline bytes. BTF distinguishes them — `VAR` against `FUNC` — and
|
||||
that is where the fix belongs rather than in a guess.
|
||||
- **`ldls-run` is a development harness, not the deliverable.** It runs on the
|
||||
build machine so that loader failures cost a second instead of a boot. The
|
||||
thing that ships is the SchOS-side host.
|
||||
- **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
|
||||
|
|
|
|||
Loading…
Reference in a new issue