SchOS/kernel/sys/sys_fs.c
Nexoniarz 51983144e3 exec: Linux's 2 MB of arguments; mmap: merge neighbouring mappings
exec took 32 arguments and 64 environment variables, in fixed arrays,
so a glob over more than 31 files failed with E2BIG. It now reads the
caller's argv and envp where they are, through the uaccess helpers and
a bounce page -- a second thread rewriting the array after a check
cannot slip a kernel address in -- and sizes the new stack to fit
them. The limits are Linux's: ARG_MAX, 2 MB of strings and pointers,
and 128 KB for any one string (USER_STR_MAX follows it).

Every mmap took one of a process's 256 VMA slots, and zsh takes one per
16 KB heap block, so `x=($(<file))` over 30000 words ran out after 4 MB
of heap and exited 1 without a word. Neighbouring anonymous mappings
now share a record, as Linux merges them -- the bump allocator puts
each new one where the last ended -- and munmap trims or splits it.
Partial unmaps of device or segment mappings are refused with EINVAL
instead of kfree()ing pages that are not the process's, and a failed
allocation half way through a mapping no longer leaks what it had.

Checked under QEMU: touch over 3000 names, /bin/echo with 50000
arguments, E2BIG past 2 MB, zsh splitting 100000 words, fork loops.

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

2471 lines
88 KiB
C

