Give userspace a raw disk: /dev/hda and /dev/hdb
Nothing outside the kernel could touch a disk. sys_mount does not even look at the device it is handed -- it mounts the one SchFS volume found at boot and ignores `source` -- so a program had no way to read a partition table, format a volume or write a boot sector. An installer is a program that needs all three, so this is the first piece of it. A character device in devsw rather than a separate block layer, because that is where the offset already lives: fileread/filewrite pass f->off and advance it by what comes back, which is everything a seekable device needs. The minor is the IDE device number, the same numbering iderw() uses; two schemes for the same disks would be a bug waiting to happen. Writes bypass every filesystem above them, which is inherent to a raw device and why an installer must target a disk it has not mounted. The nodes are created unconditionally, like every other entry in /dev. That matters more than it looks: with no node, `echo x > /dev/hdb` silently creates an ordinary file of that name and appears to work. It did, and it agreed with itself on read-back; only the host's copy of the disk showed nothing had been written. st_rdev was never filled, so every node in /dev reported "0, 0" under ls -l. Harmless while nothing read it, wrong as soon as something has to tell two disks apart. Filled with the full Linux dev_t encoding rather than (major << 8 | minor), which only works while both stay small. Verified against the host as an independent oracle, both directions: the guest wrote to /dev/hdb and the host saw those bytes at offset 0; the host wrote at offset 4096 and the guest read them back with dd skip=. ls -l now shows 1,1 for console, 2,0 for fb0 and 8,1 for hdb. Known limit, commented in disk.c: devsw passes the offset as a 32-bit uint, so a raw disk is addressable to 4 GB. That must be widened before anyone installs onto a real disk. bootcheck 108 [ok] with -Fb -Tty -Usb -Sock -Evdev -Ps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b428a50145
commit
66152ad2f2
7 changed files with 148 additions and 1 deletions
2
Makefile
2
Makefile
|
|
@ -24,7 +24,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
|
||||||
panic.o uaccess.o qr.o time.o)
|
panic.o uaccess.o qr.o time.o)
|
||||||
|
|
||||||
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
|
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
|
||||||
bio.o cdev.o device.o fb.o ide.o input.o irq.o kbd.o mouse.o \
|
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 vga.o ramdisk.o procfs.o)
|
pci.o text.o ttf.o uart.o vga.o ramdisk.o procfs.o)
|
||||||
|
|
||||||
# Drivers for hardware that has to be *found* rather than assumed. Anything
|
# Drivers for hardware that has to be *found* rather than assumed. Anything
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,16 @@ main(void)
|
||||||
mknod("/dev/input/event0", 7, 0); // keyboard
|
mknod("/dev/input/event0", 7, 0); // keyboard
|
||||||
mknod("/dev/input/event1", 7, 1); // mouse
|
mknod("/dev/input/event1", 7, 1); // mouse
|
||||||
|
|
||||||
|
// Raw disks (kernel/dev/disk.c). The minor is the IDE device number, so
|
||||||
|
// hda is the disk the kernel booted from and hdb the second channel.
|
||||||
|
//
|
||||||
|
// Created unconditionally, the same way every other node here is: opening
|
||||||
|
// one when no drive is there fails with ENXIO, which is a better answer
|
||||||
|
// than a missing path, and it keeps this list a description of the device
|
||||||
|
// model rather than of one machine's hardware.
|
||||||
|
mknod("/dev/hda", 8, 0);
|
||||||
|
mknod("/dev/hdb", 8, 1);
|
||||||
|
|
||||||
// --- /proc -----------------------------------------------------------
|
// --- /proc -----------------------------------------------------------
|
||||||
// Synthesised status files, major 6 (kernel/dev/procfs.c). These are the
|
// Synthesised status files, major 6 (kernel/dev/procfs.c). These are the
|
||||||
// global ones; per-process /proc/<pid>/ directories need synthetic
|
// global ones; per-process /proc/<pid>/ directories need synthetic
|
||||||
|
|
|
||||||
|
|
@ -444,6 +444,18 @@ bootstrap(void)
|
||||||
mknod("/dev/input/event0", 7, 0); // keyboard
|
mknod("/dev/input/event0", 7, 0); // keyboard
|
||||||
mknod("/dev/input/event1", 7, 1); // mouse
|
mknod("/dev/input/event1", 7, 1); // mouse
|
||||||
|
|
||||||
|
// Raw disks (kernel/dev/disk.c). The minor is the IDE device number, so
|
||||||
|
// hda is device 0 and hdb device 1 -- the same numbering iderw() uses.
|
||||||
|
//
|
||||||
|
// Created unconditionally, like everything else here: opening one when no
|
||||||
|
// drive is present fails with ENXIO, which is a better answer than a
|
||||||
|
// missing path. It also matters that these exist even when absent -- with
|
||||||
|
// no node, `echo x > /dev/hdb` silently creates an ordinary file of that
|
||||||
|
// name and appears to succeed, which is a confusing way to discover the
|
||||||
|
// node was never made.
|
||||||
|
mknod("/dev/hda", 8, 0);
|
||||||
|
mknod("/dev/hdb", 8, 1);
|
||||||
|
|
||||||
// /proc is a real filesystem (kernel/fs/procfs.c), not a handful of nodes:
|
// /proc is a real filesystem (kernel/fs/procfs.c), not a handful of nodes:
|
||||||
// it has to be, because one directory per process cannot be created ahead
|
// it has to be, because one directory per process cannot be created ahead
|
||||||
// of time. That is what makes ps and top work.
|
// of time. That is what makes ps and top work.
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,10 @@ void fb_mapped(void);
|
||||||
int fbread(struct inode*, uint, char*, int);
|
int fbread(struct inode*, uint, char*, int);
|
||||||
int fbwrite(struct inode*, uint, char*, int);
|
int fbwrite(struct inode*, uint, char*, int);
|
||||||
int fb_ioctl(int request, addr_t arg);
|
int fb_ioctl(int request, addr_t arg);
|
||||||
|
// dev/disk.c -- /dev/hdN, raw seekable access to a whole disk. The minor is the
|
||||||
|
// IDE device number. Writes bypass every filesystem above them.
|
||||||
|
int diskread(struct inode*, uint, char*, int);
|
||||||
|
int diskwrite(struct inode*, uint, char*, int);
|
||||||
// drivers/net/e1000 -- raw Ethernet. No protocol stack above it yet, so these
|
// drivers/net/e1000 -- raw Ethernet. No protocol stack above it yet, so these
|
||||||
// are the whole interface: queue a frame, take a frame, and the MAC.
|
// are the whole interface: queue a frame, take a frame, and the MAC.
|
||||||
int e1000_transmit(const void *frame, int len);
|
int e1000_transmit(const void *frame, int len);
|
||||||
|
|
|
||||||
|
|
@ -104,4 +104,8 @@ struct devsw devsw[] = {
|
||||||
// /dev/input/eventN (major 7) — raw evdev events, kernel/dev/input.c. The
|
// /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.
|
// minor is the device index, so event0 is the keyboard and event1 the mouse.
|
||||||
[7] = { inputread, inputwrite, inputready, inputclose, inputopen },
|
[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 },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
100
kernel/dev/disk.c
Normal file
100
kernel/dev/disk.c
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
// Raw block devices: /dev/hda, /dev/hdb.
|
||||||
|
//
|
||||||
|
// Until this existed, nothing outside the kernel could touch a disk. sys_mount
|
||||||
|
// does not even look at the device it is given -- it mounts the one SchFS
|
||||||
|
// volume the kernel found at boot and ignores `source` -- so a program had no
|
||||||
|
// way to read a partition table, format a volume, or write a boot sector. An
|
||||||
|
// installer is exactly a program that needs all three.
|
||||||
|
//
|
||||||
|
// This is a character device in the devsw table rather than a separate block
|
||||||
|
// layer, because that is where the offset already is: fileread/filewrite pass
|
||||||
|
// f->off and advance it by whatever comes back, which is all a seekable device
|
||||||
|
// needs. `dd if=/dev/hdb bs=4096 count=1` works, and so does lseek.
|
||||||
|
//
|
||||||
|
// The minor number is the IDE device number: 0 is the disk the kernel booted
|
||||||
|
// from, 1 is the second channel. Same numbering iderw() uses, deliberately --
|
||||||
|
// two numbering schemes for the same disks is a bug waiting to be written.
|
||||||
|
//
|
||||||
|
// WARNING, and the reason this file is short: writes here bypass the
|
||||||
|
// filesystem entirely. Writing to a device with a mounted filesystem on it will
|
||||||
|
// corrupt that filesystem, because the buffer cache above still believes its
|
||||||
|
// own copy. That is inherent to a raw device and not a defect -- but it is why
|
||||||
|
// an installer must target a disk it has not mounted.
|
||||||
|
|
||||||
|
#include "types.h"
|
||||||
|
#include "defs.h"
|
||||||
|
#include "param.h"
|
||||||
|
#include "spinlock.h"
|
||||||
|
#include "sleeplock.h"
|
||||||
|
#include "fs.h"
|
||||||
|
#include "buf.h"
|
||||||
|
#include "file.h"
|
||||||
|
#include "errno.h"
|
||||||
|
|
||||||
|
// A byte offset arrives as a 32-bit `uint` from the devsw signature, so a raw
|
||||||
|
// device is addressable to 4 GB. Larger disks need the signature widened --
|
||||||
|
// worth doing before anyone installs onto a real one, and called out here
|
||||||
|
// rather than discovered as silent wraparound at the 4 GB mark.
|
||||||
|
#define DISK_MAX_BYTES 0xFFFFFFFFULL
|
||||||
|
|
||||||
|
int
|
||||||
|
diskread(struct inode *ip, uint off, char *dst, int n)
|
||||||
|
{
|
||||||
|
int dev = ip->minor;
|
||||||
|
|
||||||
|
if (!ide_disk_present(dev))
|
||||||
|
return -ENXIO;
|
||||||
|
if (n <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int done = 0;
|
||||||
|
while (done < n) {
|
||||||
|
uint64 pos = (uint64)off + (uint64)done;
|
||||||
|
uint blk = (uint)(pos / BSIZE);
|
||||||
|
uint inblk = (uint)(pos % BSIZE);
|
||||||
|
int want = BSIZE - (int)inblk;
|
||||||
|
if (want > n - done)
|
||||||
|
want = n - done;
|
||||||
|
|
||||||
|
struct buf *b = bread((uint)dev, blk);
|
||||||
|
memmove(dst + done, b->data + inblk, want);
|
||||||
|
brelse(b);
|
||||||
|
|
||||||
|
done += want;
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
diskwrite(struct inode *ip, uint off, char *src, int n)
|
||||||
|
{
|
||||||
|
int dev = ip->minor;
|
||||||
|
|
||||||
|
if (!ide_disk_present(dev))
|
||||||
|
return -ENXIO;
|
||||||
|
if (n <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int done = 0;
|
||||||
|
while (done < n) {
|
||||||
|
uint64 pos = (uint64)off + (uint64)done;
|
||||||
|
uint blk = (uint)(pos / BSIZE);
|
||||||
|
uint inblk = (uint)(pos % BSIZE);
|
||||||
|
int want = BSIZE - (int)inblk;
|
||||||
|
if (want > n - done)
|
||||||
|
want = n - done;
|
||||||
|
|
||||||
|
// Read-modify-write for anything that is not a whole block. bread() first
|
||||||
|
// even when the whole block is being replaced would be a wasted revolution,
|
||||||
|
// but distinguishing the two cases means trusting the caller's alignment;
|
||||||
|
// the disk is not the bottleneck in an installer and correctness here is
|
||||||
|
// worth more than the read.
|
||||||
|
struct buf *b = bread((uint)dev, blk);
|
||||||
|
memmove(b->data + inblk, src + done, want);
|
||||||
|
bwrite(b);
|
||||||
|
brelse(b);
|
||||||
|
|
||||||
|
done += want;
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,17 @@ struct linux_stat {
|
||||||
#define LINUX_S_IFREG 0100000
|
#define LINUX_S_IFREG 0100000
|
||||||
#define LINUX_S_IFLNK 0120000
|
#define LINUX_S_IFLNK 0120000
|
||||||
|
|
||||||
|
// Linux's dev_t: 12 bits of major and 8 of minor packed into the low 32, with
|
||||||
|
// the overflow of each above that. Spelled out in full rather than as
|
||||||
|
// (major << 8 | minor), because that is how the other side decodes it and a
|
||||||
|
// shortcut here would only work while both numbers stayed small.
|
||||||
|
static uint64
|
||||||
|
linux_makedev(uint32 major, uint32 minor)
|
||||||
|
{
|
||||||
|
return ((uint64)(major & 0xfffu) << 8) | (uint64)(minor & 0xffu)
|
||||||
|
| ((uint64)(major & ~0xfffu) << 32) | ((uint64)(minor & ~0xffu) << 12);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
stati_linux(struct inode *ip, struct linux_stat *st)
|
stati_linux(struct inode *ip, struct linux_stat *st)
|
||||||
{
|
{
|
||||||
|
|
@ -74,6 +85,12 @@ stati_linux(struct inode *ip, struct linux_stat *st)
|
||||||
}
|
}
|
||||||
st->st_mode = type_bits | perm_bits;
|
st->st_mode = type_bits | perm_bits;
|
||||||
|
|
||||||
|
// Which device this node actually is. Left at zero until now, so every
|
||||||
|
// entry in /dev printed as "0, 0" under ls -l -- harmless while nothing
|
||||||
|
// read it, and wrong the moment something has to tell two disks apart.
|
||||||
|
if (ip->type == T_DEV)
|
||||||
|
st->st_rdev = linux_makedev(ip->major, ip->minor);
|
||||||
|
|
||||||
st->st_atime = ip->atime; st->st_mtime = ip->mtime; st->st_ctime = ip->ctime;
|
st->st_atime = ip->atime; st->st_mtime = ip->mtime; st->st_ctime = ip->ctime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue