Raise the disk ceiling: 64-bit blocks, no FSSIZE panic, LBA48

SchFS is a 64-bit format addressing 64 ZB. The stack under it was not,
and the lowest limit was three orders of magnitude below the one anybody
would have guessed:

  ide.c   panicked on any block >= FSSIZE -- 150000 blocks, 614 MB
  ide.c   computed the sector in an int, and addressed it with LBA28
  bio.c   took a 32-bit block number, capping any device at 16 TB
  schfs   refused blocks past 0xffffffff, mirroring the cache

The FSSIZE check is the one that mattered. FSSIZE describes the size of a
v1 filesystem *image*; it had no business bounding a physical device, and
it meant a disk larger than half a gigabyte panicked the kernel rather
than being read.

struct buf's blockno is uint64 now, and so is bread's. idestart computes
the sector in 64 bits and picks LBA48 when it does not fit in 28 -- per
request, so everything that fits stays on the LBA28 path every existing
image has been read through. The 48-bit registers are written high half
first, because each is a two-deep FIFO and the other order leaves the
drive addressing whatever the low bytes alone describe.

Verified with a 2 GB disk carrying a marker at 1 GiB -- block 262144,
well past the old panic at 150000. The guest reads it back:
"MARKER-PAST-THE-OLD-LIMIT". schfs-check passes including the round-trip;
bootcheck 108 [ok].

Two limits deliberately left, both noted rather than guessed at: raw
/dev/hdN access still goes through devsw's 32-bit offset, so a raw device
is addressable to 4 GB; and usb_storage computes a 32-bit LBA, which is
what SCSI READ(10) carries -- READ(16) is a different command, not a
wider variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-10 18:50:31 +02:00
commit 527e6eac42
5 changed files with 63 additions and 20 deletions

View file

