ldls: a Linux driver, driving real hardware

docs/plan/ldls.md milestone L3c. ldls/kpi/ implements 41 of 8139too's 94
symbols rather than logging them: register access to a mapped BAR, the PCI
calls to /dev/pci, allocation to mmap and PCIIOC_DMA, register_netdev to
/dev/netdev.

    # ldlsd /lib/modules/8139too.ko /lib/modules/btf
      found   10ec:8139 at 00:05.0
      bar 1    0x0000000081060000  256 mem
      calling probe, with 41 of 94 symbols implemented
      kpi: mapped BAR 1 -- phys 0x0000000081060000, 256 bytes
      kpi: the driver read a MAC off the hardware: 52:54:00🆎cd:ef
      netuser: ldls0 is a process now, 52:54:0🆎cd:ef
      probe returned 0 after 0 stub call(s)

52:54:00🆎cd:ef is what the machine was configured with. Nothing invented
it: the driver bit-banged the card's EEPROM, serially, through a BAR mapped by
/dev/pci, and handed back what it found. That it matches means the loader, the
relocations, the BTF offsets, the BAR mapping and the register accessors are
all correct at once, which is why it was the thing to aim at. Zero stub calls:
every symbol probe reaches is implemented.

Two bugs, one of them the kernel's.

Mine first: pci_iomap checked a failed mmap for -1 only, while SchOS returns
-errno. It returned -13 as a pointer, the driver took that as its register
base, and the first write landed at 0x4e -- thirteen below where it meant to
be.

Underneath it, the real one. sys_mmap rounds a length up to a page, the only
granularity an MMU has, and pciuser_mmio_ok() compared that against the BAR's
own size. rtl8139's registers are 256 bytes, the request became 4096, and the
answer was -EACCES for a device the process legitimately held. A BAR smaller
than a page could never be mapped at all, and nothing had noticed because the
only consumer until now was virtio-gpu, whose BARs are large. The check is now
against the pages the BAR lies in, and the widening is named where it happens:
a sub-page BAR hands over the whole page, forced by the hardware rather than
chosen.

That is exactly what hybrid.md said a foreign driver would be good for --
"what it asks for is what a foreign driver would ask for too" -- arriving as a
concrete bug rather than a prediction.

kpi_io_ok() now 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. It is also what turned the fault above into two lines naming the offsets.

Not done, and said so in the output rather than only in the doc: no frame has
moved. ndo_open is never called and there is no transmit or receive path, so
ldlsd releases the interface instead of leaving it up -- a link that reports
itself up with nothing draining its queue is a black hole, and the stack would
send DHCP into it for ever. Down is the true answer until packets move. That
is L3d, now listed separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-14 09:59:14 +02:00
commit 42f98130c4
11 changed files with 1071 additions and 44 deletions

View file

@ -802,12 +802,19 @@ $(BUILD)/lib/%.o: lib/%.S
# own -- a specific rule beats the pattern one below. ldls/ko.c, btf.c and # own -- a specific rule beats the pattern one below. ldls/ko.c, btf.c and
# stub.c are freestanding and build unchanged for either side, which is the # stub.c are freestanding and build unchanged for either side, which is the
# whole point of having written them that way. # whole point of having written them that way.
LDLS_GUEST = $(addprefix $(BUILD)/ldls/guest/, ko.o btf.o stub.o) LDLS_GUEST = $(addprefix $(BUILD)/ldls/guest/, ko.o btf.o stub.o) \
$(addprefix $(BUILD)/ldls/guest/kpi/, kpi.o pci.o io.o net.o)
$(BUILD)/ldls/guest/%.o: ldls/%.c ldls/ko.h ldls/btf.h ldls/stub.h $(BUILD)/ldls/guest/%.o: ldls/%.c ldls/ko.h ldls/btf.h ldls/stub.h
@mkdir -p $(BUILD)/ldls/guest @mkdir -p $(BUILD)/ldls/guest
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) -c $< -o $@
# The KPI -- the Linux symbols, implemented. Guest-only: it reaches /dev/pci
# and /dev/netdev, which exist on SchOS and nowhere else.
$(BUILD)/ldls/guest/kpi/%.o: ldls/kpi/%.c ldls/kpi/kpi.h
@mkdir -p $(BUILD)/ldls/guest/kpi
$(CC) $(CFLAGS) -I./ldls -c $< -o $@
$(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST) $(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST)
@mkdir -p $(BUILD)/cmd @mkdir -p $(BUILD)/cmd
$(CC) $(CFLAGS) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o $(CC) $(CFLAGS) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o

View file

