Nexoniarz is the sole author and SchOS was never published under Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text, every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE, README.md, docs/licensing.md and the site say so. projects/schfs-linux follows the rest of the tree. The GPL gives copyleft: a fork has to ship its source. It also lifts the old "Linux drivers, never" rule, since GPL-2.0-only code combines with GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still has to pick one version, so docs/licensing.md now keeps Apache-2.0 and GPLv3 code out of the kernel to keep Linux code possible there. BSD 4-clause code goes from "avoid" to "no", because the advertising clause is GPL-incompatible. LDLS still ships no .ko. That boundary is now project policy rather than a legal requirement, and ldls.md, devices.md and licensing.md section 6 say so. refmod declares license=GPL, and ldls-check passes (27 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
8.1 KiB
C
185 lines
8.1 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// /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 IRQ + read()
|
|
//
|
|
// 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.
|
|
// A dword of PCI configuration space. A virtio device says where its registers
|
|
// live in vendor capabilities, which are here and nowhere else -- so without
|
|
// this a driver cannot find its own device.
|
|
struct pciuser_cfg {
|
|
unsigned int index; // in
|
|
unsigned int offset; // in: dword-aligned, 0..0xFC
|
|
unsigned int value; // out
|
|
unsigned int pad;
|
|
};
|
|
|
|
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
|
|
#define PCIIOC_CFG 0x9006 // struct pciuser_cfg -- read config space
|
|
#define PCIIOC_PANICFB 0x9007 // struct pciuser_panicfb -- see below
|
|
#define PCIIOC_IRQ 0x9005 // in: int index -- deliver this device's
|
|
// interrupts to this process
|
|
|
|
// Interrupts arrive by *reading* this device, which is how Linux's uio does it
|
|
// and for the same reason: a descriptor composes with poll, and a driver that
|
|
// stops reading falls behind rather than losing events.
|
|
//
|
|
// ioctl(fd, PCIIOC_IRQ, &index);
|
|
// for (;;) {
|
|
// unsigned long long n;
|
|
// read(fd, &n, sizeof n); // blocks until the device interrupts
|
|
// ... service the device, and ACK IT AT THE DEVICE ...
|
|
// write(fd, &n, sizeof n); // re-arm
|
|
// }
|
|
//
|
|
// **The write is not optional.** A PCI interrupt line is level-triggered: it
|
|
// stays asserted until the driver clears the cause *at the device*, and the
|
|
// kernel cannot do that because clearing it is device-specific -- it is a
|
|
// different register on every chip. So the kernel masks the line when the
|
|
// interrupt arrives and leaves it masked until the write says the device has
|
|
// been dealt with.
|
|
//
|
|
// Without that, EOI is signalled on a line that is still asserted and the
|
|
// interrupt re-fires immediately, for ever. That is not hypothetical: it
|
|
// happened in this kernel, at 72,000 interrupts a second, and what it looked
|
|
// like from userspace was a clock running seven times slow.
|
|
// See docs/history/kernel.md section 13.
|
|
//
|
|
// One device per process. A driver drives one thing; if you need two, run two.
|
|
|
|
|
|
|
|
// --- the crash screen, on a display the kernel no longer drives ------------
|
|
//
|
|
// A userspace display driver leaves a hole the kernel cannot see around: when
|
|
// the machine panics, the screen this process is scanning out is frozen at
|
|
// whatever it last put there. The machine is dead and its second monitor is
|
|
// still showing a shell prompt.
|
|
//
|
|
// The kernel cannot fix that by itself. It let go of the device deliberately
|
|
// (PCIIOC_CLAIM), and the process that took it will not run again -- panic
|
|
// means no scheduling, so there is nobody left to ask.
|
|
//
|
|
// So a userspace display driver tells the kernel, in advance, where its
|
|
// scanout lives:
|
|
//
|
|
// struct pciuser_panicfb pfb;
|
|
// pfb.index = index; // the device, already claimed
|
|
// pfb.paddr = dma.paddr; // the framebuffer, from PCIIOC_DMA
|
|
// pfb.width = 1280;
|
|
// pfb.height = 800;
|
|
// pfb.stride = 1280 * 4; // bytes per row; XRGB8888 only
|
|
// ioctl(fd, PCIIOC_PANICFB, &pfb);
|
|
//
|
|
// and on a panic the kernel paints the crash report into that memory and asks
|
|
// the driver it handed the device to -- its *kernel* driver, the one that was
|
|
// told to let go -- to put it on screen one last time. That driver resets the
|
|
// device and brings up a minimal queue from static memory: no allocation, no
|
|
// locks, nothing that can fail differently depending on what the machine was
|
|
// doing when it died.
|
|
//
|
|
// Registering is a promise about memory, so it is checked like one: the range
|
|
// must lie inside a block this process got from PCIIOC_DMA on this device.
|
|
// Without that check it would be a way to nominate any physical address in the
|
|
// machine to be overwritten at panic, which is a worse bug than the one it
|
|
// fixes.
|
|
//
|
|
// A zero paddr unregisters. The registration goes away with the claim and with
|
|
// the process, so a driver that exits leaves nothing behind.
|
|
struct pciuser_panicfb {
|
|
unsigned int index; // in: the claimed device
|
|
unsigned long long paddr; // in: physical address of the scanout, 0 to
|
|
// unregister
|
|
unsigned int width; // in: pixels
|
|
unsigned int height;
|
|
unsigned int stride; // in: bytes per row
|
|
};
|
|
|
|
// 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
|