SchOS/kernel/fs/fs.c
Nexoniarz eab13241a0 hwreport: describe a machine onto a USB stick, from a Linux-style /sys
hwreport writes one folder to the first read-write FAT mount, or mounts
the first USB stick that takes vfat itself and unmounts it afterwards:
summary.txt (machine, firmware, CPU, memory, every PCI and USB device
with its driver or "-- no driver"), dmesg.txt, lspci.txt in the shape of
lspci -nnk -xxx, lsusb.txt, dmi.txt, memmap.txt, the ACPI tables as raw
.dat files iasl -d reads, and copies of /proc. Serials, the UUID and the
MSDM/SLIC tables (a Windows licence key) are left out.

It reads /sys, which is new: sysfs at Linux's paths and in Linux's
formats -- /sys/bus/pci/devices with uevent, config and resource,
/sys/bus/usb/devices, /sys/firmware/acpi/tables, dmi/tables, memmap
and efi, /sys/class/dmi/id. toybox's lspci and lsusb work unmodified
against it. The USB core now keeps a table of attached devices for it.

The machine's name comes from SMBIOS (kernel/dev/dmi.c). The UEFI loader
hands over the entry point from the configuration table, the kernel
falls back to the 0xF0000 scan, and the log gets Linux's "DMI:" line.

Two bugs found on the way:

- The loader handed over at most 40 memory-map entries, in firmware
  order. A laptop's UEFI map is 60-150 descriptors with RAM above 4 GB
  listed last, so that is what was dropped. The entries now have their
  own area at 0x31000 with room for 256, are sorted before merging, and
  the kernel's 32 usable regions keep the largest when there are more.
- MemTotal and sysinfo's totalram were PHYSTOP, an address, which counts
  the PCI hole: a 6 GB machine reported 8. Now the usable RAM, summed.

usb.ids moves to /usr/share/hwdata, where lsusb looks for it, and
pci.ids joins it (`make pci-ids`, BSD-3-Clause like usb.ids).

Checked under QEMU q35: the report written to a FAT32 stick, fsck.fat
clean, DSDT.dat disassembled by iasl on the host, lspci -k and lsusb
showing names, 6 GB of RAM seen as 6137 MB.

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

2020 lines
55 KiB
C