@ -28,6 +28,9 @@
#include "ko.h" #include "ko.h"
#include "stub.h" #include "stub.h"
#include "btf.h" #include "btf.h"
#include "pciuser.h"
#include "kpi/kpi.h"
extern void kpi_unregister_netdev(void *dev);
// --- small output, since lib/ has no printf --------------------------------- // --- small output, since lib/ has no printf ---------------------------------
@ -179,12 +182,29 @@ ldls_stub(long idx, unsigned long *args)
struct rctx { struct ko *k; struct stubs *s; }; struct rctx { struct ko *k; struct stubs *s; };
// The KPI first, a logging stub second.
//
// An unimplemented symbol still resolves -- to something that reports itself
// and returns zero -- so the driver carries on and the *next* thing it wants
// becomes visible. That is what made L1's measurement possible and it stays
// useful for exactly the same reason: the work queue keeps writing itself.
static int g_kpi_used;
static int k_nundef_global;
static int static int
resolve(void *ctx, const char *name, unsigned long long *out) resolve(void *ctx, const char *name, unsigned long long *out)
{ {
struct rctx *r = (struct rctx *)ctx; struct rctx *r = (struct rctx *)ctx;
void *fn;
int i; int i;
fn = kpi_find(name);
if (fn) {
*out = (unsigned long long)fn;
g_kpi_used++;
return 0;
}
for (i = 0; i < r->k->nundef; i++) for (i = 0; i < r->k->nundef; i++)
if (streq(r->k->undef[i], name)) { if (streq(r->k->undef[i], name)) {
*out = stub_addr(r->s, i); *out = stub_addr(r->s, i);
@ -210,6 +230,88 @@ find_symbol(struct ko *k, const char *want)
return 0; return 0;
} }
// --- reading and writing a struct at an offset BTF gave us --------------------
static unsigned int
rd32(unsigned long base, unsigned int off)
{ unsigned int v; bcopy_(&v, (void *)(base + off), 4); return v; }
static unsigned long
rd64(unsigned long base, unsigned int off)
{ unsigned long v; bcopy_(&v, (void *)(base + off), 8); return v; }
static void
wr16(unsigned long base, unsigned int off, unsigned short v)
{ bcopy_((void *)(base + off), &v, 2); }
static void
wr32(unsigned long base, unsigned int off, unsigned int v)
{ bcopy_((void *)(base + off), &v, 4); }
static void
wr64(unsigned long base, unsigned int off, unsigned long v)
{ bcopy_((void *)(base + off), &v, 8); }
// --- finding the hardware ------------------------------------------------------
// Claim the first device the driver says it supports.
//
// The driver's own id_table is the authority on what it can drive, so the
// match is made against that rather than against anything LDLS knows -- which
// is the property that keeps this general: nothing here has heard of an
// rtl8139.
static int
claim_for(unsigned long idtab, unsigned int id_size, unsigned int id_vendor,
unsigned int id_device, struct pciuser_info *out)
{
int count = 0, i, n;
kpi.pcifd = open("/dev/pci", O_RDWR);
if (kpi.pcifd < 0) { puts_("ldlsd: cannot open /dev/pci\n"); return -1; }
if (ioctl(kpi.pcifd, PCIIOC_COUNT, &count) < 0) return -1;
for (i = 0; i < count; i++) {
struct pciuser_info in;
in.index = (unsigned int)i;
if (ioctl(kpi.pcifd, PCIIOC_INFO, &in) < 0)
continue;
for (n = 0; n < 4096; n++) {
unsigned long e = idtab + (unsigned long)n * id_size;
unsigned int v = rd32(e, id_vendor), d = rd32(e, id_device);
if (!v && !d) break;
if (v != in.vendor || d != in.device) continue;
puts_(" found "); puthexn(in.vendor, 4); puts_(":");
puthexn(in.device, 4); puts_(" at ");
puthexn(in.bus, 2); puts_(":"); puthexn(in.slot, 2);
puts_("."); puthexn(in.func, 1);
if (in.driver[0]) { puts_(", held by "); puts_(in.driver); }
puts_("\n");
if (ioctl(kpi.pcifd, PCIIOC_CLAIM, &i) < 0) {
puts_(" the kernel would not release it\n");
return -1;
}
// Re-read: claiming is what enables memory space and bus
// mastering, and a BAR read before that can legitimately be zero.
in.index = (unsigned int)i;
if (ioctl(kpi.pcifd, PCIIOC_INFO, &in) < 0) return -1;
kpi.index = i;
for (int b = 0; b < 6; b++) {
kpi.bar_base[b] = in.bar[b].base;
kpi.bar_size[b] = in.bar[b].size;
kpi.bar_flags[b] = in.bar[b].flags;
}
*out = in;
return 0;
}
}
puts_(" no device on this machine matches the driver's id_table\n");
return -1;
}
// --- being the PCI core ------------------------------------------------------- // --- being the PCI core -------------------------------------------------------
static unsigned int static unsigned int
@ -232,26 +334,6 @@ lay(struct btf *b, const char *type, const char *member)
return off; return off;
} }
static unsigned int
rd32(unsigned long base, unsigned int off)
{ unsigned int v; bcopy_(&v, (void *)(base + off), 4); return v; }
static unsigned long
rd64(unsigned long base, unsigned int off)
{ unsigned long v; bcopy_(&v, (void *)(base + off), 8); return v; }
static void
wr16(unsigned long base, unsigned int off, unsigned short v)
{ bcopy_((void *)(base + off), &v, 2); }
static void
wr32(unsigned long base, unsigned int off, unsigned int v)
{ bcopy_((void *)(base + off), &v, 4); }
static void
wr64(unsigned long base, unsigned int off, unsigned long v)
{ bcopy_((void *)(base + off), &v, 8); }
#define LDLS_IORESOURCE_IO 0x00000100 #define LDLS_IORESOURCE_IO 0x00000100
#define LDLS_IORESOURCE_MEM 0x00000200 #define LDLS_IORESOURCE_MEM 0x00000200
@ -333,25 +415,59 @@ drive_probe(const char *btfpath)
if (nid > 6) { puts_(" ... and "); putn(nid - 6); puts_(" more\n"); } if (nid > 6) { puts_(" ... and "); putn(nid - 6); puts_(" more\n"); }
if (!nid) return; if (!nid) return;
// Find the real device, claim it, and describe *it* rather than something
// made up. This is the line between L2 and L3c: probe used to be handed a
// pci_dev whose shape was right and whose contents were invented, and now
// every field in it came off the bus.
{
struct pciuser_info in;
if (claim_for(idtab, id_size, id_vendor, id_device, &in) < 0)
return;
pdev = (unsigned long)getmem(dev_size); pdev = (unsigned long)getmem(dev_size);
if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); return; } if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); return; }
{
unsigned long r0 = pdev + dev_res;
unsigned long r1 = r0 + res_size;
wr16(pdev, dev_vendor, (unsigned short)rd32(idtab, id_vendor)); wr16(pdev, dev_vendor, in.vendor);
wr16(pdev, dev_device, (unsigned short)rd32(idtab, id_device)); wr16(pdev, dev_device, in.device);
wr32(pdev, dev_devfn, 0x18); wr32(pdev, dev_devfn, (unsigned int)((in.slot << 3) | in.func));
wr32(pdev, dev_irq, 11); wr32(pdev, dev_irq, in.irq);
wr64(r0, res_start, 0xc000);
wr64(r0, res_end, 0xc0ff); for (int b = 0; b < 6; b++) {
wr64(r0, res_flags, LDLS_IORESOURCE_IO); unsigned long r = pdev + dev_res + (unsigned long)b * res_size;
wr64(r1, res_start, 0xfebc0000); if (!in.bar[b].base)
wr64(r1, res_end, 0xfebc00ff); continue;
wr64(r1, res_flags, LDLS_IORESOURCE_MEM); wr64(r, res_start, in.bar[b].base);
wr64(r, res_end, in.bar[b].base + in.bar[b].size - 1);
wr64(r, res_flags, (in.bar[b].flags & PCIUSER_BAR_IO)
? LDLS_IORESOURCE_IO : LDLS_IORESOURCE_MEM);
puts_(" bar "); putn(b); puts_(" 0x");
puthexn(in.bar[b].base, 16); puts_(" ");
putn((long long)in.bar[b].size);
puts_((in.bar[b].flags & PCIUSER_BAR_IO) ? " io\n" : " mem\n");
}
} }
puts_("\n calling probe\n\n"); // sizeof(struct net_device), for alloc_etherdev_mqs -- a driver reaches
// its private area past it and gets silent corruption if this is wrong.
{
extern unsigned int kpi_netdev_size;
int nd = btf_find(&b, "net_device", BTF_KIND_STRUCT);
kpi_netdev_size = nd ? btf_type_size(&b, nd) : 0;
puts_(" netdev "); putn(kpi_netdev_size); puts_(" bytes\n");
}
// Counted over the *symbol list*, not over resolutions: a symbol is
// resolved once per relocation that names it, and reporting that number
// would have said 275 of 94.
{
int impl = 0, i;
for (i = 0; i < k_nundef_global; i++)
if (kpi_find(g_names[i])) impl++;
puts_("\n calling probe, with "); putn(impl);
puts_(" of "); putn(k_nundef_global);
puts_(" symbols implemented\n\n");
}
g_calls = 0; g_calls = 0;
{ {
int (*probe)(void *, void *) = int (*probe)(void *, void *) =
@ -361,6 +477,24 @@ drive_probe(const char *btfpath)
int r = probe((void *)pdev, (void *)idtab); int r = probe((void *)pdev, (void *)idtab);
puts_("\n probe returned "); putn(r); puts_("\n probe returned "); putn(r);
puts_(" after "); putn(g_calls); puts_(" stub call(s)\n"); puts_(" after "); putn(g_calls); puts_(" stub call(s)\n");
if (r == 0 && kpi.netfd >= 0) {
// The driver is up on real hardware and has registered an
// interface. What is *not* built is the path that carries
// frames -- ndo_open, ndo_start_xmit and the receive side.
//
// So the interface is released rather than left up. A link
// that reports itself up with nothing draining its queue is a
// black hole: the stack would send DHCP into it for ever and
// every layer above would believe the network was merely
// quiet. Down is the true answer until packets move.
puts_("\n the driver is up on real hardware and has an "
"interface.\n");
puts_(" carrying frames is the half of L3 that is not built, "
"so the\n interface is released rather than left up "
"and silent.\n");
kpi_unregister_netdev(0);
}
} }
} }
} }
@ -416,6 +550,8 @@ main(int argc, char *argv[])
stub_build(&stubs, code, k.nundef); stub_build(&stubs, code, k.nundef);
g_names = k.undef; g_names = k.undef;
g_count = k.nundef; g_count = k.nundef;
k_nundef_global = k.nundef;
kpi.netfd = -1;
for (i = 0; i < k.nundef; i++) for (i = 0; i < k.nundef; i++)
if (stub_is_bare(k.undef[i])) if (stub_is_bare(k.undef[i]))
stub_make_bare(code + (unsigned long long)i * STUB_SIZE); stub_make_bare(code + (unsigned long long)i * STUB_SIZE);

