SchOS/kernel/sys/sys_fs.c
Nexoniarz 2a441af3c6 ldls: a network, in a process
docs/plan/ldls.md L3a -- the class bridge, and the first of the three pieces
L3 turned out to be.

/dev/netdev is /dev/pci with the arrows reversed. That hands a device to a
process; this lets a process be one. The kernel side is netdev_ops and nothing
else -- transmit, receive, mac, link_up, the same four entries e1000
publishes -- so a process claims the device, supplies a MAC, and from then on
read() gives it frames the stack wants sent and write() hands it frames that
arrived.

cmd/netecho proves it and is deliberately not a driver: no hardware, none
claimed, every frame answered by the program itself. DHCP so the interface
gets an address at all, ARP so the stack can find it, ICMP so it can be
pinged. All built by hand from RFCs 826, 791, 792 and 2131.

    # netecho &
    netuser: ldls0 is a process now, 2:4c:44:4c:53:1
    netecho: I am 10.9.0.1 and will offer 10.9.0.2
    netecho: answered DHCP, offering an address

    # cat /proc/netif
    interface ldls0   driver netuser   address 10.9.0.2   gateway 10.9.0.1

    # ping 10.9.0.1 3
    netecho: answered an ARP
    netecho: answered a ping
    3 packets transmitted, 3 received, 0% packet loss

The interface got its address by DHCP from a process and then answered ping
from one. lwIP never learns that the thing behind netdev_ops is not a driver,
which is the whole property the bridge exists to have.

It dies the way it should, too. kill the process and link_up() answers false
immediately, so the stack refuses to send -- "ping: sendto: -5" -- rather than
swallowing packets into a driver that is not there. Start another and the
*same* interface comes back, keeping its address. That came free:
netuser_release_all() sits beside pciuser_release_all() in exit() for exactly
the same reason, and the reason is that a process holding a device is one that
may crash.

One thing the kernel needed: netinit() starts DHCP on the devices that exist
when it runs, so an interface created later -- which is how an LDLS-hosted
driver arrives -- would have come up with no address and no way to get one.
net_attach_late() fixes that, and lives in kernel/net/net.c because that is
the only file allowed to include lwIP's headers.

Recorded as not done, because it is not: this proves the bridge, not a driver.
Nothing in that test loads a module. That is why it was done first -- if ping
had not worked here the bridge would be wrong, and now a failure with a real
driver can only be the driver's side. L3b is the SchOS-side host, L3c is the
fifteen symbols for real, and L3 is the two joined.

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

