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

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

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

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

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

1975 lines
54 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;
// 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 (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;
}
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)
{
// 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;
// A private copy, because following a link rewrites what is left of it.
char safe[MAXPATH];
safestrcpy(safe, path, MAXPATH);
char *spath = safe;
if (err)
*err = ENOENT;
// 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);
if (err)
*err = ENOTDIR;
return 0;
}
if (nameiparent && *spath == '\0') {
iunlock(ip);
return ip;
}
if ((next = dirlookup(ip, name, 0)) == 0) {
iunlockput(ip);
return 0;
}
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.
char tgt[MAXPATH];
int len = (++links > MAXSYMLINKS) ? -1
: readi(next, 0, (uint64)tgt, 0, MAXPATH - 1);
iunlockput(next);
int rest = strlen(spath);
if (len <= 0 || len + 1 + rest >= MAXPATH) {
iput(ip);
if (err)
*err = len < 0 ? ELOOP : len == 0 ? ENOENT : ENAMETOOLONG;
return 0;
}
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 (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, 0, 0);
}
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, 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[DIRSIZ];
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);
}