View file

@ -80,6 +80,15 @@ Three of those are worth expanding on, because each encodes a decision:
a BAR of a device this process holds is refused. That is the whole difference a BAR of a device this process holds is refused. That is the whole difference
between this and `/dev/mem` with extra steps. between this and `/dev/mem` with extra steps.
The check is against the *pages* a BAR lies in rather than the BAR itself,
because a page is the only granularity an MMU has. A BAR smaller than a page
therefore hands the process the whole page it sits in — forced by the hardware
rather than chosen. Comparing against the BAR's own size instead meant a
sub-page BAR could not be mapped at all, which nothing noticed until an
rtl8139 with 256 bytes of registers turned up; see
[../plan/ldls.md](../plan/ldls.md), "The bug a foreign driver found in its
first minute".
**DMA is a call, not a mapping.** The process is told the *physical* address, **DMA is a call, not a mapping.** The process is told the *physical* address,
which is precisely what it cannot work out for itself, and the memory comes from which is precisely what it cannot work out for itself, and the memory comes from
an arena reserved at boot before the general allocator gets anything. an arena reserved at boot before the general allocator gets anything.

View file

@ -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.** | | **`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". | | ~~**`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 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. | | **`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 done — a Linux driver now drives real hardware through `/dev/pci` and reads the card's own MAC off its EEPROM. L3d, the packet path, is what remains. |
| **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. | | **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. | | **`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. |

View file

