SchOS/kernel/sys/sys_fs.c
Nexoniarz 4e3e345b41 kbd: three layouts, and the arrow keys were sitting on top of UTF-8
`∞`, `→`, `≤` and `'` could not be typed. `ą` and `ę` could. That pairing is
the whole diagnosis: KBD_UP was 0xE2 and its neighbours filled 0xE0..0xE9,
which are exactly the UTF-8 lead bytes for three-byte characters. Every
character in U+0800..U+FFFF -- most punctuation, every arrow, every maths
symbol, the whole of CJK -- arrived as a value the console read as an arrow key
and helpfully turned into `ESC [ A`. Two-byte sequences lead with 0xC4 and
nothing claimed 0xC4, so Polish came through and the rest did not.

The chord codes already knew this rule and said so in a comment. The navigation
keys predate them and were never brought in line. Stated once, in kbd.h: a key
produces either a Unicode codepoint or a value above 0x10FFFF, and nothing in
between means anything. KEY_* -> KBD_*, which also settles a silent collision
-- include/input.h defines its own KEY_HOME, KEY_UP and KEY_END as evdev
numbers, and kbd.c includes both headers, so three macros were being quietly
redefined in the one file that used them.

Then the layouts, which is what the codepoint-sized keycodes were for.

kernel/dev/keymap.c: us, pl, de. A layout is four tables -- plain, Shift,
AltGr, Shift+AltGr -- of uint32 codepoints. uint32 and not uchar is the reason
the file exists: `ó` fits in a byte and `ą` does not, and half a Polish
alphabet is not a Polish layout.

Two things deliberately are *not* tables:

  - **Ctrl is derived from the plain codepoint.** The table it replaces was
    indexed by scancode, which quietly assumed US -- on QWERTZ the Y and Z keys
    swap, so Ctrl+Z would have suspended a job only on some layouts.
  - **Caps Lock is a case toggle on the result**, so AltGr+a with Caps Lock is
    `Ą`. No second table could say that, because Caps Lock combines with AltGr
    rather than replacing it. Latin Extended-A alternates upper/lower by
    codepoint but not all the same way round -- Ł is U+0141 (odd) and Ą is
    U+0104 (even) -- so Polish lands in both halves and the parity matters.

kbdgetc() encodes to UTF-8 and returns the bytes one call at a time.
consoleintr()'s contract is bytes: the serial line hands it UTF-8 already and
it could not tell an encoded codepoint from a byte of the same value.

The layouts live in the kernel. Linux's loadkeys compiles a text keymap and
uploads it a key at a time through KDSKBENT, which puts the data in userspace
-- and a machine whose /usr has not mounted then has no layout at all. Here the
kernel always has a working keyboard and `loadkeys` only names which one.
KB_GETLAYOUT/KB_SETLAYOUT/KB_LISTLAYOUT; /etc/vconsole.conf and an
os.schos.keymap job for the boot-time choice. No dead keys, so French and
Spanish are absent rather than wrong.

/etc/locale.conf and LANG=C.UTF-8 in profile and zshenv. Only the charset half
of that is real -- musl decodes UTF-8 whatever the locale says and ships no
locale data -- but an unset LANG reads as "ASCII only" to everything that asks,
which is its own way of producing `?`.

And two symlink bugs found while chasing

    zsh: locking failed for /root/.zsh_history: operation not permitted