@ -1,7 +1,11 @@
struct buf { struct buf {
int flags; int flags;
uint dev; uint dev;
uint blockno; // 64-bit, because the filesystem's block numbers are. This was a uint, which
// capped any device at 2^32 blocks (16 TB at 4 KB) no matter what the format
// above it could address -- SchFS is a 64-bit format and was being served by
// a 32-bit cache.
uint64 blockno;
struct sleeplock lock; struct sleeplock lock;
uint refcnt; uint refcnt;
struct buf *prev; // LRU cache list struct buf *prev; // LRU cache list

View file

@ -20,7 +20,7 @@ void ignore_sysret(void);
// bio.c // bio.c
void binit(void); void binit(void);
struct buf* bread(uint, uint); struct buf* bread(uint, uint64);
void brelse(struct buf*); void brelse(struct buf*);
void bwrite(struct buf*); void bwrite(struct buf*);

View file

@ -77,7 +77,7 @@ bdevrw(struct buf *b)
// If not found, allocate a buffer. // If not found, allocate a buffer.
// In either case, return locked buffer. // In either case, return locked buffer.
static struct buf* static struct buf*
bget(uint dev, uint blockno) bget(uint dev, uint64 blockno)
{ {
struct buf *b; struct buf *b;
@ -112,7 +112,7 @@ bget(uint dev, uint blockno)
// Return a locked buf with the contents of the indicated block. // Return a locked buf with the contents of the indicated block.
struct buf* struct buf*
bread(uint dev, uint blockno) bread(uint dev, uint64 blockno)
{ {
struct buf *b; struct buf *b;

View file

@ -25,6 +25,11 @@
#define IDE_CMD_RDMUL 0xc4 #define IDE_CMD_RDMUL 0xc4
#define IDE_CMD_SETMULT 0xc6 #define IDE_CMD_SETMULT 0xc6
#define IDE_CMD_WRMUL 0xc5 #define IDE_CMD_WRMUL 0xc5
// The 48-bit forms. Same operations, an address that is not capped at 128 GB.
#define IDE_CMD_READ_EXT 0x24
#define IDE_CMD_WRITE_EXT 0x34
#define IDE_CMD_RDMUL_EXT 0x29
#define IDE_CMD_WRMUL_EXT 0x39
// idequeue points to the buf now being read/written to the disk. // idequeue points to the buf now being read/written to the disk.
// idequeue->qnext points to the next buf to be processed. // idequeue->qnext points to the next buf to be processed.
@ -154,25 +159,60 @@ idestart(struct buf *b)
{ {
if(b == 0) if(b == 0)
panic("idestart"); panic("idestart");
if(b->blockno >= FSSIZE)
panic("incorrect blockno"); // No FSSIZE check. It used to panic on any block past 150000 -- 614 MB at
// BSIZE -- which is a constant describing the *v1 filesystem image*, not the
// disk. It had no business bounding a device, and it was the lowest ceiling
// in the stack by three orders of magnitude: the format addresses 64 ZB and
// the drive was refused past half a gigabyte.
int sector_per_block = BSIZE/SECTOR_SIZE; int sector_per_block = BSIZE/SECTOR_SIZE;
int sector = b->blockno * sector_per_block; uint64 sector = b->blockno * (uint64)sector_per_block;
int read_cmd = (sector_per_block == 1) ? IDE_CMD_READ : IDE_CMD_RDMUL;
int write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE : IDE_CMD_WRMUL;
// No `sector_per_block > 7` panic here any more. That limit existed because // No `sector_per_block > 7` panic here any more. That limit existed because
// nothing had set the drive's multiple count, so anything past the tested // nothing had set the drive's multiple count, so anything past the tested
// two sectors was a guess; ideinit() issues SET MULTIPLE MODE now, and the // two sectors was a guess; ideinit() issues SET MULTIPLE MODE now, and the
// count is whatever BSIZE says. // count is whatever BSIZE says.
// LBA28 addresses 2^28 sectors, which is 128 GB and no more. Past that the
// 48-bit commands are the only way to name the sector at all, so the mode is
// chosen per request: LBA28 stays the path for everything that fits, because
// it is the one every existing image has been read through.
int lba48 = sector >= (1ULL << 28);
int read_cmd, write_cmd;
if(lba48){
read_cmd = (sector_per_block == 1) ? IDE_CMD_READ_EXT : IDE_CMD_RDMUL_EXT;
write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE_EXT : IDE_CMD_WRMUL_EXT;
} else {
read_cmd = (sector_per_block == 1) ? IDE_CMD_READ : IDE_CMD_RDMUL;
write_cmd = (sector_per_block == 1) ? IDE_CMD_WRITE : IDE_CMD_WRMUL;
}
idewait(0); idewait(0);
outb(0x3f6, 0); // generate interrupt outb(0x3f6, 0); // generate interrupt
outb(0x1f2, sector_per_block); // number of sectors
if(lba48){
// The high half goes first, then the low half into the same registers:
// each is a two-deep FIFO, and writing them in the other order leaves the
// drive addressing whichever sector the low bytes alone describe.
outb(0x1f2, 0); // sector count, high byte
outb(0x1f3, (sector >> 24) & 0xff);
outb(0x1f4, (sector >> 32) & 0xff);
outb(0x1f5, (sector >> 40) & 0xff);
outb(0x1f2, sector_per_block); // sector count, low byte
outb(0x1f3, sector & 0xff);
outb(0x1f4, (sector >> 8) & 0xff);
outb(0x1f5, (sector >> 16) & 0xff);
// LBA48 carries no address bits in the drive/head register.
outb(0x1f6, 0x40 | ((b->dev&1)<<4));
} else {
outb(0x1f2, sector_per_block);
outb(0x1f3, sector & 0xff); outb(0x1f3, sector & 0xff);
outb(0x1f4, (sector >> 8) & 0xff); outb(0x1f4, (sector >> 8) & 0xff);
outb(0x1f5, (sector >> 16) & 0xff); outb(0x1f5, (sector >> 16) & 0xff);
outb(0x1f6, 0xe0 | ((b->dev&1)<<4) | ((sector>>24)&0x0f)); outb(0x1f6, 0xe0 | ((b->dev&1)<<4) | ((sector>>24)&0x0f));
}
if(b->flags & B_DIRTY){ if(b->flags & B_DIRTY){
outb(0x1f7, write_cmd); outb(0x1f7, write_cmd);
outsl(0x1f0, b->data, BSIZE/4); outsl(0x1f0, b->data, BSIZE/4);

View file

@ -112,10 +112,9 @@ kd_read(void *ctx, uint64_t blk, void *out)
if (blk == 0) if (blk == 0)
return -1; return -1;
// The buffer cache addresses blocks with a uint, which caps the volume long // The buffer cache takes 64-bit block numbers now, so this no longer caps the
// before the on-disk format does. Refusing out here beats truncating and // volume; the check remains as an overflow guard on the multiply below.
// reading some unrelated sector, which would surface as corruption. if (blk > (~0ULL) / SCHFS_BPB)
if (blk > 0xffffffffULL / SCHFS_BPB)
return -1; return -1;
for (int i = 0; i < SCHFS_BPB; i++) { for (int i = 0; i < SCHFS_BPB; i++) {
@ -133,7 +132,7 @@ kd_write(void *ctx, uint64_t blk, const void *in)
if (blk == 0) if (blk == 0)
return -1; return -1;
if (blk > 0xffffffffULL / SCHFS_BPB) if (blk > (~0ULL) / SCHFS_BPB)
return -1; return -1;
// Straight through the buffer cache, with no log. The xv6 log exists to make // Straight through the buffer cache, with no log. The xv6 log exists to make