#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 "core/kmem.h" #include "uapi/errno.h" #include "uapi/inotify.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/pty.h" #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); } // Terminals as Linux numbers them: tty0..63 are (4, n), /dev/tty is (5, 0) and // /dev/console (5, 1). SchOS keeps them all under major 1 (kernel/fs/devfs.c), // and Linux programs look at the number rather than the name. Xorg is the one // that matters: it uses VTs only if stat("/dev/tty0") says major 4 // (xf86HasTTYs), and otherwise runs on none -- no VT switch, no KD_GRAPHICS, // the console drawing over it, and Ctrl+Alt+F-keys doing nothing. static uint64 linux_tty_rdev(uint32 major, uint32 minor) { if (major == 1 && minor == 0) return linux_makedev(4, 0); if (major == 1 && minor >= 11 && minor <= 16) return linux_makedev(4, minor - 10); if (major == 1 && minor == 10) return linux_makedev(5, 0); if (major == 1 && minor == 1) return linux_makedev(5, 1); // Pseudo-terminals, as Linux numbers them: ptmx (5, 2), slaves 136. if (major == PTMX_MAJOR) return linux_makedev(5, 2); if (major == PTS_MAJOR) return linux_makedev(136, minor); if (major == PTM_MAJOR) return linux_makedev(128, minor); return linux_makedev(major, minor); } 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; } // A filesystem that keeps real permission bits -- SchFS does -- reports // those instead. tmux, ssh and gpg refuse a directory others can read. if (ip->mode) perm_bits = ip->mode & 07777; 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_tty_rdev(ip->major, ip->minor); st->st_atime = ip->atime; st->st_mtime = ip->mtime; st->st_ctime = ip->ctime; } // The number stat() reports for a device node, split back into major and // minor. /sys must agree with stat() -- udev matches one against the other. void dev_stat_numbers(int major, int minor, int *maj, int *min) { uint64 r = linux_tty_rdev((uint32)major, (uint32)minor); *maj = (int)(((r >> 8) & 0xfff) | ((r >> 32) & ~0xfffULL)); *min = (int)((r & 0xff) | ((r >> 12) & ~0xffULL)); } // ----------------------------------------------------------------------- // 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()->files->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) { // Locked: threads share the table, and two picking the same empty slot // would each believe they owned it. files_lock(); for (int fd = 0; fd < NOFILE; fd++) { if (!myproc()->files->ofile[fd]) { myproc()->files->ofile[fd] = f; myproc()->files->flags[fd] = 0; files_unlock(); return fd; } } files_unlock(); return -1; } // Remember the absolute path `f` was opened by, for /proc//fd. `path` is // joined to the directory it was resolved from -- the cwd, or dirfd's own // remembered path -- and `.`, `..` and repeated slashes are folded out. This // is lexical: a symlink along the way stays in the name. Nothing depends on // it being there, so running out of memory just leaves it unset. static void file_setpath(struct file *f, int dirfd, const char *path) { const char *base = ""; if (path[0] != '/') { struct file *d; if (dirfd == AT_FDCWD) base = myproc()->cwd_path; else if (dirfd >= 0 && dirfd < NOFILE && (d = myproc()->files->ofile[dirfd]) && d->path) base = d->path; else return; } char *buf = kalloc(); if (!buf) return; int n = 0; // buf is "/a/b", no trailing slash for (int pass = 0; pass < 2; pass++) { const char *s = pass ? path : base; for (;;) { while (*s == '/') s++; const char *e = s; while (*e && *e != '/') e++; int len = (int)(e - s); if (len == 0) break; if (len == 2 && s[0] == '.' && s[1] == '.') { while (n > 0 && buf[--n] != '/') ; } else if (!(len == 1 && s[0] == '.')) { if (n + 1 + len >= PGSIZE) { kfree(buf); return; } buf[n++] = '/'; memmove(buf + n, s, len); n += len; } s = e; } } if (n == 0) buf[n++] = '/'; buf[n] = 0; if ((f->path = kmalloc(n + 1)) != 0) memmove(f->path, buf, n + 1); kfree(buf); } // 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()->files->ofile[dirfd]) return 0; struct file *f = myproc()->files->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; } // The mode argument of a Linux open/mkdir, or -1 from a native program, whose // calls have no such argument and leave whatever was in the register. static int linux_arg_mode(int n) { int mode; if (!myproc()->syscall_map) return -1; argint(n, &mode); return mode & 07777; } static struct inode* createat_x(int dirfd, char *path, short type, short major, short minor, int *err, int mode) { 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; // The permission bits asked for, less the umask -- or, from a caller that // has none to give, what open() and mkdir() would ask for. A symlink's // are 0777 on Linux whatever the umask, and nothing reads them. if (type == T_SYMLINK) ip->mode = 0777; else ip->mode = (uint)(mode >= 0 ? mode : type == T_DIR ? 0777 : 0666) & 07777 & ~myproc()->umask; 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; } inotify_event(dp->dev, dp->inum, IN_CREATE | (type == T_DIR ? IN_ISDIR : 0), name, 0); iunlockput(dp); return ip; } // The device and inode a path names, for inotify's watches. 0, or -errno. int path_identity(char *path, int follow, int onlydir, uint *dev, uint64 *inum) { struct inode *ip; int err = ENOENT, r = 0; begin_op(); ip = follow ? at_nameif(AT_FDCWD, path, &err) : at_namei(AT_FDCWD, path); if (!ip) { end_op(); return -err; } ilock(ip); if (onlydir && ip->type != T_DIR) r = -ENOTDIR; *dev = ip->dev; *inum = ip->inum; iunlockput(ip); end_op(); return r; } static struct inode* create(char *path, short type, short major, short minor) { return createat_x(AT_FDCWD, path, type, major, minor, 0, -1); } // ----------------------------------------------------------------------- // 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()->files->ofile[oldfd])) return -EBADF; if (newfd < 0 || newfd >= NOFILE) return -EBADF; if (oldfd == newfd) return newfd; if (myproc()->files->ofile[newfd]) fileclose(myproc()->files->ofile[newfd]); myproc()->files->ofile[newfd] = filedup(f); myproc()->files->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()->files->ofile[newfd]) fileclose(myproc()->files->ofile[newfd]); myproc()->files->ofile[newfd] = filedup(f); myproc()->files->flags[newfd] = 0; return newfd; } addr_t sys_close(void) { int fd; struct file *f; if (argfd(0, &fd, &f) < 0) return -EBADF; myproc()->files->ofile[fd] = 0; myproc()->files->flags[fd] = 0; lock_close_fd(f); fileclose(f); return 0; } addr_t sys_flock(void) { struct file *f; int op; if (argfd(0, 0, &f) < 0) return -EBADF; if (argint(1, &op) < 0) return -EINVAL; return lock_flock(f, op); } 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()->files->ofile[fd]) return -1; struct file *f = myproc()->files->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()->files->ofile[nfd]) { myproc()->files->ofile[nfd] = filedup(f); myproc()->files->flags[nfd] = (cmd == F_DUPFD_CLOEXEC) ? FD_CLOEXEC : 0; return nfd; } } return -EMFILE; } case F_GETFD: return myproc()->files->flags[fd]; case F_SETFD: myproc()->files->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; case F_ADD_SEALS: case F_GET_SEALS: if (f->type != FD_MEMFD) return -EINVAL; if (cmd == F_ADD_SEALS && !f->writable) return -EPERM; return memfd_seals(f->memfd_seg, cmd == F_ADD_SEALS ? arg : -1); case F_GETLK: case F_SETLK: case F_SETLKW: case F_OFD_GETLK: case F_OFD_SETLK: case F_OFD_SETLKW: { // A pointer, which argint() above would have cut to 32 bits. uint64 a; if (argaddr(2, &a) < 0) return -EFAULT; return lock_fcntl(f, cmd, (addr_t)a); } } 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()->files->ofile[fd0] = 0; fileclose(rf); fileclose(wf); return -1; } fd[0] = fd0; fd[1] = fd1; return 0; } // The flags were ignored, and they are the point of pipe2: libevent makes its // signal pipe with O_NONBLOCK and drains it until EAGAIN, so the drain blocked // on the read after the last byte and tmux's client stopped dead. addr_t sys_pipe2(void) { addr_t fdarray; struct file *rf, *wf; int fd0, fd1, flags; if (argaddr(0, &fdarray) < 0) return -1; argint(1, &flags); 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()->files->ofile[fd0] = 0; fileclose(rf); fileclose(wf); return -1; } rf->nonblock = wf->nonblock = (flags & O_NONBLOCK) != 0; if (flags & O_CLOEXEC) { myproc()->files->flags[fd0] |= FD_CLOEXEC; myproc()->files->flags[fd1] |= FD_CLOEXEC; } 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, linux_arg_mode(2)); // 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); } // /dev/ptmx: every open is a new pseudo-terminal, and the file holds its // master rather than the ptmx node. See kernel/dev/pty.c. if (ip->type == T_DEV && ip->major == PTMX_MAJOR) { struct inode *m = ptmx_open(ip); if (!m) { iunlockput(ip); end_op(); return -ENOSPC; } ip = m; } 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; file_setpath(f, AT_FDCWD, path); 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, linux_arg_mode(3)))) { 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); // /dev/ptmx: every open is a new pseudo-terminal, and the file holds its // master rather than the ptmx node. See kernel/dev/pty.c. if (ip->type == T_DEV && ip->major == PTMX_MAJOR) { struct inode *m = ptmx_open(ip); if (!m) { iunlockput(ip); end_op(); return -ENOSPC; } ip = m; } 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; file_setpath(f, dirfd, path); 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; } // A socket, a pipe or an anonymous inode: Linux answers with its type and // an inode number, which is what `ls -l /proc//fd` and every program // asking "is my stdin a socket?" look at. ino matches /proc//fd. if (f->type != FD_INODE && f->type != FD_MEMFD && myproc()->syscall_map) { struct linux_stat *ls = (struct linux_stat *)st; memset(ls, 0, sizeof(*ls)); ls->st_mode = f->type == FD_SOCKET ? LINUX_S_IFSOCK | 0777 : f->type == FD_PIPE ? LINUX_S_IFIFO | 0600 : 0600; ls->st_ino = f->ino; ls->st_nlink = 1; ls->st_blksize = PGSIZE; 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, linux_arg_mode(1)))) { 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, (int)(mode & 07777)); 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; } inotify_event(dp->dev, dp->inum, IN_CREATE, name, 0); inotify_event(ip->dev, ip->inum, IN_ATTRIB, 0, 0); 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); } // A name removed from dp: IN_DELETE there, and for the inode itself either // IN_ATTRIB (a link went) or IN_DELETE_SELF (the last one did). Before the // caller drops nlink, so the last link is nlink == 1 here. static void inotify_unlinked(struct inode *dp, struct inode *ip, char *name) { inotify_event(dp->dev, dp->inum, IN_DELETE | (ip->type == T_DIR ? IN_ISDIR : 0), name, 0); inotify_event(ip->dev, ip->inum, ip->nlink <= 1 || ip->type == T_DIR ? IN_DELETE_SELF : IN_ATTRIB, 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); } inotify_unlinked(dp, ip, name); 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(); inotify_file(f, IN_MODIFY); 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); inotify_event(ip->dev, ip->inum, IN_MODIFY, 0, 0); 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. // // renameat() is this with directory fds. It used to be a copy of the old // rename, both faults included, and apk -- which renames every file it // installs and its database -- met them both. static addr_t do_rename(int olddirfd, char *oldpath, int newdirfd, char *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; begin_op(); if (!(ip = at_namei(olddirfd, 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 = 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) { err = (!dp_old || !dp_new) ? ENOENT : EXDEV; if (dp_old) iput(dp_old); if (dp_new) iput(dp_new); goto bad_rename; } uint new_dev = dp_new->dev; // for inotify, after dp_new is put uint64 new_inum = dp_new->inum; // 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; } if (ip_dst->nlink <= 1) inotify_event(ip_dst->dev, ip_dst->inum, IN_DELETE_SELF, 0, 0); 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; } { uint32 cookie = inotify_cookie(); inotify_event(dp_old->dev, dp_old->inum, IN_MOVED_FROM, oldname, cookie); inotify_event(new_dev, new_inum, IN_MOVED_TO, newname, cookie); inotify_event(ip->dev, ip->inum, IN_MOVE_SELF, 0, 0); } 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; } addr_t sys_rename(void) { char *oldpath, *newpath; if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -EFAULT; return do_rename(AT_FDCWD, oldpath, AT_FDCWD, newpath); } // 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, -1))) { 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; } // A /proc link can change after its inode was loaded -- an fd dup2()ed // over -- so its size is not trusted: the read says how long it is. int proc = ip->dev == PROCFS_DEV; int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size; int got = readi(ip, 0, (uint64)buf, 0, n); if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; } iunlockput(ip); return got; } // ----------------------------------------------------------------------- // 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->files->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; } // The four that apply to any descriptor at all. Rust's standard library // sets close-on-exec with FIOCLEX and non-blocking with FIONBIO rather // than with fcntl, and on a pipe or a socket those fell through to the // ENOTTY below: glycin's image loaders failed with "Not a tty" and GTK // could load no icon. if (request == 0x5451 || request == 0x5450) { // FIOCLEX, FIONCLEX if (request == 0x5451) myproc()->files->flags[fd] |= FD_CLOEXEC; else myproc()->files->flags[fd] &= ~FD_CLOEXEC; return 0; } if (request == 0x5421) { // FIONBIO int on; if (copy_from_user(&on, argp, sizeof(on)) < 0) return -EFAULT; f->nonblock = on != 0; return 0; } if (request == 0x5452) // FIOASYNC return 0; if (request == FIONREAD && (f->type == FD_EVENT || f->type == FD_PIPE)) { int n = f->type == FD_PIPE ? pipe_avail(f->pipe) : ev_fionread(f->ev); if (n < 0) return n; return put_user(n, argp) < 0 ? -EFAULT : 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); if (is_dev && (f->ip->major == PTM_MAJOR || f->ip->major == PTS_MAJOR)) return pty_ioctl(f->ip, (uint)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. // TIOCLINUX. Only subcode 6, the modifier state, is answered -- with // "none held", which is true often enough. mc asks it after every key on // a Linux console, and the ioctl used to succeed without writing: the 6 // it passed in came back as Ctrl+AltGr held, and Enter arrived as an // unknown chord printed as a dot. if (request == 0x541C) { uchar sub; if (copy_from_user(&sub, argp, 1) < 0) return -EFAULT; if (sub != 6) return -EINVAL; sub = 0; return copy_to_user(argp, &sub, 1) < 0 ? -EFAULT : 0; } 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_set_process(vt_for_minor(f->ip->minor), m->mode, m->relsig, m->acqsig); 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)); struct vt *v = vt_for_minor(f->ip->minor); m->mode = (char)v->mode; m->relsig = (short)v->relsig; m->acqsig = (short)v->acqsig; return 0; } // The display server's half of a switch; see vt_switch(). By value. if (request == VT_RELDISP) return vt_reldisp(vt_for_minor(f->ip->minor), (int)argp); // Terminals are a fixed set here and are never freed. if (request == VT_DISALLOCATE) return 0; if (request == KDSETMODE) { if (argp != KD_TEXT && argp != KD_GRAPHICS) return -EINVAL; vt_set_kdmode(vt_for_minor(f->ip->minor), (int)argp); return 0; } if (request == KDGETMODE) { int *out = (int*)user_ptr(argp, sizeof(int)); if (!out) return -EFAULT; *out = vt_for_minor(f->ip->minor)->kdmode; return 0; } if (request == KDSKBMODE) { if (argp > K_OFF) return -EINVAL; vt_for_minor(f->ip->minor)->kbmode = (int)argp; return 0; } if (request == KDGKBMODE) { int *out = (int*)user_ptr(argp, sizeof(int)); if (!out) return -EFAULT; *out = vt_for_minor(f->ip->minor)->kbmode; return 0; } if (request == KDGKBTYPE) { char *out = (char*)user_ptr(argp, 1); if (!out) return -EFAULT; *out = KB_101; return 0; } // LEDs and the bell: nothing to light and no speaker on this path. if (request == KDGETLED || request == KDGKBLED) { char *out = (char*)user_ptr(argp, 1); if (!out) return -EFAULT; *out = 0; return 0; } if (request == KDSETLED || request == KDSKBLED || request == KIOCSOUND || request == KDMKTONE) 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). // The new bits go in the inode and through iupdate to the filesystem. One // that keeps none (FAT) holds them only while the inode is cached, which is // as much as it can; there is no failure to report. static int chmod_inode(struct inode *ip, int mode) { if (fs_readonly(ip->dev)) return -EROFS; ilock(ip); ip->mode = (uint)mode & 07777; if (ip->mode == 0) ip->mode = 01000000; // "no bits", as distinct from "never set" iupdate(ip); inotify_event(ip->dev, ip->inum, IN_ATTRIB, 0, 0); iunlock(ip); return 0; } addr_t sys_chmod(void) { char *path; int mode, err, r; struct inode *ip; if (argstr(0, &path) < 0) return -EFAULT; argint(1, &mode); begin_op(); if (!(ip = at_nameif(AT_FDCWD, path, &err))) { end_op(); return -err; } r = chmod_inode(ip, mode); iput(ip); end_op(); return r; } addr_t sys_fchmod(void) { struct file *f; int mode, r; if (argfd(0, 0, &f) < 0) return -EBADF; argint(1, &mode); if (f->type != FD_INODE || !f->ip) return 0; begin_op(); r = chmod_inode(f->ip, mode); end_op(); return r; } 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); inotify_event(ip->dev, ip->inum, IN_ATTRIB, 0, 0); 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. // Make `path` this process's root: where an absolute path starts and where // ".." stops (see namex_from). The working directory is left where it is, as // on Linux; a program that means to stay inside chdir()s there next. addr_t sys_chroot(void) { char *path; struct inode *ip; if (argstr(0, &path) < 0) return -EFAULT; begin_op(); if ((ip = namei(path)) == 0) { end_op(); return -ENOENT; } ilock(ip); if (ip->type != T_DIR) { iunlockput(ip); end_op(); return -ENOTDIR; } iunlock(ip); if (myproc()->root) iput(myproc()->root); myproc()->root = ip; end_op(); return 0; } // ----------------------------------------------------------------------- // 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, linux_arg_mode(2)))) { 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); } // The errors unlink_path() gives, for the same reason: apk removes each file // before it writes the new one and expects ENOENT when there was none. Every // failure here was -1, read as EPERM, and every file failed to extract. addr_t sys_unlinkat(void) { int dirfd, flags, err; char *path; struct inode *ip, *dp; char name[VFS_NAME_MAX + 1]; uint off; argint(0, &dirfd); if (argstr(1, &path) < 0) return -EFAULT; argint(2, &flags); begin_op(); if (!(dp = at_nameiparent(dirfd, path, name))) { end_op(); 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("unlinkat: nlink < 1"); if (ip->type == T_DIR && !(flags & AT_REMOVEDIR)) { err = EISDIR; goto badip; } if (ip->type != T_DIR && (flags & AT_REMOVEDIR)) { err = ENOTDIR; goto badip; } if (ip->type == T_DIR && dirempty(ip) != 1) { err = ENOTEMPTY; goto badip; } if (dirunlink(dp, name, off) < 0) { err = EIO; goto badip; } if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); } inotify_unlinked(dp, ip, name); iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return 0; badip: iunlockput(ip); bad: iunlockput(dp); end_op(); return -err; } addr_t sys_renameat(void) { int olddirfd, newdirfd; char *oldpath, *newpath; argint(0, &olddirfd); argint(2, &newdirfd); if (argstr(1, &oldpath) < 0 || argstr(3, &newpath) < 0) return -EFAULT; return do_rename(olddirfd, oldpath, newdirfd, newpath); } 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; } inotify_event(dp->dev, dp->inum, IN_CREATE, name, 0); inotify_event(ip->dev, ip->inum, IN_ATTRIB, 0, 0); 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; } // A /proc link can change after its inode was loaded -- an fd dup2()ed // over -- so its size is not trusted: the read says how long it is. int proc = ip->dev == PROCFS_DEV; int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size; int got = readi(ip, 0, (uint64)buf, 0, n); if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; } iunlockput(ip); return got; } addr_t sys_fchmodat(void) { int dirfd, mode, err, r; char *path; struct inode *ip; argint(0, &dirfd); if (argstr(1, &path) < 0) return -EFAULT; argint(2, &mode); begin_op(); if (!(ip = at_nameif(dirfd, path, &err))) { end_op(); return -err; } r = chmod_inode(ip, mode); iput(ip); end_op(); return r; } 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; } // faccessat2: faccessat with the flags argument the original never had. musl // sends any faccessat with flags here, and without it GLib's every "does this // file exist" (F_OK, AT_SYMLINK_NOFOLLOW) failed with EINVAL. The mode is not // checked, as in faccessat above; AT_EACCESS changes nothing for the same // reason. addr_t sys_faccessat2(void) { int dirfd, mode, flags, err = ENOENT; char *path; struct inode *ip; argint(0, &dirfd); if (argstr(1, &path) < 0) return -EFAULT; argint(2, &mode); argint(3, &flags); if (flags & ~(AT_SYMLINK_NOFOLLOW | AT_EACCESS | AT_EMPTY_PATH)) return -EINVAL; if ((flags & AT_EMPTY_PATH) && path[0] == '\0') { if (dirfd < 0 || dirfd >= NOFILE || !myproc()->files->ofile[dirfd]) return -EBADF; return 0; } begin_op(); ip = (flags & AT_SYMLINK_NOFOLLOW) ? at_namei(dirfd, path) : 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()->files->ofile[dirfd]) return -1; struct file *f = myproc()->files->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) return procfs_mount(target); // 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) return devfs_mount(target); // 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; }