From 24f79194ab0ca86bdaebc0d2578c3d2ab781b98d Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 13 Sep 2026 15:34:28 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- Makefile | 4 +- cmd/fbinfo/main.c | 4 +- cmd/pcitest/main.c | 196 ++++++++++++++++++++++++++++++ docs/plan/hybrid.md | 39 ++++++ include/defs.h | 6 + include/pciuser.h | 97 +++++++++++++++ include/posix.h | 17 +++ kernel/core/main.c | 1 + kernel/dev/cdev.c | 4 + kernel/dev/pciuser.c | 279 +++++++++++++++++++++++++++++++++++++++++++ kernel/fs/devfs.c | 1 + kernel/proc/proc.c | 5 + kernel/sys/sys_fs.c | 3 + kernel/sys/sys_mem.c | 33 ++++- 14 files changed, 678 insertions(+), 11 deletions(-) create mode 100644 cmd/pcitest/main.c create mode 100644 include/pciuser.h create mode 100644 kernel/dev/pciuser.c diff --git a/Makefile b/Makefile index 35ba8ef..ebb4c75 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o mouse.o \ - pci.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o) + pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o) # Drivers for hardware that has to be *found* rather than assumed. Anything # discovered by walking a bus belongs here; the fixed-address legacy devices @@ -107,7 +107,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = init schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt +CMDS = init schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- diff --git a/cmd/fbinfo/main.c b/cmd/fbinfo/main.c index b106441..bfc5ed8 100644 --- a/cmd/fbinfo/main.c +++ b/cmd/fbinfo/main.c @@ -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) diff --git a/cmd/pcitest/main.c b/cmd/pcitest/main.c new file mode 100644 index 0000000..b08adbc --- /dev/null +++ b/cmd/pcitest/main.c @@ -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 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 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(); +} diff --git a/docs/plan/hybrid.md b/docs/plan/hybrid.md index 33c474b..15cad48 100644 --- a/docs/plan/hybrid.md +++ b/docs/plan/hybrid.md @@ -43,6 +43,45 @@ the one that survives contact with a real machine. --- +## Built so far + +**Mechanisms 2 and 4 exist.** `/dev/pci` (include/pciuser.h, kernel/dev/pciuser.c) +enumerates the bus, hands a device to a process exclusively, maps a BAR into it +and allocates memory a device can be pointed at. `pcitest` proves it end to end: + +``` + # bdf vendor:device class driver + 6 00:04.0 8086:100e network e1000 + 7 00:07.0 8086:10d3 network - + +device 7: 8086:10d3 (network) + claimed + BAR0 at 0x0000000081060000, 128 KB + regs[0..3] = 0x00140261 0x00000000 0x00080283 0x00000000 + MMIO reaches the device + DMA 8 KB at virtual 0x40001000, physical 0x3f8eb000 +``` + +Those register values are the e1000e's CTRL and STATUS, with full-duplex and +link-up set -- which is how you tell a working mapping from one that reads +0xFFFFFFFF off an unmapped address. Claiming a device the kernel drives is +refused, and everything a process held is released when it exits, because a +driver that crashed must not keep its hardware away from its replacement. + +**Interrupts (mechanism 3) and the control channel (mechanism 1) are next.** + +### One thing the interface is deliberately not + +It is about **PCI devices**, not about any driver model, and its first users +will be SchOS's own drivers. That ordering is the design: 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. The +difference matters technically -- a Linux-shaped interface would be wrong for +the evictions this plan actually calls for -- and it happens to be the only +version that survives a licensing conversation. + +--- + ## The mechanisms Four, and only the fourth is large. diff --git a/include/defs.h b/include/defs.h index 55082b2..4557676 100644 --- a/include/defs.h +++ b/include/defs.h @@ -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; diff --git a/include/pciuser.h b/include/pciuser.h new file mode 100644 index 0000000..bf25004 --- /dev/null +++ b/include/pciuser.h @@ -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 diff --git a/include/posix.h b/include/posix.h index f44a6cd..0859535 100644 --- a/include/posix.h +++ b/include/posix.h @@ -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 diff --git a/kernel/core/main.c b/kernel/core/main.c index 2381433..9b8d8d3 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -137,6 +137,7 @@ kmain(uint32 fb_addr) // again from here would land in the last few hundred milliseconds of boot // and change nothing. platform_register_devices(fb_addr); + pciuser_init(); pci_init(); if (boot_verbose) cprintf("verbose boot: pci_init done\n"); pinit(); diff --git a/kernel/dev/cdev.c b/kernel/dev/cdev.c index dd5233d..09eaa10 100644 --- a/kernel/dev/cdev.c +++ b/kernel/dev/cdev.c @@ -114,4 +114,8 @@ struct devsw devsw[] = { // device number. No `ready` hook: a disk is always ready, which is what // fileread assumes when one is absent. [8] = { diskread, diskwrite }, + // /dev/pci (major 9) -- PCI devices reachable from userspace, for drivers + // that do not live in the kernel. Registers arrive through mmap and + // everything else through ioctl; see include/pciuser.h. + [9] = { pciuser_read, pciuser_write }, }; diff --git a/kernel/dev/pciuser.c b/kernel/dev/pciuser.c new file mode 100644 index 0000000..5f8ebcd --- /dev/null +++ b/kernel/dev/pciuser.c @@ -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; +} diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c index 1b58e22..0aa42a9 100644 --- a/kernel/fs/devfs.c +++ b/kernel/fs/devfs.c @@ -87,6 +87,7 @@ static const struct devnode devnodes[] = { { "stdout", 1, 1, D_ROOT }, { "stderr", 1, 1, D_ROOT }, { "fb0", 2, 0, D_ROOT }, + { "pci", 9, 0, D_ROOT }, { "mouse", 3, 1, D_ROOT }, // Raw disks. Created whether or not a drive is present: opening one with no diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index c6a019e..14434b6 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -375,6 +375,11 @@ exit(int status) panic("init resurrection failed: could not exec /bin/schinit"); } + // Give back any device this process had claimed. A driver that crashed must + // not keep its hardware locked away from the process that replaces it -- + // that is the entire point of running drivers outside the kernel. + pciuser_release_all(myproc()); + myproc()->xstate = status; for(fd = 0; fd < NOFILE; fd++){ diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index e6a69af..789e186 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -17,6 +17,7 @@ #include "schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */ #include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */ #include "vt.h" /* the terminals TIOC*/VT_* act on */ +#include "pciuser.h" // ----------------------------------------------------------------------- // Linux-ABI-compatible struct stat (x86_64 "struct stat" from @@ -1032,6 +1033,8 @@ addr_t sys_ioctl(void) return fb_ioctl((int)request, argp); if (is_dev && f->ip->major == INPUT_MAJOR) return input_ioctl(f->ip, (int)request, argp); + if (is_dev && f->ip->major == PCIUSER_MAJOR) + return pciuser_ioctl(request, argp); int is_tty = (is_dev && f->ip->major == 1); if (!is_tty) { diff --git a/kernel/sys/sys_mem.c b/kernel/sys/sys_mem.c index 31222c1..b113e68 100644 --- a/kernel/sys/sys_mem.c +++ b/kernel/sys/sys_mem.c @@ -12,15 +12,13 @@ #include "file.h" #include "syscall.h" #include "errno.h" +#include "posix.h" +#include "pciuser.h" extern int mappages(pml4e_t *pgdir, void *va, uint64 size, uint64 pa, int perm); -#define MAP_SHARED 0x01 -#define MAP_PRIVATE 0x02 -#define MAP_FIXED 0x10 -#define MAP_ANONYMOUS 0x20 -#define PROT_NONE 0 -#define PROT_WRITE 0x2 +/* MAP_* and PROT_* now live in include/posix.h, so userspace and the kernel + compare the same numbers. */ // Reject user mapping requests that fall outside user space. Without this, // MAP_FIXED at an arbitrary address could create user-accessible PTEs over @@ -158,6 +156,29 @@ sys_mmap(void) return va; } + // /dev/pci: the offset is the physical address of a BAR. + // + // Checked against the BARs of the devices *this process has claimed*, + // which is what stops it being /dev/mem with extra steps -- without that + // test a process could map the kernel's own memory and call it a driver. + if (f && f->type == FD_INODE && f->ip->type == T_DEV && + f->ip->major == PCIUSER_MAJOR) { + uint64 phys = PGROUNDDOWN(offset); + uint64 len = PGROUNDUP(length); + if (!pciuser_mmio_ok(phys, len)) + return -EACCES; + // PTE_PWT: device registers are not memory, and a cached write to a + // doorbell arrives whenever the cache feels like it, which for a + // driver is indistinguishable from hardware that ignores you. + if (mappages(myproc()->pgdir, (void*)va, len, phys, + PTE_W | PTE_U | PTE_PWT) < 0) + return -ENOMEM; + v->addr = va; v->length = len; v->used = 1; + v->no_free = 1; // device memory is not ours to free + v->shmid = -1; + return va; + } + if (f && f->type == FD_INODE && f->ip->type == T_DEV && f->ip->major == 2) { // Map the framebuffer where it actually is. This used to map a // hardcoded 0xfd000000 for a hardcoded length, so a request for more