SchOS/kernel/sys/sys_fs.c
Nexoniarz 1c1b0c9570 tty: six virtual terminals, and the plumbing that makes them terminals
/dev/tty1..tty6, Alt+F1..F6 or `chvt N`. Each is an independent terminal --
its own screen, cursor, line discipline, foreground process group and
session. Two shells on two terminals are unrelated processes that share a
keyboard and a screen.

What made it possible is a backing store. The console drew straight into the
framebuffer and scrolled by moving pixels, which works exactly as long as
nothing else wants the screen: there was nowhere to put terminal 2 while
terminal 1 was visible, and nothing to repaint terminal 1 from. Each terminal
owns a grid of cells now and the framebuffer shows whichever grid is active,
so vt_switch() is set-index, repaint, return -- nothing is saved on the way
out because the outgoing grid has been current all along.

The rule the layer is built on: a terminal does not know whether it is
visible. Writes land in the grid, the cursor moves, the grid scrolls, all
unconditionally; drawing is one question asked at the bottom of vt.c. Every
alternative spreads "am I on screen" through the escape parser, and then a
terminal switched away from mid-sequence comes back wrong.

Everything that was a single global is per-terminal now: tty_fg_pgrp (deleted),
termios, the input ring, the escape parser, the UTF-8 decoder, and the cursor
-- which also stops being pixels in gfx.c and becomes cells in the terminal.
TIOCSCTTY was `return 0` and now records proc->ctty, which is what /dev/tty
resolves through; /dev/tty was an alias for /dev/console with the same minor,
and they are different questions.

kbd.c names Alt+F1..F6 as KEY_ALT_F1+n, above 0xFF on purpose -- every byte
value is already a character or a UTF-8 continuation, and a chord colliding
with one would be typed into the terminal instead of switching away from it.
What the chord means is decided in console.c, where the terminals are.

VT_ACTIVATE / VT_WAITACTIVE / VT_GETSTATE / VT_OPENQRY, Linux's numbers.
The argument to ACTIVATE is by value, not a pointer -- same shape as TCFLSH.

Two bugs found by screenshotting the result rather than reading the log:

  - zsh emits 0x0F to leave alt-charset mode, because that is what the `linux`
    terminfo says rmacs is, and the console drew CP437 glyph 0x0F: a sun, in
    the prompt, on every line. A terminal never draws a C0 control character.
  - SGR 7 was unimplemented, so zsh's partial-line marker rendered as a bare
    '#' instead of a reversed block. Reverse video swaps the colours at draw
    time, not at store time, so turning it off restores what was underneath.

Verified by screendump: tty1 and tty2 show different screens, and switching
back repaints tty1's earlier content from its grid. schfs-check passes.

docs/architecture/terminals.md, including what it does not do -- no
scrollback, no reflow, no VT_PROCESS mode for a display server to veto a
switch, which Schcom will need before it owns Alt+F7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-13 00:08:00 +02:00

1917 lines
67 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 console's real geometry, not a VT100's.
//
// This used to answer 24x80 unconditionally -- the size of a terminal
// SchOS has never drawn. Every program that lays out a screen asks
// this one question first: a line editor to decide where to wrap, `ls`
// to pick a column count, fastfetch to place its logo beside the text.
// At 1024x768 with the 8x16 font the console is 128x48, so all of them
// were formatting for a screen a third the area of the real one, and
// each looked like a separate bug in a separate program.
int cols = gfx_cols(), rows = gfx_rows();
// Serial-only boot, with no framebuffer attached: 80x24 is then a
// stated default rather than a wrong measurement.
ws->ws_col = cols > 0 ? (uint16)cols : 80;
ws->ws_row = rows > 0 ? (uint16)rows : 24;
ws->ws_xpixel = (uint16)gfx_width();
ws->ws_ypixel = (uint16)gfx_height();
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 describe whether the kernel or a process drives
// switching. There is only the kernel here, so report the default and
// accept the default; refusing outright would stop a program that only
// wanted to check.
if (request == VT_GETMODE || request == VT_SETMODE)
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;
}