SchOS/cmd/pcitest/main.c
Nexoniarz d1f50c4da3 hybrid: the driver is killed, and the driver comes back
H2 of docs/plan/hybrid.md -- the milestone the plan itself calls the one
that proves the whole idea, and the one that cannot be skipped.

    # cat /proc/schinit
    1 4 11 os.schos.vgpud
    # kill 4
    schinitd: restarting os.schos.vgpud
    vgpud: claimed
    vgpud: pid 8 drew a test pattern from userspace

The test pattern ends in a twelve-block tally of the drawing process's
pid, because a second run that redraws an identical picture is
indistinguishable from a first run that never stopped. Read off the
virtio-gpu scanout across four generations it says 4, 8, 10, 12, and the
DMA arena hands back the same physical address every time.

What it took:

* A halt the kernel can apply to a device it does not understand.
  Clearing PCI_CMD_BUS_MASTER is the only one there is, and it has to
  happen before the DMA goes back to the arena -- a dead driver's device
  keeps reading a framebuffer the kernel is about to give to somebody
  else. Claim sets the bit, release clears it; pcitest shows it in a bm
  column, with a third state so a failed read is never printed as off.

* A refusal. vgpu_release() asks vga_fb_restore() whether there is a
  framebuffer to put the console back on, and declines to hand over the
  device if there is not. The rule that the kernel keeps a minimal
  driver for everything on the path to a shell is worth writing down
  only if something enforces it.

* The DMA arena not being owned by two allocators at once. This is the
  bug that stopped H2: freevm() frees every mapped page below PHYSTOP
  and the arena is the top 16 MB of exactly that range, so a DMA block
  mapped into a driver went onto the page allocator's free list when the
  driver died, while pciuser_release_all() handed the same pages back to
  the arena. The restarted vgpud asked for its framebuffer, got pages
  kalloc() had already given out -- including its own page tables -- and
  dma_alloc()'s zeroing wiped them, so it died inside the ioctl on the
  return from a syscall whose stack had stopped existing. do_dma() now
  records a VMA with no_free set, the way shared memory does.

* One exit status. KeepAlive must restart a driver that crashed and must
  not restart one that has looked at the machine and found its device
  absent: on hardware with no virtio-gpu that was fifteen spawns in
  forty-five seconds, each printing a line to the console, for ever.
  SCHINIT_EXIT_NORESTART is 78, sysexits.h's EX_CONFIG. It is not a new
  manifest key -- the wait status has always travelled from PID 1 to the
  supervisor, and note_exit() simply stopped discarding it.

The milestone's third clause, "the compositor reattaches", is not done
and is not claimed: there is no compositor running. That is recorded in
the plan as a Schcom milestone rather than counted here.

Closes the register row "A released PCI device" in docs/plan/1.0.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 00:05:43 +02:00

267 lines
9.6 KiB
C

// 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
// pcitest <index> x the same, then assert an interrupt and exit WITHOUT
// acking it -- which is what a driver that crashed looks
// like from the kernel's side, and the case that used to
// leave a level-triggered line asserted with no handler
#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 irq class bm 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;
// Bus mastering, straight out of the command register.
//
// Worth a column of its own because it is the one bit that says
// whether a device can still touch memory, and the whole lifecycle
// turns on it: claiming a device sets it, releasing one clears it,
// and a driver that died leaves a device that must not be left
// reading buffers the kernel is about to hand to somebody else.
struct pciuser_cfg cfg;
cfg.index = (unsigned int)i;
cfg.offset = 0x04; // PCI_COMMAND
cfg.value = 0;
// Three states, not two. A read that failed is not the same as a
// bit that is clear, and printing them the same way is how a
// column stops being evidence -- which is exactly what happened
// the first time this was run.
const char *bm = "? ";
if (ioctl(fd, PCIIOC_CFG, &cfg) == 0)
bm = (cfg.value & 0x0004) ? "on " : "-- ";
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(" ");
if (in.irq == 0xFF) puts(" - "); else { putn(in.irq); puts(" "); }
puts(class_name(in.class));
puts(" ");
puts(bm);
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");
// Mechanism 3. Ask for the device's interrupts, then make it interrupt.
//
// The e1000 family has an Interrupt Cause Set register at 0x00C8: writing
// a cause bit asserts the line exactly as the hardware would, which is the
// only way to test interrupt delivery without traffic arriving from
// somewhere. Reading ICR at 0x00C0 clears it, which is the ack the kernel
// cannot do for us.
if (in.vendor != 0x8086 || in.class != 0x02) {
puts("\nmechanisms 2 and 4 work: a driver could live out here.\n");
puts("(interrupts untested -- that part knows only Intel NICs)\n");
exit();
}
if (ioctl(fd, PCIIOC_IRQ, &idx) < 0) {
puts(" interrupts refused -- the line is shared with a kernel driver\n");
puts("\nmechanisms 2 and 4 work: a driver could live out here.\n");
exit();
}
puts(" interrupts routed here (line "); putn(in.irq); puts(")\n");
volatile unsigned int *IMS = &regs[0x00D0 / 4]; // interrupt mask set
volatile unsigned int *ICS = &regs[0x00C8 / 4]; // interrupt cause set
volatile unsigned int *ICR = &regs[0x00C0 / 4]; // cause read, clears
*IMS = 0x00000004; // LSC: link status change, a cause with no traffic
(void)*ICR; // clear anything already pending
*ICS = 0x00000004; // assert it
// Crash simulation: assert the cause and leave, without reading or
// acking. The line is asserted and this process is about to stop existing.
if (argc > 2 && argv[2][0] == 'x') {
puts(" interrupt asserted; exiting WITHOUT acking (simulated crash)\n");
exit();
}
unsigned long long n = 0;
if (read(fd, &n, sizeof n) != (int)sizeof n) {
puts(" read() did not return an interrupt\n");
exit();
}
puts(" interrupt delivered, count = "); putn((int)n); puts("\n");
unsigned int cause = *ICR; // ack at the device
puts(" ICR = 0x"); puthex(cause, 8); puts("\n");
write(fd, &n, sizeof n); // re-arm
puts("\nall three mechanisms work: a driver could live out here.\n");
exit();
}