diff --git a/kernel/mem/vmm/map.go b/kernel/mem/vmm/map.go index 298e29a..ccfa128 100644 --- a/kernel/mem/vmm/map.go +++ b/kernel/mem/vmm/map.go @@ -9,7 +9,34 @@ import ( "github.com/achilleasa/gopher-os/kernel/mem/pmm" ) +// ReservedZeroedFrame is a special zero-cleared frame allocated by the +// vmm package's Init function. The purpose of this frame is to assist +// in implementing on-demand memory allocation when mapping it in +// conjunction with the CopyOnWrite flag. Here is an example of how it +// can be used: +// +// func ReserveOnDemand(start vmm.Page, pageCount int) *kernel.Error { +// var err *kernel.Error +// mapFlags := vmm.FlagPresent|vmm.FlagCopyOnWrite +// for page := start; pageCount > 0; pageCount, page = pageCount-1, page+1 { +// if err = vmm.Map(page, vmm.ReservedZeroedFrame, mapFlags); err != nil { +// return err +// } +// } +// return nil +// } +// +// In the above example, page mappings are set up for the requested number of +// pages but no physical memory is reserved for their contents. A write to any +// of the above pages will trigger a page-fault causing a new frame to be +// allocated, cleared (the blank frame is copied to the new frame) and +// installed in-place with RW permissions. +var ReservedZeroedFrame pmm.Frame + var ( + // protectReservedZeroedPage is set to true to prevent mapping to + protectReservedZeroedPage bool + // nextAddrFn is used by used by tests to override the nextTableAddr // calculations used by Map. When compiling the kernel this function // will be automatically inlined. @@ -21,14 +48,21 @@ var ( // which will cause a fault if called in user-mode. flushTLBEntryFn = cpu.FlushTLBEntry - errNoHugePageSupport = &kernel.Error{Module: "vmm", Message: "huge pages are not supported"} + errNoHugePageSupport = &kernel.Error{Module: "vmm", Message: "huge pages are not supported"} + errAttemptToRWMapReservedFrame = &kernel.Error{Module: "vmm", Message: "reserved blank frame cannot be mapped with a RW flag"} ) // Map establishes a mapping between a virtual page and a physical memory frame // using the currently active page directory table. Calls to Map will use the // supplied physical frame allocator to initialize missing page tables at each // paging level supported by the MMU. +// +// Attempts to map ReservedZeroedFrame with a RW flag will result in an error. func Map(page Page, frame pmm.Frame, flags PageTableEntryFlag) *kernel.Error { + if protectReservedZeroedPage && frame == ReservedZeroedFrame && (flags&FlagRW) != 0 { + return errAttemptToRWMapReservedFrame + } + var err *kernel.Error walk(page.Address(), func(pteLevel uint8, pte *pageTableEntry) bool { @@ -76,7 +110,13 @@ func Map(page Page, frame pmm.Frame, flags PageTableEntryFlag) *kernel.Error { // to a fixed virtual address overwriting any previous mapping. The temporary // mapping mechanism is primarily used by the kernel to access and initialize // inactive page tables. +// +// Attempts to map ReservedZeroedFrame will result in an error. func MapTemporary(frame pmm.Frame) (Page, *kernel.Error) { + if protectReservedZeroedPage && frame == ReservedZeroedFrame { + return 0, errAttemptToRWMapReservedFrame + } + if err := Map(PageFromAddress(tempMappingAddr), frame, FlagPresent|FlagRW); err != nil { return 0, err } diff --git a/kernel/mem/vmm/map_test.go b/kernel/mem/vmm/map_test.go index cee8379..1ecd15c 100644 --- a/kernel/mem/vmm/map_test.go +++ b/kernel/mem/vmm/map_test.go @@ -144,6 +144,24 @@ func TestMapTemporaryErrorsAmd64(t *testing.T) { t.Fatalf("got unexpected error %v", err) } }) + + t.Run("map BlankReservedFrame RW", func(t *testing.T) { + defer func() { protectReservedZeroedPage = false }() + + protectReservedZeroedPage = true + if err := Map(Page(0), ReservedZeroedFrame, FlagRW); err != errAttemptToRWMapReservedFrame { + t.Fatalf("expected errAttemptToRWMapReservedFrame; got: %v", err) + } + }) + + t.Run("temp-map BlankReservedFrame RW", func(t *testing.T) { + defer func() { protectReservedZeroedPage = false }() + + protectReservedZeroedPage = true + if _, err := MapTemporary(ReservedZeroedFrame); err != errAttemptToRWMapReservedFrame { + t.Fatalf("expected errAttemptToRWMapReservedFrame; got: %v", err) + } + }) } func TestUnmapAmd64(t *testing.T) { diff --git a/kernel/mem/vmm/vmm.go b/kernel/mem/vmm/vmm.go index be04f6f..b698f5c 100644 --- a/kernel/mem/vmm/vmm.go +++ b/kernel/mem/vmm/vmm.go @@ -120,11 +120,35 @@ func generalProtectionFaultHandler(_ uint64, frame *irq.Frame, regs *irq.Regs) { panicFn(nil) } +// reserveZeroedFrame reserves a physical frame to be used together with +// FlagCopyOnWrite for lazy allocation requests. +func reserveZeroedFrame() *kernel.Error { + var ( + err *kernel.Error + tempPage Page + ) + + if ReservedZeroedFrame, err = frameAllocator(); err != nil { + return err + } else if tempPage, err = mapTemporaryFn(ReservedZeroedFrame); err != nil { + return err + } + mem.Memset(tempPage.Address(), 0, mem.PageSize) + unmapFn(tempPage) + + // From this point on, ReservedZeroedFrame cannot be mapped with a RW flag + protectReservedZeroedPage = true + return nil +} + // Init initializes the vmm system and installs paging-related exception // handlers. func Init() *kernel.Error { + if err := reserveZeroedFrame(); err != nil { + return err + } + handleExceptionWithCodeFn(irq.PageFaultException, pageFaultHandler) handleExceptionWithCodeFn(irq.GPFException, generalProtectionFaultHandler) - return nil } diff --git a/kernel/mem/vmm/vmm_test.go b/kernel/mem/vmm/vmm_test.go index f7e732b..f4ecbb0 100644 --- a/kernel/mem/vmm/vmm_test.go +++ b/kernel/mem/vmm/vmm_test.go @@ -213,14 +213,69 @@ func TestGPtHandler(t *testing.T) { func TestInit(t *testing.T) { defer func() { + frameAllocator = nil + mapTemporaryFn = MapTemporary + unmapFn = Unmap handleExceptionWithCodeFn = irq.HandleExceptionWithCode }() - handleExceptionWithCodeFn = func(_ irq.ExceptionNum, _ irq.ExceptionHandlerWithCode) {} + // reserve space for an allocated page + reservedPage := make([]byte, mem.PageSize) - if err := Init(); err != nil { - t.Fatal(err) - } + t.Run("success", func(t *testing.T) { + // fill page with junk + for i := 0; i < len(reservedPage); i++ { + reservedPage[i] = byte(i % 256) + } + + SetFrameAllocator(func() (pmm.Frame, *kernel.Error) { + addr := uintptr(unsafe.Pointer(&reservedPage[0])) + return pmm.Frame(addr >> mem.PageShift), nil + }) + unmapFn = func(p Page) *kernel.Error { return nil } + mapTemporaryFn = func(f pmm.Frame) (Page, *kernel.Error) { return Page(f), nil } + handleExceptionWithCodeFn = func(_ irq.ExceptionNum, _ irq.ExceptionHandlerWithCode) {} + + if err := Init(); err != nil { + t.Fatal(err) + } + + // reserved page should be zeroed + for i := 0; i < len(reservedPage); i++ { + if reservedPage[i] != 0 { + t.Errorf("expected reserved page to be zeroed; got byte %d at index %d", reservedPage[i], i) + } + } + }) + + t.Run("blank page allocation error", func(t *testing.T) { + expErr := &kernel.Error{Module: "test", Message: "out of memory"} + + SetFrameAllocator(func() (pmm.Frame, *kernel.Error) { return pmm.InvalidFrame, expErr }) + unmapFn = func(p Page) *kernel.Error { return nil } + mapTemporaryFn = func(f pmm.Frame) (Page, *kernel.Error) { return Page(f), nil } + handleExceptionWithCodeFn = func(_ irq.ExceptionNum, _ irq.ExceptionHandlerWithCode) {} + + if err := Init(); err != expErr { + t.Fatalf("expected error: %v; got %v", expErr, err) + } + }) + + t.Run("blank page mapping error", func(t *testing.T) { + expErr := &kernel.Error{Module: "test", Message: "map failed"} + + SetFrameAllocator(func() (pmm.Frame, *kernel.Error) { + addr := uintptr(unsafe.Pointer(&reservedPage[0])) + return pmm.Frame(addr >> mem.PageShift), nil + }) + unmapFn = func(p Page) *kernel.Error { return nil } + mapTemporaryFn = func(f pmm.Frame) (Page, *kernel.Error) { return Page(f), expErr } + handleExceptionWithCodeFn = func(_ irq.ExceptionNum, _ irq.ExceptionHandlerWithCode) {} + + if err := Init(); err != expErr { + t.Fatalf("expected error: %v; got %v", expErr, err) + } + }) } func readTTY(fb []byte) string {