SchOS/kernel/sys/sys_fs.c
Nexoniarz a1c965da1e include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

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

2471 lines
88 KiB
C

#include "core/types.h"
#include "core/defs.h"
#include "core/param.h"
#include "uapi/stat.h"
#include "arch/mmu.h"
#include "arch/memlayout.h"
#include "core/proc.h"
#include "fs/fs.h"
#include "core/spinlock.h"
#include "core/sleeplock.h"
#include "fs/file.h"
#include "uapi/fcntl.h"
#include "uapi/posix.h"
#include "core/syscall.h"
#include "core/uaccess.h"
#include "uapi/errno.h"
#include "fs/schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
#include "gfx/gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */
#include "dev/vt.h" /* the terminals the TIOC and VT_ ioctls act on */
#include "dev/pciuser.h"
#include "dev/keymap.h" /* KB_*LAYOUT — the keyboard layout catalogue */
#include "net/netuser.h"
#include "fs/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/dev/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;
}