SchOS/ldls/kpi/pci.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

152 lines
4.7 KiB
C

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