// 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; }