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
4
Makefile
4
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))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
196
cmd/pcitest/main.c
Normal 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();
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
97
include/pciuser.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
};
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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++){
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue