SchOS/kernel/dev/disk.c
Nexoniarz ad2b321d56 dev: raw disks are 64-bit, so a real drive stops wrapping at 4 GB
struct file already carried a uint64 offset and bread already took a
uint64 block number. The narrowing was one boundary in the middle:
devsw's read and write took a 32-bit uint, so fileread passed a 64-bit
f->off into a 32-bit parameter and the top half was discarded.

The effect was not a refusal at 4 GB but silent wraparound past it. A
read at 5 GB returned the bytes at 1 GB, reporting success. disk.c had a
DISK_MAX_BYTES constant documenting the limit, and nothing anywhere
referenced it -- the cap was described but never enforced, which is the
worst of the three options.

The signature is now uint64 across all nine files that carry it, and
diskread/diskwrite no longer narrow the block number on the way to
bread. Widening this is a prerequisite for NVMe and for any real disk:
namespaces start well above 4 GB and the failure mode reads valid data
from the wrong place.

Verified end to end rather than by inspection. A 6 GB sparse image with
"PAST4GB-MARKER-OK" written at byte 5368709120, attached as /dev/hdb to a
ramdisk-root kernel so device 1 was free:

  # dd if=/dev/hdb bs=4096 skip=1310720 count=1 | od -c | head -2
  0000000   P   A   S   T   4   G   B   -   M   A   R   K   E   R   -   O

Under the old code that offset truncates to 1 GB, where the image is
zeroes. The whole path is exercised: f->off, devsw, diskread, bread and
ide.c's LBA48.

Worth recording for next time: `make ramroot` stages the ESP itself, and
running `make esp` afterwards rebuilds the kernel without RAMROOT and
silently undoes it. The tell is a 855 KB kernel where a 25 MB one is
expected.

110 [ok], no failures, on the default build.
2026-08-11 12:15:38 +02:00

101 lines
3.3 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"
// The byte offset is 64-bit, all the way from struct file's off through the
// devsw signature to bread's block number. It used to narrow to a 32-bit uint
// at the devsw boundary, which capped a raw device at 4 GB -- and the constant
// that documented the cap was never referenced by anything, so the failure was
// not a refusal at 4 GB but silent wraparound past it. Nothing here may narrow
// it again: an NVMe namespace or any modern disk is far beyond that.
int
diskread(struct inode *ip, uint64 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 = off + (uint64)done;
uint64 blk = 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, uint64 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 = off + (uint64)done;
uint64 blk = 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;
}