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.
101 lines
3.3 KiB
C
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;
|
|
}
|