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

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