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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 15:28:32 +02:00
commit 2cfa3f0bf2
9 changed files with 361 additions and 129 deletions

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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;
}