1.0: the acceptance test drops its reboot clause (a live system is the finish line) and passes. Schcom is kept with a job: the installer's GUI first, SchDE on top later (roadmap section 5). The old 'LDLS ships no Linux code' policy is gone from ldls/devices/licensing/Makefile/.gitignore/ldlsd; NOTICE records the r8169 modules (GPL-2.0-only) and the Realtek firmware. The download button points at the v0.1.0 release asset; the stale 64 MB ISO under docs/Website is removed. mkaroot.sh's -dumbSched comment no longer claims SchOS has no threads. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
455 lines
23 KiB
Markdown
455 lines
23 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, and on more than one driver. `8139too` and `e1000` — unmodified GPL
|
||
kernel modules — load into a SchOS process, drive their hardware through
|
||
`/dev/pci`, and carry packets (both under QEMU). A third, Linux's `r8169`, is
|
||
the NIC driver on the ASUS TUF A15 laptop, and it is the first that needs a
|
||
*set* of modules and not one; it is described under *A set of modules*. It has
|
||
registered its interface and read its PHY on that laptop; whether it carries
|
||
traffic there has not been confirmed yet.
|
||
|
||
```
|
||
# 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 e1000 run is the more interesting of the two, because it is a **handover**:
|
||
the kernel was driving that NIC, and gave it up.
|
||
|
||
```
|
||
net: eth1 is gone; interface 1 removed
|
||
e1000: released to userspace; the interface is down
|
||
rehomed 23 %gs per-CPU reference(s)
|
||
calling probe, with 82 of 155 symbols implemented
|
||
kpi: the driver read a MAC off the hardware: 52:54:00:12:34:57
|
||
net: ldls0 attached as interface 2, asking for an address
|
||
net: ldls0 10.9.0.15/255.255.255.0 via 10.9.0.2
|
||
```
|
||
|
||
Everything the second driver needed that the first did not is below, and the
|
||
pattern in it is worth naming up front: **none of it was a missing feature.**
|
||
Every one was a call that succeeded, returned zero, and did nothing.
|
||
|
||
---
|
||
|
||
## The rules it is built on
|
||
|
||
Five, and everything else is an implementation of them.
|
||
|
||
### 1. Linux's drivers ship in the image
|
||
|
||
SchOS is GPL-2.0-or-later, and GPL-2.0-only code combines with it, so there is no
|
||
licensing reason to keep Linux's drivers out of an image. They are in: the **r8169 set**
|
||
(`mdio-bus`, `libphy`, `mdio_devres`, the Realtek PHY driver and `r8169`, unmodified) and
|
||
the RTL8168 PHY firmware are what the ISO carries for the laptop's network, with their
|
||
licence and corresponding source recorded in `NOTICE`
|
||
([../licensing.md](../licensing.md) §6).
|
||
|
||
(An earlier version of this rule said the opposite — that LDLS would ship the layer and never
|
||
the thing it loads. That was written when SchOS was Apache-2.0 and the combination could not
|
||
be distributed; it stopped being true with the move to the GPL, and the owner has said Linux
|
||
code and blobs may be shipped.)
|
||
|
||
What is left is mechanical. The repository holds no `*.ko` — a binary does not belong in git, so
|
||
`.gitignore` refuses them — and `make ldls-r8169` (before `make iso`) stages the set from a
|
||
Linux build, together with its BTF, because a module, its dependencies and the BTF that
|
||
describes them have to come from the same build. `make ldls-stage KO=…` does the same for any
|
||
other module, which is then a distribution of *that* module's code, under its own licence.
|
||
|
||
### 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.
|
||
|
||
#### Three 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.
|
||
|
||
**Every other per-CPU variable.** The guard turned out to be one instance of a
|
||
general problem. `current` is another — `mov %gs:const_current_task(%rip), %r8`
|
||
— and e1000 has 23 such references across four variables. Defusing does not
|
||
work here: the guard's *value* is arbitrary, and `current`'s is not, because
|
||
the driver dereferences what it reads.
|
||
|
||
What does work is that **a per-CPU variable in a single-threaded host is just a
|
||
variable**. `ko_fixup_percpu()` takes the `%gs` off, leaving the same opcode
|
||
reading the same width in the same direction, from storage the host supplies
|
||
per name. Eight bytes become seven and a `nop`. The displacement is measured
|
||
from the end of the *shortened* instruction, and the displacement is not always
|
||
last — `cmpb $0x0, %gs:softnet_data+0x75(%rip)` has an immediate after it — so
|
||
there is just enough of an x86 decoder in there to know how long the
|
||
instruction is. Anything it does not recognise is refused with the symbol
|
||
named, which is rule 5 again.
|
||
|
||
`ldlsd` hands out zeroed blobs, except for `current`, which points at the rest
|
||
of its own: a driver that walks into the task it thinks it has reads zeroes.
|
||
|
||
**The compiler's thunks.** `__x86_indirect_thunk_rN` and `__x86_return_thunk`
|
||
are retpolines, not API, and they are pure control flow — `jmp *%r8` and `ret`.
|
||
A stub cannot stand in for one, because a stub's whole behaviour is to log a
|
||
name and return, and what a thunk is *for* is the jump. `ldls/kpi/thunk.c` is
|
||
fifteen lines of assembly and is the single highest-value file in the layer.
|
||
|
||
That one cost a day. e1000 refills its receive ring through a function pointer
|
||
and the compiler made it a *tail* call:
|
||
|
||
```
|
||
mov 0x2a0(%rbx),%r8 ; adapter->alloc_rx_buf
|
||
... ; compute E1000_DESC_UNUSED(ring) == 255
|
||
jmp __x86_indirect_thunk_r8
|
||
```
|
||
|
||
so the driver asked for 255 receive buffers, the shim answered "yes", and the
|
||
symptom was a NIC that transmitted perfectly and received nothing with `RDT`
|
||
sitting at zero.
|
||
|
||
**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.
|
||
|
||
### A set of modules — what r8169 needed
|
||
|
||
Linux's `r8169` is not one module. It needs `mdio-bus`, `libphy`, `mdio_devres`,
|
||
the Realtek PHY driver, the NIC itself, the RTL PHY firmware, and the BTF of the
|
||
kernel they came from. `make ldls-r8169` stages all of that from the running
|
||
kernel; `etc/schinit/ldls-r8169.job` starts `ldlsd` on it at boot, with its output
|
||
in `/var/log/ldls-r8169.log`. The job has `KeepAlive = no`: a driver that crashes
|
||
on real hardware would otherwise be restarted into the same crash every few seconds.
|
||
|
||
What that added to `ldlsd`:
|
||
|
||
- **A comma-separated module set**, loaded in dependency order, each linking
|
||
against the ones before it, with the BTF read before any `init_module` runs.
|
||
A library module (`libphy`) just exports symbols to the next; the PCI `probe`
|
||
runs once, at the end, for whichever module registered a `pci_driver`.
|
||
- **A driver core**: bus match, driver probe, so the PHY driver binds to the PHY
|
||
the way it does in Linux; **devres**; a kernel `printf`; real `udelay`/`msleep`/
|
||
`ktime`; and **1000 Hz `jiffies`**, because the module's `HZ` is compiled in and
|
||
phylib re-arms its link poll `HZ` jiffies out.
|
||
- **Firmware** read from `/lib/firmware`.
|
||
- **phylib's data**, the `link_mode_params` table and its bitmaps.
|
||
- **The symbols whose stub-zero was a lie**: list validity, `try_module_get`,
|
||
`dev_err_probe`, `eth_platform_get_mac_address`, `napi_consume_skb` (as a stub it
|
||
leaked one buffer per transmitted frame until the DMA heap ran out), and the
|
||
rest of the table at the end of this document.
|
||
- `pdev->bus` is a real `pci_bus`; `net_device.priv_len` is set from BTF (the
|
||
driver's `memset` of its ring bookkeeping is bounds-checked against it, and
|
||
`__fortify_panic` fired on a length of zero); and `cmovcc` from per-CPU memory is
|
||
rewritten like the other `%gs` forms.
|
||
- **`__preempt_count` starts at `PREEMPT_ENABLED`.** mdio-bus keeps the register
|
||
it has just read in `r11` across `preempt_enable()`, which on x86 is
|
||
`decl %gs:__preempt_count; je`, to a call that preserves every register. With the
|
||
count at zero the call was made, into a logging stub whose syscall left `RFLAGS`
|
||
(0x246) in `r11` — so every MDIO read returned `0x02460246`. `preempt_schedule` is
|
||
now a bare `ret`.
|
||
- `__fortify_panic` names the module and function (`r8169: rtl_open.cold+0x…`) so
|
||
a photograph of the screen is enough, and exits instead of spinning on a core and
|
||
holding `/dev/netdev`.
|
||
|
||
The kernel did two things for it. It turns **ASPM off on Realtek NIC links** at
|
||
boot, as `r8169` does through `pci_disable_link_state`, which a process cannot;
|
||
and a network interface that **appears after boot starts the stack** — with the
|
||
native driver out of the registry the laptop boots with no interface at all, and
|
||
`ldls0` arrives later. `ldlsd` says which refusal it got when attaching: `EBUSY` is
|
||
another process, anything else is the kernel.
|
||
|
||
The native `drivers/net/r8169/` stays in the tree, with Linux's 8168H setup, as
|
||
the fallback; it is out of the registry.
|
||
|
||
### 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.
|
||
|
||
Three pieces of it are not register access and are worth naming, because each
|
||
one is something a driver does that a poll loop does not obviously provide:
|
||
|
||
- **The interrupt handler runs.** Not to receive packets — LDLS was already
|
||
calling the NAPI poll by hand — but because a driver's handler is where it
|
||
does everything *else*. e1000 queues its watchdog from there, and the
|
||
watchdog is what sets `TCTL.EN`. A driver whose handler never ran was a
|
||
driver whose transmitter was never switched on. Calling it from the loop is
|
||
safe: a handler's first act is to read its cause register, and with nothing
|
||
pending it returns `IRQ_NONE`.
|
||
- **Deferred work runs.** `queue_delayed_work_on` keeps the item and
|
||
`kpi_work_run()` calls what is due from the same loop. `jiffies` had to start
|
||
moving for that to mean anything; it comes from `uptime()`, at 100 Hz.
|
||
- **`netif_carrier_on`/`off` write the bit.** `netif_carrier_ok()` is an inline
|
||
that tests `__LINK_STATE_NOCARRIER` in `net_device.state`, so a no-op means
|
||
the driver reads "the carrier is already up" and skips its whole link-up
|
||
path. The bit number comes from the kernel's BTF (`btf_enum_value()`), not
|
||
from us — rule 3½ applied to a constant rather than an offset.
|
||
|
||
And **DMA in small pieces**. `dma_alloc_coherent` maps onto `PCIIOC_DMA`
|
||
exactly; `dma_map_single` does not. 8139too never needed it, because it copies
|
||
each frame into a ring it allocated coherently up front; e1000 hands the device
|
||
individual buffers. Since a process cannot know the physical address of an
|
||
ordinary page, those buffers come from the arena too, a chunk at a time behind
|
||
a bump pointer.
|
||
|
||
The address arithmetic for that is not a call at all. `dma_map_single` is a
|
||
static inline, so what reaches the shim is already expanded: the module
|
||
computes `pa = va - page_offset_base`, then `page = vmemmap_base + (pa >> 12) *
|
||
64`. Both bases are undefined *data* symbols — which means LDLS supplies them,
|
||
and setting both to zero makes the whole thing reversible with no lookup table.
|
||
|
||
### The back end — `/dev/pci`
|
||
|
||
Already built, for `vgpud`, before any of this existed. The design 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.
|
||
|
||
### Giving a device up — `release`
|
||
|
||
A driver in the kernel has a `release` hook, and `/dev/pci` calls it before
|
||
handing its device to a process. The honest answer to "may I have the one you are
|
||
using" was *no* until a driver could be told to let go; e1000 is the first that can.
|
||
|
||
Letting go is three things, and the third was missed at first:
|
||
|
||
1. quiesce the hardware — mask interrupts, clear `RCTL` and `TCTL`;
|
||
2. **mask the interrupt line.** The kernel's handler is gone and the device is
|
||
about to be brought up by a driver in a process, so nothing will acknowledge
|
||
the cause. The console said what that looks like: `irq 11: 449905
|
||
interrupts/second -- a device on this line is not acknowledging it`. It
|
||
comes back when the process asks for it with `PCIIOC_IRQ`.
|
||
3. **take the interface out of the stack.** `net_detach()` is the missing half
|
||
of `net_attach_late()`, and it went missing because nothing had ever taken a
|
||
device back. An interface left behind keeps its address and the route to its
|
||
subnet, so the stack goes on choosing it, and every frame reaches a
|
||
`transmit` that now correctly refuses. Packets stop and nothing reports an
|
||
error — which is indistinguishable from a driver in a process that does not
|
||
work, and was mistaken for exactly that.
|
||
|
||
### Ports — the TSS I/O permission bitmap
|
||
|
||
A Linux driver does not always ask. `ioread32` is a call the KPI can implement;
|
||
`outl` is an *instruction* compiled into the module, and e1000's reset path
|
||
picks it deliberately — "these controllers can't ack the 64-bit write when
|
||
issuing the reset, so use IO-mapping as a workaround". On QEMU's 82540EM there
|
||
is no route to a working NIC that avoids it.
|
||
|
||
So a process holding a device gets that device's I/O BARs, **and no other
|
||
ports**. x86 offers two ways to grant this and they are far apart: `IOPL=3` is
|
||
one flag and hands over all 65536 ports, including the interrupt controller,
|
||
the timer and the disk; the TSS I/O permission bitmap is per port. A foreign
|
||
driver already has DMA with no IOMMU, and the answer to "this is not a security
|
||
boundary" is not to move it further — so it is the bitmap, at 8 KB per CPU of
|
||
resident memory. See [../../include/arch/ioperm.h](../../include/arch/ioperm.h).
|
||
|
||
Writing it found a latent hole. The TSS's I/O map base is at offset 102 and the
|
||
old code wrote offset 64; the field it meant to set stayed 0, and a base of 0
|
||
with a limit of `0x67` does not mean "no bitmap" — it means the bitmap starts at
|
||
the top of the TSS. The CPU had been checking ports 0–831 against the TSS's own
|
||
bytes, which are `RSP0`: a kernel stack pointer whose zero bits are permission.
|
||
|
||
### 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 handler, the NAPI poll and the driver's
|
||
deferred work are all called from one 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, and so are the
|
||
retpolines, per `thunk.c`. Foreign code, no IOMMU, three hardening measures
|
||
fewer.
|
||
|
||
**Per-CPU means per-process.** Correct with one thread, and it is the same
|
||
assumption as the no-op locks below, arrived at from the other side.
|
||
|
||
**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
|
||
|
||
- **Three drivers, one kernel version.** `8139too` and `e1000` on 6.18.46
|
||
against QEMU, and `r8169` on that version against QEMU (where it finds no
|
||
device) and on one laptop (where it gets as far as the PHY). The second driver
|
||
was worth more than the first: it found the per-CPU problem, the retpoline
|
||
problem, the port-I/O problem, the interrupt storm and a seven-year-old hole in
|
||
the TSS. Nothing here has been tried on a second kernel, and "LDLS runs Linux
|
||
drivers" is still a claim about a category from a sample of three.
|
||
- **No interrupts**, per the polling note above.
|
||
- **`jiffies` assumes `HZ` is 1000**, which is what the kernels LDLS loads from
|
||
are built with (`ldls_load.hz` can say otherwise). It comes from the
|
||
monotonic clock in milliseconds.
|
||
- **One work item at a time, on the main loop.** A driver whose deferred work
|
||
blocks stops everything, including the transmit path it is probably waiting
|
||
for.
|
||
- **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 `make ldls-stage` / `ldls-r8169` copy it from the Linux kernel the module
|
||
came from. A kernel built without `CONFIG_DEBUG_INFO_BTF` has neither.
|
||
- **There is address-space groundwork nothing uses.** `kernel/arch/x86_64/vm.c`
|
||
maps the kernel image a second time at `LDLS_KALIAS`, in the top 2 GB where a
|
||
`-mcmodel=kernel` module can reach, with a `vmemmap` and a module area beside it
|
||
— at every boot — and `include/ldls/ldls.h` describes a `/dev/ldls` control
|
||
channel to load modules into the kernel. That was the start of an in-kernel
|
||
loader, contradicting rule 2. Nothing calls either, and it should be finished
|
||
as a decision or deleted.
|
||
- **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 sysfs and no unload.** Modules depend on each other and firmware is read
|
||
from `/lib/firmware`, but `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 — and the
|
||
single most expensive thing in this layer. **A stub that returns zero is not
|
||
safe when zero is a meaningful answer**, and the list of ways that bit is
|
||
now long enough to be a rule rather than an anecdote:
|
||
|
||
| stub | what zero meant | what it looked like |
|
||
|---|---|---|
|
||
| `memcpy` | the copy did not happen | frames arrived as the right *length* of zeroes |
|
||
| `_find_next_bit` | "bit 0 is set" | `for_each_cpu` never terminated; `ndo_open` hung |
|
||
| `netif_carrier_off` | the carrier is already up | driver skipped its link-up path; `TCTL.EN` never set |
|
||
| `__x86_indirect_thunk_r8` | the jump was discarded | 255 receive buffers requested, none allocated, `RDT` at 0 |
|
||
| `dma_map_page_attrs` | bus address 0 | the NIC pointed at the bottom of physical memory |
|
||
|
||
Not one of these reported an error, and only the last is a fault waiting to
|
||
happen. A search function, a predicate, a jump and an address are all things
|
||
for which zero is a specific answer rather than an absent one.
|