SchOS/kernel/fs/fs.c
Nexoniarz 03cc316001 schfs: the commit barrier, reflink, and attributes with an index over them
The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.

Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.

Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.

Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.

Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:

  - statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
    so every field after it landed one slot late: userland read f_frsize
    out of f_namelen, got 255, and df reported a 32 MB root filesystem
    as 1992 KB. It hid for as long as it did because the fields used to
    be invented constants, so wrong numbers looked like the usual ones.
  - struct inode's nlink was a short against SchFS's uint32, so a file
    past 32767 names came back negative -- and iput frees an object
    whose count reaches zero. It is an int now, signed on purpose, so an
    underflow lands somewhere a check can see rather than wrapping.
  - filewrite split every write to fit v1's log. That limit belongs to
    the journal, not to writing, and SchFS makes one transaction of
    whatever it is handed: a 1 MB write was becoming 170 commits and
    170 flushes for nothing. fs_ops carries max_write now, and SchFS
    sets no limit.

Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.

Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:02:02 +02:00

1907 lines
51 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 "file.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;
// Defined in fs/procfs.c -- the synthetic /proc.
extern const struct fs_ops procfs_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);
}
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;
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. uptime() is assumed to return ticks;
// replace with a real wall-clock source if your kernel has one.
static uint
now_sec(void)
{
return (uint)uptime();
}
// ===========================================================================
// 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;
ip->valid = 0;
ip->ops = ops_for(dev);
release(&itable.lock);
return ip;
}
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);
}
ip->ref--;
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;
}
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 ((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
// ===========================================================================
static char *
skipelem(char *path, char *name)
{
char *s;
int len;
while (*path == '/')
path++;
if (*path == 0)
return 0;
s = path;
while (*path != '/' && *path != 0)
path++;
len = path - s;
if (len >= DIRSIZ)
memmove(name, s, DIRSIZ);
else {
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 8
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)
{
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.
static struct inode *
namex_from(struct inode *start, char *path, int nameiparent, char *name)
{
struct inode *ip, *next;
// Collapse consecutive slashes into a safe local copy.
char safe[MAXPATH];
int i = 0, j = 0;
while (path[i] != '\0' && j < MAXPATH - 1) {
if (path[i] == '/') {
safe[j++] = '/';
while (path[i] == '/') i++;
} else {
safe[j++] = path[i++];
}
}
safe[j] = '\0';
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)) != 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);
return 0;
}
if (nameiparent && *spath == '\0') {
iunlock(ip);
return ip;
}
if ((next = dirlookup(ip, name, 0)) == 0) {
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = mount_cross(next);
}
if (nameiparent) {
iput(ip);
return 0;
}
return ip;
}
static struct inode *
namex(char *path, int nameiparent, char *name)
{
return namex_from(myproc()->cwd, path, nameiparent, name);
}
struct inode *
namei(char *path)
{
char name[DIRSIZ];
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[DIRSIZ];
return namex_from(start, path, 0, name);
}
struct inode *
nameiparentfrom(struct inode *start, char *path, char *name)
{
return namex_from(start, path, 1, name);
}
// ===========================================================================
// Symlink traversal
//
// Fixed from original:
// - Buffer was char path[MAXPATH+1]. Original called readi(..., MAXPATH)
// then a second readi(... path+len, len, MAXPATH+1) to assert nothing
// more was there. The second read could write 1 byte past the buffer
// if len == MAXPATH.
// - Now: read at most MAXPATH-1 bytes, always NUL-terminate.
// ===========================================================================
struct inode *
divesymlink(struct inode *isym)
{
struct inode *ip = isym;
char path[MAXPATH];
int deep = 0;
do {
int len = readi(ip, 0, (uint64)path, 0, MAXPATH - 1);
if (len < 0) len = 0;
path[len] = '\0';
iunlockput(ip);
if (++deep > 10)
return 0; // symlink loop
if ((ip = namei(path)) == 0)
return 0;
ilock(ip);
} while (ip->type == T_SYMLINK);
return ip;
}
// ===========================================================================
// 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);
}