ldls: the same loader, on SchOS
docs/plan/ldls.md milestone L3b. ldls/ko.c, btf.c and stub.c were written
freestanding so they would build unchanged on both sides; cmd/ldlsd collects
that promise -- the same three files, linked against lib/ instead of a libc,
in an ordinary SchOS process.
# ldlsd /lib/modules/8139too.ko /lib/modules/btf
module 8139too license GPL wants 94 symbols
defused 6 %gs stack-guard reference(s)
calling init_module
1 _printk(...)
| 8139too: 8139too Fast Ethernet driver 0.9.28
2 __pci_register_driver(...)
btf 122279 types
layout pci_driver.probe +16 pci_dev 2680 bytes
driver "8139too" devices it claims: 10ec:8139 ... and 18 more
calling probe
1 alloc_etherdev_mqs(0x368, 0x1)
probe returned -12 after 1 stub call(s)
Byte for byte the host harness's behaviour, which is what the shared source
was for. Two earlier predictions came good with no work: a process's mmap area
starts at 0x40000000, inside the window R_X86_64_32S needs, and the kernel
never sets EFER.NXE so every mapping is executable. The second is a gap rather
than a feature and is now recorded as one -- LDLS is relying on the absence of
a protection an OS should have.
One thing did not just work. The first run faulted before probe's first call,
err 5 -- present, read, user -- at a kernel address, with the rip resolving to
mov %gs:0x0(%rip),%rax # rtl8139_init_one+0x26
Modules are built -mstack-protector-guard-reg=gs, so the canary comes through
the GS segment, and that is an ABI between the kernel and its own per-CPU area
which a host that is not that kernel cannot provide. ko_defuse_gs() rewrites
those references to xor %reg,%reg.
There are two forms and the second is easy to miss: the prologue loads the
guard with mov (8b) and the epilogue compares it with sub (2b) before a jne to
__stack_chk_fail. xor is right for both -- it zeroes the saved canary and sets
ZF, so the epilogue's branch falls through exactly as a passing check does. Six
sites in 8139too.
It checks every site before patching any and refuses the whole operation on
anything it does not recognise. That refusal is what found the sub form, by
declining to guess -- a loader that half-understands an instruction should not
be editing it.
make ldls-stage/ldls-unstage put a module and its kernel's BTF into the local
sysroot and take them out again. Not part of `all`, and .gitignore refuses
*.ko, so an image only carries somebody else's GPL code between two commands
you typed on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
2a441af3c6
commit
3fd1cc5279
7 changed files with 738 additions and 3 deletions
|
|
@ -95,7 +95,7 @@ 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`** | L0, L1, L2 and L3a done — the loader handles 2,440 stock modules; 449 of 500 run `init_module`; `8139too`'s `probe()` runs to completion; and `/dev/netdev` lets a process be an interface, with `ping` working over one. L3b (the SchOS-side host) and L3c (the fifteen symbols, for real) open. No Linux driver has yet touched hardware. |
|
||||
| **`ldls.md`** | L0, L1, L2 and L3a done — the loader handles 2,440 stock modules; 449 of 500 run `init_module`; `8139too`'s `probe()` runs to completion; and `/dev/netdev` lets a process be an interface, with `ping` working over one. L3b done too — `ldlsd` runs the same loader on SchOS and gets a real driver's `probe()` to return. L3c (the fifteen symbols, against `/dev/pci`) open. No Linux driver has yet touched hardware. |
|
||||
| **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. |
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ One device, one process, claimed and released, on the `/dev/pci` discipline.
|
|||
| **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. | **done**, except `/dev/pci` — see below |
|
||||
| **L3a** | **The class bridge.** `/dev/netdev`: a process claims it and becomes an interface. `ping` over a *process*. | **done** — `kernel/dev/netuser.c`, `cmd/netecho` |
|
||||
| **L3b** | The SchOS-side host: `ldls-run`'s loader and BTF work, as a program on SchOS, reaching hardware through `/dev/pci`. | |
|
||||
| **L3b** | The SchOS-side host: `ldls-run`'s loader and BTF work, as a program on SchOS. | **done** — `cmd/ldlsd`; `/dev/pci` is L3c's half |
|
||||
| **L3c** | The fifteen symbols, implemented rather than stubbed, against a real rtl8139. | |
|
||||
| **L3** | **A packet.** The two joined: **`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 |
|
||||
|
|
@ -547,6 +547,94 @@ allowed to include lwIP's headers.
|
|||
|
||||
---
|
||||
|
||||
## L3b: the same loader, on SchOS
|
||||
|
||||
`ldls/ko.c`, `ldls/btf.c` and `ldls/stub.c` were written freestanding so they
|
||||
would build unchanged on both sides. `cmd/ldlsd` is the collection of that
|
||||
promise: the same three files, linked against `lib/` instead of a libc, in an
|
||||
ordinary SchOS process.
|
||||
|
||||
```
|
||||
# ldlsd /lib/modules/8139too.ko /lib/modules/btf
|
||||
/lib/modules/8139too.ko
|
||||
module 8139too
|
||||
license GPL
|
||||
wants 94 symbols
|
||||
defused 6 %gs stack-guard reference(s)
|
||||
loaded 33988 bytes at 0x000000004001f000
|
||||
|
||||
calling init_module
|
||||
1 _printk(...)
|
||||
| 8139too: 8139too Fast Ethernet driver 0.9.28
|
||||
2 __pci_register_driver(...)
|
||||
init_module returned 0 after 2 stub call(s)
|
||||
|
||||
btf 122279 types
|
||||
layout pci_driver.probe +16 pci_dev 2680 bytes
|
||||
driver "8139too"
|
||||
devices it claims:
|
||||
10ec:8139
|
||||
... and 18 more
|
||||
|
||||
calling probe
|
||||
1 alloc_etherdev_mqs(0x368, 0x1)
|
||||
probe returned -12 after 1 stub call(s)
|
||||
```
|
||||
|
||||
Byte for byte the behaviour of the host harness, which is what the shared
|
||||
source was for. Two predictions from earlier milestones came good without any
|
||||
work: a process's mmap area starts at `0x40000000`, so the module lands inside
|
||||
the window `R_X86_64_32S` needs, and the kernel never sets `EFER.NXE`, so every
|
||||
mapping is executable.
|
||||
|
||||
That second one is **a gap, not a feature**. A process being able to execute
|
||||
its own heap is normally a thing an operating system prevents, and LDLS is
|
||||
quietly relying on its absence. It belongs on Gate 0's list.
|
||||
|
||||
### The one thing that did not just work
|
||||
|
||||
The first run on SchOS faulted before probe's first call:
|
||||
|
||||
```
|
||||
pid 8 ldlsd: trap 14 err 5 on cpu 0 rip 0x4002138e addr 0xffff80007de5fc60
|
||||
```
|
||||
|
||||
`err 5` is present, read, user mode, and the address is a *kernel* one. The rip
|
||||
resolved to `.text+0x206e`, which is:
|
||||
|
||||
```
|
||||
mov %gs:0x0(%rip),%rax # rtl8139_init_one+0x26
|
||||
```
|
||||
|
||||
Linux builds modules with `-mstack-protector-guard-reg=gs`, so the stack canary
|
||||
is fetched through the GS segment. That is an ABI between the kernel and its
|
||||
own per-CPU area, and **a host that is not that kernel cannot provide it** — on
|
||||
SchOS the GS base belongs to the kernel, so a user-mode read of it faults on a
|
||||
kernel address.
|
||||
|
||||
`ko_defuse_gs()` rewrites those references to `xor %reg, %reg`. There are two
|
||||
forms and the second is easy to miss: the prologue *loads* the guard with `mov`
|
||||
(`8b`), and the epilogue *compares* it with `sub` (`2b`) followed by a `jne` to
|
||||
`__stack_chk_fail`. `xor` is correct for both — it makes the saved canary zero,
|
||||
and it sets ZF so the epilogue's branch falls through exactly as a passing
|
||||
check does. Six sites in `8139too`.
|
||||
|
||||
It checks every site before patching any, and refuses the whole operation on
|
||||
anything it does not recognise. A loader that half-understands an instruction
|
||||
should not be editing it — and that refusal is what found the `sub` form, by
|
||||
declining to guess.
|
||||
|
||||
### Getting the files there
|
||||
|
||||
`make ldls-stage KO=… BTF=…` copies a module and its kernel's BTF into the
|
||||
local sysroot. It is deliberately not part of `all`, `.gitignore` refuses
|
||||
`*.ko`, and `make ldls-unstage` removes them again. An image built between
|
||||
those two commands has somebody else's GPL code in it and must not be
|
||||
redistributed — which is exactly what [../licensing.md](../licensing.md) §6
|
||||
says about the combination, and why it is a thing you type on purpose.
|
||||
|
||||
---
|
||||
|
||||
## What it will not do
|
||||
|
||||
- **It is not a security boundary, and here that is worse than usual.** There
|
||||
|
|
@ -592,6 +680,14 @@ allowed to include lwIP's headers.
|
|||
- **`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.
|
||||
- **The stack protector is gone from a hosted driver.** `ko_defuse_gs()`
|
||||
removes it because the ABI it depends on cannot be supplied here. That is a
|
||||
hardening measure lost, on foreign code, in a host with no IOMMU — worth
|
||||
saying plainly rather than filing as a detail.
|
||||
- **LDLS 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 nothing in `mmap` currently
|
||||
offers one.
|
||||
- **BTF has to come from somewhere.** On Linux it is `/sys/kernel/btf/vmlinux`.
|
||||
SchOS has no such file and never will — so the user brings the BTF along with
|
||||
the module, from the kernel the module was built for. Both are theirs,
|
||||
|
|
|
|||
Loading…
Reference in a new issue