exec: Linux's 2 MB of arguments; mmap: merge neighbouring mappings

exec took 32 arguments and 64 environment variables, in fixed arrays,
so a glob over more than 31 files failed with E2BIG. It now reads the
caller's argv and envp where they are, through the uaccess helpers and
a bounce page -- a second thread rewriting the array after a check
cannot slip a kernel address in -- and sizes the new stack to fit
them. The limits are Linux's: ARG_MAX, 2 MB of strings and pointers,
and 128 KB for any one string (USER_STR_MAX follows it).

Every mmap took one of a process's 256 VMA slots, and zsh takes one per
16 KB heap block, so `x=($(<file))` over 30000 words ran out after 4 MB
of heap and exited 1 without a word. Neighbouring anonymous mappings
now share a record, as Linux merges them -- the bump allocator puts
each new one where the last ended -- and munmap trims or splits it.
Partial unmaps of device or segment mappings are refused with EINVAL
instead of kfree()ing pages that are not the process's, and a failed
allocation half way through a mapping no longer leaks what it had.

Checked under QEMU: touch over 3000 names, /bin/echo with 50000
arguments, E2BIG past 2 MB, zsh splitting 100000 words, fork loops.

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-23 13:20:29 +02:00
commit 51983144e3
7 changed files with 366 additions and 133 deletions

View file

