SchOS/kernel/dev/disk.c
Nexoniarz 66152ad2f2 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>
2026-08-09 22:19:40 +02:00

100 lines
3.2 KiB
C

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