Files
zinnia/fs/ops.c
T
2026-10-25 18:35:03 +01:00

657 lines
18 KiB
C

// SPDX-License-Identifier: ISC
#include <zinnia/clock.h>
#include <zinnia/vfs.h>
#include <string.h>
static bool is_read_only(struct vfs_node* node) {
return node->sb && (node->sb->flags & MNT_RDONLY);
}
errno_t vfs_node_try_access(
struct vfs_node* node,
const struct ident* identity,
enum access_flags access,
bool use_real
) {
mutex_lock(&node->lock);
const mode_t mode = node->mode;
const uid_t owner = node->uid;
const gid_t group = node->gid;
mutex_unlock(&node->lock);
if ((access & W_OK) && is_read_only(node) &&
(node->type == VNODE_REGULAR || node->type == VNODE_DIRECTORY || node->type == VNODE_SYMLINK))
return EROFS;
if ((access & X_OK) && node->type == VNODE_REGULAR && node->sb && (node->sb->flags & MNT_NOEXEC))
return EACCES;
if (ident_is_privileged(identity, use_real)) {
if ((access & X_OK) && node->type != VNODE_DIRECTORY && !(mode & (__S_IXUSR | __S_IXGRP | __S_IXOTH)))
return EACCES;
return 0;
}
mode_t granted;
if ((use_real ? identity->uid : identity->euid) == owner)
granted = (mode >> 6) & 7;
else if (ident_in_group(identity, group, use_real))
granted = (mode >> 3) & 7;
else
granted = mode & 7;
return (access & (R_OK | W_OK | X_OK) & ~granted) ? EACCES : 0;
}
static errno_t sync_node(struct vfs_node* node) {
return node->ops && node->ops->sync ? node->ops->sync(node) : 0;
}
static timespec_t now() {
return time_from_nano(clock_get_realtime_ns());
}
// Looks up the parent directory of `path` and checks that `identity` may write to it.
// Returns a new reference to the parent inode in `out`.
static errno_t get_writable_parent(struct vfs_path path, const struct ident* identity, struct vfs_node** out) {
struct vfs_path parent;
errno_t status = vfs_path_lookup_parent(path, &parent);
if (status)
return status;
struct vfs_node* node = vfs_entry_get_inode(parent.entry);
vfs_path_drop(parent);
if (!node)
return ENOENT;
if (node->type != VNODE_DIRECTORY) {
vfs_node_unref(node);
return ENOTDIR;
}
status = vfs_node_try_access(node, identity, W_OK | X_OK, false);
if (status) {
vfs_node_unref(node);
return status;
}
*out = node;
return 0;
}
errno_t vfs_mkdir(
struct vfs_path root,
struct vfs_path cwd,
const char* path,
mode_t mode,
const struct ident* identity
) {
struct vfs_path target;
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &target);
if (status)
return status;
struct vfs_node* parent;
status = get_writable_parent(target, identity, &parent);
if (status)
goto out;
status = parent->ops->dir.mkdir ? parent->ops->dir.mkdir(parent, &target, mode, identity) : EPERM;
vfs_node_unref(parent);
out:
vfs_path_drop(target);
return status;
}
errno_t vfs_symlink(
struct vfs_path root,
struct vfs_path cwd,
const char* path,
const char* target_path,
const struct ident* identity
) {
struct vfs_path target;
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &target);
if (status)
return status;
struct vfs_node* parent;
status = get_writable_parent(target, identity, &parent);
if (status)
goto out;
status = parent->ops->dir.symlink
? parent->ops->dir.symlink(parent, &target, target_path, strlen(target_path), identity)
: EPERM;
vfs_node_unref(parent);
out:
vfs_path_drop(target);
return status;
}
static bool last_component_is(const char* path, const char* name) {
size_t len = strlen(path);
while (len > 1 && path[len - 1] == '/')
len--;
size_t begin = len;
while (begin > 0 && path[begin - 1] != '/')
begin--;
return len - begin == strlen(name) && !memcmp(path + begin, name, len - begin);
}
static errno_t check_sticky(struct vfs_node* dir, struct vfs_node* victim, const struct ident* identity) {
if (identity->euid == 0)
return 0;
mutex_lock(&dir->lock);
const bool sticky = dir->mode & __S_ISVTX;
const uid_t dir_owner = dir->uid;
mutex_unlock(&dir->lock);
if (!sticky)
return 0;
mutex_lock(&victim->lock);
const uid_t owner = victim->uid;
mutex_unlock(&victim->lock);
return identity->euid == owner || identity->euid == dir_owner ? 0 : EPERM;
}
errno_t vfs_unlink(struct vfs_path root, struct vfs_path cwd, const char* path, const struct ident* identity) {
struct vfs_path target;
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_EXIST, &target);
if (status)
return status;
struct vfs_node* node = vfs_entry_get_inode(target.entry);
if (!node) {
status = ENOENT;
goto out;
}
if (node->type == VNODE_DIRECTORY) {
status = EISDIR;
goto out_node;
}
struct vfs_node* parent;
status = get_writable_parent(target, identity, &parent);
if (status)
goto out_node;
status = check_sticky(parent, node, identity);
if (!status)
status = parent->ops->dir.unlink ? parent->ops->dir.unlink(parent, &target, identity) : EPERM;
vfs_node_unref(parent);
out_node:
vfs_node_unref(node);
out:
vfs_path_drop(target);
return status;
}
// Probe for a single entry to check if the directory is empty.
static errno_t check_dir_empty(struct vfs_node* node, struct vfs_entry* entry, const struct ident* identity) {
struct __dirent probe;
size_t count = 0;
if (node->ops->dir.get_dir_entries) {
errno_t status = node->ops->dir.get_dir_entries(node, entry, 0, &probe, 1, identity, &count);
if (status)
return status;
}
return count > 0 ? ENOTEMPTY : 0;
}
errno_t vfs_rmdir(struct vfs_path root, struct vfs_path cwd, const char* path, const struct ident* identity) {
if (last_component_is(path, "."))
return EINVAL;
if (last_component_is(path, ".."))
return ENOTEMPTY;
struct vfs_path target;
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_EXIST, &target);
if (status)
return status;
struct vfs_node* node = vfs_entry_get_inode(target.entry);
if (!node) {
status = ENOENT;
goto out;
}
if (node->type != VNODE_DIRECTORY) {
status = ENOTDIR;
goto out_node;
}
if (target.entry == target.mount->root) {
status = EBUSY;
goto out_node;
}
status = check_dir_empty(node, target.entry, identity);
if (status)
goto out_node;
struct vfs_node* parent;
status = get_writable_parent(target, identity, &parent);
if (status)
goto out_node;
status = check_sticky(parent, node, identity);
if (!status) {
if (parent->ops->dir.rmdir)
status = parent->ops->dir.rmdir(parent, &target, identity);
else if (parent->ops->dir.unlink)
status = parent->ops->dir.unlink(parent, &target, identity);
else
status = EPERM;
}
vfs_node_unref(parent);
out_node:
vfs_node_unref(node);
out:
vfs_path_drop(target);
return status;
}
errno_t vfs_mknod(
struct vfs_path root,
struct vfs_path cwd,
const char* path,
mode_t mode,
struct vfs_mknod_target* target,
const struct ident* identity,
struct vfs_node** out
) {
struct vfs_path new_path;
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &new_path);
if (status)
return status;
struct vfs_node* parent;
status = get_writable_parent(new_path, identity, &parent);
if (status)
goto out;
status = parent->ops->dir.mknod ? parent->ops->dir.mknod(parent, &new_path, mode, target, identity) : ENODEV;
vfs_node_unref(parent);
if (!status && out) {
*out = vfs_entry_get_inode(new_path.entry);
if (!*out)
status = ENOENT;
}
out:
vfs_path_drop(new_path);
return status;
}
static bool entry_is_ancestor(struct vfs_entry* ancestor, struct vfs_entry* entry) {
for (struct vfs_entry* it = entry; it; it = it->parent) {
if (it == ancestor)
return true;
}
return false;
}
static bool entry_points_to(struct vfs_entry* entry, struct vfs_node* node) {
struct vfs_node* current = vfs_entry_get_inode(entry);
if (current)
vfs_node_unref(current);
return current == node;
}
static errno_t check_rename_target(struct vfs_node* moved, struct vfs_path target, const struct ident* identity) {
struct vfs_node* existing = vfs_entry_get_inode(target.entry);
if (!existing)
return 0;
errno_t status = 0;
if (moved->type == VNODE_DIRECTORY && existing->type != VNODE_DIRECTORY)
status = ENOTDIR;
else if (moved->type != VNODE_DIRECTORY && existing->type == VNODE_DIRECTORY)
status = EISDIR;
else if (existing->type == VNODE_DIRECTORY)
status = check_dir_empty(existing, target.entry, identity);
vfs_node_unref(existing);
return status;
}
static errno_t check_rename_perms(
struct vfs_node* moved,
struct vfs_path new,
struct vfs_node* old_parent,
struct vfs_node* new_parent,
const struct ident* identity
) {
errno_t status = check_sticky(old_parent, moved, identity);
if (status)
return status;
struct vfs_node* existing = vfs_entry_get_inode(new.entry);
if (existing) {
status = check_sticky(new_parent, existing, identity);
vfs_node_unref(existing);
if (status)
return status;
}
if (moved->type == VNODE_DIRECTORY && old_parent != new_parent)
return vfs_node_try_access(moved, identity, W_OK, false);
return 0;
}
static errno_t do_rename(
struct vfs_path old,
struct vfs_path new,
struct vfs_node* moved,
const struct ident* identity
) {
struct vfs_node* old_parent;
errno_t status = get_writable_parent(old, identity, &old_parent);
if (status)
return status;
struct vfs_node* new_parent;
status = get_writable_parent(new, identity, &new_parent);
if (status) {
vfs_node_unref(old_parent);
return status;
}
status = check_rename_perms(moved, new, old_parent, new_parent, identity);
if (!status) {
if (old_parent->ops->dir.rename)
status = old_parent->ops->dir.rename(old_parent, &old, new_parent, &new, identity);
else
status = EPERM;
}
vfs_node_unref(new_parent);
vfs_node_unref(old_parent);
return status;
}
errno_t vfs_rename(
struct vfs_path root,
struct vfs_path old_cwd,
const char* old_path,
struct vfs_path new_cwd,
const char* new_path,
const struct ident* identity
) {
if (last_component_is(old_path, ".") || last_component_is(old_path, "..") || last_component_is(new_path, ".") ||
last_component_is(new_path, ".."))
return EINVAL;
struct vfs_path old;
errno_t status = vfs_path_lookup(root, old_cwd, old_path, identity, VFS_LOOKUP_MUST_EXIST, &old);
if (status)
return status;
struct vfs_path new;
status = vfs_path_lookup(root, new_cwd, new_path, identity, 0, &new);
if (status)
goto out_old;
if (old.mount != new.mount) {
status = EXDEV;
goto out_new;
}
if (old.entry == old.mount->root || new.entry == new.mount->root) {
status = EBUSY;
goto out_new;
}
if (old.entry == new.entry)
goto out_new;
struct vfs_node* moved = vfs_entry_get_inode(old.entry);
if (!moved) {
status = ENOENT;
goto out_new;
}
if (moved->type == VNODE_DIRECTORY && entry_is_ancestor(old.entry, new.entry))
status = EINVAL;
if (!status && !entry_points_to(new.entry, moved)) {
status = check_rename_target(moved, new, identity);
if (!status)
status = do_rename(old, new, moved, identity);
}
vfs_node_unref(moved);
out_new:
vfs_path_drop(new);
out_old:
vfs_path_drop(old);
return status;
}
errno_t vfs_link(
struct vfs_path root,
struct vfs_path old_cwd,
const char* old_path,
struct vfs_path new_cwd,
const char* new_path,
bool follow_symlinks,
const struct ident* identity
) {
enum vfs_lookup_flags flags = VFS_LOOKUP_MUST_EXIST;
if (follow_symlinks)
flags |= VFS_LOOKUP_FOLLOW_SYMLINKS;
struct vfs_path old;
errno_t status = vfs_path_lookup(root, old_cwd, old_path, identity, flags, &old);
if (status)
return status;
struct vfs_node* target = vfs_entry_get_inode(old.entry);
if (!target) {
status = ENOENT;
goto out_old;
}
if (target->type == VNODE_DIRECTORY) {
status = EPERM;
goto out_target;
}
struct vfs_path new;
status = vfs_path_lookup(root, new_cwd, new_path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &new);
if (status)
goto out_target;
if (new.mount != old.mount) {
status = EXDEV;
goto out_new;
}
struct vfs_node* parent;
status = get_writable_parent(new, identity, &parent);
if (status)
goto out_new;
status = parent->ops->dir.link ? parent->ops->dir.link(parent, &new, target, identity) : EPERM;
vfs_node_unref(parent);
out_new:
vfs_path_drop(new);
out_target:
vfs_node_unref(target);
out_old:
vfs_path_drop(old);
return status;
}
static mode_t node_type_mode(enum vfs_node_type type) {
switch (type) {
case VNODE_DIRECTORY:
return __S_IFDIR;
case VNODE_SYMLINK:
return __S_IFLNK;
case VNODE_FIFO:
return __S_IFIFO;
case VNODE_BLOCKDEV:
return __S_IFBLK;
case VNODE_CHARDEV:
return __S_IFCHR;
case VNODE_SOCKET:
return __S_IFSOCK;
default:
return __S_IFREG;
}
}
void vfs_stat(struct vfs_node* node, struct __stat* out) {
mutex_lock(&node->lock);
*out = (struct __stat){
.st_ino = node->id,
.st_mode = node->mode | node_type_mode(node->type),
.st_nlink = node->nlink,
.st_uid = node->uid,
.st_gid = node->gid,
.st_rdev = node->rdev,
.st_size = node->size,
.st_atim = node->atime,
.st_mtim = node->mtime,
.st_ctim = node->ctime,
.st_blksize = 4096, // TODO: Get from file system
.st_blocks = node->blocks,
};
mutex_unlock(&node->lock);
}
static bool is_owner(struct vfs_node* node, const struct ident* identity) {
return identity->euid == 0 || identity->euid == node->uid;
}
errno_t vfs_chmod(struct vfs_node* node, mode_t mode, const struct ident* identity) {
if (is_read_only(node))
return EROFS;
mutex_lock(&node->lock);
if (!is_owner(node, identity)) {
mutex_unlock(&node->lock);
return EPERM;
}
mode &= 07777;
if (identity->euid != 0 && node->type == VNODE_REGULAR && !ident_in_group(identity, node->gid, false))
mode &= ~__S_ISGID;
node->mode = mode;
node->ctime = now();
mutex_unlock(&node->lock);
return sync_node(node);
}
errno_t vfs_chown(struct vfs_node* node, uid_t uid, gid_t gid, const struct ident* identity) {
if (is_read_only(node))
return EROFS;
mutex_lock(&node->lock);
if (identity->euid != 0) {
const bool uid_ok = uid == (uid_t)-1 || uid == node->uid;
const bool gid_ok = gid == (gid_t)-1 || gid == node->gid || ident_in_group(identity, gid, false);
if (identity->euid != node->uid || !uid_ok || !gid_ok) {
mutex_unlock(&node->lock);
return EPERM;
}
}
if (uid == (uid_t)-1 && gid == (gid_t)-1) {
mutex_unlock(&node->lock);
return 0;
}
if (uid != (uid_t)-1)
node->uid = uid;
if (gid != (gid_t)-1)
node->gid = gid;
if (identity->euid != 0 && node->type == VNODE_REGULAR && (node->mode & (__S_IXUSR | __S_IXGRP | __S_IXOTH)))
node->mode &= ~(__S_ISUID | __S_ISGID);
node->ctime = now();
mutex_unlock(&node->lock);
return sync_node(node);
}
static bool valid_utime(const timespec_t* ts) {
return ts->tv_nsec == __UTIME_NOW || ts->tv_nsec == __UTIME_OMIT ||
(ts->tv_nsec >= 0 && ts->tv_nsec < 1'000'000'000);
}
errno_t vfs_utimens(struct vfs_node* node, const timespec_t* times, const struct ident* identity) {
const timespec_t current = now();
timespec_t ts[2] = {
{.tv_nsec = __UTIME_NOW},
{.tv_nsec = __UTIME_NOW},
};
if (times) {
ts[0] = times[0];
ts[1] = times[1];
if (!valid_utime(&ts[0]) || !valid_utime(&ts[1]))
return EINVAL;
}
if (ts[0].tv_nsec == __UTIME_OMIT && ts[1].tv_nsec == __UTIME_OMIT)
return 0;
if (is_read_only(node))
return EROFS;
const bool only_now = (ts[0].tv_nsec == __UTIME_NOW || ts[0].tv_nsec == __UTIME_OMIT) &&
(ts[1].tv_nsec == __UTIME_NOW || ts[1].tv_nsec == __UTIME_OMIT);
mutex_lock(&node->lock);
const bool owner = is_owner(node, identity);
mutex_unlock(&node->lock);
if (!owner) {
if (!only_now)
return EPERM;
errno_t status = vfs_node_try_access(node, identity, W_OK, false);
if (status)
return status;
}
mutex_lock(&node->lock);
for (size_t i = 0; i < 2; i++) {
if (ts[i].tv_nsec == __UTIME_OMIT)
continue;
timespec_t* target = i == 0 ? &node->atime : &node->mtime;
*target = ts[i].tv_nsec == __UTIME_NOW ? current : ts[i];
}
node->ctime = current;
mutex_unlock(&node->lock);
return sync_node(node);
}
errno_t vfs_node_check_exec(struct vfs_node* node, const struct ident* identity) {
if (node->type != VNODE_REGULAR)
return EACCES;
return vfs_node_try_access(node, identity, X_OK, false);
}
errno_t vfs_truncate(struct vfs_node* node, uint64_t length) {
if (node->type == VNODE_DIRECTORY)
return EISDIR;
if (node->type != VNODE_REGULAR)
return EINVAL;
if (!node->ops->regular.truncate)
return EPERM;
return node->ops->regular.truncate(node, length);
}
errno_t vfs_read_link(struct vfs_node* node, iovec_iter_t* iter, size_t* out_read) {
if (node->type != VNODE_SYMLINK)
return EINVAL;
if (!node->ops->symlink.read_link)
return EINVAL;
return node->ops->symlink.read_link(node, iter, out_read);
}
errno_t vfs_statvfs(struct vfs_node* node, struct __statvfs* out) {
if (!node->sb || !node->sb->ops || !node->sb->ops->statvfs)
return ENOSYS;
return node->sb->ops->statvfs(node->sb, out);
}