SchOS/kernel/dev/blkdev.c
Nexoniarz 7a1e63a553 fs: follow symlinks like Linux; Linux mknod; /proc/partitions
Symlinks are followed in namex_from() now, in the middle of a path as
well as at the end, and a relative target resolves from the directory
that holds the link rather than from the caller's cwd. `ln -s f d1/s;
cat d1/s` said "Symbolic link loop" because it looked for ./f. A loop is
ELOOP after 40 links, a dangling link ENOENT, and open(O_CREAT) through
a link to a file opens the file. divesymlink() is gone.

mknod from a Linux program passes (mode, dev) where the native call
passes (major, minor), and was read as the native one: `mknod n c 1 2`
made node 432,258, and mkfifo made a character device. Both are decoded
now; a FIFO is EPERM, and an existing name is EEXIST.

/proc/mounts listed devtmpfs twice: once from a fixed line and once
from the mount table. The fixed line is gone, and mount_add() refuses
a filesystem mounted on its own root, which is where a second devtmpfs
mount lands. A read-only FAT mount says ro.

/proc/partitions exists, in Linux's format, and /proc/filesystems lists
vfat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 10:50:41 +02:00

407 lines
11 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// The block device registry, and the partition tables on what is in it.
// See include/blkdev.h.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "blkdev.h"
struct blkdev {
int used;
char name[16];
const struct blkdev_ops *ops;
void *priv;
uint64 start; // first sector, in the disk's own sectors
uint64 nsectors;
uint32 secsize;
int parent; // the disk's slot for a partition, -1 for a disk
int schfs; // 1: typed SchFS; -1: typed FAT; 0: anything else
};
static struct blkdev devs[NBLKDEV];
static struct spinlock blklock;
static int blkinit;
static struct blkdev *
lookup(uint dev)
{
if (dev < BLKDEV_FIRST || dev >= BLKDEV_FIRST + NBLKDEV)
return 0;
struct blkdev *d = &devs[dev - BLKDEV_FIRST];
return d->used ? d : 0;
}
static int
add(const char *name, const struct blkdev_ops *ops, void *priv,
uint64 start, uint64 nsectors, uint32 secsize, int parent, int schfs)
{
if (!blkinit) {
initlock(&blklock, "blkdev");
blkinit = 1;
}
acquire(&blklock);
for (int i = 0; i < NBLKDEV; i++) {
if (devs[i].used)
continue;
struct blkdev *d = &devs[i];
safestrcpy(d->name, name, sizeof(d->name));
d->ops = ops;
d->priv = priv;
d->start = start;
d->nsectors = nsectors;
d->secsize = secsize;
d->parent = parent;
d->schfs = schfs;
__sync_synchronize();
d->used = 1;
release(&blklock);
return BLKDEV_FIRST + i;
}
release(&blklock);
cprintf("blkdev: no slot for %s; it will not be usable\n", name);
return -1;
}
// --- partition tables -------------------------------------------------------
static uint32 le32(const uchar *p) { return p[0] | p[1] << 8 | p[2] << 16 | (uint32)p[3] << 24; }
static uint64 le64(const uchar *p) { return le32(p) | (uint64)le32(p + 4) << 32; }
// A GUID as GPT stores it: the first three fields little-endian, the last two
// as bytes. Compared against the printed form, so the constant in blkdev.h is
// the one a person types into sgdisk.
static int
guid_is(const uchar *g, const char *text)
{
static const int order[16] = { 3,2,1,0, 5,4, 7,6, 8,9, 10,11,12,13,14,15 };
uchar want[16];
int n = 0;
for (const char *p = text; *p && n < 32; p++) {
int v;
if (*p == '-')
continue;
if (*p >= '0' && *p <= '9') v = *p - '0';
else if (*p >= 'a' && *p <= 'f') v = *p - 'a' + 10;
else if (*p >= 'A' && *p <= 'F') v = *p - 'A' + 10;
else return 0;
if (n & 1) want[n / 2] |= v; else want[n / 2] = v << 4;
n++;
}
for (int i = 0; i < 16; i++)
if (g[order[i]] != want[i])
return 0;
return 1;
}
static int
guid_zero(const uchar *g)
{
for (int i = 0; i < 16; i++)
if (g[i])
return 0;
return 1;
}
static void
partname(char *out, const char *disk, int n)
{
int i = 0;
for (; disk[i] && i < 12; i++)
out[i] = disk[i];
out[i++] = 'p';
if (n >= 10)
out[i++] = '0' + n / 10;
out[i++] = '0' + n % 10;
out[i] = 0;
}
static int
readsec(struct blkdev *d, uint64 lba, uchar *buf)
{
return d->ops->rw(d->priv, lba, 1, buf, 0);
}
#define ESP_TYPE "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
// The GUID partition table: the header in sector 1, and its entries wherever
// it says. Only the primary copy is read -- a disk whose primary is damaged
// gets reported as having no partitions rather than half of some.
//
// The EFI system partition is registered like any other, as FAT: the root
// probe passes over it (see blkdev_is_schfs), and kernel/fs/fat mounts it.
static int
scan_gpt(int disk, uchar *sec)
{
struct blkdev *d = &devs[disk];
int found = 0;
if (readsec(d, 1, sec) < 0 || memcmp(sec, "EFI PART", 8) != 0)
return -1;
uint64 elba = le64(sec + 72);
uint32 count = le32(sec + 80);
uint32 esize = le32(sec + 84);
if (esize < 128 || esize > d->secsize || (esize & (esize - 1)) ||
count > 256 || elba < 2 || elba >= d->nsectors) {
cprintf("blkdev: %s: GPT header makes no sense; ignoring it\n", d->name);
return -1;
}
uint32 per = d->secsize / esize;
for (uint32 i = 0; i < count; i++) {
if (i % per == 0 && readsec(d, elba + i / per, sec) < 0)
break;
uchar *e = sec + (i % per) * esize;
if (guid_zero(e))
continue;
uint64 first = le64(e + 32), last = le64(e + 40);
if (first == 0 || last < first || last >= d->nsectors)
continue;
char name[16];
partname(name, d->name, i + 1);
int schfs = guid_is(e, SCHFS_GPT_TYPE) ? 1 : guid_is(e, ESP_TYPE) ? -1 : 0;
if (add(name, d->ops, d->priv, first, last - first + 1, d->secsize,
disk, schfs) >= 0) {
kinfo("blkdev: %s: %d MB%s\n", name,
(int)(((last - first + 1) * d->secsize) >> 20),
schfs > 0 ? ", SchFS" : schfs < 0 ? ", EFI system" : "");
found++;
}
}
return found;
}
// The PC partition table, for disks that have one instead. Primary entries
// only; a protective entry (0xEE) means the disk is GPT and was read above.
//
// A FAT volume with no partition table -- a "superfloppy", which is how a
// good many USB sticks leave the factory -- also ends in 55 AA, and its boot
// code would read as four nonsense entries. Linux tells them apart by the
// status byte, which in a real entry is 0x00 or 0x80 and in boot code is
// whatever the code happens to be.
static int
fat_type(uint32 type)
{
switch (type) {
case 0x01: case 0x04: case 0x06: case 0x0B: case 0x0C: case 0x0E: case 0xEF:
return 1;
}
return 0;
}
static void
scan_mbr(int disk, uchar *sec)
{
struct blkdev *d = &devs[disk];
if (readsec(d, 0, sec) < 0 || sec[510] != 0x55 || sec[511] != 0xAA)
return;
for (int i = 0; i < 4; i++) {
uchar *e = sec + 446 + i * 16;
uint32 type = e[4], first = le32(e + 8), n = le32(e + 12);
// Empty, protective, and extended (which would need its own chain read).
if ((e[0] != 0x00 && e[0] != 0x80) ||
type == 0 || type == 0xEE || type == 0x05 || type == 0x0F ||
n == 0 || first + (uint64)n > d->nsectors)
continue;
char name[16];
partname(name, d->name, i + 1);
if (add(name, d->ops, d->priv, first, n, d->secsize, disk,
fat_type(type) ? -1 : 0) >= 0)
kinfo("blkdev: %s: %d MB, MBR type %x\n", name,
(int)(((uint64)n * d->secsize) >> 20), type);
}
}
int
blkdev_register(const char *name, const struct blkdev_ops *ops, void *priv,
uint64 nsectors, uint32 secsize)
{
if (secsize == 0 || secsize > BSIZE || BSIZE % secsize)
return -1;
int dev = add(name, ops, priv, 0, nsectors, secsize, -1, 0);
if (dev < 0)
return -1;
uchar *sec = (uchar *)kalloc();
if (sec) {
if (scan_gpt(dev - BLKDEV_FIRST, sec) < 0)
scan_mbr(dev - BLKDEV_FIRST, sec);
kfree((char *)sec);
}
return dev;
}
void
blkdev_unregister(int dev)
{
struct blkdev *d = lookup(dev);
if (!d)
return;
int disk = dev - BLKDEV_FIRST;
acquire(&blklock);
for (int i = 0; i < NBLKDEV; i++) {
if (devs[i].used && (i == disk || devs[i].parent == disk)) {
devs[i].used = 0;
kinfo("blkdev: %s removed\n", devs[i].name);
release(&blklock);
binval(BLKDEV_FIRST + i);
acquire(&blklock);
}
}
release(&blklock);
}
// --- I/O ---------------------------------------------------------------------
int
blkdev_present(uint dev)
{
return lookup(dev) != 0;
}
// Same contract as iderw(): fill b->data and set B_VALID on a read, write it
// out and clear B_DIRTY on a write. A failed transfer panics, for the reason
// usb_storage_rw() gives: bread() has no way to report one, and a buffer that
// silently holds the wrong bytes is how a filesystem is corrupted.
void
blkdev_rw(struct buf *b)
{
struct blkdev *d = lookup(b->dev);
int write = (b->flags & B_DIRTY) != 0;
if (!holdingsleep(&b->lock))
panic("blkdev_rw: buf not locked");
if (!d)
panic("blkdev_rw: no such device");
uint32 per = BSIZE / d->secsize;
uint64 lba = b->blockno * per;
if (lba + per > d->nsectors)
panic("blkdev_rw: block out of range");
if (d->ops->rw(d->priv, d->start + lba, per, b->data, write) < 0) {
cprintf("blkdev: %s: %s of block %d failed\n", d->name,
write ? "write" : "read", (int)b->blockno);
panic("blkdev_rw: transfer failed");
}
b->flags |= B_VALID;
b->flags &= ~B_DIRTY;
}
int
blkdev_xfer(uint dev, uint64 lba, uint32 count, void *buf, int write)
{
struct blkdev *d = lookup(dev);
if (!d || count == 0 || (uint64)count * d->secsize > BSIZE ||
lba + count > d->nsectors || lba + count < lba)
return -1;
return d->ops->rw(d->priv, d->start + lba, count, buf, write) < 0 ? -1 : 0;
}
uint64
blkdev_sectors(uint dev, uint32 *secsize)
{
struct blkdev *d = lookup(dev);
if (!d)
return 0;
if (secsize)
*secsize = d->secsize;
return d->nsectors;
}
int
blkdev_flush(uint dev)
{
struct blkdev *d = lookup(dev);
if (!d || !d->ops->flush)
return 0;
return d->ops->flush(d->priv);
}
int
blkdev_nth(int i)
{
for (int s = 0; s < NBLKDEV; s++)
if (devs[s].used && i-- == 0)
return BLKDEV_FIRST + s;
return -1;
}
const char *
blkdev_name(uint dev)
{
struct blkdev *d = lookup(dev);
return d ? d->name : "?";
}
int
blkdev_is_schfs(uint dev)
{
struct blkdev *d = lookup(dev);
return d ? d->schfs : 0;
}
// --- /proc/partitions --------------------------------------------------------
static int
put(char *buf, int cap, int off, const char *s)
{
while (*s && off < cap)
buf[off++] = *s++;
return off;
}
static int
putu(char *buf, int cap, int off, uint64 v)
{
char t[24];
int n = 0;
do { t[n++] = '0' + v % 10; v /= 10; } while (v);
while (n && off < cap)
buf[off++] = t[--n];
return off;
}
// putu(), right-aligned in `w` columns, the way printf("%*llu") would.
static int
putw(char *buf, int cap, int off, uint64 v, int w)
{
int n = 1;
for (uint64 t = v; t >= 10; t /= 10)
n++;
for (; w > n; w--)
off = put(buf, cap, off, " ");
return putu(buf, cap, off, v);
}
// /proc/partitions, in Linux's format to the column, because that is what
// reads it: lsblk's fallback, fdisk -l, every installer and rescue script.
// Sizes are in 1 KB blocks. The major is 8 for all of them, the one devtmpfs
// gives every disk here, and the minor is the device number, as in /dev.
int
blkdev_info(char *buf, int cap)
{
int off = put(buf, cap, 0, "major minor #blocks name\n\n");
for (int s = 0; s < NBLKDEV; s++) {
struct blkdev *d = &devs[s];
if (!d->used)
continue;
off = putw(buf, cap, off, 8, 4);
off = putw(buf, cap, off, BLKDEV_FIRST + s, 8);
off = putw(buf, cap, off, (d->nsectors * d->secsize) >> 10, 11);
off = put(buf, cap, off, " ");
off = put(buf, cap, off, d->name);
off = put(buf, cap, off, "\n");
}
return off;
}