ldls: ping, over a Linux driver
docs/plan/ldls.md L3d, and with it L3 -- the milestone the plan calls the one
that proves the whole idea and the one that cannot be skipped.
# ldlsd /lib/modules/8139too.ko /lib/modules/btf &
kpi: the driver read a MAC off the hardware: 52:54:00🆎cd:ef
ndo_open returned 0
carrying frames. the interface is ldls0.
# cat /proc/netif
interface ldls0 state up address 10.9.0.15 gateway 10.9.0.2
# ping 10.9.0.2 4
64 bytes from 10.9.0.2: icmp_seq=1 time=30 ms
64 bytes from 10.9.0.2: icmp_seq=2 time=20 ms
64 bytes from 10.9.0.2: icmp_seq=3 time=20 ms
64 bytes from 10.9.0.2: icmp_seq=4 time=20 ms
10.9.0.0/24 is reachable only through ldls0, and ldls0 is 8139too running in a
SchOS process. The address was obtained by DHCP over the same driver first.
lwIP -> netdev_ops -> /dev/netdev -> ldlsd -> ndo_start_xmit -> a BAR mapped by
/dev/pci -> the wire, and the whole way back. Nothing in the kernel knows a
Linux driver is involved; nothing in the driver knows it is not in Linux.
It polls rather than taking interrupts. /dev/pci delivers them by blocking
read(), which needs a thread this program 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.
Four things Linux's alloc_etherdev does that allocating does not, each found by
the driver faulting on it: dev->dev_addr is a pointer aimed at perm_addr;
dev->mc and dev->uc are list heads and are empty only when they point at
themselves; dev->_tx is an array of netdev_queue, because netif_start_queue
clears a bit in _tx[0].state; and __LINK_STATE_START is set by dev_open before
ndo_open is called.
Two did not announce themselves, and they are the ones worth recording.
netif_running(dev) gates 8139too's receive loop. Without it the chip received
the frame, the driver acknowledged the interrupt and left the loop, and nothing
came out -- while every counter said packets were arriving. Found by reading
the chip's own CBR register through the BAR and seeing the hardware had written
600 bytes the driver never collected.
memcpy was still a logging stub. The driver copied each frame into its sk_buff,
the copy did not happen, and the stack was handed exactly the right length of
zeroes; DHCP never completed and nothing reported an error. A stub that returns
zero is not safe for a function whose whole job is a side effect, and the
memory primitives are the worst case because they fail silently and plausibly.
ldls/kpi/string.c exists because of it.
Still one driver, one kernel version, tried once, against an emulated card --
which is what the plan has always promised and is worth repeating next to a
working ping.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
42f98130c4
commit
888c48fd96
9 changed files with 809 additions and 14 deletions
|
|
@ -184,8 +184,8 @@ One device, one process, claimed and released, on the `/dev/pci` discipline.
|
|||
| **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. | **done** — `cmd/ldlsd`; `/dev/pci` is L3c's half |
|
||||
| **L3c** | The fifteen symbols, implemented rather than stubbed, against a real rtl8139. | **done** — `ldls/kpi/`; the driver reads the real MAC |
|
||||
| **L3d** | The packet path: `ndo_open`, `ndo_start_xmit`, receive. | |
|
||||
| **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. | |
|
||||
| **L3d** | The packet path: `ndo_open`, `ndo_start_xmit`, receive. | **done** |
|
||||
| **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. | **done** |
|
||||
| **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()`,
|
||||
|
|
@ -725,6 +725,79 @@ that is L3d.
|
|||
|
||||
---
|
||||
|
||||
## L3d: ping, over a Linux driver
|
||||
|
||||
```
|
||||
# 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.
|
||||
|
||||
# cat /proc/netif
|
||||
interface ldls0
|
||||
mac 52:54:00:ab:cd:ef
|
||||
state up
|
||||
address 10.9.0.15
|
||||
gateway 10.9.0.2
|
||||
|
||||
# ping 10.9.0.2 4
|
||||
64 bytes from 10.9.0.2: icmp_seq=1 time=30 ms
|
||||
64 bytes from 10.9.0.2: icmp_seq=2 time=20 ms
|
||||
64 bytes from 10.9.0.2: icmp_seq=3 time=20 ms
|
||||
64 bytes from 10.9.0.2: icmp_seq=4 time=20 ms
|
||||
```
|
||||
|
||||
`10.9.0.0/24` is reachable **only** through `ldls0`, and `ldls0` is `8139too`
|
||||
running in a SchOS process. The address on it was obtained by DHCP, over the
|
||||
same driver, before the ping.
|
||||
|
||||
`lwIP` → `netdev_ops` → `/dev/netdev` → `ldlsd` → `ndo_start_xmit` → a BAR
|
||||
mapped by `/dev/pci` → the wire, and the whole way back. Nothing in the kernel
|
||||
knows a Linux driver is involved; nothing in the driver knows it is not in
|
||||
Linux.
|
||||
|
||||
### 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 this
|
||||
program does not have. So `ldlsd` calls the driver's NAPI poll from a loop
|
||||
instead: the same work, in a context LDLS can actually provide. It costs
|
||||
latency and a spinning process, and it is written down here rather than hidden.
|
||||
|
||||
### Four things `alloc_etherdev` does that allocating does not
|
||||
|
||||
A driver gets its `net_device` from `alloc_etherdev_mqs`, and the function does
|
||||
considerably more than return memory. Each of these was found by the driver
|
||||
faulting on it, which is a slow way to learn a list and an extremely reliable
|
||||
one:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `dev->dev_addr` | a **pointer**, aimed at the device's own `perm_addr`. The driver writes its MAC into the chip with `*(u32 *)dev->dev_addr`. |
|
||||
| `dev->mc`, `dev->uc` | list heads, which are *empty* when they point at themselves. Zeroed means a null `next`, and the receive-mode setup walks them. |
|
||||
| `dev->_tx` | an array of `netdev_queue`, because `netif_start_queue` clears a bit in `dev->_tx[0].state`. |
|
||||
| `dev->state` | `__LINK_STATE_START`, set by Linux's `dev_open` **before** it calls `ndo_open`. |
|
||||
|
||||
### The two that did not announce themselves
|
||||
|
||||
Everything above crashed, which made it easy. Two did not.
|
||||
|
||||
**`netif_running(dev)`.** Without `__LINK_STATE_START`, 8139too's receive loop
|
||||
exited immediately — so the chip received the frame, the driver acknowledged
|
||||
the interrupt, and nothing came out. Every counter said packets were arriving.
|
||||
It was found by reading the chip's own `CBR` register through the BAR and
|
||||
seeing that the hardware had written 600 bytes the driver never collected.
|
||||
|
||||
**`memcpy`.** It was still a logging stub while the receive path was being
|
||||
brought up. The driver copied each frame into its `sk_buff`, the copy did not
|
||||
happen, and the stack was handed exactly the right *length* of zeroes. DHCP
|
||||
never completed and nothing anywhere reported an error. **A stub that returns
|
||||
zero is not safe for a function whose whole job is a side effect** — and the
|
||||
memory primitives are the worst case, because they fail silently and
|
||||
plausibly. `ldls/kpi/string.c` exists because of it.
|
||||
|
||||
---
|
||||
|
||||
## What it will not do
|
||||
|
||||
- **It is not a security boundary, and here that is worse than usual.** There
|
||||
|
|
@ -795,12 +868,16 @@ that is L3d.
|
|||
with the link down, because nothing in lwIP supports taking an interface out
|
||||
of its list. From the stack's point of view that is the same thing; from
|
||||
`/proc/netif`'s it is a `ldls0` that stays listed after its process is gone.
|
||||
- **No frame has moved.** L3c ends at a successful `probe()`. The packet path
|
||||
— `ndo_open`, `ndo_start_xmit`, receive, and the skb handling all three need
|
||||
— is L3d and is the larger half.
|
||||
- **41 of 94 symbols.** The rest are still logging stubs, which is deliberate:
|
||||
an unimplemented symbol that reports itself keeps the work queue writing
|
||||
itself, exactly as it did at L1.
|
||||
- **It polls instead of taking interrupts**, per the note above. A busy
|
||||
interface spins a processor; an idle one wakes 200 times before sleeping.
|
||||
- **One driver, one kernel version, tried once.** `8139too` on 6.18.46 against
|
||||
QEMU's rtl8139. That is what the milestone table has always promised and it
|
||||
is worth repeating next to a working `ping`: nothing here has been tried on a
|
||||
second driver, a second kernel, or real silicon.
|
||||
- **Some symbols are still logging stubs.** That is deliberate — an
|
||||
unimplemented symbol that reports itself keeps the work queue writing itself,
|
||||
exactly as it did at L1 — but it means a driver reaching one gets zero and
|
||||
carries on, and `memcpy` proved how quiet that can be.
|
||||
- **A lock is a no-op.** In one process with no preemption inside the driver
|
||||
there is nobody to race with, which is correct right up until the driver has
|
||||
an interrupt thread. That is the sleeping-context assumption named above and
|
||||
|
|
|
|||
Loading…
Reference in a new issue