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:
parent
3fd1cc5279
commit
42f98130c4
11 changed files with 1071 additions and 44 deletions
206
cmd/ldlsd/main.c
206
cmd/ldlsd/main.c
|
|
@ -28,6 +28,9 @@
|
|||
#include "ko.h"
|
||||
#include "stub.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 ---------------------------------
|
||||
|
||||
|
|
@ -179,12 +182,29 @@ ldls_stub(long idx, unsigned long *args)
|
|||
|
||||
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
|
||||
resolve(void *ctx, const char *name, unsigned long long *out)
|
||||
{
|
||||
struct rctx *r = (struct rctx *)ctx;
|
||||
void *fn;
|
||||
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++)
|
||||
if (streq(r->k->undef[i], name)) {
|
||||
*out = stub_addr(r->s, i);
|
||||
|
|
@ -210,6 +230,88 @@ find_symbol(struct ko *k, const char *want)
|
|||
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 -------------------------------------------------------
|
||||
|
||||
static unsigned int
|
||||
|
|
@ -232,26 +334,6 @@ lay(struct btf *b, const char *type, const char *member)
|
|||
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_MEM 0x00000200
|
||||
|
||||
|
|
@ -333,25 +415,59 @@ drive_probe(const char *btfpath)
|
|||
if (nid > 6) { puts_(" ... and "); putn(nid - 6); puts_(" more\n"); }
|
||||
if (!nid) return;
|
||||
|
||||
pdev = (unsigned long)getmem(dev_size);
|
||||
if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); 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.
|
||||
{
|
||||
unsigned long r0 = pdev + dev_res;
|
||||
unsigned long r1 = r0 + res_size;
|
||||
struct pciuser_info in;
|
||||
|
||||
wr16(pdev, dev_vendor, (unsigned short)rd32(idtab, id_vendor));
|
||||
wr16(pdev, dev_device, (unsigned short)rd32(idtab, id_device));
|
||||
wr32(pdev, dev_devfn, 0x18);
|
||||
wr32(pdev, dev_irq, 11);
|
||||
wr64(r0, res_start, 0xc000);
|
||||
wr64(r0, res_end, 0xc0ff);
|
||||
wr64(r0, res_flags, LDLS_IORESOURCE_IO);
|
||||
wr64(r1, res_start, 0xfebc0000);
|
||||
wr64(r1, res_end, 0xfebc00ff);
|
||||
wr64(r1, res_flags, LDLS_IORESOURCE_MEM);
|
||||
if (claim_for(idtab, id_size, id_vendor, id_device, &in) < 0)
|
||||
return;
|
||||
|
||||
pdev = (unsigned long)getmem(dev_size);
|
||||
if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); return; }
|
||||
|
||||
wr16(pdev, dev_vendor, in.vendor);
|
||||
wr16(pdev, dev_device, in.device);
|
||||
wr32(pdev, dev_devfn, (unsigned int)((in.slot << 3) | in.func));
|
||||
wr32(pdev, dev_irq, in.irq);
|
||||
|
||||
for (int b = 0; b < 6; b++) {
|
||||
unsigned long r = pdev + dev_res + (unsigned long)b * res_size;
|
||||
if (!in.bar[b].base)
|
||||
continue;
|
||||
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;
|
||||
{
|
||||
int (*probe)(void *, void *) =
|
||||
|
|
@ -361,6 +477,24 @@ drive_probe(const char *btfpath)
|
|||
int r = probe((void *)pdev, (void *)idtab);
|
||||
puts_("\n probe returned "); putn(r);
|
||||
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);
|
||||
g_names = k.undef;
|
||||
g_count = k.nundef;
|
||||
k_nundef_global = k.nundef;
|
||||
kpi.netfd = -1;
|
||||
for (i = 0; i < k.nundef; i++)
|
||||
if (stub_is_bare(k.undef[i]))
|
||||
stub_make_bare(code + (unsigned long long)i * STUB_SIZE);
|
||||
|
|
|
|||
Loading…
Reference in a new issue