SchOS/kernel/dev/cdev.c
Nexoniarz 24f79194ab 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
2026-09-13 15:34:28 +02:00

121 lines
3.6 KiB
C

#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "x86.h"
#include "errno.h"
extern int consoleread(struct inode*, uint64, char*, int);
extern int consolewrite(struct inode*, uint64, char*, int);
extern int mouseread(struct inode*, uint64, char*, int);
static uint64 rseed = 0x2A99554433221100ULL;
int
nullwrite(struct inode *ip, uint64 off, char *dst, int n)
{
return n;
}
int
nullread(struct inode *ip, uint64 off, char *dst, int n)
{
return 0;
}
int
zeroread(struct inode *ip, uint64 off, char *dst, int n)
{
memset(dst, 0, n);
return n;
}
int
randomread(struct inode *ip, uint64 off, char *dst, int n)
{
for(int i = 0; i < n; i++){
rseed = rseed * 6364136223846793005ULL + 1442695040888963407ULL;
dst[i] = (rseed >> 32) & 0xFF;
}
return n;
}
// /dev/full — reads as an endless run of zero bytes, every write fails with
// ENOSPC. Programs use it to exercise their out-of-disk-space paths.
int
fullwrite(struct inode *ip, uint64 off, char *dst, int n)
{
return -ENOSPC;
}
int
mousewrite(struct inode *ip, uint64 off, char *dst, int n)
{
return n;
}
int
memread(struct inode *ip, uint64 off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consoleread(ip, off, dst, n);
case 2: return nullread(ip, off, dst, n);
case 3: return consoleread(ip, off, dst, n);
/* 10 = /dev/tty, 11..16 = /dev/tty1..tty6. consoleread resolves the
minor to a terminal; see vt_for_minor(). */
case 10: case 11: case 12: case 13: case 14: case 15: case 16:
return consoleread(ip, off, dst, n);
case 5: return zeroread(ip, off, dst, n);
case 7: return zeroread(ip, off, dst, n); // /dev/full reads as zeros
case 8:
case 9: return randomread(ip, off, dst, n);
default: return -1;
}
}
int
memwrite(struct inode *ip, uint64 off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consolewrite(ip, off, dst, n);
case 10: case 11: case 12: case 13: case 14: case 15: case 16:
return consolewrite(ip, off, dst, n);
case 7: return fullwrite(ip, off, dst, n); // /dev/full: always ENOSPC
case 2:
case 5:
case 8:
case 9: return nullwrite(ip, off, dst, n);
default: return -1;
}
}
struct devsw devsw[] = {
[0] = { 0, 0 },
[1] = { memread, memwrite },
// /dev/fb0 (major 2) — the framebuffer, kernel/dev/fb.c. This used to be
// { nullread, nullwrite }, so writing pixels to it succeeded and did
// nothing.
// The close hook gives the text console its screen back when whatever was
// drawing on the framebuffer exits — see fbclose().
[2] = { fbread, fbwrite, 0, fbclose },
[3] = { mouseread, mousewrite },
[4] = { 0, 0 },
// /dev/tty (major 5) — alias for the console
[5] = { memread, memwrite },
// /proc status files (major 6) — see kernel/dev/procfs.c
[6] = { procread, procwrite },
// /dev/input/eventN (major 7) — raw evdev events, kernel/dev/input.c. The
// minor is the device index, so event0 is the keyboard and event1 the mouse.
[7] = { inputread, inputwrite, inputready, inputclose, inputopen },
// /dev/hdN (major 8) — raw disks, kernel/dev/disk.c. The minor is the IDE
// 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 },
};