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
82 lines
3.4 KiB
C
82 lines
3.4 KiB
C
// 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);
|