From 2cfa3f0bf203944e9699198e6ee7dc9a007bcfe5 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 24 Oct 2026 15:28:32 +0200 Subject: [PATCH] kernel: chroot, #! scripts, rename and mmap as apk needs them Found by running Alpine's apk on SchOS, one failure at a time: - chroot(2) was a stub returning EPERM. Each process now has a root: where an absolute path starts and where .. stops. Mounts remember whose root they were made under, and a walk leaves a mount by the way it came in, so one filesystem can be mounted twice. - devtmpfs and proc mounted only on /dev and /proc, whatever the target; a chroot got "already mounted". - mmap never reused freed address space, so musl's malloc used up the 256 mapping records while 780 MB were free. A mergeable mapping now goes straight after an anonymous one whose end has room. - unlinkat returned -1 (EPERM) where apk expects ENOENT, and renameat was an old copy of rename: no replacing the target, and v1 dirents written into SchFS. Both now share rename's implementation. - execve runs #! scripts, and says ENOEXEC for what it cannot run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- include/core/defs.h | 4 +- include/core/proc.h | 4 ++ kernel/fs/devfs.c | 23 +++---- kernel/fs/exec.c | 143 +++++++++++++++++++++++++++++++++++++++---- kernel/fs/fs.c | 101 +++++++++++++++++++++++++----- kernel/fs/procfs.c | 18 +++--- kernel/proc/proc.c | 4 ++ kernel/sys/sys_fs.c | 135 +++++++++++++++++++--------------------- kernel/sys/sys_mem.c | 58 ++++++++++++++++-- 9 files changed, 361 insertions(+), 129 deletions(-) diff --git a/include/core/defs.h b/include/core/defs.h index 53c9ac9..94ce346 100644 --- a/include/core/defs.h +++ b/include/core/defs.h @@ -339,8 +339,8 @@ int procwrite(struct inode*, uint64, char*, int); // namespace, the other the content. int proc_gen_global(int id, char *buf, int cap); int proc_write_sysrq(int user_src, uint64 src, uint n); -void procfs_mount(void); -void devfs_mount(void); +int procfs_mount(const char *target); +int devfs_mount(const char *target); // input.c -- raw evdev event devices void inputinit(void); diff --git a/include/core/proc.h b/include/core/proc.h index 5aca25a..369ddef 100644 --- a/include/core/proc.h +++ b/include/core/proc.h @@ -313,6 +313,10 @@ struct proc { // rt_sigreturn. See "The FPU" above scheduler() in kernel/proc/proc.c. uint8 fpu[512] __attribute__((aligned(16))); uint8 sigfpu[512] __attribute__((aligned(16))); + + // chroot(2): where an absolute path starts, and where ".." stops. 0 is the + // system's root. Inherited across fork, held as a reference like cwd. + struct inode *root; }; #define FD_CLOEXEC 1 diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c index 5fd54d9..befd9a3 100644 --- a/kernel/fs/devfs.c +++ b/kernel/fs/devfs.c @@ -318,18 +318,19 @@ const struct fs_ops devfs_ops = { .getdents = devfs_getdents, }; -// Graft it over /dev. Called by init, which has made the directory. -void -devfs_mount(void) +// Graft it over `target` -- /dev from init, and again wherever a chroot +// wants one. It was always /dev whatever was asked for, so a second mount +// failed as "already mounted" and a chroot had no devices. +int +devfs_mount(const char *target) { - struct inode *dp = namei("/dev"); + struct inode *dp = namei((char *)target); + int r; - if (dp == 0) { - cprintf("devfs: /dev does not exist; not mounted\n"); - return; - } - if (mount_add(dp->dev, dp->inum, DEVFS_DEV, DEVFS_ROOT, "/dev", "dev", - "devtmpfs") < 0) - cprintf("devfs: /dev already has something mounted on it\n"); + if (dp == 0) + return -ENOENT; + r = mount_add(dp->dev, dp->inum, DEVFS_DEV, DEVFS_ROOT, (char *)target, "dev", + "devtmpfs"); iput(dp); + return r < 0 ? -EBUSY : 0; } diff --git a/kernel/fs/exec.c b/kernel/fs/exec.c index 5ffc131..2744fb8 100644 --- a/kernel/fs/exec.c +++ b/kernel/fs/exec.c @@ -32,16 +32,31 @@ extern int linux_syscall_map_size; // rewrite the array between a check and a use, and a pointer swapped for a // kernel address afterwards would have copied kernel memory into the new // program's stack. +// +// `pre` goes in front, always from the kernel, and `skip` drops that many of +// the array's own: a #! script runs as `interp [arg] script argv[1]...`, which +// is the caller's array less its argv[0] behind three strings of ours. struct argsrc { int user; char **k; addr_t u; + char *pre[3]; + int npre; + uint64 skip; }; -// Pointer i of the array, 0 past its end; -1 on a fault. +// Pointer i of the array, 0 past its end; -1 on a fault. *kern says which +// side of the boundary it points to. static int -arg_ptr(struct argsrc *s, uint64 i, addr_t *p) +arg_ptr(struct argsrc *s, uint64 i, addr_t *p, int *kern) { + if (i < (uint64)s->npre) { + *p = (addr_t)s->pre[i]; + *kern = 1; + return 0; + } + i = i - s->npre + s->skip; + *kern = !s->user; if (s->user) return s->u ? fetchaddr(s->u + i * sizeof(addr_t), p) : (*p = 0, 0); *p = s->k ? (addr_t)s->k[i] : 0; @@ -49,9 +64,9 @@ arg_ptr(struct argsrc *s, uint64 i, addr_t *p) } static long -arg_len(struct argsrc *s, addr_t p) +arg_len(addr_t p, int kern) { - return s->user ? strlen_user(p) : (long)strlen((char *)p); + return kern ? (long)strlen((char *)p) : strlen_user(p); } // Count an array and its bytes, against Linux's limits: ARG_MAX in all, @@ -61,13 +76,14 @@ arg_measure(struct argsrc *s, uint64 *count, uint64 *bytes, uint64 already) { for (uint64 i = 0; ; i++) { addr_t p; - if (arg_ptr(s, i, &p) < 0) + int kern; + if (arg_ptr(s, i, &p, &kern) < 0) return -EFAULT; if (p == 0) { *count = i; return 0; } - long n = arg_len(s, p); + long n = arg_len(p, kern); if (n < 0) return n == -EFAULT ? -EFAULT : -E2BIG; if (n + 1 > ARG_STRLEN_MAX) @@ -113,16 +129,17 @@ arg_copy(struct argsrc *s, uint64 count, pml4e_t *pgdir, addr_t *at, { for (uint64 i = 0; i < count; i++) { addr_t p; - if (arg_ptr(s, i, &p) < 0 || p == 0) + int kern; + if (arg_ptr(s, i, &p, &kern) < 0 || p == 0) return -1; - long n = arg_len(s, p); + long n = arg_len(p, kern); if (n < 0 || *at + (addr_t)n + 1 > end) return -1; if (tbl_put(t, *at) < 0) return -1; for (long done = 0; done < n; ) { long c = n - done > PGSIZE ? PGSIZE : n - done; - if (s->user) { + if (!kern) { if (copy_from_user(bounce, p + done, c) < 0) return -1; } else { @@ -141,6 +158,7 @@ arg_copy(struct argsrc *s, uint64 count, pml4e_t *pgdir, addr_t *at, } static int do_exec(char *path, struct argsrc *av, struct argsrc *ev); +static int exec_elf(char *path, struct argsrc *av, struct argsrc *ev); // --- Dynamic linking --------------------------------------------------------- // @@ -252,8 +270,98 @@ exec_user(char *path, addr_t uargv, addr_t uenvp) return do_exec(path, &a, &e); } +// --- #! scripts -------------------------------------------------------------- +// +// A file that starts with #! names the program that runs it, and one optional +// argument, on its first line. Without this every script had to be started +// as `sh script`; apk runs its packages' triggers and install scripts with +// execve() directly, and each one failed with "Operation not permitted". +// +// If `path` is a script, puts "interp\0arg\0" in `line` (a page) and returns +// 1, with *argp pointing at the argument or 0. 0 if it is not one; <0 errno. +static int +script_interp(char *path, char *line, char **argp) +{ + struct inode *ip; + int err, n; + + begin_op(); + if ((ip = nameifollow(0, path, &err)) == 0) { + end_op(); + return -err; + } + ilock(ip); + n = readi(ip, 0, (uint64)line, 0, 256); + iunlockput(ip); + end_op(); + if (n < 2 || line[0] != '#' || line[1] != '!') + return 0; + + // Linux's rules: the line ends at a newline or after 255 bytes; blanks + // separate the interpreter from the argument, and everything after the + // first blank run is one argument, spaces and all. + line[n < 256 ? n : 255] = 0; + char *nl = line; + while (*nl && *nl != '\n') + nl++; + *nl = 0; + char *q = line + 2; + while (*q == ' ' || *q == '\t') + q++; + if (*q == 0) + return -ENOEXEC; + char *ie = q; + while (*ie && *ie != ' ' && *ie != '\t') + ie++; + char *a = ie; + while (*a == ' ' || *a == '\t') + a++; + char *ae = a + strlen(a); + while (ae > a && (ae[-1] == ' ' || ae[-1] == '\t' || ae[-1] == '\r')) + ae--; + *ae = 0; + *ie = 0; + memmove(line, q, ie - q + 1); + if (*a) { + int il = ie - q + 1; + memmove(line + il, a, ae - a + 1); + *argp = line + il; + } else { + *argp = 0; + } + return 1; +} + static int do_exec(char *path, struct argsrc *av, struct argsrc *ev) +{ + char *page = kalloc(); + if (!page) + return -ENOMEM; + char *line = page, *spath = page + 2048, *arg; + int r = script_interp(path, line, &arg); + if (r <= 0) { + kfree(page); + return r < 0 ? r : exec_elf(path, av, ev); + } + + // One level: an interpreter that is itself a script is ENOEXEC, which + // exec_elf() says by itself when it finds no ELF header there. + struct argsrc a = *av; + safestrcpy(spath, path, 2048); + a.npre = 0; + a.pre[a.npre++] = line; + if (arg) + a.pre[a.npre++] = arg; + a.pre[a.npre++] = spath; + a.skip = av->skip + 1; + r = exec_elf(line, &a, ev); + kfree(page); + return r; +} + +static int +exec_elf(char *path, struct argsrc *av, struct argsrc *ev) { uint64 _t0 = rdtsc(); fsstat.exec_calls++; @@ -293,6 +401,7 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) begin_op(); int in_op = 1; // the error path closes the transaction only once + int ret = -1; int err; if((ip = nameifollow(0, path, &err)) == 0){ @@ -305,11 +414,17 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) ilock(ip); pgdir = 0; - if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf)) + if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf)){ + ret = -ENOEXEC; // too short to be anything we can run goto bad; + } - if(elf.magic != ELF_MAGIC) + // Neither ELF nor, by now, a script. ENOEXEC is what tells musl's + // execvp() to try it with /bin/sh. + if(elf.magic != ELF_MAGIC){ + ret = -ENOEXEC; goto bad; + } if((pgdir = setupkvm()) == 0) goto bad; @@ -327,8 +442,10 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) if (interp[0]) { struct elfhdr ielf; uint64 ihdr; - if ((ip = nameifollow(0, interp, &err)) == 0) + if ((ip = nameifollow(0, interp, &err)) == 0) { + ret = -err; goto bad; + } ilock(ip); if (readi(ip, 0, (uint64)&ielf, 0, sizeof(ielf)) != sizeof(ielf) || ielf.magic != ELF_MAGIC || ielf.type != ET_DYN) @@ -529,5 +646,5 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) iunlockput(ip); if(in_op) end_op(); - return -1; + return ret; } diff --git a/kernel/fs/fs.c b/kernel/fs/fs.c index 1272120..38a0fe9 100644 --- a/kernel/fs/fs.c +++ b/kernel/fs/fs.c @@ -1675,6 +1675,11 @@ static struct { char path[MAXPATH]; char source[MAXPATH]; char type[16]; + // The root of whoever mounted it, 0 for the system's. One filesystem can + // be mounted twice -- /dev, and /dev again inside a chroot -- and ".." + // out of its root has to know which mount it is leaving. See mount_up(). + uint ctxdev; + uint64 ctxinum; } m[NMOUNT]; } mtable; @@ -1722,6 +1727,9 @@ mount_add(uint dev, uint64 inum, uint fsdev, uint64 root, mstrcpy(mtable.m[i].path, path, MAXPATH); mstrcpy(mtable.m[i].source, source, MAXPATH); mstrcpy(mtable.m[i].type, type, 16); + struct inode *r = myproc() ? myproc()->root : 0; + mtable.m[i].ctxdev = r ? r->dev : 0; + mtable.m[i].ctxinum = r ? r->inum : 0; release(&mtable.lock); return 0; } @@ -1794,24 +1802,43 @@ mount_down(uint dev, uint64 inum, uint *fsdev, uint64 *root) static int mount_up(uint dev, uint64 inum, uint *pdev, uint64 *pinum) { + // Of several mounts of the same filesystem, the one made under the + // caller's own root. namex_from() asks this only for a walk that started + // inside a mount; one that crossed into it knows which mount it took. + struct inode *r = myproc() ? myproc()->root : 0; + uint cdev = r ? r->dev : 0; + uint64 cinum = r ? r->inum : 0; + int found = -1; + acquire(&mtable.lock); for (int i = 0; i < NMOUNT; i++) if (mtable.m[i].used && mtable.m[i].fsdev == dev && mtable.m[i].root == inum) { - *pdev = mtable.m[i].dev; - *pinum = mtable.m[i].inum; - release(&mtable.lock); - return 1; + if (found < 0 || + (mtable.m[i].ctxdev == cdev && mtable.m[i].ctxinum == cinum)) + found = i; } + if (found >= 0) { + *pdev = mtable.m[found].dev; + *pinum = mtable.m[found].inum; + } release(&mtable.lock); - return 0; + return found >= 0; } // Follow every mount stacked on this inode, releasing each one as we go. A // loop rather than a single step because nothing stops a second filesystem // being mounted on the first one's root. +// The mounts one lookup has crossed, innermost last, so that ".." leaves each +// the way the walk came in. See namex_from(). +#define NCROSS 16 +struct crossing { + int n; + struct { uint rdev; uint64 rinum; uint cdev; uint64 cinum; } c[NCROSS]; +}; + static struct inode * -mount_cross(struct inode *ip) +mount_cross_rec(struct inode *ip, struct crossing *x) { uint fsdev; uint64 root; @@ -1820,6 +1847,11 @@ mount_cross(struct inode *ip) while (mount_down(ip->dev, ip->inum, &fsdev, &root)) { if (++hops > NMOUNT) break; /* mounts stacked in a cycle */ + if (x && x->n < NCROSS) { + x->c[x->n].rdev = fsdev; x->c[x->n].rinum = root; + x->c[x->n].cdev = ip->dev; x->c[x->n].cinum = ip->inum; + x->n++; + } struct inode *next = iget(fsdev, root); iput(ip); ip = next; @@ -1827,6 +1859,23 @@ mount_cross(struct inode *ip) return ip; } +// Where an absolute path starts: the caller's root, or the system's. +static struct inode * +root_inode(void) +{ + struct proc *p = myproc(); + if (p && p->root) + return idup(p->root); + return iget(SCHFS_DEV, schfs_kroot(SCHFS_DEV)); +} + +static int +is_proc_root(struct inode *ip) +{ + struct proc *p = myproc(); + return p && p->root && ip->dev == p->root->dev && ip->inum == p->root->inum; +} + // Resolves `path` starting from `start` (idup'd internally — caller keeps // ownership of its own reference) rather than always `myproc()->cwd`. This is // what makes a real (non-AT_FDCWD) dirfd argument to the *at() syscalls @@ -1872,22 +1921,43 @@ namex_from(struct inode *start, char *path, int nameiparent, char *name, // Root is a SchFS object, not a v1 inode. ops_for(SCHFS_DEV) then routes // every operation on it through schfs_ops, which is the whole point: the v1 // format code in this file is no longer reachable from `/`. - ip = (*spath == '/') ? iget(SCHFS_DEV, schfs_kroot(SCHFS_DEV)) : idup(start); + struct crossing x; + x.n = 0; + ip = (*spath == '/') ? root_inode() : idup(start); // The root itself can be a mount point, and a relative walk can start on one. - ip = mount_cross(ip); + ip = mount_cross_rec(ip, &x); while ((spath = skipelem(spath, name, &toolong)) != 0) { - // ".." out of a mounted filesystem's root leaves it. The filesystem down - // there does not know it is mounted anywhere and would answer with its own - // root, which turns the mount into a subtree you can enter and never - // leave. Step up to the covered directory first and resolve ".." there. if (name[0] == '.' && name[1] == '.' && name[2] == '\0') { + // ".." at a chroot's root is the root: that is what makes it one. + if (is_proc_root(ip)) + continue; + // ".." out of a mounted filesystem's root leaves it. The filesystem down + // there does not know it is mounted anywhere and would answer with its + // own root, which turns the mount into a subtree you can enter and never + // leave. Step up to the covered directory first and resolve ".." there + // -- the one this walk came in through, if it did, since the same + // filesystem can be mounted in more than one place. uint pdev; uint64 pinum; - if (mount_up(ip->dev, ip->inum, &pdev, &pinum)) { + int up = 0; + if (x.n > 0 && x.c[x.n - 1].rdev == ip->dev && + x.c[x.n - 1].rinum == ip->inum) { + x.n--; + pdev = x.c[x.n].cdev; + pinum = x.c[x.n].cinum; + up = 1; + } else { + up = mount_up(ip->dev, ip->inum, &pdev, &pinum); + } + if (up) { struct inode *cover = iget(pdev, pinum); iput(ip); ip = cover; + // The covered directory may itself be the chroot's root -- a chroot + // made at a mount point. + if (is_proc_root(ip)) + continue; } } @@ -1907,7 +1977,7 @@ namex_from(struct inode *start, char *path, int nameiparent, char *name, goto fail; } iunlock(ip); - next = mount_cross(next); + next = mount_cross_rec(next, &x); ilock(next); if (next->type != T_SYMLINK || (*spath == '\0' && (!follow || nameiparent))) { @@ -1945,7 +2015,8 @@ namex_from(struct inode *start, char *path, int nameiparent, char *name, spath = safe; if (*spath == '/') { iput(ip); - ip = mount_cross(iget(SCHFS_DEV, schfs_kroot(SCHFS_DEV))); + x.n = 0; + ip = mount_cross_rec(root_inode(), &x); } } diff --git a/kernel/fs/procfs.c b/kernel/fs/procfs.c index 6b295d9..584a226 100644 --- a/kernel/fs/procfs.c +++ b/kernel/fs/procfs.c @@ -615,16 +615,16 @@ const struct fs_ops procfs_ops = { // so they are unreachable and harmless. Removing them from init is a separate, // later cleanup -- doing both at once would make a boot failure ambiguous // between "the mount is wrong" and "the nodes are missing". -void -procfs_mount(void) +int +procfs_mount(const char *target) { - struct inode *dp = namei("/proc"); + struct inode *dp = namei((char *)target); + int r; - if (dp == 0) { - cprintf("procfs: /proc does not exist; not mounted\n"); - return; - } - if (mount_add(dp->dev, dp->inum, PROCFS_DEV, PROCFS_ROOT, "/proc", "proc", "proc") < 0) - cprintf("procfs: /proc already has something mounted on it\n"); + if (dp == 0) + return -ENOENT; + r = mount_add(dp->dev, dp->inum, PROCFS_DEV, PROCFS_ROOT, (char *)target, + "proc", "proc"); iput(dp); + return r < 0 ? -EBUSY : 0; } diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index f707c266..f132828 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -503,6 +503,7 @@ fork(void) np->gid = myproc()->gid; np->egid = myproc()->egid; np->sgid = myproc()->sgid; np->cwd = idup(myproc()->cwd); + np->root = myproc()->root ? idup(myproc()->root) : 0; memmove(np->cwd_path, myproc()->cwd_path, sizeof(myproc()->cwd_path)); np->fs_base = myproc()->fs_base; @@ -662,8 +663,11 @@ exit(int status) begin_op(); iput(myproc()->cwd); + if (myproc()->root) + iput(myproc()->root); end_op(); myproc()->cwd = 0; + myproc()->root = 0; acquire(&ptable.lock); diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index c90764b..8ba18f9 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -1083,23 +1083,26 @@ addr_t sys_truncate(void) // no two directory locks are ever held at once -- each is locked, used, and // unlocked before the next -- and `ip` is unlocked throughout, held alive by // the nlink bump instead. -addr_t sys_rename(void) +// +// renameat() is this with directory fds. It used to be a copy of the old +// rename, both faults included, and apk -- which renames every file it +// installs and its database -- met them both. +static addr_t +do_rename(int olddirfd, char *oldpath, int newdirfd, char *newpath) { - char *oldpath, *newpath; struct inode *ip, *dp_old, *dp_new, *ip_src, *ip_dst; char oldname[VFS_NAME_MAX + 1], newname[VFS_NAME_MAX + 1]; uint off, dstoff; int err = EPERM; - if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -EFAULT; begin_op(); - if (!(ip = namei(oldpath))) { end_op(); return -ENOENT; } + if (!(ip = at_namei(olddirfd, oldpath))) { end_op(); return -ENOENT; } ilock(ip); // Renaming a directory needs the ".." fixup and the loop check that are // not written here. EISDIR names what is missing; EPERM did not. if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; } ip->nlink++; iupdate(ip); iunlock(ip); - dp_old = nameiparent(oldpath, oldname); - dp_new = nameiparent(newpath, newname); + dp_old = at_nameiparent(olddirfd, oldpath, oldname); + dp_new = at_nameiparent(newdirfd, newpath, newname); if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) { err = (!dp_old || !dp_new) ? ENOENT : EXDEV; if (dp_old) iput(dp_old); @@ -1155,6 +1158,13 @@ bad_rename: ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -err; } +addr_t sys_rename(void) +{ + char *oldpath, *newpath; + if (argstr(0, &oldpath) < 0 || argstr(1, &newpath) < 0) return -EFAULT; + return do_rename(AT_FDCWD, oldpath, AT_FDCWD, newpath); +} + // symlink(2). // // The errno matters more here than anywhere else in this file, because the one @@ -1922,7 +1932,26 @@ addr_t sys_fstatfs(void) } // chroot: not supported. -addr_t sys_chroot(void) { return -EPERM; } +// Make `path` this process's root: where an absolute path starts and where +// ".." stops (see namex_from). The working directory is left where it is, as +// on Linux; a program that means to stay inside chdir()s there next. +addr_t sys_chroot(void) +{ + char *path; + struct inode *ip; + + if (argstr(0, &path) < 0) return -EFAULT; + begin_op(); + if ((ip = namei(path)) == 0) { end_op(); return -ENOENT; } + ilock(ip); + if (ip->type != T_DIR) { iunlockput(ip); end_op(); return -ENOTDIR; } + iunlock(ip); + if (myproc()->root) + iput(myproc()->root); + myproc()->root = ip; + end_op(); + return 0; +} // ----------------------------------------------------------------------- // at-syscalls @@ -1963,86 +1992,50 @@ addr_t sys_mknodat(void) return do_mknod(dirfd, path, (uint32)mode, dev); } +// The errors unlink_path() gives, for the same reason: apk removes each file +// before it writes the new one and expects ENOENT when there was none. Every +// failure here was -1, read as EPERM, and every file failed to extract. addr_t sys_unlinkat(void) { - int dirfd, flags; + int dirfd, flags, err; char *path; struct inode *ip, *dp; char name[VFS_NAME_MAX + 1]; uint off; argint(0, &dirfd); - if (argstr(1, &path) < 0) return -1; + if (argstr(1, &path) < 0) return -EFAULT; argint(2, &flags); begin_op(); - if (!(dp = at_nameiparent(dirfd, path, name))) { end_op(); return -1; } + if (!(dp = at_nameiparent(dirfd, path, name))) { end_op(); return -ENOENT; } 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; - } + if (namecmp(name, ".") == 0 || namecmp(name, "..") == 0) { err = EINVAL; goto bad; } + if (!(ip = dirlookup(dp, name, &off))) { err = ENOENT; goto bad; } ilock(ip); if (ip->nlink < 1) panic("unlinkat: nlink < 1"); - if (ip->type == T_DIR && !(flags & AT_REMOVEDIR)) { - 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 && !(flags & AT_REMOVEDIR)) { err = EISDIR; goto badip; } + if (ip->type != T_DIR && (flags & AT_REMOVEDIR)) { err = ENOTDIR; goto badip; } + if (ip->type == T_DIR && dirempty(ip) != 1) { err = ENOTEMPTY; goto badip; } + if (dirunlink(dp, name, off) < 0) { err = EIO; goto badip; } if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); } iunlockput(dp); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return 0; +badip: + iunlockput(ip); +bad: + iunlockput(dp); + end_op(); + return -err; } addr_t sys_renameat(void) { int olddirfd, newdirfd; char *oldpath, *newpath; - argint(0, &olddirfd); 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[VFS_NAME_MAX + 1], newname[VFS_NAME_MAX + 1]; - uint off; - struct dirent de; - begin_op(); - if (!(ip = at_namei(olddirfd, oldpath))) { end_op(); return -ENOENT; } - ilock(ip); - if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; } - ip->nlink++; iupdate(ip); iunlock(ip); - dp_old = at_nameiparent(olddirfd, oldpath, oldname); - dp_new = at_nameiparent(newdirfd, newpath, newname); - if (!dp_old || !dp_new || dp_old->dev != ip->dev || dp_new->dev != ip->dev) { - int e = (!dp_old || !dp_new) ? ENOENT : EXDEV; - if (dp_old) iput(dp_old); - if (dp_new) iput(dp_new); - ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -e; - } - ilock(dp_new); - if (dirlink(dp_new, newname, ip->inum) < 0) { - iunlockput(dp_new); iput(dp_old); - ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); end_op(); return -EEXIST; - } - iunlockput(dp_new); - ilock(dp_old); - if (!(ip_src = dirlookup(dp_old, oldname, &off))) { - iunlockput(dp_old); ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); - end_op(); return -ENOENT; - } - iput(ip_src); - memset(&de, 0, sizeof(de)); - if (writei(dp_old, 0, (uint64)&de, off, sizeof(de)) != sizeof(de)) - panic("renameat: writei"); - iunlockput(dp_old); - ilock(ip); ip->nlink--; iupdate(ip); iunlockput(ip); - end_op(); - return 0; + argint(0, &olddirfd); + argint(2, &newdirfd); + if (argstr(1, &oldpath) < 0 || argstr(3, &newpath) < 0) return -EFAULT; + return do_rename(olddirfd, oldpath, newdirfd, newpath); } addr_t sys_renameat2(void) { return sys_renameat(); } // ignore flags @@ -2216,17 +2209,13 @@ addr_t sys_mount(void) // 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; - } + if (strncmp(fstype, "proc", 5) == 0) + return procfs_mount(target); // devtmpfs: the same, and the one init actually needs. /dev used to be // twenty mknod() calls; see the note at the top of kernel/fs/devfs.c. - if (strncmp(fstype, "devtmpfs", 9) == 0 || strncmp(fstype, "dev", 4) == 0) { - devfs_mount(); - return 0; - } + if (strncmp(fstype, "devtmpfs", 9) == 0 || strncmp(fstype, "dev", 4) == 0) + return devfs_mount(target); // sysfs: the hardware, as Linux's /sys lays it out. kernel/fs/sysfs.c. if (strncmp(fstype, "sysfs", 6) == 0) diff --git a/kernel/sys/sys_mem.c b/kernel/sys/sys_mem.c index 6108fff..bede4e9 100644 --- a/kernel/sys/sys_mem.c +++ b/kernel/sys/sys_mem.c @@ -76,6 +76,52 @@ vma_note_anon(struct proc *p, uint64 va, uint64 len) return 0; } +// Nothing recorded and nothing mapped anywhere in [va, va+len). +static int +range_free(struct proc *p, uint64 va, uint64 len) +{ + for (int i = 0; i < NVMA; i++) { + struct vma *v = &p->vma[i]; + if (v->used && va < v->addr + v->length && v->addr < va + len) + return 0; + } + for (uint64 a = va; a < va + len; a += PGSIZE) + if (uva2ka(p->pgdir, (char *)a)) + return 0; + return 1; +} + +// Where a new mapping of `len` goes. +// +// Fresh addresses from the bump pointer, except for memory that can merge: +// that goes straight after an anonymous mapping whose end has room, so it +// extends the record rather than taking a slot. Without this a slot was +// spent each time the record that ended at the bump pointer had been trimmed, +// and every time munmap split one -- and musl's malloc frees 32 KB pieces out +// of the middle of larger ones all day. apk ran out of NVMA slots within a +// second of starting to install, with 780 MB free, and said "Out of memory". +// It also gives freed address space back, which the bump pointer never did. +static uint64 +mmap_va(struct proc *p, uint64 len, int mergeable) +{ + if (p->mmap_next == 0) + p->mmap_next = 0x40000000; + if (mergeable) { + for (int i = 0; i < NVMA; i++) { + struct vma *v = &p->vma[i]; + uint64 e = v->addr + v->length; + if (vma_anon(v) && e >= 0x40000000 && e + len <= p->mmap_next && + range_free(p, e, len)) + return e; + } + } + uint64 va = p->mmap_next; + if (bad_user_range(va, len)) + return 0; + p->mmap_next += len; + return va; +} + // Record [va, va+len) as anonymous memory wherever no mapping already covers // it, for a MAP_FIXED that may land anywhere: inside an earlier mapping, across // the end of one, or in a gap. Below sz is the program image and its heap, @@ -210,13 +256,13 @@ sys_mmap(void) // slot index, which a single allocation bigger than 8MB — e.g. a large // realloc() growth — could map straight past, panicking mappages("remap") // when it hit an already-present PTE in the next slot's territory). - // munmap()'d ranges are not reclaimed for reuse; that's fine at this scale. - if (myproc()->mmap_next == 0) - myproc()->mmap_next = 0x40000000; - va = myproc()->mmap_next; - if (bad_user_range(va, length)) + // Anonymous memory and private file copies may also reuse a freed range + // next to an existing one; see mmap_va(). + int mergeable = !(f && (f->type == FD_MEMFD || + (f->type == FD_INODE && f->ip->type == T_DEV))); + va = mmap_va(myproc(), length, mergeable); + if (va == 0) return -ENOMEM; - myproc()->mmap_next += length; int is_fb = 0; int shmid = -1;