SchOS/ldls/kpi/kpi.c
Nexoniarz 42f98130c4 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
2026-09-14 09:59:14 +02:00

278 lines
9.8 KiB
C

// 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]));