hybrid: /dev/pci -- a driver can live outside the kernel now

docs/plan/hybrid.md mechanisms 2 and 4: MMIO and DMA to a process.

A process can enumerate the bus, claim a device exclusively, map its BARs and
get memory the device can be pointed at. Proven end to end against an e1000e
-- the exact device class we cannot drive and would want a foreign driver for:

    device 7: 8086:10d3 (network)
      claimed
      BAR0 at 0x81060000, 128 KB
      regs[0..3] = 0x00140261 0x00000000 0x00080283 0x00000000
      MMIO reaches the device
      DMA 8 KB at virtual 0x40001000, physical 0x3f8eb000

Those are the e1000e's CTRL and STATUS with full-duplex and link-up set, which
is how you tell a real mapping from one reading 0xFFFFFFFF off an unmapped
address. Claiming a device the kernel drives is refused -- two writers of one
register set looks like hardware that has started lying -- and everything a
process held is released when it exits, because a driver that crashed must not
keep its hardware away from its replacement.

mmap's offset is the BAR's physical address, checked against the BARs of
devices *this process has claimed*. Without that check the device is /dev/mem
with extra steps and a process could map the kernel's own memory. Mappings are
write-through: a cached write to a doorbell arrives whenever the cache feels
like it, which from a driver's side is indistinguishable from hardware that
ignores you.

The interface is about PCI devices, not about any driver model, and its first
users will be SchOS's own drivers when virtio-gpu is evicted. That ordering is
the design rather than an accident: an interface whose first consumers are
native drivers is a general mechanism something foreign can use later, instead
of a hole shaped like something foreign.

Also: PROT_* and MAP_* moved to include/posix.h. They were defined only in
sys_mem.c, so cmd/fbinfo carried a private copy and this wanted a third --
two copies of a number the kernel compares against, where a mismatch shows up
as a mapping with the wrong permissions rather than an error.

No IOMMU, so this isolates a driver against its own bugs and not against its
own DMA. The plan says so at length and it is still true.

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-13 15:34:28 +02:00
commit 24f79194ab
14 changed files with 678 additions and 11 deletions

View file

@ -21,9 +21,7 @@
#include "fcntl.h"
#include "fb.h"
#define PROT_READ 0x1
#define PROT_WRITE 0x2
#define MAP_SHARED 0x01
#include "posix.h" /* PROT_*, MAP_* -- one definition, shared */
static void
puts(const char *s)

196
cmd/pcitest/main.c Normal file
View file