1963 lines
69 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 "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
// -----------------------------------------------------------------------
addr_t sys_open(void)
{
char *path;
int fd, omode;
struct file *f;
struct inode *ip;
if (argstr(0, &path) < 0 || argint(1, &omode) < 0) return -EFAULT;
begin_op();
if (omode & O_CREATE) {
// O_EXCL means "create it or fail", so the file existing is the error.
if (omode & O_EXCL) {
struct inode *ex = namei(path);
if (ex) { iput(ex); end_op(); return -EEXIST; }
}
int rofs = 0;
ip = createat_x(AT_FDCWD, path,
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &rofs);
// A refusal because the filesystem is read-only is not "no such file".
// A shell prints the same sentence for every create failure, so
// returning ENOENT here made writing to a snapshot look like a missing
// directory.
if (!ip) { end_op(); return rofs ? -EROFS : -ENOENT; }
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
itrunc(ip);
} else {
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip);
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
if (ip->type == T_DIR && (omode & O_ACCMODE) != O_RDONLY) { iunlockput(ip); end_op(); return -EISDIR; }
if ((omode & O_TRUNC) && ip->type == T_FILE) itrunc(ip);
}
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
if (f) fileclose(f);
iunlockput(ip); end_op(); return -EMFILE;
}
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip;
f->off = (omode & O_APPEND) ? ip->size : 0;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
f->nonblock = (omode & O_NONBLOCK) != 0;
// Tell the device a descriptor was opened. /dev/input needs this to drop
// events that queued while nobody was listening; see inputopen().
if (ip->type == T_DEV && ip->major >= 0 && ip->major < NDEV &&
devsw[ip->major].open)
devsw[ip->major].open(ip);
return fd;
}
addr_t sys_openat(void)
{
int dirfd;
char *path;
int omode, fd;
struct file *f;
struct inode *ip;
argint(0, &dirfd); // AT_FDCWD = -100, or a real fd to resolve `path` from
if (argstr(1, &path) < 0 || argint(2, &omode) < 0) return -EFAULT;
begin_op();
if (omode & O_CREATE) {
// O_EXCL means "create it or fail", so the file existing is the error.
if (omode & O_EXCL) {
struct inode *ex = at_namei(dirfd, path);
if (ex) { iput(ex); end_op(); return -EEXIST; }
}
if (!(ip = createat(dirfd, path, T_FILE, 0, 0))) { end_op(); return -ENOENT; }
} else {
if (!(ip = at_namei(dirfd, path))) { end_op(); return -ENOENT; }
ilock(ip);
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
if (ip->type == T_DIR && (omode & O_ACCMODE) != O_RDONLY) { iunlockput(ip); end_op(); return -EISDIR; }
}
// O_TRUNC and O_APPEND used to be dropped on this path while sys_open
// honoured both, so the same shell redirection behaved differently
// depending on which entry point libc happened to call.
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
itrunc(ip);
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
if (f) fileclose(f);
iunlockput(ip); end_op(); return -EMFILE;
}
uint64 start = (omode & O_APPEND) ? ip->size : 0;
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip; f->off = start;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
f->nonblock = (omode & O_NONBLOCK) != 0;
// Tell the device a descriptor was opened. /dev/input needs this to drop
// events that queued while nobody was listening; see inputopen().
if (ip->type == T_DEV && ip->major >= 0 && ip->major < NDEV &&
devsw[ip->major].open)
devsw[ip->major].open(ip);
return fd;
}
addr_t sys_fstat(void)
{
struct file *f;
char *st;
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
if (argfd(0, 0, &f) < 0 || argptr(1, &st, size) < 0) return -1;
if (f->type != FD_INODE) return -1;
ilock(f->ip);
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
else stati(f->ip, (struct stat*)st);
iunlock(f->ip);
return 0;
}
addr_t sys_stat(void)
{
char *path;
char *st;
struct inode *ip;
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
if (argstr(0, &path) < 0 || argptr(1, &st, size) < 0) return -1;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip);
if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st);
else stati(ip, (struct stat*)st);
iunlockput(ip);
end_op();
return 0;
}
addr_t sys_lstat(void) { return sys_stat(); }
addr_t sys_access(void)
{
char *path;
if (argstr(0, &path) < 0) return -1;
begin_op();
struct inode *ip = namei(path);
if (!ip) { end_op(); return -ENOENT; }
iput(ip); end_op();
return 0;
}
addr_t sys_chdir(void)
{
char *path;
struct inode *ip;
begin_op();
if (argstr(0, &path) < 0) { end_op(); return -EFAULT; }
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip);
// ENOTDIR, not a bare -1. `cd` into a file must fail, and it did, but the
// shell reported "Operation not permitted" -- which reads like a
// permissions problem and sends you looking in entirely the wrong place.
if (ip->type != T_DIR) { iunlockput(ip); end_op(); return -ENOTDIR; }
iunlock(ip); iput(myproc()->cwd); end_op();
myproc()->cwd = ip;
if (path[0] == '/') {
memmove(myproc()->cwd_path, path, strnlen(path, 255));
myproc()->cwd_path[255] = 0;
} else if (strncmp(path, "..", 3) == 0) {
int n = strnlen(myproc()->cwd_path, 255);
if (n > 1) { while (n > 0 && myproc()->cwd_path[n-1] != '/') n--; if (n > 1) n--; myproc()->cwd_path[n] = 0; }
} else if (strncmp(path, ".", 2) != 0) {
int n = strnlen(myproc()->cwd_path, 255), pn = strnlen(path, 255);
if (n > 1 && n + 1 + pn < 255) { myproc()->cwd_path[n] = '/'; memmove(myproc()->cwd_path + n + 1, path, pn + 1); }
else if (n == 1 && 1 + pn < 255) memmove(myproc()->cwd_path + 1, path, pn + 1);
}
return 0;
}
addr_t sys_fchdir(void)
{
struct file *f;
if (argfd(0, 0, &f) < 0 || f->type != FD_INODE) return -1;
ilock(f->ip);
if (f->ip->type != T_DIR) { iunlock(f->ip); return -1; }
iunlock(f->ip);
iput(myproc()->cwd); idup(f->ip); myproc()->cwd = f->ip;
myproc()->cwd_path[0] = '/'; myproc()->cwd_path[1] = 0;
return 0;
}
addr_t sys_getcwd(void)
{
addr_t ubuf;
int size;
argaddr(0, &ubuf); argint(1, &size);
if (size <= 0) return -EINVAL;
int n = strnlen(myproc()->cwd_path, 255) + 1;
if (n > size) return -ERANGE;
if (copy_to_user(ubuf, myproc()->cwd_path, n) < 0) return -EFAULT;
// Real Linux getcwd(2) returns the length written (including the NUL),
// not the buffer address — musl's getcwd() wrapper checks buf[0]=='/'
// AND treats a 0 return as failure, but callers relying on the actual
// byte count (or the raw syscall() return value directly) need this.
return n;
}
addr_t sys_mkdir(void)
{
char *path;
struct inode *ip;
if (argstr(0, &path) < 0) return -EFAULT;
begin_op();
if ((ip = namei(path)) != 0) { // already there: mkdir -p relies on this
iput(ip);
end_op();
return -EEXIST;
}
if (!(ip = create(path, T_DIR, 0, 0))) { end_op(); return -ENOENT; }
iunlockput(ip); end_op();
return 0;
}
addr_t sys_mknod(void)
{
struct inode *ip;
char *path;
int major, minor;
begin_op();
if (argstr(0, &path) < 0 || argint(1, &major) < 0 || argint(2, &minor) < 0 ||
!(ip = create(path, T_DEV, major, minor))) { end_op(); return -1; }
iunlockput(ip); end_op();
return 0;
}
addr_t sys_link(void)
{
char name[DIRSIZ], *new, *old;
struct inode *dp, *ip;
if (argstr(0, &old) < 0 || argstr(1, &new) < 0) return -1;
begin_op();
if (!(ip = namei(old))) { end_op(); return -1; }
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
ip->nlink++; iupdate(ip); iunlock(ip);
if (!(dp = nameiparent(new, name))) goto bad_link;
ilock(dp);
if (dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0) { iunlockput(dp); goto bad_link; }
iunlockput(dp); iput(ip); end_op();
return 0;
bad_link:
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op();
return -1;
}
addr_t sys_unlink(void)
{
struct inode *ip, *dp;
char name[DIRSIZ], *path;
uint off;
if (argstr(0, &path) < 0) return -1;
begin_op();
if (!(dp = nameiparent(path, name))) { end_op(); return -1; }
ilock(dp);
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) goto bad_unlink;
if (!(ip = dirlookup(dp, name, &off))) goto bad_unlink;
ilock(ip);
if (ip->nlink < 1) panic("unlink: nlink < 1");
if (ip->type == T_DIR && dirempty(ip) != 1) { iunlockput(ip); goto bad_unlink; }
if (dirunlink(dp, name, off) < 0) { iunlockput(ip); goto bad_unlink; }
if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op(); return 0;
bad_unlink: iunlockput(dp); end_op(); return -1;
}
addr_t sys_exec(void)
{
char *path, *argv[MAXARG], *envp[MAXENV];
addr_t uargv, uenvp, uarg;
if (argstr(0, &path) < 0 || argaddr(1, &uargv) < 0) return -EFAULT;
memset(argv, 0, sizeof(argv));
for (int i = 0; ; i++) {
if (i >= NELEM(argv)) return -E2BIG;
if (fetchaddr(uargv + sizeof(addr_t) * i, &uarg) < 0) return -EFAULT;
if (!uarg) { argv[i] = 0; break; }
if (fetchstr(uarg, &argv[i]) < 0) return -EFAULT;
}
// The third argument. This is Linux's execve(2), so musl has been passing
// an environment here all along -- it was simply never read, and every
// program on the system started with an empty one.
//
// A null envp is legal and means exactly that. It is also what the
// first-party exec() wrapper passes, so old callers keep their old
// behaviour without knowing about this.
memset(envp, 0, sizeof(envp));
if (argaddr(2, &uenvp) == 0 && uenvp) {
for (int i = 0; ; i++) {
if (i >= NELEM(envp)) return -E2BIG;
if (fetchaddr(uenvp + sizeof(addr_t) * i, &uarg) < 0) return -EFAULT;
if (!uarg) { envp[i] = 0; break; }
if (fetchstr(uarg, &envp[i]) < 0) return -EFAULT;
}
}
return exec(path, argv, envp);
}
// ftruncate/truncate both grow and shrink. Growing used to be quietly dropped
// here; itrunc_to now takes it, and each filesystem decides what it can do --
// SchFS extends into a hole, v1 refuses because it has no way to represent one.
addr_t sys_ftruncate(void)
{
struct file *f;
addr_t length;
if (argfd(0, 0, &f) < 0 || argaddr(1, &length) < 0) return -1;
if ((int64)length < 0) return -EINVAL;
if (f->type != FD_INODE || !f->writable) return -1;
begin_op(); ilock(f->ip);
itrunc_to(f->ip, (uint64)length);
iunlock(f->ip); end_op();
return 0;
}
addr_t sys_truncate(void)
{
char *path;
addr_t length;
struct inode *ip;
if (argstr(0, &path) < 0 || argaddr(1, &length) < 0) return -1;
if ((int64)length < 0) return -EINVAL;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip);
itrunc_to(ip, (uint64)length);
iunlockput(ip); end_op();
return 0;
}
addr_t sys_rename(void)
{
char *oldpath, *newpath;
struct inode *ip, *dp_old, *dp_new, *ip_src;
char oldname[DIRSIZ], newname[DIRSIZ];
uint off;
struct dirent de;
if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -1;
begin_op();
if (!(ip = namei(oldpath))) { end_op(); return -1; }
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
ip->nlink++; iupdate(ip); iunlock(ip);
dp_old = nameiparent(oldpath, oldname);
dp_new = nameiparent(newpath, newname);
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
if (dp_old) iput(dp_old);
if (dp_new) iput(dp_new);
goto bad_rename;
}
ilock(dp_new);
if (dirlink(dp_new, newname, ip->inum) < 0) { iunlockput(dp_new); iput(dp_old); goto bad_rename; }
iunlockput(dp_new);
ilock(dp_old);
if (!(ip_src = dirlookup(dp_old, oldname, &off))) {
iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
}
iput(ip_src);
memset(&de, 0, sizeof(de));
if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de)) panic("rename: writei");
iunlockput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op(); return 0;
bad_rename:
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
}
addr_t sys_symlink(void)
{
char *target, *linkpath;
if (argstr(0, &target) < 0 || argstr(1, &linkpath) < 0) return -1;
begin_op();
struct inode *ip = create(linkpath, T_SYMLINK, 0, 0);
if (!ip) { end_op(); return -1; }
int len = strlen(target);
if (writei(ip, 0, (uint64)target, 0, len) != len) { iunlockput(ip); end_op(); return -1; }
iunlockput(ip); end_op();
return 0;
}
addr_t sys_readlink(void)
{
char *path, *buf;
int bufsiz;
if (argstr(0, &path) < 0 || argint(2, &bufsiz) < 0 || bufsiz <= 0) return -1;
if (argptr(1, &buf, bufsiz) < 0) return -1;
struct inode *ip;
if (!(ip = namei(path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -1; }
iunlockput(ip);
return n;
}
// -----------------------------------------------------------------------
// getdents64 — Linux directory reading
// -----------------------------------------------------------------------
addr_t
sys_getdents64(void)
{
struct file *f;
addr_t ubuf;
int count;
if (argfd(0, 0, &f) < 0 || argaddr(1, &ubuf) < 0 || argint(2, &count) < 0)
return -1;
if (f->type != FD_INODE) return -EBADF;
if (count <= 0) return -EINVAL;
// Validate the whole output window once; records are then written through
// the raw user VA, which is safe across page boundaries.
char *out = (char*)user_ptr(ubuf, (uint64)count);
if (!out) return -EFAULT;
struct inode *ip = f->ip;
ilock(ip);
if (ip->type != T_DIR) { iunlock(ip); return -ENOTDIR; }
// How a directory is laid out is the filesystem's business, and the two
// here could hardly differ more: v1 is a hash table of fixed 16-byte slots
// capped at 14-character names, SchFS is a range of B-tree keys holding
// names up to 255 bytes. Both produce the same record stream, and f->off is
// whatever position each one needs in order to resume.
int written = getdents(ip, out, count, &f->off);
iunlock(ip);
return written < 0 ? -EIO : written;
}
// -----------------------------------------------------------------------
// Custom SchOSFS ops
// -----------------------------------------------------------------------
addr_t sys_delete(void)
{
struct inode *ip, *dp, *ip2;
char name[DIRSIZ], *path;
uint off;
if (argstr(0, &path) < 0) return -1;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip);
if (ip->showmode == 1) {
ip->showmode = 0; iupdate(ip); iunlockput(ip); end_op();
cprintf("moved to recycle bin\n");
return 0;
}
iunlockput(ip);
if (!(dp = nameiparent(path, name))) { end_op(); return -1; }
ilock(dp);
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) { iunlockput(dp); end_op(); return -1; }
if (!(ip2 = dirlookup(dp, name, &off))) { iunlockput(dp); end_op(); return -1; }
ilock(ip2);
if (ip2->nlink < 1) panic("delete: nlink < 1");
if (ip2->type == T_DIR && dirempty(ip2) != 1) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
if (dirunlink(dp, name, off) < 0) { iunlockput(ip2); iunlockput(dp); end_op(); return -1; }
if (ip2->type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp); ip2->nlink--; iupdate(ip2); iunlockput(ip2);
end_op(); cprintf("deleted forever\n");
return 0;
}
addr_t sys_show(void)
{
char *path;
struct inode *ip;
if (argstr(0, &path) < 0) return -1;
begin_op();
if (!(ip = namei(path))) { end_op(); return -ENOENT; }
ilock(ip); ip->showmode = 1; iupdate(ip); iunlockput(ip);
end_op(); return 0;
}
addr_t sys_showstat(void)
{
char *path;
struct stat *st;
struct inode *ip;
if (argstr(0, &path) < 0 || argptr(1, (void*)&st, sizeof(*st)) < 0) return -1;
if (!(ip = namei(path))) return -ENOENT;
ilock(ip); stati(ip, st); iunlockput(ip);
cprintf("\nfile type: ");
switch (st->type) {
case T_FILE: cprintf("T_FILE\n"); break;
case T_DIR: cprintf("T_DIR\n"); break;
case T_EXTENT: cprintf("T_EXTENT\n"); break;
case T_DEV: cprintf("T_DEVICE\n"); break;
default: cprintf("UNKNOWN\n"); break;
}
cprintf("disk device: %d\ninode: %d\nlinks: %d\nsize(B): %d\n",
st->dev, st->ino, st->nlink, st->size);
if (st->type == T_EXTENT) {
for (int ad = 0; st->addrs[ad] && ad < NDIRECT+1; ad++) {
cprintf("extent ptr: 0x%x len: %d\n",
(st->addrs[ad] & ~0xff) >> 8, st->addrs[ad] & 0xff);
}
}
return 0;
}
// -----------------------------------------------------------------------
// Device I/O
// -----------------------------------------------------------------------
addr_t sys_vga_sync(void)
{
addr_t addr;
if (argaddr(0, &addr) < 0) return -1;
return 0;
}
addr_t sys_getmouse(void)
{
struct mouse_data *m;
if (argptr(0, (void*)&m, sizeof(*m)) < 0) return -1;
m->x = get_mouse_x(); m->y = get_mouse_y(); m->buttons = get_mouse_buttons();
return 0;
}
addr_t sys_ioctl(void)
{
int fd;
addr_t request, argp;
struct file *f;
if (argfd(0, &fd, &f) < 0) return -1;
argaddr(1, &request); argaddr(2, &argp);
int is_dev = (f->type == FD_INODE && f->ip && f->ip->type == 3);
// FICLONE: make this file share another's data blocks. Linux's spelling,
// because `cp --reflink` and every other tool that knows about cloning
// already asks for it by this number -- inventing our own would mean
// nothing could use the feature without being patched.
//
// The argument is a *file descriptor*, not a pointer, which is why it is
// read before the argp-shaped handling below.
if (request == FICLONE) {
struct file *sf;
struct proc *pr = myproc();
int sfd = (int)argp;
if (f->type != FD_INODE || !f->ip || !f->writable)
return -EBADF;
if (sfd < 0 || sfd >= NOFILE || (sf = pr->ofile[sfd]) == 0)
return -EBADF;
if (sf->type != FD_INODE || !sf->ip || !sf->readable)
return -EBADF;
// Both sides must be on the same filesystem: a clone shares blocks, and
// blocks belong to one volume.
if (sf->ip->dev != f->ip->dev)
return -EXDEV;
if (!SCHFS_IS_VOL_DEV(f->ip->dev))
return -EOPNOTSUPP;
if (f->ip->type != T_FILE || sf->ip->type != T_FILE)
return -EINVAL;
int r = schfs_kreflink(f->ip->dev, sf->ip->inum, f->ip->inum);
if (r != 0)
return -EIO;
// The destination inode's size changed underneath the VFS's copy.
ilock(f->ip);
f->ip->size = sf->ip->size;
iunlock(f->ip);
return 0;
}
// /dev/fb0 (major 2) — geometry queries. This has to come before the
// catch-all below, which answers every non-tty ioctl with 0: a program
// asking FBIOGET_VSCREENINFO would have been told "success" and then
// read an untouched buffer, i.e. a 0x0 screen, which looks like a
// driver bug rather than a missing one.
if (is_dev && f->ip->major == 2)
return fb_ioctl((int)request, argp);
if (is_dev && f->ip->major == INPUT_MAJOR)
return input_ioctl(f->ip, (int)request, argp);
if (is_dev && f->ip->major == PCIUSER_MAJOR)
return pciuser_ioctl(request, argp);
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;
}
if (request == VT_GETMODE) {
struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m));
if (!m) return -EFAULT;
memset(m, 0, sizeof(*m));
m->mode = (char)vt_for_minor(f->ip->minor)->mode;
return 0;
}
return 0;
}
// -----------------------------------------------------------------------
// POSIX filesystem extensions
// -----------------------------------------------------------------------
// (at_namei/at_nameiparent/at_dir are defined above, next to create()/
// createat() — every *at() syscall below uses them for dirfd resolution.)
// chmod / fchmod / chown / fchown / lchown — stubs (no permission model yet).
addr_t sys_chmod(void) { return 0; }
addr_t sys_fchmod(void) { return 0; }
addr_t sys_chown(void) { return 0; }
addr_t sys_fchown(void) { return 0; }
addr_t sys_lchown(void) { return 0; }
// rmdir: remove an empty directory (same as unlink for dirs).
addr_t sys_rmdir(void)
{
char *path;
if (argstr(0, &path) < 0) return -1;
// Reuse sys_unlink semantics — it already handles T_DIR + isdirempty check.
return sys_unlink();
}
// fsync / fdatasync / sync — stubs (log-based FS commits on every op).
// sync / fsync / fdatasync.
//
// These returned 0 without doing anything, which was defensible while every
// write reached the disk before its syscall returned. It is not defensible now:
// tree blocks are marked dirty in the buffer cache and pushed out at commit, so
// "flush what is outstanding" finally names a real action. An empty transaction
// drives the same barrier a real one does -- the dirty buffers go out and the
// device is told to commit them.
addr_t sys_sync(void)
{
schfs_ksync_all();
return 0;
}
addr_t sys_fsync(void)
{
struct file *f;
if (argfd(0, 0, &f) < 0) return -EBADF;
// Whole-volume rather than per-file: SchFS has no way to make one object
// durable on its own. A transaction group is the unit that commits, and
// this file's last write is in whichever group is open.
if (f->type == FD_INODE && f->ip && SCHFS_IS_VOL_DEV(f->ip->dev))
return schfs_ksync(f->ip->dev) == 0 ? 0 : -EIO;
return 0;
}
// No separate data-only path: the inode record and the data it describes are
// records in the same tree and commit together. Reporting success for a weaker
// guarantee than was asked for would be the wrong way round; this is stronger.
addr_t sys_fdatasync(void){ return sys_fsync(); }
// pread: read from fd at given offset without changing file position.
addr_t sys_pread(void)
{
struct file *f;
int n;
char *p;
uint64 offset;
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 ||
argptr(1, &p, n) < 0 || argaddr(3, &offset) < 0) return -1;
if (f->type != FD_INODE) return -1;
ilock(f->ip);
int r = readi(f->ip, 0, (uint64)p, (uint)offset, n);
iunlock(f->ip);
return r;
}
// pwrite: write to fd at given offset without changing file position.
addr_t sys_pwrite(void)
{
struct file *f;
int n;
char *p;
uint64 offset;
if (argfd(0, 0, &f) < 0 || argint(2, &n) < 0 ||
argptr(1, &p, n) < 0 || argaddr(3, &offset) < 0) return -1;
if (f->type != FD_INODE || !f->writable) return -1;
begin_op(); ilock(f->ip);
int r = writei(f->ip, 0, (uint64)p, (uint)offset, n);
iunlock(f->ip); end_op();
return r;
}
// utime / utimes / utimensat — timestamps not tracked; stubs.
addr_t sys_utime(void) { return 0; }
addr_t sys_utimes(void) { return 0; }
addr_t sys_utimensat(void) { return 0; }
// sendfile: not implemented.
addr_t sys_sendfile(void) { return -ENOSYS; }
// preadv2 / pwritev2: scatter/gather at offset — redirect to readv/writev.
addr_t sys_preadv2(void) { return sys_readv(); }
addr_t sys_pwritev2(void){ return sys_writev(); }
// fallocate: preallocate space — we don't track this; stub success.
addr_t sys_fallocate(void) { return 0; }
// statfs / fstatfs.
//
// These used to return the same six invented constants whatever they were
// asked about -- 150000 blocks, half of them free -- so df reported a
// filesystem that did not exist and reported it identically for every mount.
// The numbers are real now, and which ones you get depends on which filesystem
// the path landed on.
static void fill_statfs(struct statfs *sf, uint dev) {
uint64 total = 0, freeb = 0, objects = 0;
memset(sf, 0, sizeof(*sf));
sf->f_type = 0x534b4658; // 'SKFX'
if (schfs_kstatfs(dev, &total, &freeb, &objects) == 0) {
sf->f_bsize = SCHFS_BLKSIZE;
sf->f_frsize = SCHFS_BLKSIZE;
sf->f_blocks = (int64)total;
sf->f_bfree = (int64)freeb;
sf->f_bavail = (int64)freeb; // no reserve, so nothing held back
// Both zero, deliberately. There is no inode table to run out of and
// the object id space is 2^64, so there is no honest pair of numbers
// here -- and df computes a "% inodes used" from them, which against a
// real f_files and a zero f_ffree would show every SchFS mount as
// completely full. Zeroes are what filesystems that allocate inodes
// dynamically report, and what df is written to expect from them.
sf->f_files = 0;
sf->f_ffree = 0;
sf->f_namelen = SCHFS_NAME_MAX;
(void)objects;
return;
}
// v1, /proc, or anything else: the old shape, but at least with a block
// size that matches the one the buffer cache actually uses.
sf->f_bsize = BSIZE;
sf->f_frsize = BSIZE;
sf->f_namelen = DIRSIZ;
}
addr_t sys_statfs(void)
{
char *path;
addr_t bufp;
if (argstr(0, &path) < 0 || argaddr(1, &bufp) < 0) return -1;
struct statfs *sf = (struct statfs*)user_ptr(bufp, sizeof(*sf));
if (!sf) return -EFAULT;
// Resolve the path: which filesystem it lands on is the whole question,
// and mount crossing happens during resolution.
begin_op();
struct inode *ip = namei(path);
if (!ip) { end_op(); return -ENOENT; }
uint dev = ip->dev;
iput(ip);
end_op();
fill_statfs(sf, dev);
return 0;
}
addr_t sys_fstatfs(void)
{
struct file *f;
addr_t bufp;
if (argfd(0, 0, &f) < 0 || argaddr(1, &bufp) < 0) return -1;
struct statfs *sf = (struct statfs*)user_ptr(bufp, sizeof(*sf));
if (!sf) return -EFAULT;
if (f->type != FD_INODE || !f->ip) return -EBADF;
fill_statfs(sf, f->ip->dev);
return 0;
}
// chroot: not supported.
addr_t sys_chroot(void) { return -EPERM; }
// -----------------------------------------------------------------------
// at-syscalls
// -----------------------------------------------------------------------
addr_t sys_mkdirat(void)
{
int dirfd;
char *path;
struct inode *ip;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -EFAULT;
begin_op();
if ((ip = at_namei(dirfd, path)) != 0) { // see sys_mkdir
iput(ip);
end_op();
return -EEXIST;
}
if (!(ip = createat(dirfd, path, T_DIR, 0, 0))) { end_op(); return -ENOENT; }
iunlockput(ip); end_op();
return 0;
}
addr_t sys_mknodat(void)
{
int dirfd;
char *path;
uint64 dev;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &(int){0}); // mode — ignored
argaddr(3, &dev);
int major = (dev >> 8) & 0xff;
int minor = dev & 0xff;
begin_op();
struct inode *ip = createat(dirfd, path, T_DEV, (short)major, (short)minor);
if (!ip) { end_op(); return -1; }
iunlockput(ip); end_op();
return 0;
}
addr_t sys_unlinkat(void)
{
int dirfd, flags;
char *path;
struct inode *ip, *dp;
char name[DIRSIZ];
uint off;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &flags);
begin_op();
if (!(dp = at_nameiparent(dirfd, path, name))) { end_op(); return -1; }
ilock(dp);
if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) {
iunlockput(dp); end_op(); return -1;
}
if (!(ip = dirlookup(dp, name, &off))) {
iunlockput(dp); end_op(); return -1;
}
ilock(ip);
if (ip->nlink < 1) panic("unlinkat: nlink < 1");
if (ip->type == T_DIR && !(flags & AT_REMOVEDIR)) {
iunlockput(ip); iunlockput(dp); end_op(); return -1;
}
if (ip->type == T_DIR && dirempty(ip) != 1) {
iunlockput(ip); iunlockput(dp); end_op(); return -1;
}
if (dirunlink(dp, name, off) < 0) {
iunlockput(ip); iunlockput(dp); end_op(); return -1;
}
if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); }
iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op();
return 0;
}
addr_t sys_renameat(void)
{
int olddirfd, newdirfd;
char *oldpath, *newpath;
argint(0, &olddirfd); argstr(1, &oldpath);
argint(2, &newdirfd); argstr(3, &newpath);
// Delegate to sys_rename internals — fake the args for rename.
// Easiest: place path pointers in registers and call rename logic directly.
struct inode *ip, *dp_old, *dp_new, *ip_src;
char oldname[DIRSIZ], newname[DIRSIZ];
uint off;
struct dirent de;
begin_op();
if (!(ip = at_namei(olddirfd, oldpath))) { end_op(); return -1; }
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
ip->nlink++; iupdate(ip); iunlock(ip);
dp_old = at_nameiparent(olddirfd, oldpath, oldname);
dp_new = at_nameiparent(newdirfd, newpath, newname);
if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) {
if (dp_old) iput(dp_old);
if (dp_new) iput(dp_new);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
}
ilock(dp_new);
if (dirlink(dp_new, newname, ip->inum) < 0) {
iunlockput(dp_new); iput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
}
iunlockput(dp_new);
ilock(dp_old);
if (!(ip_src = dirlookup(dp_old, oldname, &off))) {
iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -1;
}
iput(ip_src);
memset(&de, 0, sizeof(de));
if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("renameat: writei");
iunlockput(dp_old);
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip);
end_op();
return 0;
}
addr_t sys_renameat2(void) { return sys_renameat(); } // ignore flags
addr_t sys_linkat(void)
{
int olddirfd, newdirfd;
char *old, *new;
struct inode *dp, *ip;
char name[DIRSIZ];
argint(0, &olddirfd); argstr(1, &old);
argint(2, &newdirfd); argstr(3, &new);
argint(4, &(int){0}); // flags
begin_op();
if (!(ip = at_namei(olddirfd, old))) { end_op(); return -1; }
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -1; }
ip->nlink++; iupdate(ip); iunlock(ip);
if (!(dp = at_nameiparent(newdirfd, new, name))) goto bad_linkat;
ilock(dp);
if (dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0) {
iunlockput(dp); goto bad_linkat;
}
iunlockput(dp); iput(ip); end_op();
return 0;
bad_linkat:
ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op();
return -1;
}
addr_t sys_symlinkat(void)
{
char *target, *linkpath;
int newdirfd;
if (argstr(0, &target) < 0) return -1;
argint(1, &newdirfd);
if (argstr(2, &linkpath) < 0) return -1;
begin_op();
struct inode *ip = createat(newdirfd, linkpath, T_SYMLINK, 0, 0);
if (!ip) { end_op(); return -1; }
int len = strlen(target);
if (writei(ip, 0, (uint64)target, 0, len) != len) {
iunlockput(ip); end_op(); return -1;
}
iunlockput(ip); end_op();
return 0;
}
addr_t sys_readlinkat(void)
{
int dirfd;
char *path, *buf;
int bufsiz;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
if (argint(3, &bufsiz) < 0 || bufsiz <= 0) return -1;
if (argptr(2, &buf, bufsiz) < 0) return -1;
struct inode *ip;
if (!(ip = at_namei(dirfd, path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -1; }
iunlockput(ip);
return n;
}
addr_t sys_fchmodat(void) { return 0; } // stub
addr_t sys_fchownat(void) { return 0; } // stub
addr_t sys_faccessat(void)
{
int dirfd;
char *path;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &(int){0}); // amode — ignored
// arg3 flags — ignored
begin_op();
struct inode *ip = at_namei(dirfd, path);
if (!ip) { end_op(); return -ENOENT; }
iput(ip); end_op();
return 0;
}
addr_t sys_fstatat(void)
{
int dirfd, flags;
char *path;
char *st;
int size = myproc()->syscall_map ? sizeof(struct linux_stat) : sizeof(struct stat);
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
if (argptr(2, &st, size) < 0) return -1;
argint(3, &flags);
// AT_EMPTY_PATH: if path is empty, fstat the dirfd itself.
if ((flags & AT_EMPTY_PATH) && path[0] == '\0') {
if (dirfd < 0 || dirfd >= NOFILE || !myproc()->ofile[dirfd]) return -1;
struct file *f = myproc()->ofile[dirfd];
if (f->type != FD_INODE) return -1;
ilock(f->ip);
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
else stati(f->ip, (struct stat*)st);
iunlock(f->ip);
return 0;
}
begin_op();
struct inode *ip = at_namei(dirfd, path);
if (!ip) { end_op(); return -ENOENT; }
ilock(ip);
if (myproc()->syscall_map) stati_linux(ip, (struct linux_stat*)st);
else stati(ip, (struct stat*)st);
iunlockput(ip);
end_op();
return 0;
}
// -----------------------------------------------------------------------
// mount / umount2
//
// A mount here is a substitution made during path resolution: nothing on
// either disk records it, so mounting is adding a row to a table and
// unmounting is removing it. There is no filesystem probing to do at mount
// time either -- SchFS's uberblock scan already ran at boot, and this only
// decides where in the namespace the result appears.
// -----------------------------------------------------------------------
addr_t sys_mount(void)
{
char *source, *target, *fstype;
if (argstr(0, &source) < 0 || argstr(1, &target) < 0 ||
argstr(2, &fstype) < 0)
return -EFAULT;
// proc: a synthetic filesystem, so there is no source device to check and
// no volume to have found at boot. `mount -t proc proc /proc`.
if (strncmp(fstype, "proc", 5) == 0) {
procfs_mount();
return 0;
}
// devtmpfs: the same, and the one init actually needs. /dev used to be
// twenty mknod() calls; see the note at the top of kernel/fs/devfs.c.
if (strncmp(fstype, "devtmpfs", 9) == 0 || strncmp(fstype, "dev", 4) == 0) {
devfs_mount();
return 0;
}
// Only SchFS. Checking the name still matters: a caller that asked for
// something else should be told no rather than quietly handed this.
if (strncmp(fstype, "schfs", 6) != 0)
return -ENODEV;
// `mount -t schfs @name /mnt` mounts a snapshot, read-only.
//
// The source finally means something. It was ignored outright before --
// there was one volume and nothing to choose between -- and a snapshot is
// the first second thing to mount. `@` rather than a path because a
// snapshot is not a device: there is no node in /dev for it and there
// should not be, since it lives inside the volume that is already mounted.
int snapmount = 0;
uint snapdev = 0;
if (source && source[0] == '@') {
const char *nm = source + 1;
unsigned nlen = 0;
while (nm[nlen] && nlen < 64)
nlen++;
if (nlen == 0)
return -EINVAL;
// Distinct codes, because "resource busy" for every possible failure
// tells the caller nothing and cost an hour of guessing once already.
//
// Snapshots are taken of the root volume: a snapshot is named inside
// the volume that holds it, and there is no syntax yet for saying which
// volume that is.
int r = schfs_ksnap_mount(SCHFS_DEV, nm, nlen);
if (r < 0) {
switch (r) {
case -1: return -ENODEV; /* no volume mounted to look in */
case -2: return -EBUSY; /* every snapshot slot in use */
case -3: return -ENOMEM; /* no scratch blocks */
default: return -ENOENT; /* no snapshot by that name */
}
}
snapdev = (uint)r;
snapmount = 1;
}
if (!schfs_kmounted())
return -ENODEV;
// The source finally names a disk.
//
// It was ignored outright before -- there was one volume and nothing to
// choose between -- so `mount -t schfs /dev/hdb /mnt` mounted whatever the
// kernel had probed at boot and said nothing about it. Now an explicit
// source is resolved, and mounting a second SchFS disk works.
//
// An empty source, or the bare word "schfs", still means the root volume:
// that is what /etc/fstab and every existing script pass.
uint fsdev_src = SCHFS_DEV;
if (!snapmount && source && source[0] && strncmp(source, "schfs", 6) != 0) {
begin_op();
struct inode *sp = namei(source);
if (!sp) { end_op(); return -ENOENT; }
ilock(sp);
// A raw disk is a character device in the devsw table (major 8, see
// kernel/dev/cdev.c) whose minor is the IDE device number.
if (sp->type != T_DEV || sp->major != 8) {
iunlockput(sp); end_op();
return -ENOTBLK;
}
uint bdev = (uint)sp->minor;
iunlockput(sp);
end_op();
int r = schfs_kmount_dev(bdev);
if (r < 0) {
// -EBUSY also covers "already mounted", which is the common typo
// and deserves to be distinguishable from "no free slot" one day.
return r;
}
fsdev_src = (uint)r;
}
uint64 root = snapmount ? schfs_ksnap_root(snapdev) : schfs_kroot(fsdev_src);
if (root == 0) {
if (snapmount)
schfs_ksnap_unmount(snapdev);
return -ENODEV;
}
begin_op();
struct inode *dp = namei(target);
if (!dp) { end_op(); return -ENOENT; }
ilock(dp);
if (dp->type != T_DIR) { iunlockput(dp); end_op(); return -ENOTDIR; }
uint dev = dp->dev;
uint64 inum = dp->inum;
iunlockput(dp);
end_op();
// Mounting a filesystem onto a directory that is already on *it* would let
// an unmount strand the covered directory behind its own mount: umount
// names the mounted root, and a self-mount makes that ambiguous.
//
// This read `dev == SCHFS_DEV`, which was the same thing while SchFS was
// the only volume there could be. A snapshot is a second device, and its
// mount point is necessarily on the live root -- there is nowhere else to
// put it -- so the old spelling refused every snapshot mount.
uint fsdev = snapmount ? snapdev : fsdev_src;
if (dev == fsdev)
return -EBUSY;
if (mount_add(dev, inum, fsdev, root, target, source, "schfs") != 0) {
if (snapmount)
schfs_ksnap_unmount(snapdev);
else if (fsdev_src != SCHFS_DEV)
schfs_kumount(fsdev_src); /* undo the volume mount we just did */
return -EBUSY;
}
return 0;
}
addr_t sys_umount2(void)
{
char *target;
if (argstr(0, &target) < 0) return -EFAULT;
begin_op();
struct inode *dp = namei(target);
if (!dp) { end_op(); return -ENOENT; }
// Resolving the path already crossed the mount, so this is the mounted
// filesystem's root rather than the directory it covers. The table knows
// which mount point that root belongs to.
uint dev = dp->dev;
uint64 inum = dp->inum;
iput(dp);
end_op();
if (mount_del_by_root(dev, inum) != 0)
return -EINVAL;
// Release the slot with the mount. Without this the table entry went away
// and the slot stayed taken, so a volume could be unmounted and never
// mounted again until reboot.
if (SCHFS_IS_SNAP_DEV(dev))
schfs_ksnap_unmount(dev);
else if (SCHFS_IS_VOL_DEV(dev) && dev != SCHFS_DEV)
// Pushes the volume's dirty buffers out and drives a cache flush before
// letting go. The root volume (SCHFS_DEV) refuses -- there is nothing
// to fall back to -- and mount_del_by_root has already failed for it,
// since nothing mounts the root over anything.
schfs_kumount(dev);
return 0;
}
// -----------------------------------------------------------------------
// Attributes, and the index over them (SchFS phase 5)
//
// setxattr/getxattr/removexattr keep Linux's shape, so a program that already
// knows how to attach metadata to a file needs no changes. schfs_query is the
// one call with no Linux equivalent: it asks the *filesystem* a question
// instead of walking it, which is the whole point of building the index.
// -----------------------------------------------------------------------
// Resolve a path to a SchFS inode, and refuse anything that is not one.
// Attributes live in the tree, so there is nowhere to put them on v1 or /proc.
static int
xattr_inode(char *path, struct inode **ip)
{
begin_op();
struct inode *p = namei(path);
if (!p) { end_op(); return -ENOENT; }
if (!SCHFS_IS_VOL_DEV(p->dev)) { iput(p); end_op(); return -EOPNOTSUPP; }
end_op();
*ip = p;
return 0;
}
addr_t sys_setxattr(void)
{
char *path, *name;
addr_t uval;
int size, flags;
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
if (argaddr(2, &uval) < 0 || argint(3, &size) < 0) return -EFAULT;
argint(4, &flags); /* XATTR_CREATE/REPLACE: unused */
if (size < 0 || size > SCHFS_ATTR_VALUE_MAX) return -E2BIG;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
char val[SCHFS_ATTR_VALUE_MAX];
if (size > 0 && copy_from_user(val, uval, (uint64)size) < 0) return -EFAULT;
struct inode *ip;
int r = xattr_inode(path, &ip);
if (r < 0) return r;
// Stored as a string: nothing here knows the caller's intent, and a string
// is the type whose equality comparison is the one a query wants.
r = schfs_kattr_set(ip->dev, ip->inum, name, (unsigned)namelen,
val, (unsigned)size, SCHFS_ATTR_STRING);
iput(ip);
return r == 0 ? 0 : -EIO;
}
addr_t sys_getxattr(void)
{
char *path, *name;
addr_t uval;
int size;
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
if (argaddr(2, &uval) < 0 || argint(3, &size) < 0) return -EFAULT;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
struct inode *ip;
int r = xattr_inode(path, &ip);
if (r < 0) return r;
char val[SCHFS_ATTR_VALUE_MAX];
unsigned vallen = 0;
// A zero size means "how big is it?", which is how every caller sizes its
// buffer before asking again.
r = schfs_kattr_get(ip->dev, ip->inum, name, (unsigned)namelen,
size > 0 ? val : 0, (unsigned)(size > 0 ? size : 0),
&vallen);
iput(ip);
if (r == 0) return -ENODATA;
if (r == -3) return -ERANGE;
if (r != 1) return -EIO;
if (size == 0) return (addr_t)vallen;
if (copy_to_user(uval, val, vallen) < 0) return -EFAULT;
return (addr_t)vallen;
}
addr_t sys_removexattr(void)
{
char *path, *name;
if (argstr(0, &path) < 0 || argstr(1, &name) < 0) return -EFAULT;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
struct inode *ip;
int r = xattr_inode(path, &ip);
if (r < 0) return r;
r = schfs_kattr_del(ip->dev, ip->inum, name, (unsigned)namelen);
iput(ip);
return r == 0 ? 0 : -EIO;
}
// Declare an index on an attribute name. Covers values written afterwards.
addr_t sys_schfs_index(void)
{
char *name;
if (argstr(0, &name) < 0) return -EFAULT;
int namelen = strlen(name);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
return schfs_kindex(SCHFS_DEV, name, (unsigned)namelen) == 0 ? 0 : -EIO;
}
// Every object whose `name` attribute equals `value`, as object ids.
//
// Object ids rather than paths: the index maps values to objects, and a
// filesystem keyed that way has no cheap way back to a name. Turning an id into
// a path is a caller's problem, and a rarer one than the query itself.
addr_t sys_schfs_query(void)
{
char *name, *value;
addr_t uout;
int cap;
if (argstr(0, &name) < 0 || argstr(1, &value) < 0) return -EFAULT;
if (argaddr(2, &uout) < 0 || argint(3, &cap) < 0) return -EFAULT;
if (cap <= 0 || cap > 256) return -EINVAL;
int namelen = strlen(name), vallen = strlen(value);
if (namelen <= 0 || namelen > SCHFS_ATTR_NAME_MAX) return -ERANGE;
if (vallen > SCHFS_ATTR_VALUE_MAX) return -E2BIG;
uint64 ids[64];
if (cap > (int)(sizeof(ids) / sizeof(ids[0])))
cap = (int)(sizeof(ids) / sizeof(ids[0]));
int n = schfs_kquery(SCHFS_DEV, name, (unsigned)namelen,
value, (unsigned)vallen, ids, cap);
if (n < 0) return -ENOENT; /* no such index */
if (n > 0 && copy_to_user(uout, ids, (uint64)n * sizeof(uint64)) < 0)
return -EFAULT;
return (addr_t)n;
}