on every boot. symlink() is the classic atomic lock -- create the link, and if
it exists someone else holds it -- and zsh distinguishes EEXIST ("contended,
retry") from anything else ("this cannot work, give up"). sys_symlink returned
a bare -1, which is -EPERM, so every zsh on the machine took the second branch
on a filesystem that supports symlinks perfectly well. Same errno class as
commit 75ccf98.

The second one the first was hiding: **divesymlink() has been in fs.c since the
fork and nothing ever called it.** A symlink behaved as a small file containing
its own target, so `cat link` printed the target's *name*. open() and openat()
follow the final component now (O_NOFOLLOW gives ELOOP), and stat() and lstat()
stop being the same function, since which one follows is the only difference
between them. Mid-path symlinks are still not resolved -- that belongs in
namex(), which would re-enter lookup holding a directory lock.

Verified on the emulated keyboard, not just over serial: AltGr+a/e/s gives
ąęś, Shift+AltGr gives ĄĘŚ, Caps Lock+AltGr gives ĄĘŚ, de types zy for the
physical y/z keys and @€µ on AltGr. `∞ 'x' → ≤ • — …` typed over serial now
survives. Three fresh boots: 0 lock failures, 0 faults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:09:32 +02:00

2106 lines
75 KiB
C

#include "types.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "mmu.h"
#include "memlayout.h"
#include "proc.h"
#include "fs.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "fcntl.h"
#include "posix.h"
#include "syscall.h"
#include "uaccess.h"
#include "errno.h"
#include "schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
#include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */
#include "vt.h" /* the terminals TIOC*/VT_* act on */
#include "pciuser.h"
#include "keymap.h" /* KB_*LAYOUT — the keyboard layout catalogue */
#include "netuser.h"
// -----------------------------------------------------------------------
// 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
// -----------------------------------------------------------------------
// Resolve a symlink opened as the final path component.
//
// divesymlink() has been in kernel/fs/fs.c since the fork and nothing ever
// called it, so a symlink behaved as a small file containing its own target:
// `cat link` printed the target's *name* rather than its contents, and every
// program that follows a symlink to find something followed it nowhere.
//
// Only the last component. A symlink in the middle of a path -- /a/link/b --
// is still not resolved; that belongs in namex(), which would have to re-enter
// path lookup while holding a directory lock, and getting that wrong is a
// deadlock rather than a wrong answer. Final-component resolution is what
// open(), exec() and stat() need, which is nearly everything that matters.
//
// Takes a locked inode and returns a locked inode, or 0 with everything
// released -- the same contract divesymlink() already had.
static struct inode *
follow_final(struct inode *ip, int omode)
{
if (ip->type != T_SYMLINK)
return ip;
if (omode & O_NOFOLLOW) {
// POSIX: opening a symlink itself is an error unless O_PATH, which
// this kernel does not have. Saying so beats handing back the link.
iunlockput(ip);
return 0;
}
return divesymlink(ip);
}
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);
if (!(ip = follow_final(ip, omode))) { end_op(); return -ELOOP; }
// 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);
if (!(ip = follow_final(ip, omode))) { end_op(); return -ELOOP; }
// 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;
}
// 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;
begin_op();
if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; }
ilock(ip);
if (follow && ip->type == T_SYMLINK && !(ip = divesymlink(ip))) {
end_op();
return -ELOOP;
}
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;
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;
}
// 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[DIRSIZ], *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 (dirlink(dp, name, ip->inum) < 0) { iunlockput(dp); err = EEXIST; goto bad_link; }
iunlockput(dp); iput(ip); end_op();
return 0;
bad_link:
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op();
return -err;
}
// Same correction as link() and rename() above, and in the same path.
//
// **ENOENT is the one that mattered.** A program that removes a file it is not
// sure exists writes `if (unlink(p) < 0 && errno != ENOENT) complain()`, which
// is the standard shape -- and against -1/EPERM every such test took the
// complaining branch. zsh saving its history is one of them, and it went on
// reporting a failure after the history had in fact been written.
addr_t sys_unlink(void)
{
struct inode *ip, *dp;
char name[DIRSIZ], *path;
uint off;
int err = EPERM;
if (argstr(0, &path) < 0) return -EFAULT;
begin_op();
if (!(dp = nameiparent(path, name))) { end_op(); return -ENOENT; }
ilock(dp);
// "." and ".." are refused by name, which is what POSIX asks for.
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) { err = EINVAL; goto bad_unlink; }
if (!(ip = dirlookup(dp, name, &off))) { err = ENOENT; goto bad_unlink; }
ilock(ip);
if (ip->nlink < 1) panic("unlink: nlink < 1");
if (ip->type == T_DIR && dirempty(ip) != 1) { iunlockput(ip); err = ENOTEMPTY; goto bad_unlink; }
if (dirunlink(dp, name, off) < 0) { iunlockput(ip); err = EIO; 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 -err;
}
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;
int err = EPERM;
if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -EFAULT;
begin_op();
if (!(ip = namei(oldpath))) { end_op(); return -ENOENT; }
ilock(ip);
// Renaming a directory needs the ".." fixup and the loop check that are
// not written here. EISDIR names what is missing; EPERM did not.
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
ip->nlink++; iupdate(ip); iunlock(ip);
dp_old = nameiparent(oldpath, oldname);
dp_new = nameiparent(newpath, newname);
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
err = (!dp_old || !dp_new) ? ENOENT : EXDEV;
if (dp_old) iput(dp_old);
if (dp_new) iput(dp_new);
goto bad_rename;
}
ilock(dp_new);
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);
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 -err;
}
// symlink(2).
//
// The errno matters more here than anywhere else in this file, because the one
// widespread use of symlink() is as an atomic lock: create a link, and if it
// already exists someone else holds the lock. zsh's history locking is exactly
// that, and it distinguishes EEXIST ("contended, wait and retry") from
// anything else ("this cannot work, give up"). Returning a bare -1 -- which is
// -EPERM -- put every zsh on the machine in the second bucket, and printed
//
// zsh: locking failed for /root/.zsh_history: operation not permitted
//
// on every boot, on a filesystem that supports symlinks perfectly well.
static addr_t
do_symlink(int newdirfd, char *target, char *linkpath)
{
struct inode *ip;
int len;
if (*target == 0)
return -ENOENT; // an empty target resolves to nothing
begin_op();
// Distinguishing the two ways createat() returns 0 -- the name is taken,
// or the path to it does not exist -- has to be done before the create,
// because createat() does not say which.
if ((ip = at_namei(newdirfd, linkpath)) != 0) {
iput(ip);
end_op();
return -EEXIST;
}
if (!(ip = createat(newdirfd, linkpath, T_SYMLINK, 0, 0))) {
end_op();
return -ENOENT;
}
len = strlen(target);
if (writei(ip, 0, (uint64)target, 0, len) != len) {
iunlockput(ip);
end_op();
return -EIO;
}
iunlockput(ip);
end_op();
return 0;
}
addr_t sys_symlink(void)
{
char *target, *linkpath;
if (argstr(0, &target) < 0 || argstr(1, &linkpath) < 0) return -EFAULT;
return do_symlink(AT_FDCWD, target, linkpath);
}
addr_t sys_readlink(void)
{
char *path, *buf;
int bufsiz;
if (argstr(0, &path) < 0) return -EFAULT;
if (argint(2, &bufsiz) < 0 || bufsiz <= 0) return -EINVAL;
if (argptr(1, &buf, bufsiz) < 0) return -EFAULT;
struct inode *ip;
if (!(ip = namei(path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
}
// -----------------------------------------------------------------------
// getdents64 — Linux directory reading
// -----------------------------------------------------------------------
addr_t
sys_getdents64(void)
{
struct file *f;
addr_t ubuf;
int count;
if (argfd(0, 0, &f) < 0 || argaddr(1, &ubuf) < 0 || argint(2, &count) < 0)
return -1;
if (f->type != FD_INODE) return -EBADF;
if (count <= 0) return -EINVAL;
// Validate the whole output window once; records are then written through
// the raw user VA, which is safe across page boundaries.
char *out = (char*)user_ptr(ubuf, (uint64)count);
if (!out) return -EFAULT;
struct inode *ip = f->ip;
ilock(ip);
if (ip->type != T_DIR) { iunlock(ip); return -ENOTDIR; }
// How a directory is laid out is the filesystem's business, and the two
// here could hardly differ more: v1 is a hash table of fixed 16-byte slots
// capped at 14-character names, SchFS is a range of B-tree keys holding
// names up to 255 bytes. Both produce the same record stream, and f->off is
// whatever position each one needs in order to resume.
int written = getdents(ip, out, count, &f->off);
iunlock(ip);
return written < 0 ? -EIO : written;
}
// -----------------------------------------------------------------------
// Custom SchOSFS ops
// -----------------------------------------------------------------------
addr_t sys_delete(void)
{
struct inode *ip, *dp, *ip2;
char name[DIRSIZ], *path;
uint off;
if (argstr(0, &path) < 0) return -1;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip);
if (ip->showmode == 1) {
ip->showmode = 0; iupdate(ip); iunlockput(ip); end_op();
cprintf("moved to recycle bin\n");
return 0;
}
iunlockput(ip);
if (!(dp = nameiparent(path, name))) { end_op(); return -1; }
ilock(dp);
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) { iunlockput(dp); end_op(); return -1; }
if (!(ip2 = dirlookup(dp, name, &off))) { iunlockput(dp); end_op(); return -1; }
ilock(ip2);
if (ip2->nlink < 1) panic("delete: nlink < 1");
if (ip2->type == T_DIR && dirempty(ip2) != 1) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
if (dirunlink(dp, name, off) < 0) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
if (ip2->type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp); ip2->nlink--; iupdate(ip2); iunlockput(ip2);
end_op(); cprintf("deleted forever\n");
return 0;
}
addr_t sys_show(void)
{
char *path;
struct inode *ip;
if (argstr(0, &path) < 0) return -1;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip); ip->showmode = 1; iupdate(ip); iunlockput(ip);
end_op(); return 0;
}
addr_t sys_showstat(void)
{
char *path;
struct stat *st;
struct inode *ip;
if (argstr(0, &path) < 0 || argptr(1, (void*)&st, sizeof(*st)) < 0) return -1;
if (!(ip = namei(path))) return -ENOENT;
ilock(ip); stati(ip, st); iunlockput(ip);
cprintf("\nfile type: ");
switch (st->type) {
case T_FILE: cprintf("T_FILE\n"); break;
case T_DIR: cprintf("T_DIR\n"); break;
case T_EXTENT: cprintf("T_EXTENT\n"); break;
case T_DEV: cprintf("T_DEVICE\n"); break;
default: cprintf("UNKNOWN\n"); break;
}
cprintf("disk device: %d\ninode: %d\nlinks: %d\nsize(B): %d\n",
st->dev, st->ino, st->nlink, st->size);
if (st->type == T_EXTENT) {
for (int ad = 0; st->addrs[ad] && ad < NDIRECT+1; ad++) {
cprintf("extent ptr: 0x%x len: %d\n",
(st->addrs[ad] & ~0xff) >> 8, st->addrs[ad] & 0xff);
}
}
return 0;
}
// -----------------------------------------------------------------------
// Device I/O
// -----------------------------------------------------------------------
addr_t sys_vga_sync(void)
{
addr_t addr;
if (argaddr(0, &addr) < 0) return -1;
return 0;
}
addr_t sys_getmouse(void)
{
struct mouse_data *m;
if (argptr(0, (void*)&m, sizeof(*m)) < 0) return -1;
m->x = get_mouse_x(); m->y = get_mouse_y(); m->buttons = get_mouse_buttons();
return 0;
}
addr_t sys_ioctl(void)
{
int fd;
addr_t request, argp;
struct file *f;
if (argfd(0, &fd, &f) < 0) return -1;
argaddr(1, &request); argaddr(2, &argp);
int is_dev = (f->type == FD_INODE && f->ip && f->ip->type == 3);
// FICLONE: make this file share another's data blocks. Linux's spelling,
// because `cp --reflink` and every other tool that knows about cloning
// already asks for it by this number -- inventing our own would mean
// nothing could use the feature without being patched.
//
// The argument is a *file descriptor*, not a pointer, which is why it is
// read before the argp-shaped handling below.
if (request == FICLONE) {
struct file *sf;
struct proc *pr = myproc();
int sfd = (int)argp;
if (f->type != FD_INODE || !f->ip || !f->writable)
return -EBADF;
if (sfd < 0 || sfd >= NOFILE || (sf = pr->ofile[sfd]) == 0)
return -EBADF;
if (sf->type != FD_INODE || !sf->ip || !sf->readable)
return -EBADF;
// Both sides must be on the same filesystem: a clone shares blocks, and
// blocks belong to one volume.
if (sf->ip->dev != f->ip->dev)
return -EXDEV;
if (!SCHFS_IS_VOL_DEV(f->ip->dev))
return -EOPNOTSUPP;
if (f->ip->type != T_FILE || sf->ip->type != T_FILE)
return -EINVAL;
int r = schfs_kreflink(f->ip->dev, sf->ip->inum, f->ip->inum);
if (r != 0)
return -EIO;
// The destination inode's size changed underneath the VFS's copy.
ilock(f->ip);
f->ip->size = sf->ip->size;
iunlock(f->ip);
return 0;
}
// /dev/fb0 (major 2) — geometry queries. This has to come before the
// catch-all below, which answers every non-tty ioctl with 0: a program
// asking FBIOGET_VSCREENINFO would have been told "success" and then
// read an untouched buffer, i.e. a 0x0 screen, which looks like a
// driver bug rather than a missing one.
if (is_dev && f->ip->major == 2)
return fb_ioctl((int)request, argp);
if (is_dev && f->ip->major == INPUT_MAJOR)
return input_ioctl(f->ip, (int)request, argp);
if (is_dev && f->ip->major == PCIUSER_MAJOR)
return pciuser_ioctl(request, argp);
if (is_dev && f->ip->major == NETUSER_MAJOR)
return netuser_ioctl(request, argp);
int is_tty = (is_dev && f->ip->major == 1);
if (!is_tty) {
// -ENOTTY, not 0.
//
// This used to answer every unrecognised request with success, which
// is a lie with consequences: a program asking TIOCGWINSZ on a *pipe*
// was told "fine" and read back an untouched buffer, concluded it was
// talking to a terminal, and formatted for one. That is why
// `ls /bin | wc -l` reported 48 -- ls was printing five columns into
// a pipe. isatty() is built on exactly this call.
return -ENOTTY;
}
if (request == TCGETS) {
struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
if (!tp) return -EFAULT;
// The real thing, not a hardcoded snapshot: whatever was last set.
cons_get_termios(f->ip->minor, tp);
return 0;
}
if (request == TCSETS || request == TCSETSW || request == TCSETSF) {
struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
if (!tp) return -EFAULT;
cons_set_termios(f->ip->minor, tp);
return 0;
}
if (request == TIOCGWINSZ) {
struct winsize *ws = (struct winsize*)user_ptr(argp, sizeof(*ws));
if (!ws) return -EFAULT;
// The terminal's own grid, and nothing else.
//
// This used to answer 24x80 unconditionally -- the size of a terminal
// SchOS has never drawn -- and was then fixed to compute the answer
// live from gfx_cols()/gfx_rows(). That was a second source of truth,
// and it disagreed with the first: the grids are sized once at
// vt_init(), which runs before the display driver is found on the PCI
// bus, so they were 80x24 while this reported 160x50. Programs
// formatted for a screen the grid did not have -- `ls` split words
// across the wrap, text stopped two thirds of the way down, and zsh's
// cursor tracking was wrong on every line.
//
// One number, from the terminal being asked. They cannot disagree.
struct vt *v = vt_for_minor(f->ip->minor);
ws->ws_col = (uint16)v->cols;
ws->ws_row = (uint16)v->rows;
ws->ws_xpixel = (uint16)(v->cols * gfx_char_w());
ws->ws_ypixel = (uint16)(v->rows * gfx_char_h());
return 0;
}
// The console's size is a property of the framebuffer, so a request to
// change it cannot be honoured -- but it is not an error either: programs
// set it to propagate a size they already learned. Accept and ignore.
if (request == TIOCSWINSZ) return 0;
// TCFLSH discards buffered input, output, or both. Unlike every other
// request here the argument is the queue selector *by value*, not a
// pointer -- passing it to user_ptr would fault on a legitimate call.
//
// Output needs no action: the console writes straight to the framebuffer
// and has no queue to drop. Input does, and answering 0 without draining
// it -- which is what the catch-all below did -- is how a keystroke typed
// during a command survives the flush and lands in the next prompt.
if (request == TCFLSH) {
if (argp == TCIFLUSH || argp == TCIOFLUSH)
cons_flush_input(f->ip->minor);
return 0;
}
// Controlling-terminal foreground process group.
//
// TIOCGPGRP used to answer with myproc()->pid, which happened to agree with
// the old getpgrp() stub (also myproc()->pid). Once getpgrp() started
// reporting the *real* process group, the two disagreed permanently, and
// a shell's standard startup loop
// while (tcgetpgrp(tty) != getpgrp()) kill(0, SIGTTIN);
// spun forever. Both ends now read the same variable.
if (request == TIOCGPGRP) {
int pg = cons_fg_pgrp(f->ip->minor);
put_user(pg, argp);
return 0;
}
if (request == TIOCSPGRP) {
int pg;
if (copy_from_user(&pg, argp, sizeof(pg)) < 0)
return -EFAULT;
if (pg <= 0)
return -EINVAL;
cons_set_fg_pgrp(f->ip->minor, pg);
return 0;
}
// TIOCSCTTY was `return 0` -- it claimed to make this terminal the
// caller's controlling terminal and did nothing, because there was one
// terminal and nothing to choose. It records the choice now, which is what
// /dev/tty resolves through and what a session leader on tty3 needs so
// that its /dev/tty is tty3 and not whatever is on screen.
if (request == TIOCSCTTY) {
struct vt *v = vt_for_minor(f->ip->minor);
myproc()->ctty = vt_index(v) + 1;
v->sid = myproc()->sid;
return 0;
}
// --- virtual terminal control ---------------------------------------
//
// The argument is the terminal number by value for ACTIVATE and
// WAITACTIVE, and a pointer for GETSTATE and OPENQRY. Passing argp to
// user_ptr for the first two would fault on a legitimate call, which is
// the same shape of bug TCFLSH had.
if (request == VT_ACTIVATE)
return vt_switch((int)argp - 1) == 0 ? 0 : -EINVAL;
if (request == VT_WAITACTIVE) {
// Nothing to wait for: vt_switch repaints synchronously, so by the
// time VT_ACTIVATE has returned the terminal is already active.
// Answering success is honest here; blocking would be a lie about
// work that has already happened.
int want = (int)argp - 1;
if (want < 0 || want >= NVT) return -EINVAL;
return vt_active == want ? 0 : -EINVAL;
}
if (request == VT_GETSTATE) {
struct vt_stat *st = (struct vt_stat*)user_ptr(argp, sizeof(*st));
if (!st) return -EFAULT;
st->v_active = (uint16)(vt_active + 1);
st->v_signal = 0;
// Bit n set means terminal n is in use. Bit 0 is unused because
// terminals are numbered from 1.
st->v_state = 0;
for (int i = 0; i < NVT; i++)
if (vt_get(i)->opened > 0)
st->v_state |= (uint16)(1u << (i + 1));
return 0;
}
if (request == VT_OPENQRY) {
int *out = (int*)user_ptr(argp, sizeof(int));
if (!out) return -EFAULT;
for (int i = 0; i < NVT; i++)
if (vt_get(i)->opened == 0) { *out = i + 1; return 0; }
*out = -1;
return -ENFILE;
}
// VT_GETMODE/VT_SETMODE: who drives switching.
//
// These were accepted and ignored, which was a lie with a consequence --
// a program could ask for the chord and be told yes while the kernel kept
// taking it. VT_PROCESS now genuinely stops the kernel switching, so an
// application that wants Ctrl+Alt+Shift+F2 can have it.
if (request == VT_SETMODE) {
struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m));
if (!m) return -EFAULT;
if (m->mode != VT_AUTO && m->mode != VT_PROCESS) return -EINVAL;
vt_for_minor(f->ip->minor)->mode = m->mode;
return 0;
}
// Which chord the kernel switches on. SchOS's own ioctl -- see the long
// comment in include/kbd.h for why this has to be settable at all.
if (request == VT_SETCHORD) {
struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c));
if (!c) return -EFAULT;
// Only real modifier bits, and a base that leaves room for NVT keys
// below the release flag. A base of 0 with no modifiers is the
// documented way to turn the chord off, so it is not an error.
if (c->mods & ~(VTC_SHIFT | VTC_CTRL | VTC_ALT | VTC_RALT))
return -EINVAL;
if (c->mods && (c->base == 0 || c->base + NVT > 0x80))
return -EINVAL;
vt_chord_mods = c->mods;
vt_chord_base = c->base;
return 0;
}
if (request == VT_GETCHORD) {
struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c));
if (!c) return -EFAULT;
c->mods = (uint16)vt_chord_mods;
c->base = (uint16)vt_chord_base;
return 0;
}
// Which keyboard layout the console uses. One layout for the machine, not
// one per terminal: there is one keyboard, and a user who switches to
// tty3 has not changed which keys their fingers are on.
if (request == KB_SETLAYOUT) {
struct kb_layout *k = (struct kb_layout*)user_ptr(argp, sizeof(*k));
if (!k) return -EFAULT;
k->name[sizeof(k->name) - 1] = 0;
if (keymap_set(k->name) < 0)
return -EINVAL;
return 0;
}
if (request == KB_GETLAYOUT || request == KB_LISTLAYOUT) {
struct kb_layout *k = (struct kb_layout*)user_ptr(argp, sizeof(*k));
if (!k) return -EFAULT;
const struct keylayout *l;
if (request == KB_GETLAYOUT) {
l = keymap_active();
} else {
l = keymap_by_index(k->index);
if (!l) return -EINVAL; // the documented end of the list
}
safestrcpy(k->name, l->name, sizeof(k->name));
safestrcpy(k->desc, l->desc, sizeof(k->desc));
return 0;
}
if (request == VT_GETMODE) {
struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m));
if (!m) return -EFAULT;
memset(m, 0, sizeof(*m));
m->mode = (char)vt_for_minor(f->ip->minor)->mode;
return 0;
}
return 0;
}
// -----------------------------------------------------------------------
// POSIX filesystem extensions
// -----------------------------------------------------------------------
// (at_namei/at_nameiparent/at_dir are defined above, next to create()/
// createat() — every *at() syscall below uses them for dirfd resolution.)
// chmod / fchmod / chown / fchown / lchown — stubs (no permission model yet).
addr_t sys_chmod(void) { return 0; }
addr_t sys_fchmod(void) { return 0; }
addr_t sys_chown(void) { return 0; }
addr_t sys_fchown(void) { return 0; }
addr_t sys_lchown(void) { return 0; }
// rmdir: remove an empty directory (same as unlink for dirs).
addr_t sys_rmdir(void)
{
char *path;
if (argstr(0, &path) < 0) return -1;
// Reuse sys_unlink semantics — it already handles T_DIR + isdirempty check.
return sys_unlink();
}
// fsync / fdatasync / sync — stubs (log-based FS commits on every op).
// sync / fsync / fdatasync.
//
// These returned 0 without doing anything, which was defensible while every
// write reached the disk before its syscall returned. It is not defensible now:
// tree blocks are marked dirty in the buffer cache and pushed out at commit, so
// "flush what is outstanding" finally names a real action. An empty transaction
// drives the same barrier a real one does -- the dirty buffers go out and the
// device is told to commit them.
addr_t sys_sync(void)
{
schfs_ksync_all();
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 -ENOENT; }
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
ip->nlink++; iupdate(ip); iunlock(ip);
dp_old = at_nameiparent(olddirfd, oldpath, oldname);
dp_new = at_nameiparent(newdirfd, newpath, newname);
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
int e = (!dp_old || !dp_new) ? ENOENT : EXDEV;
if (dp_old) iput(dp_old);
if (dp_new) iput(dp_new);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -e;
}
ilock(dp_new);
if (dirlink(dp_new, newname, ip->inum) < 0) {
iunlockput(dp_new); iput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -EEXIST;
}
iunlockput(dp_new);
ilock(dp_old);
if (!(ip_src = dirlookup(dp_old, oldname, &off))) {
iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op(); return -ENOENT;
}
iput(ip_src);
memset(&de, 0, sizeof(de));
if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("renameat: writei");
iunlockput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op();
return 0;
}
addr_t sys_renameat2(void) { return sys_renameat(); } // ignore flags
addr_t sys_linkat(void)
{
int olddirfd, newdirfd;
char *old, *new;
struct inode *dp, *ip;
char name[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 -EFAULT;
argint(1, &newdirfd);
if (argstr(2, &linkpath) < 0) return -EFAULT;
return do_symlink(newdirfd, target, linkpath);
}
addr_t sys_readlinkat(void)
{
int dirfd;
char *path, *buf;
int bufsiz;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -EFAULT;
if (argint(3, &bufsiz) < 0 || bufsiz <= 0) return -EINVAL;
if (argptr(2, &buf, bufsiz) < 0) return -EFAULT;
struct inode *ip;
if (!(ip = at_namei(dirfd, path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
}
addr_t sys_fchmodat(void) { return 0; } // stub
addr_t sys_fchownat(void) { return 0; } // stub
addr_t sys_faccessat(void)
{
int dirfd;
char *path;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &(int){0}); // amode — ignored
// arg3 flags — ignored
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;
}
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.
// -----------------------------------------------------------------------
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;
}