#include "types.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "mmu.h"
#include "memlayout.h"
#include "proc.h"
#include "fs.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "fcntl.h"
#include "posix.h"
#include "syscall.h"
#include "uaccess.h"
#include "errno.h"
#include "schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
#include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */
#include "vt.h" /* the terminals TIOC*/VT_* act on */
#include "pciuser.h"
#include "keymap.h" /* KB_*LAYOUT — the keyboard layout catalogue */
#include "netuser.h"
#include "fat.h"
// -----------------------------------------------------------------------
// Linux-ABI-compatible struct stat (x86_64 "struct stat" from
// musl 1.2.5 bits/stat.h) — musl/toybox/dash parse st_mode directly
// out of this exact byte layout, which bears no resemblance to SchOS's
// own native `struct stat` (stat.h). Without this, every Linux-ABI process
// reading stat() results (e.g. ls checking S_ISDIR to decide whether to
// list a directory or just print its name) gets garbage.
// -----------------------------------------------------------------------
struct linux_stat {
uint64 st_dev;
uint64 st_ino;
uint64 st_nlink;
uint32 st_mode;
uint32 st_uid;
uint32 st_gid;
uint32 __pad0;
uint64 st_rdev;
int64 st_size;
int64 st_blksize;
int64 st_blocks;
int64 st_atime, st_atime_nsec;
int64 st_mtime, st_mtime_nsec;
int64 st_ctime, st_ctime_nsec;
int64 __unused[3];
};
#define LINUX_S_IFDIR 0040000
#define LINUX_S_IFCHR 0020000
#define LINUX_S_IFREG 0100000
#define LINUX_S_IFLNK 0120000
#define LINUX_S_IFSOCK 0140000
#define LINUX_S_IFMT 0170000
#define LINUX_S_IFBLK 0060000
#define LINUX_S_IFIFO 0010000
// Linux's dev_t: 12 bits of major and 8 of minor packed into the low 32, with
// the overflow of each above that. Spelled out in full rather than as
// (major << 8 | minor), because that is how the other side decodes it and a
// shortcut here would only work while both numbers stayed small.
static uint64
linux_makedev(uint32 major, uint32 minor)
{
return ((uint64)(major & 0xfffu) << 8) | (uint64)(minor & 0xffu)
| ((uint64)(major & ~0xfffu) << 32) | ((uint64)(minor & ~0xffu) << 12);
}
static void
stati_linux(struct inode *ip, struct linux_stat *st)
{
memset(st, 0, sizeof(*st));
st->st_dev = ip->dev;
st->st_ino = ip->inum;
st->st_nlink = ip->nlink > 0 ? ip->nlink : 1;
st->st_size = ip->size;
st->st_blksize = 512;
st->st_blocks = (ip->size + 511) / 512;
// This filesystem's rwmode is a 2-bit read/write flag, not a real POSIX
// permission/execute bitmask, so there's no per-file "is executable"
// tracking to report accurately. Regular files are marked executable
// unconditionally (0755) — everything under /bin is meant to run, and
// shells like mksh check st_mode's x bits (or access(X_OK), which goes
// through the same stat data) before exec'ing, so leaving execute unset
// made every program "Permission denied".
uint32 type_bits, perm_bits;
switch (ip->type) {
case T_DIR: type_bits = LINUX_S_IFDIR; perm_bits = 0755; break;
case T_DEV: type_bits = LINUX_S_IFCHR; perm_bits = 0666; break;
case T_SYMLINK: type_bits = LINUX_S_IFLNK; perm_bits = 0777; break;
case T_SOCK: type_bits = LINUX_S_IFSOCK; perm_bits = 0755; break;
default: type_bits = LINUX_S_IFREG; perm_bits = (ip->rwmode & 2) ? 0755 : 0555; break;
}
st->st_mode = type_bits | perm_bits;
// Which device this node actually is. Left at zero until now, so every
// entry in /dev printed as "0, 0" under ls -l -- harmless while nothing
// read it, and wrong the moment something has to tell two disks apart.
if (ip->type == T_DEV)
st->st_rdev = linux_makedev(ip->major, ip->minor);
st->st_atime = ip->atime; st->st_mtime = ip->mtime; st->st_ctime = ip->ctime;
}
// -----------------------------------------------------------------------
// Internal helpers
// -----------------------------------------------------------------------
static int
argfd(int n, int *pfd, struct file **pf)
{
int fd;
struct file *f;
if (argint(n, &fd) < 0)
return -1;
if (fd < 0 || fd >= NOFILE || !(f = myproc()->ofile[fd]))
return -1;
if (pfd) *pfd = fd;
if (pf) *pf = f;
return 0;
}
// Not static: sys_net.c hands out descriptors for sockets and needs the same
// allocator, so that a socket fd is indistinguishable from any other.
int
fdalloc(struct file *f)
{
for (int fd = 0; fd < NOFILE; fd++) {
if (!myproc()->ofile[fd]) {
myproc()->ofile[fd] = f;
myproc()->ofile_flags[fd] = 0;
return fd;
}
}
return -1;
}
// Resolves a dirfd (AT_FDCWD or a real fd) to the inode a relative path
// should be resolved from. 0 on an invalid dirfd.
static struct inode *
at_dir(int dirfd)
{
if (dirfd == AT_FDCWD) return myproc()->cwd;
if (dirfd < 0 || dirfd >= NOFILE || !myproc()->ofile[dirfd]) return 0;
struct file *f = myproc()->ofile[dirfd];
if (f->type != FD_INODE) return 0;
return f->ip;
}
// Resolves an at-path: absolute paths always go from root; relative paths
// resolve from dirfd's inode (AT_FDCWD means myproc()->cwd, same as before —
// a real dirfd now actually works instead of being silently ignored or
// rejected).
static struct inode *
at_namei(int dirfd, char *path)
{
if (!path) return 0;
if (path[0] == '/') return namei(path);
struct inode *start = at_dir(dirfd);
if (!start) return 0;
return nameifrom(start, path);
}
// at_namei() that follows a final symlink as well, and says why it failed.
static struct inode *
at_nameif(int dirfd, char *path, int *err)
{
*err = ENOENT;
if (!path) return 0;
struct inode *start = (path[0] == '/') ? myproc()->cwd : at_dir(dirfd);
if (!start) { *err = EBADF; return 0; }
return nameifollow(start, path, err);
}
static struct inode *
at_nameiparent(int dirfd, char *path, char *name)
{
if (!path || !path[0]) return 0;
if (path[0] == '/') return nameiparent(path, name);
struct inode *start = at_dir(dirfd);
if (!start) return 0;
return nameiparentfrom(start, path, name);
}
// 1 if some component of `path` is longer than NAME_MAX, which is why a
// lookup through nameiparent() failed when it did. It reports no reason.
static int
path_toolong(const char *path)
{
int n = 0;
if (strnlen(path, VFS_PATH_MAX) >= VFS_PATH_MAX) return 1;
for (; *path; path++) {
n = (*path == '/') ? 0 : n + 1;
if (n > VFS_NAME_MAX) return 1;
}
return 0;
}
static struct inode*
createat_x(int dirfd, char *path, short type, short major, short minor,
int *err)
{
struct inode *ip, *dp;
char name[VFS_NAME_MAX + 1];
uint off;
if (!(dp = at_nameiparent(dirfd, path, name))) {
if (err && path && path_toolong(path)) *err = ENAMETOOLONG;
return 0;
}
ilock(dp);
// `err` lets the caller report why. Returning 0 alone made a refused
// write say "No such file or directory", which is what a shell prints for
// every create failure and tells the user nothing about why.
if (fs_readonly(dp->dev)) {
iunlockput(dp);
if (err) *err = EROFS;
return 0;
}
// A FAT volume has nowhere to put a device node, a symlink or a socket.
if (dp->ops->refuse_types & (1u << type)) {
iunlockput(dp);
if (err) *err = EPERM;
return 0;
}
if ((ip = dirlookup(dp, name, &off)) != 0) {
iunlockput(dp);
ilock(ip);
// The name already exists. When the caller wants an ordinary file
// (the open(O_CREAT) path), POSIX says to just open what is there --
// the existing object only has to not be a directory. This used to
// require the existing inode to be T_FILE *as well*, so opening a
// device node with O_CREAT failed and was reported as ENOENT: plain
// `> /dev/null` did not work, despite /dev/null existing.
// mkdir/mknod ask for T_DIR/T_DEV and still correctly fail here,
// which is what lets them report EEXIST.
// A symlink found here dangles: open() already followed every link
// that leads somewhere. Writing into it would rewrite its target.
if (type == T_FILE && ip->type != T_DIR && ip->type != T_SYMLINK)
return ip;
iunlockput(ip);
return 0;
}
// v1 and SchFS cannot fail either of these for a reason the caller could
// have avoided. FAT can: the volume fills up, the fixed root directory of
// FAT16 holds 512 names and no more, and a name may hold characters FAT
// does not allow. So a failure is undone and reported, not a panic.
if (!(ip = ialloc(dp->dev, type))) {
iunlockput(dp);
if (err) *err = ENOSPC;
return 0;
}
ilock(ip);
ip->major = major; ip->minor = minor; ip->nlink = 1;
ip->showmode = 1;
iupdate(ip);
if (type == T_DIR) {
dp->nlink++;
iupdate(dp);
build_dir_tree(ip, dp->inum);
}
int r;
if ((r = dirlink(dp, name, ip->inum)) < 0) {
if (type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp);
ip->nlink = 0;
iupdate(ip);
iunlockput(ip); // the last reference: ifree gives it back
if (err) *err = r == -EINVAL ? EINVAL : ENOSPC;
return 0;
}
iunlockput(dp);
return ip;
}
static struct inode*
create(char *path, short type, short major, short minor)
{
return createat_x(AT_FDCWD, path, type, major, minor, 0);
}
// -----------------------------------------------------------------------
// File descriptor ops
// -----------------------------------------------------------------------
addr_t sys_dup(void)
{
struct file *f;
int fd;
if (argfd(0, 0, &f) < 0 || (fd = fdalloc(f)) < 0) return -1;
filedup(f);
return fd;
}
addr_t sys_dup2(void)
{
int oldfd, newfd;
struct file *f;
if (argint(0, &oldfd) < 0 || argint(1, &newfd) < 0) return -1;
if (oldfd < 0 || oldfd >= NOFILE || !(f = myproc()->ofile[oldfd])) return -EBADF;
if (newfd < 0 || newfd >= NOFILE) return -EBADF;
if (oldfd == newfd) return newfd;
if (myproc()->ofile[newfd]) fileclose(myproc()->ofile[newfd]);
myproc()->ofile[newfd] = filedup(f);
myproc()->ofile_flags[newfd] = 0; // dup2 never carries FD_CLOEXEC over
return newfd;
}
addr_t sys_dup3(void)
{
struct file *f;
int oldfd, newfd;
argint(2, &(int){0}); // flags — ignored for now
if (argfd(0, &oldfd, &f) < 0 || argint(1, &newfd) < 0) return -EBADF;
if (newfd < 0 || newfd >= NOFILE) return -EBADF;
// Unlike dup2, equal fds are an error (dup3(2)). Without this check we
// would fileclose(f) and then filedup() the file we just released.
if (oldfd == newfd) return -EINVAL;
if (myproc()->ofile[newfd]) fileclose(myproc()->ofile[newfd]);
myproc()->ofile[newfd] = filedup(f);
myproc()->ofile_flags[newfd] = 0;
return newfd;
}
addr_t sys_close(void)
{
int fd;
struct file *f;
if (argfd(0, &fd, &f) < 0) return -EBADF;
myproc()->ofile[fd] = 0;
myproc()->ofile_flags[fd] = 0;
fileclose(f);
return 0;
}
addr_t
sys_fcntl(void)
{
int fd, cmd, arg;
if (argint(0, &fd) < 0 || argint(1, &cmd) < 0 || argint(2, &arg) < 0) return -1;
if (fd < 0 || fd >= NOFILE || !myproc()->ofile[fd]) return -1;
struct file *f = myproc()->ofile[fd];
switch (cmd) {
case F_DUPFD:
case F_DUPFD_CLOEXEC: {
if (arg < 0) return -EINVAL;
for (int nfd = arg; nfd < NOFILE; nfd++) {
if (!myproc()->ofile[nfd]) {
myproc()->ofile[nfd] = filedup(f);
myproc()->ofile_flags[nfd] = (cmd == F_DUPFD_CLOEXEC) ? FD_CLOEXEC : 0;
return nfd;
}
}
return -EMFILE;
}
case F_GETFD: return myproc()->ofile_flags[fd];
case F_SETFD: myproc()->ofile_flags[fd] = arg; return 0;
case F_GETFL: {
int flags = 0;
if (f->readable && f->writable) flags = O_RDWR;
else if (f->writable) flags = O_WRONLY;
// else O_RDONLY = 0
if (f->nonblock) flags |= O_NONBLOCK;
return flags;
}
// O_NONBLOCK is the only settable flag that means anything here, and it
// has to actually be stored: the usual sequence is F_GETFL then
// F_SETFL(flags | O_NONBLOCK), and a no-op F_SETFL turns that into a
// program that believes its reads will return immediately and then blocks
// on the first one. The rest (O_APPEND, O_ASYNC, O_DIRECT) are ignored.
case F_SETFL:
f->nonblock = (arg & O_NONBLOCK) != 0;
return 0;
}
return -EINVAL;
}
// -----------------------------------------------------------------------
// I/O
// -----------------------------------------------------------------------
addr_t sys_read(void)
{
struct file *f;
int n;
char *p;
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0) return -1;
return fileread(f, p, n);
}
addr_t sys_write(void)
{
struct file *f;
int n;
uint64 p;
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0) return -1;
return filewrite(f, (char*)p, n);
}
addr_t sys_readv(void)
{
struct file *f;
addr_t iov_addr;
int iovcnt, total = 0;
if (argfd(0, 0, &f) < 0) return -EBADF;
argaddr(1, &iov_addr); argint(2, &iovcnt);
if (iovcnt <= 0 || iovcnt > UIO_MAXIOV) return -EINVAL;
for (int i = 0; i < iovcnt; i++) {
struct iovec iov;
if (copy_from_user(&iov, iov_addr + i * sizeof(iov), sizeof(iov)) < 0)
return -EFAULT;
if (!iov.iov_len) continue;
char *buf = user_ptr((addr_t)iov.iov_base, iov.iov_len);
if (!buf) return -EFAULT;
int r = fileread(f, buf, (int)iov.iov_len);
if (r <= 0) return total > 0 ? total : r;
total += r;
}
return total;
}
addr_t sys_writev(void)
{
struct file *f;
addr_t iov_addr;
int iovcnt, total = 0;
if (argfd(0, 0, &f) < 0) return -EBADF;
argaddr(1, &iov_addr); argint(2, &iovcnt);
if (iovcnt <= 0 || iovcnt > UIO_MAXIOV) return -EINVAL;
for (int i = 0; i < iovcnt; i++) {
struct iovec iov;
if (copy_from_user(&iov, iov_addr + i * sizeof(iov), sizeof(iov)) < 0)
return -EFAULT;
if (!iov.iov_len) continue;
char *buf = user_ptr((addr_t)iov.iov_base, iov.iov_len);
if (!buf) return -EFAULT;
int r = filewrite(f, buf, (int)iov.iov_len);
if (r < 0) return total > 0 ? total : r;
total += r;
}
return total;
}
addr_t sys_lseek(void)
{
struct file *f;
addr_t off_raw;
int whence;
if (argfd(0, 0, &f) < 0 || argaddr(1, &off_raw) < 0 || argint(2, &whence) < 0) return (addr_t)-EFAULT;
if (f->type != FD_INODE && f->type != FD_MEMFD) return (addr_t)-ESPIPE;
int64 off = (int64)off_raw, newoff;
// SEEK_END needs the length, and a memfd's length is not in an inode.
int64 end = (f->type == FD_MEMFD) ? (int64)memfd_size(f->memfd_seg)
: (int64)f->ip->size;
switch (whence) {
case 0: newoff = off; break;
case 1: newoff = (int64)f->off + off; break;
case 2: newoff = end + off; break;
default: return (addr_t)-EINVAL;
}
if (newoff < 0) return (addr_t)-EINVAL;
// Seeking past the end is legal and is how a sparse file is made: the gap
// reads as zeros and costs nothing on a filesystem with extents. This used
// to clamp to end-of-file, which silently turned "seek to 1 GB and write"
// into "append", and any write that follows still gets validated by writei
// on a filesystem that cannot represent a hole.
f->off = (uint64)newoff;
return (addr_t)newoff;
}
// -----------------------------------------------------------------------
// Pipe
// -----------------------------------------------------------------------
addr_t sys_pipe(void)
{
int *fd;
struct file *rf, *wf;
int fd0 = -1, fd1;
if (argptr(0, (void*)&fd, 2*sizeof(fd[0])) < 0 || pipealloc(&rf, &wf) < 0) return -1;
if ((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0) {
if (fd0 >= 0) myproc()->ofile[fd0] = 0;
fileclose(rf); fileclose(wf);
return -1;
}
fd[0] = fd0; fd[1] = fd1;
return 0;
}
addr_t sys_pipe2(void)
{
addr_t fdarray;
struct file *rf, *wf;
int fd0, fd1;
if (argaddr(0, &fdarray) < 0) return -1;
int *fds = (int*)user_ptr(fdarray, 2 * sizeof(int));
if (!fds || pipealloc(&rf, &wf) < 0) return -1;
fd0 = fd1 = -1;
if ((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0) {
if (fd0 >= 0) myproc()->ofile[fd0] = 0;
fileclose(rf); fileclose(wf);
return -1;
}
fds[0] = fd0; fds[1] = fd1;
return 0;
}
// -----------------------------------------------------------------------
// Filesystem ops
// -----------------------------------------------------------------------
// The lookup for opening something that exists. A final symlink is followed,
// from the directory it is in, unless O_NOFOLLOW, which makes one an ELOOP the
// way POSIX has it (this kernel has no O_PATH to open the link itself).
// Returns the inode unlocked, or 0 with *err set.
static struct inode *
open_lookup(int dirfd, char *path, int omode, int *err)
{
if (!(omode & O_NOFOLLOW))
return at_nameif(dirfd, path, err);
struct inode *ip = at_namei(dirfd, path);
*err = ENOENT;
if (!ip) return 0;
ilock(ip);
if (ip->type == T_SYMLINK) { iunlockput(ip); *err = ELOOP; return 0; }
iunlock(ip);
return ip;
}
// open(O_CREAT) on a name that exists through a symlink opens the target, so
// `echo hi > link` writes where the link points. 0 if there is nothing there
// (or O_EXCL), and the caller creates.
static struct inode *
open_existing(int dirfd, char *path, int omode)
{
int err;
if (omode & (O_EXCL | O_NOFOLLOW)) return 0;
return at_nameif(dirfd, path, &err);
}
addr_t sys_open(void)
{
char *path;
int fd, omode;
struct file *f;
struct inode *ip;
if (argstr(0, &path) < 0 || argint(1, &omode) < 0) return -EFAULT;
begin_op();
if (omode & O_CREATE) {
// O_EXCL means "create it or fail", so the file existing is the error.
if (omode & O_EXCL) {
struct inode *ex = namei(path);
if (ex) { iput(ex); end_op(); return -EEXIST; }
}
int cerr = 0;
if ((ip = open_existing(AT_FDCWD, path, omode)) != 0) {
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
} else
ip = createat_x(AT_FDCWD, path,
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &cerr);
// A refusal because the filesystem is read-only is not "no such file".
// A shell prints the same sentence for every create failure, so
// returning ENOENT here made writing to a snapshot look like a missing
// directory.
if (!ip) { end_op(); return cerr ? -cerr : -ENOENT; }
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
itrunc(ip);
} else {
int err;
if (!(ip = open_lookup(AT_FDCWD, path, omode, &err))) { end_op(); return -err; }
ilock(ip);
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
if (ip->type == T_DIR && (omode & O_ACCMODE) != O_RDONLY) { iunlockput(ip); end_op(); return -EISDIR; }
if ((omode & O_TRUNC) && ip->type == T_FILE) itrunc(ip);
}
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
if (f) fileclose(f);
iunlockput(ip); end_op(); return -EMFILE;
}
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip;
f->off = (omode & O_APPEND) ? ip->size : 0;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
f->nonblock = (omode & O_NONBLOCK) != 0;
// Tell the device a descriptor was opened. /dev/input needs this to drop
// events that queued while nobody was listening; see inputopen().
if (ip->type == T_DEV && ip->major >= 0 && ip->major < NDEV &&
devsw[ip->major].open)
devsw[ip->major].open(ip);
return fd;
}
addr_t sys_openat(void)
{
int dirfd;
char *path;
int omode, fd;
struct file *f;
struct inode *ip;
argint(0, &dirfd); // AT_FDCWD = -100, or a real fd to resolve `path` from
if (argstr(1, &path) < 0 || argint(2, &omode) < 0) return -EFAULT;
begin_op();
if (omode & O_CREATE) {
// O_EXCL means "create it or fail", so the file existing is the error.
if (omode & O_EXCL) {
struct inode *ex = at_namei(dirfd, path);
if (ex) { iput(ex); end_op(); return -EEXIST; }
}
int cerr = 0;
if ((ip = open_existing(dirfd, path, omode)) != 0) {
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
} else if (!(ip = createat_x(dirfd, path, T_FILE, 0, 0, &cerr))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
} else {
int err;
if (!(ip = open_lookup(dirfd, path, omode, &err))) { end_op(); return -err; }
ilock(ip);
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
if (ip->type == T_DIR && (omode & O_ACCMODE) != O_RDONLY) { iunlockput(ip); end_op(); return -EISDIR; }
}
// O_TRUNC and O_APPEND used to be dropped on this path while sys_open
// honoured both, so the same shell redirection behaved differently
// depending on which entry point libc happened to call.
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
itrunc(ip);
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
if (f) fileclose(f);
iunlockput(ip); end_op(); return -EMFILE;
}
uint64 start = (omode & O_APPEND) ? ip->size : 0;
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip; f->off = start;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
f->nonblock = (omode & O_NONBLOCK) != 0;
// Tell the device a descriptor was opened. /dev/input needs this to drop
// events that queued while nobody was listening; see inputopen().
if (ip->type == T_DEV && ip->major >= 0 && ip->major < NDEV &&
devsw[ip->major].open)
devsw[ip->major].open(ip);
return fd;
}
addr_t sys_fstat(void)
{
struct file *f;
char *st;
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
if (argfd(0, 0, &f) < 0 || argptr(1, &st, size) < 0) return -EFAULT;
// A memfd has no inode, and its size is the one field with an answer --
// which is the field a caller is asking for when it fstat()s a buffer.
// This used to refuse anything that was not FD_INODE, so filestat()'s own
// memfd branch was unreachable from the syscall that needs it.
if (f->type == FD_MEMFD) {
uint64 sz = memfd_size(f->memfd_seg);
if (myproc()->syscall_map) {
struct linux_stat *ls = (struct linux_stat *)st;
memset(ls, 0, sizeof(*ls));
ls->st_mode = LINUX_S_IFREG | 0600;
ls->st_nlink = 1;
ls->st_size = (int64)sz;
} else {
struct stat *ss = (struct stat *)st;
memset(ss, 0, sizeof(*ss));
ss->type = T_FILE;
ss->nlink = 1;
ss->size = sz;
}
return 0;
}
if (f->type != FD_INODE) return -EINVAL;
ilock(f->ip);
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
else stati(f->ip, (struct stat*)st);
iunlock(f->ip);
return 0;
}
// stat/lstat differ in exactly one thing: whether a final symlink is followed.
// They used to be the same function, which was invisible while nothing
// followed symlinks anywhere and becomes wrong the moment something does.
static addr_t
do_stat(int dirfd, char *path, char *st, int follow)
{
struct inode *ip;
int err = ENOENT;
begin_op();
if (!(ip = follow ? at_nameif(dirfd, path, &err) : at_namei(dirfd, path))) {
end_op();
return -err;
}
ilock(ip);
if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st);
else stati(ip, (struct stat*)st);
iunlockput(ip);
end_op();
return 0;
}
addr_t sys_stat(void)
{
char *path, *st;
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
if (argstr(0, &path) < 0) return -EFAULT;
if (argptr(1, &st, size) < 0) return -EFAULT;
return do_stat(AT_FDCWD, path, st, 1);
}
addr_t sys_lstat(void)
{
char *path, *st;
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
if (argstr(0, &path) < 0) return -EFAULT;
if (argptr(1, &st, size) < 0) return -EFAULT;
return do_stat(AT_FDCWD, path, st, 0);
}
addr_t sys_access(void)
{
char *path;
if (argstr(0, &path) < 0) return -1;
int err;
begin_op();
struct inode *ip = at_nameif(AT_FDCWD, path, &err);
if (!ip) { end_op(); return -err; }
iput(ip); end_op();
return 0;
}
addr_t sys_chdir(void)
{
char *path;
struct inode *ip;
begin_op();
if (argstr(0, &path) < 0) { end_op(); return -EFAULT; }
int err;
if (!(ip = at_nameif(AT_FDCWD, path, &err))) { end_op(); return -err; }
ilock(ip);
// ENOTDIR, not a bare -1. `cd` into a file must fail, and it did, but the
// shell reported "Operation not permitted" -- which reads like a
// permissions problem and sends you looking in entirely the wrong place.
if (ip->type != T_DIR) { iunlockput(ip); end_op(); return -ENOTDIR; }
iunlock(ip); iput(myproc()->cwd); end_op();
myproc()->cwd = ip;
if (path[0] == '/') {
memmove(myproc()->cwd_path, path, strnlen(path, 255));
myproc()->cwd_path[255] = 0;
} else if (strncmp(path, "..", 3) == 0) {
int n = strnlen(myproc()->cwd_path, 255);
if (n > 1) { while (n > 0 && myproc()->cwd_path[n-1] != '/') n--; if (n > 1) n--; myproc()->cwd_path[n] = 0; }
} else if (strncmp(path, ".", 2) != 0) {
int n = strnlen(myproc()->cwd_path, 255), pn = strnlen(path, 255);
if (n > 1 && n + 1 + pn < 255) { myproc()->cwd_path[n] = '/'; memmove(myproc()->cwd_path + n + 1, path, pn + 1); }
else if (n == 1 && 1 + pn < 255) memmove(myproc()->cwd_path + 1, path, pn + 1);
}
return 0;
}
addr_t sys_fchdir(void)
{
struct file *f;
if (argfd(0, 0, &f) < 0 || f->type != FD_INODE) return -1;
ilock(f->ip);
if (f->ip->type != T_DIR) { iunlock(f->ip); return -1; }
iunlock(f->ip);
iput(myproc()->cwd); idup(f->ip); myproc()->cwd = f->ip;
myproc()->cwd_path[0] = '/'; myproc()->cwd_path[1] = 0;
return 0;
}
addr_t sys_getcwd(void)
{
addr_t ubuf;
int size;
argaddr(0, &ubuf); argint(1, &size);
if (size <= 0) return -EINVAL;
int n = strnlen(myproc()->cwd_path, 255) + 1;
if (n > size) return -ERANGE;
if (copy_to_user(ubuf, myproc()->cwd_path, n) < 0) return -EFAULT;
// Real Linux getcwd(2) returns the length written (including the NUL),
// not the buffer address — musl's getcwd() wrapper checks buf[0]=='/'
// AND treats a 0 return as failure, but callers relying on the actual
// byte count (or the raw syscall() return value directly) need this.
return n;
}
addr_t sys_mkdir(void)
{
char *path;
struct inode *ip;
if (argstr(0, &path) < 0) return -EFAULT;
begin_op();
if ((ip = namei(path)) != 0) { // already there: mkdir -p relies on this
iput(ip);
end_op();
return -EEXIST;
}
int cerr = 0;
if (!(ip = createat_x(AT_FDCWD, path, T_DIR, 0, 0, &cerr))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
iunlockput(ip); end_op();
return 0;
}
// What a Linux mknod(mode, dev) asks for, as a type here. Character and block
// devices are both T_DEV (the major says which driver); a regular file is what
// mknod(S_IFREG) has always meant. Named pipes and sockets have no on-disk
// form here yet, and Linux's answer for a filesystem that cannot hold one is
// EPERM. Returns the type, or a negative errno.
static int
mknod_type(uint32 mode)
{
switch (mode & LINUX_S_IFMT) {
case 0:
case LINUX_S_IFREG: return T_FILE;
case LINUX_S_IFCHR:
case LINUX_S_IFBLK: return T_DEV;
default: return -EPERM;
}
}
static addr_t
do_mknod(int dirfd, char *path, uint32 mode, uint64 dev)
{
int type = mknod_type(mode);
if (type < 0) return type;
// Linux's dev_t: 12 bits of major in 8..19 and the rest from bit 32, the
// minor's low byte in 0..7 and the rest from bit 20. The other half of
// linux_makedev().
int major = (int)(((dev >> 8) & 0xfff) | ((dev >> 32) & ~0xfffull));
int minor = (int)((dev & 0xff) | ((dev >> 12) & ~0xffull));
if (type == T_FILE) major = minor = 0;
begin_op();
// createat_x() opens an existing file when asked for one, as open(O_CREAT)
// wants. mknod() must not.
struct inode *ex = at_namei(dirfd, path);
if (ex) { iput(ex); end_op(); return -EEXIST; }
int cerr = 0;
struct inode *ip = createat_x(dirfd, path, type, (short)major,
(short)minor, &cerr);
if (!ip) { end_op(); return cerr ? -cerr : -EEXIST; }
iunlockput(ip); end_op();
return 0;
}
// The native call is mknod(path, major, minor); a Linux program's is
// mknod(path, mode, dev), on the same slot through the syscall map. Reading
// the Linux one as the native one made `mknod n c 1 2` a node numbered from
// the mode bits: 432, 258.
addr_t sys_mknod(void)
{
struct inode *ip;
char *path;
int major, minor;
if (argstr(0, &path) < 0 || argint(1, &major) < 0 || argint(2, &minor) < 0)
return -EFAULT;
if (myproc()->syscall_map) {
uint64 dev;
argaddr(2, &dev);
return do_mknod(AT_FDCWD, path, (uint32)major, dev);
}
begin_op();
if (!(ip = create(path, T_DEV, major, minor))) { end_op(); return -1; }
iunlockput(ip); end_op();
return 0;
}
// link() and rename() answer with real errno values now, and it is worth saying
// why that needed doing.
//
// Every failure in these used to be `return -1`, and -1 is -EPERM by the time
// it reaches a program. So linking a file that does not exist reported
// "operation not permitted" rather than "no such file" -- not a cosmetic
// difference, because a caller distinguishes the two and acts on it.
//
// zsh is how it surfaced. Its history locking links a lockfile and its history
// save renames a temporary over the real file, and on a machine with no
// history yet both legitimately fail the first time:
//
// zsh: locking failed for /root/.zsh_history: operation not permitted
// zsh: failed to write history file /root/.zsh_history.new: operation not
// permitted
//
// -- neither of which is what happened. About ninety other `return -1`s remain
// in this file. These are corrected because these were the ones found, and
// renameat() is corrected alongside rename() because musl's rename() calls it.
addr_t sys_link(void)
{
char name[VFS_NAME_MAX + 1], *new, *old;
struct inode *dp, *ip;
int err = EPERM;
if (argstr(0, &old) < 0 || argstr(1, &new) < 0) return -EFAULT;
begin_op();
if (!(ip = namei(old))) { end_op(); return -ENOENT; }
ilock(ip);
// The one place EPERM is right: POSIX names it for a directory.
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EPERM; }
ip->nlink++; iupdate(ip); iunlock(ip);
if (!(dp = nameiparent(new, name))) { err = ENOENT; goto bad_link; }
ilock(dp);
if (dp->dev != ip->dev) { iunlockput(dp); err = EXDEV; goto bad_link; }
if (dp->ops->no_hardlinks) { iunlockput(dp); err = EPERM; goto bad_link; }
if (dirlink(dp, name, ip->inum) < 0) { iunlockput(dp); err = EEXIST; goto bad_link; }
iunlockput(dp); iput(ip); end_op();
return 0;
bad_link:
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op();
return -err;
}
// Same correction as link() and rename() above, and in the same path.
//
// **ENOENT is the one that mattered.** A program that removes a file it is not
// sure exists writes `if (unlink(p) < 0 && errno != ENOENT) complain()`, which
// is the standard shape -- and against -1/EPERM every such test took the
// complaining branch. zsh saving its history is one of them, and it went on
// reporting a failure after the history had in fact been written.
// A socket node, for bind(2).
//
// create() is static because everything that creates a file is in this file;
// AF_UNIX bind is the one exception, and it gets a named door rather than
// create() being exported -- the difference matters, because `create` with an
// arbitrary type is a much bigger thing to hand out than "make me a socket".
//
// Returns a locked inode, like create() does.
struct inode *
create_sock(char *path)
{
return create(path, T_SOCK, 0, 0);
}
// The body of unlink(2), by path. Factored out so that a failed bind can take
// its own node back out without going through the syscall entry, and so there
// is one implementation of what unlink means rather than two.
//
// The caller brackets this with begin_op()/end_op(), the same as sys_unlink.
int
unlink_path(char *path)
{
struct inode *ip, *dp;
char name[VFS_NAME_MAX + 1];
uint off;
int err = EPERM;
if (!(dp = nameiparent(path, name))) return -ENOENT;
ilock(dp);
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) { err = EINVAL; goto bad; }
if (!(ip = dirlookup(dp, name, &off))) { err = ENOENT; goto bad; }
ilock(ip);
if (ip->nlink < 1) panic("unlink: nlink < 1");
if (ip->type == T_DIR && dirempty(ip) != 1) { iunlockput(ip); err = ENOTEMPTY; goto bad; }
if (dirunlink(dp, name, off) < 0) { iunlockput(ip); err = EIO; goto bad; }
if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip);
return 0;
bad:
iunlockput(dp);
return -err;
}
addr_t sys_unlink(void)
{
char *path;
int r;
if (argstr(0, &path) < 0) return -EFAULT;
begin_op();
r = unlink_path(path);
end_op();
return r;
}
addr_t sys_exec(void)
{
char *path;
addr_t uargv, uenvp;
if (argstr(0, &path) < 0 || argaddr(1, &uargv) < 0) return -EFAULT;
// The arrays are read where they are, by exec_user(), rather than copied
// into fixed arrays here: there used to be room for 32 arguments and 64
// variables, and `rm *` in a directory of 40 files failed with E2BIG.
//
// envp is Linux's third argument, which musl had been passing all along
// while nothing read it. A null envp is legal and means "none"; it is also
// what the first-party exec() wrapper passes.
if (argaddr(2, &uenvp) < 0)
uenvp = 0;
return exec_user(path, uargv, uenvp);
}
// ftruncate/truncate both grow and shrink. Growing used to be quietly dropped
// here; itrunc_to now takes it, and each filesystem decides what it can do --
// SchFS extends into a hole, v1 refuses because it has no way to represent one.
addr_t sys_ftruncate(void)
{
struct file *f;
addr_t length;
if (argfd(0, 0, &f) < 0 || argaddr(1, &length) < 0) return -1;
if ((int64)length < 0) return -EINVAL;
// A memfd starts at size zero and is given its size here. That is the
// whole lifecycle a Wayland client performs: memfd_create, ftruncate,
// mmap.
if (f->type == FD_MEMFD)
return f->writable ? memfd_truncate(f->memfd_seg, (uint64)length)
: -EBADF;
if (f->type != FD_INODE || !f->writable) return -EINVAL;
begin_op(); ilock(f->ip);
itrunc_to(f->ip, (uint64)length);
iunlock(f->ip); end_op();
return 0;
}
addr_t sys_truncate(void)
{
char *path;
addr_t length;
struct inode *ip;
if (argstr(0, &path) < 0 || argaddr(1, &length) < 0) return -1;
if ((int64)length < 0) return -EINVAL;
int err;
begin_op();
if (!(ip = at_nameif(AT_FDCWD, path, &err))) { end_op(); return -err; }
ilock(ip);
itrunc_to(ip, (uint64)length);
iunlockput(ip); end_op();
return 0;
}
// rename(2).
//
// Two things were wrong here and only one of them showed.
//
// **The old name was removed by hand.** `memset(&de, 0, ...)` then a writei()
// straight into the parent directory's data at the offset dirlookup() gave
// back. That is the v1 filesystem's on-disk layout -- an array of dirents --
// written directly, bypassing dp->ops. On SchFS a directory is a B-tree with
// an HTree index over it, so zeroing "the record at that offset" wrote into a
// structure that does not work that way and left the name in the index. Every
// other caller in this file uses dirunlink(); this one had been open-coding a
// filesystem it no longer ran on.
//
// **And an existing destination was refused.** POSIX says rename() replaces
// it atomically, and the one program everybody notices this with is a shell
// saving history: zsh writes $HISTFILE.new and renames it over $HISTFILE, so
// a rename that cannot replace leaves a .zsh_history.new behind after every
// session and never updates the history at all.
//
// The lock discipline is the delicate part, and it is inherited rather than
// invented: dp_old and dp_new are frequently the *same* directory reached
// through two nameiparent() calls, which is two references to one inode. So
// no two directory locks are ever held at once -- each is locked, used, and
// unlocked before the next -- and `ip` is unlocked throughout, held alive by
// the nlink bump instead.
addr_t sys_rename(void)
{
char *oldpath, *newpath;
struct inode *ip, *dp_old, *dp_new, *ip_src, *ip_dst;
char oldname[VFS_NAME_MAX + 1], newname[VFS_NAME_MAX + 1];
uint off, dstoff;
int err = EPERM;
if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -EFAULT;
begin_op();
if (!(ip = namei(oldpath))) { end_op(); return -ENOENT; }
ilock(ip);
// Renaming a directory needs the ".." fixup and the loop check that are
// not written here. EISDIR names what is missing; EPERM did not.
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
ip->nlink++; iupdate(ip); iunlock(ip);
dp_old = nameiparent(oldpath, oldname);
dp_new = nameiparent(newpath, newname);
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
err = (!dp_old || !dp_new) ? ENOENT : EXDEV;
if (dp_old) iput(dp_old);
if (dp_new) iput(dp_new);
goto bad_rename;
}
// Clear the destination, if anything is there.
ilock(dp_new);
if ((ip_dst = dirlookup(dp_new, newname, &dstoff)) != 0) {
// Both names already reach the same file. POSIX: succeed and do
// nothing -- which includes not unlinking either name, since removing
// one and adding it back is only equivalent when nothing else is
// watching.
if (ip_dst->inum == ip->inum) {
iput(ip_dst); iunlockput(dp_new); iput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op(); return 0;
}
ilock(ip_dst);
// Replacing a directory would need it to be empty and would need the
// parent's link count fixed up. Refusing is honest; the directory case
// of rename is unimplemented as a whole.
if (ip_dst->type == T_DIR) {
iunlockput(ip_dst); iunlockput(dp_new); iput(dp_old);
err = EISDIR; goto bad_rename;
}
if (dirunlink(dp_new, newname, dstoff) < 0) {
iunlockput(ip_dst); iunlockput(dp_new); iput(dp_old);
err = EIO; goto bad_rename;
}
ip_dst->nlink--; iupdate(ip_dst); iunlockput(ip_dst);
}
if (dirlink(dp_new, newname, ip->inum) < 0) {
iunlockput(dp_new); iput(dp_old); err = EEXIST; goto bad_rename;
}
iunlockput(dp_new);
ilock(dp_old);
if (!(ip_src = dirlookup(dp_old, oldname, &off))) {
iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op(); return -ENOENT;
}
iput(ip_src);
// Through the filesystem, not through writei(). See the note above.
if (dirunlink(dp_old, oldname, off) < 0) {
iunlockput(dp_old); err = EIO; goto bad_rename;
}
iunlockput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op(); return 0;
bad_rename:
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -err;
}
// symlink(2).
//
// The errno matters more here than anywhere else in this file, because the one
// widespread use of symlink() is as an atomic lock: create a link, and if it
// already exists someone else holds the lock. zsh's history locking is exactly
// that, and it distinguishes EEXIST ("contended, wait and retry") from
// anything else ("this cannot work, give up"). Returning a bare -1 -- which is
// -EPERM -- put every zsh on the machine in the second bucket, and printed
//
// zsh: locking failed for /root/.zsh_history: operation not permitted
//
// on every boot, on a filesystem that supports symlinks perfectly well.
static addr_t
do_symlink(int newdirfd, char *target, char *linkpath)
{
struct inode *ip;
int len;
if (*target == 0)
return -ENOENT; // an empty target resolves to nothing
begin_op();
// Distinguishing the two ways createat() returns 0 -- the name is taken,
// or the path to it does not exist -- has to be done before the create,
// because createat() does not say which.
if ((ip = at_namei(newdirfd, linkpath)) != 0) {
iput(ip);
end_op();
return -EEXIST;
}
int cerr = 0;
if (!(ip = createat_x(newdirfd, linkpath, T_SYMLINK, 0, 0, &cerr))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
len = strlen(target);
if (writei(ip, 0, (uint64)target, 0, len) != len) {
iunlockput(ip);
end_op();
return -EIO;
}
iunlockput(ip);
end_op();
return 0;
}
addr_t sys_symlink(void)
{
char *target, *linkpath;
if (argstr(0, &target) < 0 || argstr(1, &linkpath) < 0) return -EFAULT;
return do_symlink(AT_FDCWD, target, linkpath);
}
addr_t sys_readlink(void)
{
char *path, *buf;
int bufsiz;
if (argstr(0, &path) < 0) return -EFAULT;
if (argint(2, &bufsiz) < 0 || bufsiz <= 0) return -EINVAL;
if (argptr(1, &buf, bufsiz) < 0) return -EFAULT;
struct inode *ip;
if (!(ip = namei(path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
}
// -----------------------------------------------------------------------
// getdents64 — Linux directory reading
// -----------------------------------------------------------------------
addr_t
sys_getdents64(void)
{
struct file *f;
addr_t ubuf;
int count;
if (argfd(0, 0, &f) < 0 || argaddr(1, &ubuf) < 0 || argint(2, &count) < 0)
return -1;
if (f->type != FD_INODE) return -EBADF;
if (count <= 0) return -EINVAL;
// Validate the whole output window once; records are then written through
// the raw user VA, which is safe across page boundaries.
char *out = (char*)user_ptr(ubuf, (uint64)count);
if (!out) return -EFAULT;
struct inode *ip = f->ip;
ilock(ip);
if (ip->type != T_DIR) { iunlock(ip); return -ENOTDIR; }
// How a directory is laid out is the filesystem's business, and the two
// here could hardly differ more: v1 is a hash table of fixed 16-byte slots
// capped at 14-character names, SchFS is a range of B-tree keys holding
// names up to 255 bytes. Both produce the same record stream, and f->off is
// whatever position each one needs in order to resume.
int written = getdents(ip, out, count, &f->off);
iunlock(ip);
return written < 0 ? -EIO : written;
}
// -----------------------------------------------------------------------
// Custom SchOSFS ops
// -----------------------------------------------------------------------
addr_t sys_delete(void)
{
struct inode *ip, *dp, *ip2;
char name[VFS_NAME_MAX + 1], *path;
uint off;
if (argstr(0, &path) < 0) return -1;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip);
if (ip->showmode == 1) {
ip->showmode = 0; iupdate(ip); iunlockput(ip); end_op();
cprintf("moved to recycle bin\n");
return 0;
}
iunlockput(ip);
if (!(dp = nameiparent(path, name))) { end_op(); return -1; }
ilock(dp);
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) { iunlockput(dp); end_op(); return -1; }
if (!(ip2 = dirlookup(dp, name, &off))) { iunlockput(dp); end_op(); return -1; }
ilock(ip2);
if (ip2->nlink < 1) panic("delete: nlink < 1");
if (ip2->type == T_DIR && dirempty(ip2) != 1) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
if (dirunlink(dp, name, off) < 0) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
if (ip2->type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp); ip2->nlink--; iupdate(ip2); iunlockput(ip2);
end_op(); cprintf("deleted forever\n");
return 0;
}
addr_t sys_show(void)
{
char *path;
struct inode *ip;
if (argstr(0, &path) < 0) return -1;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip); ip->showmode = 1; iupdate(ip); iunlockput(ip);
end_op(); return 0;
}
addr_t sys_showstat(void)
{
char *path;
struct stat *st;
struct inode *ip;
if (argstr(0, &path) < 0 || argptr(1, (void*)&st, sizeof(*st)) < 0) return -1;
if (!(ip = namei(path))) return -ENOENT;
ilock(ip); stati(ip, st); iunlockput(ip);
cprintf("\nfile type: ");
switch (st->type) {
case T_FILE: cprintf("T_FILE\n"); break;
case T_DIR: cprintf("T_DIR\n"); break;
case T_EXTENT: cprintf("T_EXTENT\n"); break;
case T_DEV: cprintf("T_DEVICE\n"); break;
default: cprintf("UNKNOWN\n"); break;
}
cprintf("disk device: %d\ninode: %d\nlinks: %d\nsize(B): %d\n",
st->dev, st->ino, st->nlink, st->size);
if (st->type == T_EXTENT) {
for (int ad = 0; st->addrs[ad] && ad < NDIRECT+1; ad++) {
cprintf("extent ptr: 0x%x len: %d\n",
(st->addrs[ad] & ~0xff) >> 8, st->addrs[ad] & 0xff);
}
}
return 0;
}
// -----------------------------------------------------------------------
// Device I/O
// -----------------------------------------------------------------------
addr_t sys_vga_sync(void)
{
addr_t addr;
if (argaddr(0, &addr) < 0) return -1;
return 0;
}
addr_t sys_getmouse(void)
{
struct mouse_data *m;
if (argptr(0, (void*)&m, sizeof(*m)) < 0) return -1;
m->x = get_mouse_x(); m->y = get_mouse_y(); m->buttons = get_mouse_buttons();
return 0;
}
addr_t sys_ioctl(void)
{
int fd;
addr_t request, argp;
struct file *f;
if (argfd(0, &fd, &f) < 0) return -1;
argaddr(1, &request); argaddr(2, &argp);
int is_dev = (f->type == FD_INODE && f->ip && f->ip->type == 3);
// FICLONE: make this file share another's data blocks. Linux's spelling,
// because `cp --reflink` and every other tool that knows about cloning
// already asks for it by this number -- inventing our own would mean
// nothing could use the feature without being patched.
//
// The argument is a *file descriptor*, not a pointer, which is why it is
// read before the argp-shaped handling below.
if (request == FICLONE) {
struct file *sf;
struct proc *pr = myproc();
int sfd = (int)argp;
if (f->type != FD_INODE || !f->ip || !f->writable)
return -EBADF;
if (sfd < 0 || sfd >= NOFILE || (sf = pr->ofile[sfd]) == 0)
return -EBADF;
if (sf->type != FD_INODE || !sf->ip || !sf->readable)
return -EBADF;
// Both sides must be on the same filesystem: a clone shares blocks, and
// blocks belong to one volume.
if (sf->ip->dev != f->ip->dev)
return -EXDEV;
if (!SCHFS_IS_VOL_DEV(f->ip->dev))
return -EOPNOTSUPP;
if (f->ip->type != T_FILE || sf->ip->type != T_FILE)
return -EINVAL;
int r = schfs_kreflink(f->ip->dev, sf->ip->inum, f->ip->inum);
if (r != 0)
return -EIO;
// The destination inode's size changed underneath the VFS's copy.
ilock(f->ip);
f->ip->size = sf->ip->size;
iunlock(f->ip);
return 0;
}
// /dev/fb0 (major 2) — geometry queries. This has to come before the
// catch-all below, which answers every non-tty ioctl with 0: a program
// asking FBIOGET_VSCREENINFO would have been told "success" and then
// read an untouched buffer, i.e. a 0x0 screen, which looks like a
// driver bug rather than a missing one.
if (is_dev && f->ip->major == 2)
return fb_ioctl((int)request, argp);
if (is_dev && f->ip->major == INPUT_MAJOR)
return input_ioctl(f->ip, (int)request, argp);
if (is_dev && f->ip->major == PCIUSER_MAJOR)
return pciuser_ioctl(request, argp);
if (is_dev && f->ip->major == NETUSER_MAJOR)
return netuser_ioctl(request, argp);
if (is_dev && f->ip->major == AUDIO_MAJOR)
return dsp_ioctl((int)request, argp);
int is_tty = (is_dev && f->ip->major == 1);
if (!is_tty) {
// -ENOTTY, not 0.
//
// This used to answer every unrecognised request with success, which
// is a lie with consequences: a program asking TIOCGWINSZ on a *pipe*
// was told "fine" and read back an untouched buffer, concluded it was
// talking to a terminal, and formatted for one. That is why
// `ls /bin | wc -l` reported 48 -- ls was printing five columns into
// a pipe. isatty() is built on exactly this call.
return -ENOTTY;
}
if (request == TCGETS) {
struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
if (!tp) return -EFAULT;
// The real thing, not a hardcoded snapshot: whatever was last set.
cons_get_termios(f->ip->minor, tp);
return 0;
}
if (request == TCSETS || request == TCSETSW || request == TCSETSF) {
struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
if (!tp) return -EFAULT;
cons_set_termios(f->ip->minor, tp);
return 0;
}
if (request == TIOCGWINSZ) {
struct winsize *ws = (struct winsize*)user_ptr(argp, sizeof(*ws));
if (!ws) return -EFAULT;
// The terminal's own grid, and nothing else.
//
// This used to answer 24x80 unconditionally -- the size of a terminal
// SchOS has never drawn -- and was then fixed to compute the answer
// live from gfx_cols()/gfx_rows(). That was a second source of truth,
// and it disagreed with the first: the grids are sized once at
// vt_init(), which runs before the display driver is found on the PCI
// bus, so they were 80x24 while this reported 160x50. Programs
// formatted for a screen the grid did not have -- `ls` split words
// across the wrap, text stopped two thirds of the way down, and zsh's
// cursor tracking was wrong on every line.
//
// One number, from the terminal being asked. They cannot disagree.
struct vt *v = vt_for_minor(f->ip->minor);
ws->ws_col = (uint16)v->cols;
ws->ws_row = (uint16)v->rows;
ws->ws_xpixel = (uint16)(v->cols * gfx_char_w());
ws->ws_ypixel = (uint16)(v->rows * gfx_char_h());
return 0;
}
// The console's size is a property of the framebuffer, so a request to
// change it cannot be honoured -- but it is not an error either: programs
// set it to propagate a size they already learned. Accept and ignore.
if (request == TIOCSWINSZ) return 0;
// TCFLSH discards buffered input, output, or both. Unlike every other
// request here the argument is the queue selector *by value*, not a
// pointer -- passing it to user_ptr would fault on a legitimate call.
//
// Output needs no action: the console writes straight to the framebuffer
// and has no queue to drop. Input does, and answering 0 without draining
// it -- which is what the catch-all below did -- is how a keystroke typed
// during a command survives the flush and lands in the next prompt.
if (request == TCFLSH) {
if (argp == TCIFLUSH || argp == TCIOFLUSH)
cons_flush_input(f->ip->minor);
return 0;
}
// Controlling-terminal foreground process group.
//
// TIOCGPGRP used to answer with myproc()->pid, which happened to agree with
// the old getpgrp() stub (also myproc()->pid). Once getpgrp() started
// reporting the *real* process group, the two disagreed permanently, and
// a shell's standard startup loop
// while (tcgetpgrp(tty) != getpgrp()) kill(0, SIGTTIN);
// spun forever. Both ends now read the same variable.
if (request == TIOCGPGRP) {
int pg = cons_fg_pgrp(f->ip->minor);
put_user(pg, argp);
return 0;
}
if (request == TIOCSPGRP) {
int pg;
if (copy_from_user(&pg, argp, sizeof(pg)) < 0)
return -EFAULT;
if (pg <= 0)
return -EINVAL;
cons_set_fg_pgrp(f->ip->minor, pg);
return 0;
}
// TIOCSCTTY was `return 0` -- it claimed to make this terminal the
// caller's controlling terminal and did nothing, because there was one
// terminal and nothing to choose. It records the choice now, which is what
// /dev/tty resolves through and what a session leader on tty3 needs so
// that its /dev/tty is tty3 and not whatever is on screen.
if (request == TIOCSCTTY) {
struct vt *v = vt_for_minor(f->ip->minor);
myproc()->ctty = vt_index(v) + 1;
v->sid = myproc()->sid;
return 0;
}
// --- virtual terminal control ---------------------------------------
//
// The argument is the terminal number by value for ACTIVATE and
// WAITACTIVE, and a pointer for GETSTATE and OPENQRY. Passing argp to
// user_ptr for the first two would fault on a legitimate call, which is
// the same shape of bug TCFLSH had.
if (request == VT_ACTIVATE)
return vt_switch((int)argp - 1) == 0 ? 0 : -EINVAL;
if (request == VT_WAITACTIVE) {
// Nothing to wait for: vt_switch repaints synchronously, so by the
// time VT_ACTIVATE has returned the terminal is already active.
// Answering success is honest here; blocking would be a lie about
// work that has already happened.
int want = (int)argp - 1;
if (want < 0 || want >= NVT) return -EINVAL;
return vt_active == want ? 0 : -EINVAL;
}
if (request == VT_GETSTATE) {
struct vt_stat *st = (struct vt_stat*)user_ptr(argp, sizeof(*st));
if (!st) return -EFAULT;
st->v_active = (uint16)(vt_active + 1);
st->v_signal = 0;
// Bit n set means terminal n is in use. Bit 0 is unused because
// terminals are numbered from 1.
st->v_state = 0;
for (int i = 0; i < NVT; i++)
if (vt_get(i)->opened > 0)
st->v_state |= (uint16)(1u << (i + 1));
return 0;
}
if (request == VT_OPENQRY) {
int *out = (int*)user_ptr(argp, sizeof(int));
if (!out) return -EFAULT;
for (int i = 0; i < NVT; i++)
if (vt_get(i)->opened == 0) { *out = i + 1; return 0; }
*out = -1;
return -ENFILE;
}
// VT_GETMODE/VT_SETMODE: who drives switching.
//
// These were accepted and ignored, which was a lie with a consequence --
// a program could ask for the chord and be told yes while the kernel kept
// taking it. VT_PROCESS now genuinely stops the kernel switching, so an
// application that wants Ctrl+Alt+Shift+F2 can have it.
if (request == VT_SETMODE) {
struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m));
if (!m) return -EFAULT;
if (m->mode != VT_AUTO && m->mode != VT_PROCESS) return -EINVAL;
vt_for_minor(f->ip->minor)->mode = m->mode;
return 0;
}
// Which chord the kernel switches on. SchOS's own ioctl -- see the long
// comment in include/kbd.h for why this has to be settable at all.
if (request == VT_SETCHORD) {
struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c));
if (!c) return -EFAULT;
// Only real modifier bits, and a base that leaves room for NVT keys
// below the release flag. A base of 0 with no modifiers is the
// documented way to turn the chord off, so it is not an error.
if (c->mods & ~(VTC_SHIFT | VTC_CTRL | VTC_ALT | VTC_RALT))
return -EINVAL;
if (c->mods && (c->base == 0 || c->base + NVT > 0x80))
return -EINVAL;
vt_chord_mods = c->mods;
vt_chord_base = c->base;
return 0;
}
if (request == VT_GETCHORD) {
struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c));
if (!c) return -EFAULT;
c->mods = (uint16)vt_chord_mods;
c->base = (uint16)vt_chord_base;
return 0;
}
// Which keyboard layout the console uses. One layout for the machine, not
// one per terminal: there is one keyboard, and a user who switches to
// tty3 has not changed which keys their fingers are on.
if (request == KB_SETLAYOUT) {
struct kb_layout *k = (struct kb_layout*)user_ptr(argp, sizeof(*k));
if (!k) return -EFAULT;
k->name[sizeof(k->name) - 1] = 0;
if (keymap_set(k->name) < 0)
return -EINVAL;
return 0;
}
if (request == KB_GETLAYOUT || request == KB_LISTLAYOUT) {
struct kb_layout *k = (struct kb_layout*)user_ptr(argp, sizeof(*k));
if (!k) return -EFAULT;
const struct keylayout *l;
if (request == KB_GETLAYOUT) {
l = keymap_active();
} else {
l = keymap_by_index(k->index);
if (!l) return -EINVAL; // the documented end of the list
}
safestrcpy(k->name, l->name, sizeof(k->name));
safestrcpy(k->desc, l->desc, sizeof(k->desc));
return 0;
}
if (request == VT_GETMODE) {
struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m));
if (!m) return -EFAULT;
memset(m, 0, sizeof(*m));
m->mode = (char)vt_for_minor(f->ip->minor)->mode;
return 0;
}
return 0;
}
// -----------------------------------------------------------------------
// POSIX filesystem extensions
// -----------------------------------------------------------------------
// (at_namei/at_nameiparent/at_dir are defined above, next to create()/
// createat() — every *at() syscall below uses them for dirfd resolution.)
// chmod / fchmod / chown / fchown / lchown — stubs (no permission model yet).
addr_t sys_chmod(void) { return 0; }
addr_t sys_fchmod(void) { return 0; }
addr_t sys_chown(void) { return 0; }
addr_t sys_fchown(void) { return 0; }
addr_t sys_lchown(void) { return 0; }
// rmdir: remove an empty directory (same as unlink for dirs).
addr_t sys_rmdir(void)
{
char *path;
if (argstr(0, &path) < 0) return -1;
// Reuse sys_unlink semantics — it already handles T_DIR + isdirempty check.
return sys_unlink();
}
// fsync / fdatasync / sync — stubs (log-based FS commits on every op).
// sync / fsync / fdatasync.
//
// These returned 0 without doing anything, which was defensible while every
// write reached the disk before its syscall returned. It is not defensible now:
// tree blocks are marked dirty in the buffer cache and pushed out at commit, so
// "flush what is outstanding" finally names a real action. An empty transaction
// drives the same barrier a real one does -- the dirty buffers go out and the
// device is told to commit them.
addr_t sys_sync(void)
{
schfs_ksync_all();
fat_ksync_all(0);
return 0;
}
addr_t sys_fsync(void)
{
struct file *f;
if (argfd(0, 0, &f) < 0) return -EBADF;
// Whole-volume rather than per-file: SchFS has no way to make one object
// durable on its own. A transaction group is the unit that commits, and
// this file's last write is in whichever group is open.
if (f->type == FD_INODE && f->ip && SCHFS_IS_VOL_DEV(f->ip->dev))
return schfs_ksync(f->ip->dev) == 0 ? 0 : -EIO;
// FAT has written everything by the time a write returns; what is left is
// the drive's own cache.
if (f->type == FD_INODE && f->ip && FAT_IS_DEV(f->ip->dev))
fat_ksync_all(0);
return 0;
}
// No separate data-only path: the inode record and the data it describes are
// records in the same tree and commit together. Reporting success for a weaker
// guarantee than was asked for would be the wrong way round; this is stronger.
addr_t sys_fdatasync(void){ return sys_fsync(); }
// pread: read from fd at given offset without changing file position.
addr_t sys_pread(void)
{
struct file *f;
int n;
char *p;
uint64 offset;
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 ||
argptr(1, &p, n) < 0 || argaddr(3, &offset) < 0) return -1;
if (f->type != FD_INODE) return -1;
ilock(f->ip);
int r = readi(f->ip, 0, (uint64)p, (uint)offset, n);
iunlock(f->ip);
return r;
}
// pwrite: write to fd at given offset without changing file position.
addr_t sys_pwrite(void)
{
struct file *f;
int n;
char *p;
uint64 offset;
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 ||
argptr(1, &p, n) < 0 || argaddr(3, &offset) < 0) return -1;
if (f->type != FD_INODE || !f->writable) return -1;
begin_op(); ilock(f->ip);
int r = writei(f->ip, 0, (uint64)p, (uint)offset, n);
iunlock(f->ip); end_op();
return r;
}
// utime / utimes / utimensat — timestamps not tracked; stubs.
addr_t sys_utime(void) { return 0; }
addr_t sys_utimes(void) { return 0; }
// utimensat / futimens.
//
// This was a stub returning 0, which was worse than not having it: toybox's
// touch calls utimensat first and creates the file only when that fails with
// ENOENT, so `touch newfile` succeeded and made nothing, on every filesystem.
#define UTIME_NOW ((1L << 30) - 1)
#define UTIME_OMIT ((1L << 30) - 2)
addr_t sys_utimensat(void)
{
int dirfd = 0, flags = 0;
addr_t upath = 0, utimes = 0;
struct inode *ip;
struct timespec ts[2];
argint(0, &dirfd);
argaddr(1, &upath);
argaddr(2, &utimes);
argint(3, &flags);
if (utimes && copyin(myproc()->pgdir, utimes, ts, sizeof(ts)) < 0)
return -EFAULT;
if (upath == 0) { // futimens(fd, times)
struct file *f;
if (argfd(0, 0, &f) < 0 || f->type != FD_INODE || !f->ip)
return -EBADF;
ip = idup(f->ip);
} else {
char *path;
if (argstr(1, &path) < 0) return -EFAULT;
int err = ENOENT;
begin_op();
ip = (flags & AT_SYMLINK_NOFOLLOW) ? at_namei(dirfd, path)
: at_nameif(dirfd, path, &err);
end_op();
if (!ip) return -err;
}
uint64 now = 0, us = 0;
wallclock(&now, &us);
begin_op();
ilock(ip);
if (fs_readonly(ip->dev)) { iunlockput(ip); end_op(); return -EROFS; }
for (int i = 0; i < 2; i++) {
long sec = utimes ? ts[i].tv_sec : (long)now;
if (utimes && ts[i].tv_nsec == UTIME_OMIT) continue;
if (utimes && ts[i].tv_nsec == UTIME_NOW) sec = (long)now;
if (i == 0) ip->atime = (uint)sec;
else ip->mtime = (uint)sec;
}
ip->ctime = (uint)now;
iupdate(ip);
iunlockput(ip);
end_op();
return 0;
}
// sendfile: not implemented.
addr_t sys_sendfile(void) { return -ENOSYS; }
// preadv2 / pwritev2: scatter/gather at offset — redirect to readv/writev.
addr_t sys_preadv2(void) { return sys_readv(); }
addr_t sys_pwritev2(void){ return sys_writev(); }
// fallocate: preallocate space — we don't track this; stub success.
addr_t sys_fallocate(void) { return 0; }
// statfs / fstatfs.
//
// These used to return the same six invented constants whatever they were
// asked about -- 150000 blocks, half of them free -- so df reported a
// filesystem that did not exist and reported it identically for every mount.
// The numbers are real now, and which ones you get depends on which filesystem
// the path landed on.
static void fill_statfs(struct statfs *sf, uint dev) {
uint64 total = 0, freeb = 0, objects = 0;
memset(sf, 0, sizeof(*sf));
sf->f_type = 0x534b4658; // 'SKFX'
if (schfs_kstatfs(dev, &total, &freeb, &objects) == 0) {
sf->f_bsize = SCHFS_BLKSIZE;
sf->f_frsize = SCHFS_BLKSIZE;
sf->f_blocks = (int64)total;
sf->f_bfree = (int64)freeb;
sf->f_bavail = (int64)freeb; // no reserve, so nothing held back
// Both zero, deliberately. There is no inode table to run out of and
// the object id space is 2^64, so there is no honest pair of numbers
// here -- and df computes a "% inodes used" from them, which against a
// real f_files and a zero f_ffree would show every SchFS mount as
// completely full. Zeroes are what filesystems that allocate inodes
// dynamically report, and what df is written to expect from them.
sf->f_files = 0;
sf->f_ffree = 0;
sf->f_namelen = SCHFS_NAME_MAX;
(void)objects;
return;
}
uint64 csize, clusters, freec;
if (fat_kstatfs(dev, &csize, &clusters, &freec) == 0) {
sf->f_type = 0x4d44; // MSDOS_SUPER_MAGIC, as Linux says
sf->f_bsize = (int64)csize;
sf->f_frsize = (int64)csize;
sf->f_blocks = (int64)clusters;
sf->f_bfree = (int64)freec;
sf->f_bavail = (int64)freec;
sf->f_namelen = 255;
return;
}
// v1, /proc, or anything else: the old shape, but at least with a block
// size that matches the one the buffer cache actually uses.
sf->f_bsize = BSIZE;
sf->f_frsize = BSIZE;
sf->f_namelen = DIRSIZ;
}
addr_t sys_statfs(void)
{
char *path;
addr_t bufp;
if (argstr(0, &path) < 0 || argaddr(1, &bufp) < 0) return -1;
struct statfs *sf = (struct statfs*)user_ptr(bufp, sizeof(*sf));
if (!sf) return -EFAULT;
// Resolve the path: which filesystem it lands on is the whole question,
// and mount crossing happens during resolution.
int err;
begin_op();
struct inode *ip = at_nameif(AT_FDCWD, path, &err);
if (!ip) { end_op(); return -err; }
uint dev = ip->dev;
iput(ip);
end_op();
fill_statfs(sf, dev);
return 0;
}
addr_t sys_fstatfs(void)
{
struct file *f;
addr_t bufp;
if (argfd(0, 0, &f) < 0 || argaddr(1, &bufp) < 0) return -1;
struct statfs *sf = (struct statfs*)user_ptr(bufp, sizeof(*sf));
if (!sf) return -EFAULT;
if (f->type != FD_INODE || !f->ip) return -EBADF;
fill_statfs(sf, f->ip->dev);
return 0;
}
// chroot: not supported.
addr_t sys_chroot(void) { return -EPERM; }
// -----------------------------------------------------------------------
// at-syscalls
// -----------------------------------------------------------------------
addr_t sys_mkdirat(void)
{
int dirfd;
char *path;
struct inode *ip;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -EFAULT;
begin_op();
if ((ip = at_namei(dirfd, path)) != 0) { // see sys_mkdir
iput(ip);
end_op();
return -EEXIST;
}
int cerr = 0;
if (!(ip = createat_x(dirfd, path, T_DIR, 0, 0, &cerr))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
iunlockput(ip); end_op();
return 0;
}
addr_t sys_mknodat(void)
{
int dirfd;
char *path;
uint64 dev;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
int mode;
argint(2, &mode);
argaddr(3, &dev);
return do_mknod(dirfd, path, (uint32)mode, dev);
}
addr_t sys_unlinkat(void)
{
int dirfd, flags;
char *path;
struct inode *ip, *dp;
char name[VFS_NAME_MAX + 1];
uint off;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &flags);
begin_op();
if (!(dp = at_nameiparent(dirfd, path, name))) { end_op(); return -1; }
ilock(dp);
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) {
iunlockput(dp); end_op(); return -1;
}
if (!(ip = dirlookup(dp, name, &off))) {
iunlockput(dp); end_op(); return -1;
}
ilock(ip);
if (ip->nlink < 1) panic("unlinkat: nlink < 1");
if (ip->type == T_DIR && !(flags & AT_REMOVEDIR)) {
iunlockput(ip); iunlockput(dp); end_op(); return -1;
}
if (ip->type == T_DIR && dirempty(ip) != 1) {
iunlockput(ip); iunlockput(dp); end_op(); return -1;
}
if (dirunlink(dp, name, off) < 0) {
iunlockput(ip); iunlockput(dp); end_op(); return -1;
}
if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op();
return 0;
}
addr_t sys_renameat(void)
{
int olddirfd, newdirfd;
char *oldpath, *newpath;
argint(0, &olddirfd); argstr(1, &oldpath);
argint(2, &newdirfd); argstr(3, &newpath);
// Delegate to sys_rename internals — fake the args for rename.
// Easiest: place path pointers in registers and call rename logic directly.
struct inode *ip, *dp_old, *dp_new, *ip_src;
char oldname[VFS_NAME_MAX + 1], newname[VFS_NAME_MAX + 1];
uint off;
struct dirent de;
begin_op();
if (!(ip = at_namei(olddirfd, oldpath))) { end_op(); return -ENOENT; }
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
ip->nlink++; iupdate(ip); iunlock(ip);
dp_old = at_nameiparent(olddirfd, oldpath, oldname);
dp_new = at_nameiparent(newdirfd, newpath, newname);
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
int e = (!dp_old || !dp_new) ? ENOENT : EXDEV;
if (dp_old) iput(dp_old);
if (dp_new) iput(dp_new);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -e;
}
ilock(dp_new);
if (dirlink(dp_new, newname, ip->inum) < 0) {
iunlockput(dp_new); iput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -EEXIST;
}
iunlockput(dp_new);
ilock(dp_old);
if (!(ip_src = dirlookup(dp_old, oldname, &off))) {
iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op(); return -ENOENT;
}
iput(ip_src);
memset(&de, 0, sizeof(de));
if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("renameat: writei");
iunlockput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op();
return 0;
}
addr_t sys_renameat2(void) { return sys_renameat(); } // ignore flags
addr_t sys_linkat(void)
{
int olddirfd, newdirfd;
char *old, *new;
struct inode *dp, *ip;
char name[VFS_NAME_MAX + 1];
argint(0, &olddirfd); argstr(1, &old);
argint(2, &newdirfd); argstr(3, &new);
argint(4, &(int){0}); // flags
begin_op();
if (!(ip = at_namei(olddirfd, old))) { end_op(); return -1; }
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
ip->nlink++; iupdate(ip); iunlock(ip);
if (!(dp = at_nameiparent(newdirfd, new, name))) goto bad_linkat;
ilock(dp);
if (dp->dev != ip->dev || dp->ops->no_hardlinks ||
dirlink(dp, name, ip->inum) < 0) {
iunlockput(dp); goto bad_linkat;
}
iunlockput(dp); iput(ip); end_op();
return 0;
bad_linkat:
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op();
return -1;
}
addr_t sys_symlinkat(void)
{
char *target, *linkpath;
int newdirfd;
if (argstr(0, &target) < 0) return -EFAULT;
argint(1, &newdirfd);
if (argstr(2, &linkpath) < 0) return -EFAULT;
return do_symlink(newdirfd, target, linkpath);
}
addr_t sys_readlinkat(void)
{
int dirfd;
char *path, *buf;
int bufsiz;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -EFAULT;
if (argint(3, &bufsiz) < 0 || bufsiz <= 0) return -EINVAL;
if (argptr(2, &buf, bufsiz) < 0) return -EFAULT;
struct inode *ip;
if (!(ip = at_namei(dirfd, path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
}
addr_t sys_fchmodat(void) { return 0; } // stub
addr_t sys_fchownat(void) { return 0; } // stub
addr_t sys_faccessat(void)
{
int dirfd;
char *path;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &(int){0}); // amode — ignored
// arg3 flags — ignored
int err;
begin_op();
struct inode *ip = at_nameif(dirfd, path, &err);
if (!ip) { end_op(); return -err; }
iput(ip); end_op();
return 0;
}
addr_t sys_fstatat(void)
{
int dirfd, flags;
char *path;
char *st;
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
if (argptr(2, &st, size) < 0) return -1;
argint(3, &flags);
// AT_EMPTY_PATH: if path is empty, fstat the dirfd itself.
if ((flags & AT_EMPTY_PATH) && path[0] == '\0') {
if (dirfd < 0 || dirfd >= NOFILE || !myproc()->ofile[dirfd]) return -1;
struct file *f = myproc()->ofile[dirfd];
if (f->type != FD_INODE) return -1;
ilock(f->ip);
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
else stati(f->ip, (struct stat*)st);
iunlock(f->ip);
return 0;
}
return do_stat(dirfd, path, st, !(flags & AT_SYMLINK_NOFOLLOW));
}
// -----------------------------------------------------------------------
// mount / umount2
//
// A mount here is a substitution made during path resolution: nothing on
// either disk records it, so mounting is adding a row to a table and
// unmounting is removing it. There is no filesystem probing to do at mount
// time either -- SchFS's uberblock scan already ran at boot, and this only
// decides where in the namespace the result appears.
// -----------------------------------------------------------------------
// The block device a path names: a node with major 8, whose minor is the
// device number. Returns it, or a negative errno.
static int
source_bdev(char *source)
{
begin_op();
struct inode *sp = namei(source);
if (!sp) { end_op(); return -ENOENT; }
ilock(sp);
int r = (sp->type == T_DEV && sp->major == 8) ? sp->minor : -ENOTBLK;
iunlockput(sp);
end_op();
return r;
}
// `mount -t vfat /dev/usb0p1 /mnt`. MS_RDONLY (flag 1, argument 3) mounts it
// read-only; nothing else in the flags or the data means anything to FAT yet.
static int
mount_fat(char *source, char *target)
{
int flags = 0;
argint(3, &flags);
if (!source || !source[0])
return -EINVAL;
int bdev = source_bdev(source);
if (bdev < 0)
return bdev;
begin_op();
struct inode *dp = namei(target);
if (!dp) { end_op(); return -ENOENT; }
ilock(dp);
if (dp->type != T_DIR) { iunlockput(dp); end_op(); return -ENOTDIR; }
uint dev = dp->dev;
uint64 inum = dp->inum;
iunlockput(dp);
end_op();
int fsdev = fat_kmount((uint)bdev, flags & 1);
if (fsdev < 0)
return fsdev;
if (mount_add(dev, inum, (uint)fsdev, FAT_ROOT_INO, target, source,
"vfat") != 0) {
fat_kumount((uint)fsdev);
return -EBUSY;
}
return 0;
}
addr_t sys_mount(void)
{
char *source, *target, *fstype;
if (argstr(0, &source) < 0 || argstr(1, &target) < 0 ||
argstr(2, &fstype) < 0)
return -EFAULT;
// proc: a synthetic filesystem, so there is no source device to check and
// no volume to have found at boot. `mount -t proc proc /proc`.
if (strncmp(fstype, "proc", 5) == 0) {
procfs_mount();
return 0;
}
// devtmpfs: the same, and the one init actually needs. /dev used to be
// twenty mknod() calls; see the note at the top of kernel/fs/devfs.c.
if (strncmp(fstype, "devtmpfs", 9) == 0 || strncmp(fstype, "dev", 4) == 0) {
devfs_mount();
return 0;
}
// sysfs: the hardware, as Linux's /sys lays it out. kernel/fs/sysfs.c.
if (strncmp(fstype, "sysfs", 6) == 0)
return sysfs_mount(target);
// FAT, under the three names Linux knows it by. vfat is what everything
// means today; msdos is the same driver without long names on Linux, and
// here simply the same driver.
if (strncmp(fstype, "vfat", 5) == 0 || strncmp(fstype, "msdos", 6) == 0 ||
strncmp(fstype, "fat", 4) == 0)
return mount_fat(source, target);
// Otherwise SchFS. Checking the name still matters: a caller that asked
// for something else should be told no rather than quietly handed this.
if (strncmp(fstype, "schfs", 6) != 0)
return -ENODEV;
// `mount -t schfs @name /mnt` mounts a snapshot, read-only.
//
// The source finally means something. It was ignored outright before --
// there was one volume and nothing to choose between -- and a snapshot is
// the first second thing to mount. `@` rather than a path because a
// snapshot is not a device: there is no node in /dev for it and there
// should not be, since it lives inside the volume that is already mounted.
int snapmount = 0;
uint snapdev = 0;
if (source && source[0] == '@') {
const char *nm = source + 1;
unsigned nlen = 0;
while (nm[nlen] && nlen < 64)
nlen++;
if (nlen == 0)
return -EINVAL;
// Distinct codes, because "resource busy" for every possible failure
// tells the caller nothing and cost an hour of guessing once already.
//
// Snapshots are taken of the root volume: a snapshot is named inside
// the volume that holds it, and there is no syntax yet for saying which
// volume that is.
int r = schfs_ksnap_mount(SCHFS_DEV, nm, nlen);
if (r < 0) {
switch (r) {
case -1: return -ENODEV; /* no volume mounted to look in */
case -2: return -EBUSY; /* every snapshot slot in use */
case -3: return -ENOMEM; /* no scratch blocks */
default: return -ENOENT; /* no snapshot by that name */
}
}
snapdev = (uint)r;
snapmount = 1;
}
if (!schfs_kmounted())
return -ENODEV;
// The source finally names a disk.
//
// It was ignored outright before -- there was one volume and nothing to
// choose between -- so `mount -t schfs /dev/hdb /mnt` mounted whatever the
// kernel had probed at boot and said nothing about it. Now an explicit
// source is resolved, and mounting a second SchFS disk works.
//
// An empty source, or the bare word "schfs", still means the root volume:
// that is what /etc/fstab and every existing script pass.
uint fsdev_src = SCHFS_DEV;
if (!snapmount && source && source[0] && strncmp(source, "schfs", 6) != 0) {
begin_op();
struct inode *sp = namei(source);
if (!sp) { end_op(); return -ENOENT; }
ilock(sp);
// A raw disk is a character device in the devsw table (major 8, see
// kernel/dev/cdev.c) whose minor is the IDE device number.
if (sp->type != T_DEV || sp->major != 8) {
iunlockput(sp); end_op();
return -ENOTBLK;
}
uint bdev = (uint)sp->minor;
iunlockput(sp);
end_op();
int r = schfs_kmount_dev(bdev);
if (r < 0) {
// -EBUSY also covers "already mounted", which is the common typo
// and deserves to be distinguishable from "no free slot" one day.
return r;
}
fsdev_src = (uint)r;
}
uint64 root = snapmount ? schfs_ksnap_root(snapdev) : schfs_kroot(fsdev_src);
if (root == 0) {
if (snapmount)
schfs_ksnap_unmount(snapdev);
return -ENODEV;
}
begin_op();
struct inode *dp = namei(target);
if (!dp) { end_op(); return -ENOENT; }
ilock(dp);
if (dp->type != T_DIR) { iunlockput(dp); end_op(); return -ENOTDIR; }
uint dev = dp->dev;
uint64 inum = dp->inum;
iunlockput(dp);
end_op();
// Mounting a filesystem onto a directory that is already on *it* would let
// an unmount strand the covered directory behind its own mount: umount
// names the mounted root, and a self-mount makes that ambiguous.
//
// This read `dev == SCHFS_DEV`, which was the same thing while SchFS was
// the only volume there could be. A snapshot is a second device, and its
// mount point is necessarily on the live root -- there is nowhere else to
// put it -- so the old spelling refused every snapshot mount.
uint fsdev = snapmount ? snapdev : fsdev_src;
if (dev == fsdev)
return -EBUSY;
if (mount_add(dev, inum, fsdev, root, target, source, "schfs") != 0) {
if (snapmount)
schfs_ksnap_unmount(snapdev);
else if (fsdev_src != SCHFS_DEV)
schfs_kumount(fsdev_src); /* undo the volume mount we just did */
return -EBUSY;
}
return 0;
}
addr_t sys_umount2(void)
{
char *target;
if (argstr(0, &target) < 0) return -EFAULT;
begin_op();
struct inode *dp = namei(target);
if (!dp) { end_op(); return -ENOENT; }
// Resolving the path already crossed the mount, so this is the mounted
// filesystem's root rather than the directory it covers. The table knows
// which mount point that root belongs to.
uint dev = dp->dev;
uint64 inum = dp->inum;
iput(dp);
end_op();
// Something still open or a working directory inside it -- the root
// included, which nothing holds once the namei above has let go: the
// volume cannot be let go of, since its inodes would outlive it. Linux
// says EBUSY here, and `umount` prints "target is busy".
if (FAT_IS_DEV(dev) && ibusy(dev, 0))
return -EBUSY;
if (mount_del_by_root(dev, inum) != 0)
return -EINVAL;
// Release the slot with the mount. Without this the table entry went away
// and the slot stayed taken, so a volume could be unmounted and never
// mounted again until reboot.
if (SCHFS_IS_SNAP_DEV(dev))
schfs_ksnap_unmount(dev);
else if (SCHFS_IS_VOL_DEV(dev) && dev != SCHFS_DEV)
// Pushes the volume's dirty buffers out and drives a cache flush before
// letting go. The root volume (SCHFS_DEV) refuses -- there is nothing
// to fall back to -- and mount_del_by_root has already failed for it,
// since nothing mounts the root over anything.
schfs_kumount(dev);
else if (FAT_IS_DEV(dev))
fat_kumount(dev);
return 0;
}
// -----------------------------------------------------------------------
// Attributes, and the index over them (SchFS phase 5)
//
// setxattr/getxattr/removexattr keep Linux's shape, so a program that already
// knows how to attach metadata to a file needs no changes. schfs_query is the
// one call with no Linux equivalent: it asks the *filesystem* a question
// instead of walking it, which is the whole point of building the index.
// -----------------------------------------------------------------------
// Resolve a path to a SchFS inode, and refuse anything that is not one.
// Attributes live in the tree, so there is nowhere to put them on v1 or /proc.
static int
xattr_inode(char *path, struct inode **ip)
{
begin_op();
struct inode *p = namei(path);
if (!p) { end_op(); return -ENOENT; }
if (!SCHFS_IS_VOL_DEV(p->dev)) { iput(p); end_op(); return -EOPNOTSUPP; }
end_op();
*ip = p;
return 0;
}
addr_t sys_setxattr(void)
{
char *path, *name;
addr_t uval;
int size, flags;
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
if (argaddr(2, &uval) < 0 || argint(3, &size) < 0) return -EFAULT;
argint(4, &flags); /* XATTR_CREATE/REPLACE: unused */
if (size < 0 || size > SCHFS_ATTR_VALUE_MAX) return -E2BIG;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
char val[SCHFS_ATTR_VALUE_MAX];
if (size > 0 && copy_from_user(val, uval, (uint64)size) < 0) return -EFAULT;
struct inode *ip;
int r = xattr_inode(path, &ip);
if (r < 0) return r;
// Stored as a string: nothing here knows the caller's intent, and a string
// is the type whose equality comparison is the one a query wants.
r = schfs_kattr_set(ip->dev, ip->inum, name, (unsigned)namelen,
val, (unsigned)size, SCHFS_ATTR_STRING);
iput(ip);
return r == 0 ? 0 : -EIO;
}
addr_t sys_getxattr(void)
{
char *path, *name;
addr_t uval;
int size;
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
if (argaddr(2, &uval) < 0 || argint(3, &size) < 0) return -EFAULT;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
struct inode *ip;
int r = xattr_inode(path, &ip);
if (r < 0) return r;
char val[SCHFS_ATTR_VALUE_MAX];
unsigned vallen = 0;
// A zero size means "how big is it?", which is how every caller sizes its
// buffer before asking again.
r = schfs_kattr_get(ip->dev, ip->inum, name, (unsigned)namelen,
size > 0 ? val : 0, (unsigned)(size > 0 ? size : 0),
&vallen);
iput(ip);
if (r == 0) return -ENODATA;
if (r == -3) return -ERANGE;
if (r != 1) return -EIO;
if (size == 0) return (addr_t)vallen;
if (copy_to_user(uval, val, vallen) < 0) return -EFAULT;
return (addr_t)vallen;
}
addr_t sys_removexattr(void)
{
char *path, *name;
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
struct inode *ip;
int r = xattr_inode(path, &ip);
if (r < 0) return r;
r = schfs_kattr_del(ip->dev, ip->inum, name, (unsigned)namelen);
iput(ip);
return r == 0 ? 0 : -EIO;
}
// Declare an index on an attribute name. Covers values written afterwards.
addr_t sys_schfs_index(void)
{
char *name;
if (argstr(0, &name) < 0) return -EFAULT;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
return schfs_kindex(SCHFS_DEV, name, (unsigned)namelen) == 0 ? 0 : -EIO;
}
// Every object whose `name` attribute equals `value`, as object ids.
//
// Object ids rather than paths: the index maps values to objects, and a
// filesystem keyed that way has no cheap way back to a name. Turning an id into
// a path is a caller's problem, and a rarer one than the query itself.
addr_t sys_schfs_query(void)
{
char *name, *value;
addr_t uout;
int cap;
if (argstr(0, &name) < 0 || argstr(1, &value) < 0) return -EFAULT;
if (argaddr(2, &uout) < 0 || argint(3, &cap) < 0) return -EFAULT;
if (cap <= 0 || cap > 256) return -EINVAL;
int namelen = strlen(name), vallen = strlen(value);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
if (vallen > SCHFS_ATTR_VALUE_MAX) return -E2BIG;
uint64 ids[64];
if (cap > (int)(sizeof(ids) / sizeof(ids[0])))
cap = (int)(sizeof(ids) / sizeof(ids[0]));
int n = schfs_kquery(SCHFS_DEV, name, (unsigned)namelen,
value, (unsigned)vallen, ids, cap);
if (n < 0) return -ENOENT; /* no such index */
if (n > 0 && copy_to_user(uout, ids, (uint64)n * sizeof(uint64)) < 0)
return -EFAULT;
return (addr_t)n;
}