SchOS/kernel/dev/cdev.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

111 lines
3 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);
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 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 },
};