#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" // ----------------------------------------------------------------------- // 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 // 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; 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); } 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); } static struct inode* createat_x(int dirfd, char *path, short type, short major, short minor, int *rofs) { struct inode *ip, *dp; char name[DIRSIZ]; uint off; if (!(dp = at_nameiparent(dirfd, path, name))) return 0; ilock(dp); // Nothing below this point tolerates a refusal: ialloc and dirlink both // panic on failure, because on a writable filesystem they cannot fail for // any reason the caller could have avoided. // // The flag lets the caller report EROFS. 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 (rofs) *rofs = 1; 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. if (type == T_FILE && ip->type != T_DIR) return ip; iunlockput(ip); return 0; } if (!(ip = ialloc(dp->dev, type))) panic("create: ialloc"); 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); } if (dirlink(dp, name, ip->inum) < 0) panic("create: dirlink"); iunlockput(dp); return ip; } static struct inode* createat(int dirfd, char *path, short type, short major, short minor) { return createat_x(dirfd, path, type, major, minor, 0); } 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)-1; if (f->type != FD_INODE) return (addr_t)-1; int64 off = (int64)off_raw, newoff; switch (whence) { case 0: newoff = off; break; case 1: newoff = (int64)f->off + off; break; case 2: newoff = (int64)f->ip->size + off; break; default: return (addr_t)-1; } if (newoff < 0) return (addr_t)-1; // 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 // ----------------------------------------------------------------------- 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 rofs = 0; ip = createat_x(AT_FDCWD, path, (omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &rofs); // 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 rofs ? -EROFS : -ENOENT; } if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT)) itrunc(ip); } else { if (!(ip = namei(path))) { end_op(); return -ENOENT; } 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; } } if (!(ip = createat(dirfd, path, T_FILE, 0, 0))) { end_op(); return -ENOENT; } } else { if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; } 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 -1; 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; } addr_t sys_stat(void) { char *path; char *st; struct inode *ip; int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat); if (argstr(0, &path) < 0 || argptr(1, &st, size) < 0) return -1; begin_op(); if (!(ip = namei(path))) { end_op(); return -ENOENT; } 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_lstat(void) { return sys_stat(); } addr_t sys_access(void) { char *path; if (argstr(0, &path) < 0) return -1; begin_op(); struct inode *ip = namei(path); if (!ip) { end_op(); return -ENOENT; } 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; } if (!(ip = namei(path))) { end_op(); return -ENOENT; } 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; } if (!(ip = create(path, T_DIR, 0, 0))) { end_op(); return -ENOENT; } iunlockput(ip); end_op(); return 0; } addr_t sys_mknod(void) { struct inode *ip; char *path; int major, minor; begin_op(); if (argstr(0, &path) < 0 || argint(1, &major) < 0 || argint(2, &minor) < 0 || !(ip = create(path, T_DEV, major, minor))) { end_op(); return -1; } iunlockput(ip); end_op(); return 0; } addr_t sys_link(void) { char name[DIRSIZ], *new, *old; struct inode *dp, *ip; if (argstr(0, &old) < 0 || argstr(1, &new) < 0) return -1; begin_op(); if (!(ip = namei(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 = nameiparent(new, name))) goto bad_link; ilock(dp); if (dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0) { iunlockput(dp); goto bad_link; } iunlockput(dp); iput(ip); end_op(); return 0; bad_link: ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1; } addr_t sys_unlink(void) { struct inode *ip, *dp; char name[DIRSIZ], *path; uint off; if (argstr(0, &path) < 0) return -1; begin_op(); if (!(dp = nameiparent(path, name))) { end_op(); return -1; } ilock(dp); if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) goto bad_unlink; if (!(ip = dirlookup(dp, name, &off))) goto bad_unlink; ilock(ip); if (ip->nlink < 1) panic("unlink: nlink < 1"); if (ip->type == T_DIR && dirempty(ip) != 1) { iunlockput(ip); goto bad_unlink; } if (dirunlink(dp, name, off) < 0) { iunlockput(ip); goto bad_unlink; } if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); } iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return 0; bad_unlink: iunlockput(dp); end_op(); return -1; } addr_t sys_exec(void) { char *path, *argv[MAXARG], *envp[MAXENV]; addr_t uargv, uenvp, uarg; if (argstr(0, &path) < 0 || argaddr(1, &uargv) < 0) return -EFAULT; memset(argv, 0, sizeof(argv)); for (int i = 0; ; i++) { if (i >= NELEM(argv)) return -E2BIG; if (fetchaddr(uargv + sizeof(addr_t) * i, &uarg) < 0) return -EFAULT; if (!uarg) { argv[i] = 0; break; } if (fetchstr(uarg, &argv[i]) < 0) return -EFAULT; } // The third argument. This is Linux's execve(2), so musl has been passing // an environment here all along -- it was simply never read, and every // program on the system started with an empty one. // // A null envp is legal and means exactly that. It is also what the // first-party exec() wrapper passes, so old callers keep their old // behaviour without knowing about this. memset(envp, 0, sizeof(envp)); if (argaddr(2, &uenvp) == 0 && uenvp) { for (int i = 0; ; i++) { if (i >= NELEM(envp)) return -E2BIG; if (fetchaddr(uenvp + sizeof(addr_t) * i, &uarg) < 0) return -EFAULT; if (!uarg) { envp[i] = 0; break; } if (fetchstr(uarg, &envp[i]) < 0) return -EFAULT; } } return exec(path, argv, envp); } // 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; if (f->type != FD_INODE || !f->writable) return -1; 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; begin_op(); if (!(ip = namei(path))) { end_op(); return -ENOENT; } ilock(ip); itrunc_to(ip, (uint64)length); iunlockput(ip); end_op(); return 0; } addr_t sys_rename(void) { char *oldpath, *newpath; struct inode *ip, *dp_old, *dp_new, *ip_src; char oldname[DIRSIZ], newname[DIRSIZ]; uint off; struct dirent de; if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -1; begin_op(); if (!(ip = namei(oldpath))) { end_op(); return -1; } ilock(ip); if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; } 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) { if (dp_old) iput(dp_old); if (dp_new) iput(dp_new); goto bad_rename; } ilock(dp_new); if (dirlink(dp_new, newname, ip->inum) < 0) { iunlockput(dp_new); iput(dp_old); 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 -1; } iput(ip_src); memset(&de, 0, sizeof(de)); if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de)) panic("rename: writei"); 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 -1; } addr_t sys_symlink(void) { char *target, *linkpath; if (argstr(0, &target) < 0 || argstr(1, &linkpath) < 0) return -1; begin_op(); struct inode *ip = create(linkpath, T_SYMLINK, 0, 0); if (!ip) { end_op(); return -1; } int len = strlen(target); if (writei(ip, 0, (uint64)target, 0, len) != len) { iunlockput(ip); end_op(); return -1; } iunlockput(ip); end_op(); return 0; } addr_t sys_readlink(void) { char *path, *buf; int bufsiz; if (argstr(0, &path) < 0 || argint(2, &bufsiz) < 0 || bufsiz <= 0) return -1; if (argptr(1, &buf, bufsiz) < 0) return -1; 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 -1; } 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[DIRSIZ], *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); 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; } 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(); 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; 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; } addr_t sys_utimensat(void) { 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; } // 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. begin_op(); struct inode *ip = namei(path); if (!ip) { end_op(); return -ENOENT; } 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; } if (!(ip = createat(dirfd, path, T_DIR, 0, 0))) { end_op(); return -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; argint(2, &(int){0}); // mode — ignored argaddr(3, &dev); int major = (dev >> 8) & 0xff; int minor = dev & 0xff; begin_op(); struct inode *ip = createat(dirfd, path, T_DEV, (short)major, (short)minor); if (!ip) { end_op(); return -1; } iunlockput(ip); end_op(); return 0; } addr_t sys_unlinkat(void) { int dirfd, flags; char *path; struct inode *ip, *dp; char name[DIRSIZ]; 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[DIRSIZ], newname[DIRSIZ]; uint off; struct dirent de; begin_op(); if (!(ip = at_namei(olddirfd, oldpath))) { end_op(); return -1; } ilock(ip); if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; } 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) { if (dp_old) iput(dp_old); if (dp_new) iput(dp_new); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1; } 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 -1; } 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 -1; } 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[DIRSIZ]; 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 || 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 -1; argint(1, &newdirfd); if (argstr(2, &linkpath) < 0) return -1; begin_op(); struct inode *ip = createat(newdirfd, linkpath, T_SYMLINK, 0, 0); if (!ip) { end_op(); return -1; } int len = strlen(target); if (writei(ip, 0, (uint64)target, 0, len) != len) { iunlockput(ip); end_op(); return -1; } iunlockput(ip); end_op(); return 0; } addr_t sys_readlinkat(void) { int dirfd; char *path, *buf; int bufsiz; argint(0, &dirfd); if (argstr(1, &path) < 0) return -1; if (argint(3, &bufsiz) < 0 || bufsiz <= 0) return -1; if (argptr(2, &buf, bufsiz) < 0) return -1; 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 -1; } 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 begin_op(); struct inode *ip = at_namei(dirfd, path); if (!ip) { end_op(); return -ENOENT; } 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; } begin_op(); struct inode *ip = at_namei(dirfd, path); if (!ip) { end_op(); return -ENOENT; } 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; } // ----------------------------------------------------------------------- // 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. // ----------------------------------------------------------------------- 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; } // Only 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(); 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); 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; }