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

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;
}