@ -0,0 +1,196 @@
// SPDX-License-Identifier: Apache-2.0
//
// pcitest -- prove that a driver could live out here.
//
// docs/plan/hybrid.md mechanisms 2 and 4, exercised end to end from a process:
// enumerate the bus, claim a device, map its registers, read them, and get
// memory the device could be pointed at.
//
// It is deliberately a test and not a driver. The question it answers is "does
// the mechanism work", which has to be answered before the first real eviction
// -- otherwise a virtio-gpu that fails in userspace has two candidate causes
// and no way to tell them apart.
//
// pcitest list the bus
// pcitest <index> claim that device, map BAR0, read it, allocate DMA
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "posix.h"
#include "pciuser.h"
static void
puts(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void *)s, n);
}
static void
puthex(unsigned long long v, int digits)
{
static const char h[] = "0123456789abcdef";
char b[17];
for (int i = digits - 1; i >= 0; i--) { b[i] = h[v & 0xF]; v >>= 4; }
write(1, b, digits);
}
static void
putn(int v)
{
char b[12];
int i = 0;
if (v < 0) { write(1, "-", 1); v = -v; }
if (v == 0) { write(1, "0", 1); return; }
while (v > 0) { b[i++] = '0' + (v % 10); v /= 10; }
while (i-- > 0) write(1, &b[i], 1);
}
static int
num(const char *s)
{
int v = 0;
while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0');
return v;
}
// What the class byte means, for the handful that matter here. Not a full
// table: a listing that says "class 02" is only marginally better than one
// that says nothing, and a full one is 200 lines of data nobody reads.
static const char *
class_name(unsigned char c)
{
switch (c) {
case 0x01: return "storage";
case 0x02: return "network";
case 0x03: return "display";
case 0x04: return "multimedia";
case 0x06: return "bridge";
case 0x0c: return "serial bus";
default: return "other";
}
}
int
main(int argc, char *argv[])
{
int fd = open("/dev/pci", O_RDWR);
if (fd < 0) {
puts("pcitest: cannot open /dev/pci\n");
exit();
}
int count = 0;
if (ioctl(fd, PCIIOC_COUNT, &count) < 0) {
puts("pcitest: PCIIOC_COUNT failed\n");
exit();
}
if (argc == 1) {
puts(" # bdf vendor:device class driver\n");
for (int i = 0; i < count; i++) {
struct pciuser_info in;
in.index = (unsigned int)i;
if (ioctl(fd, PCIIOC_INFO, &in) < 0)
continue;
puts(" "); putn(i); puts(" ");
puthex(in.bus, 2); puts(":"); puthex(in.slot, 2);
puts("."); puthex(in.func, 1); puts(" ");
puthex(in.vendor, 4); puts(":"); puthex(in.device, 4); puts(" ");
puts(class_name(in.class));
puts(" ");
puts(in.driver[0] ? in.driver : "-");
puts("\n");
}
puts("\npcitest <n> to claim one and read its registers\n");
exit();
}
int idx = num(argv[1]);
struct pciuser_info in;
in.index = (unsigned int)idx;
if (ioctl(fd, PCIIOC_INFO, &in) < 0) {
puts("pcitest: no device "); putn(idx); puts("\n");
exit();
}
puts("device "); putn(idx); puts(": ");
puthex(in.vendor, 4); puts(":"); puthex(in.device, 4);
puts(" ("); puts(class_name(in.class)); puts(")");
if (in.driver[0]) { puts(", held by "); puts(in.driver); }
puts("\n");
if (ioctl(fd, PCIIOC_CLAIM, &idx) < 0) {
// EBUSY is the expected answer for anything the kernel drives, and it
// is the right answer until a driver can be told to let go. Saying so
// beats a bare failure.
puts(" claim refused -- the kernel is driving it, or someone else is\n");
exit();
}
puts(" claimed\n");
// The first memory BAR. A device with none is not one a driver reaches
// this way, and saying that is more useful than mapping nothing.
int b = -1;
for (int i = 0; i < PCIUSER_MAXBAR; i++)
if (in.bar[i].base && (in.bar[i].flags & PCIUSER_BAR_MMIO)) { b = i; break; }
if (b < 0) {
puts(" no memory BAR to map\n");
exit();
}
puts(" BAR"); putn(b); puts(" at 0x"); puthex(in.bar[b].base, 16);
puts(", "); putn((int)(in.bar[b].size / 1024)); puts(" KB\n");
unsigned long long len = in.bar[b].size;
if (len > 4096) len = 4096; // a page is enough to prove the point
volatile unsigned int *regs =
(volatile unsigned int *)mmap(0, len, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, in.bar[b].base);
if (regs == 0 || (long)regs == -1) {
puts(" mmap of the BAR failed\n");
exit();
}
puts(" mapped at 0x"); puthex((unsigned long long)regs, 16); puts("\n");
puts(" regs[0..3] =");
for (int i = 0; i < 4; i++) {
puts(" 0x");
puthex(regs[i], 8);
}
puts("\n");
// All-ones across the board means the read did not reach the device: an
// unmapped PCI read returns 0xFFFFFFFF rather than faulting, so this is
// the difference between "mapped the wrong thing" and "it works", and it
// is worth checking rather than eyeballing.
if (regs[0] == 0xFFFFFFFFu && regs[1] == 0xFFFFFFFFu) {
puts(" all ones -- the mapping does not reach the device\n");
} else {
puts(" MMIO reaches the device\n");
}
struct pciuser_dma dma;
dma.size = 8192;
if (ioctl(fd, PCIIOC_DMA, &dma) < 0) {
puts(" DMA allocation failed\n");
exit();
}
puts(" DMA 8 KB at virtual 0x"); puthex(dma.uaddr, 16);
puts(", physical 0x"); puthex(dma.paddr, 16); puts("\n");
// Write through the mapping and read it back: proves the memory is really
// there and really writable, which a physical address alone does not.
volatile unsigned int *buf = (volatile unsigned int *)dma.uaddr;
buf[0] = 0x5C405; buf[1] = 0xD1DD1E;
if (buf[0] == 0x5C405u && buf[1] == 0xD1DD1Eu)
puts(" DMA buffer reads back what was written\n");
else
puts(" DMA buffer did not read back correctly\n");
puts("\nmechanisms 2 and 4 work: a driver could live out here.\n");
exit();
}