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

@ -53,6 +53,12 @@ extern uint64 boot_tsc;
void monotonic(uint64 *sec, uint64 *usec);
int proc_write_schinit(int user_src, uint64 src, uint n);
int proc_wedge(const char *name);
void pciuser_init(void);
int pciuser_ioctl(uint64 request, uint64 argp);
int pciuser_read(struct inode*, uint64, char*, int);
int pciuser_write(struct inode*, uint64, char*, int);
int pciuser_mmio_ok(uint64 phys, uint64 len);
void pciuser_release_all(struct proc*);
int proc_reincarnate(int status);
void proc_kill_init(void);
extern uint init_generation;

97
include/pciuser.h Normal file
View file

@ -0,0 +1,97 @@
// SPDX-License-Identifier: Apache-2.0
//
// /dev/pci -- device access from userspace.
//
// docs/plan/hybrid.md, mechanisms 2 and 4: MMIO and DMA to a process. This is
// the interface a driver that does not live in the kernel talks to, and it is
// deliberately about *PCI devices*, not about any particular driver model.
//
// That generality is the point, and it is worth being explicit about why.
// The first users of this will be SchOS's own drivers, evicted one at a time
// -- virtio-gpu, then xHCI -- and the interface has to be judged by them.
// Something that hosts foreign drivers later is then a consumer of a general
// mechanism rather than a hole shaped like one, which is both the honest
// design and the only one that stays honest under examination.
//
// What a driver needs from a kernel, and all it needs:
//
// - to find its device, and read what the bus says about it INFO
// - to be given it, exclusively CLAIM
// - to reach its registers mmap(BAR)
// - memory it can hand to the device DMA
// - to be told when the device wants attention (interrupts,
// not yet)
//
// There is no IOMMU. A process holding a DMA buffer can point the device at
// any physical address, so this isolates a driver against its own *bugs* and
// not against its own *intent*. docs/plan/hybrid.md says so at length and it
// remains true here: a crashed driver that restarts instead of taking the
// machine down is the whole benefit, and it is not a security boundary.
#ifndef PCIUSER_H
#define PCIUSER_H
#define PCIUSER_MAJOR 9
#define PCIUSER_MAXBAR 6
struct pciuser_bar {
unsigned long long base; // physical address, 0 when the BAR is unused
unsigned long long size;
unsigned int flags; // PCIUSER_BAR_*
unsigned int pad;
};
#define PCIUSER_BAR_MMIO 0x01 // memory space; mappable
#define PCIUSER_BAR_IO 0x02 // I/O port space; not mappable
#define PCIUSER_BAR_PREFETCH 0x04
// One device, as the bus describes it. `index` is the only field the caller
// fills in; everything else comes back.
struct pciuser_info {
unsigned int index; // in: 0 .. PCIIOC_COUNT-1
unsigned char bus, slot, func;
unsigned char header_type;
unsigned short vendor, device;
unsigned char class, subclass, prog_if, revision;
unsigned char irq; // interrupt line, 0xFF when unrouted
unsigned char claimed; // 1 when a userspace process holds it
unsigned short pad;
// The name of whatever owns it: an in-kernel driver, "userspace" when a
// process has claimed it, or empty when nothing has.
char driver[16];
struct pciuser_bar bar[PCIUSER_MAXBAR];
};
// A DMA buffer. The kernel allocates it from the DMA arena, maps it into the
// caller, and reports the physical address the *device* must be given -- which
// is the entire reason a driver cannot do this for itself with mmap.
struct pciuser_dma {
unsigned long long size; // in: bytes, rounded up to a page
unsigned long long uaddr; // out: where it landed in this process
unsigned long long paddr; // out: what to program into the device
};
#define PCIIOC_COUNT 0x9000 // out: int, how many devices the bus has
#define PCIIOC_INFO 0x9001 // struct pciuser_info
#define PCIIOC_CLAIM 0x9002 // in: int index -- take it from the kernel
#define PCIIOC_RELEASE 0x9003 // in: int index
#define PCIIOC_DMA 0x9004 // struct pciuser_dma
// Registers are reached with mmap() on this device, where the *offset is the
// physical address of the BAR*:
//
// ioctl(fd, PCIIOC_INFO, &info);
// ioctl(fd, PCIIOC_CLAIM, &info.index);
// regs = mmap(0, info.bar[0].size, PROT_READ|PROT_WRITE, MAP_SHARED,
// fd, info.bar[0].base);
//
// The kernel refuses an offset that is not inside a BAR of a device this
// process has claimed, which is what stops this being /dev/mem with extra
// steps.
#endif // PCIUSER_H

View file

@ -350,6 +350,23 @@ struct statfs {
#define FALLOC_FL_KEEP_SIZE 1
// poll(2) event bits (Linux values)
// mmap(2) protection and flags.
//
// These lived only in kernel/sys/sys_mem.c, so every userspace caller wrote
// its own copy -- cmd/fbinfo had one and cmd/pcitest wanted a third. Two
// private copies of a number the kernel compares against is a mismatch
// waiting to happen, and the symptom would be a mapping with the wrong
// permissions rather than an error.
#define PROT_NONE 0x0
#define PROT_READ 0x1
#define PROT_WRITE 0x2
#define PROT_EXEC 0x4
#define MAP_SHARED 0x01
#define MAP_PRIVATE 0x02
#define MAP_FIXED 0x10
#define MAP_ANONYMOUS 0x20
// waitpid() options. The kernel tests `options & 1`; this is that bit, named.
#define WNOHANG 0x001