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:
parent
6f3175d8a5
commit
2cfa3f0bf2
9 changed files with 361 additions and 129 deletions
143
kernel/fs/exec.c
143
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue