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:
parent
eab13241a0
commit
51983144e3
7 changed files with 366 additions and 133 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue