Third guess at a chord, and the third one would have been wrong too: a display
manager that grabs Ctrl+Alt+F<n> matches loosely, so Ctrl+Alt+Shift+F2 never
reaches the guest either. Extra modifiers do not get past a loose grab.
The real problem is that this is not knowable from inside. A key the host
swallowed is indistinguishable from a key nobody pressed, so the guest cannot
probe for a free combination, and there is no combination that is free
everywhere:
the HOST takes Alt+F4, Alt+F2, Ctrl+Alt+F1..F6 (loosely matched),
Ctrl+Alt+digit (QEMU's own console tabs)
APPLICATIONS want Alt+anything (Meta -- zsh binds Meta-digit as
digit-argument), Ctrl+letter, Ctrl+Shift+digit
(Ctrl+Shift+2 types ^@), bare F-keys
the LAYOUT wants AltGr+letter
So it is a setting. VT_SETCHORD/VT_GETCHORD, and in userspace:
chvt -c ctrl+shift+F Ctrl+Shift+F1..F6 (the new default)
chvt -c ctrl+alt+shift+F
chvt -c alt+1
chvt -c off no chord; chvt N is then the only way
chvt report the active terminal and the chord
`off` is a legitimate setting rather than a failure mode. On a machine whose
host grabs everything worth binding, `chvt N` is the honest answer.
The default moved to Ctrl+Shift+F1..F6: display managers bind Ctrl+Alt+F<n>,
not Ctrl+Shift+F<n>, and Ctrl+Shift with a *function* key is not a character
on any layout -- unlike Ctrl+Shift+digit, which is.
Caught while writing it: VTC_RALT was 0x08 while kbd.h's RALT is (1<<7), since
(1<<3) is CAPSLOCK. A chord set from userspace with AltGr would simply never
have matched, and the only symptom would have been a key that does nothing.
A _Static_assert now holds the two headers together.
Verified: Ctrl+Alt+F2 leaves the screen byte-identical, Ctrl+Shift+F2 switches,
Ctrl+Shift+F1 returns to a pixel-identical tty1, and chvt -c round-trips
through alt+1 and off and back. schfs-check passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
1957 lines
68 KiB
C
1957 lines
68 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "stat.h"
|
|
#include "mmu.h"
|
|
#include "memlayout.h"
|
|
#include "proc.h"
|
|
#include "fs.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "file.h"
|
|
#include "fcntl.h"
|
|
#include "posix.h"
|
|
#include "syscall.h"
|
|
#include "uaccess.h"
|
|
#include "errno.h"
|
|
#include "schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
|
|
#include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */
|
|
#include "vt.h" /* the terminals TIOC*/VT_* act on */
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Linux-ABI-compatible struct stat (x86_64 "struct stat" from
|
|
// musl 1.2.5 bits/stat.h) — musl/toybox/dash parse st_mode directly
|
|
// out of this exact byte layout, which bears no resemblance to SchOS's
|
|
// own native `struct stat` (stat.h). Without this, every Linux-ABI process
|
|
// reading stat() results (e.g. ls checking S_ISDIR to decide whether to
|
|
// list a directory or just print its name) gets garbage.
|
|
// -----------------------------------------------------------------------
|
|
|
|
struct linux_stat {
|
|
uint64 st_dev;
|
|
uint64 st_ino;
|
|
uint64 st_nlink;
|
|
uint32 st_mode;
|
|
uint32 st_uid;
|
|
uint32 st_gid;
|
|
uint32 __pad0;
|
|
uint64 st_rdev;
|
|
int64 st_size;
|
|
int64 st_blksize;
|
|
int64 st_blocks;
|
|
int64 st_atime, st_atime_nsec;
|
|
int64 st_mtime, st_mtime_nsec;
|
|
int64 st_ctime, st_ctime_nsec;
|
|
int64 __unused[3];
|
|
};
|
|
|
|
#define LINUX_S_IFDIR 0040000
|
|
#define LINUX_S_IFCHR 0020000
|
|
#define LINUX_S_IFREG 0100000
|
|
#define LINUX_S_IFLNK 0120000
|
|
|
|
// Linux's dev_t: 12 bits of major and 8 of minor packed into the low 32, with
|
|
// the overflow of each above that. Spelled out in full rather than as
|
|
// (major << 8 | minor), because that is how the other side decodes it and a
|
|
// shortcut here would only work while both numbers stayed small.
|
|
static uint64
|
|
linux_makedev(uint32 major, uint32 minor)
|
|
{
|
|
return ((uint64)(major & 0xfffu) << 8) | (uint64)(minor & 0xffu)
|
|
| ((uint64)(major & ~0xfffu) << 32) | ((uint64)(minor & ~0xffu) << 12);
|
|
}
|
|
|
|
static void
|
|
stati_linux(struct inode *ip, struct linux_stat *st)
|
|
{
|
|
memset(st, 0, sizeof(*st));
|
|
st->st_dev = ip->dev;
|
|
st->st_ino = ip->inum;
|
|
st->st_nlink = ip->nlink > 0 ? ip->nlink : 1;
|
|
st->st_size = ip->size;
|
|
st->st_blksize = 512;
|
|
st->st_blocks = (ip->size + 511) / 512;
|
|
|
|
// This filesystem's rwmode is a 2-bit read/write flag, not a real POSIX
|
|
// permission/execute bitmask, so there's no per-file "is executable"
|
|
// tracking to report accurately. Regular files are marked executable
|
|
// unconditionally (0755) — everything under /bin is meant to run, and
|
|
// shells like mksh check st_mode's x bits (or access(X_OK), which goes
|
|
// through the same stat data) before exec'ing, so leaving execute unset
|
|
// made every program "Permission denied".
|
|
uint32 type_bits, perm_bits;
|
|
switch (ip->type) {
|
|
case T_DIR: type_bits = LINUX_S_IFDIR; perm_bits = 0755; break;
|
|
case T_DEV: type_bits = LINUX_S_IFCHR; perm_bits = 0666; break;
|
|
case T_SYMLINK: type_bits = LINUX_S_IFLNK; perm_bits = 0777; break;
|
|
default: type_bits = LINUX_S_IFREG; perm_bits = (ip->rwmode & 2) ? 0755 : 0555; break;
|
|
}
|
|
st->st_mode = type_bits | perm_bits;
|
|
|
|
// Which device this node actually is. Left at zero until now, so every
|
|
// entry in /dev printed as "0, 0" under ls -l -- harmless while nothing
|
|
// read it, and wrong the moment something has to tell two disks apart.
|
|
if (ip->type == T_DEV)
|
|
st->st_rdev = linux_makedev(ip->major, ip->minor);
|
|
|
|
st->st_atime = ip->atime; st->st_mtime = ip->mtime; st->st_ctime = ip->ctime;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Internal helpers
|
|
// -----------------------------------------------------------------------
|
|
|
|
static int
|
|
argfd(int n, int *pfd, struct file **pf)
|
|
{
|
|
int fd;
|
|
struct file *f;
|
|
if (argint(n, &fd) < 0)
|
|
return -1;
|
|
if (fd < 0 || fd >= NOFILE || !(f = myproc()->ofile[fd]))
|
|
return -1;
|
|
if (pfd) *pfd = fd;
|
|
if (pf) *pf = f;
|
|
return 0;
|
|
}
|
|
|
|
// Not static: sys_net.c hands out descriptors for sockets and needs the same
|
|
// allocator, so that a socket fd is indistinguishable from any other.
|
|
int
|
|
fdalloc(struct file *f)
|
|
{
|
|
for (int fd = 0; fd < NOFILE; fd++) {
|
|
if (!myproc()->ofile[fd]) {
|
|
myproc()->ofile[fd] = f;
|
|
myproc()->ofile_flags[fd] = 0;
|
|
return fd;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Resolves a dirfd (AT_FDCWD or a real fd) to the inode a relative path
|
|
// should be resolved from. 0 on an invalid dirfd.
|
|
static struct inode *
|
|
at_dir(int dirfd)
|
|
{
|
|
if (dirfd == AT_FDCWD) return myproc()->cwd;
|
|
if (dirfd < 0 || dirfd >= NOFILE || !myproc()->ofile[dirfd]) return 0;
|
|
struct file *f = myproc()->ofile[dirfd];
|
|
if (f->type != FD_INODE) return 0;
|
|
return f->ip;
|
|
}
|
|
|
|
// Resolves an at-path: absolute paths always go from root; relative paths
|
|
// resolve from dirfd's inode (AT_FDCWD means myproc()->cwd, same as before —
|
|
// a real dirfd now actually works instead of being silently ignored or
|
|
// rejected).
|
|
static struct inode *
|
|
at_namei(int dirfd, char *path)
|
|
{
|
|
if (!path) return 0;
|
|
if (path[0] == '/') return namei(path);
|
|
struct inode *start = at_dir(dirfd);
|
|
if (!start) return 0;
|
|
return nameifrom(start, path);
|
|
}
|
|
|
|
static struct inode *
|
|
at_nameiparent(int dirfd, char *path, char *name)
|
|
{
|
|
if (!path || !path[0]) return 0;
|
|
if (path[0] == '/') return nameiparent(path, name);
|
|
struct inode *start = at_dir(dirfd);
|
|
if (!start) return 0;
|
|
return nameiparentfrom(start, path, name);
|
|
}
|
|
|
|
static struct inode*
|
|
createat_x(int dirfd, char *path, short type, short major, short minor,
|
|
int *rofs)
|
|
{
|
|
struct inode *ip, *dp;
|
|
char name[DIRSIZ];
|
|
uint off;
|
|
if (!(dp = at_nameiparent(dirfd, path, name))) return 0;
|
|
ilock(dp);
|
|
// Nothing below this point tolerates a refusal: ialloc and dirlink both
|
|
// panic on failure, because on a writable filesystem they cannot fail for
|
|
// any reason the caller could have avoided.
|
|
//
|
|
// The flag lets the caller report EROFS. Returning 0 alone made a refused
|
|
// write say "No such file or directory", which is what a shell prints for
|
|
// every create failure and tells the user nothing about why.
|
|
if (fs_readonly(dp->dev)) {
|
|
iunlockput(dp);
|
|
if (rofs) *rofs = 1;
|
|
return 0;
|
|
}
|
|
if ((ip = dirlookup(dp, name, &off)) != 0) {
|
|
iunlockput(dp);
|
|
ilock(ip);
|
|
// The name already exists. When the caller wants an ordinary file
|
|
// (the open(O_CREAT) path), POSIX says to just open what is there --
|
|
// the existing object only has to not be a directory. This used to
|
|
// require the existing inode to be T_FILE *as well*, so opening a
|
|
// device node with O_CREAT failed and was reported as ENOENT: plain
|
|
// `> /dev/null` did not work, despite /dev/null existing.
|
|
// mkdir/mknod ask for T_DIR/T_DEV and still correctly fail here,
|
|
// which is what lets them report EEXIST.
|
|
if (type == T_FILE && ip->type != T_DIR)
|
|
return ip;
|
|
iunlockput(ip);
|
|
return 0;
|
|
}
|
|
if (!(ip = ialloc(dp->dev, type))) panic("create: ialloc");
|
|
ilock(ip);
|
|
ip->major = major; ip->minor = minor; ip->nlink = 1;
|
|
ip->showmode = 1;
|
|
iupdate(ip);
|
|
if (type == T_DIR) {
|
|
dp->nlink++;
|
|
iupdate(dp);
|
|
build_dir_tree(ip, dp->inum);
|
|
}
|
|
if (dirlink(dp, name, ip->inum) < 0) panic("create: dirlink");
|
|
iunlockput(dp);
|
|
return ip;
|
|
}
|
|
|
|
static struct inode*
|
|
createat(int dirfd, char *path, short type, short major, short minor)
|
|
{
|
|
return createat_x(dirfd, path, type, major, minor, 0);
|
|
}
|
|
|
|
static struct inode*
|
|
create(char *path, short type, short major, short minor)
|
|
{
|
|
return createat_x(AT_FDCWD, path, type, major, minor, 0);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// File descriptor ops
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_dup(void)
|
|
{
|
|
struct file *f;
|
|
int fd;
|
|
if (argfd(0, 0, &f) < 0 || (fd = fdalloc(f)) < 0) return -1;
|
|
filedup(f);
|
|
return fd;
|
|
}
|
|
|
|
addr_t sys_dup2(void)
|
|
{
|
|
int oldfd, newfd;
|
|
struct file *f;
|
|
if (argint(0, &oldfd) < 0 || argint(1, &newfd) < 0) return -1;
|
|
if (oldfd < 0 || oldfd >= NOFILE || !(f = myproc()->ofile[oldfd])) return -EBADF;
|
|
if (newfd < 0 || newfd >= NOFILE) return -EBADF;
|
|
if (oldfd == newfd) return newfd;
|
|
if (myproc()->ofile[newfd]) fileclose(myproc()->ofile[newfd]);
|
|
myproc()->ofile[newfd] = filedup(f);
|
|
myproc()->ofile_flags[newfd] = 0; // dup2 never carries FD_CLOEXEC over
|
|
return newfd;
|
|
}
|
|
|
|
addr_t sys_dup3(void)
|
|
{
|
|
struct file *f;
|
|
int oldfd, newfd;
|
|
argint(2, &(int){0}); // flags — ignored for now
|
|
if (argfd(0, &oldfd, &f) < 0 || argint(1, &newfd) < 0) return -EBADF;
|
|
if (newfd < 0 || newfd >= NOFILE) return -EBADF;
|
|
// Unlike dup2, equal fds are an error (dup3(2)). Without this check we
|
|
// would fileclose(f) and then filedup() the file we just released.
|
|
if (oldfd == newfd) return -EINVAL;
|
|
if (myproc()->ofile[newfd]) fileclose(myproc()->ofile[newfd]);
|
|
myproc()->ofile[newfd] = filedup(f);
|
|
myproc()->ofile_flags[newfd] = 0;
|
|
return newfd;
|
|
}
|
|
|
|
addr_t sys_close(void)
|
|
{
|
|
int fd;
|
|
struct file *f;
|
|
if (argfd(0, &fd, &f) < 0) return -EBADF;
|
|
myproc()->ofile[fd] = 0;
|
|
myproc()->ofile_flags[fd] = 0;
|
|
fileclose(f);
|
|
return 0;
|
|
}
|
|
|
|
addr_t
|
|
sys_fcntl(void)
|
|
{
|
|
int fd, cmd, arg;
|
|
if (argint(0, &fd) < 0 || argint(1, &cmd) < 0 || argint(2, &arg) < 0) return -1;
|
|
if (fd < 0 || fd >= NOFILE || !myproc()->ofile[fd]) return -1;
|
|
struct file *f = myproc()->ofile[fd];
|
|
switch (cmd) {
|
|
case F_DUPFD:
|
|
case F_DUPFD_CLOEXEC: {
|
|
if (arg < 0) return -EINVAL;
|
|
for (int nfd = arg; nfd < NOFILE; nfd++) {
|
|
if (!myproc()->ofile[nfd]) {
|
|
myproc()->ofile[nfd] = filedup(f);
|
|
myproc()->ofile_flags[nfd] = (cmd == F_DUPFD_CLOEXEC) ? FD_CLOEXEC : 0;
|
|
return nfd;
|
|
}
|
|
}
|
|
return -EMFILE;
|
|
}
|
|
case F_GETFD: return myproc()->ofile_flags[fd];
|
|
case F_SETFD: myproc()->ofile_flags[fd] = arg; return 0;
|
|
case F_GETFL: {
|
|
int flags = 0;
|
|
if (f->readable && f->writable) flags = O_RDWR;
|
|
else if (f->writable) flags = O_WRONLY;
|
|
// else O_RDONLY = 0
|
|
if (f->nonblock) flags |= O_NONBLOCK;
|
|
return flags;
|
|
}
|
|
// O_NONBLOCK is the only settable flag that means anything here, and it
|
|
// has to actually be stored: the usual sequence is F_GETFL then
|
|
// F_SETFL(flags | O_NONBLOCK), and a no-op F_SETFL turns that into a
|
|
// program that believes its reads will return immediately and then blocks
|
|
// on the first one. The rest (O_APPEND, O_ASYNC, O_DIRECT) are ignored.
|
|
case F_SETFL:
|
|
f->nonblock = (arg & O_NONBLOCK) != 0;
|
|
return 0;
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// I/O
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_read(void)
|
|
{
|
|
struct file *f;
|
|
int n;
|
|
char *p;
|
|
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argptr(1, &p, n) < 0) return -1;
|
|
return fileread(f, p, n);
|
|
}
|
|
|
|
addr_t sys_write(void)
|
|
{
|
|
struct file *f;
|
|
int n;
|
|
uint64 p;
|
|
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0) return -1;
|
|
return filewrite(f, (char*)p, n);
|
|
}
|
|
|
|
addr_t sys_readv(void)
|
|
{
|
|
struct file *f;
|
|
addr_t iov_addr;
|
|
int iovcnt, total = 0;
|
|
if (argfd(0, 0, &f) < 0) return -EBADF;
|
|
argaddr(1, &iov_addr); argint(2, &iovcnt);
|
|
if (iovcnt <= 0 || iovcnt > UIO_MAXIOV) return -EINVAL;
|
|
for (int i = 0; i < iovcnt; i++) {
|
|
struct iovec iov;
|
|
if (copy_from_user(&iov, iov_addr + i * sizeof(iov), sizeof(iov)) < 0)
|
|
return -EFAULT;
|
|
if (!iov.iov_len) continue;
|
|
char *buf = user_ptr((addr_t)iov.iov_base, iov.iov_len);
|
|
if (!buf) return -EFAULT;
|
|
int r = fileread(f, buf, (int)iov.iov_len);
|
|
if (r <= 0) return total > 0 ? total : r;
|
|
total += r;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
addr_t sys_writev(void)
|
|
{
|
|
struct file *f;
|
|
addr_t iov_addr;
|
|
int iovcnt, total = 0;
|
|
if (argfd(0, 0, &f) < 0) return -EBADF;
|
|
argaddr(1, &iov_addr); argint(2, &iovcnt);
|
|
if (iovcnt <= 0 || iovcnt > UIO_MAXIOV) return -EINVAL;
|
|
for (int i = 0; i < iovcnt; i++) {
|
|
struct iovec iov;
|
|
if (copy_from_user(&iov, iov_addr + i * sizeof(iov), sizeof(iov)) < 0)
|
|
return -EFAULT;
|
|
if (!iov.iov_len) continue;
|
|
char *buf = user_ptr((addr_t)iov.iov_base, iov.iov_len);
|
|
if (!buf) return -EFAULT;
|
|
int r = filewrite(f, buf, (int)iov.iov_len);
|
|
if (r < 0) return total > 0 ? total : r;
|
|
total += r;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
addr_t sys_lseek(void)
|
|
{
|
|
struct file *f;
|
|
addr_t off_raw;
|
|
int whence;
|
|
if (argfd(0, 0, &f) < 0 || argaddr(1, &off_raw) < 0 || argint(2, &whence) < 0) return (addr_t)-1;
|
|
if (f->type != FD_INODE) return (addr_t)-1;
|
|
int64 off = (int64)off_raw, newoff;
|
|
switch (whence) {
|
|
case 0: newoff = off; break;
|
|
case 1: newoff = (int64)f->off + off; break;
|
|
case 2: newoff = (int64)f->ip->size + off; break;
|
|
default: return (addr_t)-1;
|
|
}
|
|
if (newoff < 0) return (addr_t)-1;
|
|
// Seeking past the end is legal and is how a sparse file is made: the gap
|
|
// reads as zeros and costs nothing on a filesystem with extents. This used
|
|
// to clamp to end-of-file, which silently turned "seek to 1 GB and write"
|
|
// into "append", and any write that follows still gets validated by writei
|
|
// on a filesystem that cannot represent a hole.
|
|
f->off = (uint64)newoff;
|
|
return (addr_t)newoff;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Pipe
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_pipe(void)
|
|
{
|
|
int *fd;
|
|
struct file *rf, *wf;
|
|
int fd0 = -1, fd1;
|
|
if (argptr(0, (void*)&fd, 2*sizeof(fd[0])) < 0 || pipealloc(&rf, &wf) < 0) return -1;
|
|
if ((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0) {
|
|
if (fd0 >= 0) myproc()->ofile[fd0] = 0;
|
|
fileclose(rf); fileclose(wf);
|
|
return -1;
|
|
}
|
|
fd[0] = fd0; fd[1] = fd1;
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_pipe2(void)
|
|
{
|
|
addr_t fdarray;
|
|
struct file *rf, *wf;
|
|
int fd0, fd1;
|
|
if (argaddr(0, &fdarray) < 0) return -1;
|
|
int *fds = (int*)user_ptr(fdarray, 2 * sizeof(int));
|
|
if (!fds || pipealloc(&rf, &wf) < 0) return -1;
|
|
fd0 = fd1 = -1;
|
|
if ((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0) {
|
|
if (fd0 >= 0) myproc()->ofile[fd0] = 0;
|
|
fileclose(rf); fileclose(wf);
|
|
return -1;
|
|
}
|
|
fds[0] = fd0; fds[1] = fd1;
|
|
return 0;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Filesystem ops
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_open(void)
|
|
{
|
|
char *path;
|
|
int fd, omode;
|
|
struct file *f;
|
|
struct inode *ip;
|
|
if (argstr(0, &path) < 0 || argint(1, &omode) < 0) return -EFAULT;
|
|
begin_op();
|
|
if (omode & O_CREATE) {
|
|
// O_EXCL means "create it or fail", so the file existing is the error.
|
|
if (omode & O_EXCL) {
|
|
struct inode *ex = namei(path);
|
|
if (ex) { iput(ex); end_op(); return -EEXIST; }
|
|
}
|
|
int rofs = 0;
|
|
ip = createat_x(AT_FDCWD, path,
|
|
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &rofs);
|
|
// A refusal because the filesystem is read-only is not "no such file".
|
|
// A shell prints the same sentence for every create failure, so
|
|
// returning ENOENT here made writing to a snapshot look like a missing
|
|
// directory.
|
|
if (!ip) { end_op(); return rofs ? -EROFS : -ENOENT; }
|
|
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
|
|
itrunc(ip);
|
|
} else {
|
|
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
|
|
ilock(ip);
|
|
// Directories may only be opened for reading, but callers (e.g.
|
|
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
|
|
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
|
|
if (ip->type == T_DIR && (omode & O_ACCMODE) != O_RDONLY) { iunlockput(ip); end_op(); return -EISDIR; }
|
|
if ((omode & O_TRUNC) && ip->type == T_FILE) itrunc(ip);
|
|
}
|
|
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
|
|
if (f) fileclose(f);
|
|
iunlockput(ip); end_op(); return -EMFILE;
|
|
}
|
|
iunlock(ip); end_op();
|
|
f->type = FD_INODE; f->ip = ip;
|
|
f->off = (omode & O_APPEND) ? ip->size : 0;
|
|
f->readable = !(omode & O_WRONLY);
|
|
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
|
|
f->nonblock = (omode & O_NONBLOCK) != 0;
|
|
// Tell the device a descriptor was opened. /dev/input needs this to drop
|
|
// events that queued while nobody was listening; see inputopen().
|
|
if (ip->type == T_DEV && ip->major >= 0 && ip->major < NDEV &&
|
|
devsw[ip->major].open)
|
|
devsw[ip->major].open(ip);
|
|
return fd;
|
|
}
|
|
|
|
addr_t sys_openat(void)
|
|
{
|
|
int dirfd;
|
|
char *path;
|
|
int omode, fd;
|
|
struct file *f;
|
|
struct inode *ip;
|
|
argint(0, &dirfd); // AT_FDCWD = -100, or a real fd to resolve `path` from
|
|
if (argstr(1, &path) < 0 || argint(2, &omode) < 0) return -EFAULT;
|
|
begin_op();
|
|
if (omode & O_CREATE) {
|
|
// O_EXCL means "create it or fail", so the file existing is the error.
|
|
if (omode & O_EXCL) {
|
|
struct inode *ex = at_namei(dirfd, path);
|
|
if (ex) { iput(ex); end_op(); return -EEXIST; }
|
|
}
|
|
if (!(ip = createat(dirfd, path, T_FILE, 0, 0))) { end_op(); return -ENOENT; }
|
|
} else {
|
|
if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; }
|
|
ilock(ip);
|
|
// Directories may only be opened for reading, but callers (e.g.
|
|
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
|
|
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
|
|
if (ip->type == T_DIR && (omode & O_ACCMODE) != O_RDONLY) { iunlockput(ip); end_op(); return -EISDIR; }
|
|
}
|
|
// O_TRUNC and O_APPEND used to be dropped on this path while sys_open
|
|
// honoured both, so the same shell redirection behaved differently
|
|
// depending on which entry point libc happened to call.
|
|
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
|
|
itrunc(ip);
|
|
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
|
|
if (f) fileclose(f);
|
|
iunlockput(ip); end_op(); return -EMFILE;
|
|
}
|
|
uint64 start = (omode & O_APPEND) ? ip->size : 0;
|
|
iunlock(ip); end_op();
|
|
f->type = FD_INODE; f->ip = ip; f->off = start;
|
|
f->readable = !(omode & O_WRONLY);
|
|
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
|
|
f->nonblock = (omode & O_NONBLOCK) != 0;
|
|
// Tell the device a descriptor was opened. /dev/input needs this to drop
|
|
// events that queued while nobody was listening; see inputopen().
|
|
if (ip->type == T_DEV && ip->major >= 0 && ip->major < NDEV &&
|
|
devsw[ip->major].open)
|
|
devsw[ip->major].open(ip);
|
|
return fd;
|
|
}
|
|
|
|
addr_t sys_fstat(void)
|
|
{
|
|
struct file *f;
|
|
char *st;
|
|
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
|
|
if (argfd(0, 0, &f) < 0 || argptr(1, &st, size) < 0) return -1;
|
|
if (f->type != FD_INODE) return -1;
|
|
ilock(f->ip);
|
|
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
|
|
else stati(f->ip, (struct stat*)st);
|
|
iunlock(f->ip);
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_stat(void)
|
|
{
|
|
char *path;
|
|
char *st;
|
|
struct inode *ip;
|
|
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
|
|
if (argstr(0, &path) < 0 || argptr(1, &st, size) < 0) return -1;
|
|
begin_op();
|
|
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
|
|
ilock(ip);
|
|
if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st);
|
|
else stati(ip, (struct stat*)st);
|
|
iunlockput(ip);
|
|
end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_lstat(void) { return sys_stat(); }
|
|
|
|
addr_t sys_access(void)
|
|
{
|
|
char *path;
|
|
if (argstr(0, &path) < 0) return -1;
|
|
begin_op();
|
|
struct inode *ip = namei(path);
|
|
if (!ip) { end_op(); return -ENOENT; }
|
|
iput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_chdir(void)
|
|
{
|
|
char *path;
|
|
struct inode *ip;
|
|
begin_op();
|
|
if (argstr(0, &path) < 0) { end_op(); return -EFAULT; }
|
|
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
|
|
ilock(ip);
|
|
// ENOTDIR, not a bare -1. `cd` into a file must fail, and it did, but the
|
|
// shell reported "Operation not permitted" -- which reads like a
|
|
// permissions problem and sends you looking in entirely the wrong place.
|
|
if (ip->type != T_DIR) { iunlockput(ip); end_op(); return -ENOTDIR; }
|
|
iunlock(ip); iput(myproc()->cwd); end_op();
|
|
myproc()->cwd = ip;
|
|
if (path[0] == '/') {
|
|
memmove(myproc()->cwd_path, path, strnlen(path, 255));
|
|
myproc()->cwd_path[255] = 0;
|
|
} else if (strncmp(path, "..", 3) == 0) {
|
|
int n = strnlen(myproc()->cwd_path, 255);
|
|
if (n > 1) { while (n > 0 && myproc()->cwd_path[n-1] != '/') n--; if (n > 1) n--; myproc()->cwd_path[n] = 0; }
|
|
} else if (strncmp(path, ".", 2) != 0) {
|
|
int n = strnlen(myproc()->cwd_path, 255), pn = strnlen(path, 255);
|
|
if (n > 1 && n + 1 + pn < 255) { myproc()->cwd_path[n] = '/'; memmove(myproc()->cwd_path + n + 1, path, pn + 1); }
|
|
else if (n == 1 && 1 + pn < 255) memmove(myproc()->cwd_path + 1, path, pn + 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_fchdir(void)
|
|
{
|
|
struct file *f;
|
|
if (argfd(0, 0, &f) < 0 || f->type != FD_INODE) return -1;
|
|
ilock(f->ip);
|
|
if (f->ip->type != T_DIR) { iunlock(f->ip); return -1; }
|
|
iunlock(f->ip);
|
|
iput(myproc()->cwd); idup(f->ip); myproc()->cwd = f->ip;
|
|
myproc()->cwd_path[0] = '/'; myproc()->cwd_path[1] = 0;
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_getcwd(void)
|
|
{
|
|
addr_t ubuf;
|
|
int size;
|
|
argaddr(0, &ubuf); argint(1, &size);
|
|
if (size <= 0) return -EINVAL;
|
|
int n = strnlen(myproc()->cwd_path, 255) + 1;
|
|
if (n > size) return -ERANGE;
|
|
if (copy_to_user(ubuf, myproc()->cwd_path, n) < 0) return -EFAULT;
|
|
// Real Linux getcwd(2) returns the length written (including the NUL),
|
|
// not the buffer address — musl's getcwd() wrapper checks buf[0]=='/'
|
|
// AND treats a 0 return as failure, but callers relying on the actual
|
|
// byte count (or the raw syscall() return value directly) need this.
|
|
return n;
|
|
}
|
|
|
|
addr_t sys_mkdir(void)
|
|
{
|
|
char *path;
|
|
struct inode *ip;
|
|
if (argstr(0, &path) < 0) return -EFAULT;
|
|
begin_op();
|
|
if ((ip = namei(path)) != 0) { // already there: mkdir -p relies on this
|
|
iput(ip);
|
|
end_op();
|
|
return -EEXIST;
|
|
}
|
|
if (!(ip = create(path, T_DIR, 0, 0))) { end_op(); return -ENOENT; }
|
|
iunlockput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_mknod(void)
|
|
{
|
|
struct inode *ip;
|
|
char *path;
|
|
int major, minor;
|
|
begin_op();
|
|
if (argstr(0, &path) < 0 || argint(1, &major) < 0 || argint(2, &minor) < 0 ||
|
|
!(ip = create(path, T_DEV, major, minor))) { end_op(); return -1; }
|
|
iunlockput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_link(void)
|
|
{
|
|
char name[DIRSIZ], *new, *old;
|
|
struct inode *dp, *ip;
|
|
if (argstr(0, &old) < 0 || argstr(1, &new) < 0) return -1;
|
|
begin_op();
|
|
if (!(ip = namei(old))) { end_op(); return -1; }
|
|
ilock(ip);
|
|
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
|
|
ip->nlink++; iupdate(ip); iunlock(ip);
|
|
if (!(dp = nameiparent(new, name))) goto bad_link;
|
|
ilock(dp);
|
|
if (dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0) { iunlockput(dp); goto bad_link; }
|
|
iunlockput(dp); iput(ip); end_op();
|
|
return 0;
|
|
bad_link:
|
|
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op();
|
|
return -1;
|
|
}
|
|
|
|
addr_t sys_unlink(void)
|
|
{
|
|
struct inode *ip, *dp;
|
|
char name[DIRSIZ], *path;
|
|
uint off;
|
|
if (argstr(0, &path) < 0) return -1;
|
|
begin_op();
|
|
if (!(dp = nameiparent(path, name))) { end_op(); return -1; }
|
|
ilock(dp);
|
|
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) goto bad_unlink;
|
|
if (!(ip = dirlookup(dp, name, &off))) goto bad_unlink;
|
|
ilock(ip);
|
|
if (ip->nlink < 1) panic("unlink: nlink < 1");
|
|
if (ip->type == T_DIR && dirempty(ip) != 1) { iunlockput(ip); goto bad_unlink; }
|
|
if (dirunlink(dp, name, off) < 0) { iunlockput(ip); goto bad_unlink; }
|
|
if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); }
|
|
iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip);
|
|
end_op(); return 0;
|
|
bad_unlink: iunlockput(dp); end_op(); return -1;
|
|
}
|
|
|
|
addr_t sys_exec(void)
|
|
{
|
|
char *path, *argv[MAXARG], *envp[MAXENV];
|
|
addr_t uargv, uenvp, uarg;
|
|
if (argstr(0, &path) < 0 || argaddr(1, &uargv) < 0) return -EFAULT;
|
|
memset(argv, 0, sizeof(argv));
|
|
for (int i = 0; ; i++) {
|
|
if (i >= NELEM(argv)) return -E2BIG;
|
|
if (fetchaddr(uargv + sizeof(addr_t) * i, &uarg) < 0) return -EFAULT;
|
|
if (!uarg) { argv[i] = 0; break; }
|
|
if (fetchstr(uarg, &argv[i]) < 0) return -EFAULT;
|
|
}
|
|
|
|
// The third argument. This is Linux's execve(2), so musl has been passing
|
|
// an environment here all along -- it was simply never read, and every
|
|
// program on the system started with an empty one.
|
|
//
|
|
// A null envp is legal and means exactly that. It is also what the
|
|
// first-party exec() wrapper passes, so old callers keep their old
|
|
// behaviour without knowing about this.
|
|
memset(envp, 0, sizeof(envp));
|
|
if (argaddr(2, &uenvp) == 0 && uenvp) {
|
|
for (int i = 0; ; i++) {
|
|
if (i >= NELEM(envp)) return -E2BIG;
|
|
if (fetchaddr(uenvp + sizeof(addr_t) * i, &uarg) < 0) return -EFAULT;
|
|
if (!uarg) { envp[i] = 0; break; }
|
|
if (fetchstr(uarg, &envp[i]) < 0) return -EFAULT;
|
|
}
|
|
}
|
|
return exec(path, argv, envp);
|
|
}
|
|
|
|
// ftruncate/truncate both grow and shrink. Growing used to be quietly dropped
|
|
// here; itrunc_to now takes it, and each filesystem decides what it can do --
|
|
// SchFS extends into a hole, v1 refuses because it has no way to represent one.
|
|
addr_t sys_ftruncate(void)
|
|
{
|
|
struct file *f;
|
|
addr_t length;
|
|
if (argfd(0, 0, &f) < 0 || argaddr(1, &length) < 0) return -1;
|
|
if ((int64)length < 0) return -EINVAL;
|
|
if (f->type != FD_INODE || !f->writable) return -1;
|
|
begin_op(); ilock(f->ip);
|
|
itrunc_to(f->ip, (uint64)length);
|
|
iunlock(f->ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_truncate(void)
|
|
{
|
|
char *path;
|
|
addr_t length;
|
|
struct inode *ip;
|
|
if (argstr(0, &path) < 0 || argaddr(1, &length) < 0) return -1;
|
|
if ((int64)length < 0) return -EINVAL;
|
|
begin_op();
|
|
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
|
|
ilock(ip);
|
|
itrunc_to(ip, (uint64)length);
|
|
iunlockput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_rename(void)
|
|
{
|
|
char *oldpath, *newpath;
|
|
struct inode *ip, *dp_old, *dp_new, *ip_src;
|
|
char oldname[DIRSIZ], newname[DIRSIZ];
|
|
uint off;
|
|
struct dirent de;
|
|
if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -1;
|
|
begin_op();
|
|
if (!(ip = namei(oldpath))) { end_op(); return -1; }
|
|
ilock(ip);
|
|
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
|
|
ip->nlink++; iupdate(ip); iunlock(ip);
|
|
dp_old = nameiparent(oldpath, oldname);
|
|
dp_new = nameiparent(newpath, newname);
|
|
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
|
|
if (dp_old) iput(dp_old);
|
|
if (dp_new) iput(dp_new);
|
|
goto bad_rename;
|
|
}
|
|
ilock(dp_new);
|
|
if (dirlink(dp_new, newname, ip->inum) < 0) { iunlockput(dp_new); iput(dp_old); goto bad_rename; }
|
|
iunlockput(dp_new);
|
|
ilock(dp_old);
|
|
if (!(ip_src = dirlookup(dp_old, oldname, &off))) {
|
|
iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
|
|
}
|
|
iput(ip_src);
|
|
memset(&de, 0, sizeof(de));
|
|
if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de)) panic("rename: writei");
|
|
iunlockput(dp_old);
|
|
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
|
|
end_op(); return 0;
|
|
bad_rename:
|
|
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
|
|
}
|
|
|
|
addr_t sys_symlink(void)
|
|
{
|
|
char *target, *linkpath;
|
|
if (argstr(0, &target) < 0 || argstr(1, &linkpath) < 0) return -1;
|
|
begin_op();
|
|
struct inode *ip = create(linkpath, T_SYMLINK, 0, 0);
|
|
if (!ip) { end_op(); return -1; }
|
|
int len = strlen(target);
|
|
if (writei(ip, 0, (uint64)target, 0, len) != len) { iunlockput(ip); end_op(); return -1; }
|
|
iunlockput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_readlink(void)
|
|
{
|
|
char *path, *buf;
|
|
int bufsiz;
|
|
if (argstr(0, &path) < 0 || argint(2, &bufsiz) < 0 || bufsiz <= 0) return -1;
|
|
if (argptr(1, &buf, bufsiz) < 0) return -1;
|
|
struct inode *ip;
|
|
if (!(ip = namei(path))) return -ENOENT;
|
|
ilock(ip);
|
|
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
|
|
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
|
|
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -1; }
|
|
iunlockput(ip);
|
|
return n;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// getdents64 — Linux directory reading
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t
|
|
sys_getdents64(void)
|
|
{
|
|
struct file *f;
|
|
addr_t ubuf;
|
|
int count;
|
|
if (argfd(0, 0, &f) < 0 || argaddr(1, &ubuf) < 0 || argint(2, &count) < 0)
|
|
return -1;
|
|
if (f->type != FD_INODE) return -EBADF;
|
|
if (count <= 0) return -EINVAL;
|
|
// Validate the whole output window once; records are then written through
|
|
// the raw user VA, which is safe across page boundaries.
|
|
char *out = (char*)user_ptr(ubuf, (uint64)count);
|
|
if (!out) return -EFAULT;
|
|
|
|
struct inode *ip = f->ip;
|
|
ilock(ip);
|
|
if (ip->type != T_DIR) { iunlock(ip); return -ENOTDIR; }
|
|
// How a directory is laid out is the filesystem's business, and the two
|
|
// here could hardly differ more: v1 is a hash table of fixed 16-byte slots
|
|
// capped at 14-character names, SchFS is a range of B-tree keys holding
|
|
// names up to 255 bytes. Both produce the same record stream, and f->off is
|
|
// whatever position each one needs in order to resume.
|
|
int written = getdents(ip, out, count, &f->off);
|
|
iunlock(ip);
|
|
return written < 0 ? -EIO : written;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Custom SchOSFS ops
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_delete(void)
|
|
{
|
|
struct inode *ip, *dp, *ip2;
|
|
char name[DIRSIZ], *path;
|
|
uint off;
|
|
if (argstr(0, &path) < 0) return -1;
|
|
begin_op();
|
|
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
|
|
ilock(ip);
|
|
if (ip->showmode == 1) {
|
|
ip->showmode = 0; iupdate(ip); iunlockput(ip); end_op();
|
|
cprintf("moved to recycle bin\n");
|
|
return 0;
|
|
}
|
|
iunlockput(ip);
|
|
if (!(dp = nameiparent(path, name))) { end_op(); return -1; }
|
|
ilock(dp);
|
|
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) { iunlockput(dp); end_op(); return -1; }
|
|
if (!(ip2 = dirlookup(dp, name, &off))) { iunlockput(dp); end_op(); return -1; }
|
|
ilock(ip2);
|
|
if (ip2->nlink < 1) panic("delete: nlink < 1");
|
|
if (ip2->type == T_DIR && dirempty(ip2) != 1) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
|
|
if (dirunlink(dp, name, off) < 0) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
|
|
if (ip2->type == T_DIR) { dp->nlink--; iupdate(dp); }
|
|
iunlockput(dp); ip2->nlink--; iupdate(ip2); iunlockput(ip2);
|
|
end_op(); cprintf("deleted forever\n");
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_show(void)
|
|
{
|
|
char *path;
|
|
struct inode *ip;
|
|
if (argstr(0, &path) < 0) return -1;
|
|
begin_op();
|
|
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
|
|
ilock(ip); ip->showmode = 1; iupdate(ip); iunlockput(ip);
|
|
end_op(); return 0;
|
|
}
|
|
|
|
addr_t sys_showstat(void)
|
|
{
|
|
char *path;
|
|
struct stat *st;
|
|
struct inode *ip;
|
|
if (argstr(0, &path) < 0 || argptr(1, (void*)&st, sizeof(*st)) < 0) return -1;
|
|
if (!(ip = namei(path))) return -ENOENT;
|
|
ilock(ip); stati(ip, st); iunlockput(ip);
|
|
cprintf("\nfile type: ");
|
|
switch (st->type) {
|
|
case T_FILE: cprintf("T_FILE\n"); break;
|
|
case T_DIR: cprintf("T_DIR\n"); break;
|
|
case T_EXTENT: cprintf("T_EXTENT\n"); break;
|
|
case T_DEV: cprintf("T_DEVICE\n"); break;
|
|
default: cprintf("UNKNOWN\n"); break;
|
|
}
|
|
cprintf("disk device: %d\ninode: %d\nlinks: %d\nsize(B): %d\n",
|
|
st->dev, st->ino, st->nlink, st->size);
|
|
if (st->type == T_EXTENT) {
|
|
for (int ad = 0; st->addrs[ad] && ad < NDIRECT+1; ad++) {
|
|
cprintf("extent ptr: 0x%x len: %d\n",
|
|
(st->addrs[ad] & ~0xff) >> 8, st->addrs[ad] & 0xff);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Device I/O
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_vga_sync(void)
|
|
{
|
|
addr_t addr;
|
|
if (argaddr(0, &addr) < 0) return -1;
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_getmouse(void)
|
|
{
|
|
struct mouse_data *m;
|
|
if (argptr(0, (void*)&m, sizeof(*m)) < 0) return -1;
|
|
m->x = get_mouse_x(); m->y = get_mouse_y(); m->buttons = get_mouse_buttons();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_ioctl(void)
|
|
{
|
|
int fd;
|
|
addr_t request, argp;
|
|
struct file *f;
|
|
if (argfd(0, &fd, &f) < 0) return -1;
|
|
argaddr(1, &request); argaddr(2, &argp);
|
|
int is_dev = (f->type == FD_INODE && f->ip && f->ip->type == 3);
|
|
|
|
// FICLONE: make this file share another's data blocks. Linux's spelling,
|
|
// because `cp --reflink` and every other tool that knows about cloning
|
|
// already asks for it by this number -- inventing our own would mean
|
|
// nothing could use the feature without being patched.
|
|
//
|
|
// The argument is a *file descriptor*, not a pointer, which is why it is
|
|
// read before the argp-shaped handling below.
|
|
if (request == FICLONE) {
|
|
struct file *sf;
|
|
struct proc *pr = myproc();
|
|
int sfd = (int)argp;
|
|
|
|
if (f->type != FD_INODE || !f->ip || !f->writable)
|
|
return -EBADF;
|
|
if (sfd < 0 || sfd >= NOFILE || (sf = pr->ofile[sfd]) == 0)
|
|
return -EBADF;
|
|
if (sf->type != FD_INODE || !sf->ip || !sf->readable)
|
|
return -EBADF;
|
|
// Both sides must be on the same filesystem: a clone shares blocks, and
|
|
// blocks belong to one volume.
|
|
if (sf->ip->dev != f->ip->dev)
|
|
return -EXDEV;
|
|
if (!SCHFS_IS_VOL_DEV(f->ip->dev))
|
|
return -EOPNOTSUPP;
|
|
if (f->ip->type != T_FILE || sf->ip->type != T_FILE)
|
|
return -EINVAL;
|
|
|
|
int r = schfs_kreflink(f->ip->dev, sf->ip->inum, f->ip->inum);
|
|
if (r != 0)
|
|
return -EIO;
|
|
|
|
// The destination inode's size changed underneath the VFS's copy.
|
|
ilock(f->ip);
|
|
f->ip->size = sf->ip->size;
|
|
iunlock(f->ip);
|
|
return 0;
|
|
}
|
|
|
|
// /dev/fb0 (major 2) — geometry queries. This has to come before the
|
|
// catch-all below, which answers every non-tty ioctl with 0: a program
|
|
// asking FBIOGET_VSCREENINFO would have been told "success" and then
|
|
// read an untouched buffer, i.e. a 0x0 screen, which looks like a
|
|
// driver bug rather than a missing one.
|
|
if (is_dev && f->ip->major == 2)
|
|
return fb_ioctl((int)request, argp);
|
|
if (is_dev && f->ip->major == INPUT_MAJOR)
|
|
return input_ioctl(f->ip, (int)request, argp);
|
|
|
|
int is_tty = (is_dev && f->ip->major == 1);
|
|
if (!is_tty) {
|
|
// -ENOTTY, not 0.
|
|
//
|
|
// This used to answer every unrecognised request with success, which
|
|
// is a lie with consequences: a program asking TIOCGWINSZ on a *pipe*
|
|
// was told "fine" and read back an untouched buffer, concluded it was
|
|
// talking to a terminal, and formatted for one. That is why
|
|
// `ls /bin | wc -l` reported 48 -- ls was printing five columns into
|
|
// a pipe. isatty() is built on exactly this call.
|
|
return -ENOTTY;
|
|
}
|
|
if (request == TCGETS) {
|
|
struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
|
|
if (!tp) return -EFAULT;
|
|
// The real thing, not a hardcoded snapshot: whatever was last set.
|
|
cons_get_termios(f->ip->minor, tp);
|
|
return 0;
|
|
}
|
|
if (request == TCSETS || request == TCSETSW || request == TCSETSF) {
|
|
struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
|
|
if (!tp) return -EFAULT;
|
|
cons_set_termios(f->ip->minor, tp);
|
|
return 0;
|
|
}
|
|
if (request == TIOCGWINSZ) {
|
|
struct winsize *ws = (struct winsize*)user_ptr(argp, sizeof(*ws));
|
|
if (!ws) return -EFAULT;
|
|
// The terminal's own grid, and nothing else.
|
|
//
|
|
// This used to answer 24x80 unconditionally -- the size of a terminal
|
|
// SchOS has never drawn -- and was then fixed to compute the answer
|
|
// live from gfx_cols()/gfx_rows(). That was a second source of truth,
|
|
// and it disagreed with the first: the grids are sized once at
|
|
// vt_init(), which runs before the display driver is found on the PCI
|
|
// bus, so they were 80x24 while this reported 160x50. Programs
|
|
// formatted for a screen the grid did not have -- `ls` split words
|
|
// across the wrap, text stopped two thirds of the way down, and zsh's
|
|
// cursor tracking was wrong on every line.
|
|
//
|
|
// One number, from the terminal being asked. They cannot disagree.
|
|
struct vt *v = vt_for_minor(f->ip->minor);
|
|
ws->ws_col = (uint16)v->cols;
|
|
ws->ws_row = (uint16)v->rows;
|
|
ws->ws_xpixel = (uint16)(v->cols * gfx_char_w());
|
|
ws->ws_ypixel = (uint16)(v->rows * gfx_char_h());
|
|
return 0;
|
|
}
|
|
// The console's size is a property of the framebuffer, so a request to
|
|
// change it cannot be honoured -- but it is not an error either: programs
|
|
// set it to propagate a size they already learned. Accept and ignore.
|
|
if (request == TIOCSWINSZ) return 0;
|
|
// TCFLSH discards buffered input, output, or both. Unlike every other
|
|
// request here the argument is the queue selector *by value*, not a
|
|
// pointer -- passing it to user_ptr would fault on a legitimate call.
|
|
//
|
|
// Output needs no action: the console writes straight to the framebuffer
|
|
// and has no queue to drop. Input does, and answering 0 without draining
|
|
// it -- which is what the catch-all below did -- is how a keystroke typed
|
|
// during a command survives the flush and lands in the next prompt.
|
|
if (request == TCFLSH) {
|
|
if (argp == TCIFLUSH || argp == TCIOFLUSH)
|
|
cons_flush_input(f->ip->minor);
|
|
return 0;
|
|
}
|
|
// Controlling-terminal foreground process group.
|
|
//
|
|
// TIOCGPGRP used to answer with myproc()->pid, which happened to agree with
|
|
// the old getpgrp() stub (also myproc()->pid). Once getpgrp() started
|
|
// reporting the *real* process group, the two disagreed permanently, and
|
|
// a shell's standard startup loop
|
|
// while (tcgetpgrp(tty) != getpgrp()) kill(0, SIGTTIN);
|
|
// spun forever. Both ends now read the same variable.
|
|
if (request == TIOCGPGRP) {
|
|
int pg = cons_fg_pgrp(f->ip->minor);
|
|
put_user(pg, argp);
|
|
return 0;
|
|
}
|
|
if (request == TIOCSPGRP) {
|
|
int pg;
|
|
if (copy_from_user(&pg, argp, sizeof(pg)) < 0)
|
|
return -EFAULT;
|
|
if (pg <= 0)
|
|
return -EINVAL;
|
|
cons_set_fg_pgrp(f->ip->minor, pg);
|
|
return 0;
|
|
}
|
|
// TIOCSCTTY was `return 0` -- it claimed to make this terminal the
|
|
// caller's controlling terminal and did nothing, because there was one
|
|
// terminal and nothing to choose. It records the choice now, which is what
|
|
// /dev/tty resolves through and what a session leader on tty3 needs so
|
|
// that its /dev/tty is tty3 and not whatever is on screen.
|
|
if (request == TIOCSCTTY) {
|
|
struct vt *v = vt_for_minor(f->ip->minor);
|
|
myproc()->ctty = vt_index(v) + 1;
|
|
v->sid = myproc()->sid;
|
|
return 0;
|
|
}
|
|
|
|
// --- virtual terminal control ---------------------------------------
|
|
//
|
|
// The argument is the terminal number by value for ACTIVATE and
|
|
// WAITACTIVE, and a pointer for GETSTATE and OPENQRY. Passing argp to
|
|
// user_ptr for the first two would fault on a legitimate call, which is
|
|
// the same shape of bug TCFLSH had.
|
|
if (request == VT_ACTIVATE)
|
|
return vt_switch((int)argp - 1) == 0 ? 0 : -EINVAL;
|
|
|
|
if (request == VT_WAITACTIVE) {
|
|
// Nothing to wait for: vt_switch repaints synchronously, so by the
|
|
// time VT_ACTIVATE has returned the terminal is already active.
|
|
// Answering success is honest here; blocking would be a lie about
|
|
// work that has already happened.
|
|
int want = (int)argp - 1;
|
|
if (want < 0 || want >= NVT) return -EINVAL;
|
|
return vt_active == want ? 0 : -EINVAL;
|
|
}
|
|
|
|
if (request == VT_GETSTATE) {
|
|
struct vt_stat *st = (struct vt_stat*)user_ptr(argp, sizeof(*st));
|
|
if (!st) return -EFAULT;
|
|
st->v_active = (uint16)(vt_active + 1);
|
|
st->v_signal = 0;
|
|
// Bit n set means terminal n is in use. Bit 0 is unused because
|
|
// terminals are numbered from 1.
|
|
st->v_state = 0;
|
|
for (int i = 0; i < NVT; i++)
|
|
if (vt_get(i)->opened > 0)
|
|
st->v_state |= (uint16)(1u << (i + 1));
|
|
return 0;
|
|
}
|
|
|
|
if (request == VT_OPENQRY) {
|
|
int *out = (int*)user_ptr(argp, sizeof(int));
|
|
if (!out) return -EFAULT;
|
|
for (int i = 0; i < NVT; i++)
|
|
if (vt_get(i)->opened == 0) { *out = i + 1; return 0; }
|
|
*out = -1;
|
|
return -ENFILE;
|
|
}
|
|
|
|
// VT_GETMODE/VT_SETMODE: who drives switching.
|
|
//
|
|
// These were accepted and ignored, which was a lie with a consequence --
|
|
// a program could ask for the chord and be told yes while the kernel kept
|
|
// taking it. VT_PROCESS now genuinely stops the kernel switching, so an
|
|
// application that wants Ctrl+Alt+Shift+F2 can have it.
|
|
if (request == VT_SETMODE) {
|
|
struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m));
|
|
if (!m) return -EFAULT;
|
|
if (m->mode != VT_AUTO && m->mode != VT_PROCESS) return -EINVAL;
|
|
vt_for_minor(f->ip->minor)->mode = m->mode;
|
|
return 0;
|
|
}
|
|
// Which chord the kernel switches on. SchOS's own ioctl -- see the long
|
|
// comment in include/kbd.h for why this has to be settable at all.
|
|
if (request == VT_SETCHORD) {
|
|
struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c));
|
|
if (!c) return -EFAULT;
|
|
// Only real modifier bits, and a base that leaves room for NVT keys
|
|
// below the release flag. A base of 0 with no modifiers is the
|
|
// documented way to turn the chord off, so it is not an error.
|
|
if (c->mods & ~(VTC_SHIFT | VTC_CTRL | VTC_ALT | VTC_RALT))
|
|
return -EINVAL;
|
|
if (c->mods && (c->base == 0 || c->base + NVT > 0x80))
|
|
return -EINVAL;
|
|
vt_chord_mods = c->mods;
|
|
vt_chord_base = c->base;
|
|
return 0;
|
|
}
|
|
if (request == VT_GETCHORD) {
|
|
struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c));
|
|
if (!c) return -EFAULT;
|
|
c->mods = (uint16)vt_chord_mods;
|
|
c->base = (uint16)vt_chord_base;
|
|
return 0;
|
|
}
|
|
|
|
if (request == VT_GETMODE) {
|
|
struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m));
|
|
if (!m) return -EFAULT;
|
|
memset(m, 0, sizeof(*m));
|
|
m->mode = (char)vt_for_minor(f->ip->minor)->mode;
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// POSIX filesystem extensions
|
|
// -----------------------------------------------------------------------
|
|
// (at_namei/at_nameiparent/at_dir are defined above, next to create()/
|
|
// createat() — every *at() syscall below uses them for dirfd resolution.)
|
|
|
|
// chmod / fchmod / chown / fchown / lchown — stubs (no permission model yet).
|
|
addr_t sys_chmod(void) { return 0; }
|
|
addr_t sys_fchmod(void) { return 0; }
|
|
addr_t sys_chown(void) { return 0; }
|
|
addr_t sys_fchown(void) { return 0; }
|
|
addr_t sys_lchown(void) { return 0; }
|
|
|
|
// rmdir: remove an empty directory (same as unlink for dirs).
|
|
addr_t sys_rmdir(void)
|
|
{
|
|
char *path;
|
|
if (argstr(0, &path) < 0) return -1;
|
|
// Reuse sys_unlink semantics — it already handles T_DIR + isdirempty check.
|
|
return sys_unlink();
|
|
}
|
|
|
|
// fsync / fdatasync / sync — stubs (log-based FS commits on every op).
|
|
// sync / fsync / fdatasync.
|
|
//
|
|
// These returned 0 without doing anything, which was defensible while every
|
|
// write reached the disk before its syscall returned. It is not defensible now:
|
|
// tree blocks are marked dirty in the buffer cache and pushed out at commit, so
|
|
// "flush what is outstanding" finally names a real action. An empty transaction
|
|
// drives the same barrier a real one does -- the dirty buffers go out and the
|
|
// device is told to commit them.
|
|
addr_t sys_sync(void)
|
|
{
|
|
schfs_ksync_all();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_fsync(void)
|
|
{
|
|
struct file *f;
|
|
if (argfd(0, 0, &f) < 0) return -EBADF;
|
|
// Whole-volume rather than per-file: SchFS has no way to make one object
|
|
// durable on its own. A transaction group is the unit that commits, and
|
|
// this file's last write is in whichever group is open.
|
|
if (f->type == FD_INODE && f->ip && SCHFS_IS_VOL_DEV(f->ip->dev))
|
|
return schfs_ksync(f->ip->dev) == 0 ? 0 : -EIO;
|
|
return 0;
|
|
}
|
|
|
|
// No separate data-only path: the inode record and the data it describes are
|
|
// records in the same tree and commit together. Reporting success for a weaker
|
|
// guarantee than was asked for would be the wrong way round; this is stronger.
|
|
addr_t sys_fdatasync(void){ return sys_fsync(); }
|
|
|
|
// pread: read from fd at given offset without changing file position.
|
|
addr_t sys_pread(void)
|
|
{
|
|
struct file *f;
|
|
int n;
|
|
char *p;
|
|
uint64 offset;
|
|
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 ||
|
|
argptr(1, &p, n) < 0 || argaddr(3, &offset) < 0) return -1;
|
|
if (f->type != FD_INODE) return -1;
|
|
ilock(f->ip);
|
|
int r = readi(f->ip, 0, (uint64)p, (uint)offset, n);
|
|
iunlock(f->ip);
|
|
return r;
|
|
}
|
|
|
|
// pwrite: write to fd at given offset without changing file position.
|
|
addr_t sys_pwrite(void)
|
|
{
|
|
struct file *f;
|
|
int n;
|
|
char *p;
|
|
uint64 offset;
|
|
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 ||
|
|
argptr(1, &p, n) < 0 || argaddr(3, &offset) < 0) return -1;
|
|
if (f->type != FD_INODE || !f->writable) return -1;
|
|
begin_op(); ilock(f->ip);
|
|
int r = writei(f->ip, 0, (uint64)p, (uint)offset, n);
|
|
iunlock(f->ip); end_op();
|
|
return r;
|
|
}
|
|
|
|
// utime / utimes / utimensat — timestamps not tracked; stubs.
|
|
addr_t sys_utime(void) { return 0; }
|
|
addr_t sys_utimes(void) { return 0; }
|
|
addr_t sys_utimensat(void) { return 0; }
|
|
|
|
// sendfile: not implemented.
|
|
addr_t sys_sendfile(void) { return -ENOSYS; }
|
|
|
|
// preadv2 / pwritev2: scatter/gather at offset — redirect to readv/writev.
|
|
addr_t sys_preadv2(void) { return sys_readv(); }
|
|
addr_t sys_pwritev2(void){ return sys_writev(); }
|
|
|
|
// fallocate: preallocate space — we don't track this; stub success.
|
|
addr_t sys_fallocate(void) { return 0; }
|
|
|
|
// statfs / fstatfs.
|
|
//
|
|
// These used to return the same six invented constants whatever they were
|
|
// asked about -- 150000 blocks, half of them free -- so df reported a
|
|
// filesystem that did not exist and reported it identically for every mount.
|
|
// The numbers are real now, and which ones you get depends on which filesystem
|
|
// the path landed on.
|
|
static void fill_statfs(struct statfs *sf, uint dev) {
|
|
uint64 total = 0, freeb = 0, objects = 0;
|
|
|
|
memset(sf, 0, sizeof(*sf));
|
|
sf->f_type = 0x534b4658; // 'SKFX'
|
|
|
|
if (schfs_kstatfs(dev, &total, &freeb, &objects) == 0) {
|
|
sf->f_bsize = SCHFS_BLKSIZE;
|
|
sf->f_frsize = SCHFS_BLKSIZE;
|
|
sf->f_blocks = (int64)total;
|
|
sf->f_bfree = (int64)freeb;
|
|
sf->f_bavail = (int64)freeb; // no reserve, so nothing held back
|
|
// Both zero, deliberately. There is no inode table to run out of and
|
|
// the object id space is 2^64, so there is no honest pair of numbers
|
|
// here -- and df computes a "% inodes used" from them, which against a
|
|
// real f_files and a zero f_ffree would show every SchFS mount as
|
|
// completely full. Zeroes are what filesystems that allocate inodes
|
|
// dynamically report, and what df is written to expect from them.
|
|
sf->f_files = 0;
|
|
sf->f_ffree = 0;
|
|
sf->f_namelen = SCHFS_NAME_MAX;
|
|
(void)objects;
|
|
return;
|
|
}
|
|
|
|
// v1, /proc, or anything else: the old shape, but at least with a block
|
|
// size that matches the one the buffer cache actually uses.
|
|
sf->f_bsize = BSIZE;
|
|
sf->f_frsize = BSIZE;
|
|
sf->f_namelen = DIRSIZ;
|
|
}
|
|
|
|
addr_t sys_statfs(void)
|
|
{
|
|
char *path;
|
|
addr_t bufp;
|
|
if (argstr(0, &path) < 0 || argaddr(1, &bufp) < 0) return -1;
|
|
struct statfs *sf = (struct statfs*)user_ptr(bufp, sizeof(*sf));
|
|
if (!sf) return -EFAULT;
|
|
|
|
// Resolve the path: which filesystem it lands on is the whole question,
|
|
// and mount crossing happens during resolution.
|
|
begin_op();
|
|
struct inode *ip = namei(path);
|
|
if (!ip) { end_op(); return -ENOENT; }
|
|
uint dev = ip->dev;
|
|
iput(ip);
|
|
end_op();
|
|
|
|
fill_statfs(sf, dev);
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_fstatfs(void)
|
|
{
|
|
struct file *f;
|
|
addr_t bufp;
|
|
if (argfd(0, 0, &f) < 0 || argaddr(1, &bufp) < 0) return -1;
|
|
struct statfs *sf = (struct statfs*)user_ptr(bufp, sizeof(*sf));
|
|
if (!sf) return -EFAULT;
|
|
if (f->type != FD_INODE || !f->ip) return -EBADF;
|
|
fill_statfs(sf, f->ip->dev);
|
|
return 0;
|
|
}
|
|
|
|
// chroot: not supported.
|
|
addr_t sys_chroot(void) { return -EPERM; }
|
|
|
|
// -----------------------------------------------------------------------
|
|
// at-syscalls
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_mkdirat(void)
|
|
{
|
|
int dirfd;
|
|
char *path;
|
|
struct inode *ip;
|
|
argint(0, &dirfd);
|
|
if (argstr(1, &path) < 0) return -EFAULT;
|
|
begin_op();
|
|
if ((ip = at_namei(dirfd, path)) != 0) { // see sys_mkdir
|
|
iput(ip);
|
|
end_op();
|
|
return -EEXIST;
|
|
}
|
|
if (!(ip = createat(dirfd, path, T_DIR, 0, 0))) { end_op(); return -ENOENT; }
|
|
iunlockput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_mknodat(void)
|
|
{
|
|
int dirfd;
|
|
char *path;
|
|
uint64 dev;
|
|
argint(0, &dirfd);
|
|
if (argstr(1, &path) < 0) return -1;
|
|
argint(2, &(int){0}); // mode — ignored
|
|
argaddr(3, &dev);
|
|
int major = (dev >> 8) & 0xff;
|
|
int minor = dev & 0xff;
|
|
begin_op();
|
|
struct inode *ip = createat(dirfd, path, T_DEV, (short)major, (short)minor);
|
|
if (!ip) { end_op(); return -1; }
|
|
iunlockput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_unlinkat(void)
|
|
{
|
|
int dirfd, flags;
|
|
char *path;
|
|
struct inode *ip, *dp;
|
|
char name[DIRSIZ];
|
|
uint off;
|
|
argint(0, &dirfd);
|
|
if (argstr(1, &path) < 0) return -1;
|
|
argint(2, &flags);
|
|
begin_op();
|
|
if (!(dp = at_nameiparent(dirfd, path, name))) { end_op(); return -1; }
|
|
ilock(dp);
|
|
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) {
|
|
iunlockput(dp); end_op(); return -1;
|
|
}
|
|
if (!(ip = dirlookup(dp, name, &off))) {
|
|
iunlockput(dp); end_op(); return -1;
|
|
}
|
|
ilock(ip);
|
|
if (ip->nlink < 1) panic("unlinkat: nlink < 1");
|
|
if (ip->type == T_DIR && !(flags & AT_REMOVEDIR)) {
|
|
iunlockput(ip); iunlockput(dp); end_op(); return -1;
|
|
}
|
|
if (ip->type == T_DIR && dirempty(ip) != 1) {
|
|
iunlockput(ip); iunlockput(dp); end_op(); return -1;
|
|
}
|
|
if (dirunlink(dp, name, off) < 0) {
|
|
iunlockput(ip); iunlockput(dp); end_op(); return -1;
|
|
}
|
|
if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); }
|
|
iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip);
|
|
end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_renameat(void)
|
|
{
|
|
int olddirfd, newdirfd;
|
|
char *oldpath, *newpath;
|
|
argint(0, &olddirfd); argstr(1, &oldpath);
|
|
argint(2, &newdirfd); argstr(3, &newpath);
|
|
// Delegate to sys_rename internals — fake the args for rename.
|
|
// Easiest: place path pointers in registers and call rename logic directly.
|
|
struct inode *ip, *dp_old, *dp_new, *ip_src;
|
|
char oldname[DIRSIZ], newname[DIRSIZ];
|
|
uint off;
|
|
struct dirent de;
|
|
begin_op();
|
|
if (!(ip = at_namei(olddirfd, oldpath))) { end_op(); return -1; }
|
|
ilock(ip);
|
|
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
|
|
ip->nlink++; iupdate(ip); iunlock(ip);
|
|
dp_old = at_nameiparent(olddirfd, oldpath, oldname);
|
|
dp_new = at_nameiparent(newdirfd, newpath, newname);
|
|
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
|
|
if (dp_old) iput(dp_old);
|
|
if (dp_new) iput(dp_new);
|
|
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
|
|
}
|
|
ilock(dp_new);
|
|
if (dirlink(dp_new, newname, ip->inum) < 0) {
|
|
iunlockput(dp_new); iput(dp_old);
|
|
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
|
|
}
|
|
iunlockput(dp_new);
|
|
ilock(dp_old);
|
|
if (!(ip_src = dirlookup(dp_old, oldname, &off))) {
|
|
iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
|
|
}
|
|
iput(ip_src);
|
|
memset(&de, 0, sizeof(de));
|
|
if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
|
|
panic("renameat: writei");
|
|
iunlockput(dp_old);
|
|
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
|
|
end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_renameat2(void) { return sys_renameat(); } // ignore flags
|
|
|
|
addr_t sys_linkat(void)
|
|
{
|
|
int olddirfd, newdirfd;
|
|
char *old, *new;
|
|
struct inode *dp, *ip;
|
|
char name[DIRSIZ];
|
|
argint(0, &olddirfd); argstr(1, &old);
|
|
argint(2, &newdirfd); argstr(3, &new);
|
|
argint(4, &(int){0}); // flags
|
|
begin_op();
|
|
if (!(ip = at_namei(olddirfd, old))) { end_op(); return -1; }
|
|
ilock(ip);
|
|
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
|
|
ip->nlink++; iupdate(ip); iunlock(ip);
|
|
if (!(dp = at_nameiparent(newdirfd, new, name))) goto bad_linkat;
|
|
ilock(dp);
|
|
if (dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0) {
|
|
iunlockput(dp); goto bad_linkat;
|
|
}
|
|
iunlockput(dp); iput(ip); end_op();
|
|
return 0;
|
|
bad_linkat:
|
|
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op();
|
|
return -1;
|
|
}
|
|
|
|
addr_t sys_symlinkat(void)
|
|
{
|
|
char *target, *linkpath;
|
|
int newdirfd;
|
|
if (argstr(0, &target) < 0) return -1;
|
|
argint(1, &newdirfd);
|
|
if (argstr(2, &linkpath) < 0) return -1;
|
|
begin_op();
|
|
struct inode *ip = createat(newdirfd, linkpath, T_SYMLINK, 0, 0);
|
|
if (!ip) { end_op(); return -1; }
|
|
int len = strlen(target);
|
|
if (writei(ip, 0, (uint64)target, 0, len) != len) {
|
|
iunlockput(ip); end_op(); return -1;
|
|
}
|
|
iunlockput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_readlinkat(void)
|
|
{
|
|
int dirfd;
|
|
char *path, *buf;
|
|
int bufsiz;
|
|
argint(0, &dirfd);
|
|
if (argstr(1, &path) < 0) return -1;
|
|
if (argint(3, &bufsiz) < 0 || bufsiz <= 0) return -1;
|
|
if (argptr(2, &buf, bufsiz) < 0) return -1;
|
|
struct inode *ip;
|
|
if (!(ip = at_namei(dirfd, path))) return -ENOENT;
|
|
ilock(ip);
|
|
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
|
|
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
|
|
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -1; }
|
|
iunlockput(ip);
|
|
return n;
|
|
}
|
|
|
|
addr_t sys_fchmodat(void) { return 0; } // stub
|
|
addr_t sys_fchownat(void) { return 0; } // stub
|
|
|
|
addr_t sys_faccessat(void)
|
|
{
|
|
int dirfd;
|
|
char *path;
|
|
argint(0, &dirfd);
|
|
if (argstr(1, &path) < 0) return -1;
|
|
argint(2, &(int){0}); // amode — ignored
|
|
// arg3 flags — ignored
|
|
begin_op();
|
|
struct inode *ip = at_namei(dirfd, path);
|
|
if (!ip) { end_op(); return -ENOENT; }
|
|
iput(ip); end_op();
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_fstatat(void)
|
|
{
|
|
int dirfd, flags;
|
|
char *path;
|
|
char *st;
|
|
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
|
|
argint(0, &dirfd);
|
|
if (argstr(1, &path) < 0) return -1;
|
|
if (argptr(2, &st, size) < 0) return -1;
|
|
argint(3, &flags);
|
|
|
|
// AT_EMPTY_PATH: if path is empty, fstat the dirfd itself.
|
|
if ((flags & AT_EMPTY_PATH) && path[0] == '\0') {
|
|
if (dirfd < 0 || dirfd >= NOFILE || !myproc()->ofile[dirfd]) return -1;
|
|
struct file *f = myproc()->ofile[dirfd];
|
|
if (f->type != FD_INODE) return -1;
|
|
ilock(f->ip);
|
|
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
|
|
else stati(f->ip, (struct stat*)st);
|
|
iunlock(f->ip);
|
|
return 0;
|
|
}
|
|
|
|
begin_op();
|
|
struct inode *ip = at_namei(dirfd, path);
|
|
if (!ip) { end_op(); return -ENOENT; }
|
|
ilock(ip);
|
|
if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st);
|
|
else stati(ip, (struct stat*)st);
|
|
iunlockput(ip);
|
|
end_op();
|
|
return 0;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// mount / umount2
|
|
//
|
|
// A mount here is a substitution made during path resolution: nothing on
|
|
// either disk records it, so mounting is adding a row to a table and
|
|
// unmounting is removing it. There is no filesystem probing to do at mount
|
|
// time either -- SchFS's uberblock scan already ran at boot, and this only
|
|
// decides where in the namespace the result appears.
|
|
// -----------------------------------------------------------------------
|
|
|
|
addr_t sys_mount(void)
|
|
{
|
|
char *source, *target, *fstype;
|
|
if (argstr(0, &source) < 0 || argstr(1, &target) < 0 ||
|
|
argstr(2, &fstype) < 0)
|
|
return -EFAULT;
|
|
|
|
// proc: a synthetic filesystem, so there is no source device to check and
|
|
// no volume to have found at boot. `mount -t proc proc /proc`.
|
|
if (strncmp(fstype, "proc", 5) == 0) {
|
|
procfs_mount();
|
|
return 0;
|
|
}
|
|
|
|
// devtmpfs: the same, and the one init actually needs. /dev used to be
|
|
// twenty mknod() calls; see the note at the top of kernel/fs/devfs.c.
|
|
if (strncmp(fstype, "devtmpfs", 9) == 0 || strncmp(fstype, "dev", 4) == 0) {
|
|
devfs_mount();
|
|
return 0;
|
|
}
|
|
|
|
// Only SchFS. Checking the name still matters: a caller that asked for
|
|
// something else should be told no rather than quietly handed this.
|
|
if (strncmp(fstype, "schfs", 6) != 0)
|
|
return -ENODEV;
|
|
|
|
// `mount -t schfs @name /mnt` mounts a snapshot, read-only.
|
|
//
|
|
// The source finally means something. It was ignored outright before --
|
|
// there was one volume and nothing to choose between -- and a snapshot is
|
|
// the first second thing to mount. `@` rather than a path because a
|
|
// snapshot is not a device: there is no node in /dev for it and there
|
|
// should not be, since it lives inside the volume that is already mounted.
|
|
int snapmount = 0;
|
|
uint snapdev = 0;
|
|
if (source && source[0] == '@') {
|
|
const char *nm = source + 1;
|
|
unsigned nlen = 0;
|
|
while (nm[nlen] && nlen < 64)
|
|
nlen++;
|
|
if (nlen == 0)
|
|
return -EINVAL;
|
|
|
|
// Distinct codes, because "resource busy" for every possible failure
|
|
// tells the caller nothing and cost an hour of guessing once already.
|
|
//
|
|
// Snapshots are taken of the root volume: a snapshot is named inside
|
|
// the volume that holds it, and there is no syntax yet for saying which
|
|
// volume that is.
|
|
int r = schfs_ksnap_mount(SCHFS_DEV, nm, nlen);
|
|
if (r < 0) {
|
|
switch (r) {
|
|
case -1: return -ENODEV; /* no volume mounted to look in */
|
|
case -2: return -EBUSY; /* every snapshot slot in use */
|
|
case -3: return -ENOMEM; /* no scratch blocks */
|
|
default: return -ENOENT; /* no snapshot by that name */
|
|
}
|
|
}
|
|
snapdev = (uint)r;
|
|
snapmount = 1;
|
|
}
|
|
if (!schfs_kmounted())
|
|
return -ENODEV;
|
|
|
|
// The source finally names a disk.
|
|
//
|
|
// It was ignored outright before -- there was one volume and nothing to
|
|
// choose between -- so `mount -t schfs /dev/hdb /mnt` mounted whatever the
|
|
// kernel had probed at boot and said nothing about it. Now an explicit
|
|
// source is resolved, and mounting a second SchFS disk works.
|
|
//
|
|
// An empty source, or the bare word "schfs", still means the root volume:
|
|
// that is what /etc/fstab and every existing script pass.
|
|
uint fsdev_src = SCHFS_DEV;
|
|
if (!snapmount && source && source[0] && strncmp(source, "schfs", 6) != 0) {
|
|
begin_op();
|
|
struct inode *sp = namei(source);
|
|
if (!sp) { end_op(); return -ENOENT; }
|
|
ilock(sp);
|
|
// A raw disk is a character device in the devsw table (major 8, see
|
|
// kernel/dev/cdev.c) whose minor is the IDE device number.
|
|
if (sp->type != T_DEV || sp->major != 8) {
|
|
iunlockput(sp); end_op();
|
|
return -ENOTBLK;
|
|
}
|
|
uint bdev = (uint)sp->minor;
|
|
iunlockput(sp);
|
|
end_op();
|
|
|
|
int r = schfs_kmount_dev(bdev);
|
|
if (r < 0) {
|
|
// -EBUSY also covers "already mounted", which is the common typo
|
|
// and deserves to be distinguishable from "no free slot" one day.
|
|
return r;
|
|
}
|
|
fsdev_src = (uint)r;
|
|
}
|
|
|
|
uint64 root = snapmount ? schfs_ksnap_root(snapdev) : schfs_kroot(fsdev_src);
|
|
if (root == 0) {
|
|
if (snapmount)
|
|
schfs_ksnap_unmount(snapdev);
|
|
return -ENODEV;
|
|
}
|
|
|
|
begin_op();
|
|
struct inode *dp = namei(target);
|
|
if (!dp) { end_op(); return -ENOENT; }
|
|
ilock(dp);
|
|
if (dp->type != T_DIR) { iunlockput(dp); end_op(); return -ENOTDIR; }
|
|
uint dev = dp->dev;
|
|
uint64 inum = dp->inum;
|
|
iunlockput(dp);
|
|
end_op();
|
|
|
|
// Mounting a filesystem onto a directory that is already on *it* would let
|
|
// an unmount strand the covered directory behind its own mount: umount
|
|
// names the mounted root, and a self-mount makes that ambiguous.
|
|
//
|
|
// This read `dev == SCHFS_DEV`, which was the same thing while SchFS was
|
|
// the only volume there could be. A snapshot is a second device, and its
|
|
// mount point is necessarily on the live root -- there is nowhere else to
|
|
// put it -- so the old spelling refused every snapshot mount.
|
|
uint fsdev = snapmount ? snapdev : fsdev_src;
|
|
if (dev == fsdev)
|
|
return -EBUSY;
|
|
if (mount_add(dev, inum, fsdev, root, target, source, "schfs") != 0) {
|
|
if (snapmount)
|
|
schfs_ksnap_unmount(snapdev);
|
|
else if (fsdev_src != SCHFS_DEV)
|
|
schfs_kumount(fsdev_src); /* undo the volume mount we just did */
|
|
return -EBUSY;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_umount2(void)
|
|
{
|
|
char *target;
|
|
if (argstr(0, &target) < 0) return -EFAULT;
|
|
|
|
begin_op();
|
|
struct inode *dp = namei(target);
|
|
if (!dp) { end_op(); return -ENOENT; }
|
|
// Resolving the path already crossed the mount, so this is the mounted
|
|
// filesystem's root rather than the directory it covers. The table knows
|
|
// which mount point that root belongs to.
|
|
uint dev = dp->dev;
|
|
uint64 inum = dp->inum;
|
|
iput(dp);
|
|
end_op();
|
|
|
|
if (mount_del_by_root(dev, inum) != 0)
|
|
return -EINVAL;
|
|
|
|
// Release the slot with the mount. Without this the table entry went away
|
|
// and the slot stayed taken, so a volume could be unmounted and never
|
|
// mounted again until reboot.
|
|
if (SCHFS_IS_SNAP_DEV(dev))
|
|
schfs_ksnap_unmount(dev);
|
|
else if (SCHFS_IS_VOL_DEV(dev) && dev != SCHFS_DEV)
|
|
// Pushes the volume's dirty buffers out and drives a cache flush before
|
|
// letting go. The root volume (SCHFS_DEV) refuses -- there is nothing
|
|
// to fall back to -- and mount_del_by_root has already failed for it,
|
|
// since nothing mounts the root over anything.
|
|
schfs_kumount(dev);
|
|
return 0;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Attributes, and the index over them (SchFS phase 5)
|
|
//
|
|
// setxattr/getxattr/removexattr keep Linux's shape, so a program that already
|
|
// knows how to attach metadata to a file needs no changes. schfs_query is the
|
|
// one call with no Linux equivalent: it asks the *filesystem* a question
|
|
// instead of walking it, which is the whole point of building the index.
|
|
// -----------------------------------------------------------------------
|
|
|
|
// Resolve a path to a SchFS inode, and refuse anything that is not one.
|
|
// Attributes live in the tree, so there is nowhere to put them on v1 or /proc.
|
|
static int
|
|
xattr_inode(char *path, struct inode **ip)
|
|
{
|
|
begin_op();
|
|
struct inode *p = namei(path);
|
|
if (!p) { end_op(); return -ENOENT; }
|
|
if (!SCHFS_IS_VOL_DEV(p->dev)) { iput(p); end_op(); return -EOPNOTSUPP; }
|
|
end_op();
|
|
*ip = p;
|
|
return 0;
|
|
}
|
|
|
|
addr_t sys_setxattr(void)
|
|
{
|
|
char *path, *name;
|
|
addr_t uval;
|
|
int size, flags;
|
|
|
|
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
|
|
if (argaddr(2, &uval) < 0 || argint(3, &size) < 0) return -EFAULT;
|
|
argint(4, &flags); /* XATTR_CREATE/REPLACE: unused */
|
|
|
|
if (size < 0 || size > SCHFS_ATTR_VALUE_MAX) return -E2BIG;
|
|
int namelen = strlen(name);
|
|
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
|
|
|
|
char val[SCHFS_ATTR_VALUE_MAX];
|
|
if (size > 0 && copy_from_user(val, uval, (uint64)size) < 0) return -EFAULT;
|
|
|
|
struct inode *ip;
|
|
int r = xattr_inode(path, &ip);
|
|
if (r < 0) return r;
|
|
|
|
// Stored as a string: nothing here knows the caller's intent, and a string
|
|
// is the type whose equality comparison is the one a query wants.
|
|
r = schfs_kattr_set(ip->dev, ip->inum, name, (unsigned)namelen,
|
|
val, (unsigned)size, SCHFS_ATTR_STRING);
|
|
iput(ip);
|
|
return r == 0 ? 0 : -EIO;
|
|
}
|
|
|
|
addr_t sys_getxattr(void)
|
|
{
|
|
char *path, *name;
|
|
addr_t uval;
|
|
int size;
|
|
|
|
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
|
|
if (argaddr(2, &uval) < 0 || argint(3, &size) < 0) return -EFAULT;
|
|
|
|
int namelen = strlen(name);
|
|
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
|
|
|
|
struct inode *ip;
|
|
int r = xattr_inode(path, &ip);
|
|
if (r < 0) return r;
|
|
|
|
char val[SCHFS_ATTR_VALUE_MAX];
|
|
unsigned vallen = 0;
|
|
// A zero size means "how big is it?", which is how every caller sizes its
|
|
// buffer before asking again.
|
|
r = schfs_kattr_get(ip->dev, ip->inum, name, (unsigned)namelen,
|
|
size > 0 ? val : 0, (unsigned)(size > 0 ? size : 0),
|
|
&vallen);
|
|
iput(ip);
|
|
|
|
if (r == 0) return -ENODATA;
|
|
if (r == -3) return -ERANGE;
|
|
if (r != 1) return -EIO;
|
|
if (size == 0) return (addr_t)vallen;
|
|
if (copy_to_user(uval, val, vallen) < 0) return -EFAULT;
|
|
return (addr_t)vallen;
|
|
}
|
|
|
|
addr_t sys_removexattr(void)
|
|
{
|
|
char *path, *name;
|
|
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
|
|
|
|
int namelen = strlen(name);
|
|
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
|
|
|
|
struct inode *ip;
|
|
int r = xattr_inode(path, &ip);
|
|
if (r < 0) return r;
|
|
r = schfs_kattr_del(ip->dev, ip->inum, name, (unsigned)namelen);
|
|
iput(ip);
|
|
return r == 0 ? 0 : -EIO;
|
|
}
|
|
|
|
// Declare an index on an attribute name. Covers values written afterwards.
|
|
addr_t sys_schfs_index(void)
|
|
{
|
|
char *name;
|
|
if (argstr(0, &name) < 0) return -EFAULT;
|
|
int namelen = strlen(name);
|
|
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
|
|
return schfs_kindex(SCHFS_DEV, name, (unsigned)namelen) == 0 ? 0 : -EIO;
|
|
}
|
|
|
|
// Every object whose `name` attribute equals `value`, as object ids.
|
|
//
|
|
// Object ids rather than paths: the index maps values to objects, and a
|
|
// filesystem keyed that way has no cheap way back to a name. Turning an id into
|
|
// a path is a caller's problem, and a rarer one than the query itself.
|
|
addr_t sys_schfs_query(void)
|
|
{
|
|
char *name, *value;
|
|
addr_t uout;
|
|
int cap;
|
|
|
|
if (argstr(0, &name) < 0 || argstr(1, &value) < 0) return -EFAULT;
|
|
if (argaddr(2, &uout) < 0 || argint(3, &cap) < 0) return -EFAULT;
|
|
if (cap <= 0 || cap > 256) return -EINVAL;
|
|
|
|
int namelen = strlen(name), vallen = strlen(value);
|
|
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
|
|
if (vallen > SCHFS_ATTR_VALUE_MAX) return -E2BIG;
|
|
|
|
uint64 ids[64];
|
|
if (cap > (int)(sizeof(ids) / sizeof(ids[0])))
|
|
cap = (int)(sizeof(ids) / sizeof(ids[0]));
|
|
|
|
int n = schfs_kquery(SCHFS_DEV, name, (unsigned)namelen,
|
|
value, (unsigned)vallen, ids, cap);
|
|
if (n < 0) return -ENOENT; /* no such index */
|
|
if (n > 0 && copy_to_user(uout, ids, (uint64)n * sizeof(uint64)) < 0)
|
|
return -EFAULT;
|
|
return (addr_t)n;
|
|
}
|