// SCHFS - File system implementation.
//
// Five layers:
// + Blocks: allocator for raw disk blocks.
// + Log: crash recovery for multi-step updates.
// + Files: inode allocator, reading, writing, metadata.
// + Directories: inode with special contents (list of other inodes!)
// + Names: paths like /usr/rtm/schfs/fs.c for convenient naming.
//
// Derived from "New File System For XV6" by Qi Ma, Leyi Pan, Ao Sun and
// Peiran Xu (MIT License), which itself extends xv6's fs.c. The HTree directory
// index, the Ext4-style extents, the inode bitmap and the symlink support all
// come from there. See NOTICE.
//
// Changes from the original xv6-based fs.c:
// 1. CRC32c checksums on every inode and the superblock.
// 2. Timestamps: atime/mtime/ctime written on alloc, read, write, trunc.
// 3. Proper extent tree for T_EXTENT inodes: struct fsv1_extent packed into
// addrs[], with an indirect extent block for larger files.
// Replaces the lossy "addr*256|len" encoding that overflowed on large
// physical block numbers.
// 4. itrunc for T_EXTENT fixed (original leaked the indirect block and had
// an off-by-one loop termination).
// 5. readi/writei honour the user_dst/user_src flag via copyout/copyin.
// 6. tree_search uses OVERFLOW_DIR_START/END instead of bare magic numbers.
// 7. contiguous_block_allocation: removed the dangerous mid-transaction
// end_op()/begin_op() calls.
// 8. divesymlink: fixed one-byte buffer overrun in the "short read" check.
// 9. updatesb: writes CRC32c checksum before flushing to disk.
// 10. Dead stubs (check_log_full, check_free, cal_block_index, tree_add)
// removed.
// This FS is a joke lol, suck my dick chinese people lol
#include "types.h"
#include "x86.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "spinlock.h"
#include "proc.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "fsstat.h"
#include "file.h"
#include "fat.h"
#include "errno.h"
extern uint uptime(void);
#define min(a, b) ((a) < (b) ? (a) : (b))
// ===========================================================================
// CRC32c (Castagnoli polynomial 0x82F63B78)
// Must be byte-for-byte identical to the copy in mkfs.c.
// ===========================================================================
static uint crc32c_table[256];
static int crc32c_ready = 0;
static void
crc32c_init(void)
{
uint i, j, crc;
for (i = 0; i < 256; i++) {
crc = i;
for (j = 0; j < 8; j++)
crc = (crc >> 1) ^ (crc & 1 ? 0x82F63B78U : 0);
crc32c_table[i] = crc;
}
crc32c_ready = 1;
}
static uint
crc32c(uint crc, const void *buf, uint len)
{
const uchar *p = (const uchar *)buf;
crc = ~crc;
while (len--)
crc = (crc >> 8) ^ crc32c_table[(crc ^ *p++) & 0xFF];
return ~crc;
}
// Compute CRC32c of a dinode. The checksum field itself is zeroed for the
// computation so the result is stable regardless of what was stored there.
static uint
inode_checksum(struct dinode *dip)
{
uint saved = dip->checksum;
dip->checksum = 0;
uint ck = crc32c(0, dip, sizeof(*dip));
dip->checksum = saved;
return ck;
}
// ===========================================================================
// Superblock
// ===========================================================================
struct superblock sb;
struct spinlock sblock;
void
readsb(int dev, struct superblock *sb)
{
struct buf *bp = bread(dev, 1);
memmove(sb, bp->data, sizeof(*sb));
brelse(bp);
}
static void
updatesb(int dev, struct superblock *sb)
{
// Recompute checksum with the checksum field zeroed.
sb->checksum = 0;
sb->checksum = crc32c(0, sb, sizeof(*sb));
struct buf *bp = bread(dev, 1);
memmove(bp->data, sb, sizeof(*sb));
log_write(bp);
brelse(bp);
}
void
fsinit(int dev)
{
if (!crc32c_ready)
crc32c_init();
initlock(&sblock, "superblock");
readsb(dev, &sb);
if (sb.magic != FSMAGIC)
panic("fsinit: invalid file system");
// Validate superblock checksum. A zero checksum means an old image that
// predates this field; skip validation so we stay backward-compatible.
if (sb.checksum != 0) {
uint stored = sb.checksum;
sb.checksum = 0;
uint computed = crc32c(0, &sb, sizeof(sb));
sb.checksum = stored;
if (computed != stored)
panic("fsinit: superblock checksum mismatch");
}
initlog(dev);
}
// ===========================================================================
// Block allocator
// ===========================================================================
static void
bzero(int dev, int bno)
{
struct buf *bp = bread(dev, bno);
memset(bp->data, 0, BSIZE);
// Direct write (not logged): the bitmap log_write in balloc is the commit
// point. If we crash before commit, the bitmap reverts and this block stays
// free — the zero content is harmless. Logging every new block would burn
// most of LOGSIZE when tree_expand pre-allocates hash-tree blocks.
bwrite(bp);
brelse(bp);
}
static uint
balloc(uint dev)
{
int b, bi, m;
struct buf *bp = 0;
for (b = 0; b < sb.size; b += BPB) {
bp = bread(dev, BBLOCK(b, sb));
for (bi = 0; bi < BPB && b + bi < sb.size; bi++) {
m = 1 << (bi % 8);
if ((bp->data[bi / 8] & m) == 0) {
bp->data[bi / 8] |= m;
log_write(bp);
brelse(bp);
acquire(&sblock);
sb.freeblocks--;
release(&sblock);
updatesb(dev, &sb);
bzero(dev, b + bi);
return b + bi;
}
}
brelse(bp);
}
panic("balloc: out of blocks");
}
static void
bfree(int dev, uint b)
{
struct buf *bp;
int bi, m;
bp = bread(dev, BBLOCK(b, sb));
bi = b % BPB;
m = 1 << (bi % 8);
if ((bp->data[bi / 8] & m) == 0)
panic("bfree: freeing free block");
bp->data[bi / 8] &= ~m;
log_write(bp);
brelse(bp);
acquire(&sblock);
sb.freeblocks++;
release(&sblock);
updatesb(dev, &sb);
}
// ===========================================================================
// Inode table
// ===========================================================================
struct {
struct spinlock lock;
struct inode inode[NINODE];
} itable;
void
iinit(void)
{
initlock(&itable.lock, "itable");
for (int i = 0; i < NINODE; i++)
initsleeplock(&itable.inode[i].lock, "inode");
}
struct inode *iget(uint dev, uint64 inum);
// ===========================================================================
// Filesystem dispatch
//
// Every entry point below the syscall layer goes through ip->ops, which iget()
// picks from the device number. The v1 implementations keep their original
// bodies and are simply named v1_*; the public functions are the thin wrappers
// at the bottom of this file.
//
// The one rule worth stating: a filesystem's hooks are only ever called on
// inodes belonging to that filesystem, so none of them needs to check.
// ===========================================================================
static void v1_iload(struct inode *);
static void v1_istore(struct inode *);
static void v1_ifree(struct inode *);
static int v1_read(struct inode *, int, uint64, uint64, uint);
static int v1_write(struct inode *, int, uint64, uint64, uint);
static void v1_truncate(struct inode *, uint64);
static void v1_itrunc_all(struct inode *);
static void v1_itrunc_to(struct inode *, uint64);
static struct inode *v1_dirlookup(struct inode *, char *, uint *);
static int v1_dirlink(struct inode *, char *, uint64);
static int v1_dirunlink(struct inode *, char *, uint);
static int v1_dirempty(struct inode *);
static struct inode *v1_ialloc(uint, short);
static int v1_getdents(struct inode *, char *, int, uint64 *);
static const struct fs_ops v1_ops = {
.name = "schfs-v1",
.iload = v1_iload,
.istore = v1_istore,
.ifree = v1_ifree,
.read = v1_read,
.write = v1_write,
.truncate = v1_truncate,
.dirlookup = v1_dirlookup,
.dirlink = v1_dirlink,
.dirunlink = v1_dirunlink,
.dirempty = v1_dirempty,
.ialloc = v1_ialloc,
.getdents = v1_getdents,
// A write has to fit in the on-disk log, with room for the inode block, the
// bitmap block and the log header. This is the constant filewrite used to
// apply to every filesystem, SchFS included.
.max_write = ((LOGSIZE-1-1-2) / 2) * 512,
};
// Defined in fs/schfs/vfs.c, which is only linked when SchFS is built in.
extern const struct fs_ops schfs_ops;
extern const struct fs_ops fat_ops;
// Defined in fs/procfs.c -- the synthetic /proc.
extern const struct fs_ops procfs_ops;
extern const struct fs_ops devfs_ops;
extern const struct fs_ops sysfs_ops;
// Is this filesystem read-only?
//
// A mounted snapshot is. Callers must ask *before* they start mutating: the
// create path allocates an inode and then panics if the dirlink fails, which is
// a reasonable assumption about a writable filesystem and a crash on a
// read-only one.
int
fs_readonly(uint dev)
{
return SCHFS_IS_SNAP_DEV(dev) || (FAT_IS_DEV(dev) && fat_readonly(dev));
}
static const struct fs_ops *
ops_for(uint dev)
{
// Every mounted SchFS volume, and every mounted snapshot. One table serves
// them all: the operations take an inode, an inode carries its device, and
// kmount.c turns that device into the right volume.
if (SCHFS_IS_ANY_DEV(dev)) return &schfs_ops;
if (dev == PROCFS_DEV) return &procfs_ops;
if (dev == DEVFS_DEV) return &devfs_ops;
if (dev == SYSFS_DEV) return &sysfs_ops;
if (FAT_IS_DEV(dev)) return &fat_ops;
return &v1_ops;
}
// ===========================================================================
// Inode bitmap helpers
// ===========================================================================
static int
searchibmap(uint dev)
{
int b, bi, m;
struct buf *bp = 0;
for (b = 0; b < sb.ninodes; b += BPB) {
bp = bread(dev, IBBLOCK(b, sb));
for (bi = 0; bi < BPB && b + bi < sb.ninodes; bi++) {
m = 1 << (bi % 8);
if ((bp->data[bi / 8] & m) == 0) {
bp->data[bi / 8] |= m;
log_write(bp);
brelse(bp);
acquire(&sblock);
sb.freeinodes--;
release(&sblock);
updatesb(dev, &sb);
return b + bi;
}
}
brelse(bp);
}
panic("searchibmap: out of free inodes");
}
static void
ifree(int dev, int inum)
{
struct buf *bp;
int bi, m;
bp = bread(dev, IBBLOCK(inum, sb));
bi = inum % BPB;
m = 1 << (bi % 8);
if ((bp->data[bi / 8] & m) == 0)
panic("ifree: freeing free inode");
bp->data[bi / 8] &= ~m;
log_write(bp);
brelse(bp);
acquire(&sblock);
sb.freeinodes++;
release(&sblock);
updatesb(dev, &sb);
}
// ===========================================================================
// Timestamp helper
// ===========================================================================
// Returns the current time in seconds since the epoch.
//
// It used to return uptime() under a comment saying to replace it with a real
// wall-clock source "if your kernel has one". It has one -- see wallclock() in
// kernel/core/time.c -- and the same fix went into SchFS's know(), where the
// symptom was visible: every file created after boot was stamped 1970, and the
// timestamps advanced a hundred times too fast because uptime() counts 10 ms
// ticks and they were being read as seconds.
static uint
now_sec(void)
{
uint64 sec;
wallclock(&sec, 0);
return (uint)sec;
}
// ===========================================================================
// Inode allocation / update
// ===========================================================================
struct inode *
v1_ialloc(uint dev, short type)
{
int inum = searchibmap(dev);
struct buf *bp = bread(dev, IBLOCK(inum, sb));
struct dinode *dip = (struct dinode *)bp->data + inum % IPB;
memset(dip, 0, sizeof(*dip));
dip->type = type;
dip->rwmode = RW;
dip->supermode = 0;
dip->showmode = 1;
uint ts = now_sec();
dip->atime = ts;
dip->mtime = ts;
dip->ctime = ts;
dip->checksum = inode_checksum(dip);
log_write(bp);
brelse(bp);
return iget(dev, inum);
}
void
v1_istore(struct inode *ip)
{
struct buf *bp = bread(ip->dev, IBLOCK(ip->inum, sb));
struct dinode *dip = (struct dinode *)bp->data + ip->inum % IPB;
dip->type = ip->type;
dip->major = ip->major;
dip->minor = ip->minor;
// v1's on-disk field is a short and cannot be widened without changing the
// format, so the narrowing is the format's, not an accident of this line.
// The in-memory count is an int now because SchFS needs the range.
dip->nlink = (short)ip->nlink;
dip->size = ip->size;
dip->rwmode = ip->rwmode;
dip->supermode = ip->supermode;
dip->showmode = ip->showmode;
dip->atime = ip->atime;
dip->mtime = ip->mtime;
dip->ctime = ip->ctime;
memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
dip->checksum = inode_checksum(dip);
log_write(bp);
brelse(bp);
}
// ===========================================================================
// iget / idup / ilock / iunlock / iput
// ===========================================================================
struct inode *
iget(uint dev, uint64 inum)
{
struct inode *ip, *empty;
acquire(&itable.lock);
empty = 0;
for (ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++) {
if (ip->ref > 0 && ip->dev == dev && ip->inum == inum) {
ip->ref++;
release(&itable.lock);
return ip;
}
if (empty == 0 && ip->ref == 0)
empty = ip;
}
if (empty == 0)
panic("iget: no inodes");
ip = empty;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
FSSTAT_TAKE(fsstat.inode_used, fsstat.inode_peak); // 0 -> 1
ip->valid = 0;
ip->ops = ops_for(dev);
release(&itable.lock);
return ip;
}
// Is (dev, inum) in the inode cache with a reference? For a filesystem that
// keeps its own table of inode numbers and wants to know which it can drop.
int
iheld(uint dev, uint64 inum)
{
int held = 0;
acquire(&itable.lock);
for (struct inode *ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++)
if (ip->ref > 0 && ip->dev == dev && ip->inum == inum) {
held = 1;
break;
}
release(&itable.lock);
return held;
}
// Is anything on dev held, other than `except` (a mounted root, which the
// mount itself holds)? For umount.
int
ibusy(uint dev, uint64 except)
{
int busy = 0;
acquire(&itable.lock);
for (struct inode *ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++)
if (ip->ref > 0 && ip->dev == dev && ip->inum != except) {
busy = 1;
break;
}
release(&itable.lock);
return busy;
}
struct inode *
idup(struct inode *ip)
{
acquire(&itable.lock);
ip->ref++;
release(&itable.lock);
return ip;
}
static void
v1_iload(struct inode *ip)
{
struct buf *bp = bread(ip->dev, IBLOCK(ip->inum, sb));
struct dinode *dip = (struct dinode *)bp->data + ip->inum % IPB;
// Validate checksum. Zero means an old image without checksums; skip.
if (dip->checksum != 0) {
uint computed = inode_checksum(dip);
if (computed != dip->checksum)
panic("ilock: inode checksum mismatch");
}
ip->type = dip->type;
ip->major = dip->major;
ip->minor = dip->minor;
ip->nlink = dip->nlink;
ip->size = dip->size;
ip->rwmode = dip->rwmode;
ip->supermode = dip->supermode;
ip->showmode = dip->showmode;
ip->atime = dip->atime;
ip->mtime = dip->mtime;
ip->ctime = dip->ctime;
memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
brelse(bp);
ip->valid = 1;
if (ip->type == 0)
panic("ilock: no type");
}
void
ilock(struct inode *ip)
{
if (ip == 0 || ip->ref < 1)
panic("ilock");
acquiresleep(&ip->lock);
if (ip->valid == 0)
ip->ops->iload(ip);
}
void
iunlock(struct inode *ip)
{
if (ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
panic("iunlock");
releasesleep(&ip->lock);
}
void
iput(struct inode *ip)
{
acquire(&itable.lock);
if (ip->ref == 1 && ip->valid && ip->nlink == 0) {
acquiresleep(&ip->lock);
release(&itable.lock);
ip->ops->ifree(ip);
ip->valid = 0;
releasesleep(&ip->lock);
acquire(&itable.lock);
}
if (--ip->ref == 0)
FSSTAT_DROP(fsstat.inode_used);
release(&itable.lock);
}
void
iunlockput(struct inode *ip)
{
iunlock(ip);
iput(ip);
}
// ===========================================================================
// Extent-tree block mapping (T_EXTENT inodes)
//
// Inline extents: addrs[0..NDIRECT-1] hold packed struct fsv1_extent.
// MAX_INLINE_EXTENTS = NDIRECT*sizeof(uint)/sizeof(fsv1_extent) = 4.
// Indirect extent block: addrs[NDIRECT] → physical block containing
// BSIZE/sizeof(fsv1_extent) = 85 more extents.
// addrs[NDIRECT+1] and addrs[NDIRECT+2] are unused for T_EXTENT (future use).
// ===========================================================================
// Find the physical address for logical block bn.
// Allocates a new extent entry (and physical block) if alloc != 0.
static uint
bmap_extent(struct inode *ip, uint bn, int alloc)
{
struct fsv1_extent *ext = (struct fsv1_extent *)ip->addrs;
int n = 0;
// --- Search inline extents ---
for (n = 0; n < (int)MAX_INLINE_EXTENTS; n++) {
if (ext[n].len == 0)
break;
if (bn >= ext[n].start_block && bn < ext[n].start_block + ext[n].len)
return ext[n].start_blkaddr + (bn - ext[n].start_block);
}
// --- Search indirect extent block ---
if (ip->addrs[NDIRECT] != 0) {
struct buf *bp = bread(ip->dev, ip->addrs[NDIRECT]);
struct fsv1_extent *iext = (struct fsv1_extent *)bp->data;
int nind = BSIZE / sizeof(struct fsv1_extent);
for (int i = 0; i < nind; i++) {
if (iext[i].len == 0)
break;
if (bn >= iext[i].start_block && bn < iext[i].start_block + iext[i].len) {
uint addr = iext[i].start_blkaddr + (bn - iext[i].start_block);
brelse(bp);
return addr;
}
}
brelse(bp);
}
if (!alloc)
return 0;
// --- Allocate a new physical block ---
uint addr = balloc(ip->dev);
// Try to extend the last inline extent if blocks are contiguous.
if (n > 0) {
struct fsv1_extent *last = &ext[n - 1];
if (bn == last->start_block + last->len &&
addr == last->start_blkaddr + last->len) {
last->len++;
return addr;
}
}
// Fits in a new inline slot?
if (n < (int)MAX_INLINE_EXTENTS) {
ext[n].start_block = bn;
ext[n].start_blkaddr = addr;
ext[n].len = 1;
return addr;
}
// Spill to indirect extent block.
if (ip->addrs[NDIRECT] == 0)
ip->addrs[NDIRECT] = balloc(ip->dev);
struct buf *bp = bread(ip->dev, ip->addrs[NDIRECT]);
struct fsv1_extent *iext = (struct fsv1_extent *)bp->data;
int nind = BSIZE / sizeof(struct fsv1_extent);
for (int i = 0; i < nind; i++) {
if (iext[i].len == 0) {
// Try contiguity with previous indirect extent.
if (i > 0 &&
bn == iext[i-1].start_block + iext[i-1].len &&
addr == iext[i-1].start_blkaddr + iext[i-1].len) {
iext[i-1].len++;
} else {
iext[i].start_block = bn;
iext[i].start_blkaddr = addr;
iext[i].len = 1;
}
log_write(bp);
brelse(bp);
return addr;
}
}
brelse(bp);
panic("bmap_extent: out of extent entries");
}
// Free all data blocks referenced by a T_EXTENT inode.
static void
itrunc_extent(struct inode *ip)
{
struct fsv1_extent *ext = (struct fsv1_extent *)ip->addrs;
// Free inline extents.
for (int n = 0; n < (int)MAX_INLINE_EXTENTS; n++) {
if (ext[n].len == 0)
break;
for (uint b = 0; b < ext[n].len; b++)
bfree(ip->dev, ext[n].start_blkaddr + b);
ext[n].start_block = 0;
ext[n].start_blkaddr = 0;
ext[n].len = 0;
}
// Free indirect extent block and its extents.
if (ip->addrs[NDIRECT]) {
struct buf *bp = bread(ip->dev, ip->addrs[NDIRECT]);
struct fsv1_extent *iext = (struct fsv1_extent *)bp->data;
int nind = BSIZE / sizeof(struct fsv1_extent);
for (int i = 0; i < nind; i++) {
if (iext[i].len == 0)
break;
for (uint b = 0; b < iext[i].len; b++)
bfree(ip->dev, iext[i].start_blkaddr + b);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
}
// ===========================================================================
// bmap: logical block → physical block (dispatch by inode type)
// ===========================================================================
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
if (ip->type == T_EXTENT)
return bmap_extent(ip, bn, 1);
// Classic direct / singly-indirect / doubly-indirect / triply-indirect.
if (bn < NDIRECT) {
if ((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
// Singly indirect
if (bn < NINDIRECT) {
if ((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint *)bp->data;
if ((addr = a[bn]) == 0) {
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
bn -= NINDIRECT;
// Doubly indirect
if (bn < NINDIRECT * NINDIRECT) {
if ((addr = ip->addrs[NDIRECT + 1]) == 0)
ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint *)bp->data;
if ((addr = a[bn / NINDIRECT]) == 0) {
a[bn / NINDIRECT] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
bp = bread(ip->dev, addr);
a = (uint *)bp->data;
if ((addr = a[bn % NINDIRECT]) == 0) {
a[bn % NINDIRECT] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
bn -= NINDIRECT * NINDIRECT;
// Triply indirect
if (bn < NINDIRECT * NINDIRECT * NINDIRECT) {
if ((addr = ip->addrs[NDIRECT + 2]) == 0)
ip->addrs[NDIRECT + 2] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint *)bp->data;
uint idx1 = bn / (NINDIRECT * NINDIRECT);
if ((addr = a[idx1]) == 0) {
a[idx1] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
bp = bread(ip->dev, addr);
a = (uint *)bp->data;
uint idx2 = (bn / NINDIRECT) % NINDIRECT;
if ((addr = a[idx2]) == 0) {
a[idx2] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
bp = bread(ip->dev, addr);
a = (uint *)bp->data;
uint idx3 = bn % NINDIRECT;
if ((addr = a[idx3]) == 0) {
a[idx3] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}
// ===========================================================================
// itrunc: free all data blocks
// ===========================================================================
void
v1_itrunc_all(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
if (ip->type == T_EXTENT) {
itrunc_extent(ip);
ip->size = 0;
ip->ctime = now_sec();
iupdate(ip);
return;
}
// Direct blocks
for (i = 0; i < NDIRECT; i++) {
if (ip->addrs[i]) {
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
// Singly indirect
if (ip->addrs[NDIRECT]) {
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint *)bp->data;
for (j = 0; j < NINDIRECT; j++)
if (a[j]) bfree(ip->dev, a[j]);
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
// Doubly indirect
if (ip->addrs[NDIRECT + 1]) {
bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
a = (uint *)bp->data;
for (j = 0; j < NINDIRECT; j++) {
if (a[j]) {
struct buf *bp2 = bread(ip->dev, a[j]);
uint *a2 = (uint *)bp2->data;
for (int k = 0; k < NINDIRECT; k++)
if (a2[k]) bfree(ip->dev, a2[k]);
brelse(bp2);
bfree(ip->dev, a[j]);
}
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT + 1]);
ip->addrs[NDIRECT + 1] = 0;
}
// Triply indirect
if (ip->addrs[NDIRECT + 2]) {
bp = bread(ip->dev, ip->addrs[NDIRECT + 2]);
a = (uint *)bp->data;
for (j = 0; j < NINDIRECT; j++) {
if (!a[j]) continue;
struct buf *bp2 = bread(ip->dev, a[j]);
uint *a2 = (uint *)bp2->data;
for (int k = 0; k < NINDIRECT; k++) {
if (!a2[k]) continue;
struct buf *bp3 = bread(ip->dev, a2[k]);
uint *a3 = (uint *)bp3->data;
for (int l = 0; l < NINDIRECT; l++)
if (a3[l]) bfree(ip->dev, a3[l]);
brelse(bp3);
bfree(ip->dev, a2[k]);
}
brelse(bp2);
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT + 2]);
ip->addrs[NDIRECT + 2] = 0;
}
ip->size = 0;
ip->ctime = now_sec();
iupdate(ip);
}
// ===========================================================================
// itrunc_to: shrink inode to newsize, freeing blocks beyond it.
// ip must be locked. newsize must be <= ip->size.
// Triply-indirect partial truncate is not implemented; those blocks are
// only fully freed when newsize falls below their range start.
// ===========================================================================
void
v1_itrunc_to(struct inode *ip, uint64 newsize)
{
int i, j;
struct buf *bp;
uint *a;
if (newsize >= ip->size)
return;
if (ip->type == T_EXTENT) {
if (newsize == 0)
itrunc_extent(ip);
ip->size = newsize;
ip->ctime = now_sec();
iupdate(ip);
return;
}
// first block index that is no longer needed
uint first_free = (newsize == 0) ? 0 : (newsize - 1) / BSIZE + 1;
// Direct blocks
for (i = first_free; i < NDIRECT; i++) {
if (ip->addrs[i]) {
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
// Singly indirect (logical blocks NDIRECT .. NDIRECT+NINDIRECT-1)
if (ip->addrs[NDIRECT]) {
if (first_free <= NDIRECT) {
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint *)bp->data;
for (j = 0; j < NINDIRECT; j++)
if (a[j]) bfree(ip->dev, a[j]);
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
} else {
uint local = first_free - NDIRECT;
if (local < NINDIRECT) {
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint *)bp->data;
int mod = 0;
for (j = local; j < NINDIRECT; j++)
if (a[j]) { bfree(ip->dev, a[j]); a[j] = 0; mod = 1; }
if (mod) log_write(bp);
brelse(bp);
}
}
}
// Doubly indirect (logical blocks NDIRECT+NINDIRECT .. +NINDIRECT^2-1)
if (ip->addrs[NDIRECT + 1]) {
uint di_start = NDIRECT + NINDIRECT;
if (first_free <= di_start) {
bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
a = (uint *)bp->data;
for (j = 0; j < NINDIRECT; j++) {
if (!a[j]) continue;
struct buf *bp2 = bread(ip->dev, a[j]);
uint *a2 = (uint *)bp2->data;
for (int k = 0; k < NINDIRECT; k++)
if (a2[k]) bfree(ip->dev, a2[k]);
brelse(bp2);
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT + 1]);
ip->addrs[NDIRECT + 1] = 0;
} else {
uint off = first_free - di_start;
if (off < (uint)(NINDIRECT * NINDIRECT)) {
uint idx1 = off / NINDIRECT;
uint idx2 = off % NINDIRECT;
bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
a = (uint *)bp->data;
int bp_mod = 0;
for (j = idx1; j < NINDIRECT; j++) {
if (!a[j]) continue;
struct buf *bp2 = bread(ip->dev, a[j]);
uint *a2 = (uint *)bp2->data;
int bp2_mod = 0;
int k_start = (j == (int)idx1) ? (int)idx2 : 0;
for (int k = k_start; k < NINDIRECT; k++)
if (a2[k]) { bfree(ip->dev, a2[k]); a2[k] = 0; bp2_mod = 1; }
if (bp2_mod) log_write(bp2);
brelse(bp2);
if (k_start == 0) {
bfree(ip->dev, a[j]);
a[j] = 0;
bp_mod = 1;
}
}
if (bp_mod) log_write(bp);
brelse(bp);
}
}
}
// Triply indirect: only fully freed when first_free is at or before its range
if (ip->addrs[NDIRECT + 2]) {
uint ti_start = NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT;
if (first_free <= ti_start) {
bp = bread(ip->dev, ip->addrs[NDIRECT + 2]);
a = (uint *)bp->data;
for (j = 0; j < NINDIRECT; j++) {
if (!a[j]) continue;
struct buf *bp2 = bread(ip->dev, a[j]);
uint *a2 = (uint *)bp2->data;
for (int k = 0; k < NINDIRECT; k++) {
if (!a2[k]) continue;
struct buf *bp3 = bread(ip->dev, a2[k]);
uint *a3 = (uint *)bp3->data;
for (int l = 0; l < NINDIRECT; l++)
if (a3[l]) bfree(ip->dev, a3[l]);
brelse(bp3);
bfree(ip->dev, a2[k]);
}
brelse(bp2);
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT + 2]);
ip->addrs[NDIRECT + 2] = 0;
}
}
ip->size = newsize;
ip->mtime = now_sec();
ip->ctime = now_sec();
iupdate(ip);
}
// ===========================================================================
// stati
// ===========================================================================
void
stati(struct inode *ip, struct stat *st)
{
struct stat kst;
memset(&kst, 0, sizeof(kst));
kst.dev = ip->dev;
kst.ino = ip->inum;
kst.type = ip->type;
kst.nlink = ip->nlink;
kst.size = ip->size;
kst.rwmode = ip->rwmode;
kst.supermode = ip->supermode;
kst.showmode = ip->showmode;
kst.atime = ip->atime;
kst.mtime = ip->mtime;
kst.ctime = ip->ctime;
for (int i = 0; i < NDIRECT + 3; i++)
kst.addrs[i] = ip->addrs[i];
memmove(st, &kst, sizeof(kst));
}
// ===========================================================================
// readi / writei
// ===========================================================================
int
v1_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
{
int tot = 0;
struct buf *bp;
if (off >= ip->size)
return 0;
if (off + n > ip->size)
n = ip->size - off;
while (tot < (int)n) {
uint addr = bmap(ip, off / BSIZE);
if (addr == 0)
break;
bp = bread(ip->dev, addr);
if (bp == 0)
break;
int m = min((int)n - tot, (int)(BSIZE - off % BSIZE));
if (user_dst) {
if (copyout(myproc()->pgdir, dst, bp->data + (off % BSIZE), m) < 0) {
brelse(bp);
break;
}
} else {
memmove((void *)dst, bp->data + (off % BSIZE), m);
}
brelse(bp);
tot += m;
off += m;
dst += m;
}
// Update access time. Caller is responsible for calling iupdate() if
// the change needs to be persisted immediately (avoid calling it in hot
// paths like readdir).
ip->atime = now_sec();
return tot;
}
int
v1_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
{
if (off > ip->size || off + n < off)
return -1;
if ((uint64)off + n > (uint64)MAXFILE * BSIZE)
return -1;
uint tot, m;
struct buf *bp;
for (tot = 0; tot < n; tot += m, off += m, src += m) {
bp = bread(ip->dev, bmap(ip, off / BSIZE));
m = min(n - tot, BSIZE - off % BSIZE);
if (user_src) {
if (copyin(myproc()->pgdir, (addr_t)src, bp->data + (off % BSIZE), (uint64)m) < 0) {
brelse(bp);
break;
}
} else {
memmove(bp->data + (off % BSIZE), (void *)src, m);
}
log_write(bp);
brelse(bp);
}
if (off > ip->size)
ip->size = off;
ip->mtime = now_sec();
ip->ctime = now_sec();
iupdate(ip);
return tot;
}
// ===========================================================================
// Directory HTree
// ===========================================================================
int
namecmp(const char *s, const char *t)
{
return strncmp(s, t, DIRSIZ);
}
// BKDR hash → bucket index in [16, 16+HASHRANGE).
unsigned int
BKDRHash(char *str)
{
unsigned int seed = 131;
unsigned int hash = 0;
while (*str)
hash = hash * seed + (*str++);
return (hash % HASHRANGE) + 16;
}
// ---------------------------------------------------------------------------
// tree_expand: grow directory inode to cover byte offset `want`.
// Allocates backing blocks via bmap and zero-fills any partial-block tail.
// ---------------------------------------------------------------------------
uint
tree_expand(struct inode *dp, uint want)
{
if (want < dp->size)
return want;
// Ensure all blocks up to the one containing `want` exist.
uint end_block = want / BSIZE;
for (uint b = dp->size / BSIZE; b <= end_block; b++)
bmap(dp, b);
// Zero-fill the old partial tail of the current last block.
if (dp->size > 0) {
uint inner = dp->size % BSIZE;
if (inner != 0) {
uint fill_end = (dp->size / BSIZE == want / BSIZE)
? (want % BSIZE) + 1
: BSIZE;
struct buf *bp = bread(dp->dev, bmap(dp, (dp->size - 1) / BSIZE));
memset(bp->data + inner, 0, fill_end - inner);
log_write(bp);
brelse(bp);
}
}
dp->size = want + 1;
dp->mtime = now_sec();
dp->ctime = now_sec();
iupdate(dp);
return want;
}
// ---------------------------------------------------------------------------
// tree_search: hash-table directory lookup / slot-allocation.
//
// `add == 0` → find entry matching `name` (de->inum = 0 if not found).
// `add == 1` → find a free slot (expands the dir if needed).
//
// Primary bucket: [hash*16*sizeof(dirent), (hash+1)*16*sizeof(dirent))
// Overflow bucket: [OVERFLOW_DIR_START, OVERFLOW_DIR_END)
// ---------------------------------------------------------------------------
void
tree_search(struct inode *dp, char *name, uint *poff, struct dirent *de, uint add)
{
unsigned int hash_value = BKDRHash(name);
uint tree_node_off = (uint)(hash_value * DIRENT_BUCKET_BYTES);
uint tree_next_node_off = (uint)((hash_value + 1) * DIRENT_BUCKET_BYTES);
if (tree_node_off > MAXFSIZE) {
cprintf("tree_search: name=%s hash=%u off=%u MAXFSIZE=%u\n",
name, hash_value, tree_node_off, MAXFSIZE);
panic("tree_search: bucket offset exceeds MAXFSIZE");
}
// Primary bucket
for (; tree_node_off < tree_next_node_off; tree_node_off += sizeof(struct dirent)) {
if (tree_node_off >= dp->size) {
if (add)
tree_expand(dp, tree_next_node_off - 1);
else {
de->inum = 0;
return;
}
}
if (readi(dp, 0, (uint64)de, tree_node_off, sizeof(struct dirent)) != sizeof(struct dirent)) {
cprintf("tree_search: readi failed: name=%s off=%u size=%u\n",
name, tree_node_off, dp->size);
panic("tree_search: readi");
}
if (!add) {
if (namecmp(de->name, name) == 0) {
if (poff) *poff = tree_node_off;
return;
}
} else {
if (de->inum == 0) {
if (poff) *poff = tree_node_off;
return;
}
}
}
// Overflow bucket
tree_node_off = OVERFLOW_DIR_START;
tree_next_node_off = OVERFLOW_DIR_END;
for (; tree_node_off < tree_next_node_off; tree_node_off += sizeof(struct dirent)) {
if (tree_node_off >= dp->size) {
if (add)
tree_expand(dp, tree_next_node_off - 1);
else {
de->inum = 0;
return;
}
}
if (readi(dp, 0, (uint64)de, tree_node_off, sizeof(struct dirent)) != sizeof(struct dirent)) {
cprintf("tree_search: readi failed (overflow): name=%s off=%u size=%u\n",
name, tree_node_off, dp->size);
panic("tree_search: readi overflow");
}
if (!add) {
if (namecmp(de->name, name) == 0) {
if (poff) *poff = tree_node_off;
return;
}
} else {
if (de->inum == 0) {
if (poff) *poff = tree_node_off;
return;
}
}
}
de->inum = 0;
}
// ===========================================================================
// dirlookup / dirlink / dirlookup_old / build_dir_tree
// ===========================================================================
struct inode *
v1_dirlookup(struct inode *dp, char *name, uint *poff)
{
uint off, inum;
struct dirent de;
if (dp->type != T_DIR)
panic("dirlookup: not a directory");
// "." and ".." are stored at fixed offsets 0 and sizeof(dirent).
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) {
for (off = 0; off < 2 * sizeof(de); off += sizeof(de)) {
if (readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlookup: read");
if (de.inum == 0)
continue;
if (namecmp(name, de.name) == 0) {
if (poff) *poff = off;
inum = de.inum;
return iget(dp->dev, inum);
}
}
return 0;
}
// The format stores DIRSIZ bytes of a name. A longer one cannot be here,
// and comparing only its first DIRSIZ would find a different file.
if (strlen(name) > DIRSIZ)
return 0;
tree_search(dp, name, poff, &de, 0);
if (de.inum)
return iget(dp->dev, de.inum);
return 0;
}
int
v1_dirlink(struct inode *dp, char *name, uint64 inum)
{
uint off = 0;
struct dirent de;
struct inode *ip;
if (strlen(name) > DIRSIZ)
return -1; // see v1_dirlookup
if ((ip = dirlookup(dp, name, 0)) != 0) {
iput(ip);
return -1; // already exists
}
tree_search(dp, name, &off, &de, 1);
strncpy(de.name, name, DIRSIZ);
de.inum = inum;
if (writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink: writei");
return 0;
}
// Remove the entry `name`, which dirlookup reported lives at byte offset `off`.
// v1 marks a slot free by zeroing it in place; the hash bucket keeps its size.
static int
v1_dirunlink(struct inode *dp, char *name, uint off)
{
struct dirent de;
(void)name;
memset(&de, 0, sizeof(de));
if (writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
return -1;
return 0;
}
// 1 if the directory holds nothing but "." and "..".
static int
v1_dirempty(struct inode *dp)
{
struct dirent de;
for (uint64 off = 2 * sizeof(de); off < dp->size; off += sizeof(de)) {
if (readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
return -1;
if (de.inum != 0)
return 0;
}
return 1;
}
static void
v1_ifree(struct inode *ip)
{
itrunc(ip);
ifree(ip->dev, ip->inum);
}
static void
v1_truncate(struct inode *ip, uint64 newsize)
{
// Truncating to zero has its own path: it releases the indirect blocks
// wholesale rather than walking back from an offset, and it is by far the
// common case (every O_TRUNC open).
if (newsize == 0)
v1_itrunc_all(ip);
else
v1_itrunc_to(ip, newsize);
}
// Read directory entries in getdents64 layout.
//
// Directories are hash-indexed (see tree_search/tree_expand), so dp->size can
// be large and mostly empty even for a handful of real entries -- around 115 KB
// for the ~55 files in /bin. Reading one 16-byte dirent at a time meant a
// bmap()+bread() per slot, nearly all of them empty, which was slow enough to
// look like a hang. A whole block at a time fixes that.
static int
v1_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
{
struct dirent block[BSIZE / sizeof(struct dirent)];
int written = 0;
while (*poff < dp->size) {
uint64 blockoff = *poff - (*poff % BSIZE);
int nread = readi(dp, 0, (uint64)block, blockoff, BSIZE);
if (nread <= 0)
break;
int nslots = nread / sizeof(struct dirent);
int slot = (int)((*poff - blockoff) / sizeof(struct dirent));
for (; slot < nslots; slot++) {
uint64 entry_off = blockoff + (uint64)(slot + 1) * sizeof(struct dirent);
struct dirent *xde = &block[slot];
if (xde->inum == 0) { *poff = entry_off; continue; }
int namelen = 0;
while (namelen < DIRSIZ && xde->name[namelen]) namelen++;
int reclen = (8 + 8 + 2 + 1 + namelen + 1 + 7) & ~7;
if (written + reclen > cap)
return written; /* buffer full; resume from *poff */
*poff = entry_off;
uint8 dtype = 0; /* DT_UNKNOWN */
if (xde->inum == dp->inum) {
// "." and any other self-referential entry: iget() would hand back this
// same, already-locked inode, and ilock() on it would self-deadlock on
// the sleeplock the caller is still holding. We already know it is a
// directory, so skip the relock entirely.
dtype = 4; /* DT_DIR */
} else {
struct inode *eip = iget(dp->dev, xde->inum);
if (eip) {
ilock(eip);
if (eip->type == T_DIR) dtype = 4; /* DT_DIR */
else if (eip->type == T_DEV) dtype = 2; /* DT_CHR */
else dtype = 8; /* DT_REG */
iunlockput(eip);
}
}
char *kptr = out + written;
uint64 *p64 = (uint64 *)kptr;
p64[0] = xde->inum;
p64[1] = *poff;
*(uint16 *)(kptr + 16) = (uint16)reclen;
*(uint8 *)(kptr + 18) = dtype;
memmove(kptr + 19, xde->name, namelen);
kptr[19 + namelen] = '\0';
written += reclen;
}
}
return written;
}
// Linear-scan fallback (for readdir iteration over the whole directory).
struct inode *
dirlookup_old(struct inode *dp, char *name, uint *poff)
{
uint off, inum;
struct dirent de;
if (dp->type != T_DIR)
panic("dirlookup_old: not a directory");
for (off = 0; off < dp->size; off += sizeof(de)) {
if (readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlookup_old: read");
if (de.inum == 0)
continue;
if (namecmp(name, de.name) == 0) {
if (poff) *poff = off;
inum = de.inum;
return iget(dp->dev, inum);
}
}
return 0;
}
void
build_dir_tree(struct inode *dp, uint64 pinum)
{
dp->size = 2 * sizeof(struct dirent);
iupdate(dp);
struct dirent de;
// Write "." entry at offset 0.
if (readi(dp, 0, (uint64)&de, 0, sizeof(de)) != sizeof(de))
panic("build_dir_tree: readi .");
strncpy(de.name, ".", DIRSIZ);
de.inum = dp->inum;
if (writei(dp, 0, (uint64)&de, 0, sizeof(de)) != sizeof(de))
panic("build_dir_tree: writei .");
// Write ".." entry at offset sizeof(dirent).
if (readi(dp, 0, (uint64)&de, sizeof(de), sizeof(de)) != sizeof(de))
panic("build_dir_tree: readi ..");
strncpy(de.name, "..", DIRSIZ);
de.inum = pinum;
if (writei(dp, 0, (uint64)&de, sizeof(de), sizeof(de)) != sizeof(de))
panic("build_dir_tree: writei ..");
}
// ===========================================================================
// Path lookup
// ===========================================================================
// Copy the next component of `path` into `name` (VFS_NAME_MAX + 1 bytes) and
// return what follows it, or 0 at the end. A component longer than
// VFS_NAME_MAX sets *toolong and ends the walk.
static char *
skipelem(char *path, char *name, int *toolong)
{
char *s;
int len;
while (*path == '/')
path++;
if (*path == 0)
return 0;
s = path;
while (*path != '/' && *path != 0)
path++;
len = path - s;
if (len > VFS_NAME_MAX) {
*toolong = 1;
return 0;
}
memmove(name, s, len);
name[len] = 0;
while (*path == '/')
path++;
return path;
}
// ===========================================================================
// Mount table
//
// A mount is a substitution made during path resolution: when the walk lands on
// a directory that is a mount point, it continues from the mounted filesystem's
// root instead. Nothing on either disk records it, so unmounting is forgetting.
//
// The reverse direction matters as much as the forward one. ".." at the root of
// a mounted filesystem has to leave it, and the mounted filesystem has no idea
// it is mounted anywhere -- SchFS would answer with its own root, which is how
// a subtree becomes a place you can walk into and never out of.
// ===========================================================================
#define NMOUNT 32
static struct {
struct spinlock lock;
struct {
int used;
uint dev; // device holding the mount point directory
uint64 inum; // the mount point directory itself
uint fsdev; // device of the filesystem mounted on it
uint64 root; // that filesystem's root inode
// What userland called it. Resolution never needs these -- it works in
// (dev, inum) -- but /proc/mounts does, and a path cannot be recovered from
// an inode without walking the whole tree backwards. So the strings are
// kept at mount time, when the caller still has them.
char path[MAXPATH];
char source[MAXPATH];
char type[16];
} m[NMOUNT];
} mtable;
// Copy a string into a fixed field, always NUL-terminating.
static void
mstrcpy(char *dst, const char *src, int cap)
{
int i = 0;
if (src)
for (; i < cap - 1 && src[i]; i++)
dst[i] = src[i];
dst[i] = '\0';
}
void
mountinit(void)
{
initlock(&mtable.lock, "mtable");
}
// Attach `fsdev`:`root` over the directory `dev`:`inum`.
int
mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
const char *path, const char *source, const char *type)
{
// The lookup that found the mount point already crossed any mount on it,
// so mounting devtmpfs on /dev a second time lands on devtmpfs's own root.
// That stacked it on itself and listed it twice in /proc/mounts.
if (dev == fsdev && inum == root)
return -1;
acquire(&mtable.lock);
for (int i = 0; i < NMOUNT; i++)
if (mtable.m[i].used && mtable.m[i].dev == dev &&
mtable.m[i].inum == inum) {
release(&mtable.lock);
return -1; /* already mounted there */
}
for (int i = 0; i < NMOUNT; i++)
if (!mtable.m[i].used) {
mtable.m[i].used = 1;
mtable.m[i].dev = dev;
mtable.m[i].inum = inum;
mtable.m[i].fsdev = fsdev;
mtable.m[i].root = root;
mstrcpy(mtable.m[i].path, path, MAXPATH);
mstrcpy(mtable.m[i].source, source, MAXPATH);
mstrcpy(mtable.m[i].type, type, 16);
release(&mtable.lock);
return 0;
}
release(&mtable.lock);
return -1; /* table full */
}
// Walk the mount table for /proc/mounts. Returns 0 and fills the caller's
// buffers for entry `i`, or -1 when there is no such entry.
//
// An index rather than a callback because the caller formats into a growing
// string buffer and must not hold this lock while doing it.
int
mount_get(int i, char *path, char *source, char *type, uint *fsdev)
{
if (i < 0 || i >= NMOUNT)
return -1;
acquire(&mtable.lock);
if (!mtable.m[i].used) {
release(&mtable.lock);
return -1;
}
mstrcpy(path, mtable.m[i].path, MAXPATH);
mstrcpy(source, mtable.m[i].source, MAXPATH);
mstrcpy(type, mtable.m[i].type, 16);
if (fsdev)
*fsdev = mtable.m[i].fsdev;
release(&mtable.lock);
return 0;
}
// Detach by naming the *mounted* filesystem's root, which is what a path to
// the mount point resolves to once the mount exists. Naming the covered
// directory instead is not possible from userland: the whole point of the
// mount is that the path no longer reaches it.
int
mount_del_by_root(uint fsdev, uint64 root)
{
acquire(&mtable.lock);
for (int i = 0; i < NMOUNT; i++)
if (mtable.m[i].used && mtable.m[i].fsdev == fsdev &&
mtable.m[i].root == root) {
mtable.m[i].used = 0;
release(&mtable.lock);
return 0;
}
release(&mtable.lock);
return -1;
}
// If dev:inum is a mount point, hand back what is mounted on it.
static int
mount_down(uint dev, uint64 inum, uint *fsdev, uint64 *root)
{
acquire(&mtable.lock);
for (int i = 0; i < NMOUNT; i++)
if (mtable.m[i].used && mtable.m[i].dev == dev &&
mtable.m[i].inum == inum) {
*fsdev = mtable.m[i].fsdev;
*root = mtable.m[i].root;
release(&mtable.lock);
return 1;
}
release(&mtable.lock);
return 0;
}
// If dev:inum is a mounted filesystem's root, hand back the directory it
// covers, so ".." can climb out of it.
static int
mount_up(uint dev, uint64 inum, uint *pdev, uint64 *pinum)
{
acquire(&mtable.lock);
for (int i = 0; i < NMOUNT; i++)
if (mtable.m[i].used && mtable.m[i].fsdev == dev &&
mtable.m[i].root == inum) {
*pdev = mtable.m[i].dev;
*pinum = mtable.m[i].inum;
release(&mtable.lock);
return 1;
}
release(&mtable.lock);
return 0;
}
// Follow every mount stacked on this inode, releasing each one as we go. A
// loop rather than a single step because nothing stops a second filesystem
// being mounted on the first one's root.
static struct inode *
mount_cross(struct inode *ip)
{
uint fsdev;
uint64 root;
int hops = 0;
while (mount_down(ip->dev, ip->inum, &fsdev, &root)) {
if (++hops > NMOUNT)
break; /* mounts stacked in a cycle */
struct inode *next = iget(fsdev, root);
iput(ip);
ip = next;
}
return ip;
}
// Resolves `path` starting from `start` (idup'd internally — caller keeps
// ownership of its own reference) rather than always `myproc()->cwd`. This is
// what makes a real (non-AT_FDCWD) dirfd argument to the *at() syscalls
// actually work: an absolute path still resolves from root either way.
//
// Symlinks are followed the way Linux follows them. One in the middle of the
// path always is; the last component only when `follow` says so, since
// lstat(), readlink(), unlink() and friends want the link itself. A relative
// target is resolved from the directory that holds the link, not from the
// caller's cwd, and more than MAXSYMLINKS of them in one lookup is ELOOP.
//
// The link is read with no directory locked: the walk only ever holds a
// reference to the directory it is in, so re-entering it costs nothing.
#define MAXSYMLINKS 40
static struct inode *
namex_from(struct inode *start, char *path, int nameiparent, char *name,
int follow, int *err)
{
struct inode *ip, *next;
int links = 0, toolong = 0;
if (err)
*err = ENOENT;
if (strnlen(path, VFS_PATH_MAX) >= VFS_PATH_MAX) {
if (err)
*err = ENAMETOOLONG;
return 0;
}
// A private copy, because following a link rewrites what is left of it.
// A page each rather than on the stack: PATH_MAX twice over is the whole of
// a kernel stack's slack, and a page is exactly PATH_MAX.
char *safe = kalloc(), *tgt = 0;
if (!safe) {
if (err)
*err = ENOMEM;
return 0;
}
safestrcpy(safe, path, VFS_PATH_MAX);
char *spath = safe;
// Root is a SchFS object, not a v1 inode. ops_for(SCHFS_DEV) then routes
// every operation on it through schfs_ops, which is the whole point: the v1
// format code in this file is no longer reachable from `/`.
ip = (*spath == '/') ? iget(SCHFS_DEV, schfs_kroot(SCHFS_DEV)) : idup(start);
// The root itself can be a mount point, and a relative walk can start on one.
ip = mount_cross(ip);
while ((spath = skipelem(spath, name, &toolong)) != 0) {
// ".." out of a mounted filesystem's root leaves it. The filesystem down
// there does not know it is mounted anywhere and would answer with its own
// root, which turns the mount into a subtree you can enter and never
// leave. Step up to the covered directory first and resolve ".." there.
if (name[0] == '.' && name[1] == '.' && name[2] == '\0') {
uint pdev;
uint64 pinum;
if (mount_up(ip->dev, ip->inum, &pdev, &pinum)) {
struct inode *cover = iget(pdev, pinum);
iput(ip);
ip = cover;
}
}
ilock(ip);
if (ip->type != T_DIR) {
iunlockput(ip);
if (err)
*err = ENOTDIR;
goto fail;
}
if (nameiparent && *spath == '\0') {
iunlock(ip);
goto out;
}
if ((next = dirlookup(ip, name, 0)) == 0) {
iunlockput(ip);
goto fail;
}
iunlock(ip);
next = mount_cross(next);
ilock(next);
if (next->type != T_SYMLINK || (*spath == '\0' && (!follow || nameiparent))) {
iunlock(next);
iput(ip);
ip = next;
continue;
}
// A link: put its target in front of what is left of the path and carry
// on from the directory it was found in, or from root.
if (!tgt && !(tgt = kalloc())) {
iunlockput(next);
iput(ip);
if (err)
*err = ENOMEM;
goto fail;
}
int len = (++links > MAXSYMLINKS) ? -1
: readi(next, 0, (uint64)tgt, 0, VFS_PATH_MAX - 1);
iunlockput(next);
int rest = strlen(spath);
if (len <= 0 || len + 1 + rest >= VFS_PATH_MAX) {
iput(ip);
if (err)
*err = len < 0 ? ELOOP : len == 0 ? ENOENT : ENAMETOOLONG;
goto fail;
}
tgt[len] = '\0';
if (rest) {
tgt[len] = '/';
memmove(tgt + len + 1, spath, rest + 1);
}
memmove(safe, tgt, strlen(tgt) + 1);
spath = safe;
if (*spath == '/') {
iput(ip);
ip = mount_cross(iget(SCHFS_DEV, schfs_kroot(SCHFS_DEV)));
}
}
if (toolong) {
iput(ip);
if (err)
*err = ENAMETOOLONG;
goto fail;
}
if (nameiparent) {
iput(ip);
goto fail;
}
out:
kfree(safe);
if (tgt)
kfree(tgt);
return ip;
fail:
kfree(safe);
if (tgt)
kfree(tgt);
return 0;
}
static struct inode *
namex(char *path, int nameiparent, char *name)
{
return namex_from(myproc()->cwd, path, nameiparent, name, 0, 0);
}
struct inode *
namei(char *path)
{
char name[VFS_NAME_MAX + 1];
return namex(path, 0, name);
}
struct inode *
nameiparent(char *path, char *name)
{
return namex(path, 1, name);
}
// dirfd-relative counterparts, for the *at() syscalls (openat/fstatat/
// unlinkat/...) when given a real directory fd instead of AT_FDCWD.
struct inode *
nameifrom(struct inode *start, char *path)
{
char name[VFS_NAME_MAX + 1];
return namex_from(start, path, 0, name, 0, 0);
}
struct inode *
nameiparentfrom(struct inode *start, char *path, char *name)
{
return namex_from(start, path, 1, name, 0, 0);
}
// The lookup open(), stat(), chdir() and exec() want: a final symlink is
// followed too, and on failure `*err` says why (ENOENT, ENOTDIR, ELOOP,
// ENAMETOOLONG), so a loop is not reported as a missing file.
struct inode *
nameifollow(struct inode *start, char *path, int *err)
{
char name[VFS_NAME_MAX + 1];
return namex_from(start ? start : myproc()->cwd, path, 0, name, 1, err);
}
// ===========================================================================
// Public entry points
//
// Thin wrappers over ip->ops. Everything above the filesystem layer calls
// these and never learns which filesystem it is talking to; adding another one
// is a new fs_ops table and nothing else.
// ===========================================================================
void
iupdate(struct inode *ip)
{
ip->ops->istore(ip);
}
int
readi(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
{
return ip->ops->read(ip, user_dst, dst, off, n);
}
int
writei(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
{
return ip->ops->write(ip, user_src, src, off, n);
}
void
itrunc(struct inode *ip)
{
ip->ops->truncate(ip, 0);
}
void
itrunc_to(struct inode *ip, uint64 newsize)
{
ip->ops->truncate(ip, newsize);
}
struct inode *
dirlookup(struct inode *dp, char *name, uint *poff)
{
return dp->ops->dirlookup(dp, name, poff);
}
int
dirlink(struct inode *dp, char *name, uint64 inum)
{
return dp->ops->dirlink(dp, name, inum);
}
int
dirunlink(struct inode *dp, char *name, uint off)
{
return dp->ops->dirunlink(dp, name, off);
}
int
dirempty(struct inode *dp)
{
return dp->ops->dirempty(dp);
}
int
getdents(struct inode *dp, char *out, int cap, uint64 *poff)
{
return dp->ops->getdents(dp, out, cap, poff);
}
// The device decides the filesystem, so a caller that wants an object on a
// particular volume names that volume rather than a filesystem.
struct inode *
ialloc(uint dev, short type)
{
return ops_for(dev)->ialloc(dev, type);
}