summaryrefslogtreecommitdiff
path: root/fs
diff options
context:
space:
mode:
Diffstat (limited to 'fs')
-rw-r--r--fs/chardev.cc43
-rw-r--r--fs/dcache.cc287
-rw-r--r--fs/devfs.cc137
-rw-r--r--fs/fd.cc216
-rw-r--r--fs/file_table.cc164
-rw-r--r--fs/fs_struct.cc90
-rw-r--r--fs/initramfs.cc285
-rw-r--r--fs/inode.cc166
-rw-r--r--fs/module.mk17
-rw-r--r--fs/namei.cc250
-rw-r--r--fs/pipe.cc278
-rw-r--r--fs/ramfs/file.cc216
-rw-r--r--fs/ramfs/inode.cc164
-rw-r--r--fs/ramfs/super.cc154
-rw-r--r--fs/super.cc95
-rw-r--r--fs/vfs_init.cc78
16 files changed, 2640 insertions, 0 deletions
diff --git a/fs/chardev.cc b/fs/chardev.cc
new file mode 100644
index 0000000..fa96276
--- /dev/null
+++ b/fs/chardev.cc
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * chardev.cc — Character device registry
+ *
+ * Simple linked list of registered cdevs, searched by dev number.
+ */
+
+#include <lume/chardev.h>
+#include <lume/list.h>
+#include <lume/klog.h>
+#include <kernel/sync/spinlock.h>
+
+static list_node g_cdev_list;
+static Spinlock g_cdev_lock;
+
+void cdev_init()
+{
+ list_init(&g_cdev_list);
+}
+
+void cdev_register(cdev* dev)
+{
+ LockGuard guard(g_cdev_lock);
+ list_init(&dev->list);
+ list_add_tail(&dev->list, &g_cdev_list);
+ printk("[cdev] registered '%s' (%d,%d)\n",
+ dev->name, MAJOR(dev->dev), MINOR(dev->dev));
+}
+
+cdev* cdev_find(uint32 dev_nr)
+{
+ LockGuard guard(g_cdev_lock);
+ list_node* pos;
+ for (pos = g_cdev_list.prev; pos != &g_cdev_list; pos = pos->prev) {
+ cdev* cd = container_of(pos, cdev, list);
+ if (cd->dev == dev_nr) {
+ return cd;
+ }
+ }
+ return nullptr;
+}
diff --git a/fs/dcache.cc b/fs/dcache.cc
new file mode 100644
index 0000000..94ab287
--- /dev/null
+++ b/fs/dcache.cc
@@ -0,0 +1,287 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * dcache.cc — Directory entry cache
+ *
+ * Provides dentry allocation, lookup, instantiation, and d_path.
+ */
+
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* ================================================================== */
+/* Hash function */
+/* ================================================================== */
+
+uint32 full_name_hash(const char* name, uint32 len)
+{
+ uint32 hash = 0;
+ for (uint32 i = 0; i < len; i++)
+ hash = hash * 31 + static_cast<uint8>(name[i]);
+ return hash;
+}
+
+/* ================================================================== */
+/* d_set_type — set DCACHE_*_TYPE bits from inode mode */
+/* ================================================================== */
+
+void d_set_type(dentry* de, uint32 i_mode)
+{
+ de->d_flags &= ~DCACHE_ENTRY_TYPE;
+
+ if (S_ISDIR(i_mode))
+ de->d_flags |= DCACHE_DIRECTORY_TYPE;
+ else if (S_ISLNK(i_mode))
+ de->d_flags |= DCACHE_SYMLINK_TYPE;
+ else if (S_ISREG(i_mode))
+ de->d_flags |= DCACHE_REGULAR_TYPE;
+ else
+ de->d_flags |= DCACHE_SPECIAL_TYPE;
+}
+
+/* ================================================================== */
+/* d_alloc — allocate a dentry under a parent */
+/* ================================================================== */
+
+static void dentry_init(dentry* de, dentry* parent, const qstr* name)
+{
+ de->d_flags = DCACHE_MISS_TYPE; /* negative until instantiated */
+ de->d_parent = parent ? parent : de;
+ de->d_inode = nullptr;
+ de->d_op = nullptr;
+ de->d_sb = parent ? parent->d_sb : nullptr;
+ de->d_time = 0;
+ de->d_fsdata = nullptr;
+ de->d_lockref.store(1);
+
+ list_init(&de->d_lru);
+ list_init(&de->d_child);
+ list_init(&de->d_children);
+ list_init(&de->d_alias);
+
+ /* Copy name */
+ if (name && name->name && name->len > 0) {
+ uint32 len = name->len;
+ if (len < DNAME_INLINE_LEN) {
+ for (uint32 i = 0; i < len; i++)
+ de->d_iname[i] = name->name[i];
+ de->d_iname[len] = '\0';
+ de->d_name.name = reinterpret_cast<const char*>(de->d_iname);
+ } else {
+ /* Heap-allocate for long names */
+ char* buf = new char[len + 1];
+ for (uint32 i = 0; i < len; i++)
+ buf[i] = name->name[i];
+ buf[len] = '\0';
+ de->d_name.name = buf;
+ }
+ de->d_name.len = len;
+ de->d_name.hash = name->hash ? name->hash : full_name_hash(name->name, len);
+ } else {
+ de->d_iname[0] = '\0';
+ de->d_name.name = reinterpret_cast<const char*>(de->d_iname);
+ de->d_name.len = 0;
+ de->d_name.hash = 0;
+ }
+}
+
+dentry* d_alloc(dentry* parent, const qstr* name)
+{
+ auto* de = new dentry{};
+ if (!de) return nullptr;
+
+ dentry_init(de, parent, name);
+ de->d_sb = parent->d_sb;
+
+ /* Use sb default d_op if set */
+ if (parent->d_sb->s_d_op)
+ de->d_op = parent->d_sb->s_d_op;
+
+ /* Link into parent's children list */
+ if (parent) {
+ LockGuard guard(parent->d_lock);
+ list_add_tail(&de->d_child, &parent->d_children);
+ dget(parent); /* child holds ref on parent */
+ }
+
+ return de;
+}
+
+dentry* d_alloc_anon(super_block* sb)
+{
+ auto* de = new dentry{};
+ if (!de) return nullptr;
+
+ dentry_init(de, nullptr, nullptr);
+ de->d_sb = sb;
+ de->d_parent = de; /* root: parent is self */
+
+ return de;
+}
+
+/* ================================================================== */
+/* d_instantiate — bind an inode to a dentry */
+/* ================================================================== */
+
+void d_instantiate(dentry* de, inode* ip)
+{
+ if (!ip) return;
+
+ LockGuard guard(de->d_lock);
+ de->d_inode = ip;
+ d_set_type(de, ip->i_mode);
+
+ /* Add to inode's alias list */
+ list_add_tail(&de->d_alias, &ip->i_dentry);
+}
+
+/* ================================================================== */
+/* d_make_root — create root dentry for a superblock */
+/* ================================================================== */
+
+dentry* d_make_root(inode* root_inode)
+{
+ if (!root_inode) return nullptr;
+
+ dentry* de = d_alloc_anon(root_inode->i_sb);
+ if (!de) {
+ iput(root_inode);
+ return nullptr;
+ }
+
+ d_instantiate(de, root_inode);
+ return de;
+}
+
+/* ================================================================== */
+/* d_add — combined d_instantiate + d_rehash */
+/* ================================================================== */
+
+void d_add(dentry* de, inode* ip)
+{
+ d_instantiate(de, ip);
+ /* d_rehash would add to dcache hash table; simplified for now */
+}
+
+/* ================================================================== */
+/* d_lookup — search children of parent for a name match */
+/* ================================================================== */
+
+dentry* d_lookup(const dentry* parent, const qstr* name)
+{
+ LockGuard guard(const_cast<Spinlock&>(parent->d_lock));
+
+ list_node* pos;
+ list_for_each(pos, &parent->d_children) {
+ dentry* child = list_entry(pos, dentry, d_child);
+
+ if (child->d_name.len != name->len)
+ continue;
+
+ /* Compare names */
+ const char* a = child->d_name.name;
+ const char* b = name->name;
+ uint32 len = name->len;
+ bool match = true;
+ for (uint32 i = 0; i < len; i++) {
+ if (a[i] != b[i]) { match = false; break; }
+ }
+ if (match) {
+ dget(child);
+ return child;
+ }
+ }
+
+ return nullptr;
+}
+
+/* ================================================================== */
+/* dget / dput — dentry reference counting */
+/* ================================================================== */
+
+dentry* dget(dentry* de)
+{
+ if (de)
+ de->d_lockref.fetch_add(1);
+ return de;
+}
+
+void dput(dentry* de)
+{
+ if (!de) return;
+
+ int old = de->d_lockref.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Last reference — could free, but for ramfs we keep dentries alive.
+ * A real implementation would check d_op->d_delete and possibly
+ * add to LRU for later reclaim. For now, just leak (Phase 1). */
+}
+
+/* ================================================================== */
+/* d_drop / d_delete */
+/* ================================================================== */
+
+void d_drop(dentry* de)
+{
+ /* Remove from parent's children list */
+ LockGuard guard(de->d_lock);
+ if (!list_empty(&de->d_child))
+ list_del_init(&de->d_child);
+}
+
+void d_delete(dentry* de)
+{
+ d_drop(de);
+}
+
+/* ================================================================== */
+/* d_path — build absolute pathname from a path */
+/* ================================================================== */
+
+char* d_path(const path* p, char* buf, int buflen)
+{
+ if (!p || !p->de || buflen <= 0)
+ return nullptr;
+
+ /* Build path by walking up d_parent chain, writing backwards */
+ char* end = buf + buflen - 1;
+ *end = '\0';
+
+ dentry* de = p->de;
+
+ if (d_is_root(de)) {
+ if (end <= buf) return nullptr;
+ *(--end) = '/';
+ return end;
+ }
+
+ while (!d_is_root(de)) {
+ uint32 len = de->d_name.len;
+ end -= len;
+ if (end <= buf) return nullptr;
+
+ const char* name = de->d_name.name;
+ for (uint32 i = 0; i < len; i++)
+ end[i] = name[i];
+
+ *(--end) = '/';
+ de = de->d_parent;
+ }
+
+ return end;
+}
+
+/* ================================================================== */
+/* dcache_init */
+/* ================================================================== */
+
+void dcache_init()
+{
+ /* Future: init dcache hash table */
+}
diff --git a/fs/devfs.cc b/fs/devfs.cc
new file mode 100644
index 0000000..b4b2cf3
--- /dev/null
+++ b/fs/devfs.cc
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * devfs.cc — /dev population and chardev-aware open
+ *
+ * Creates /dev as a subdirectory under the ramfs root, then
+ * populates device nodes. On open(), if the inode is S_IFCHR,
+ * we look up the cdev by i_rdev and use its fops.
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/chardev.h>
+#include <lume/mount.h>
+#include <lume/klog.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* ================================================================== */
+/* devfs_create_node — create a char device inode under parent */
+/* ================================================================== */
+
+static int devfs_create_node(dentry* parent, const char* name,
+ uint32 major, uint32 minor)
+{
+ inode* dir_ip = parent->d_inode;
+ if (!dir_ip || !dir_ip->i_op || !dir_ip->i_op->mknod) {
+ return -EPERM;
+ }
+
+ qstr q{};
+ q.name = name;
+ q.len = 0;
+ for (const char* s = name; *s; s++) {
+ q.len++;
+ }
+
+ dentry* de = d_alloc(parent, &q);
+ if (!de) {
+ return -ENOMEM;
+ }
+
+ int err = dir_ip->i_op->mknod(dir_ip, de, S_IFCHR | 0666,
+ MKDEV(major, minor));
+ if (err) {
+ return err;
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* devfs_open — chardev-aware open hook */
+/* ================================================================== */
+
+/*
+ * Called from sys_openat when the inode is S_IFCHR.
+ * Replaces f_op with the chardev's file_operations.
+ */
+int devfs_chrdev_open(inode* ip, file* fp)
+{
+ if (!S_ISCHR(ip->i_mode)) {
+ return 0;
+ }
+
+ cdev* cd = cdev_find(ip->i_rdev);
+ if (!cd || !cd->fops) {
+ return -ENXIO; /* No such device */
+ }
+
+ fp->f_op = cd->fops;
+
+ /* Call the device's own open if it has one */
+ if (fp->f_op->open) {
+ return fp->f_op->open(ip, fp);
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* devfs_init — create /dev and populate device nodes */
+/* ================================================================== */
+
+void devfs_init()
+{
+ if (!g_root_dentry || !g_root_dentry->d_inode) {
+ printk(KERN_ERR "[devfs] no root filesystem!\n");
+ return;
+ }
+
+ /* Create /dev directory — or reuse if initramfs already created it.
+ * Linux: devtmpfs similarly handles pre-existing directories. */
+ inode* root_ip = g_root_dentry->d_inode;
+
+ qstr dev_name{};
+ dev_name.name = "dev";
+ dev_name.len = 3;
+ dev_name.hash = full_name_hash("dev", 3);
+
+ dentry* dev_de = d_lookup(g_root_dentry, &dev_name);
+ if (dev_de) {
+ /* /dev already exists (from initramfs) — reuse it */
+ printk("[devfs] reusing existing /dev directory\n");
+ } else {
+ /* Create /dev from scratch */
+ if (!root_ip->i_op || !root_ip->i_op->mkdir) {
+ printk(KERN_ERR "[devfs] root has no mkdir!\n");
+ return;
+ }
+
+ dev_de = d_alloc(g_root_dentry, &dev_name);
+ if (!dev_de) {
+ printk(KERN_ERR "[devfs] failed to alloc /dev dentry\n");
+ return;
+ }
+
+ int err = root_ip->i_op->mkdir(root_ip, dev_de, S_IFDIR | 0755);
+ if (err) {
+ printk(KERN_ERR "[devfs] mkdir /dev failed: %d\n", err);
+ return;
+ }
+ }
+
+ /* Populate: /dev/console (5,1) */
+ devfs_create_node(dev_de, "console", 5, 1);
+
+ /* Populate: /dev/null (1,3) */
+ devfs_create_node(dev_de, "null", 1, 3);
+
+ /* Populate: /dev/zero (1,5) */
+ devfs_create_node(dev_de, "zero", 1, 5);
+
+ printk("[devfs] /dev populated (console, null, zero)\n");
+}
diff --git a/fs/fd.cc b/fs/fd.cc
new file mode 100644
index 0000000..5fc8ab0
--- /dev/null
+++ b/fs/fd.cc
@@ -0,0 +1,216 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * fd.cc — File descriptor table management
+ */
+
+#include <lume/fdtable.h>
+#include <lume/file.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+/* ================================================================== */
+/* Bitmap helpers */
+/* ================================================================== */
+
+static inline bool bit_test(const uint64* bmap, int bit) {
+ return (bmap[bit / 64] >> (bit % 64)) & 1;
+}
+static inline void bit_set(uint64* bmap, int bit) {
+ bmap[bit / 64] |= (1ULL << (bit % 64));
+}
+static inline void bit_clear(uint64* bmap, int bit) {
+ bmap[bit / 64] &= ~(1ULL << (bit % 64));
+}
+
+/* ================================================================== */
+/* alloc_files_struct */
+/* ================================================================== */
+
+files_struct* alloc_files_struct()
+{
+ auto* files = new files_struct{};
+ if (!files) return nullptr;
+
+ files->count.store(1);
+ files->next_fd = 0;
+ files->max_fds = NR_OPEN_DEFAULT;
+
+ for (int i = 0; i < NR_OPEN_DEFAULT; i++)
+ files->fd_array[i] = nullptr;
+
+ int bmap_words = (NR_OPEN_DEFAULT + 63) / 64;
+ for (int i = 0; i < bmap_words; i++) {
+ files->close_on_exec_init[i] = 0;
+ files->open_fds_init[i] = 0;
+ }
+
+ return files;
+}
+
+/* ================================================================== */
+/* put_files_struct — drop reference, free on last */
+/* ================================================================== */
+
+void put_files_struct(files_struct* files)
+{
+ if (!files) return;
+
+ int old = files->count.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Close all open fds */
+ for (int i = 0; i < files->max_fds; i++) {
+ if (files->fd_array[i]) {
+ fput(files->fd_array[i]);
+ files->fd_array[i] = nullptr;
+ }
+ }
+
+ delete files;
+}
+
+/* ================================================================== */
+/* dup_fd — duplicate entire fd table (for fork) */
+/* ================================================================== */
+
+files_struct* dup_fd(files_struct* old_files)
+{
+ auto* new_files = alloc_files_struct();
+ if (!new_files) return nullptr;
+
+ LockGuard guard(old_files->file_lock);
+
+ for (int i = 0; i < old_files->max_fds; i++) {
+ file* fp = old_files->fd_array[i];
+ if (fp) {
+ get_file(fp);
+ new_files->fd_array[i] = fp;
+ }
+ }
+
+ /* Copy bitmaps */
+ int bmap_words = (NR_OPEN_DEFAULT + 63) / 64;
+ for (int i = 0; i < bmap_words; i++) {
+ new_files->open_fds_init[i] = old_files->open_fds_init[i];
+ new_files->close_on_exec_init[i] = old_files->close_on_exec_init[i];
+ }
+
+ new_files->next_fd = old_files->next_fd;
+
+ return new_files;
+}
+
+/* ================================================================== */
+/* alloc_fd — find lowest free fd >= start */
+/* ================================================================== */
+
+int alloc_fd(files_struct* files, unsigned int start)
+{
+ LockGuard guard(files->file_lock);
+
+ for (int fd = static_cast<int>(start); fd < files->max_fds; fd++) {
+ if (!bit_test(files->open_fds_init, fd)) {
+ bit_set(files->open_fds_init, fd);
+ files->fd_array[fd] = nullptr; /* placeholder, fd_install sets real file */
+ if (fd >= files->next_fd)
+ files->next_fd = fd + 1;
+ return fd;
+ }
+ }
+
+ return -EMFILE;
+}
+
+/* ================================================================== */
+/* fd_install — place a file* at a given fd */
+/* ================================================================== */
+
+void fd_install(files_struct* files, int fd, file* fp)
+{
+ LockGuard guard(files->file_lock);
+ files->fd_array[fd] = fp;
+}
+
+/* ================================================================== */
+/* fcheck — look up file by fd (no refcount bump) */
+/* ================================================================== */
+
+file* fcheck(files_struct* files, int fd)
+{
+ if (fd < 0 || fd >= files->max_fds) return nullptr;
+ return files->fd_array[fd];
+}
+
+/* ================================================================== */
+/* close_fd */
+/* ================================================================== */
+
+int close_fd(files_struct* files, int fd)
+{
+ file* fp;
+ {
+ LockGuard guard(files->file_lock);
+ if (fd < 0 || fd >= files->max_fds) return -EBADF;
+
+ fp = files->fd_array[fd];
+ if (!fp) return -EBADF;
+
+ files->fd_array[fd] = nullptr;
+ bit_clear(files->open_fds_init, fd);
+ bit_clear(files->close_on_exec_init, fd);
+ }
+
+ fput(fp);
+ return 0;
+}
+
+/* ================================================================== */
+/* do_dup2 — duplicate oldfd to newfd */
+/* ================================================================== */
+
+int do_dup2(files_struct* files, int oldfd, int newfd, int flags)
+{
+ if (oldfd == newfd) return -EINVAL;
+
+ file* existing = nullptr;
+ {
+ LockGuard guard(files->file_lock);
+
+ if (oldfd < 0 || oldfd >= files->max_fds) return -EBADF;
+ if (newfd < 0 || newfd >= files->max_fds) return -EBADF;
+
+ file* old_fp = files->fd_array[oldfd];
+ if (!old_fp) return -EBADF;
+
+ existing = files->fd_array[newfd];
+
+ get_file(old_fp);
+ files->fd_array[newfd] = old_fp;
+ bit_set(files->open_fds_init, newfd);
+
+ if (flags & O_CLOEXEC)
+ bit_set(files->close_on_exec_init, newfd);
+ else
+ bit_clear(files->close_on_exec_init, newfd);
+ }
+
+ if (existing)
+ fput(existing);
+
+ return newfd;
+}
+
+/* ================================================================== */
+/* do_close_on_exec — close all fds with CLOEXEC set */
+/* ================================================================== */
+
+void do_close_on_exec(files_struct* files)
+{
+ for (int fd = 0; fd < files->max_fds; fd++) {
+ if (bit_test(files->close_on_exec_init, fd)) {
+ close_fd(files, fd);
+ }
+ }
+}
diff --git a/fs/file_table.cc b/fs/file_table.cc
new file mode 100644
index 0000000..06095c1
--- /dev/null
+++ b/fs/file_table.cc
@@ -0,0 +1,164 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * file_table.cc — Open file allocation and reference counting
+ */
+
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+/* ================================================================== */
+/* alloc_empty_file */
+/* ================================================================== */
+
+file* alloc_empty_file(int flags)
+{
+ auto* fp = new file{};
+ if (!fp) return nullptr;
+
+ fp->f_count.store(1);
+ fp->f_flags = flags;
+ fp->f_mode = 0;
+ fp->f_op = nullptr;
+ fp->f_mapping = nullptr;
+ fp->f_inode = nullptr;
+ fp->f_pos = 0;
+ fp->private_data = nullptr;
+ fp->f_path.mnt = nullptr;
+ fp->f_path.de = nullptr;
+
+ /* Derive f_mode from O_* flags */
+ int acc = flags & O_ACCMODE;
+ if (acc == O_RDONLY)
+ fp->f_mode = FMODE_READ | FMODE_CAN_READ;
+ else if (acc == O_WRONLY)
+ fp->f_mode = FMODE_WRITE | FMODE_CAN_WRITE;
+ else if (acc == O_RDWR)
+ fp->f_mode = FMODE_READ | FMODE_WRITE | FMODE_CAN_READ | FMODE_CAN_WRITE;
+
+ fp->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
+
+ return fp;
+}
+
+/* ================================================================== */
+/* get_file / fput */
+/* ================================================================== */
+
+file* get_file(file* f)
+{
+ f->f_count.fetch_add(1);
+ return f;
+}
+
+void fput(file* fp)
+{
+ if (!fp) return;
+
+ int old = fp->f_count.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Last reference — release */
+ if (fp->f_op && fp->f_op->release)
+ fp->f_op->release(fp->f_inode, fp);
+
+ if (fp->f_path.de)
+ dput(fp->f_path.de);
+
+ /* Future: mntput(fp->f_path.mnt) */
+
+ delete fp;
+}
+
+/* ================================================================== */
+/* vfs_read / vfs_write / vfs_llseek / vfs_fstat */
+/* ================================================================== */
+
+int64 vfs_read(file* fp, char* buf, uint64 count, int64* pos)
+{
+ if (!(fp->f_mode & FMODE_CAN_READ))
+ return -EBADF;
+ if (!fp->f_op || !fp->f_op->read)
+ return -EINVAL;
+
+ return fp->f_op->read(fp, buf, count, pos);
+}
+
+int64 vfs_write(file* fp, const char* buf, uint64 count, int64* pos)
+{
+ if (!(fp->f_mode & FMODE_CAN_WRITE))
+ return -EBADF;
+ if (!fp->f_op || !fp->f_op->write)
+ return -EINVAL;
+
+ return fp->f_op->write(fp, buf, count, pos);
+}
+
+int64 vfs_llseek(file* fp, int64 offset, int whence)
+{
+ if (fp->f_op && fp->f_op->llseek)
+ return fp->f_op->llseek(fp, offset, whence);
+ return generic_file_llseek(fp, offset, whence);
+}
+
+int64 generic_file_llseek(file* fp, int64 offset, int whence)
+{
+ int64 new_pos;
+ switch (whence) {
+ case SEEK_SET:
+ new_pos = offset;
+ break;
+ case SEEK_CUR:
+ new_pos = fp->f_pos + offset;
+ break;
+ case SEEK_END:
+ new_pos = fp->f_inode->i_size + offset;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (new_pos < 0) return -EINVAL;
+ fp->f_pos = new_pos;
+ return new_pos;
+}
+
+int vfs_fstat(file* fp, kstat* stat)
+{
+ inode* ip = file_inode(fp);
+ if (!ip) return -EBADF;
+
+ if (ip->i_op && ip->i_op->getattr) {
+ return ip->i_op->getattr(&fp->f_path, stat, 0, 0);
+ }
+
+ generic_fillattr(ip, stat);
+ return 0;
+}
+
+/* ================================================================== */
+/* path_get / path_put (simplified) */
+/* ================================================================== */
+
+void path_get(const path* p)
+{
+ if (p->de) dget(p->de);
+ /* Future: mntget(p->mnt) */
+}
+
+void path_put(const path* p)
+{
+ if (p->de) dput(p->de);
+ /* Future: mntput(p->mnt) */
+}
+
+/* ================================================================== */
+/* mntget / mntput (stubs for Phase 1) */
+/* ================================================================== */
+
+vfsmount* mntget(vfsmount* mnt) { return mnt; }
+void mntput(vfsmount*) {}
diff --git a/fs/fs_struct.cc b/fs/fs_struct.cc
new file mode 100644
index 0000000..090819f
--- /dev/null
+++ b/fs/fs_struct.cc
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * fs_struct.cc — Per-task filesystem context (cwd, root)
+ */
+
+#include <lume/fs_struct.h>
+#include <lume/slab.h>
+
+/* ================================================================== */
+/* alloc_fs_struct */
+/* ================================================================== */
+
+fs_struct* alloc_fs_struct()
+{
+ auto* fs = new fs_struct{};
+ if (!fs) return nullptr;
+
+ fs->count.store(1);
+ fs->umask = 022;
+ fs->root = {nullptr, nullptr};
+ fs->pwd = {nullptr, nullptr};
+
+ return fs;
+}
+
+/* ================================================================== */
+/* put_fs_struct */
+/* ================================================================== */
+
+void put_fs_struct(fs_struct* fs)
+{
+ if (!fs) return;
+
+ int old = fs->count.fetch_sub(1);
+ if (old > 1) return;
+
+ path_put(&fs->root);
+ path_put(&fs->pwd);
+ delete fs;
+}
+
+/* ================================================================== */
+/* copy_fs_struct — for fork without CLONE_FS */
+/* ================================================================== */
+
+fs_struct* copy_fs_struct(const fs_struct* old_fs)
+{
+ auto* fs = alloc_fs_struct();
+ if (!fs) return nullptr;
+
+ fs->umask = old_fs->umask;
+
+ fs->root = old_fs->root;
+ path_get(&fs->root);
+
+ fs->pwd = old_fs->pwd;
+ path_get(&fs->pwd);
+
+ return fs;
+}
+
+/* ================================================================== */
+/* set_fs_root / set_fs_pwd */
+/* ================================================================== */
+
+void set_fs_root(fs_struct* fs, const path* p)
+{
+ path old_root;
+ {
+ LockGuard guard(fs->lock);
+ old_root = fs->root;
+ fs->root = *p;
+ path_get(p);
+ }
+ path_put(&old_root);
+}
+
+void set_fs_pwd(fs_struct* fs, const path* p)
+{
+ path old_pwd;
+ {
+ LockGuard guard(fs->lock);
+ old_pwd = fs->pwd;
+ fs->pwd = *p;
+ path_get(p);
+ }
+ path_put(&old_pwd);
+}
diff --git a/fs/initramfs.cc b/fs/initramfs.cc
new file mode 100644
index 0000000..ba10a3e
--- /dev/null
+++ b/fs/initramfs.cc
@@ -0,0 +1,285 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * initramfs.cc — Unpack embedded CPIO archive into ramfs
+ *
+ * The build system embeds a newc-format CPIO archive into the kernel
+ * ELF as the section .init.ramfs. The linker script exports
+ * __initramfs_start / __initramfs_end symbols.
+ *
+ * At boot, populate_rootfs() walks the CPIO entries and creates
+ * directories and files in the already-mounted ramfs root.
+ *
+ * Reference: Linux init/initramfs.c
+ *
+ * CPIO "newc" header format (110 bytes, ASCII hex fields):
+ * magic[6] "070701"
+ * ino[8] mode[8] uid[8] gid[8] nlink[8] mtime[8]
+ * filesize[8] devmajor[8] devminor[8] rdevmajor[8] rdevminor[8]
+ * namesize[8] check[8]
+ * filename (namesize bytes, NUL terminated, padded to 4-byte boundary)
+ * file data (filesize bytes, padded to 4-byte boundary)
+ */
+
+#include <lume/klog.h>
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* Linker-provided symbols for the embedded CPIO archive */
+extern "C" const char __initramfs_start[];
+extern "C" const char __initramfs_end[];
+
+/* ramfs inode creation (from fs/ramfs/inode.cc) */
+extern inode* ramfs_get_inode(super_block* sb, const inode* dir,
+ uint32 mode, uint32 rdev);
+
+/* Global root dentry (from fs/vfs_init.cc) */
+extern dentry* g_root_dentry;
+
+/* ================================================================== */
+/* CPIO newc header parsing */
+/* ================================================================== */
+
+struct cpio_header {
+ char magic[6];
+ char ino[8];
+ char mode[8];
+ char uid[8];
+ char gid[8];
+ char nlink[8];
+ char mtime[8];
+ char filesize[8];
+ char devmajor[8];
+ char devminor[8];
+ char rdevmajor[8];
+ char rdevminor[8];
+ char namesize[8];
+ char check[8];
+};
+
+static constexpr int kCpioHeaderSize = 110;
+
+static uint32 parse_hex8(const char* s)
+{
+ uint32 val = 0;
+ for (int i = 0; i < 8; i++) {
+ char c = s[i];
+ uint32 nibble;
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else return val;
+ val = (val << 4) | nibble;
+ }
+ return val;
+}
+
+static inline uint32 align4(uint32 v)
+{
+ return (v + 3) & ~3U;
+}
+
+/* ================================================================== */
+/* Path resolution helpers */
+/* ================================================================== */
+
+static dentry* cpio_mkdir_p(dentry* root, const char* path,
+ const char** out_name)
+{
+ dentry* cur = root;
+
+ while (*path == '/') path++;
+
+ while (true) {
+ const char* slash = path;
+ while (*slash && *slash != '/') slash++;
+
+ if (*slash == '\0') {
+ *out_name = path;
+ return cur;
+ }
+
+ uint32 len = static_cast<uint32>(slash - path);
+
+ qstr name;
+ name.name = path;
+ name.len = len;
+ name.hash = full_name_hash(path, len);
+
+ dentry* child = d_lookup(cur, &name);
+ if (!child) {
+ child = d_alloc(cur, &name);
+ if (!child) return nullptr;
+
+ inode* ip = ramfs_get_inode(cur->d_inode->i_sb, cur->d_inode,
+ S_IFDIR | 0755, 0);
+ if (!ip) return nullptr;
+
+ d_instantiate(child, ip);
+ dget(child);
+ inc_nlink(cur->d_inode);
+ }
+
+ cur = child;
+ path = slash + 1;
+ while (*path == '/') path++;
+ }
+}
+
+/* ================================================================== */
+/* populate_rootfs — the main entry point */
+/* ================================================================== */
+
+void populate_rootfs()
+{
+ const char* p = __initramfs_start;
+ const char* end = __initramfs_end;
+ uint64 archive_size = static_cast<uint64>(end - p);
+
+ if (archive_size == 0) {
+ printk("[initramfs] no embedded archive found\n");
+ return;
+ }
+
+ printk("[initramfs] unpacking %llu bytes...\n", archive_size);
+
+ int file_count = 0;
+ int dir_count = 0;
+
+ while (p + kCpioHeaderSize <= end) {
+ auto* hdr = reinterpret_cast<const cpio_header*>(p);
+
+ /* Verify magic */
+ if (hdr->magic[0] != '0' || hdr->magic[1] != '7' ||
+ hdr->magic[2] != '0' || hdr->magic[3] != '7' ||
+ hdr->magic[4] != '0' || hdr->magic[5] != '1') {
+ printk("[initramfs] bad cpio magic at offset %llu\n",
+ static_cast<uint64>(p - __initramfs_start));
+ break;
+ }
+
+ uint32 mode = parse_hex8(hdr->mode);
+ uint32 filesize = parse_hex8(hdr->filesize);
+ uint32 namesize = parse_hex8(hdr->namesize);
+
+ const char* name = p + kCpioHeaderSize;
+ uint32 name_padded = align4(kCpioHeaderSize + namesize);
+ const char* data = p + name_padded;
+ uint32 data_padded = align4(filesize);
+
+ const char* next = p + name_padded + data_padded;
+
+ /* Check for TRAILER!!! end-of-archive marker */
+ if (namesize == 11 && name[0] == 'T' && name[1] == 'R' &&
+ name[2] == 'A' && name[3] == 'I' && name[4] == 'L' &&
+ name[5] == 'E' && name[6] == 'R' && name[7] == '!' &&
+ name[8] == '!' && name[9] == '!') {
+ break;
+ }
+
+ /* Skip "." entry */
+ if (namesize == 2 && name[0] == '.' && name[1] == '\0') {
+ p = next;
+ continue;
+ }
+
+ /* Resolve parent directory, creating intermediate dirs */
+ const char* basename = nullptr;
+ dentry* parent = cpio_mkdir_p(g_root_dentry, name, &basename);
+ if (!parent || !basename || basename[0] == '\0') {
+ p = next;
+ continue;
+ }
+
+ /* Build qstr for the final component */
+ uint32 blen = 0;
+ while (basename[blen] && basename[blen] != '/') blen++;
+
+ qstr bname;
+ bname.name = basename;
+ bname.len = blen;
+ bname.hash = full_name_hash(basename, blen);
+
+ if (S_ISDIR(mode)) {
+ dentry* existing = d_lookup(parent, &bname);
+ if (!existing) {
+ dentry* de = d_alloc(parent, &bname);
+ if (de) {
+ inode* ip = ramfs_get_inode(parent->d_inode->i_sb,
+ parent->d_inode,
+ S_IFDIR | (mode & 0777), 0);
+ if (ip) {
+ d_instantiate(de, ip);
+ dget(de);
+ inc_nlink(parent->d_inode);
+ dir_count++;
+ }
+ }
+ }
+ } else if (S_ISREG(mode)) {
+ dentry* de = d_alloc(parent, &bname);
+ if (de) {
+ inode* ip = ramfs_get_inode(parent->d_inode->i_sb,
+ parent->d_inode,
+ S_IFREG | (mode & 0777), 0);
+ if (ip) {
+ d_instantiate(de, ip);
+ dget(de);
+
+ /* Write file content via ramfs write path */
+ if (filesize > 0 && ip->i_fop && ip->i_fop->write) {
+ file tmp_fp = {};
+ tmp_fp.f_inode = ip;
+ tmp_fp.f_path.de = de;
+ tmp_fp.f_pos = 0;
+ tmp_fp.f_flags = O_WRONLY;
+ tmp_fp.f_count.store(1);
+
+ int64 pos = 0;
+ ip->i_fop->write(&tmp_fp, data,
+ filesize, &pos);
+ }
+
+ file_count++;
+ printk("[initramfs] %s (%u bytes, mode %03o)\n",
+ name, filesize, mode & 0777);
+ }
+ }
+ } else if (S_ISLNK(mode)) {
+ /* Symlink: file data is the target path (not NUL-terminated).
+ * Linux: init/initramfs.c do_symlink() */
+ dentry* de = d_alloc(parent, &bname);
+ if (de) {
+ inode* ip = ramfs_get_inode(parent->d_inode->i_sb,
+ parent->d_inode,
+ S_IFLNK | 0777, 0);
+ if (ip) {
+ /* Store symlink target in inode.
+ * Allocate a kernel buffer and copy the target. */
+ char* target = new char[filesize + 1];
+ if (target) {
+ for (uint32 i = 0; i < filesize; i++)
+ target[i] = data[i];
+ target[filesize] = '\0';
+ ip->i_link = target;
+ }
+
+ d_instantiate(de, ip);
+ dget(de);
+ file_count++;
+ printk("[initramfs] %s -> %s (symlink)\n",
+ name, target ? target : "?");
+ }
+ }
+ }
+
+ p = next;
+ }
+
+ printk("[initramfs] done: %d files, %d dirs\n",
+ file_count, dir_count);
+}
diff --git a/fs/inode.cc b/fs/inode.cc
new file mode 100644
index 0000000..1d3d006
--- /dev/null
+++ b/fs/inode.cc
@@ -0,0 +1,166 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * inode.cc — Inode allocation, lifecycle, and generic helpers
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+namespace {
+lume::atomic<uint64> g_next_ino{1};
+} // namespace
+
+/* ================================================================== */
+/* new_inode — allocate a fresh inode on a superblock */
+/* ================================================================== */
+
+inode* new_inode(super_block* sb)
+{
+ inode* ip = nullptr;
+
+ if (sb->s_op && sb->s_op->alloc_inode)
+ ip = sb->s_op->alloc_inode(sb);
+ else
+ ip = new inode{};
+
+ if (!ip) return nullptr;
+
+ /* Zero-init key fields */
+ ip->i_sb = sb;
+ ip->i_ino = g_next_ino.fetch_add(1);
+ ip->i_mode = 0;
+ ip->i_flags = 0;
+ ip->i_uid = 0;
+ ip->i_gid = 0;
+ ip->i_nlink = 1;
+ ip->i_rdev = 0;
+ ip->i_size = 0;
+ ip->i_blocks = 0;
+ ip->i_bytes = 0;
+ ip->i_blkbits = sb->s_blocksize_bits;
+ ip->i_generation = 0;
+ ip->i_state = I_NEW;
+ ip->i_count.store(1);
+ ip->i_writecount.store(0);
+
+ ip->i_atime_sec = ip->i_mtime_sec = ip->i_ctime_sec = 0;
+ ip->i_atime_nsec = ip->i_mtime_nsec = ip->i_ctime_nsec = 0;
+
+ ip->i_op = nullptr;
+ ip->i_fop = nullptr;
+ ip->i_mapping = &ip->i_data;
+ ip->i_private = nullptr;
+ ip->i_pipe = nullptr;
+
+ /* Init embedded address_space */
+ ip->i_data.host = ip;
+ ip->i_data.a_ops = nullptr;
+ ip->i_data.nrpages = 0;
+ ip->i_data.flags = 0;
+
+ /* Init lists */
+ list_init(&ip->i_sb_list);
+ list_init(&ip->i_dentry);
+
+ /* Add to superblock's inode list */
+ {
+ LockGuard guard(sb->s_inode_list_lock);
+ list_add_tail(&ip->i_sb_list, &sb->s_inodes);
+ }
+
+ return ip;
+}
+
+/* ================================================================== */
+/* ihold / iput — reference counting */
+/* ================================================================== */
+
+void ihold(inode* ip)
+{
+ ip->i_count.fetch_add(1);
+}
+
+void iput(inode* ip)
+{
+ if (!ip) return;
+
+ int old = ip->i_count.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Last reference dropped — evict */
+ if (ip->i_sb->s_op && ip->i_sb->s_op->evict_inode)
+ ip->i_sb->s_op->evict_inode(ip);
+
+ /* Remove from sb list */
+ {
+ LockGuard guard(ip->i_sb->s_inode_list_lock);
+ list_del(&ip->i_sb_list);
+ }
+
+ if (ip->i_sb->s_op && ip->i_sb->s_op->destroy_inode)
+ ip->i_sb->s_op->destroy_inode(ip);
+ else
+ delete ip;
+}
+
+/* ================================================================== */
+/* nlink helpers */
+/* ================================================================== */
+
+void set_nlink(inode* ip, uint32 nlink) { ip->i_nlink = nlink; }
+void inc_nlink(inode* ip) { ip->i_nlink++; }
+void drop_nlink(inode* ip) { ip->i_nlink--; }
+void clear_nlink(inode* ip) { ip->i_nlink = 0; }
+
+/* ================================================================== */
+/* inode_init_owner */
+/* ================================================================== */
+
+void inode_init_owner(inode* ip, const inode* dir, uint32 mode)
+{
+ ip->i_uid = 0; /* Phase 1: always root */
+ ip->i_gid = 0;
+ ip->i_mode = mode;
+}
+
+/* ================================================================== */
+/* generic_fillattr — fill kstat from inode (used by getattr) */
+/* ================================================================== */
+
+void generic_fillattr(inode* ip, kstat* stat)
+{
+ stat->st_dev = ip->i_sb->s_dev;
+ stat->st_ino = ip->i_ino;
+ stat->st_mode = ip->i_mode;
+ stat->st_nlink = ip->i_nlink;
+ stat->st_uid = ip->i_uid;
+ stat->st_gid = ip->i_gid;
+ stat->st_rdev = ip->i_rdev;
+ stat->__pad = 0;
+ stat->st_size = ip->i_size;
+ stat->st_blksize = 1 << ip->i_blkbits;
+ stat->__pad2 = 0;
+ stat->st_blocks = ip->i_blocks;
+ stat->st_atime_sec = ip->i_atime_sec;
+ stat->st_atime_nsec = ip->i_atime_nsec;
+ stat->st_mtime_sec = ip->i_mtime_sec;
+ stat->st_mtime_nsec = ip->i_mtime_nsec;
+ stat->st_ctime_sec = ip->i_ctime_sec;
+ stat->st_ctime_nsec = ip->i_ctime_nsec;
+ stat->__unused[0] = 0;
+ stat->__unused[1] = 0;
+}
+
+/* ================================================================== */
+/* mark_inode_dirty (stub) */
+/* ================================================================== */
+
+void mark_inode_dirty(inode* ip)
+{
+ ip->i_state |= I_DIRTY_SYNC;
+ /* Phase 1: no writeback, ramfs is memory-only */
+}
diff --git a/fs/module.mk b/fs/module.mk
new file mode 100644
index 0000000..f1be157
--- /dev/null
+++ b/fs/module.mk
@@ -0,0 +1,17 @@
+# fs/module.mk — VFS subsystem build rules
+
+SRCS_CC += fs/super.cc
+SRCS_CC += fs/inode.cc
+SRCS_CC += fs/dcache.cc
+SRCS_CC += fs/file_table.cc
+SRCS_CC += fs/fd.cc
+SRCS_CC += fs/fs_struct.cc
+SRCS_CC += fs/namei.cc
+SRCS_CC += fs/ramfs/inode.cc
+SRCS_CC += fs/ramfs/file.cc
+SRCS_CC += fs/ramfs/super.cc
+SRCS_CC += fs/vfs_init.cc
+SRCS_CC += fs/chardev.cc
+SRCS_CC += fs/devfs.cc
+SRCS_CC += fs/pipe.cc
+SRCS_CC += fs/initramfs.cc
diff --git a/fs/namei.cc b/fs/namei.cc
new file mode 100644
index 0000000..9124ea9
--- /dev/null
+++ b/fs/namei.cc
@@ -0,0 +1,250 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * namei.cc — Pathname resolution (path lookup)
+ *
+ * Simplified Phase 1: no symlink, no RCU, no mount traversal.
+ */
+
+#include <lume/namei.h>
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/mount.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static const char* next_component(const char* path, qstr* result)
+{
+ while (*path == '/') path++;
+ if (*path == '\0') return nullptr;
+
+ const char* start = path;
+ while (*path != '\0' && *path != '/') path++;
+
+ uint32 len = static_cast<uint32>(path - start);
+ result->name = start;
+ result->len = len;
+ result->hash = full_name_hash(start, len);
+
+ return path;
+}
+
+static bool is_dot(const qstr* name) {
+ return name->len == 1 && name->name[0] == '.';
+}
+static bool is_dotdot(const qstr* name) {
+ return name->len == 2 && name->name[0] == '.' && name->name[1] == '.';
+}
+
+/* ================================================================== */
+/* walk_component */
+/* ================================================================== */
+
+static dentry* walk_component(dentry* parent, const qstr* name)
+{
+ if (!parent->d_inode || !S_ISDIR(parent->d_inode->i_mode))
+ return reinterpret_cast<dentry*>(-ENOTDIR);
+
+ if (is_dot(name))
+ return dget(parent);
+
+ if (is_dotdot(name))
+ return dget(parent->d_parent);
+
+ /* Try dcache */
+ dentry* child = d_lookup(parent, name);
+ if (child)
+ return child;
+
+ /* Cache miss — call filesystem lookup */
+ inode* dir = parent->d_inode;
+ if (!dir->i_op || !dir->i_op->lookup)
+ return reinterpret_cast<dentry*>(-ENOENT);
+
+ child = d_alloc(parent, name);
+ if (!child)
+ return reinterpret_cast<dentry*>(-ENOMEM);
+
+ dentry* result = dir->i_op->lookup(dir, child, 0);
+ if (result) {
+ dput(child);
+ return result;
+ }
+
+ /* If lookup didn't set an inode, this is a negative dentry
+ * (file doesn't exist). Remove from dcache and return error. */
+ if (!child->d_inode) {
+ d_drop(child);
+ dput(child);
+ return reinterpret_cast<dentry*>(-ENOENT);
+ }
+
+ return child;
+}
+
+/* ================================================================== */
+/* filename_lookup */
+/* ================================================================== */
+
+int filename_lookup(int dfd, const char* name, unsigned int flags, path* result)
+{
+ if (!name || !result)
+ return -EINVAL;
+
+ TaskControlBlock* task = current_task();
+ dentry* start;
+
+ /* Determine starting dentry */
+ if (name[0] == '/') {
+ start = task->fs->root.de;
+ if (!start) start = g_root_dentry;
+ name++;
+ } else if (dfd == AT_FDCWD) {
+ start = task->fs->pwd.de;
+ if (!start) start = g_root_dentry;
+ } else {
+ file* dir_fp = fcheck(task->files, dfd);
+ if (!dir_fp) return -EBADF;
+ start = dir_fp->f_path.de;
+ if (!start || !start->d_inode || !S_ISDIR(start->d_inode->i_mode))
+ return -ENOTDIR;
+ }
+
+ dentry* current_de = dget(start);
+ const char* p = name;
+
+ while (p && *p) {
+ qstr component;
+ const char* next = next_component(p, &component);
+ if (!next && component.len == 0)
+ break;
+
+ const char* after = next;
+ while (after && *after == '/') after++;
+ bool is_last = (!after || *after == '\0');
+
+ if (is_last && (flags & LOOKUP_PARENT)) {
+ result->de = current_de;
+ result->mnt = task->fs->root.mnt;
+ return 0;
+ }
+
+ dentry* child = walk_component(current_de, &component);
+
+ int64 err = reinterpret_cast<int64>(child);
+ if (err < 0 && err >= -4095) {
+ dput(current_de);
+ return static_cast<int>(err);
+ }
+
+ /* ---- Symlink following (Linux: step_into → pick_link) ----
+ * If the resolved component is a symlink, follow it.
+ * Linux limits this to MAXSYMLINKS (40); we use 8. */
+ constexpr int kMaxSymlinks = 8;
+ int symlink_depth = 0;
+ while (child->d_inode && S_ISLNK(child->d_inode->i_mode) &&
+ child->d_inode->i_link && symlink_depth < kMaxSymlinks) {
+ symlink_depth++;
+ const char* target = child->d_inode->i_link;
+ dput(child);
+
+ /* Absolute symlink: restart from root.
+ * Relative symlink: resolve from current parent. */
+ dentry* link_start;
+ if (target[0] == '/') {
+ link_start = dget(task->fs->root.de ? task->fs->root.de : g_root_dentry);
+ target++;
+ } else {
+ link_start = dget(current_de);
+ }
+
+ /* Walk each component of the symlink target */
+ dentry* link_cur = link_start;
+ const char* lp = target;
+ qstr lcomp;
+ while (lp && *lp) {
+ const char* lnext = next_component(lp, &lcomp);
+ if (!lnext && lcomp.len == 0) break;
+
+ dentry* lchild = walk_component(link_cur, &lcomp);
+ int64 lerr = reinterpret_cast<int64>(lchild);
+ if (lerr < 0 && lerr >= -4095) {
+ dput(link_cur);
+ dput(current_de);
+ return static_cast<int>(lerr);
+ }
+
+ dput(link_cur);
+ link_cur = lchild;
+ lp = lnext;
+ }
+
+ child = link_cur;
+ }
+
+ if (symlink_depth >= kMaxSymlinks) {
+ dput(child);
+ dput(current_de);
+ return -ELOOP;
+ }
+
+ if (!is_last && child->d_inode && !S_ISDIR(child->d_inode->i_mode)) {
+ dput(child);
+ dput(current_de);
+ return -ENOTDIR;
+ }
+
+ if (!is_last && !child->d_inode) {
+ dput(child);
+ dput(current_de);
+ return -ENOENT;
+ }
+
+ dput(current_de);
+ current_de = child;
+ p = next;
+ }
+
+ if ((flags & LOOKUP_DIRECTORY) && current_de->d_inode &&
+ !S_ISDIR(current_de->d_inode->i_mode)) {
+ dput(current_de);
+ return -ENOTDIR;
+ }
+
+ /* Negative dentry: the path was walked, but the final component
+ * does not exist (d_inode == NULL). Return -ENOENT. */
+ if (!current_de->d_inode) {
+ dput(current_de);
+ return -ENOENT;
+ }
+
+ result->de = current_de;
+ result->mnt = task->fs->root.mnt;
+ return 0;
+}
+
+int kern_path(const char* name, unsigned int flags, path* result)
+{
+ return filename_lookup(AT_FDCWD, name, flags, result);
+}
+
+dentry* lookup_one_len(const char* name, dentry* parent, int len)
+{
+ qstr q;
+ q.name = name;
+ q.len = static_cast<uint32>(len);
+ q.hash = full_name_hash(name, q.len);
+
+ return walk_component(parent, &q);
+}
+
+void namei_init() {}
diff --git a/fs/pipe.cc b/fs/pipe.cc
new file mode 100644
index 0000000..f7708f5
--- /dev/null
+++ b/fs/pipe.cc
@@ -0,0 +1,278 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * pipe.cc — Pipe implementation
+ *
+ * Implements unidirectional byte-stream pipes with blocking I/O.
+ * Each pipe has a 4KB circular buffer. Reads block when empty,
+ * writes block when full (unless O_NONBLOCK).
+ *
+ * When all writers close, read returns 0 (EOF).
+ * When all readers close, write returns -EPIPE.
+ */
+
+#include <lume/pipe.h>
+#include <lume/file.h>
+#include <lume/fdtable.h>
+#include <lume/errno.h>
+#include <lume/slab.h>
+#include <lume/fs_types.h>
+#include <lume/klog.h>
+#include <lume/sched.h>
+
+/* ================================================================== */
+/* pipe_inode_info lifecycle */
+/* ================================================================== */
+
+pipe_inode_info* alloc_pipe_info()
+{
+ auto* pi = new pipe_inode_info{};
+ if (!pi) return nullptr;
+
+ pi->rd_pos = 0;
+ pi->wr_pos = 0;
+ pi->len = 0;
+ pi->rd_wait.init("pipe_rd");
+ pi->wr_wait.init("pipe_wr");
+ pi->readers.store(0);
+ pi->writers.store(0);
+ pi->refcount.store(1);
+ return pi;
+}
+
+void free_pipe_info(pipe_inode_info* pipe)
+{
+ delete pipe;
+}
+
+static void pipe_get(pipe_inode_info* pi)
+{
+ pi->refcount.fetch_add(1);
+}
+
+static void pipe_put(pipe_inode_info* pi)
+{
+ if (pi->refcount.fetch_sub(1) == 1) {
+ free_pipe_info(pi);
+ }
+}
+
+/* ================================================================== */
+/* pipe_read */
+/* ================================================================== */
+
+static int64 pipe_read(file* fp, char* buf, uint64 count, int64* /*pos*/)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (!pi) return -EBADF;
+
+ while (true) {
+ pi->lock.acquire();
+
+ if (pi->len > 0) {
+ /* Data available — copy out */
+ int64 to_read = static_cast<int64>(count);
+ if (to_read > pi->len) to_read = pi->len;
+
+ for (int64 i = 0; i < to_read; i++) {
+ buf[i] = pi->buf[pi->rd_pos];
+ pi->rd_pos = (pi->rd_pos + 1) % PIPE_BUF_SIZE;
+ }
+ pi->len -= static_cast<int>(to_read);
+
+ pi->lock.release();
+
+ /* Wake any blocked writers */
+ pi->wr_wait.wake_all();
+ return to_read;
+ }
+
+ /* Buffer empty */
+ if (pi->writers.load() == 0) {
+ /* No writers left — EOF */
+ pi->lock.release();
+ return 0;
+ }
+
+ if (fp->f_flags & O_NONBLOCK) {
+ pi->lock.release();
+ return -EAGAIN;
+ }
+
+ pi->lock.release();
+
+ /* Sleep until data or writer closes */
+ pi->rd_wait.sleep();
+ }
+}
+
+/* ================================================================== */
+/* pipe_write */
+/* ================================================================== */
+
+static int64 pipe_write(file* fp, const char* buf, uint64 count, int64* /*pos*/)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (!pi) return -EBADF;
+
+ if (pi->readers.load() == 0) {
+ return -EPIPE; /* Broken pipe */
+ }
+
+ int64 written = 0;
+
+ while (static_cast<uint64>(written) < count) {
+ pi->lock.acquire();
+
+ if (pi->len < PIPE_BUF_SIZE) {
+ /* Space available — copy in */
+ int avail = PIPE_BUF_SIZE - pi->len;
+ int64 remaining = static_cast<int64>(count) - written;
+ int to_write = avail < static_cast<int>(remaining)
+ ? avail
+ : static_cast<int>(remaining);
+
+ for (int i = 0; i < to_write; i++) {
+ pi->buf[pi->wr_pos] = buf[written + i];
+ pi->wr_pos = (pi->wr_pos + 1) % PIPE_BUF_SIZE;
+ }
+ pi->len += to_write;
+ written += to_write;
+
+ pi->lock.release();
+
+ /* Wake any blocked readers */
+ pi->rd_wait.wake_all();
+ continue;
+ }
+
+ /* Buffer full */
+ if (pi->readers.load() == 0) {
+ pi->lock.release();
+ return written > 0 ? written : -EPIPE;
+ }
+
+ if (fp->f_flags & O_NONBLOCK) {
+ pi->lock.release();
+ return written > 0 ? written : -EAGAIN;
+ }
+
+ pi->lock.release();
+
+ /* Sleep until space or reader closes */
+ pi->wr_wait.sleep();
+ }
+
+ return written;
+}
+
+/* ================================================================== */
+/* pipe_release — called on last fput of a pipe fd */
+/* ================================================================== */
+
+static int pipe_read_release(inode* /*ip*/, file* fp)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (pi) {
+ pi->readers.fetch_sub(1);
+ /* Wake writers so they see readers==0 and get EPIPE */
+ pi->wr_wait.wake_all();
+ pipe_put(pi);
+ }
+ return 0;
+}
+
+static int pipe_write_release(inode* /*ip*/, file* fp)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (pi) {
+ pi->writers.fetch_sub(1);
+ /* Wake readers so they see writers==0 and get EOF */
+ pi->rd_wait.wake_all();
+ pipe_put(pi);
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* file_operations tables */
+/* ================================================================== */
+
+static file_operations pipe_read_fops;
+static file_operations pipe_write_fops;
+static bool pipe_fops_initialized = false;
+
+static void pipe_fops_init()
+{
+ if (pipe_fops_initialized) return;
+
+ pipe_read_fops = {};
+ pipe_read_fops.read = pipe_read;
+ pipe_read_fops.release = pipe_read_release;
+
+ pipe_write_fops = {};
+ pipe_write_fops.write = pipe_write;
+ pipe_write_fops.release = pipe_write_release;
+
+ pipe_fops_initialized = true;
+}
+
+/* ================================================================== */
+/* do_pipe2 — create a pipe, install two fds */
+/* ================================================================== */
+
+int do_pipe2(files_struct* files, int fds[2], int flags)
+{
+ pipe_fops_init();
+
+ pipe_inode_info* pi = alloc_pipe_info();
+ if (!pi) return -ENOMEM;
+
+ /* Create read-end file */
+ file* rd_fp = alloc_empty_file(O_RDONLY | (flags & O_NONBLOCK));
+ if (!rd_fp) {
+ free_pipe_info(pi);
+ return -ENFILE;
+ }
+ rd_fp->f_op = &pipe_read_fops;
+ rd_fp->private_data = pi;
+
+ /* Create write-end file */
+ file* wr_fp = alloc_empty_file(O_WRONLY | (flags & O_NONBLOCK));
+ if (!wr_fp) {
+ fput(rd_fp);
+ free_pipe_info(pi);
+ return -ENFILE;
+ }
+ wr_fp->f_op = &pipe_write_fops;
+ wr_fp->private_data = pi;
+
+ /* Bump pipe refcount for the two files */
+ pipe_get(pi); /* wr_fp gets second ref */
+ pi->readers.store(1);
+ pi->writers.store(1);
+
+ /* Allocate fds */
+ int rd_fd = alloc_fd(files, 0);
+ if (rd_fd < 0) {
+ fput(rd_fp);
+ fput(wr_fp);
+ return rd_fd;
+ }
+
+ int wr_fd = alloc_fd(files, 0);
+ if (wr_fd < 0) {
+ close_fd(files, rd_fd);
+ fput(wr_fp);
+ return wr_fd;
+ }
+
+ fd_install(files, rd_fd, rd_fp);
+ fd_install(files, wr_fd, wr_fp);
+
+ fds[0] = rd_fd;
+ fds[1] = wr_fd;
+
+ return 0;
+}
diff --git a/fs/ramfs/file.cc b/fs/ramfs/file.cc
new file mode 100644
index 0000000..6d9a070
--- /dev/null
+++ b/fs/ramfs/file.cc
@@ -0,0 +1,216 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * ramfs/file.cc — RamFS file operations (read/write via in-memory pages)
+ *
+ * Phase 1: simple flat buffer per-inode via i_private.
+ * Will migrate to page_cache integration later.
+ */
+
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/dcache.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/*
+ * Simplest possible in-memory file storage:
+ * i_private points to a dynamically grown buffer.
+ */
+struct ramfs_file_data {
+ char* buf;
+ uint64 capacity;
+};
+
+static ramfs_file_data* get_or_create_data(inode* ip)
+{
+ if (!ip->i_private) {
+ auto* data = new ramfs_file_data{};
+ data->buf = nullptr;
+ data->capacity = 0;
+ ip->i_private = data;
+ }
+ return static_cast<ramfs_file_data*>(ip->i_private);
+}
+
+static int ensure_capacity(ramfs_file_data* data, uint64 needed)
+{
+ if (needed <= data->capacity) return 0;
+
+ /* Round up to page size */
+ uint64 new_cap = (needed + 4095) & ~4095ULL;
+ char* new_buf = new char[new_cap];
+ if (!new_buf) return -ENOMEM;
+
+ /* Copy old data */
+ if (data->buf && data->capacity > 0) {
+ for (uint64 i = 0; i < data->capacity; i++)
+ new_buf[i] = data->buf[i];
+ }
+ /* Zero new region */
+ for (uint64 i = data->capacity; i < new_cap; i++)
+ new_buf[i] = 0;
+
+ delete[] data->buf;
+ data->buf = new_buf;
+ data->capacity = new_cap;
+ return 0;
+}
+
+/* ================================================================== */
+/* ramfs_read */
+/* ================================================================== */
+
+static int64 ramfs_read(file* fp, char* buf, uint64 count, int64* pos)
+{
+ inode* ip = file_inode(fp);
+ int64 offset = *pos;
+
+ if (offset < 0) return -EINVAL;
+ if (offset >= ip->i_size) return 0;
+
+ int64 avail = ip->i_size - offset;
+ if (static_cast<int64>(count) > avail)
+ count = static_cast<uint64>(avail);
+
+ auto* data = static_cast<ramfs_file_data*>(ip->i_private);
+ if (!data || !data->buf) return 0;
+
+ /* Copy to user buffer.
+ * TODO: use copy_to_user when called from syscall context */
+ for (uint64 i = 0; i < count; i++)
+ buf[i] = data->buf[offset + i];
+
+ *pos += count;
+ return static_cast<int64>(count);
+}
+
+/* ================================================================== */
+/* ramfs_write */
+/* ================================================================== */
+
+static int64 ramfs_write(file* fp, const char* buf, uint64 count, int64* pos)
+{
+ inode* ip = file_inode(fp);
+ int64 offset = *pos;
+
+ if (fp->f_flags & O_APPEND)
+ offset = ip->i_size;
+
+ if (offset < 0) return -EINVAL;
+
+ auto* data = get_or_create_data(ip);
+ uint64 end = static_cast<uint64>(offset) + count;
+
+ int err = ensure_capacity(data, end);
+ if (err) return err;
+
+ /* Copy from user buffer */
+ for (uint64 i = 0; i < count; i++)
+ data->buf[offset + i] = buf[i];
+
+ if (static_cast<int64>(end) > ip->i_size)
+ ip->i_size = end;
+
+ *pos = end;
+ mark_inode_dirty(ip);
+ return static_cast<int64>(count);
+}
+
+/* ================================================================== */
+/* ramfs_dir_iterate — getdents64 */
+/* ================================================================== */
+
+static int ramfs_dir_iterate(file* fp, dir_context* ctx)
+{
+ dentry* dir_de = file_dentry(fp);
+ inode* dir_ip = file_inode(fp);
+ int64 pos = ctx->pos;
+ int count = 0;
+
+ /* pos 0: emit "." */
+ if (pos == 0) {
+ if (!ctx->actor(ctx, ".", 1, pos, dir_ip->i_ino, DT_DIR))
+ return 0;
+ ctx->pos = 1;
+ pos = 1;
+ count++;
+ }
+
+ /* pos 1: emit ".." */
+ if (pos == 1) {
+ uint64 parent_ino = dir_de->d_parent->d_inode
+ ? dir_de->d_parent->d_inode->i_ino : dir_ip->i_ino;
+ if (!ctx->actor(ctx, "..", 2, pos, parent_ino, DT_DIR))
+ return 0;
+ ctx->pos = 2;
+ pos = 2;
+ count++;
+ }
+
+ /* pos >= 2: iterate children */
+ int64 child_idx = 2;
+ list_node* node;
+ list_for_each(node, &dir_de->d_children) {
+ dentry* child = list_entry(node, dentry, d_child);
+
+ if (!child->d_inode)
+ continue; /* skip negative dentries */
+
+ if (child_idx >= pos) {
+ uint8 d_type = DT_UNKNOWN;
+ if (S_ISDIR(child->d_inode->i_mode)) d_type = DT_DIR;
+ else if (S_ISREG(child->d_inode->i_mode)) d_type = DT_REG;
+ else if (S_ISCHR(child->d_inode->i_mode)) d_type = DT_CHR;
+ else if (S_ISBLK(child->d_inode->i_mode)) d_type = DT_BLK;
+ else if (S_ISLNK(child->d_inode->i_mode)) d_type = DT_LNK;
+ else if (S_ISFIFO(child->d_inode->i_mode)) d_type = DT_FIFO;
+
+ if (!ctx->actor(ctx, child->d_name.name,
+ static_cast<int>(child->d_name.len),
+ child_idx, child->d_inode->i_ino, d_type))
+ return 0;
+
+ ctx->pos = child_idx + 1;
+ count++;
+ }
+ child_idx++;
+ }
+
+ return count;
+}
+
+extern file_operations ramfs_file_operations;
+extern file_operations ramfs_dir_operations;
+
+file_operations ramfs_file_operations;
+file_operations ramfs_dir_operations;
+
+void ramfs_file_ops_init()
+{
+ ramfs_file_operations.llseek = nullptr;
+ ramfs_file_operations.read = ramfs_read;
+ ramfs_file_operations.write = ramfs_write;
+ ramfs_file_operations.iterate_shared = nullptr;
+ ramfs_file_operations.poll = nullptr;
+ ramfs_file_operations.unlocked_ioctl = nullptr;
+ ramfs_file_operations.mmap = nullptr;
+ ramfs_file_operations.open = nullptr;
+ ramfs_file_operations.flush = nullptr;
+ ramfs_file_operations.release = nullptr;
+ ramfs_file_operations.fsync = nullptr;
+
+ ramfs_dir_operations.llseek = nullptr;
+ ramfs_dir_operations.read = nullptr;
+ ramfs_dir_operations.write = nullptr;
+ ramfs_dir_operations.iterate_shared = ramfs_dir_iterate;
+ ramfs_dir_operations.poll = nullptr;
+ ramfs_dir_operations.unlocked_ioctl = nullptr;
+ ramfs_dir_operations.mmap = nullptr;
+ ramfs_dir_operations.open = nullptr;
+ ramfs_dir_operations.flush = nullptr;
+ ramfs_dir_operations.release = nullptr;
+ ramfs_dir_operations.fsync = nullptr;
+}
diff --git a/fs/ramfs/inode.cc b/fs/ramfs/inode.cc
new file mode 100644
index 0000000..c5e49bf
--- /dev/null
+++ b/fs/ramfs/inode.cc
@@ -0,0 +1,164 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * ramfs/inode.cc — RamFS inode operations
+ *
+ * A simple in-memory filesystem (like Linux tmpfs without swap).
+ * All data lives in memory via the page cache.
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* Forward declarations */
+extern file_operations ramfs_file_operations;
+extern file_operations ramfs_dir_operations;
+extern inode_operations ramfs_dir_inode_operations;
+
+/* ================================================================== */
+/* ramfs_get_inode — allocate + init an inode for ramfs */
+/* ================================================================== */
+
+inode* ramfs_get_inode(super_block* sb, const inode* dir, uint32 mode, uint32 rdev)
+{
+ inode* ip = new_inode(sb);
+ if (!ip) return nullptr;
+
+ inode_init_owner(ip, dir, mode);
+ /* TODO: set timestamps to current time when we have a time source */
+
+ switch (mode & S_IFMT) {
+ case S_IFREG:
+ ip->i_op = nullptr; /* regular files don't need inode_ops for Phase 1 */
+ ip->i_fop = &ramfs_file_operations;
+ break;
+ case S_IFDIR:
+ ip->i_op = &ramfs_dir_inode_operations;
+ ip->i_fop = &ramfs_dir_operations;
+ inc_nlink(ip); /* directories start with nlink=2 (. and parent's link) */
+ break;
+ case S_IFLNK:
+ /* Symlink: stub for Phase 1 */
+ break;
+ default:
+ /* Character/block devices: i_rdev */
+ ip->i_rdev = rdev;
+ break;
+ }
+
+ return ip;
+}
+
+/* ================================================================== */
+/* Directory inode operations */
+/* ================================================================== */
+
+static dentry* ramfs_lookup(inode* dir, dentry* de, unsigned int flags)
+{
+ (void)dir;
+ (void)flags;
+ /* For ramfs, everything is in dcache. If we get here, it means
+ * d_lookup() already failed, so this is a negative dentry.
+ * Just return nullptr to let the caller see de->d_inode == NULL. */
+ return nullptr;
+}
+
+static int ramfs_create(inode* dir, dentry* de, uint32 mode, bool excl)
+{
+ (void)excl;
+ inode* ip = ramfs_get_inode(dir->i_sb, dir, mode | S_IFREG, 0);
+ if (!ip) return -ENOMEM;
+
+ d_instantiate(de, ip);
+ dget(de);
+ dir->i_size += de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_mkdir(inode* dir, dentry* de, uint32 mode)
+{
+ inode* ip = ramfs_get_inode(dir->i_sb, dir, mode | S_IFDIR, 0);
+ if (!ip) return -ENOMEM;
+
+ d_instantiate(de, ip);
+ dget(de);
+ inc_nlink(dir);
+ dir->i_size += de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_rmdir(inode* dir, dentry* de)
+{
+ /* Check if directory is empty */
+ if (!list_empty(&de->d_children))
+ return -ENOTEMPTY;
+
+ drop_nlink(de->d_inode); /* remove . link */
+ drop_nlink(de->d_inode); /* remove parent's link */
+ drop_nlink(dir);
+ d_delete(de);
+ dir->i_size -= de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_unlink(inode* dir, dentry* de)
+{
+ drop_nlink(de->d_inode);
+ d_delete(de);
+ dir->i_size -= de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_link(dentry* old_de, inode* dir, dentry* new_de)
+{
+ inode* ip = old_de->d_inode;
+ if (!ip) return -ENOENT;
+
+ inc_nlink(ip);
+ ihold(ip);
+ d_instantiate(new_de, ip);
+ dget(new_de);
+ dir->i_size += new_de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_mknod(inode* dir, dentry* de, uint32 mode, uint32 rdev)
+{
+ inode* ip = ramfs_get_inode(dir->i_sb, dir, mode, rdev);
+ if (!ip) return -ENOMEM;
+
+ d_instantiate(de, ip);
+ dget(de);
+ dir->i_size += de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+inode_operations ramfs_dir_inode_operations;
+
+void ramfs_dir_inode_ops_init()
+{
+ ramfs_dir_inode_operations.lookup = ramfs_lookup;
+ ramfs_dir_inode_operations.create = ramfs_create;
+ ramfs_dir_inode_operations.link = ramfs_link;
+ ramfs_dir_inode_operations.unlink = ramfs_unlink;
+ ramfs_dir_inode_operations.symlink = nullptr;
+ ramfs_dir_inode_operations.mkdir = ramfs_mkdir;
+ ramfs_dir_inode_operations.rmdir = ramfs_rmdir;
+ ramfs_dir_inode_operations.mknod = ramfs_mknod;
+ ramfs_dir_inode_operations.rename = nullptr;
+ ramfs_dir_inode_operations.setattr = nullptr;
+ ramfs_dir_inode_operations.getattr = nullptr;
+ ramfs_dir_inode_operations.get_link = nullptr;
+ ramfs_dir_inode_operations.permission = nullptr;
+}
diff --git a/fs/ramfs/super.cc b/fs/ramfs/super.cc
new file mode 100644
index 0000000..6f929f2
--- /dev/null
+++ b/fs/ramfs/super.cc
@@ -0,0 +1,154 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * ramfs/super.cc — RamFS superblock: mount, fill_super, and init
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/mount.h>
+#include <lume/slab.h>
+#include <lume/kprintf.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+extern inode* ramfs_get_inode(super_block* sb, const inode* dir,
+ uint32 mode, uint32 rdev);
+
+/* Superblock allocation helper (defined in super.cc) */
+extern super_block* alloc_super(file_system_type* type);
+
+/* ================================================================== */
+/* ramfs super_operations */
+/* ================================================================== */
+
+static inode* ramfs_alloc_inode(super_block* sb)
+{
+ return new inode{};
+}
+
+static void ramfs_destroy_inode(inode* ip)
+{
+ /* Free ramfs_file_data if any */
+ if (ip->i_private) {
+ struct ramfs_file_data {
+ char* buf;
+ uint64 capacity;
+ };
+ auto* data = static_cast<ramfs_file_data*>(ip->i_private);
+ delete[] data->buf;
+ delete data;
+ }
+ delete ip;
+}
+
+static int ramfs_statfs(dentry* de, kstatfs* buf)
+{
+ /* Simple fake values */
+ buf->f_type = 0x858458f6; /* RAMFS_MAGIC */
+ buf->f_bsize = 4096;
+ buf->f_blocks = 0;
+ buf->f_bfree = 0;
+ buf->f_bavail = 0;
+ buf->f_files = 0;
+ buf->f_ffree = 0;
+ buf->f_namelen = 255;
+ buf->f_frsize = 4096;
+ buf->f_flags = 0;
+ return 0;
+}
+
+static super_operations ramfs_sops;
+
+static void ramfs_sops_init()
+{
+ ramfs_sops.alloc_inode = ramfs_alloc_inode;
+ ramfs_sops.destroy_inode = ramfs_destroy_inode;
+ ramfs_sops.free_inode = nullptr;
+ ramfs_sops.dirty_inode = nullptr;
+ ramfs_sops.write_inode = nullptr;
+ ramfs_sops.drop_inode = nullptr;
+ ramfs_sops.evict_inode = nullptr;
+ ramfs_sops.put_super = nullptr;
+ ramfs_sops.sync_fs = nullptr;
+ ramfs_sops.statfs = ramfs_statfs;
+ ramfs_sops.umount_begin = nullptr;
+}
+
+/* ================================================================== */
+/* ramfs_fill_super */
+/* ================================================================== */
+
+static int ramfs_fill_super(super_block* sb, void* data, int silent)
+{
+ sb->s_op = &ramfs_sops;
+ sb->s_magic = 0x858458f6;
+ sb->s_blocksize = 4096;
+ sb->s_blocksize_bits = 12;
+ sb->s_maxbytes = 0x7FFFFFFFFFFFFFFFLL;
+ sb->s_time_gran = 1;
+
+ /* Create root inode */
+ inode* root_ip = ramfs_get_inode(sb, nullptr, S_IFDIR | 0755, 0);
+ if (!root_ip) return -ENOMEM;
+
+ /* Create root dentry */
+ sb->s_root = d_make_root(root_ip);
+ if (!sb->s_root)
+ return -ENOMEM;
+
+ return 0;
+}
+
+/* ================================================================== */
+/* ramfs_mount */
+/* ================================================================== */
+
+static super_block* ramfs_mount(file_system_type* type, int flags,
+ const char* dev_name, void* data)
+{
+ super_block* sb = alloc_super(type);
+ if (!sb) return nullptr;
+
+ int err = ramfs_fill_super(sb, data, flags & SB_SILENT);
+ if (err) {
+ delete sb;
+ return nullptr;
+ }
+
+ return sb;
+}
+
+static void ramfs_kill_sb(super_block* sb)
+{
+ /* Phase 1: no cleanup needed for ramfs */
+ (void)sb;
+}
+
+/* ================================================================== */
+/* Filesystem type registration */
+/* ================================================================== */
+
+static file_system_type ramfs_fs_type;
+
+extern void ramfs_file_ops_init();
+extern void ramfs_dir_inode_ops_init();
+
+void ramfs_init()
+{
+ /* Runtime init to avoid un-relocated absolute pointers in .data */
+ ramfs_sops_init();
+ ramfs_file_ops_init();
+ ramfs_dir_inode_ops_init();
+
+ ramfs_fs_type.name = "ramfs";
+ ramfs_fs_type.fs_flags = FS_NO_DEVICE;
+ ramfs_fs_type.mount = ramfs_mount;
+ ramfs_fs_type.kill_sb = ramfs_kill_sb;
+ list_init(&ramfs_fs_type.fs_list);
+
+ register_filesystem(&ramfs_fs_type);
+}
+
diff --git a/fs/super.cc b/fs/super.cc
new file mode 100644
index 0000000..231b57c
--- /dev/null
+++ b/fs/super.cc
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * super.cc — Superblock lifecycle + filesystem type registry
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/mount.h>
+#include <lume/klog.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+/* Global root filesystem entry points */
+vfsmount* g_root_mnt = nullptr;
+dentry* g_root_dentry = nullptr;
+
+/* ================================================================== */
+/* Filesystem type registry */
+/* ================================================================== */
+
+namespace {
+
+list_head g_fs_types;
+Spinlock g_fs_type_lock;
+
+} // namespace
+
+int register_filesystem(file_system_type* fs)
+{
+ LockGuard guard(g_fs_type_lock);
+ list_add_tail(&fs->fs_list, &g_fs_types);
+ printk("[vfs] registered filesystem: %s\n", fs->name);
+ return 0;
+}
+
+file_system_type* get_fs_type(const char* name)
+{
+ LockGuard guard(g_fs_type_lock);
+ list_node* pos;
+ list_for_each(pos, &g_fs_types) {
+ auto* fs = list_entry(pos, file_system_type, fs_list);
+ /* Simple strcmp */
+ const char* a = fs->name;
+ const char* b = name;
+ while (*a && *a == *b) { a++; b++; }
+ if (*a == '\0' && *b == '\0')
+ return fs;
+ }
+ return nullptr;
+}
+
+/* ================================================================== */
+/* Superblock allocation */
+/* ================================================================== */
+
+super_block* alloc_super(file_system_type* type)
+{
+ auto* sb = new super_block{};
+ if (!sb) return nullptr;
+
+ list_init(&sb->s_list);
+ list_init(&sb->s_inodes);
+ sb->s_type = type;
+ sb->s_op = nullptr;
+ sb->s_root = nullptr;
+ sb->s_fs_info = nullptr;
+ sb->s_blocksize = 4096;
+ sb->s_blocksize_bits = 12;
+ sb->s_maxbytes = 0x7FFFFFFFFFFFFFFFLL;
+ sb->s_time_gran = 1;
+ sb->s_time_min = 0;
+ sb->s_time_max = 0x7FFFFFFFFFFFFFFFLL;
+ sb->s_flags = 0;
+ sb->s_magic = 0;
+ sb->s_active.store(1);
+ sb->s_count = 1;
+ sb->s_max_links = 0;
+ sb->s_d_op = nullptr;
+ sb->s_mounts = nullptr;
+ sb->s_dev = 0;
+ for (int i = 0; i < 32; i++) sb->s_id[i] = 0;
+
+ return sb;
+}
+
+/* ================================================================== */
+/* Init */
+/* ================================================================== */
+
+void super_init()
+{
+ list_init(&g_fs_types);
+}
diff --git a/fs/vfs_init.cc b/fs/vfs_init.cc
new file mode 100644
index 0000000..ec56e88
--- /dev/null
+++ b/fs/vfs_init.cc
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * vfs_init.cc — Bootstrap the VFS: mount ramfs root, set global root
+ *
+ * Called from kernel_main() after slab_init() and ramfs_init().
+ * This is the moral equivalent of Linux's vfs_caches_init() +
+ * mnt_init() + init_rootfs().
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/mount.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/klog.h>
+#include <lume/kprintf.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+
+/* ================================================================== */
+/* vfs_init — mount ramfs as root filesystem */
+/* ================================================================== */
+
+/*
+ * ramfs_mount is called internally via the file_system_type::mount
+ * callback registered by ramfs_init(). We look it up by name.
+ */
+extern file_system_type* get_fs_type(const char* name);
+extern super_block* alloc_super(file_system_type* type);
+
+void vfs_init()
+{
+ /* 1. Look up the ramfs filesystem type */
+ file_system_type* ramfs_type = get_fs_type("ramfs");
+ if (!ramfs_type || !ramfs_type->mount) {
+ printk(KERN_ERR "vfs_init: ramfs not registered!\n");
+ return;
+ }
+
+ /* 2. Mount ramfs */
+ super_block* sb = ramfs_type->mount(ramfs_type, 0, "ramfs", nullptr);
+ if (!sb || !sb->s_root) {
+ printk(KERN_ERR "vfs_init: failed to mount ramfs!\n");
+ return;
+ }
+
+ /* 3. Set global root */
+ g_root_dentry = sb->s_root;
+ g_root_mnt = nullptr; /* TODO: allocate a proper vfsmount when mount.cc lands */
+
+ dget(g_root_dentry); /* Hold a reference for the global root */
+
+ printk(KERN_INFO "[vfs] root filesystem mounted (ramfs)\n");
+
+ /* 4. Set up VFS state for the current (BSP idle) task,
+ * which will be inherited by all subsequently created tasks. */
+ TaskControlBlock* bsp = current_task();
+ if (bsp) {
+ if (!bsp->files) {
+ bsp->files = alloc_files_struct();
+ }
+ if (!bsp->fs) {
+ bsp->fs = alloc_fs_struct();
+ if (bsp->fs) {
+ bsp->fs->root.de = g_root_dentry;
+ bsp->fs->root.mnt = g_root_mnt;
+ bsp->fs->pwd.de = g_root_dentry;
+ bsp->fs->pwd.mnt = g_root_mnt;
+ dget(g_root_dentry); /* root ref */
+ dget(g_root_dentry); /* pwd ref */
+ }
+ }
+ printk(KERN_INFO "[vfs] BSP task VFS context initialized\n");
+ }
+}