SchOS/kernel/core/kalloc.c
Nexoniarz 0750e7cbf4 Free every usable memory region, not one contiguous run
e820_init() took the usable run *containing the kernel* and treated its
end as the top of RAM, and kinit1() walked a single range from the end
of the kernel image to there. That is the shape of a BIOS map. It is
not the shape of a UEFI one: OVMF places an ACPI NVS region at 8 MB,
so the kernel's run ended there, the map failed the PHYSTOP_MIN check,
and the kernel fell back to asserting 224 MB it had not measured --
then freed straight through the reserved hole anyway.

Keep the usable regions as a list as well as a top address. kinit1()
now frees each one, clipped to the window main() passes in, so the
holes are skipped rather than either freed or fatal. PHYSTOP still
means what it always did -- the top of what kalloc may hand out, and
the bound kfree() rejects pages above -- so no call site changes.

phystop comes from the largest region rather than the highest. The DMA
arena is carved off the top of PHYSTOP and must be physically
contiguous, and the largest region is the only one certain to have
room; a smaller region above it is left unfreed rather than raising
the bound past a hole, because kfree()'s check is worth more than the
pages it costs. Report the usable total separately from the top
address, since with a fragmented map they are different numbers and
printing one as the other is how 8 MB went unnoticed.

Verified on both paths. BIOS: unchanged at 1023 MB in 2 regions,
bootcheck 27/27. UEFI under OVMF: 1018 MB in 7 regions, up from the
8 MB that triggered the fallback, booting through vga, e1000, the RTC
and on to cpu0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 20:03:51 +02:00

158 lines
3.9 KiB
C

// Physical memory allocator, intended to allocate
// memory for user processes, kernel stacks, page table pages,
// and pipe buffers. Allocates 4096-byte pages.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
void freerange(void *vstart, void *vend);
extern char end[]; // first address after kernel loaded from ELF file
struct run {
struct run *next;
};
struct {
struct spinlock lock;
int use_lock;
struct run *freelist;
} kmem;
// Catch kmem.freelist corruption at the point it's touched, rather than
// letting a bad pointer be silently returned by kalloc() and crash whatever
// code dereferences it minutes later on an unrelated call stack.
static void
check_run(struct run *r, char *where)
{
if(r == 0)
return;
if((addr_t)r % PGSIZE || (char*)r < end || V2P(r) >= PHYSTOP){
cprintf("kalloc: corrupt freelist entry %p at %s\n", r, where);
panic("kalloc: corrupt freelist");
}
}
// vstart..vend is the window the kernel is willing to give away: from the end
// of its own image to the bottom of the DMA arena. Inside that window the
// firmware's map decides what is real -- free each usable region clipped to the
// window, rather than walking the whole span and handing out the holes.
//
// The old code walked it as one range, which is what a BIOS map looks like.
// Under UEFI it is not: OVMF puts an ACPI NVS region at 8 MB, so the run
// containing the kernel stopped there and everything above it was lost. The
// window still bounds things at both ends, so nothing below the kernel image
// and nothing inside the DMA arena is ever freed, whatever the map says.
void
kinit1(void *vstart, void *vend)
{
initlock(&kmem.lock, "kmem");
kmem.use_lock = 0;
kmem.freelist = 0; // empty
// No map at all means e820_init() could not parse one; it leaves a single
// synthetic region behind for exactly this reason, so this is belt and
// braces rather than a path anything takes.
if(nmem_usable == 0){
freerange(vstart, vend);
return;
}
addr_t lo_limit = V2P((addr_t)vstart);
addr_t hi_limit = V2P((addr_t)vend);
for(int i = 0; i < nmem_usable; i++){
addr_t lo = mem_usable[i].base;
addr_t hi = mem_usable[i].end;
if(lo < lo_limit)
lo = lo_limit;
if(hi > hi_limit)
hi = hi_limit;
if(lo >= hi)
continue;
freerange(P2V(lo), P2V(hi));
}
}
void
kinit2()
{
kmem.use_lock = 1;
}
void
freerange(void *vstart, void *vend)
{
char *p;
p = (char*)PGROUNDUP((addr_t)vstart);
for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(char *v)
{
struct run *r;
if((addr_t)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(v, 1, PGSIZE);
if(kmem.use_lock)
acquire(&kmem.lock);
check_run(kmem.freelist, "kfree entry");
r = (struct run*)v;
r->next = kmem.freelist;
kmem.freelist = r;
if(kmem.use_lock)
release(&kmem.lock);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
char*
kalloc(void)
{
struct run *r;
if(kmem.use_lock)
acquire(&kmem.lock);
check_run(kmem.freelist, "kalloc entry");
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
if(kmem.use_lock)
release(&kmem.lock);
return (char*)r;
}
// Total free physical memory, in bytes. Walks the free list; only /proc
// readers call it, so the O(n) scan is not on any hot path.
uint64
kalloc_free_bytes(void)
{
struct run *r;
uint64 n = 0;
if(kmem.use_lock)
acquire(&kmem.lock);
for(r = kmem.freelist; r; r = r->next)
n += PGSIZE;
if(kmem.use_lock)
release(&kmem.lock);
return n;
}