@ -183,7 +183,8 @@ One device, one process, claimed and released, on the `/dev/pci` discipline.
| **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 | | **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` | | **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 | | **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. | | | **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. | | | **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 | | **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 |
@ -635,6 +636,95 @@ says about the combination, and why it is a thing you type on purpose.
--- ---
## L3c: a Linux driver, driving real hardware
`ldls/kpi/` is the shim: 41 of `8139too`'s 94 symbols, implemented rather than
logged. Register access goes to a mapped BAR; `pci_enable_device`,
`pci_iomap`, `pci_request_regions` and `pci_set_master` go to `/dev/pci`;
allocation goes to `mmap` and `PCIIOC_DMA`; `register_netdev` goes to
`/dev/netdev`. Every file records its provenance, per rule 3.
```
# ldlsd /lib/modules/8139too.ko /lib/modules/btf
found 10ec:8139 at 00:05.0
bar 0 0x000000000000c000 256 io
bar 1 0x0000000081060000 256 mem
netdev 2688 bytes
calling probe, with 41 of 94 symbols implemented
kpi: net_device 2688 bytes + 872 private
kpi: mapped BAR 1 -- phys 0x0000000081060000, 256 bytes
kpi: the driver read a MAC off the hardware: 52:54:00:ab:cd:ef
net: ldls0 attached as interface 1, asking for an address
netuser: ldls0 is a process now, 52:54:0:ab:cd:ef
kpi: registered as an interface, with the hardware's own MAC
probe returned 0 after 0 stub call(s)
```
**`52:54:00:ab:cd:ef` is what the machine was configured with.** Nothing here
invented it: the driver bit-banged the card's EEPROM, serially, through a BAR
mapped by `/dev/pci`, and handed back the address it found. That it matches
means the loader, the relocations, the BTF offsets, the BAR mapping and the
register accessors are all right at once — which is why it was chosen as the
thing to aim at.
**Zero stub calls.** Every symbol that probe reaches is implemented.
### The bug a foreign driver found in its first minute
The first run against real hardware wrote to address `0x4e` and faulted.
`0x4e` is 13 below where the driver meant to write, and 13 is `EACCES`:
`pci_iomap` had returned `-13` as a pointer, because the check for a failed
`mmap` looked only for `-1` while SchOS returns `-errno`.
Underneath that was the real one. `sys_mmap` rounds a length up to a page —
the only granularity an MMU has — and `pciuser_mmio_ok()` compared that against
the BAR's own size. rtl8139's registers are **256 bytes**, the request became
4096, and the answer was `-EACCES` for a device the process legitimately held.
**A BAR smaller than a page could never be mapped at all**, and nothing had
noticed because the only consumer until now was virtio-gpu, whose BARs are
large. The check is now against the pages the BAR lies in. The widening is
real and named in the code: a sub-page BAR hands the process the whole page it
sits in, which is forced by the hardware rather than chosen.
That is precisely what [hybrid.md](hybrid.md) said a foreign driver would be
good for — *"what it asks for is what a foreign driver would ask for too"* —
arriving as a concrete bug rather than as a prediction.
### Register access is checked
`kpi_io_ok()` bounds every `ioread`/`iowrite` against the mapping `pci_iomap`
returned, reports the first few that miss, and returns without touching
memory. Two reasons: a stray access would otherwise be a page fault with no
context, and **there is no IOMMU** — a hosted driver's wild writes are exactly
the class of thing that has to be caught early or not at all.
It is also what turned the bug above from a crash into two lines naming the
offsets, which is how the `-13` was spotted.
### What is not done
`probe()` succeeds, so the driver is up and the interface exists with the
hardware's own MAC. **No frame has moved.** `ndo_open` is never called, there
is no `ndo_start_xmit`, and nothing services the receive side — so `ldlsd`
releases the interface rather than leaving it up:
```
the driver is up on real hardware and has an interface.
carrying frames is the half of L3 that is not built, so the
interface is released rather than left up and silent.
```
A link that reports itself up with nothing draining its queue is a black hole:
the stack would send DHCP into it for ever and every layer above would believe
the network was merely quiet. Down is the true answer until packets move, and
that is L3d.
---
## What it will not do ## What it will not do
- **It is not a security boundary, and here that is worse than usual.** There - **It is not a security boundary, and here that is worse than usual.** There
@ -705,10 +795,16 @@ says about the combination, and why it is a thing you type on purpose.
with the link down, because nothing in lwIP supports taking an interface out 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 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. `/proc/netif`'s it is a `ldls0` that stays listed after its process is gone.
- **`probe()` is reached with invented hardware.** The BARs are numbers the - **No frame has moved.** L3c ends at a successful `probe()`. The packet path
harness made up and `pci_iomap` returns a zeroed page. A driver that reads a — `ndo_open`, `ndo_start_xmit`, receive, and the skb handling all three need
status register gets whatever was there, which is why 8139too read a MAC — is L3d and is the larger half.
address of zeroes and was perfectly happy about it. - **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.
- **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
it is the deepest unfixed thing in the layer.
- **It will not support "Linux drivers".** It will support *one driver, on one - **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 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 the list of what has actually been run belongs in the architecture document

View file

@ -268,9 +268,32 @@ pciuser_mmio_ok(uint64 phys, uint64 len)
continue; continue;
for (int b = 0; b < PCIUSER_MAXBAR; b++) { for (int b = 0; b < PCIUSER_MAXBAR; b++) {
uint64 base = d->bar[b].base, size = d->bar[b].size; uint64 base = d->bar[b].base, size = d->bar[b].size;
uint64 lo, hi;
if (!base || !size || d->bar[b].is_io) if (!base || !size || d->bar[b].is_io)
continue; continue;
if (phys >= base && phys + len > phys && phys + len <= base + size)
// The *pages* the BAR lies in, not the BAR.
//
// sys_mmap rounds a length up to a page before asking, because that is
// the only granularity an MMU has. Comparing that against the BAR's own
// size meant a BAR smaller than a page could never be mapped at all:
// rtl8139's registers are 256 bytes, the request became 4096, and the
// answer was -EACCES for a device the process legitimately held.
//
// Nothing had noticed because the only consumer was virtio-gpu, whose
// BARs are large. The first foreign driver found it in its first minute,
// which is the thing docs/plan/hybrid.md said a foreign driver would be
// good for.
//
// The widening is real and is worth naming: a sub-page BAR hands the
// process the whole page it sits in. That is forced by the hardware
// rather than chosen -- there is no way to map less -- and firmware
// places BARs on their natural alignment, so in practice the rest of the
// page is not another device's.
lo = PGROUNDDOWN(base);
hi = PGROUNDUP(base + size);
if (phys >= lo && phys + len > phys && phys + len <= hi)
return 1; return 1;
} }
} }

96
ldls/kpi/io.c Normal file
View file

@ -0,0 +1,96 @@
// SPDX-License-Identifier: Apache-2.0
//
// Register access. See ldls/kpi/kpi.h for the provenance rule.
//
// These are the busiest symbols by a wide margin -- 538 of 8139too's 552 probe
// calls are two of them -- and they are also the shortest, because once a BAR
// is mapped a register access *is* a memory access. The whole of what these
// add over a plain dereference is the `volatile`, which stops a compiler
// deciding that reading the same address twice is redundant. For a status
// register, reading it twice is the entire point.
//
// The argument order is worth noting because it is the opposite of what a C
// programmer expects and getting it backwards produces something that almost
// works: Linux's writes take the **value first** and the address second.
#include "types.h"
#include "kpi.h"
// Every access is checked against the mapping `pci_iomap` returned.
//
// A driver that reaches outside its own BAR would otherwise take the process
// down with a page fault, and the report would be an address with no context.
// This turns that into a line naming the offset it wanted -- and since there
// is no IOMMU, a hosted driver's stray accesses are exactly the class of thing
// that has to be noticed early or not at all.
int
kpi_io_ok(void *addr, int width, int write)
{
unsigned long a = (unsigned long)addr;
if (kpi.io_base && a >= kpi.io_base &&
a + (unsigned long)width <= kpi.io_base + kpi.io_len)
return 1;
kpi.io_stray++;
if (kpi.io_stray <= 8) {
kpi_log(" kpi: the driver tried to ");
kpi_log(write ? "write " : "read ");
kpi_lognum(width);
kpi_log(" byte(s) at 0x");
kpi_loghex(a, 16);
if (kpi.io_base && a < kpi.io_base) {
kpi_log(", which is ");
kpi_lognum((long long)(kpi.io_base - a));
kpi_log(" below the mapping");
} else if (kpi.io_base) {
kpi_log(", which is past the end of the mapping");
} else {
kpi_log(", but nothing is mapped yet");
}
kpi_log("\n");
}
return 0;
}
unsigned char
kpi_ioread8(void *addr)
{
if (!kpi_io_ok(addr, 1, 0)) return 0xff;
return *(volatile unsigned char *)addr;
}
unsigned short
kpi_ioread16(void *addr)
{
if (!kpi_io_ok(addr, 2, 0)) return 0xffff;
return *(volatile unsigned short *)addr;
}
unsigned int
kpi_ioread32(void *addr)
{
if (!kpi_io_ok(addr, 4, 0)) return 0xffffffff;
return *(volatile unsigned int *)addr;
}
void
kpi_iowrite8(unsigned char v, void *addr)
{
if (!kpi_io_ok(addr, 1, 1)) return;
*(volatile unsigned char *)addr = v;
}
void
kpi_iowrite16(unsigned short v, void *addr)
{
if (!kpi_io_ok(addr, 2, 1)) return;
*(volatile unsigned short *)addr = v;
}
void
kpi_iowrite32(unsigned int v, void *addr)
{
if (!kpi_io_ok(addr, 4, 1)) return;
*(volatile unsigned int *)addr = v;
}

278
ldls/kpi/kpi.c Normal file
View file

@ -0,0 +1,278 @@
// SPDX-License-Identifier: Apache-2.0
//
// The KPI, and the table the loader resolves against. See ldls/kpi/kpi.h.
//
// Provenance, once, for every function below: the *signature* of each is a
// fact -- a compiled driver calls it with these arguments in these registers
// and there is no other way to express that. What each one *does* is written
// here from the published behaviour and from what the driver requires of it.
// No comment, macro body or implementation has been carried over from
// anywhere. docs/plan/ldls.md §3.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "posix.h"
#include "pciuser.h"
#include "netuser.h"
#include "kpi.h"
struct kpi_ctx kpi;
int kpi_verbose;
void
kpi_log(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void *)s, n);
}
void
kpi_lognum(long long v)
{
char b[24];
int i = 0;
if (v < 0) { write(1, "-", 1); v = -v; }
if (v == 0) { write(1, "0", 1); return; }
while (v > 0) { b[i++] = (char)('0' + (v % 10)); v /= 10; }
while (i-- > 0) write(1, &b[i], 1);
}
void
kpi_loghex(unsigned long long v, int digits)
{
static const char h[] = "0123456789abcdef";
char b[16];
int i;
if (digits > 16) digits = 16;
for (i = digits - 1; i >= 0; i--) { b[i] = h[v & 0xF]; v >>= 4; }
write(1, b, digits);
}
static int
eq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == 0 && *b == 0;
}
void *
kpi_find(const char *name)
{
int i;
for (i = 0; i < kpi_count; i++)
if (eq(kpi_table[i].name, name))
return kpi_table[i].fn;
return 0;
}
// --- memory -------------------------------------------------------------------
//
// There is no allocator in lib/, and a driver's allocations are neither many
// nor small, so every one of these is an mmap. Nothing is ever freed: a hosted
// driver's lifetime is the process's, and the kernel reclaims it all at exit.
// That is a leak in the strict sense and the right trade here -- writing an
// allocator to serve a program that will be killed to restart it is work spent
// on the wrong problem.
static void *
kpi_alloc(unsigned long n)
{
void *p;
if (n == 0)
n = 8;
p = mmap(0, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == 0 || (long)p == -1)
return 0;
return p; // mmap gives zeroed pages, which kzalloc wants
}
void *kpi_kmalloc(unsigned long n, unsigned int flags)
{ (void)flags; return kpi_alloc(n); }
void kpi_kfree(void *p) { (void)p; }
void *
kpi_krealloc(void *old, unsigned long n, unsigned int flags)
{
(void)flags;
// Never shrinks and never reuses. A driver that reallocs in its fast path
// would notice; none of the ones this hosts do it at all.
{
unsigned char *p = kpi_alloc(n);
unsigned char *o = (unsigned char *)old;
unsigned long i;
if (p && o)
for (i = 0; i < n; i++)
p[i] = o[i];
return p;
}
}
// dma_alloc_attrs(dev, size, dma_handle, gfp, attrs)
//
// The one allocation that cannot be an ordinary mmap: the device is going to
// read it by physical address, so it has to come from the kernel's DMA arena.
// PCIIOC_DMA is exactly that call, and it reports both addresses because the
// physical one is precisely what a process cannot work out for itself.
void *
kpi_dma_alloc_attrs(void *dev, unsigned long size, unsigned long long *handle,
unsigned int gfp, unsigned long attrs)
{
struct pciuser_dma d;
(void)dev; (void)gfp; (void)attrs;
d.size = size;
if (ioctl(kpi.pcifd, PCIIOC_DMA, &d) < 0) {
kpi_log("kpi: no DMA memory for "); kpi_lognum(size); kpi_log(" bytes\n");
return 0;
}
if (handle)
*handle = d.paddr;
if (kpi_verbose) {
kpi_log("kpi: dma "); kpi_lognum(size); kpi_log(" bytes at phys 0x");
kpi_loghex(d.paddr, 16); kpi_log("\n");
}
return (void *)d.uaddr;
}
void
kpi_dma_free_attrs(void *dev, unsigned long size, void *va,
unsigned long long handle, unsigned long attrs)
{
// PCIIOC_DMA has no free; a driver's blocks come back when it exits. See
// the note in kernel/dev/pciuser.c.
(void)dev; (void)size; (void)va; (void)handle; (void)attrs;
}
// --- things that only have to not crash ---------------------------------------
//
// A hosted driver takes locks, starts timers and waits for things. In one
// process with no preemption inside the driver, a lock is a no-op that is
// *correct* rather than merely convenient: there is nobody to race with.
//
// This stops being true the moment a driver has an interrupt thread, and the
// note in docs/plan/ldls.md about sleeping-context fidelity is about exactly
// this. It is the deepest unfixed assumption in the whole layer.
void kpi_nop(void) { }
int kpi_zero(void) { return 0; }
int kpi_one(void) { return 1; }
// --- printing ------------------------------------------------------------------
// _printk(fmt, ...) and the netdev/dev variants, which put the format second.
//
// Linux format strings are printf's with a leading level byte and %p
// extensions that expand pointers by kind. Nothing here expands them, so the
// string is printed as it stands -- which is honest and is enough for the
// lines that matter, the ones a driver prints when it has found its hardware.
static void
print_fmt(const char *f)
{
int n = 0;
if (!f)
return;
if ((unsigned char)f[0] == 0x01 && f[1] >= '0' && f[1] <= '7')
f += 2;
kpi_log(" driver: ");
kpi_log(f);
while (f[n]) n++;
if (n == 0 || f[n - 1] != '\n')
kpi_log("\n");
}
int kpi_printk(const char *fmt) { print_fmt(fmt); return 0; }
int kpi_netdev_print(void *dev, const char *fmt) { (void)dev; print_fmt(fmt); return 0; }
// --- the table -----------------------------------------------------------------
//
// Names on the left are Linux's and are facts. Everything on the right is ours.
extern void *kpi_pci_iomap(void *pdev, int bar, unsigned long max);
extern void kpi_pci_iounmap(void *pdev, void *addr);
extern int kpi_pci_enable_device(void *pdev);
extern int kpi_pci_request_regions(void *pdev, const char *name);
extern void kpi_pci_release_regions(void *pdev);
extern void kpi_pci_set_master(void *pdev);
extern int kpi_pci_read_config_word(void *pdev, int where, unsigned short *v);
extern int kpi_pci_write_config_word(void *pdev, int where, unsigned short v);
extern unsigned char kpi_ioread8(void *addr);
extern unsigned short kpi_ioread16(void *addr);
extern unsigned int kpi_ioread32(void *addr);
extern void kpi_iowrite8(unsigned char v, void *addr);
extern void kpi_iowrite16(unsigned short v, void *addr);
extern void kpi_iowrite32(unsigned int v, void *addr);
extern void *kpi_alloc_etherdev_mqs(int priv, unsigned int tx, unsigned int rx);
extern void kpi_free_netdev(void *dev);
extern int kpi_register_netdev(void *dev);
extern void kpi_unregister_netdev(void *dev);
extern void kpi_dev_addr_mod(void *dev, unsigned int off,
const unsigned char *addr, unsigned int len);
const struct kpi_sym kpi_table[] = {
// PCI
{ "pci_enable_device", (void *)kpi_pci_enable_device },
{ "pci_disable_device", (void *)kpi_nop },
{ "pci_request_regions", (void *)kpi_pci_request_regions },
{ "pci_release_regions", (void *)kpi_pci_release_regions },
{ "pci_set_master", (void *)kpi_pci_set_master },
{ "pci_iomap", (void *)kpi_pci_iomap },
{ "pci_iounmap", (void *)kpi_pci_iounmap },
{ "pci_read_config_word", (void *)kpi_pci_read_config_word },
{ "pci_write_config_word", (void *)kpi_pci_write_config_word },
// Registers
{ "ioread8", (void *)kpi_ioread8 },
{ "ioread16", (void *)kpi_ioread16 },
{ "ioread32", (void *)kpi_ioread32 },
{ "iowrite8", (void *)kpi_iowrite8 },
{ "iowrite16", (void *)kpi_iowrite16 },
{ "iowrite32", (void *)kpi_iowrite32 },
// Memory
{ "__kmalloc_noprof", (void *)kpi_kmalloc },
{ "kmalloc_noprof", (void *)kpi_kmalloc },
{ "kfree", (void *)kpi_kfree },
{ "krealloc_noprof", (void *)kpi_krealloc },
{ "dma_alloc_attrs", (void *)kpi_dma_alloc_attrs },
{ "dma_free_attrs", (void *)kpi_dma_free_attrs },
// Network
{ "alloc_etherdev_mqs", (void *)kpi_alloc_etherdev_mqs },
{ "free_netdev", (void *)kpi_free_netdev },
{ "register_netdev", (void *)kpi_register_netdev },
{ "unregister_netdev", (void *)kpi_unregister_netdev },
{ "dev_addr_mod", (void *)kpi_dev_addr_mod },
// Printing
{ "_printk", (void *)kpi_printk },
{ "netdev_info", (void *)kpi_netdev_print },
{ "netdev_err", (void *)kpi_netdev_print },
{ "_dev_info", (void *)kpi_netdev_print },
{ "_dev_err", (void *)kpi_netdev_print },
// Things a driver calls at probe that need only to return sensibly. Each is
// here because 8139too calls it and because doing nothing is the right
// answer in one process -- see the note above kpi_nop().
{ "mutex_lock", (void *)kpi_nop },
{ "mutex_unlock", (void *)kpi_nop },
{ "__mutex_init", (void *)kpi_nop },
{ "_raw_spin_lock", (void *)kpi_nop },
{ "_raw_spin_unlock", (void *)kpi_nop },
{ "_raw_spin_lock_irq", (void *)kpi_nop },
{ "_raw_spin_unlock_irq", (void *)kpi_nop },
{ "_raw_spin_lock_bh", (void *)kpi_nop },
{ "_raw_spin_unlock_bh", (void *)kpi_nop },
{ "_raw_spin_lock_irqsave", (void *)kpi_zero },
{ "_raw_spin_unlock_irqrestore", (void *)kpi_nop },
{ "timer_init_key", (void *)kpi_nop },
{ "netif_napi_add_weight_locked", (void *)kpi_nop },
{ "__const_udelay", (void *)kpi_nop },
{ "synchronize_rcu", (void *)kpi_nop },
};
const int kpi_count = (int)(sizeof(kpi_table) / sizeof(kpi_table[0]));

82
ldls/kpi/kpi.h Normal file
View file

@ -0,0 +1,82 @@
// SPDX-License-Identifier: Apache-2.0
//
// The KPI: the Linux symbols a hosted driver actually calls, implemented.
//
// docs/plan/ldls.md milestone L3c. L1 and L2 measured what a driver asks for
// and in what order; this is what answers, for real, against `/dev/pci` and
// `/dev/netdev`.
//
// **Rule 3 governs every file here.** A symbol exists and takes these
// arguments is a fact and is reproduced; the comment above it, the macro body
// and the implementation are expression and are never carried over. What each
// function *does* is written from documentation and from what the driver's
// behaviour requires -- which is the part BTF cannot give and is the whole of
// the real work.
//
// Organised under Linux's own header names so it is navigable: a reader who
// knows where `pci_iomap` lives in Linux knows where it is here.
#include "types.h"
// One entry per symbol LDLS can answer. The loader consults this before
// falling back to a logging stub, so an unimplemented symbol still reports
// itself rather than silently returning zero -- which is what made L1's
// measurement possible and stays useful for exactly the same reason.
struct kpi_sym {
const char *name;
void *fn;
};
extern const struct kpi_sym kpi_table[];
extern const int kpi_count;
// Look one up. Returns 0 when LDLS has no implementation, which is not an
// error: it means the caller should install a stub and find out whether the
// driver ever calls it.
void *kpi_find(const char *name);
// --- what the host gives the KPI ---------------------------------------------
//
// The KPI cannot open these for itself: `/dev/pci` is one device per process
// and the host has already claimed it. So the host hands over what it holds,
// once, before the driver runs.
struct kpi_ctx {
int pcifd; // /dev/pci, with the device claimed
int index; // which device, for the ioctls that name one
int netfd; // /dev/netdev, or -1 until register_netdev
// The BARs, as PCIIOC_INFO reported them. `pci_iomap` maps out of these and
// refuses anything else, which is the same check the kernel makes and is
// repeated here so the failure says which BAR rather than -EACCES.
unsigned long long bar_base[6];
unsigned long long bar_size[6];
unsigned int bar_flags[6];
// The mapping pci_iomap handed out, so register access can be checked
// against it. A foreign driver writing outside its own BAR is precisely the
// thing worth catching, and without an IOMMU it is also the thing nothing
// else will catch.
unsigned long io_base;
unsigned long io_len;
long io_stray; // accesses refused, so it is countable
unsigned char mac[6]; // filled in by dev_addr_mod
int have_mac;
};
extern struct kpi_ctx kpi;
// Output. The KPI is inside a process with no libc, and every implementation
// wants to be able to say what it did.
void kpi_log(const char *s);
void kpi_lognum(long long v);
void kpi_loghex(unsigned long long v, int digits);
// Whether to narrate every call. Register access is far too noisy to print by
// default -- 538 of 8139too's 552 probe calls are ioread8/iowrite8.
extern int kpi_verbose;
// Is this a register address we handed out? Reports the first few that are
// not, then goes quiet -- a driver that is off the rails produces thousands.
int kpi_io_ok(void *addr, int width, int write);

148
ldls/kpi/net.c Normal file
View file

@ -0,0 +1,148 @@
// SPDX-License-Identifier: Apache-2.0
//
// The network half of the KPI. See ldls/kpi/kpi.h for the provenance rule.
//
// `alloc_etherdev_mqs` is the interesting one and it is where BTF earns its
// keep a second time. A driver reaches its own private area with
// `netdev_priv(dev)`, which is an *inline function* compiled into the driver:
// the net_device, rounded up to an alignment, and the private area after it.
// So the size this allocates is not a free choice -- get it wrong and the
// driver writes its state over the tail of its own control block, and nothing
// says so until something much later reads a field that has been quietly
// overwritten.
//
// The size comes from the same BTF the offsets do.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "posix.h"
#include "netuser.h"
#include "kpi.h"
// Filled in by the host from BTF before the driver runs. Zero means the host
// could not find `struct net_device`, in which case allocating is a guess and
// is refused rather than made.
unsigned int kpi_netdev_size;
// NETDEV_ALIGN. Not a layout and not in BTF -- it is a constant in a header,
// and it has been 32 for as long as x86-64 has existed. A wrong value here is
// visible immediately as a driver that reads zeroes out of its own private
// area, which is why it is worth stating rather than hiding in an expression.
#define KPI_NETDEV_ALIGN 32
static void *g_netdev; // the one interface, for register_netdev
void *
kpi_alloc_etherdev_mqs(int priv, unsigned int txqs, unsigned int rxqs)
{
unsigned long base, total;
void *p;
(void)txqs; (void)rxqs;
if (kpi_netdev_size == 0) {
kpi_log("kpi: no sizeof(struct net_device) -- cannot allocate one\n");
return 0;
}
base = (kpi_netdev_size + (KPI_NETDEV_ALIGN - 1)) & ~(unsigned long)(KPI_NETDEV_ALIGN - 1);
total = base + (unsigned long)priv + KPI_NETDEV_ALIGN;
p = mmap(0, total, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (p == 0 || (long)p == -1)
return 0;
kpi_log(" kpi: net_device "); kpi_lognum(kpi_netdev_size);
kpi_log(" bytes + "); kpi_lognum(priv); kpi_log(" private\n");
g_netdev = p;
return p;
}
void
kpi_free_netdev(void *dev)
{
(void)dev;
}
// The MAC, as the driver read it off the chip.
//
// This is the payoff of the whole exercise and it is worth being precise about
// why: nothing here invented it. The driver bit-banged the card's EEPROM,
// serially, hundreds of register accesses, and handed the result to this. If
// it matches what the machine was actually configured with, then every layer
// underneath -- the loader, the relocations, the BTF offsets, the BAR
// mapping, the register accessors -- is right.
void
kpi_dev_addr_mod(void *dev, unsigned int off, const unsigned char *addr,
unsigned int len)
{
unsigned int i;
(void)dev;
if (off != 0 || len != 6 || !addr)
return;
for (i = 0; i < 6; i++)
kpi.mac[i] = addr[i];
kpi.have_mac = 1;
kpi_log(" kpi: the driver read a MAC off the hardware: ");
for (i = 0; i < 6; i++) {
kpi_loghex(kpi.mac[i], 2);
if (i < 5) kpi_log(":");
}
kpi_log("\n");
}
// Become an interface.
//
// /dev/netdev is the bridge docs/plan/ldls.md calls the class bridge, built
// and proved at L3a by `netecho` with no hardware behind it at all. Here there
// is: the same four calls, answered by a Linux driver.
int
kpi_register_netdev(void *dev)
{
struct netuser_create c;
int i;
(void)dev;
if (!kpi.have_mac) {
kpi_log("kpi: register_netdev before the driver found a MAC\n");
return -1;
}
kpi.netfd = open("/dev/netdev", O_RDWR);
if (kpi.netfd < 0) {
kpi_log("kpi: cannot open /dev/netdev\n");
return -1;
}
for (i = 0; i < (int)sizeof(c.name); i++) c.name[i] = 0;
for (i = 0; i < 6; i++) c.mac[i] = kpi.mac[i];
c.pad[0] = c.pad[1] = 0;
c.name[0] = 'l'; c.name[1] = 'd'; c.name[2] = 'l'; c.name[3] = 's';
if (ioctl(kpi.netfd, NETUSERIOC_CREATE, &c) < 0) {
kpi_log("kpi: /dev/netdev is already somebody else's\n");
close(kpi.netfd);
kpi.netfd = -1;
return -1;
}
kpi_log(" kpi: registered as an interface, with the hardware's own MAC\n");
return 0;
}
void
kpi_unregister_netdev(void *dev)
{
(void)dev;
if (kpi.netfd >= 0) {
ioctl(kpi.netfd, NETUSERIOC_RELEASE, 0);
close(kpi.netfd);
kpi.netfd = -1;
}
}

152
ldls/kpi/pci.c Normal file
View file

@ -0,0 +1,152 @@
// SPDX-License-Identifier: Apache-2.0
//
// The PCI half of the KPI. See ldls/kpi/kpi.h for the provenance rule.
//
// Every one of these lands on `/dev/pci`, which is the interface
// docs/plan/hybrid.md built and `vgpud` proved. That it fits a foreign driver
// without alteration is the point the hybrid plan was making when it insisted
// the interface be about *PCI devices* rather than about a driver model:
//
// "an interface whose first consumers are native drivers is a general
// mechanism that something foreign can use later, rather than a hole shaped
// like something foreign."
//
// This file is that claim being collected.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "posix.h"
#include "pciuser.h"
#include "kpi.h"
// The device is claimed by the host before the driver runs, because claiming
// is what enables memory space and bus mastering -- and a BAR read before that
// can legitimately be zero. So by the time a driver calls this, it has already
// happened, and the honest answer is success.
int
kpi_pci_enable_device(void *pdev)
{
(void)pdev;
return 0;
}
// Regions are not reserved against anyone: this process holds the whole
// device, exclusively, because /dev/pci gives out one device per process.
// There is nobody to arbitrate with.
int
kpi_pci_request_regions(void *pdev, const char *name)
{
(void)pdev; (void)name;
return 0;
}
void
kpi_pci_release_regions(void *pdev)
{
(void)pdev;
}
// Also already done, by PCIIOC_CLAIM, which sets PCI_CMD_MEMORY and
// PCI_CMD_BUS_MASTER together -- see do_claim() in kernel/dev/pciuser.c, and
// the comment there about why a driver that has to ask separately will forget.
void
kpi_pci_set_master(void *pdev)
{
(void)pdev;
}
int
kpi_pci_read_config_word(void *pdev, int where, unsigned short *v)
{
struct pciuser_cfg c;
(void)pdev;
c.index = (unsigned int)kpi.index;
c.offset = (unsigned int)where & ~3u;
c.value = 0;
if (ioctl(kpi.pcifd, PCIIOC_CFG, &c) < 0)
return -1;
if (v)
*v = (unsigned short)(c.value >> ((where & 2) * 8));
return 0;
}
// Refused, deliberately and loudly.
//
// /dev/pci has no config *write* at all, and that is the line that matters:
// a write is how a BAR gets moved out from under the kernel. A driver that
// needs one has found a real limit of this interface rather than a bug, and
// silently succeeding would turn that into a mystery somewhere else.
int
kpi_pci_write_config_word(void *pdev, int where, unsigned short v)
{
(void)pdev; (void)v;
kpi_log("kpi: the driver tried to WRITE config space at 0x");
kpi_loghex((unsigned long long)where, 2);
kpi_log(" -- /dev/pci does not allow that\n");
return -1;
}
// Map a BAR.
//
// The offset passed to mmap is the BAR's *physical address*, which is what
// makes the kernel able to check it: an offset that is not inside a BAR of a
// device this process holds is refused, and that check is the whole difference
// between /dev/pci and /dev/mem with extra steps.
void *
kpi_pci_iomap(void *pdev, int bar, unsigned long max)
{
unsigned long long len;
void *p;
(void)pdev;
if (bar < 0 || bar >= 6 || kpi.bar_base[bar] == 0) {
kpi_log("kpi: pci_iomap of BAR "); kpi_lognum(bar);
kpi_log(", which this device does not have\n");
return 0;
}
// An I/O-space BAR has no address to map. rtl8139 offers both and its driver
// prefers MMIO when it can get it; saying no here is what makes it take that
// path rather than issuing port instructions a process cannot execute.
if (kpi.bar_flags[bar] & PCIUSER_BAR_IO) {
kpi_log("kpi: BAR "); kpi_lognum(bar);
kpi_log(" is I/O space, which cannot be mapped\n");
return 0;
}
len = kpi.bar_size[bar];
if (max && max < len)
len = max;
p = mmap(0, len, PROT_READ | PROT_WRITE, MAP_SHARED, kpi.pcifd,
kpi.bar_base[bar]);
// Any small negative is an error, not an address.
//
// SchOS returns -errno from mmap rather than MAP_FAILED, so checking only
// for -1 lets every other failure through as a pointer. That is not a
// theoretical worry: this returned -EACCES, the driver took -13 as its
// register base, and the first write landed at 0x4e -- thirteen below where
// it meant to be, which is how the real fault below was eventually found.
if (p == 0 || ((long)p < 0 && (long)p > -4096)) {
kpi_log("kpi: the kernel refused to map BAR "); kpi_lognum(bar);
kpi_log(", error "); kpi_lognum(-(long)p); kpi_log("\n");
return 0;
}
kpi.io_base = (unsigned long)p;
kpi.io_len = len;
kpi_log(" kpi: mapped BAR "); kpi_lognum(bar);
kpi_log(" -- phys 0x"); kpi_loghex(kpi.bar_base[bar], 16);
kpi_log(", "); kpi_lognum((long long)len); kpi_log(" bytes\n");
return p;
}
void
kpi_pci_iounmap(void *pdev, void *addr)
{
(void)pdev; (void)addr;
}