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:
parent
6c70040f57
commit
24f79194ab
14 changed files with 678 additions and 11 deletions
279
kernel/dev/pciuser.c
Normal file
279
kernel/dev/pciuser.c
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// /dev/pci -- PCI devices, reachable from userspace.
|
||||
//
|
||||
// See include/pciuser.h for what this is and why the interface is about PCI
|
||||
// rather than about drivers. This file is the kernel half: enumeration, the
|
||||
// claim, the check that makes mmap safe, and DMA memory a device can be
|
||||
// pointed at.
|
||||
//
|
||||
// The claim is the part worth reading carefully. A device that a process has
|
||||
// claimed must no longer be touched by the kernel, or two drivers write the
|
||||
// same registers and the failure looks like hardware that has gone mad. So
|
||||
// claiming records the owner, and the kernel's own drivers are expected to
|
||||
// have been told to let go first -- which for now means "do not claim a device
|
||||
// the kernel is driving", enforced below rather than assumed.
|
||||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "param.h"
|
||||
#include "spinlock.h"
|
||||
#include "sleeplock.h"
|
||||
#include "fs.h"
|
||||
#include "file.h"
|
||||
#include "memlayout.h"
|
||||
#include "mmu.h"
|
||||
#include "proc.h"
|
||||
#include "pci.h"
|
||||
#include "errno.h"
|
||||
#include "uaccess.h"
|
||||
#include "pciuser.h"
|
||||
int mappages(pml4e_t *pgdir, void *va, uint64 size, uint64 pa, int perm);
|
||||
|
||||
// Who holds each device, or 0. Indexed the same way pci_get() is, because the
|
||||
// index a caller passes in is the one the bus scan produced and nothing
|
||||
// renumbers it afterwards.
|
||||
static struct proc *owner[NPCI];
|
||||
|
||||
static struct spinlock pciuser_lock;
|
||||
|
||||
void
|
||||
pciuser_init(void)
|
||||
{
|
||||
initlock(&pciuser_lock, "pciuser");
|
||||
}
|
||||
|
||||
// Does `phys` fall inside a memory BAR of a device this process has claimed?
|
||||
//
|
||||
// This is the whole security story for mmap, such as it is. Without it the
|
||||
// device is /dev/mem with extra steps: any process could map any physical
|
||||
// address, including the kernel's own memory. With it, a process reaches
|
||||
// exactly the registers of the devices it was given.
|
||||
//
|
||||
// Length matters as much as the base. A caller asking for one page at the end
|
||||
// of a BAR must not be handed the page after it, which on a real machine is
|
||||
// the next device's registers.
|
||||
int
|
||||
pciuser_mmio_ok(uint64 phys, uint64 len)
|
||||
{
|
||||
struct proc *p = myproc();
|
||||
int n = pci_count();
|
||||
|
||||
if (len == 0)
|
||||
return 0;
|
||||
|
||||
for (int i = 0; i < n && i < NPCI; i++) {
|
||||
if (owner[i] != p)
|
||||
continue;
|
||||
const struct pci_dev *d = pci_get(i);
|
||||
if (!d)
|
||||
continue;
|
||||
for (int b = 0; b < PCIUSER_MAXBAR; b++) {
|
||||
uint64 base = d->bar[b].base, size = d->bar[b].size;
|
||||
if (!base || !size || d->bar[b].is_io)
|
||||
continue;
|
||||
if (phys >= base && phys + len > phys && phys + len <= base + size)
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Release everything a process held. Called when it exits, because a driver
|
||||
// that crashed must not keep its device locked away from its replacement --
|
||||
// that is the entire point of running drivers in processes.
|
||||
void
|
||||
pciuser_release_all(struct proc *p)
|
||||
{
|
||||
acquire(&pciuser_lock);
|
||||
for (int i = 0; i < NPCI; i++)
|
||||
if (owner[i] == p)
|
||||
owner[i] = 0;
|
||||
release(&pciuser_lock);
|
||||
}
|
||||
|
||||
static void
|
||||
copy_name(char *dst, const char *src, int cap)
|
||||
{
|
||||
int i = 0;
|
||||
if (src)
|
||||
for (; i < cap - 1 && src[i]; i++)
|
||||
dst[i] = src[i];
|
||||
dst[i] = 0;
|
||||
}
|
||||
|
||||
static int
|
||||
do_info(uint64 argp)
|
||||
{
|
||||
struct pciuser_info *ui = (struct pciuser_info *)user_ptr(argp, sizeof(*ui));
|
||||
if (!ui)
|
||||
return -EFAULT;
|
||||
|
||||
unsigned int idx = ui->index;
|
||||
if (idx >= (unsigned int)pci_count())
|
||||
return -ENODEV;
|
||||
|
||||
const struct pci_dev *d = pci_get((int)idx);
|
||||
if (!d)
|
||||
return -ENODEV;
|
||||
|
||||
memset(ui, 0, sizeof(*ui));
|
||||
ui->index = idx;
|
||||
ui->bus = d->bus;
|
||||
ui->slot = d->slot;
|
||||
ui->func = d->func;
|
||||
ui->header_type = d->header_type;
|
||||
ui->vendor = d->vendor;
|
||||
ui->device = d->device;
|
||||
ui->class = d->class;
|
||||
ui->subclass = d->subclass;
|
||||
ui->prog_if = d->prog_if;
|
||||
ui->revision = d->revision;
|
||||
ui->irq = d->irq;
|
||||
ui->claimed = owner[idx] ? 1 : 0;
|
||||
|
||||
if (owner[idx])
|
||||
copy_name(ui->driver, "userspace", sizeof(ui->driver));
|
||||
else
|
||||
copy_name(ui->driver, d->driver, sizeof(ui->driver));
|
||||
|
||||
for (int b = 0; b < PCIUSER_MAXBAR; b++) {
|
||||
ui->bar[b].base = d->bar[b].base;
|
||||
ui->bar[b].size = d->bar[b].size;
|
||||
ui->bar[b].flags = 0;
|
||||
if (d->bar[b].base) {
|
||||
ui->bar[b].flags |= d->bar[b].is_io ? PCIUSER_BAR_IO : PCIUSER_BAR_MMIO;
|
||||
if (d->bar[b].prefetch)
|
||||
ui->bar[b].flags |= PCIUSER_BAR_PREFETCH;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
do_claim(uint64 argp)
|
||||
{
|
||||
int idx;
|
||||
if (copy_from_user(&idx, argp, sizeof(idx)) < 0)
|
||||
return -EFAULT;
|
||||
if (idx < 0 || idx >= pci_count() || idx >= NPCI)
|
||||
return -ENODEV;
|
||||
|
||||
const struct pci_dev *d = pci_get(idx);
|
||||
if (!d)
|
||||
return -ENODEV;
|
||||
|
||||
// Refuse a device the kernel is already driving.
|
||||
//
|
||||
// Two drivers on one device is not a subtle failure -- it is two writers of
|
||||
// the same registers, and what it looks like from outside is hardware that
|
||||
// has started lying. The eviction path (docs/plan/hybrid.md) is for the
|
||||
// kernel to *hand over* a device deliberately; until a driver can be told to
|
||||
// let go, the honest answer to "may I have the one you are using" is no.
|
||||
if (d->driver)
|
||||
return -EBUSY;
|
||||
|
||||
acquire(&pciuser_lock);
|
||||
if (owner[idx] && owner[idx] != myproc()) {
|
||||
release(&pciuser_lock);
|
||||
return -EBUSY;
|
||||
}
|
||||
owner[idx] = myproc();
|
||||
release(&pciuser_lock);
|
||||
|
||||
// Bus mastering and memory space, because a driver that has to ask for this
|
||||
// separately will forget, and the symptom -- DMA that silently does nothing
|
||||
// -- costs a day to find.
|
||||
pci_enable((struct pci_dev *)d, 0x0006);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
do_release(uint64 argp)
|
||||
{
|
||||
int idx;
|
||||
if (copy_from_user(&idx, argp, sizeof(idx)) < 0)
|
||||
return -EFAULT;
|
||||
if (idx < 0 || idx >= NPCI)
|
||||
return -ENODEV;
|
||||
|
||||
acquire(&pciuser_lock);
|
||||
if (owner[idx] == myproc())
|
||||
owner[idx] = 0;
|
||||
release(&pciuser_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Memory a device can be pointed at.
|
||||
//
|
||||
// mmap cannot do this: a process can map memory, but nothing tells it the
|
||||
// *physical* address, and a device only understands physical addresses. So
|
||||
// the kernel allocates from the DMA arena -- which is contiguous, which
|
||||
// kalloc's free list is not -- maps it in, and reports both.
|
||||
static int
|
||||
do_dma(uint64 argp)
|
||||
{
|
||||
struct pciuser_dma *req = (struct pciuser_dma *)user_ptr(argp, sizeof(*req));
|
||||
if (!req)
|
||||
return -EFAULT;
|
||||
|
||||
uint64 size = PGROUNDUP(req->size);
|
||||
if (size == 0 || size > 4 * 1024 * 1024)
|
||||
return -EINVAL;
|
||||
|
||||
void *kva = dma_alloc(size, PGSIZE);
|
||||
if (!kva)
|
||||
return -ENOMEM;
|
||||
|
||||
// Its own mapping in the caller, taken from the same bump allocator mmap
|
||||
// uses so the two cannot collide.
|
||||
struct proc *p = myproc();
|
||||
if (p->mmap_next == 0)
|
||||
p->mmap_next = 0x40000000;
|
||||
uint64 va = p->mmap_next;
|
||||
p->mmap_next += size;
|
||||
|
||||
uint64 phys = dma_phys(kva);
|
||||
if (mappages(p->pgdir, (void *)va, size, phys, PTE_W | PTE_U) < 0) {
|
||||
dma_free(kva, size);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
req->uaddr = va;
|
||||
req->paddr = phys;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
pciuser_ioctl(uint64 request, uint64 argp)
|
||||
{
|
||||
switch (request) {
|
||||
case PCIIOC_COUNT: {
|
||||
int n = pci_count();
|
||||
put_user(n, argp);
|
||||
return 0;
|
||||
}
|
||||
case PCIIOC_INFO: return do_info(argp);
|
||||
case PCIIOC_CLAIM: return do_claim(argp);
|
||||
case PCIIOC_RELEASE: return do_release(argp);
|
||||
case PCIIOC_DMA: return do_dma(argp);
|
||||
default: return -ENOTTY;
|
||||
}
|
||||
}
|
||||
|
||||
// Reads and writes are not how this device is used -- registers come through
|
||||
// mmap and everything else through ioctl -- but a device in devsw needs both,
|
||||
// and returning 0 is a better answer than a null pointer dereference.
|
||||
int
|
||||
pciuser_read(struct inode *ip, uint64 off, char *dst, int n)
|
||||
{
|
||||
(void)ip; (void)off; (void)dst; (void)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
pciuser_write(struct inode *ip, uint64 off, char *src, int n)
|
||||
{
|
||||
(void)ip; (void)off; (void)src;
|
||||
return n;
|
||||
}
|
||||
Loading…
Reference in a new issue