@ -31,6 +31,51 @@ bad_user_range(uint64 va, uint64 length)
return length == 0 || va + length < va || va + length > KERNBASE;
}
// Anonymous memory -- pages this process owns outright, with no segment or
// device behind them. These are the only mappings that may be merged, trimmed
// or split, because for them the page table is the whole truth: fork copies
// and munmap frees whatever is present in a range and skips the holes.
static int
vma_anon(const struct vma *v)
{
return v->used && !v->no_free && v->shmid < 0;
}
static struct vma *
vma_free_slot(struct proc *p)
{
for (int i = 0; i < NVMA; i++)
if (!p->vma[i].used)
return &p->vma[i];
return 0;
}
// Record [va, va+len) as anonymous memory.
//
// Every mapping used to take a slot of its own, so a process could hold NVMA
// of them and no more -- and zsh takes one per 16 KB heap block, so splitting
// 30000 words into an array failed after 4 MB of heap with a silent exit 1.
// Addresses come from a bump allocator, so a new mapping almost always starts
// where the last one ended, and it extends that one instead. Linux merges
// neighbouring anonymous mappings for the same reason.
static int
vma_note_anon(struct proc *p, uint64 va, uint64 len)
{
for (int i = 0; i < NVMA; i++) {
struct vma *v = &p->vma[i];
if (vma_anon(v) && v->addr + v->length == va) {
v->length += len;
return 0;
}
}
struct vma *v = vma_free_slot(p);
if (!v)
return -ENOMEM;
v->addr = va; v->length = len; v->used = 1;
v->no_free = 0; v->shmid = -1;
return 0;
}
addr_t
sys_sbrk(void)
{
@ -90,15 +135,15 @@ sys_mmap(void)
return va;
}
struct vma *v = 0;
for (int i = 0; i < NVMA; i++) {
if (!myproc()->vma[i].used) { v = &myproc()->vma[i]; break; }
}
// A slot of its own, for the mappings that cannot be merged: a segment,
// device memory, the framebuffer. Anonymous memory goes through
// vma_note_anon() instead and usually needs none.
//
// -ENOMEM, not -1. A bare -1 reaches musl as -EPERM, so a process that
// had simply used all NVMA slots was told "operation not permitted" --
// which sends you looking for a permission model this system does not
// have. malloc() turns either into NULL, but only one of them is a clue.
if (!v) return -ENOMEM;
struct vma *v = vma_free_slot(myproc());
// Ignore non-zero hint addresses (not MAP_FIXED); always pick a fresh VA.
// Bump-allocated: each mapping gets its own disjoint, exactly-length-sized
@ -139,6 +184,8 @@ sys_mmap(void)
// it is refused with the errno that says the range is wrong.
if (offset + length > have)
return -EINVAL;
if (!v)
return -ENOMEM;
shm_ref(id);
for (uint64 o = 0; o < length; o += PGSIZE) {
@ -176,10 +223,14 @@ sys_mmap(void)
for (uint64 a = va; a < va + length; a += PGSIZE) {
char *mem = kalloc();
if (!mem) return -ENOMEM;
if (!mem) {
vm_unmap_range(myproc()->pgdir, va, a - va);
return -ENOMEM;
}
memset(mem, 0, PGSIZE);
if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0) {
kfree(mem);
vm_unmap_range(myproc()->pgdir, va, a - va);
return -ENOMEM;
}
}
@ -190,8 +241,11 @@ sys_mmap(void)
readi(f->ip, 0, va, offset, length);
iunlock(f->ip);
v->addr = va; v->length = length; v->used = 1;
v->no_free = 0; v->shmid = -1;
// A private copy, so from here it is anonymous memory like any other.
if (vma_note_anon(myproc(), va, length) < 0) {
vm_unmap_range(myproc()->pgdir, va, length);
return -ENOMEM;
}
return va;
}
@ -206,6 +260,8 @@ sys_mmap(void)
uint64 len = PGROUNDUP(length);
if (!pciuser_mmio_ok(phys, len))
return -EACCES;
if (!v)
return -ENOMEM;
// PTE_PWT: device registers are not memory, and a cached write to a
// doorbell arrives whenever the cache feels like it, which for a
// driver is indistinguishable from hardware that ignores you.
@ -230,6 +286,8 @@ sys_mmap(void)
return -ENODEV;
if (length > fbsz)
length = fbsz;
if (!v)
return -ENOMEM;
if (mappages(myproc()->pgdir, (void*)va, length, fb, PTE_W | PTE_U) < 0)
return -1;
is_fb = 1;
@ -240,11 +298,22 @@ sys_mmap(void)
} else {
for (uint64 a = va; a < va + length; a += PGSIZE) {
char *mem = kalloc();
if (!mem) return -1;
if (!mem) {
vm_unmap_range(myproc()->pgdir, va, a - va);
return -ENOMEM;
}
memset(mem, 0, PGSIZE);
if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0)
return -1;
if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0) {
kfree(mem);
vm_unmap_range(myproc()->pgdir, va, a - va);
return -ENOMEM;
}
}
if (vma_note_anon(myproc(), va, length) < 0) {
vm_unmap_range(myproc()->pgdir, va, length);
return -ENOMEM;
}
return va;
}
v->addr = va; v->length = length; v->used = 1;
@ -256,27 +325,66 @@ addr_t
sys_munmap(void)
{
uint64 addr, length;
struct proc *p = myproc();
if (argaddr(0, &addr) < 0 || argaddr(1, &length) < 0)
return -1;
length = PGROUNDUP(length);
if (bad_user_range(addr, length))
if (bad_user_range(addr, length) || (addr % PGSIZE))
return -EINVAL;
uint64 end = addr + length;
// A segment or device mapping goes only as a whole, by its own address.
// Part of one is refused: unmapping it the anonymous way below would kfree
// pages that are the framebuffer's or another process's.
for (int i = 0; i < NVMA; i++) {
if (myproc()->vma[i].used && myproc()->vma[i].addr == addr) {
if (myproc()->vma[i].shmid >= 0)
shm_detach(myproc()->vma[i].shmid);
if (myproc()->vma[i].no_free)
vm_unmap_norelease(myproc()->pgdir, addr, myproc()->vma[i].length);
struct vma *v = &p->vma[i];
if (!v->used || vma_anon(v))
continue;
if (v->addr == addr) {
if (v->shmid >= 0)
shm_detach(v->shmid);
if (v->no_free)
vm_unmap_norelease(p->pgdir, addr, v->length);
else
vm_unmap_range(myproc()->pgdir, addr, myproc()->vma[i].length);
myproc()->vma[i].used = myproc()->vma[i].addr = myproc()->vma[i].length = 0;
myproc()->vma[i].no_free = 0;
myproc()->vma[i].shmid = -1;
vm_unmap_range(p->pgdir, addr, v->length);
v->used = v->addr = v->length = 0;
v->no_free = 0;
v->shmid = -1;
return 0;
}
if (addr < v->addr + v->length && end > v->addr)
return -EINVAL;
}
// Partial / untracked unmap — free pages anyway
vm_unmap_range(myproc()->pgdir, addr, length);
// Anonymous memory: trim every mapping the range touches, splitting one
// it falls in the middle of. With no slot for the split, the record keeps
// covering the hole, which is harmless -- fork and munmap skip pages that
// are not there, and the bump allocator never hands the addresses out
// again.
for (int i = 0; i < NVMA; i++) {
struct vma *v = &p->vma[i];
if (!vma_anon(v))
continue;
uint64 vs = v->addr, ve = v->addr + v->length;
if (end <= vs || addr >= ve)
continue;
if (addr <= vs && end >= ve) {
v->used = v->addr = v->length = 0;
} else if (addr <= vs) {
v->addr = end;
v->length = ve - end;
} else if (end >= ve) {
v->length = addr - vs;
} else {
struct vma *w = vma_free_slot(p);
if (w) {
w->addr = end; w->length = ve - end; w->used = 1;
w->no_free = 0; w->shmid = -1;
v->length = addr - vs;
}
}
}
vm_unmap_range(p->pgdir, addr, length);
return 0;
}