From 2c4a68a75fcf7789e79d87cf8c86aa68f9305600 Mon Sep 17 00:00:00 2001 From: Maksymilian Wilczynski Date: Sat, 20 Jun 2026 01:00:38 +0200 Subject: [PATCH] add page cache with read-ahead and dirty writeback, file-backed and anonymous mmap with CoW, VAD-based virtual memory replacing the old user region model, PFN database with eviction and reclaim, file identity for cross-open section sharing, FAT32 path cache and per-file cluster chain cache, serial.sys for COM1 keyboard input, xHCI MSI with INTx and polled fallback, PciEnableMsiAuto, waitable input queue, KeWaitForAnyEvent, per-process thread info syscall, expanded kernel stats (PFN/PTE/VAD/file-cache/disk), pool allocation tags, async I/O user buffer locking, file flush on close, add docs.md. --- .gitignore | 5 +- OVMF/OVMF_VARS_4M.fd | Bin 540672 -> 540672 bytes OVMF/OVMF_VARS_4M_d.fd | Bin 540672 -> 540672 bytes code/boot.ini | 2 +- code/boot/uefi/main.c | 31 +- code/build/build.c | 71 +- code/drivers/bus/pci/pci.c | 25 +- code/drivers/bus/usb/usb.h | 1 + code/drivers/bus/usb/usb_xhci.c | 56 +- code/drivers/display/vmwaresvga/vmwaresvga.c | 8 +- code/drivers/drivers.ini | 1 + code/drivers/fs/fat32/fat32.c | 642 ++- code/drivers/fs/fat32/fat32.h | 6 +- code/drivers/fs/fat32/fat32_mount.c | 2 + code/drivers/input/serial/serial.c | 301 ++ code/drivers/net/rtl8139/rtl8139.c | 4 +- code/drivers/net/rtl8139/rtl8139.h | 1 + code/drivers/net/rtl816x/rtl816x.c | 8 +- code/drivers/net/rtl816x/rtl816x.h | 1 + code/drivers/storage/ahci/ahci.c | 56 +- code/drivers/storage/ahci/ahci.h | 3 + code/drivers/storage/ahci/ahci_port.c | 8 +- code/kernel/include/kernel/io_file.h | 61 +- code/kernel/include/kernel/ke_debug.h | 12 +- code/kernel/include/kernel/ke_display.h | 2 + code/kernel/include/kernel/ke_event.h | 3 + code/kernel/include/kernel/ke_input.h | 12 + code/kernel/include/kernel/ke_mutex.h | 8 +- code/kernel/include/kernel/ke_process.h | 10 +- code/kernel/include/kernel/ke_sched.h | 4 + code/kernel/include/kernel/ke_system_root.h | 25 + code/kernel/include/kernel/ke_thread.h | 32 +- code/kernel/include/kernel/ke_work.h | 24 + code/kernel/include/kernel/ldr_image.h | 9 +- code/kernel/include/kernel/ldr_section.h | 67 + code/kernel/include/kernel/mm_file_cache.h | 99 + code/kernel/include/kernel/mm_pfn.h | 134 + code/kernel/include/kernel/mm_phys.h | 30 +- code/kernel/include/kernel/mm_pte.h | 105 + code/kernel/include/kernel/mm_swap.h | 31 + code/kernel/include/kernel/mm_virt.h | 216 +- code/kernel/include/kernel/ob_object.h | 4 + code/kernel/include/kernel/syscall_nr.h | 87 +- code/kernel/io_device.c | 342 +- code/kernel/io_file.c | 301 +- code/kernel/ke_apc.c | 2 +- code/kernel/ke_debug.c | 276 +- code/kernel/ke_display.c | 1432 +++++-- code/kernel/ke_event.c | 210 +- code/kernel/ke_input.c | 104 +- code/kernel/ke_irp.c | 2 +- code/kernel/ke_mutex.c | 9 +- code/kernel/ke_process.c | 168 +- code/kernel/ke_sched.c | 140 +- code/kernel/ke_selftest.c | 446 +- code/kernel/ke_semaphore.c | 1 + code/kernel/ke_smp.c | 16 +- code/kernel/ke_syscall.asm | 39 +- code/kernel/ke_syscall.c | 1801 ++++++-- code/kernel/ke_system_root.c | 97 + code/kernel/ke_thread.c | 200 +- code/kernel/ke_work.c | 117 + code/kernel/kmain.c | 244 +- code/kernel/ldr_image.c | 239 +- code/kernel/ldr_section.c | 736 ++++ code/kernel/mm_file_cache.c | 1273 ++++++ code/kernel/mm_phys.c | 887 +++- code/kernel/mm_pte.c | 575 +++ code/kernel/mm_swap.c | 243 ++ code/kernel/mm_virt.c | 3612 +++++++++++++---- code/kernel/ob_object.c | 29 +- code/public/ntlite/ddi/hal_api.h | 2 +- code/public/ntlite/ddi/io_driver.h | 22 + code/public/ntlite/ddi/pci.h | 3 + code/public/ntlite/display.h | 37 + code/public/ntlite/input.h | 7 + code/public/ntlite/status.h | 22 + code/public/ntlite/syscall.h | 389 +- code/settings.ini | 4 + code/user/apps/doom/doom.c | 58 +- code/user/apps/edit/edit.c | 10 +- code/user/apps/memtop/memtop.c | 71 + code/user/apps/shell_native/shell_native.c | 1066 ++++- code/user/apps/shell_win32/shell_win32.c | 2 + code/user/apps/video/video.c | 304 +- code/user/personalities/posix/posix.h | 17 +- code/user/personalities/posix/posix_io.c | 6 +- code/user/personalities/posix/posix_mman.c | 85 + code/user/personalities/posix/posix_proc.c | 2 +- code/user/personalities/posix/posix_thread.c | 27 +- code/user/personalities/win32/gdi32.c | 99 +- code/user/personalities/win32/kernel32.c | 418 +- code/user/runtime/heap.c | 23 + code/user/runtime/heap_exports.c | 5 + code/user/syscall/syscall_nr.inc | 87 +- code/user/syscall/syscall_stubs.asm | 43 +- code/user/system/init/init.c | 4 +- code/user/tests/async_apc/async_apc.c | 42 +- code/user/tests/async_cancel/async_cancel.c | 42 +- .../tests/dirty_writeback/dirty_writeback.c | 227 ++ code/user/tests/mmap_smoke/mmap_smoke.c | 196 + .../pcache_invalidate/pcache_invalidate.c | 223 + .../pcache_mmap_read_reuse.c | 176 + .../pcache_multithread/pcache_multithread.c | 227 ++ .../tests/pcache_pressure/pcache_pressure.c | 169 + code/user/tests/pcache_smoke/pcache_smoke.c | 156 + code/user/tests/shared_mmap/shared_mmap.c | 216 + .../surface_invalid_rect.c | 105 + .../surface_multithread_present.c | 157 + code/user/tests/surface_smoke/surface_smoke.c | 93 + docs.md | 93 + image.png | Bin 337413 -> 387088 bytes readme.md | 4 +- runa.bat | 2 +- 114 files changed, 18069 insertions(+), 2620 deletions(-) create mode 100644 code/drivers/input/serial/serial.c create mode 100644 code/kernel/include/kernel/ke_system_root.h create mode 100644 code/kernel/include/kernel/ke_work.h create mode 100644 code/kernel/include/kernel/ldr_section.h create mode 100644 code/kernel/include/kernel/mm_file_cache.h create mode 100644 code/kernel/include/kernel/mm_pfn.h create mode 100644 code/kernel/include/kernel/mm_pte.h create mode 100644 code/kernel/include/kernel/mm_swap.h create mode 100644 code/kernel/ke_system_root.c create mode 100644 code/kernel/ke_work.c create mode 100644 code/kernel/ldr_section.c create mode 100644 code/kernel/mm_file_cache.c create mode 100644 code/kernel/mm_pte.c create mode 100644 code/kernel/mm_swap.c create mode 100644 code/user/apps/memtop/memtop.c create mode 100644 code/user/personalities/posix/posix_mman.c create mode 100644 code/user/tests/dirty_writeback/dirty_writeback.c create mode 100644 code/user/tests/mmap_smoke/mmap_smoke.c create mode 100644 code/user/tests/pcache_invalidate/pcache_invalidate.c create mode 100644 code/user/tests/pcache_mmap_read_reuse/pcache_mmap_read_reuse.c create mode 100644 code/user/tests/pcache_multithread/pcache_multithread.c create mode 100644 code/user/tests/pcache_pressure/pcache_pressure.c create mode 100644 code/user/tests/pcache_smoke/pcache_smoke.c create mode 100644 code/user/tests/shared_mmap/shared_mmap.c create mode 100644 code/user/tests/surface_invalid_rect/surface_invalid_rect.c create mode 100644 code/user/tests/surface_multithread_present/surface_multithread_present.c create mode 100644 code/user/tests/surface_smoke/surface_smoke.c create mode 100644 docs.md diff --git a/.gitignore b/.gitignore index 0075d3e..5be5314 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ build.obj bin/ obj/ qemu.log -*.vhd +disk.vhd +usb.vhd +disk.vmdk +code.zip code/build/build.exe code/build/build.exe.old \ No newline at end of file diff --git a/OVMF/OVMF_VARS_4M.fd b/OVMF/OVMF_VARS_4M.fd index 1ee7a928d36fb0e2e96b5bd7e5151a74f75c18f3..a943fd52f9d83e851ae2e5b64c0983f95b77d323 100644 GIT binary patch delta 90 zcmZo@P;3CA7RD`1hh|Ln3v$}dGLz{N*XDP*yI2{$H%|=e7GU&&P`*IQQ($tMs?y{F nJq1QTAon6y`?bYP+pjHV{-xO_z|tnb+9tr(CcwT-fWrU)K-M7% delta 59 zcmZo@P;3CA7RD`1hh|JpQ&pN=pr^2%WhT=lu6DL(OxxL>G5^xsKH)W!fm53ROPc^| On*dvz0Q)il4g&z6yA)dh diff --git a/OVMF/OVMF_VARS_4M_d.fd b/OVMF/OVMF_VARS_4M_d.fd index 8037eed2cbf649adb2ae2ef2d9c81f948caabec1..36b1d5786b9b84c223e7a1e50041e3abb11e3777 100644 GIT binary patch delta 252 zcmZo@P;6*W+)$y<_-b;Zk@n;W+ZUUA46ZOTzTTW@)GxsJ2131sQ12krdkFObLVbi# zpCHs{2=xU*eT7ipAk=pV^#elvgiyaA)Ncs&2SWXYQ2!v*e=x<{qOI z0TxCCPf1|%ylqY_Oi+<#p6%^C+Zng>Y-ego*e1YIz`DIbgk=I(n*vLl0&ANBTbly= IG6fC=0AV3qRR910 delta 144 zcmZo@P;6*W+)$y<7%(}}NPBXG?TgJl23Hsv12-oc^$RctL8xE|6#}6`AygQI3Wrb; zK+0EOI^TLmC&ox1`yyAn(k@0IX44ufO9%f;6s}N SOPc~~n*v*#0{b!r4g~;YvoZky diff --git a/code/boot.ini b/code/boot.ini index 5cc5468..e6994fb 100644 --- a/code/boot.ini +++ b/code/boot.ini @@ -1,5 +1,5 @@ [kernel] ; bitmask: 1 = debug prints, 2 = self check, 4 = write debug prints to file -debug = 1 +debug = 0xffff [init] commandline = apps/shell_native.exe diff --git a/code/boot/uefi/main.c b/code/boot/uefi/main.c index 7bf1f07..c25e124 100644 --- a/code/boot/uefi/main.c +++ b/code/boot/uefi/main.c @@ -291,7 +291,7 @@ static int ParseBootIniInteger(const char* val) { static void BootIniCb(const char* section, const char* key, const char* val, void* ud) { PARSE_STATE* s = (PARSE_STATE *)ud; - // [kernel] debug = bitmask: 1=console, 2=selftests, 4=file log. + // [kernel] debug = bitmask: 1=console, 2=selftests, 4=file log, 8+=kernel diagnostics. if (!AsciiStrCmp(section, "kernel") && !AsciiStrCmp(key, "debug")) s->debug = ParseBootIniInteger(val); // [init] commandline = hello.exe. @@ -446,7 +446,7 @@ EFI_STATUS LoadPeImage(UINT8* rawFile, UINT64 virtDest, EFI_PHYSICAL_ADDRESS* ou IMAGE_DATA_DIRECTORY* dataDirs = (IMAGE_DATA_DIRECTORY*)(ntHeaders + 24 + 112); IMAGE_DATA_DIRECTORY* relocDir = &dataDirs[5]; - //IMAGE_DATA_DIRECTORY* importDir = &dataDirs[1];. + //IMAGE_DATA_DIRECTORY* importDir = &dataDirs[1]; // Allocate Physical Memory for the mapped image. UINTN pages = (sizeOfImage + PAGE_SIZE - 1) / PAGE_SIZE; @@ -619,7 +619,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable return Status; } - //Print(L"1\n");. + Print(L"1\n"); // boot.ini controls debugger and runtime policy before the kernel has a filesystem service. UINTN sz; @@ -632,7 +632,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable FreePool(bootIniRaw); } - //Print(L"2\n");. + Print(L"2\n"); // drivers.ini decides which boot and runtime drivers are loaded into the initial module list. UINT8* drvIniRaw = LoadFile(L"drivers\\drivers.ini", &sz); @@ -643,7 +643,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable FreePool(drvIniRaw); } - //Print(L"3\n");. + Print(L"3\n"); BOOT_INFO* bi = AllocateZeroPool(sizeof(BOOT_INFO)); bi->Magic = BOOT_INFO_MAGIC; @@ -675,7 +675,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable // Current mapping cursor for upper half. UINT64 virtCursor = KERNEL_VIRT_BASE; - //Print(L"4\n");. + Print(L"4\n"); // Core images are loaded first because driver import resolution depends on them. typedef struct { @@ -742,7 +742,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable while(true); } - //Print(L"5\n");. + Print(L"5\n"); // Load driver .sys files into raw buffers. for (int i = 0; i < ps.driverCount; i++) { @@ -751,9 +751,10 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable UINTN imgSz; UINT8* buf = LoadFile(path, &imgSz); - if (!buf) { - Print(L"Driver not found: %s\n", ps.drivers[i]); - continue; + if (!buf) { + Print(L"Fatal: boot driver not found: %s\n", ps.drivers[i]); + while (true); + return EFI_NOT_FOUND; } // Store file info for the kernel to process later. @@ -767,7 +768,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable mod->VirtBase = 0; // Kernel will map this later } - //Print(L"6\n");. + Print(L"6\n"); // Query GOP FIRST, before the GetMemoryMap loop. EFI_GRAPHICS_OUTPUT_PROTOCOL* gop; @@ -795,7 +796,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable } } - //Print(L"7\n");. + Print(L"7\n"); bi->ApTrampolinePhys = (uintptr_t)AllocateApTrampolinePageBelow1M(); @@ -895,7 +896,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable Map4K(PHYS_MAP_BASE + phys, phys, 0x13); // Physical Window Map, P|W|PCD } - //Print(L"8\n");. + Print(L"8\n"); // The kernel consumes a compact memory map so it does not need EFI descriptor semantics after boot. ConvertMemoryMap(&bi->MemoryMap); @@ -940,12 +941,12 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable } // Print it out to verify the bootloader actually found it before launching the kernel - //Print(L"Found RSDP at: 0x%016llX\n", bi->RsdpPhysical);. + Print(L"Found RSDP at: 0x%016llX\n", bi->RsdpPhysical); bi->PhysicalMapBase = PHYS_MAP_BASE; bi->Debug = (UINT32)ps.debug; - //Print(L"9\n");. + Print(L"9\n"); // ExitBootServices is the point of no return for firmware allocation, file I/O, and protocol access. gBS->GetMemoryMap(&mapSize, NULL, &mapKey, &descSz, &descVer); diff --git a/code/build/build.c b/code/build/build.c index deae697..6fecfb1 100644 --- a/code/build/build.c +++ b/code/build/build.c @@ -30,6 +30,7 @@ static const Driver_Target PCI_BUS_DRIVER = static const Driver_Target STANDALONE_DRIVERS[] = { DRIVER_TARGET("ps2", "obj\\drivers\\ps2", "code\\drivers\\input\\ps2\\ps2.c code\\drivers\\input\\ps2\\ps2_i8042.c"), + DRIVER_TARGET("serial", "obj\\drivers\\serial", "code\\drivers\\input\\serial\\serial.c"), DRIVER_TARGET_LIBS("ata", "obj\\drivers\\ata", "code\\drivers\\storage\\ata\\ata.c", "bin\\pci.lib"), DRIVER_TARGET_LIBS("ahci", "obj\\drivers\\ahci", "code\\drivers\\storage\\ahci\\ahci.c code\\drivers\\storage\\ahci\\ahci_port.c", "bin\\pci.lib"), DRIVER_TARGET("fat32", "obj\\drivers\\fat32", "code\\drivers\\fs\\fat32\\fat32.c code\\drivers\\fs\\fat32\\fat32_mount.c"), @@ -46,7 +47,10 @@ void create_layout() { "obj\\syscall", "obj\\runtime", "obj\\win32", "obj\\posix", "obj\\drivers", "obj\\init", "obj\\shell_win32", "obj\\shell_posix", "obj\\shell_native", "obj\\async_apc", "obj\\async_cancel", "obj\\edit", "obj\\httpget", - "obj\\tcpserve", "obj\\udp", "obj\\resolve", "obj\\ping", "obj\\doom", "obj\\video" + "obj\\tcpserve", "obj\\udp", "obj\\resolve", "obj\\ping", "obj\\doom", "obj\\video", "obj\\memtop", + "obj\\mmap_smoke", "obj\\pcache_smoke", "obj\\pcache_pressure", "obj\\dirty_writeback", "obj\\shared_mmap", + "obj\\pcache_invalidate", "obj\\pcache_mmap_read_reuse", "obj\\pcache_multithread", + "obj\\surface_smoke", "obj\\surface_invalid_rect", "obj\\surface_multithread_present" }; for (size_t i = 0; i < NOB_ARRAY_LEN(dirs); i++) nob_mkdir_if_not_exists(dirs[i]); nob_mkdir_if_not_exists(PCI_BUS_DRIVER.obj_dir); @@ -71,6 +75,41 @@ Nob_Proc run_async(const char *cmd_str) { return proc; } +// Deletes stale object files from a target directory before compiling into it. +// Link commands use directory wildcards, so removed translation units can otherwise +// survive as old .obj files and keep unresolved references alive. +bool clean_object_directory(const char *obj_dir) { + const char *cmd = nob_temp_sprintf("if exist %s\\*.obj del /q %s\\*.obj", obj_dir, obj_dir); + return run_sync(cmd); +} + +// Cleans object directories that are linked through wildcards. +// The build is intentionally simple and does not maintain per-file dependency state, +// so stale objects are more dangerous than recompiling a few extra files. +bool clean_build_object_directories(void) { + const char *dirs[] = { + "obj\\boot", "obj\\kernel", "obj\\lwip", "obj\\hal", + "obj\\syscall", "obj\\runtime", "obj\\win32", "obj\\posix", + "obj\\init", "obj\\shell_win32", "obj\\shell_posix", "obj\\shell_native", + "obj\\async_apc", "obj\\async_cancel", "obj\\edit", "obj\\httpget", + "obj\\tcpserve", "obj\\udp", "obj\\resolve", "obj\\ping", + "obj\\doom", "obj\\video", "obj\\memtop", "obj\\mmap_smoke", "obj\\pcache_smoke", "obj\\pcache_pressure", "obj\\dirty_writeback", "obj\\shared_mmap", + "obj\\pcache_invalidate", "obj\\pcache_mmap_read_reuse", "obj\\pcache_multithread", + "obj\\surface_smoke", "obj\\surface_invalid_rect", "obj\\surface_multithread_present" + }; + + for (size_t i = 0; i < NOB_ARRAY_LEN(dirs); i++) { + if (!clean_object_directory(dirs[i])) return false; + } + + if (!clean_object_directory(PCI_BUS_DRIVER.obj_dir)) return false; + for (size_t i = 0; i < NOB_ARRAY_LEN(STANDALONE_DRIVERS); i++) { + if (!clean_object_directory(STANDALONE_DRIVERS[i].obj_dir)) return false; + } + + return true; +} + // Creates an empty USB image the first time the build sees a missing usb.vhd. // The image is intentionally left unmounted and unpopulated after formatting so // callers can use it as removable FAT32 storage without the build overwriting it. @@ -111,6 +150,7 @@ int main(int argc, char **argv) { } if (!create_usb_vhd_if_missing()) return 1; create_layout(); + if (!clean_build_object_directories()) return 1; // Core images build sequentially because later images link against earlier import libraries. nob_log(NOB_INFO, "--- Building Core Subsystems ---"); @@ -196,6 +236,17 @@ int main(int argc, char **argv) { {"apps\\shell_native", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "shell_native", "/ENTRY:main"}, {"tests\\async_apc", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "async_apc", "/ENTRY:main"}, {"tests\\async_cancel", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "async_cancel", "/ENTRY:main"}, + {"tests\\mmap_smoke", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "mmap_smoke", "/ENTRY:main"}, + {"tests\\pcache_smoke", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "pcache_smoke", "/ENTRY:main"}, + {"tests\\pcache_pressure", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "pcache_pressure", "/ENTRY:main"}, + {"tests\\dirty_writeback", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "dirty_writeback", "/ENTRY:main"}, + {"tests\\shared_mmap", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "shared_mmap", "/ENTRY:main"}, + {"tests\\pcache_invalidate", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "pcache_invalidate", "/ENTRY:main"}, + {"tests\\pcache_mmap_read_reuse", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "pcache_mmap_read_reuse", "/ENTRY:main"}, + {"tests\\pcache_multithread", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "pcache_multithread", "/ENTRY:main"}, + {"tests\\surface_smoke", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "surface_smoke", "/ENTRY:main"}, + {"tests\\surface_invalid_rect", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "surface_invalid_rect", "/ENTRY:main"}, + {"tests\\surface_multithread_present", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "surface_multithread_present", "/ENTRY:main"}, {"apps\\edit", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "edit", "/ENTRY:main"}, {"apps\\httpget", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "httpget", "/ENTRY:main"}, {"apps\\tcpserve", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "tcpserve", "/ENTRY:main"}, @@ -203,7 +254,8 @@ int main(int argc, char **argv) { {"apps\\resolve", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "resolve", "/ENTRY:main"}, {"apps\\ping", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "ping", "/ENTRY:main"}, {"apps\\doom", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "doom", "/ENTRY:main"}, - {"apps\\video", "bin\\win32.lib obj\\libc.obj code\\user\\apps\\video\\bink2w64.lib", "video", "/ENTRY:main"} + {"apps\\video", "bin\\syscall.lib obj\\libc.obj code\\user\\apps\\video\\bink2w64.lib", "video", "/ENTRY:main"}, + {"apps\\memtop", "bin\\syscall.lib obj\\libc.obj obj\\runtime\\user_heap.obj obj\\runtime\\user_heap_exports.obj", "memtop", "/ENTRY:main"} }; for (size_t i = 0; i < NOB_ARRAY_LEN(apps); i++) { @@ -248,6 +300,7 @@ int main(int argc, char **argv) { {"code\\settings.ini", "Z:\\system\\settings.ini"}, {"bin\\init.exe", "Z:\\system\\init.exe"}, {"bin\\pci.sys", "Z:\\drivers\\pci.sys"}, {"bin\\ps2.sys", "Z:\\drivers\\ps2.sys"}, + {"bin\\serial.sys", "Z:\\drivers\\serial.sys"}, {"bin\\ata.sys", "Z:\\drivers\\ata.sys"}, {"bin\\ahci.sys", "Z:\\drivers\\ahci.sys"}, {"bin\\fat32.sys", "Z:\\drivers\\fat32.sys"}, {"bin\\vmwaresvga.sys", "Z:\\drivers\\vmwaresvga.sys"}, {"bin\\lwip.sys", "Z:\\drivers\\lwip.sys"}, {"bin\\usb.sys", "Z:\\drivers\\usb.sys"}, @@ -260,9 +313,19 @@ int main(int argc, char **argv) { {"bin\\httpget.exe", "Z:\\apps\\httpget.exe"}, {"bin\\tcpserve.exe", "Z:\\apps\\tcpserve.exe"}, {"bin\\udp.exe", "Z:\\apps\\udp.exe"}, {"bin\\resolve.exe", "Z:\\apps\\resolve.exe"}, {"bin\\ping.exe", "Z:\\apps\\ping.exe"}, {"bin\\doom.exe", "Z:\\apps\\doom.exe"}, - {"bin\\video.exe", "Z:\\apps\\video.exe"}, + {"bin\\video.exe", "Z:\\apps\\video.exe"}, {"bin\\memtop.exe", "Z:\\apps\\memtop.exe"}, - {"bin\\async_apc.exe", "Y:\\apps\\async_apc.exe"}, {"bin\\async_cancel.exe", "Y:\\apps\\async_cancel.exe"} + {"bin\\async_apc.exe", "Y:\\apps\\async_apc.exe"}, {"bin\\async_cancel.exe", "Y:\\apps\\async_cancel.exe"}, + {"bin\\mmap_smoke.exe", "Y:\\apps\\mmap_smoke.exe"}, {"bin\\pcache_smoke.exe", "Y:\\apps\\pcache_smoke.exe"}, + {"bin\\pcache_pressure.exe", "Y:\\apps\\pcache_pressure.exe"}, + {"bin\\dirty_writeback.exe", "Y:\\apps\\dirty_writeback.exe"}, + {"bin\\shared_mmap.exe", "Y:\\apps\\shared_mmap.exe"}, + {"bin\\pcache_invalidate.exe", "Y:\\apps\\pcache_invalidate.exe"}, + {"bin\\pcache_mmap_read_reuse.exe", "Y:\\apps\\pcache_mmap_read_reuse.exe"}, + {"bin\\pcache_multithread.exe", "Y:\\apps\\pcache_multithread.exe"}, + {"bin\\surface_smoke.exe", "Y:\\apps\\surface_smoke.exe"}, + {"bin\\surface_invalid_rect.exe", "Y:\\apps\\surface_invalid_rect.exe"}, + {"bin\\surface_multithread_present.exe", "Y:\\apps\\surface_multithread_present.exe"} }; for (size_t i = 0; i < NOB_ARRAY_LEN(copies); i++) nob_copy_file(copies[i][0], copies[i][1]); diff --git a/code/drivers/bus/pci/pci.c b/code/drivers/bus/pci/pci.c index 0f24a20..c8df4b5 100644 --- a/code/drivers/bus/pci/pci.c +++ b/code/drivers/bus/pci/pci.c @@ -13,6 +13,7 @@ #include #include #include +#include #include int _fltused; // MSVC floating-point helper anchor required by printf.obj @@ -540,7 +541,7 @@ __declspec(dllexport) KSTATUS PciMapBar(DEVICE_OBJECT* pdo, uint8_t index, uint6 return st; for (uint64_t off = 0; off < mapLength; off += PAGE_SIZE) { - st = MmMapPage(virtBase + (uintptr_t)off, physBase + off, flags, MmMapFailIfPresent); + st = MmPteMapPage(virtBase + (uintptr_t)off, physBase + off, flags, MmPteMapFailIfPresent); if (!KSUCCESS(st)) return st; } @@ -659,6 +660,28 @@ __declspec(dllexport) KSTATUS PciEnableMsi(DEVICE_OBJECT* pdo, uint8_t irqSlot) return STATUS_SUCCESS; } +// Enables a single MSI vector, allocating the nt-lite IRQ slot internally. +__declspec(dllexport) KSTATUS PciEnableMsiAuto(DEVICE_OBJECT* pdo, uint8_t* outIrqSlot) { + PCI_PDO_EXTENSION* ext; + if (!outIrqSlot || !PciIsPdo(pdo, &ext)) + return STATUS_INVALID_PARAMETER; + + if (!PciFindCapability(ext, PCI_CAP_ID_MSI)) + return STATUS_NOT_SUPPORTED; + + uint8_t irq = 0xFFu; + KSTATUS st = KeIrqAllocateMsiVector(&irq); + if (!KSUCCESS(st)) + return st; + + st = PciEnableMsi(pdo, irq); + if (!KSUCCESS(st)) + return st; + + *outIrqSlot = irq; + return STATUS_SUCCESS; +} + // Implements the driver Entry operation. KSTATUS DriverEntry(const HAL_API* hal) { if (!hal) return STATUS_INVALID_PARAMETER; diff --git a/code/drivers/bus/usb/usb.h b/code/drivers/bus/usb/usb.h index 3746a89..089db1f 100644 --- a/code/drivers/bus/usb/usb.h +++ b/code/drivers/bus/usb/usb.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #ifndef USB_DEBUG_VERBOSE diff --git a/code/drivers/bus/usb/usb_xhci.c b/code/drivers/bus/usb/usb_xhci.c index 0c394eb..847cab8 100644 --- a/code/drivers/bus/usb/usb_xhci.c +++ b/code/drivers/bus/usb/usb_xhci.c @@ -95,9 +95,9 @@ void* UsbDmaAllocPages(XHCI_CONTROLLER* xhci, size_t pages, // hid this because it had simple low memory; laptop firmware does not. PHYSICAL_ADDRESS phys = 0; if (pages == 1) { - phys = MmAllocateFrame(); + phys = MmPfnAllocate(MmPfnUseDma); } else { - phys = MmAllocateContiguous(pages); + phys = MmPfnAllocateContiguous(pages, MmPfnUseDma); } if (!phys) { @@ -1930,21 +1930,31 @@ KSTATUS XhciStartController(XHCI_CONTROLLER* xhci) { return STATUS_DEVICE_ERROR; } - st = PciGetInterrupt(xhci->Pdo, &xhci->Irq); - if (!KSUCCESS(st)) { - DbgPrint("usb: xHCI PciGetInterrupt failed st=0x%x\n", st); - return st; - } - if (xhci->Irq == 0xFFu) { - DbgPrint("usb: xHCI PCI %02x:%02x.%x has no INTx IRQ; MSI is not in pci.sys yet\n", - xhci->Bus, xhci->Dev, xhci->Fn); - return STATUS_NOT_SUPPORTED; + xhci->Irq = 0xFFu; + xhci->UsingMsi = false; + + st = PciEnableMsiAuto(xhci->Pdo, &xhci->Irq); + if (KSUCCESS(st)) { + xhci->UsingMsi = true; + DbgPrint("usb: xHCI using MSI IRQ slot %u\n", xhci->Irq); + } else { + KSTATUS intxStatus = PciGetInterrupt(xhci->Pdo, &xhci->Irq); + if (KSUCCESS(intxStatus) && xhci->Irq != 0xFFu) { + DbgPrint("usb: xHCI using legacy INTx IRQ %u\n", xhci->Irq); + } else { + xhci->Irq = 0xFFu; + DbgPrint("usb: xHCI no MSI/INTx interrupt available; using polled event-ring mode (msi=0x%x intx=0x%x)\n", + st, intxStatus); + } } - st = PciEnableDevice(xhci->Pdo, - PCI_ENABLE_MEMORY | - PCI_ENABLE_BUSMASTER | - PCI_ENABLE_INTX); + uint32_t enableFlags = PCI_ENABLE_MEMORY | PCI_ENABLE_BUSMASTER; + if (xhci->UsingMsi || xhci->Irq == 0xFFu) + enableFlags |= PCI_DISABLE_INTX; + else + enableFlags |= PCI_ENABLE_INTX; + + st = PciEnableDevice(xhci->Pdo, enableFlags); if (!KSUCCESS(st)) { DbgPrint("usb: xHCI PciEnableDevice failed st=0x%x\n", st); return st; @@ -2056,12 +2066,18 @@ KSTATUS XhciStartController(XHCI_CONTROLLER* xhci) { return st; } - KeIrqRegister(xhci->Irq, XhciIsr); - XhciEnableInterrupts(xhci); - xhci->InterruptsEnabled = true; + if (xhci->Irq != 0xFFu) { + KeIrqRegister(xhci->Irq, XhciIsr); + XhciEnableInterrupts(xhci); + xhci->InterruptsEnabled = true; + KeInsertQueueDpc(&xhci->EventDpc, (void*)0, (void*)0); + DbgPrint("usb: xHCI ISR registered on IRQ %u (%s); event ring handled by DPC\n", + xhci->Irq, xhci->UsingMsi ? "MSI" : "INTx"); + } else { + xhci->InterruptsEnabled = false; + DbgPrint("usb: xHCI started in polling mode; synchronous waits drain event ring\n"); + } xhci->Fdo->PnpState = DevicePnpStarted; - KeInsertQueueDpc(&xhci->EventDpc, (void*)0, (void*)0); - DbgPrint("usb: xHCI ISR registered on IRQ %u; event ring handled by DPC\n", xhci->Irq); st = UsbHubScheduleRootScan(&xhci->Hcd); if (!KSUCCESS(st)) diff --git a/code/drivers/display/vmwaresvga/vmwaresvga.c b/code/drivers/display/vmwaresvga/vmwaresvga.c index 180b8c2..3060af8 100644 --- a/code/drivers/display/vmwaresvga/vmwaresvga.c +++ b/code/drivers/display/vmwaresvga/vmwaresvga.c @@ -890,19 +890,13 @@ static KSTATUS VmwSvgaDisplayPresent(DISPLAY_DEVICE* device, const DISPLAY_RECT* result = syncSt; } - if (v->HardwareCursor && v->CursorVisible) - VmwSvgaCursorCommit(v); - return result; } // Implements the display Sync operation. static KSTATUS VmwSvgaDisplaySync(DISPLAY_DEVICE* device) { VMWSVGA_DEVICE* v = VmwSvgaFromDisplay(device); - KSTATUS st = VmwSvgaSyncDevice(v); - if (v && v->HardwareCursor && v->CursorVisible) - VmwSvgaCursorCommit(v); - return st; + return VmwSvgaSyncDevice(v); } // Implements the display Set Cursor Image operation. diff --git a/code/drivers/drivers.ini b/code/drivers/drivers.ini index a183c58..57ef400 100644 --- a/code/drivers/drivers.ini +++ b/code/drivers/drivers.ini @@ -1,6 +1,7 @@ [boot] pci.sys ps2.sys +serial.sys ahci.sys ata.sys usb.sys diff --git a/code/drivers/fs/fat32/fat32.c b/code/drivers/fs/fat32/fat32.c index ed99abc..836901c 100644 --- a/code/drivers/fs/fat32/fat32.c +++ b/code/drivers/fs/fat32/fat32.c @@ -128,12 +128,79 @@ typedef struct { static FAT_CACHE_ENTRY gFatCache[FAT_CACHE_SLOTS]; +#define FAT32_PATH_CACHE_SLOTS 64u +#define FAT32_PATH_CACHE_MAX 512u +#define FAT32_CHAIN_CACHE_SLOTS 16u +#define FAT32_WRITE_RUN_MAX_SECTORS 128u +#define FAT32_READ_RUN_MAX_SECTORS 512u + +typedef struct FAT32_PATH_CACHE_ENTRY { + bool Valid; + uint64_t Generation; + uint32_t Hash; + size_t PathLength; + char Path[FAT32_PATH_CACHE_MAX]; + uint32_t Cluster; + uint32_t Size; + uint8_t Attr; + uint64_t DirentLba; + uint32_t DirentOffset; +} FAT32_PATH_CACHE_ENTRY; + +typedef struct FAT32_CHAIN_CACHE_ENTRY { + bool Valid; + uint32_t LogicalStart; + uint32_t PhysicalStart; + uint32_t Length; + uint32_t NextCluster; +} FAT32_CHAIN_CACHE_ENTRY; + +// The FAT sector cache is global and direct-mapped. mmap-backed video can fault +// file pages from multiple Bink worker threads at the same time; without +// serialization one thread can evict/reload a cache slot while another thread is +// still reading a FAT entry from that slot, corrupting cluster-chain traversal. +// Use a sleepable mutex rather than a spinlock because cache misses perform disk +// I/O. The static mutex avoids allocating an object from the paging path. +static MUTEX gFatCacheMutex; +static SPINLOCK gFatCacheMutexInitLock = SPINLOCK_INIT; +static int gFatCacheMutexReady; + +static void FatCacheEnsureMutex(void) { + if (gFatCacheMutexReady) + return; + + uintptr_t flags = KeAcquireSpinlock(&gFatCacheMutexInitLock); + if (!gFatCacheMutexReady) { + memset(&gFatCacheMutex, 0, sizeof(gFatCacheMutex)); + gFatCacheMutex.Header.Type = ObjectTypeMutex; + gFatCacheMutex.Header.RefCount = 1; + gFatCacheMutex.Header.Destroy = (void*)0; + gFatCacheMutex.Lock = (SPINLOCK)SPINLOCK_INIT; + gFatCacheMutex.Owner = (struct THREAD*)0; + LIST_INIT(&gFatCacheMutex.WaitList); + gFatCacheMutexReady = 1; + } + KeReleaseSpinlock(&gFatCacheMutexInitLock, flags); +} + +static int FatCacheAcquire(void) { + if (!KeCanWait()) + return 0; + FatCacheEnsureMutex(); + return KSUCCESS(KeAcquireMutex(&gFatCacheMutex)); +} + +static void FatCacheRelease(int held) { + if (held) + KeReleaseMutex(&gFatCacheMutex); +} + // Raw (non-cached) sector I/O — forward declarations. static KSTATUS ReadSectorsRaw (FAT32_VOL *v, uint64_t lba, uint32_t count, void *buf); static KSTATUS WriteSectorsRaw(FAT32_VOL *v, uint64_t lba, uint32_t count, const void *buf); // Read one FAT sector through the cache. -// Returns a pointer into the cache buffer (valid until the next eviction). +// Caller must hold the FAT cache mutex while using the returned pointer. static KSTATUS FatCacheRead(FAT32_VOL *v, uint64_t lba, uint8_t **out) { uint32_t slot = (uint32_t)(lba % FAT_CACHE_SLOTS); FAT_CACHE_ENTRY *e = &gFatCache[slot]; @@ -170,11 +237,13 @@ static void FatCacheMarkDirty(FAT32_VOL *v, uint64_t lba) { // Implements the fat Cache Invalidate operation. static void FatCacheInvalidate(void) { + int held = FatCacheAcquire(); for (int i = 0; i < FAT_CACHE_SLOTS; i++) { gFatCache[i].vol = (FAT32_VOL*)0; gFatCache[i].valid = false; gFatCache[i].dirty = false; } + FatCacheRelease(held); } // FlushFatCache — write every dirty slot to all FAT copies, clear dirty flag. @@ -196,6 +265,10 @@ struct FAT32_VOL_S { uint64_t DataStart; // absolute LBA of cluster 2 uint32_t TotalClusters; // usable data clusters uint32_t NextFreeHint; // fix #3: start cluster for next free-scan + SPINLOCK PathCacheLock; + uint64_t PathCacheGeneration; + IO_FILESYSTEM_STATS Stats; + FAT32_PATH_CACHE_ENTRY PathCache[FAT32_PATH_CACHE_SLOTS]; }; typedef struct { uint32_t StartCluster; @@ -203,6 +276,9 @@ typedef struct { FAT32_VOL *Vol; uint64_t DirentLba; uint32_t DirentOffset; + SPINLOCK ChainCacheLock; + uint32_t ChainCacheClock; + FAT32_CHAIN_CACHE_ENTRY ChainCache[FAT32_CHAIN_CACHE_SLOTS]; } FAT32_FILE; // Raw sector I/O (bypasses FAT cache; used for file data and cache eviction). @@ -231,6 +307,7 @@ static uint64_t ClusterToLba(FAT32_VOL *v, uint32_t cluster) { // FlushFatCache — needs full FAT32_VOL definition. static void FlushFatCache(FAT32_VOL *v) { uint64_t fat0Start = (uint64_t)v->PartitionLba + v->ReservedSectors; + int held = FatCacheAcquire(); for (int i = 0; i < FAT_CACHE_SLOTS; i++) { FAT_CACHE_ENTRY *e = &gFatCache[i]; @@ -246,6 +323,8 @@ static void FlushFatCache(FAT32_VOL *v) { } e->dirty = false; } + + FatCacheRelease(held); } // FAT read / write (through cache). @@ -256,9 +335,14 @@ static uint32_t FatEntry(FAT32_VOL *v, uint32_t cluster) { + fatOffset / v->BytesPerSector; uint32_t offInSec = fatOffset % v->BytesPerSector; + int held = FatCacheAcquire(); uint8_t *sec; - if (!KSUCCESS(FatCacheRead(v, sector, &sec))) return 0; - return *(uint32_t *)(sec + offInSec) & 0x0FFFFFFF; + KSTATUS s = FatCacheRead(v, sector, &sec); + uint32_t value = 0; + if (KSUCCESS(s)) + value = *(uint32_t *)(sec + offInSec) & 0x0FFFFFFF; + FatCacheRelease(held); + return value; } // Updates fat Entry. @@ -268,19 +352,24 @@ static KSTATUS WriteFatEntry(FAT32_VOL *v, uint32_t cluster, uint32_t value) { + fatOffset / v->BytesPerSector; uint32_t offInSec = fatOffset % v->BytesPerSector; + int held = FatCacheAcquire(); uint8_t *sec; KSTATUS s = FatCacheRead(v, sector, &sec); - if (!KSUCCESS(s)) return s; + if (!KSUCCESS(s)) { + FatCacheRelease(held); + return s; + } uint32_t existing = *(uint32_t *)(sec + offInSec); *(uint32_t *)(sec + offInSec) = (existing & 0xF0000000u) | (value & 0x0FFFFFFFu); FatCacheMarkDirty(v, sector); + FatCacheRelease(held); return STATUS_SUCCESS; } // Cluster allocation (fix #3: next-free hint + two-phase wrap-around scan). -static uint32_t AllocCluster(FAT32_VOL *v) { +static uint32_t AllocClusterEx(FAT32_VOL *v, int zeroCluster) { if (v->TotalClusters == 0) return 0; uint32_t total = v->TotalClusters; @@ -295,12 +384,18 @@ static uint32_t AllocCluster(FAT32_VOL *v) { if (!KSUCCESS(WriteFatEntry(v, c, FAT32_EOC_MARK))) return 0; v->NextFreeHint = c + 1; - // Zero the cluster (required for directory correctness). - uint8_t *zero = (uint8_t *)MmAllocatePool(v->BytesPerCluster); - if (zero) { - memset(zero, 0, v->BytesPerCluster); - WriteSectors(v, ClusterToLba(v, c), v->SectorsPerCluster, zero); - MmFreePool(zero); + // Normal file growth zeroes newly allocated clusters so old + // disk contents are never exposed. Swap/pagefile creation is + // the one exception: every allocated slot is overwritten before + // it can ever be read back, so skipping these writes avoids a + // slow boot-time full-pagefile zero pass. + if (zeroCluster) { + uint8_t *zero = (uint8_t *)MmAllocatePool(v->BytesPerCluster); + if (zero) { + memset(zero, 0, v->BytesPerCluster); + WriteSectors(v, ClusterToLba(v, c), v->SectorsPerCluster, zero); + MmFreePool(zero); + } } return c; } @@ -309,14 +404,23 @@ static uint32_t AllocCluster(FAT32_VOL *v) { return 0; } -// Appends cluster. -static uint32_t AppendCluster(FAT32_VOL *v, uint32_t lastCluster) { - uint32_t newCluster = AllocCluster(v); + +static uint32_t AllocCluster(FAT32_VOL *v) { + return AllocClusterEx(v, 1); +} + +static uint32_t AppendClusterEx(FAT32_VOL *v, uint32_t lastCluster, int zeroCluster) { + uint32_t newCluster = AllocClusterEx(v, zeroCluster); if (!newCluster) return 0; if (!KSUCCESS(WriteFatEntry(v, lastCluster, newCluster))) return 0; return newCluster; } +// Appends cluster. +static uint32_t AppendCluster(FAT32_VOL *v, uint32_t lastCluster) { + return AppendClusterEx(v, lastCluster, 1); +} + // Returns chain End. static uint32_t GetChainEnd(FAT32_VOL *v, uint32_t start, uint32_t *outCount) { uint32_t cluster = start, prev = start, count = 0; @@ -973,6 +1077,302 @@ static int MatchName83V(const char *dirName, const char *dirExt, return 1; } +// Returns the path cache canonical byte used for hashes and comparisons. +static char Fat32PathCacheChar(char c) { + if (c == '/') return '\\'; + if (c >= 'A' && c <= 'Z') return (char)(c + 32); + return c; +} + +// Hashes a drive-relative path using the same case/separator rules as lookup. +static uint32_t Fat32PathCacheHash(SYS_STRING_VIEW path) { + uint32_t h = 2166136261u; + for (size_t i = 0; i < path.Length; i++) { + h ^= (uint8_t)Fat32PathCacheChar(path.Data[i]); + h *= 16777619u; + } + return h ? h : 1u; +} + +// Tests whether the cached path names the same FAT path as the caller's view. +static bool Fat32PathCachePathEquals(const FAT32_PATH_CACHE_ENTRY* e, SYS_STRING_VIEW path) { + if (!e || e->PathLength != path.Length) return false; + for (size_t i = 0; i < path.Length; i++) { + if (Fat32PathCacheChar(e->Path[i]) != Fat32PathCacheChar(path.Data[i])) + return false; + } + return true; +} + +// Adds directory search counters without holding the lock during disk I/O. +static void Fat32PathCacheAddDirectoryStats(FAT32_VOL* v, uint64_t clusters, uint64_t entries) { + if (!v) return; + uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); + v->Stats.DirectorySearches++; + v->Stats.DirectoryClustersRead += clusters; + v->Stats.DirectoryEntriesScanned += entries; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + +// Looks up a positive path-cache entry for a full drive-relative path. +static bool Fat32PathCacheLookup(FAT32_VOL* v, SYS_STRING_VIEW path, + uint32_t* outCluster, uint32_t* outSize, + uint8_t* outAttr, uint64_t* outDirentLba, + uint32_t* outDirentOffset) { + if (!v || path.Length == 0 || path.Length >= FAT32_PATH_CACHE_MAX) + return false; + + uint32_t hash = Fat32PathCacheHash(path); + uint32_t slot = hash % FAT32_PATH_CACHE_SLOTS; + bool hit = false; + + uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); + v->Stats.PathLookups++; + FAT32_PATH_CACHE_ENTRY* e = &v->PathCache[slot]; + if (e->Valid && e->Generation == v->PathCacheGeneration && + e->Hash == hash && Fat32PathCachePathEquals(e, path)) { + if (outCluster) *outCluster = e->Cluster; + if (outSize) *outSize = e->Size; + if (outAttr) *outAttr = e->Attr; + if (outDirentLba) *outDirentLba = e->DirentLba; + if (outDirentOffset) *outDirentOffset = e->DirentOffset; + v->Stats.PathCacheHits++; + hit = true; + } else { + v->Stats.PathCacheMisses++; + } + KeReleaseSpinlock(&v->PathCacheLock, flags); + return hit; +} + +// Stores a positive path lookup in the volume-local path cache. +static void Fat32PathCacheStore(FAT32_VOL* v, SYS_STRING_VIEW path, + uint32_t cluster, uint32_t size, uint8_t attr, + uint64_t direntLba, uint32_t direntOffset) { + if (!v || path.Length == 0 || path.Length >= FAT32_PATH_CACHE_MAX) + return; + + uint32_t hash = Fat32PathCacheHash(path); + uint32_t slot = hash % FAT32_PATH_CACHE_SLOTS; + + uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); + FAT32_PATH_CACHE_ENTRY* e = &v->PathCache[slot]; + memset(e, 0, sizeof(*e)); + e->Valid = true; + e->Generation = v->PathCacheGeneration; + e->Hash = hash; + e->PathLength = path.Length; + memcpy(e->Path, path.Data, path.Length); + e->Path[path.Length] = '\0'; + e->Cluster = cluster; + e->Size = size; + e->Attr = attr; + e->DirentLba = direntLba; + e->DirentOffset = direntOffset; + v->Stats.PathCacheStores++; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + +// Invalidates every positive path-cache entry after a directory or size mutation. +static void Fat32PathCacheInvalidate(FAT32_VOL* v) { + if (!v) return; + uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); + v->PathCacheGeneration++; + if (v->PathCacheGeneration == 0) + v->PathCacheGeneration = 1; + v->Stats.PathCacheInvalidations++; + v->Stats.PathCacheGeneration = v->PathCacheGeneration; + for (uint32_t i = 0; i < FAT32_PATH_CACHE_SLOTS; i++) + v->PathCache[i].Valid = false; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + +// Returns the current filesystem cache and traversal counters. +void Fat32QueryStats(IO_FILESYSTEM* fs, IO_FILESYSTEM_STATS* out) { + if (!fs || !out) return; + FAT32_VOL* v = (FAT32_VOL*)fs->DriverCtx; + if (!v) { memset(out, 0, sizeof(*out)); return; } + + uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); + *out = v->Stats; + out->PathCacheGeneration = v->PathCacheGeneration; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + + +// Adds FAT-chain traversal counters shared by the read path helpers. +static void Fat32StatsAddChainWalk(FAT32_VOL* v, uint64_t steps) { + if (!v || !steps) return; + uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); + v->Stats.FatChainWalkSteps += steps; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + +// Records one data read span and its physically contiguous cluster merges. +static void Fat32StatsAddReadRun(FAT32_VOL* v, uint32_t sectors, uint32_t merges) { + if (!v || !sectors) return; + uintptr_t flags = KeAcquireSpinlock(&v->PathCacheLock); + v->Stats.DataReadRuns++; + v->Stats.DataReadSectors += sectors; + v->Stats.DataReadRunMerges += merges; + if (v->Stats.MaxDataReadSpanSectors < sectors) + v->Stats.MaxDataReadSpanSectors = sectors; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + +// Initializes the per-open cluster-chain cache for a FAT file object. +static void Fat32FileInitChainCache(FAT32_FILE* f) { + if (!f) return; + f->ChainCacheLock = (SPINLOCK)SPINLOCK_INIT; + f->ChainCacheClock = 0; + memset(f->ChainCache, 0, sizeof(f->ChainCache)); +} + +// Invalidates cached FAT extents after the file's cluster chain changes. +static void Fat32FileInvalidateChainCache(FAT32_FILE* f) { + if (!f) return; + uintptr_t flags = KeAcquireSpinlock(&f->ChainCacheLock); + memset(f->ChainCache, 0, sizeof(f->ChainCache)); + f->ChainCacheClock = 0; + KeReleaseSpinlock(&f->ChainCacheLock, flags); + + FAT32_VOL* v = f->Vol; + if (!v) return; + flags = KeAcquireSpinlock(&v->PathCacheLock); + v->Stats.FatChainCacheInvalidations++; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + +// Locates the best cached starting point for a logical cluster index. +static bool Fat32ChainCacheLookupStart(FAT32_FILE* f, uint32_t logicalIndex, + uint32_t* outLogical, uint32_t* outCluster) { + if (!f || !outLogical || !outCluster) + return false; + + bool hit = false; + uint32_t bestLogical = 0; + uint32_t bestCluster = 0; + + uintptr_t flags = KeAcquireSpinlock(&f->ChainCacheLock); + for (uint32_t i = 0; i < FAT32_CHAIN_CACHE_SLOTS; i++) { + FAT32_CHAIN_CACHE_ENTRY* e = &f->ChainCache[i]; + if (!e->Valid || e->Length == 0) + continue; + + uint32_t end = e->LogicalStart + e->Length; + if (logicalIndex >= e->LogicalStart && logicalIndex < end) { + *outLogical = logicalIndex; + *outCluster = e->PhysicalStart + (logicalIndex - e->LogicalStart); + hit = true; + break; + } + + if (end <= logicalIndex && e->NextCluster >= 2 && e->NextCluster < FAT32_EOC) { + if (!hit || end > bestLogical) { + bestLogical = end; + bestCluster = e->NextCluster; + hit = true; + } + } + } + + if (hit && bestCluster) { + *outLogical = bestLogical; + *outCluster = bestCluster; + } + KeReleaseSpinlock(&f->ChainCacheLock, flags); + + FAT32_VOL* v = f->Vol; + if (v) { + flags = KeAcquireSpinlock(&v->PathCacheLock); + v->Stats.FatChainCacheLookups++; + if (hit) v->Stats.FatChainCacheHits++; + else v->Stats.FatChainCacheMisses++; + KeReleaseSpinlock(&v->PathCacheLock, flags); + } + return hit; +} + +// Stores a physically contiguous FAT extent in the per-open chain cache. +static void Fat32ChainCacheStore(FAT32_FILE* f, uint32_t logicalStart, + uint32_t physicalStart, uint32_t length, + uint32_t nextCluster) { + if (!f || physicalStart < 2 || physicalStart >= FAT32_EOC || length == 0) + return; + + uintptr_t flags = KeAcquireSpinlock(&f->ChainCacheLock); + uint32_t slot = f->ChainCacheClock++ % FAT32_CHAIN_CACHE_SLOTS; + FAT32_CHAIN_CACHE_ENTRY* e = &f->ChainCache[slot]; + e->Valid = true; + e->LogicalStart = logicalStart; + e->PhysicalStart = physicalStart; + e->Length = length; + e->NextCluster = nextCluster; + KeReleaseSpinlock(&f->ChainCacheLock, flags); + + FAT32_VOL* v = f->Vol; + if (!v) return; + flags = KeAcquireSpinlock(&v->PathCacheLock); + v->Stats.FatChainCacheStores++; + KeReleaseSpinlock(&v->PathCacheLock, flags); +} + +// Resolves a logical file cluster through the cached FAT-chain extent map. +static uint32_t Fat32ResolveFileCluster(FAT32_FILE* f, uint32_t logicalIndex) { + if (!f || f->StartCluster < 2 || f->StartCluster >= FAT32_EOC) + return 0; + if (logicalIndex == 0) + return f->StartCluster; + + uint32_t currentLogical = 0; + uint32_t cluster = f->StartCluster; + if (!Fat32ChainCacheLookupStart(f, logicalIndex, ¤tLogical, &cluster)) { + currentLogical = 0; + cluster = f->StartCluster; + } + + uint64_t steps = 0; + while (currentLogical < logicalIndex && cluster >= 2 && cluster < FAT32_EOC) { + cluster = FatEntry(f->Vol, cluster); + currentLogical++; + steps++; + } + Fat32StatsAddChainWalk(f->Vol, steps); + return cluster; +} + +// Builds and caches one physically contiguous read run from a starting cluster. +static bool Fat32BuildReadRun(FAT32_FILE* f, uint32_t logicalIndex, + uint32_t startCluster, uint32_t maxSectors, + uint32_t* outRunSectors, uint32_t* outNextCluster) { + if (!f || !outRunSectors || !outNextCluster) + return false; + FAT32_VOL* v = f->Vol; + if (!v || startCluster < 2 || startCluster >= FAT32_EOC) + return false; + + uint32_t runSectors = v->SectorsPerCluster; + uint32_t runClusters = 1; + uint32_t last = startCluster; + uint32_t next = FatEntry(v, last); + uint64_t steps = 1; + + while (next == last + 1 && next < FAT32_EOC && + runSectors + v->SectorsPerCluster <= maxSectors) { + last = next; + runClusters++; + runSectors += v->SectorsPerCluster; + next = FatEntry(v, last); + steps++; + } + + Fat32StatsAddChainWalk(v, steps); + Fat32ChainCacheStore(f, logicalIndex, startCluster, runClusters, next); + *outRunSectors = runSectors; + *outNextCluster = next; + return true; +} + static int SearchDirV(FAT32_VOL *v, uint32_t dirCluster, SYS_STRING_VIEW name, uint32_t *outCluster, uint32_t *outSize, uint8_t *outAttr, uint64_t *outDirentLba, uint32_t *outDirentOffset) { @@ -986,15 +1386,20 @@ static int SearchDirV(FAT32_VOL *v, uint32_t dirCluster, SYS_STRING_VIEW name, int allowShortMatch = FatNameIsStructurallyShort83V(name); Make83NameV(name, wantName83, wantExt83); uint32_t cluster = dirCluster; + uint64_t clustersRead = 0; + uint64_t entriesScanned = 0; + int found = 0; while (cluster >= 2 && cluster < FAT32_EOC) { uint64_t clusterLba = ClusterToLba(v, cluster); if (!KSUCCESS(ReadSectors(v, clusterLba, v->SectorsPerCluster, clusterBuf))) break; + clustersRead++; uint32_t entries = v->BytesPerCluster / sizeof(DIRENT); DIRENT *dir = (DIRENT *)clusterBuf; for (uint32_t i = 0; i < entries; i++) { + entriesScanned++; if (dir[i].Name[0] == 0x00) goto done; if ((uint8_t)dir[i].Name[0] == 0xE5) { hasLfn = 0; memset(lfn, 0, sizeof(lfn)); continue; } if (dir[i].Attr == ATTR_LFN) { LfnAppend(lfn, (LFN_ENTRY *)&dir[i]); hasLfn = 1; continue; } @@ -1016,15 +1421,16 @@ static int SearchDirV(FAT32_VOL *v, uint32_t dirCluster, SYS_STRING_VIEW name, if (outDirentLba) *outDirentLba = clusterLba + eb / v->BytesPerSector; if (outDirentOffset) *outDirentOffset = (uint32_t)(eb % v->BytesPerSector); } - MmFreePool(clusterBuf); - return 1; + found = 1; + goto done; } } cluster = FatEntry(v, cluster); } done: + Fat32PathCacheAddDirectoryStats(v, clustersRead, entriesScanned); MmFreePool(clusterBuf); - return 0; + return found; } static int Fat32PathNextComponent(SYS_STRING_VIEW path, size_t* cursor, @@ -1058,6 +1464,10 @@ static KSTATUS Fat32LookupPathV(FAT32_VOL *v, SYS_STRING_VIEW path, if (outDirentLba) *outDirentLba = 0; if (outDirentOffset) *outDirentOffset = 0; + if (Fat32PathCacheLookup(v, path, outCluster, outSize, &attr, + outDirentLba, outDirentOffset)) + return STATUS_SUCCESS; + size_t cursor = 0; SYS_STRING_VIEW component; while (Fat32PathNextComponent(path, &cursor, &component)) { @@ -1078,6 +1488,9 @@ static KSTATUS Fat32LookupPathV(FAT32_VOL *v, SYS_STRING_VIEW path, *outCluster = cluster; *outSize = size; + Fat32PathCacheStore(v, path, cluster, size, attr, + outDirentLba ? *outDirentLba : 0, + outDirentOffset ? *outDirentOffset : 0); return STATUS_SUCCESS; } @@ -1220,6 +1633,7 @@ KSTATUS Fat32Open(IO_FILESYSTEM *fs, SYS_STRING_VIEW path, void **fileCtx) { if (!f) return STATUS_NO_MEMORY; f->StartCluster = cluster; f->FileSize = size; f->Vol = v; f->DirentLba = direntLba; f->DirentOffset = direntOffset; + Fat32FileInitChainCache(f); *fileCtx = f; return STATUS_SUCCESS; } @@ -1233,15 +1647,9 @@ void Fat32Close(IO_FILESYSTEM *fs, void *fileCtx) { } // Read (fix #4: contiguous-run coalescing) -// We peek ahead in the FAT chain: if cluster N+1 == cluster N physically, -// extend the current run instead of issuing a new ReadSectors call. -// Run length is capped at MAX_RUN_SECTORS to stay within the AHCI bounce -// buffer (128 sectors = 64 KB). - -// FatEntry() calls in the peek loop are nearly free because they hit the -// FAT sector cache (fix #1). - -#define MAX_RUN_SECTORS 128u +// Cluster-chain lookup is cached per open file, and data spans are allowed to +// grow beyond the AHCI bounce size because the device layer now splits large +// sector requests for the concrete controller. KSTATUS Fat32Read(IO_FILESYSTEM *fs, void *fileCtx, uint64_t offset, void *buf, @@ -1257,14 +1665,12 @@ KSTATUS Fat32Read(IO_FILESYSTEM *fs, void *fileCtx, if (size > (size_t)(f->FileSize - offset)) size = (size_t)(f->FileSize - offset); size_t wanted = size; - // Allocate only as much bounce space as this read can actually use. - // The old code allocated 64 KiB and could read a 64 KiB contiguous run even - // for tiny random reads; Doom does lots of seek/read pairs for small lumps. uint32_t initialOffsetInCluster = (uint32_t)(offset % v->BytesPerCluster); uint64_t initialBytesCovered = (uint64_t)initialOffsetInCluster + (uint64_t)size; uint32_t maxUsefulSectors = (uint32_t)((initialBytesCovered + 511u) / 512u); if (maxUsefulSectors < v->SectorsPerCluster) maxUsefulSectors = v->SectorsPerCluster; - if (maxUsefulSectors > MAX_RUN_SECTORS) maxUsefulSectors = MAX_RUN_SECTORS; + if (maxUsefulSectors > FAT32_READ_RUN_MAX_SECTORS) + maxUsefulSectors = FAT32_READ_RUN_MAX_SECTORS; uint8_t *runBuf = (uint8_t *)MmAllocatePool((size_t)maxUsefulSectors * 512u); if (!runBuf) return STATUS_NO_MEMORY; @@ -1272,35 +1678,27 @@ KSTATUS Fat32Read(IO_FILESYSTEM *fs, void *fileCtx, size_t totalRead = 0; uint8_t *out = (uint8_t *)buf; KSTATUS readStatus = STATUS_SUCCESS; - uint32_t skipClusters = (uint32_t)(offset / v->BytesPerCluster); + uint32_t logicalCluster = (uint32_t)(offset / v->BytesPerCluster); uint32_t offsetInCluster = (uint32_t)(offset % v->BytesPerCluster); - uint32_t cluster = f->StartCluster; - - for (uint32_t i = 0; i < skipClusters && cluster < FAT32_EOC; i++) - cluster = FatEntry(v, cluster); + uint32_t cluster = Fat32ResolveFileCluster(f, logicalCluster); while (size > 0 && cluster >= 2 && cluster < FAT32_EOC) { - // Build the longest physically-contiguous run that can contribute to - // this caller's remaining read. Do not prefetch an unrelated 64 KiB run - // for a tiny random read. uint64_t bytesCovered = (uint64_t)offsetInCluster + (uint64_t)size; uint32_t usefulSectors = (uint32_t)((bytesCovered + 511u) / 512u); if (usefulSectors < v->SectorsPerCluster) usefulSectors = v->SectorsPerCluster; - if (usefulSectors > MAX_RUN_SECTORS) usefulSectors = MAX_RUN_SECTORS; + if (usefulSectors > FAT32_READ_RUN_MAX_SECTORS) + usefulSectors = FAT32_READ_RUN_MAX_SECTORS; if (usefulSectors > maxUsefulSectors) usefulSectors = maxUsefulSectors; - uint32_t runStart = cluster; - uint32_t runSectors = v->SectorsPerCluster; - uint32_t next = FatEntry(v, cluster); - - while (next == cluster + 1 && next < FAT32_EOC - && runSectors + v->SectorsPerCluster <= usefulSectors) { - cluster = next; - next = FatEntry(v, cluster); - runSectors += v->SectorsPerCluster; + uint32_t runSectors = 0; + uint32_t next = 0; + if (!Fat32BuildReadRun(f, logicalCluster, cluster, usefulSectors, + &runSectors, &next)) { + readStatus = STATUS_DEVICE_ERROR; + break; } - KSTATUS rs = ReadSectors(v, ClusterToLba(v, runStart), runSectors, runBuf); + KSTATUS rs = ReadSectors(v, ClusterToLba(v, cluster), runSectors, runBuf); if (!KSUCCESS(rs)) { readStatus = rs; break; @@ -1309,12 +1707,16 @@ KSTATUS Fat32Read(IO_FILESYSTEM *fs, void *fileCtx, uint32_t runBytes = runSectors * 512u; size_t avail = runBytes - offsetInCluster; size_t copy = avail < size ? avail : size; + uint32_t runClusters = runSectors / v->SectorsPerCluster; + uint32_t merges = runClusters ? runClusters - 1u : 0; + Fat32StatsAddReadRun(v, runSectors, merges); memcpy(out, runBuf + offsetInCluster, copy); out += copy; totalRead += copy; size -= copy; offsetInCluster = 0; + logicalCluster += runClusters; cluster = next; } @@ -1345,6 +1747,7 @@ KSTATUS Fat32Write(IO_FILESYSTEM *fs, void *fileCtx, FAT32_FILE *f = (FAT32_FILE *)fileCtx; FAT32_VOL *v = f->Vol; const uint8_t *src = (const uint8_t *)buf; + Fat32FileInvalidateChainCache(f); // Ensure at least one cluster exists. if (f->StartCluster < 2) { @@ -1368,7 +1771,7 @@ KSTATUS Fat32Write(IO_FILESYSTEM *fs, void *fileCtx, cluster = next; } - uint8_t *runBuf = (uint8_t *)MmAllocatePool(MAX_RUN_SECTORS * 512u); + uint8_t *runBuf = (uint8_t *)MmAllocatePool(FAT32_WRITE_RUN_MAX_SECTORS * 512u); if (!runBuf) { *bytesWritten = 0; return STATUS_NO_MEMORY; } size_t written = 0, remaining = size; @@ -1382,7 +1785,7 @@ KSTATUS Fat32Write(IO_FILESYSTEM *fs, void *fileCtx, if (offsetInCluster == 0) { while (next == cluster + 1 && next < FAT32_EOC - && runSectors + v->SectorsPerCluster <= MAX_RUN_SECTORS) { + && runSectors + v->SectorsPerCluster <= FAT32_WRITE_RUN_MAX_SECTORS) { cluster = next; next = FatEntry(v, cluster); runSectors += v->SectorsPerCluster; @@ -1439,7 +1842,9 @@ KSTATUS Fat32Write(IO_FILESYSTEM *fs, void *fileCtx, u.Flags |= FAT32_DIRENT_UPDATE_SIZE; u.Size = f->FileSize; } - DirentUpdateMetadata(v, f->DirentLba, f->DirentOffset, &u); + KSTATUS ms = DirentUpdateMetadata(v, f->DirentLba, f->DirentOffset, &u); + if (KSUCCESS(ms)) + Fat32PathCacheInvalidate(v); } *bytesWritten = written; @@ -1447,8 +1852,6 @@ KSTATUS Fat32Write(IO_FILESYSTEM *fs, void *fileCtx, } // Create. -KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize); - // Creates the requested object. KSTATUS Fat32Create(IO_FILESYSTEM *fs, SYS_STRING_VIEW path, void **fileCtx) { if (!fs || !fileCtx || !SysSvIsValid(path) || path.Length == 0) @@ -1483,7 +1886,7 @@ KSTATUS Fat32Create(IO_FILESYSTEM *fs, SYS_STRING_VIEW path, void **fileCtx) { // Create means overwrite by default for regular files. Reuse the // existing directory entry, drop the old cluster chain, and return // an open zero-length file instead of allocating a duplicate name. - KSTATUS ts = Fat32Truncate(fs, f, 0); + KSTATUS ts = Fat32Truncate(fs, f, 0, 0); if (!KSUCCESS(ts)) { MmFreePool(f); return ts; } *fileCtx = f; return STATUS_SUCCESS; @@ -1558,21 +1961,27 @@ KSTATUS Fat32Create(IO_FILESYSTEM *fs, SYS_STRING_VIEW path, void **fileCtx) { if (!f) return STATUS_NO_MEMORY; f->StartCluster = 0; f->FileSize = 0; f->Vol = v; f->DirentLba = direntLba; f->DirentOffset = direntOffset; + Fat32FileInitChainCache(f); + Fat32PathCacheInvalidate(v); *fileCtx = f; return STATUS_SUCCESS; } // Truncate. -KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize) { +KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize, uint32_t fileFlags) { (void)fs; FAT32_FILE *f = (FAT32_FILE *)fileCtx; FAT32_VOL *v = f->Vol; + Fat32FileInvalidateChainCache(f); + int zeroNewClusters = (fileFlags & IO_FILE_FLAG_SKIP_ZERO_EXTEND) ? 0 : 1; if ((uint32_t)newSize == f->FileSize) { FAT32_DIRENT_UPDATE u; memset(&u, 0, sizeof(u)); FatQueryEncodedTime(&u.Timestamp); u.Flags = FAT32_DIRENT_UPDATE_WRITE_TIME; - return DirentUpdateMetadata(v, f->DirentLba, f->DirentOffset, &u); + KSTATUS s = DirentUpdateMetadata(v, f->DirentLba, f->DirentOffset, &u); + if (KSUCCESS(s)) Fat32PathCacheInvalidate(v); + return s; } if (newSize == 0) { @@ -1584,6 +1993,7 @@ KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize) { f->FileSize = 0; KSTATUS s = DirentUpdateSize(v, f->DirentLba, f->DirentOffset, 0); FlushFatCache(v); + if (KSUCCESS(s)) Fat32PathCacheInvalidate(v); return s; } @@ -1604,7 +2014,7 @@ KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize) { ? (f->FileSize + v->BytesPerCluster - 1) / v->BytesPerCluster : 0; uint32_t lastCluster = 0; if (f->StartCluster < 2) { - uint32_t first = AllocCluster(v); + uint32_t first = AllocClusterEx(v, zeroNewClusters); if (!first) return STATUS_NO_MEMORY; f->StartCluster = first; DirentUpdateFirstCluster(v, f->DirentLba, f->DirentOffset, first); @@ -1613,7 +2023,7 @@ KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize) { lastCluster = GetChainEnd(v, f->StartCluster, NULL); } for (uint32_t i = curClusters; i < newClusters; i++) { - uint32_t next = AppendCluster(v, lastCluster); + uint32_t next = AppendClusterEx(v, lastCluster, zeroNewClusters); if (!next) return STATUS_NO_MEMORY; lastCluster = next; } @@ -1623,6 +2033,7 @@ update_size: f->FileSize = (uint32_t)newSize; { KSTATUS s = DirentUpdateSize(v, f->DirentLba, f->DirentOffset, (uint32_t)newSize); FlushFatCache(v); + if (KSUCCESS(s)) Fat32PathCacheInvalidate(v); return s; } } @@ -1646,6 +2057,7 @@ KSTATUS Fat32Delete(IO_FILESYSTEM *fs, SYS_STRING_VIEW path) { if (cluster >= 2) FreeClusterChain(v, cluster); FlushFatCache(v); + Fat32PathCacheInvalidate(v); return STATUS_SUCCESS; } @@ -1654,6 +2066,75 @@ uint64_t Fat32GetSize(IO_FILESYSTEM *fs, void *fileCtx) { (void)fs; return ((FAT32_FILE *)fileCtx)->FileSize; } +// Mixes FAT directory metadata into a per-file content generation. +static uint64_t Fat32DirentGeneration(const DIRENT *d) { + if (!d) + return 0; + + uint32_t firstCluster = ((uint32_t)d->FirstClusterHi << 16) | d->FirstClusterLo; + uint64_t generation = ((uint64_t)d->WrtDate << 48) | + ((uint64_t)d->WrtTime << 32) | + ((uint64_t)d->LastAccDate << 16) | + (uint64_t)d->Attr; + generation ^= ((uint64_t)d->CrtDate << 40); + generation ^= ((uint64_t)d->CrtTime << 24); + generation ^= ((uint64_t)d->CrtTimeTenth << 16); + generation ^= ((uint64_t)firstCluster << 1); + generation ^= ((uint64_t)d->FileSize << 17); + return generation ? generation : 1; +} + +// Returns a stable FAT file identity for cross-open image-section caches. +KSTATUS Fat32QueryIdentity(IO_FILESYSTEM *fs, void *fileCtx, IO_FILE_IDENTITY *out) { + (void)fs; + if (!fileCtx || !out) + return STATUS_INVALID_PARAMETER; + + FAT32_FILE *f = (FAT32_FILE *)fileCtx; + FAT32_VOL *v = f->Vol; + if (!v) + return STATUS_INVALID_PARAMETER; + + uint32_t startCluster = f->StartCluster; + uint32_t fileSize = f->FileSize; + uint64_t generation = ((uint64_t)startCluster << 1) ^ + ((uint64_t)fileSize << 17); + + if (f->DirentLba != 0) { + uint8_t *sec = (uint8_t *)MmAllocatePool(v->BytesPerSector); + if (!sec) + return STATUS_NO_MEMORY; + + KSTATUS s = ReadSectors(v, f->DirentLba, 1, sec); + if (!KSUCCESS(s)) { + MmFreePool(sec); + return s; + } + + DIRENT *d = (DIRENT *)(sec + f->DirentOffset); + if ((uint8_t)d->Name[0] == 0x00 || (uint8_t)d->Name[0] == 0xE5) { + MmFreePool(sec); + return STATUS_NOT_FOUND; + } + + startCluster = ((uint32_t)d->FirstClusterHi << 16) | d->FirstClusterLo; + fileSize = d->FileSize; + generation = Fat32DirentGeneration(d); + MmFreePool(sec); + } + + out->VolumeId = ((uint64_t)v->DriveIndex << 56) ^ + ((uint64_t)v->PartitionLba << 8) ^ + ((uint64_t)v->RootCluster << 1) ^ + 0x4641543332494445ULL; + out->FileId = f->DirentLba ^ + ((uint64_t)f->DirentOffset << 32) ^ + ((uint64_t)startCluster << 1); + out->Generation = generation ? generation : 1; + out->Size = fileSize; + return STATUS_SUCCESS; +} + // Directory enumeration (fix #2: cluster buffer cached in context) // Previously: allocate + ReadSectors + return 1 entry + free, every call. // Now: buffer lives in FAT32_DIR_CTX; ReadSectors fires only on cluster @@ -1757,8 +2238,7 @@ KSTATUS Fat32ReadDir(IO_FILESYSTEM *fs, void *dirCtx, KDIR_ENTRY *out) { } ctx->CurCluster = FatEntry(v, ctx->CurCluster); - ctx->EntryIndex = 0; ctx->HasLfn = 0; - memset(ctx->LfnBuf, 0, sizeof(ctx->LfnBuf)); + ctx->EntryIndex = 0; } return STATUS_NO_MORE_FILES; @@ -1962,6 +2442,7 @@ typedef struct FAT32_ASYNC_RW { uint64_t Offset; uint32_t OffsetInCluster; uint32_t Cluster; + uint32_t LogicalCluster; size_t Remaining; size_t Done; size_t Requested; @@ -1972,6 +2453,7 @@ typedef struct FAT32_ASYNC_RW { uint32_t CurrentNext; uint32_t CurrentLast; + uint32_t CurrentClusters; uint32_t CurrentOffset; size_t CurrentCopy; @@ -2088,7 +2570,13 @@ static void Fat32AsyncReadIrpComplete(IRP* irp, void* completeCtx) { ctx->Done += ctx->CurrentCopy; ctx->Remaining -= ctx->CurrentCopy; ctx->OffsetInCluster = 0; + ctx->LogicalCluster += ctx->CurrentClusters; ctx->Cluster = ctx->CurrentNext; + if (ctx->CurrentClusters) { + uint32_t sectors = ctx->CurrentClusters * ctx->Vol->SectorsPerCluster; + uint32_t merges = ctx->CurrentClusters - 1u; + Fat32StatsAddReadRun(ctx->Vol, sectors, merges); + } Fat32AsyncQueueContinuation(ctx); } @@ -2109,15 +2597,12 @@ static void Fat32AsyncReadSubmitNext(FAT32_ASYNC_RW* ctx) { FAT32_VOL* v = ctx->Vol; uint32_t runStart = ctx->Cluster; - uint32_t runSectors = v->SectorsPerCluster; - uint32_t last = ctx->Cluster; - uint32_t next = FatEntry(v, last); - - while (next == last + 1 && next < FAT32_EOC && - runSectors + v->SectorsPerCluster <= MAX_RUN_SECTORS) { - last = next; - next = FatEntry(v, last); - runSectors += v->SectorsPerCluster; + uint32_t runSectors = 0; + uint32_t next = 0; + if (!Fat32BuildReadRun(ctx->File, ctx->LogicalCluster, ctx->Cluster, + FAT32_READ_RUN_MAX_SECTORS, &runSectors, &next)) { + Fat32AsyncFinish(ctx, STATUS_DEVICE_ERROR, ctx->Done); + return; } uint32_t runBytes = runSectors * 512u; @@ -2125,7 +2610,8 @@ static void Fat32AsyncReadSubmitNext(FAT32_ASYNC_RW* ctx) { size_t copy = avail < ctx->Remaining ? avail : ctx->Remaining; ctx->CurrentNext = next; - ctx->CurrentLast = last; + ctx->CurrentLast = runStart + (runSectors / v->SectorsPerCluster) - 1u; + ctx->CurrentClusters = runSectors / v->SectorsPerCluster; ctx->CurrentOffset = ctx->OffsetInCluster; ctx->CurrentCopy = copy; @@ -2184,7 +2670,7 @@ KSTATUS Fat32ReadAsync(IO_FILESYSTEM *fs, void *fileCtx, memset(ctx, 0, sizeof(*ctx)); LIST_INIT(&ctx->Link); - ctx->RunBuf = (uint8_t*)MmAllocatePool(MAX_RUN_SECTORS * 512u); + ctx->RunBuf = (uint8_t*)MmAllocatePool(FAT32_READ_RUN_MAX_SECTORS * 512u); if (!ctx->RunBuf) { MmFreePool(ctx); complete(completeContext, STATUS_NO_MEMORY, 0); @@ -2196,17 +2682,14 @@ KSTATUS Fat32ReadAsync(IO_FILESYSTEM *fs, void *fileCtx, ctx->IsWrite = 0; ctx->Offset = offset; ctx->OffsetInCluster = (uint32_t)(offset % v->BytesPerCluster); - ctx->Cluster = f->StartCluster; + ctx->LogicalCluster = (uint32_t)(offset / v->BytesPerCluster); + ctx->Cluster = Fat32ResolveFileCluster(f, ctx->LogicalCluster); ctx->Remaining = size; ctx->Requested = size; ctx->Dst = (uint8_t*)buf; ctx->Complete = complete; ctx->CompleteContext = completeContext; - uint32_t skipClusters = (uint32_t)(offset / v->BytesPerCluster); - for (uint32_t i = 0; i < skipClusters && ctx->Cluster < FAT32_EOC; i++) - ctx->Cluster = FatEntry(v, ctx->Cluster); - Fat32AsyncQueueContinuation(ctx); return STATUS_PENDING; } @@ -2276,7 +2759,7 @@ static void Fat32AsyncWriteSubmitNext(FAT32_ASYNC_RW* ctx) { if (ctx->OffsetInCluster == 0) { while (next == last + 1 && next < FAT32_EOC && - runSectors + v->SectorsPerCluster <= MAX_RUN_SECTORS) { + runSectors + v->SectorsPerCluster <= FAT32_WRITE_RUN_MAX_SECTORS) { last = next; next = FatEntry(v, last); runSectors += v->SectorsPerCluster; @@ -2347,6 +2830,7 @@ KSTATUS Fat32WriteAsync(IO_FILESYSTEM *fs, void *fileCtx, FAT32_FILE* f = (FAT32_FILE*)fileCtx; FAT32_VOL* v = f->Vol; + Fat32FileInvalidateChainCache(f); if (f->StartCluster < 2) { uint32_t first = AllocCluster(v); @@ -2380,7 +2864,7 @@ KSTATUS Fat32WriteAsync(IO_FILESYSTEM *fs, void *fileCtx, memset(ctx, 0, sizeof(*ctx)); LIST_INIT(&ctx->Link); - ctx->RunBuf = (uint8_t*)MmAllocatePool(MAX_RUN_SECTORS * 512u); + ctx->RunBuf = (uint8_t*)MmAllocatePool(FAT32_WRITE_RUN_MAX_SECTORS * 512u); if (!ctx->RunBuf) { MmFreePool(ctx); complete(completeContext, STATUS_NO_MEMORY, 0); diff --git a/code/drivers/fs/fat32/fat32.h b/code/drivers/fs/fat32/fat32.h index ccd039f..702ee48 100644 --- a/code/drivers/fs/fat32/fat32.h +++ b/code/drivers/fs/fat32/fat32.h @@ -16,6 +16,8 @@ #include #include #include +#include +#include typedef struct FAT32_VOL_S FAT32_VOL; @@ -27,10 +29,12 @@ KSTATUS Fat32ReadAsync(IO_FILESYSTEM *fs, void *fileCtx, uint64_t offset, void * KSTATUS Fat32WriteAsync(IO_FILESYSTEM *fs, void *fileCtx, uint64_t offset, const void *buf, size_t size, IO_FS_ASYNC_COMPLETE complete, void *completeContext); KSTATUS Fat32Create(IO_FILESYSTEM *fs, SYS_STRING_VIEW path, void **fileCtx); KSTATUS Fat32Delete(IO_FILESYSTEM *fs, SYS_STRING_VIEW path); -KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize); +KSTATUS Fat32Truncate(IO_FILESYSTEM *fs, void *fileCtx, uint64_t newSize, uint32_t fileFlags); uint64_t Fat32GetSize(IO_FILESYSTEM *fs, void *fileCtx); +KSTATUS Fat32QueryIdentity(IO_FILESYSTEM *fs, void *fileCtx, IO_FILE_IDENTITY *out); KSTATUS Fat32OpenDir(IO_FILESYSTEM *fs, SYS_STRING_VIEW dirPath, void **dirCtx); KSTATUS Fat32ReadDir(IO_FILESYSTEM *fs, void *dirCtx, KDIR_ENTRY *out); void Fat32CloseDir(IO_FILESYSTEM *fs, void *dirCtx); +void Fat32QueryStats(IO_FILESYSTEM *fs, IO_FILESYSTEM_STATS *out); void Fat32InitVtable(IO_FILESYSTEM* fs, FAT32_VOL* vol); diff --git a/code/drivers/fs/fat32/fat32_mount.c b/code/drivers/fs/fat32/fat32_mount.c index cd598a2..ce1137e 100644 --- a/code/drivers/fs/fat32/fat32_mount.c +++ b/code/drivers/fs/fat32/fat32_mount.c @@ -14,9 +14,11 @@ void Fat32InitVtable(IO_FILESYSTEM* fs, FAT32_VOL* vol) { fs->Delete = Fat32Delete; fs->Truncate = Fat32Truncate; fs->GetSize = Fat32GetSize; + fs->QueryIdentity = Fat32QueryIdentity; fs->Close = Fat32Close; fs->DriverCtx = vol; fs->OpenDir = Fat32OpenDir; fs->ReadDir = Fat32ReadDir; fs->CloseDir = Fat32CloseDir; + fs->QueryStats = Fat32QueryStats; } diff --git a/code/drivers/input/serial/serial.c b/code/drivers/input/serial/serial.c new file mode 100644 index 0000000..e051893 --- /dev/null +++ b/code/drivers/input/serial/serial.c @@ -0,0 +1,301 @@ +// 16550-compatible serial input driver. This file routes COM1 receive bytes into the canonical input core. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define SERIAL_COM1_BASE 0x3F8u +#define SERIAL_COM1_IRQ 4u + +#define SERIAL_REG_DATA 0u +#define SERIAL_REG_IER 1u +#define SERIAL_REG_IIR 2u +#define SERIAL_REG_FCR 2u +#define SERIAL_REG_LCR 3u +#define SERIAL_REG_MCR 4u +#define SERIAL_REG_LSR 5u + +#define SERIAL_LSR_DATA_READY 0x01u +#define SERIAL_LSR_TX_EMPTY 0x20u +#define SERIAL_LCR_DLAB 0x80u +#define SERIAL_MCR_DTR 0x01u +#define SERIAL_MCR_RTS 0x02u +#define SERIAL_MCR_OUT2 0x08u + +#define SERIAL_IER_RX_READY 0x01u +#define SERIAL_FCR_ENABLE 0x01u +#define SERIAL_FCR_CLEAR_RX 0x02u +#define SERIAL_FCR_CLEAR_TX 0x04u +#define SERIAL_FCR_TRIGGER_14 0xC0u + +typedef enum SERIAL_ESCAPE_STATE { + SerialEscapeNone = 0, + SerialEscapeEsc, + SerialEscapeCsi, + SerialEscapeCsiNumber, +} SERIAL_ESCAPE_STATE; + +static volatile SERIAL_ESCAPE_STATE gSerialEscapeState; +static volatile uint8_t gSerialCsiNumber; +static volatile bool gSerialDropNextLf; + +// Reads one byte from a COM register. +static uint8_t SerialRead(uint16_t reg) { + return __inbyte((unsigned short)(SERIAL_COM1_BASE + reg)); +} + +// Writes one byte to a COM register. +static void SerialWrite(uint16_t reg, uint8_t value) { + __outbyte((unsigned short)(SERIAL_COM1_BASE + reg), value); +} + +// Submits one input key transition. +static void SerialSubmitTransition(INPUT_KEY key, bool pressed) { + INPUT_KEY_EVENT event; + event.Key = key; + event.Pressed = pressed; + KeInputSubmitKey(&event); +} + +// Submits one logical key press and release. +static void SerialSubmitKey(INPUT_KEY key) { + if (key == INPUT_KEY_NONE) return; + SerialSubmitTransition(key, true); + SerialSubmitTransition(key, false); +} + +// Submits one shifted key press and release. +static void SerialSubmitShiftedKey(INPUT_KEY key) { + if (key == INPUT_KEY_NONE) return; + SerialSubmitTransition(INPUT_KEY_LEFT_SHIFT, true); + SerialSubmitKey(key); + SerialSubmitTransition(INPUT_KEY_LEFT_SHIFT, false); +} + +// Submits one control-modified key press and release. +static void SerialSubmitCtrlKey(INPUT_KEY key) { + if (key == INPUT_KEY_NONE) return; + SerialSubmitTransition(INPUT_KEY_LEFT_CTRL, true); + SerialSubmitKey(key); + SerialSubmitTransition(INPUT_KEY_LEFT_CTRL, false); +} + +// Converts an ASCII printable byte into input events. +static void SerialSubmitPrintable(uint8_t ch) { + if (ch >= 'a' && ch <= 'z') { + SerialSubmitKey((INPUT_KEY)(INPUT_KEY_A + (ch - 'a'))); + return; + } + if (ch >= 'A' && ch <= 'Z') { + SerialSubmitShiftedKey((INPUT_KEY)(INPUT_KEY_A + (ch - 'A'))); + return; + } + if (ch >= '1' && ch <= '9') { + SerialSubmitKey((INPUT_KEY)(INPUT_KEY_1 + (ch - '1'))); + return; + } + if (ch == '0') { + SerialSubmitKey(INPUT_KEY_0); + return; + } + + switch (ch) { + case ' ': SerialSubmitKey(INPUT_KEY_SPACE); return; + case '-': SerialSubmitKey(INPUT_KEY_MINUS); return; + case '=': SerialSubmitKey(INPUT_KEY_EQUAL); return; + case '[': SerialSubmitKey(INPUT_KEY_LEFT_BRACKET); return; + case ']': SerialSubmitKey(INPUT_KEY_RIGHT_BRACKET); return; + case '\\': SerialSubmitKey(INPUT_KEY_BACKSLASH); return; + case ';': SerialSubmitKey(INPUT_KEY_SEMICOLON); return; + case '\'': SerialSubmitKey(INPUT_KEY_APOSTROPHE); return; + case '`': SerialSubmitKey(INPUT_KEY_GRAVE); return; + case ',': SerialSubmitKey(INPUT_KEY_COMMA); return; + case '.': SerialSubmitKey(INPUT_KEY_PERIOD); return; + case '/': SerialSubmitKey(INPUT_KEY_SLASH); return; + + case '!': SerialSubmitShiftedKey(INPUT_KEY_1); return; + case '@': SerialSubmitShiftedKey(INPUT_KEY_2); return; + case '#': SerialSubmitShiftedKey(INPUT_KEY_3); return; + case '$': SerialSubmitShiftedKey(INPUT_KEY_4); return; + case '%': SerialSubmitShiftedKey(INPUT_KEY_5); return; + case '^': SerialSubmitShiftedKey(INPUT_KEY_6); return; + case '&': SerialSubmitShiftedKey(INPUT_KEY_7); return; + case '*': SerialSubmitShiftedKey(INPUT_KEY_8); return; + case '(': SerialSubmitShiftedKey(INPUT_KEY_9); return; + case ')': SerialSubmitShiftedKey(INPUT_KEY_0); return; + case '_': SerialSubmitShiftedKey(INPUT_KEY_MINUS); return; + case '+': SerialSubmitShiftedKey(INPUT_KEY_EQUAL); return; + case '{': SerialSubmitShiftedKey(INPUT_KEY_LEFT_BRACKET); return; + case '}': SerialSubmitShiftedKey(INPUT_KEY_RIGHT_BRACKET); return; + case '|': SerialSubmitShiftedKey(INPUT_KEY_BACKSLASH); return; + case ':': SerialSubmitShiftedKey(INPUT_KEY_SEMICOLON); return; + case '"': SerialSubmitShiftedKey(INPUT_KEY_APOSTROPHE); return; + case '~': SerialSubmitShiftedKey(INPUT_KEY_GRAVE); return; + case '<': SerialSubmitShiftedKey(INPUT_KEY_COMMA); return; + case '>': SerialSubmitShiftedKey(INPUT_KEY_PERIOD); return; + case '?': SerialSubmitShiftedKey(INPUT_KEY_SLASH); return; + default: return; + } +} + +// Submits a completed CSI escape sequence. +static void SerialSubmitCsi(uint8_t finalByte) { + uint8_t number = gSerialCsiNumber; + gSerialEscapeState = SerialEscapeNone; + gSerialCsiNumber = 0; + + switch (finalByte) { + case 'A': SerialSubmitKey(INPUT_KEY_UP); return; + case 'B': SerialSubmitKey(INPUT_KEY_DOWN); return; + case 'C': SerialSubmitKey(INPUT_KEY_RIGHT); return; + case 'D': SerialSubmitKey(INPUT_KEY_LEFT); return; + case 'H': SerialSubmitKey(INPUT_KEY_HOME); return; + case 'F': SerialSubmitKey(INPUT_KEY_END); return; + case '~': + switch (number) { + case 1: SerialSubmitKey(INPUT_KEY_HOME); return; + case 3: SerialSubmitKey(INPUT_KEY_DELETE); return; + case 4: SerialSubmitKey(INPUT_KEY_END); return; + case 5: SerialSubmitKey(INPUT_KEY_PAGE_UP); return; + case 6: SerialSubmitKey(INPUT_KEY_PAGE_DOWN); return; + default: return; + } + default: return; + } +} + +// Handles one byte after an ESC prefix. +static bool SerialHandleEscapeByte(uint8_t ch) { + if (gSerialEscapeState == SerialEscapeNone) return false; + + if (gSerialEscapeState == SerialEscapeEsc) { + if (ch == '[') { + gSerialEscapeState = SerialEscapeCsi; + gSerialCsiNumber = 0; + return true; + } + + gSerialEscapeState = SerialEscapeNone; + gSerialCsiNumber = 0; + SerialSubmitKey(INPUT_KEY_ESCAPE); + return false; + } + + if (gSerialEscapeState == SerialEscapeCsi || gSerialEscapeState == SerialEscapeCsiNumber) { + if (ch >= '0' && ch <= '9') { + gSerialEscapeState = SerialEscapeCsiNumber; + if (gSerialCsiNumber < 25) + gSerialCsiNumber = (uint8_t)(gSerialCsiNumber * 10u + (ch - '0')); + return true; + } + + if (ch == ';') + return true; + + SerialSubmitCsi(ch); + return true; + } + + gSerialEscapeState = SerialEscapeNone; + gSerialCsiNumber = 0; + return false; +} + +// Routes one received serial byte into the input core. +static void SerialReceiveByte(uint8_t ch) { + if (SerialHandleEscapeByte(ch)) return; + + if (ch == 0x1Bu) { + gSerialEscapeState = SerialEscapeEsc; + gSerialCsiNumber = 0; + return; + } + + if (ch == '\r') { + SerialSubmitKey(INPUT_KEY_ENTER); + gSerialDropNextLf = true; + return; + } + + if (ch == '\n') { + if (gSerialDropNextLf) { + gSerialDropNextLf = false; + return; + } + SerialSubmitKey(INPUT_KEY_ENTER); + return; + } + + gSerialDropNextLf = false; + + if (ch == '\t') { + SerialSubmitKey(INPUT_KEY_TAB); + return; + } + + if (ch == '\b' || ch == 0x7Fu) { + SerialSubmitKey(INPUT_KEY_BACKSPACE); + return; + } + + if (ch == 0x03u) { + SerialSubmitCtrlKey(INPUT_KEY_C); + return; + } + + if (ch >= 0x20u && ch <= 0x7Eu) + SerialSubmitPrintable(ch); +} + +// Handles receive interrupts for COM1. +static void SerialIsr(void) { + for (uint32_t i = 0; i < 256u; i++) { + if ((SerialRead(SERIAL_REG_LSR) & SERIAL_LSR_DATA_READY) == 0) + break; + SerialReceiveByte(SerialRead(SERIAL_REG_DATA)); + } +} + +// Initializes COM1 as an 8N1 115200 baud input device. +static void SerialInitializeCom1(void) { + SerialWrite(SERIAL_REG_IER, 0x00u); + SerialWrite(SERIAL_REG_LCR, SERIAL_LCR_DLAB); + SerialWrite(SERIAL_REG_DATA, 0x01u); + SerialWrite(SERIAL_REG_IER, 0x00u); + SerialWrite(SERIAL_REG_LCR, 0x03u); + SerialWrite(SERIAL_REG_FCR, SERIAL_FCR_ENABLE | SERIAL_FCR_CLEAR_RX | + SERIAL_FCR_CLEAR_TX | SERIAL_FCR_TRIGGER_14); + SerialWrite(SERIAL_REG_MCR, SERIAL_MCR_DTR | SERIAL_MCR_RTS | SERIAL_MCR_OUT2); + + while (SerialRead(SERIAL_REG_LSR) & SERIAL_LSR_DATA_READY) + (void)SerialRead(SERIAL_REG_DATA); + + SerialWrite(SERIAL_REG_IER, SERIAL_IER_RX_READY); +} + +// DriverEntry registers COM1 as an input source. +KSTATUS DriverEntry(const HAL_API* hal) { + (void)hal; + + uint32_t deviceId = 0; + KSTATUS st = KeInputRegisterDevice("serial-com1", 0, &deviceId); + if (!KSUCCESS(st)) + return st; + + gSerialEscapeState = SerialEscapeNone; + gSerialCsiNumber = 0; + gSerialDropNextLf = false; + + SerialInitializeCom1(); + KeIrqRegister(SERIAL_COM1_IRQ, SerialIsr); + + DbgPrint("serial: initialized COM1 input device=%u irq=%u\n", deviceId, SERIAL_COM1_IRQ); + return STATUS_SUCCESS; +} diff --git a/code/drivers/net/rtl8139/rtl8139.c b/code/drivers/net/rtl8139/rtl8139.c index d9c07e3..a733277 100644 --- a/code/drivers/net/rtl8139/rtl8139.c +++ b/code/drivers/net/rtl8139/rtl8139.c @@ -88,8 +88,8 @@ static void Rtl8139ReadMac(RTL8139_DEVICE* r) { // Creates dma. static KSTATUS Rtl8139AllocateDma(RTL8139_DEVICE* r) { - r->RxPhys = MmAllocateContiguous(RTL8139_RX_PAGES); - r->TxPhys = MmAllocateContiguous(RTL8139_TX_PAGES); + r->RxPhys = MmPfnAllocateContiguous(RTL8139_RX_PAGES, MmPfnUseDma); + r->TxPhys = MmPfnAllocateContiguous(RTL8139_TX_PAGES, MmPfnUseDma); if (!r->RxPhys || !r->TxPhys) return STATUS_NO_MEMORY; diff --git a/code/drivers/net/rtl8139/rtl8139.h b/code/drivers/net/rtl8139/rtl8139.h index fb0d102..f00b366 100644 --- a/code/drivers/net/rtl8139/rtl8139.h +++ b/code/drivers/net/rtl8139/rtl8139.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include diff --git a/code/drivers/net/rtl816x/rtl816x.c b/code/drivers/net/rtl816x/rtl816x.c index 4ff78a3..cd8d27b 100644 --- a/code/drivers/net/rtl816x/rtl816x.c +++ b/code/drivers/net/rtl816x/rtl816x.c @@ -85,10 +85,10 @@ static void RtlReadMac(RTL816X_DEVICE* r) { // Implements the rtl Allocate Dma operation. static KSTATUS RtlAllocateDma(RTL816X_DEVICE* r) { - r->RxRingPhys = MmAllocateContiguous(RTL_RING_PAGES); - r->TxRingPhys = MmAllocateContiguous(RTL_RING_PAGES); - r->RxBuffersPhys = MmAllocateContiguous(RTL_RXBUF_PAGES); - r->TxBuffersPhys = MmAllocateContiguous(RTL_TXBUF_PAGES); + r->RxRingPhys = MmPfnAllocateContiguous(RTL_RING_PAGES, MmPfnUseDma); + r->TxRingPhys = MmPfnAllocateContiguous(RTL_RING_PAGES, MmPfnUseDma); + r->RxBuffersPhys = MmPfnAllocateContiguous(RTL_RXBUF_PAGES, MmPfnUseDma); + r->TxBuffersPhys = MmPfnAllocateContiguous(RTL_TXBUF_PAGES, MmPfnUseDma); if (!r->RxRingPhys || !r->TxRingPhys || !r->RxBuffersPhys || !r->TxBuffersPhys) return STATUS_NO_MEMORY; diff --git a/code/drivers/net/rtl816x/rtl816x.h b/code/drivers/net/rtl816x/rtl816x.h index e89e60d..d6e19eb 100644 --- a/code/drivers/net/rtl816x/rtl816x.h +++ b/code/drivers/net/rtl816x/rtl816x.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include diff --git a/code/drivers/storage/ahci/ahci.c b/code/drivers/storage/ahci/ahci.c index b4d4b70..b2b6318 100644 --- a/code/drivers/storage/ahci/ahci.c +++ b/code/drivers/storage/ahci/ahci.c @@ -58,20 +58,20 @@ static inline uint64_t cmd_tbl_phys(AHCI_DRIVE *drv, int slot) { static KSTATUS port_init_memory(AHCI_DRIVE *drv) { uint64_t flags = PAGE_PRESENT | PAGE_WRITE | PAGE_NOCACHE; - drv->structPhys = MmAllocateContiguous(PORT_PAGES_STRUCT); + drv->structPhys = MmPfnAllocateContiguous(PORT_PAGES_STRUCT, MmPfnUseDma); if (!drv->structPhys) return STATUS_NO_MEMORY; drv->structVirt = g_hal->PhysToVirt(drv->structPhys); // Force NOCACHE on DMA descriptor memory. - MmSetPageProtect((uintptr_t)drv->structVirt, + MmPteProtectRange((uintptr_t)drv->structVirt, PORT_PAGES_STRUCT * 4096, flags); memset(drv->structVirt, 0, PORT_PAGES_STRUCT * 4096); - drv->bouncePhys = MmAllocateContiguous(BOUNCE_PAGES); + drv->bouncePhys = MmPfnAllocateContiguous(BOUNCE_PAGES, MmPfnUseDma); if (!drv->bouncePhys) return STATUS_NO_MEMORY; drv->bounceVirt = g_hal->PhysToVirt(drv->bouncePhys); // Bounce buffer: NOCACHE to ensure CPU writes reach physical memory // before the HBA's DMA engine reads them. - MmSetPageProtect((uintptr_t)drv->bounceVirt, + MmPteProtectRange((uintptr_t)drv->bounceVirt, BOUNCE_PAGES * 4096, flags); memset(drv->bounceVirt, 0, BOUNCE_BYTES); @@ -203,6 +203,9 @@ static void AhciIsr(void) { // Blocks the current thread until the ISR signals completionEvent. static KSTATUS port_issue_cmd(AHCI_DRIVE *drv) { + if (drv->controller && drv->controller->pollingMode) + return port_issue_cmd_poll(drv); + HBA_PORT *port = drv->port; // Clear stale IS and snapshot before issuing so we don't mis-read a @@ -647,12 +650,25 @@ static KSTATUS AhciStartController(AHCI_CONTROLLER* ctl) { ctl->pciInfo.VendorId, ctl->pciInfo.DeviceId); - st = PciGetInterrupt(ctl->pdo, &ctl->irq); - if (!KSUCCESS(st) || ctl->irq == 0xFF) { - DbgPrint("AHCI: no PCI IRQ wired - cannot use interrupt-driven mode\n"); - return KSUCCESS(st) ? STATUS_NOT_SUPPORTED : st; + ctl->irq = 0xFFu; + ctl->usingMsi = false; + ctl->pollingMode = false; + + st = PciEnableMsiAuto(ctl->pdo, &ctl->irq); + if (KSUCCESS(st)) { + ctl->usingMsi = true; + DbgPrint("AHCI: using MSI IRQ slot %u\n", (unsigned)ctl->irq); + } else { + KSTATUS intxStatus = PciGetInterrupt(ctl->pdo, &ctl->irq); + if (KSUCCESS(intxStatus) && ctl->irq != 0xFFu) { + DbgPrint("AHCI: using legacy INTx IRQ %u\n", (unsigned)ctl->irq); + } else { + ctl->irq = 0xFFu; + ctl->pollingMode = true; + DbgPrint("AHCI: no MSI/INTx interrupt available; using polled command completion (msi=0x%x intx=0x%x)\n", + st, intxStatus); + } } - DbgPrint("AHCI: PCI IRQ line = %u\n", (unsigned)ctl->irq); st = PciGetBar(ctl->pdo, AHCI_PCI_BAR_ABAR, &ctl->abar); if (!KSUCCESS(st)) { @@ -674,10 +690,13 @@ static KSTATUS AhciStartController(AHCI_CONTROLLER* ctl) { return st; } - st = PciEnableDevice(ctl->pdo, - PCI_ENABLE_MEMORY | - PCI_ENABLE_BUSMASTER | - PCI_ENABLE_INTX); + uint32_t enableFlags = PCI_ENABLE_MEMORY | PCI_ENABLE_BUSMASTER; + if (ctl->usingMsi || ctl->pollingMode) + enableFlags |= PCI_DISABLE_INTX; + else + enableFlags |= PCI_ENABLE_INTX; + + st = PciEnableDevice(ctl->pdo, enableFlags); if (!KSUCCESS(st)) { DbgPrint("AHCI: PciEnableDevice failed: 0x%x\n", st); return st; @@ -831,11 +850,16 @@ static KSTATUS AhciStartController(AHCI_CONTROLLER* ctl) { ctl->pciInfo.Address.Device, ctl->pciInfo.Address.Function); - KeIrqRegister(ctl->irq, AhciIsr); + if (!ctl->pollingMode) { + KeIrqRegister(ctl->irq, AhciIsr); + DbgPrint("AHCI: ISR registered on IRQ %u (%s); controller drives=%d\n", + (unsigned)ctl->irq, ctl->usingMsi ? "MSI" : "INTx", ctl->driveCount); + } else { + DbgPrint("AHCI: controller started in polling mode; controller drives=%d\n", + ctl->driveCount); + } ctl->started = true; ctl->fdo->PnpState = DevicePnpStarted; - DbgPrint("AHCI: ISR registered on IRQ %u; controller drives=%d\n", - (unsigned)ctl->irq, ctl->driveCount); return STATUS_SUCCESS; } diff --git a/code/drivers/storage/ahci/ahci.h b/code/drivers/storage/ahci/ahci.h index c6ff2ec..2fa29fc 100644 --- a/code/drivers/storage/ahci/ahci.h +++ b/code/drivers/storage/ahci/ahci.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -367,6 +368,8 @@ struct AHCI_CONTROLLER { HBA_MEM* hba; uint64_t abarLength; uint8_t irq; + bool usingMsi; + bool pollingMode; AHCI_DRIVE drives[MAX_AHCI_DRIVES]; int driveCount; diff --git a/code/drivers/storage/ahci/ahci_port.c b/code/drivers/storage/ahci/ahci_port.c index 56ca872..67c00e2 100644 --- a/code/drivers/storage/ahci/ahci_port.c +++ b/code/drivers/storage/ahci/ahci_port.c @@ -12,15 +12,15 @@ KSTATUS ahci_map_mmio(uint64_t physBase, uint64_t length) { for (uint64_t phys = pageBase; phys < pageEnd; phys += PAGE_SIZE, pageIndex++) { uintptr_t virt = (uintptr_t)g_hal->PhysToVirt(phys); uint64_t flags = PAGE_PRESENT | PAGE_WRITE | PAGE_NOCACHE; - KSTATUS st = MmMapPage(virt, phys, flags, MmMapFailIfPresent); + KSTATUS st = MmPteMapPage(virt, phys, flags, MmPteMapFailIfPresent); // Ignore STATUS_ALREADY_EXISTS - a prior mapping with different flags // is possible for the first page if the bootloader was overly generous. - // Use MmSetPageProtect to force NOCACHE in that case. + // Use MmPteProtectRange to force NOCACHE in that case. if (st == STATUS_ALREADY_EXISTS) { - st = MmSetPageProtect(virt, 4096, flags); + st = MmPteProtectRange(virt, 4096, flags); } if (!KSUCCESS(st)) { - DbgPrint("AHCI: MmMapPage failed for page %d (phys=0x%016llX, st=0x%x)\n", + DbgPrint("AHCI: MmPteMapPage failed for page %d (phys=0x%016llX, st=0x%x)\n", pageIndex, (unsigned long long)phys, st); return st; } diff --git a/code/kernel/include/kernel/io_file.h b/code/kernel/include/kernel/io_file.h index 291803c..2947603 100644 --- a/code/kernel/include/kernel/io_file.h +++ b/code/kernel/include/kernel/io_file.h @@ -25,6 +25,39 @@ typedef void (*IO_FS_ASYNC_COMPLETE)(void* completeContext, KSTATUS status, size_t information); +// Filesystem-level cache and traversal statistics. +typedef struct IO_FILESYSTEM_STATS { + uint64_t PathLookups; + uint64_t PathCacheHits; + uint64_t PathCacheMisses; + uint64_t PathCacheStores; + uint64_t PathCacheInvalidations; + uint64_t PathCacheGeneration; + uint64_t DirectorySearches; + uint64_t DirectoryClustersRead; + uint64_t DirectoryEntriesScanned; + uint64_t FatChainCacheLookups; + uint64_t FatChainCacheHits; + uint64_t FatChainCacheMisses; + uint64_t FatChainCacheStores; + uint64_t FatChainCacheInvalidations; + uint64_t FatChainWalkSteps; + uint64_t DataReadRuns; + uint64_t DataReadSectors; + uint64_t DataReadRunMerges; + uint64_t MaxDataReadSpanSectors; +} IO_FILESYSTEM_STATS; + +// Stable identity for a file on a mounted filesystem. +// Filesystems that can provide this let higher layers cache file-backed +// objects by file identity instead of by one FILE_OBJECT instance. +typedef struct IO_FILE_IDENTITY { + uint64_t VolumeId; + uint64_t FileId; + uint64_t Generation; + uint64_t Size; +} IO_FILE_IDENTITY; + // Filesystem driver vtable - filled in by a filesystem driver. typedef struct IO_FILESYSTEM { // Open a file by absolute path (e.g. "apps\\shell.exe"). @@ -69,14 +102,21 @@ typedef struct IO_FILESYSTEM { // Truncate or extend the file to exactly 'newSize' bytes. KSTATUS (*Truncate)(struct IO_FILESYSTEM* fs, void* fileCtx, - uint64_t newSize); + uint64_t newSize, uint32_t fileFlags); // Return the file size in bytes. uint64_t (*GetSize)(struct IO_FILESYSTEM* fs, void* fileCtx); + // Return a stable file identity for caches that outlive one open handle. + KSTATUS (*QueryIdentity)(struct IO_FILESYSTEM* fs, void* fileCtx, + IO_FILE_IDENTITY* out); + // Release any resources the driver allocated for fileCtx. void (*Close)(struct IO_FILESYSTEM* fs, void* fileCtx); + // Query cache and traversal statistics exposed through kernel stats. + void (*QueryStats)(struct IO_FILESYSTEM* fs, IO_FILESYSTEM_STATS* out); + // Optional driver-private data. void* DriverCtx; @@ -106,9 +146,13 @@ typedef struct IO_STATUS_BLOCK { struct EVENT; struct KAPC; +typedef struct MM_LOCKED_USER_BUFFER MM_LOCKED_USER_BUFFER; typedef struct IO_ASYNC_FILE_REQUEST_OBJECT IO_ASYNC_FILE_REQUEST_OBJECT; // FILE_OBJECT - the kernel object returned to callers. +#define IO_FILE_FLAG_NO_CACHE 0x00000001u +#define IO_FILE_FLAG_SKIP_ZERO_EXTEND 0x00000002u + typedef struct FILE_OBJECT { OBJECT Header; // must be first IO_FILESYSTEM* Fs; @@ -116,6 +160,11 @@ typedef struct FILE_OBJECT { SPINLOCK Lock; // protects cached Size/Offset updates uint64_t Size; uint64_t Offset; // current position for sequential reads + uint32_t Flags; // IO_FILE_FLAG_* + uint32_t ReadaheadScore; + uint64_t ReadaheadNextOffset; + uint64_t ReadaheadIssuedUntil; + uint64_t ReadaheadSuppressedUntil; } FILE_OBJECT; // DIR_OBJECT - kernel object for an open directory iterator. @@ -137,6 +186,7 @@ __declspec(dllexport) KSTATUS IoRegisterFilesystem(uint8_t driveIndex, IO_FILESY __declspec(dllexport) KSTATUS IoUnregisterFilesystem(uint8_t driveIndex); __declspec(dllexport) KSTATUS IoUnregisterFilesystemsForDisk(uint8_t diskIndex); __declspec(dllexport) bool IoFilesystemPresent(uint8_t driveIndex); +__declspec(dllexport) void IoQueryFilesystemStats(IO_FILESYSTEM_STATS* out); // Kernel-side API. __declspec(dllexport) KSTATUS IoOpenFile(SYS_STRING_VIEW path, FILE_OBJECT **out); @@ -149,15 +199,23 @@ __declspec(dllexport) KSTATUS IoWriteFile (FILE_OBJECT* file, const void* buf, __declspec(dllexport) KSTATUS IoWriteFileAt(FILE_OBJECT* file, uint64_t offset, const void* buf, size_t size, size_t* bytesWritten); +__declspec(dllexport) KSTATUS IoFlushFile(FILE_OBJECT* file); __declspec(dllexport) KSTATUS IoReadFileAsyncKernel(FILE_OBJECT* file, void* buf, size_t size, uint64_t offset, IO_STATUS_BLOCK* iosb, struct EVENT* completionEvent, struct KAPC* completionApc, + MM_LOCKED_USER_BUFFER* lockedBuffer, + MM_LOCKED_USER_BUFFER* lockedIosb, IO_ASYNC_FILE_REQUEST_OBJECT** requestOut); __declspec(dllexport) KSTATUS IoWriteFileAsyncKernel(FILE_OBJECT* file, const void* buf, size_t size, uint64_t offset, IO_STATUS_BLOCK* iosb, struct EVENT* completionEvent, struct KAPC* completionApc, + MM_LOCKED_USER_BUFFER* lockedBuffer, + MM_LOCKED_USER_BUFFER* lockedIosb, IO_ASYNC_FILE_REQUEST_OBJECT** requestOut); __declspec(dllexport) OBJECT* IoAsyncFileRequestObject(IO_ASYNC_FILE_REQUEST_OBJECT* request); +__declspec(dllexport) KSTATUS IoWaitAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* request, + uint32_t flags); +__declspec(dllexport) struct EVENT* IoGetAsyncFileRequestWaitEvent(IO_ASYNC_FILE_REQUEST_OBJECT* request); __declspec(dllexport) KSTATUS IoCancelAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* request); __declspec(dllexport) KSTATUS IoQueryAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* request, IO_STATUS_BLOCK* iosbOut); @@ -169,6 +227,7 @@ __declspec(dllexport) KSTATUS IoRemoveDir (SYS_STRING_VIEW path); __declspec(dllexport) KSTATUS IoTruncateFile(FILE_OBJECT* file, uint64_t newSize); __declspec(dllexport) KSTATUS IoSeekFile(FILE_OBJECT* file, uint64_t offset); __declspec(dllexport) uint64_t IoGetFileSize(FILE_OBJECT* file); +__declspec(dllexport) KSTATUS IoQueryFileIdentity(FILE_OBJECT* file, IO_FILE_IDENTITY* out); __declspec(dllexport) void IoCloseFile(FILE_OBJECT* file); // Directory enumeration API. diff --git a/code/kernel/include/kernel/ke_debug.h b/code/kernel/include/kernel/ke_debug.h index a8d5d25..e899776 100644 --- a/code/kernel/include/kernel/ke_debug.h +++ b/code/kernel/include/kernel/ke_debug.h @@ -12,6 +12,16 @@ #define KE_DEBUG_SELFTEST 0x00000002u #define KE_DEBUG_FILE_LOG 0x00000004u +#define KE_DEBUG_POOL_CANARY 0x00000008u +#define KE_DEBUG_POOL_POISON 0x00000010u +#define KE_DEBUG_POOL_QUARANTINE 0x00000020u +#define KE_DEBUG_POOL_CALLERS 0x00000040u +#define KE_DEBUG_DISPLAY_ASSERTS 0x00000080u +#define KE_DEBUG_DISPLAY_TRACE 0x00000100u +#define KE_DEBUG_INPUT_TRACE 0x00000200u +#define KE_DEBUG_VM_ASSERTS 0x00000400u +#define KE_DEBUG_SYSCALL_ASSERTS 0x00000800u + void KeInitDebug(void (*PrintPtr)(char, uint32_t, uint32_t), uint32_t flags); void KeSetDebugConsoleWriter(void (*PrintPtr)(char, uint32_t, uint32_t)); uint32_t KeGetDebugFlags(void); @@ -68,7 +78,7 @@ typedef struct KFAULT_FRAME { uint64_t R15; } KFAULT_FRAME; -__declspec(dllexport) _Noreturn void KeFaultDispatchFrame(const KFAULT_FRAME* frame); +__declspec(dllexport) KSTATUS KeFaultDispatchFrame(const KFAULT_FRAME* frame); // Compatibility wrapper for any older assembly/object still calling this. __declspec(dllexport) _Noreturn void KeFaultDispatch(int vector, uintptr_t rip, diff --git a/code/kernel/include/kernel/ke_display.h b/code/kernel/include/kernel/ke_display.h index e226715..24e871c 100644 --- a/code/kernel/include/kernel/ke_display.h +++ b/code/kernel/include/kernel/ke_display.h @@ -42,7 +42,9 @@ DISPLAY_API void KeDisplaySuspendConsole(bool suspend); DISPLAY_API KSTATUS KeDisplayGetMode(DISPLAY_MODE* outMode); DISPLAY_API KSTATUS KeDisplayGetSize(int* outWidth, int* outHeight); +typedef KSTATUS (*KE_DISPLAY_ROW_SOURCE)(void* context, int rowIndex, uint32_t* scratch, size_t pixelCount); DISPLAY_API KSTATUS KeDisplayBlit(int x, int y, int w, int h, const uint32_t* pixels); +DISPLAY_API KSTATUS KeDisplayBlitRows(int x, int y, int w, int h, KE_DISPLAY_ROW_SOURCE source, void* context, uint32_t* scratch); DISPLAY_API KSTATUS KeDisplayFillRect(int x, int y, int w, int h, uint32_t color); DISPLAY_API KSTATUS KeDisplayClear(uint32_t color); DISPLAY_API KSTATUS KeDisplayCopyRect(int dstX, int dstY, int srcX, int srcY, int w, int h); diff --git a/code/kernel/include/kernel/ke_event.h b/code/kernel/include/kernel/ke_event.h index 0573d36..9577a92 100644 --- a/code/kernel/include/kernel/ke_event.h +++ b/code/kernel/include/kernel/ke_event.h @@ -16,9 +16,12 @@ typedef struct EVENT { EVENT_TYPE Type; uint32_t Signaled; LIST_ENTRY WaitList; + LIST_ENTRY MultiWaitList; } EVENT; __declspec(dllexport) KSTATUS KeEventCreate(EVENT_TYPE type, uint32_t initialState, EVENT **out); __declspec(dllexport) void KeSignalEvent(EVENT* event); __declspec(dllexport) void KeClearEvent(EVENT* event); __declspec(dllexport) KSTATUS KeWaitForEvent(EVENT* event, uint8_t alertable); +__declspec(dllexport) KSTATUS KeTryWaitEvent(EVENT* event); +__declspec(dllexport) KSTATUS KeWaitForMultipleEvents(EVENT** events, uint32_t count, uint8_t alertable, uint64_t timeoutMs, uint32_t* signaledIndexOut); diff --git a/code/kernel/include/kernel/ke_input.h b/code/kernel/include/kernel/ke_input.h index f695390..e8bc586 100644 --- a/code/kernel/include/kernel/ke_input.h +++ b/code/kernel/include/kernel/ke_input.h @@ -6,6 +6,13 @@ #include #include #include +#include +#include + +typedef struct INPUT_QUEUE { + OBJECT Header; + EVENT* WaitEvent; +} INPUT_QUEUE; void KeInputInitialize(const HAL_API* hal); @@ -16,7 +23,12 @@ __declspec(dllexport) void KeInputSubmitMouseAbsolute(const INPUT_MOUSE_ABSOLUTE __declspec(dllexport) void KeInputSubmitMouseAbsoluteScaled(uint32_t rawX, uint32_t rawY, uint32_t maxX, uint32_t maxY, int wheel, uint8_t buttons); INPUT_KEY KeInputKbPoll(void); +KSTATUS KeInputWaitKey(uint8_t alertable); bool KeInputIsKeyDown(INPUT_KEY key); +bool KeInputPeekEvent(INPUT_KEY_RECORD* record); +bool KeInputReadEvent(INPUT_KEY_RECORD* record); +KSTATUS KeInputOpenQueue(INPUT_QUEUE** out); +EVENT* KeInputQueueWaitEvent(INPUT_QUEUE* queue); INPUT_KEY KeInputReadKey(void); char KeInputReadChar(void); uint32_t KeInputGetModifiers(void); diff --git a/code/kernel/include/kernel/ke_mutex.h b/code/kernel/include/kernel/ke_mutex.h index 7db2b60..6a88659 100644 --- a/code/kernel/include/kernel/ke_mutex.h +++ b/code/kernel/include/kernel/ke_mutex.h @@ -14,7 +14,7 @@ typedef struct MUTEX { LIST_ENTRY WaitList; } MUTEX; -KSTATUS KeMutexCreate(MUTEX **out); -KSTATUS KeAcquireMutex(MUTEX* mutex); -KSTATUS KeTryAcquireMutex(MUTEX* mutex); -void KeReleaseMutex(MUTEX* mutex); +__declspec(dllexport) KSTATUS KeMutexCreate(MUTEX **out); +__declspec(dllexport) KSTATUS KeAcquireMutex(MUTEX* mutex); +__declspec(dllexport) KSTATUS KeTryAcquireMutex(MUTEX* mutex); +__declspec(dllexport) void KeReleaseMutex(MUTEX* mutex); diff --git a/code/kernel/include/kernel/ke_process.h b/code/kernel/include/kernel/ke_process.h index 759201d..8097df4 100644 --- a/code/kernel/include/kernel/ke_process.h +++ b/code/kernel/include/kernel/ke_process.h @@ -62,6 +62,7 @@ typedef struct PROCESS { int ExitCode; volatile long Terminating; volatile long Exited; + volatile long ResourcesReleased; // Main executable image. Used for cleanup. uintptr_t ImageBase; @@ -80,8 +81,8 @@ typedef struct PROCESS { // Per-process user virtual regions. The actual VA arena is global because // NT-Lite currently uses one shared address space; this list tracks // ownership so leaked user mappings can be reclaimed on process exit. - LIST_ENTRY UserRegions; - SPINLOCK UserRegionLock; + LIST_ENTRY VadList; + SPINLOCK VadLock; // Per-process loaded modules (user processes only). PROCESS_MODULE Modules[PROCESS_MODULE_MAX]; @@ -102,6 +103,7 @@ KSTATUS KeSetupUserProcessEnvironment(PROCESS* process, SYS_STRING_VIEW currentDirectory); void KeExitProcess(PROCESS* process, int exitCode); KSTATUS KeTerminateProcess(PROCESS* process, int exitCode); +void KeCompleteProcessExitIfLastThread(PROCESS* process); int KeIsProcessTerminating(PROCESS* process); KSTATUS KeGetProcessExitCode(PROCESS* process, int* exitCodeOut); @@ -118,8 +120,12 @@ LIST_ENTRY* KeGetProcessList(void); PROCESS* KeGetCurrentProcess(void); PROCESS* KeGetSystemProcess(void); struct SYS_PROCESS_INFO; +struct SYS_THREAD_INFO; // Copy a snapshot of rich process information into caller-provided storage. size_t KeCopyProcessInfo(struct SYS_PROCESS_INFO* infoOut, size_t maxCount); + +// Copy a snapshot of one process's threads into caller-provided storage. +KSTATUS KeCopyProcessThreadInfo(uint32_t pid, struct SYS_THREAD_INFO* infoOut, size_t maxCount, size_t* countOut); // Find a live process by its ProcessId. Bumps refcount; caller must ObDereferenceObject. PROCESS* KeGetProcessById(uint32_t pid); diff --git a/code/kernel/include/kernel/ke_sched.h b/code/kernel/include/kernel/ke_sched.h index bd27cca..3abb3b3 100644 --- a/code/kernel/include/kernel/ke_sched.h +++ b/code/kernel/include/kernel/ke_sched.h @@ -58,6 +58,10 @@ void KiSchedulerNoteVoluntaryBlock(THREAD* thread); void KiSchedulerMarkThreadWaiting(THREAD* thread); void KiSchedulerWakeThread(THREAD* thread, KSTATUS status); +// Blocks the current thread until at least the requested delay has elapsed. +__declspec(dllexport) KSTATUS KeDelayThread(uint64_t milliseconds); +KSTATUS KeArmCurrentThreadTimeout(uint64_t milliseconds, KSTATUS timeoutStatus); + // Voluntarily yield the CPU. Picks the next runnable thread immediately. __declspec(dllexport) void KeYield(void); diff --git a/code/kernel/include/kernel/ke_system_root.h b/code/kernel/include/kernel/ke_system_root.h new file mode 100644 index 0000000..729fab1 --- /dev/null +++ b/code/kernel/include/kernel/ke_system_root.h @@ -0,0 +1,25 @@ +// System root volume declarations. This file exposes the one selected kernel root volume to loader and process code. + +#pragma once +#include +#include +#include +#include +#include + +#define KE_SYSTEM_ROOT_MAX_DRIVES 8 + +// Selects the mounted volume that contains the kernel/system tree. +KSTATUS KeInitializeSystemRootVolume(void); + +// Returns whether the system root volume has been selected. +bool KeSystemRootReady(void); + +// Returns the selected system root drive, or zero before discovery. +uint8_t KeGetSystemRootDrive(void); + +// Builds an absolute path rooted at the selected system volume. +bool KeBuildSystemRootPath(const char* relative, char* out, size_t outSize); + +// Opens a file rooted at the selected system volume. +KSTATUS KeOpenSystemRootFile(const char* relative, FILE_OBJECT** out); diff --git a/code/kernel/include/kernel/ke_thread.h b/code/kernel/include/kernel/ke_thread.h index 5acde67..b742384 100644 --- a/code/kernel/include/kernel/ke_thread.h +++ b/code/kernel/include/kernel/ke_thread.h @@ -14,6 +14,8 @@ #define KSTACK_SIZE (64 * 1024) // 64 KB kernel stack #define USTACK_SIZE (512 * 1024) // 512 KB user stack +typedef struct THREAD THREAD; + typedef enum THREAD_PRIORITY { PriorityLow = 0, PriorityNormal = 1, @@ -28,6 +30,10 @@ typedef enum THREAD_STATE { ThreadDead = 3, } THREAD_STATE; +typedef void (*KE_WAIT_RUNDOWN_ROUTINE)(struct THREAD* thread, + void* context, + KSTATUS status); + // Saved register context - matches the layout pushed by KeSwitchContext. // The saved RSP points to this structure on the kernel stack. typedef struct KCONTEXT { @@ -36,7 +42,7 @@ typedef struct KCONTEXT { uint64_t Rip; // return address back into KeSwitchContext } KCONTEXT; -typedef struct THREAD { +struct THREAD { OBJECT Header; // must be first uint32_t ThreadId; @@ -82,7 +88,7 @@ typedef struct THREAD { // Signaled (manual-reset) when the thread exits. // Allocated in KeCreateThread, signaled in KeExitThread before teardown. - // Waiters block on this via SYS_WAIT_THREAD -> KeWaitForEvent. + // Waiters block on this through waitable thread handles. EVENT* ExitEvent; void* FpuContext; @@ -113,6 +119,12 @@ typedef struct THREAD { KSTATUS WaitStatus; LIST_ENTRY* WaitListHead; uint8_t AlertableWait; + uint64_t SleepUntilTick; + KSTATUS TimeoutStatus; + + // Optional cleanup hook for waits that use auxiliary wait blocks. + KE_WAIT_RUNDOWN_ROUTINE WaitRundownRoutine; + void* WaitRundownContext; // Kernel APC state. APCs are queued to a target thread and delivered // when that thread reaches PASSIVE_LEVEL with APC delivery enabled. @@ -135,15 +147,22 @@ typedef struct THREAD { uintptr_t UserApcDispatcher; uint8_t UserApcDeliverable; uint8_t UserApcDeliveryActive; -} THREAD; +}; +// Creates and queues a user thread. On success, *out receives a referenced +// THREAD pointer owned by the caller; release it with ObDereferenceObject() +// after copying any needed IDs/state. The live scheduler/thread lifetime is +// separate and remains valid until the thread exits and is reaped. __declspec(dllexport) KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, uintptr_t arg, THREAD_PRIORITY priority, THREAD** out); // Create a kernel-mode-only thread. fn is called directly in ring 0 with // arg; when fn returns the thread exits automatically. No user stack or TEB // is allocated. The thread is owned by the system process and runs at the -// given priority. +// given priority. On success, non-NULL out receives a referenced THREAD +// pointer owned by the caller; release it with ObDereferenceObject() after +// copying any needed IDs/state or after joining/waiting on caller-owned +// completion state. The scheduler's live thread reference is separate. __declspec(dllexport) KSTATUS KeCreateKernelThread(uintptr_t fn, uintptr_t arg, THREAD_PRIORITY priority, THREAD** out); __declspec(dllexport) KSTATUS KeCreateKernelThreadWithAffinity(uintptr_t fn, uintptr_t arg, @@ -167,9 +186,14 @@ __declspec(dllexport) void KeExitThread(THREAD* thread, int code); // waiting. Alertable waits are explicit at every call site. void KePrepareBlockThread(THREAD* thread, OBJECT* waitObject, uint8_t alertable); void KeSetThreadWaitList(THREAD* thread, LIST_ENTRY* waitListHead); +void KeSetThreadWaitRundown(THREAD* thread, KE_WAIT_RUNDOWN_ROUTINE routine, void* context); +void KeClearThreadWaitRundown(THREAD* thread); +void KeRundownThreadWait(THREAD* thread, KSTATUS status); KSTATUS KeCommitBlockThread(THREAD* thread, uint8_t alertable); KSTATUS KeBlockThread(THREAD* thread, OBJECT* waitObject, uint8_t alertable); void KeUnblockThreadWithStatus(THREAD* thread, KSTATUS status); +void KeUnblockThreadFromWaitListWithStatus(THREAD* thread, KSTATUS status); +void KeAbortThreadWait(THREAD* thread, KSTATUS status); void KeUnblockThread(THREAD* thread); KSTATUS KeAlertThread(THREAD* thread); void KiWakeThreadForKernelApc(THREAD* thread); diff --git a/code/kernel/include/kernel/ke_work.h b/code/kernel/include/kernel/ke_work.h new file mode 100644 index 0000000..ef75164 --- /dev/null +++ b/code/kernel/include/kernel/ke_work.h @@ -0,0 +1,24 @@ +// Kernel passive-level work queue declarations. + +#pragma once +#include +#include +#include + +// Work items are caller-owned embedded objects. They may be queued from +// interrupt/DPC/spinlock context and run later on a persistent system worker +// thread at PASSIVE_LEVEL. Queueing the same item while it is already queued +// is coalesced. +typedef void (*KWORK_ROUTINE)(void* context); + +typedef struct KWORK_ITEM { + LIST_ENTRY Link; + KWORK_ROUTINE Routine; + void* Context; + volatile long Queued; +} KWORK_ITEM; + +void KeInitializeWorkItem(KWORK_ITEM* item, KWORK_ROUTINE routine, void* context); +KSTATUS KeStartWorkQueue(void); +bool KeIsWorkQueueStarted(void); +bool KeQueueWorkItem(KWORK_ITEM* item); diff --git a/code/kernel/include/kernel/ldr_image.h b/code/kernel/include/kernel/ldr_image.h index e2b816d..fffcda2 100644 --- a/code/kernel/include/kernel/ldr_image.h +++ b/code/kernel/include/kernel/ldr_image.h @@ -9,11 +9,12 @@ #include struct PROCESS; +struct FILE_OBJECT; -// Map a PE32+ image from 'buffer' into the kernel virtual address space. -// On success, *baseOut receives the virtual base address. -KSTATUS LdrLoadImage(const void* buffer, size_t size, uintptr_t* baseOut, bool user); -KSTATUS LdrLoadImageForProcess(struct PROCESS* process, const void* buffer, size_t size, uintptr_t* baseOut); +// Map a PE32+ image from a bootloader/driver raw buffer into kernel space. +// User images must be loaded from FILE_OBJECTs so image sections have stable identity. +KSTATUS LdrLoadKernelImage(const void* buffer, size_t size, uintptr_t* baseOut); +KSTATUS LdrLoadImageFileForProcess(struct PROCESS* process, struct FILE_OBJECT* file, uintptr_t* baseOut); // Return the SizeOfImage recorded in the PE header for an already-mapped image. size_t LdrGetImageSize(uintptr_t base); diff --git a/code/kernel/include/kernel/ldr_section.h b/code/kernel/include/kernel/ldr_section.h new file mode 100644 index 0000000..a49e202 --- /dev/null +++ b/code/kernel/include/kernel/ldr_section.h @@ -0,0 +1,67 @@ +// Kernel image section declarations shared by loader and memory-management code. + +#pragma once + +#include +#include +#include +#include + +struct PROCESS; +struct FILE_OBJECT; +typedef struct PROCESS PROCESS; +typedef struct FILE_OBJECT FILE_OBJECT; +typedef struct IMAGE_SECTION_OBJECT IMAGE_SECTION_OBJECT; + +// Describes one PE section in image-address-space coordinates. +typedef struct IMAGE_SECTION_DESCRIPTOR { + char Name[9]; + uint32_t VirtualAddress; + uint32_t VirtualSize; + uint32_t RawDataOffset; + uint32_t RawDataSize; + uint32_t Characteristics; + uint64_t PageProtection; +} IMAGE_SECTION_DESCRIPTOR; + +// Keeps the parsed identity and layout for a PE image section object. +typedef struct IMAGE_SECTION_METADATA { + uint64_t ContentKey; + uint64_t VolumeId; + uint64_t FileId; + uint64_t FileGeneration; + uint64_t PreferredBase; + uint32_t EntryRva; + uint32_t HeaderSize; + size_t RawSize; + size_t ImageSize; + size_t PageCount; + uint32_t SectionCount; +} IMAGE_SECTION_METADATA; + +// Creates or references an image section object backed by an open file. +KSTATUS LdrCreateImageSectionFromFile(FILE_OBJECT* file, + IMAGE_SECTION_OBJECT** sectionOut); + +// Adds a reference to an existing image section object. +void LdrReferenceImageSection(IMAGE_SECTION_OBJECT* section); + +// Releases a reference returned by LdrCreateImageSectionFromFile or LdrReferenceImageSection. +void LdrDereferenceImageSection(IMAGE_SECTION_OBJECT* section); + +// Returns immutable metadata for an image section object. +const IMAGE_SECTION_METADATA* LdrImageSectionMetadata(const IMAGE_SECTION_OBJECT* section); + +// Returns the immutable PE section table owned by the image section object. +const IMAGE_SECTION_DESCRIPTOR* LdrImageSectionDescriptors(const IMAGE_SECTION_OBJECT* section, + uint32_t* countOut); + +// Retains one prototype frame owned by an image section object. +KSTATUS LdrRetainImageSectionPage(IMAGE_SECTION_OBJECT* section, + size_t pageIndex, + PHYSICAL_ADDRESS* frameOut); + +// Maps an image section into a process as an image-backed VAD. +KSTATUS LdrMapImageSection(PROCESS* process, + IMAGE_SECTION_OBJECT* section, + void** baseOut); diff --git a/code/kernel/include/kernel/mm_file_cache.h b/code/kernel/include/kernel/mm_file_cache.h new file mode 100644 index 0000000..603bb7c --- /dev/null +++ b/code/kernel/include/kernel/mm_file_cache.h @@ -0,0 +1,99 @@ +// Kernel file-page cache declarations. +// Caches PAGE_SIZE file pages shared by reads, mmap faults, and writeback. + +#pragma once + +#include +#include +#include +#include + +struct FILE_OBJECT; +typedef struct FILE_OBJECT FILE_OBJECT; + +typedef struct MM_FILE_CACHE_PAGE { + FILE_OBJECT* File; + uint64_t Offset; + PHYSICAL_ADDRESS Frame; + uint32_t ValidBytes; + uint8_t Dirty; +} MM_FILE_CACHE_PAGE; + +typedef struct MM_FILE_CACHE_STATS { + uint64_t Lookups; + uint64_t Hits; + uint64_t Misses; + uint64_t Loads; + uint64_t LoadFailures; + uint64_t RaceDiscards; + uint64_t InflightWaits; + uint64_t Invalidations; + uint64_t DirtyPages; + uint64_t MaxDirtyPages; + uint64_t DirtyMarks; + uint64_t WritebackRequests; + uint64_t WritebackPages; + uint64_t WritebackBytes; + uint64_t WritebackFailures; + uint64_t FlushFileCalls; + uint64_t FlushFileFailures; + uint64_t MappedDirtySyncs; + uint64_t MappedDirtyPages; + uint64_t Drops; + uint64_t DropFailures; + uint64_t StandbyTransitions; + uint64_t StandbyHits; + uint64_t ReadAheadRequests; + uint64_t ReadAheadPagesRequested; + uint64_t ReadAheadPagesQueued; + uint64_t ReadAheadPagesLoaded; + uint64_t ReadAheadPagesSkipped; + uint64_t ReadAheadFailures; + uint64_t ReadAheadPagesThrottled; + uint64_t MaxReadAheadPagesPerRequest; + uint64_t BatchReads; + uint64_t BatchPagesRequested; + uint64_t BatchPagesLoaded; + uint64_t MaxBatchPagesRequested; + uint64_t MaxBatchPagesLoaded; + uint64_t ResidentPages; + uint64_t ActivePages; + uint64_t StandbyPages; + uint64_t MaxResidentPages; +} MM_FILE_CACHE_STATS; + +// Returns a retained physical frame containing the file page at page-aligned +// offset. The caller owns one PFN reference and must call +// MmFileCacheReleasePage(). Bytes beyond ValidBytes are zero-filled. +KSTATUS MmFileCacheGetPage(FILE_OBJECT* file, uint64_t offset, MM_FILE_CACHE_PAGE* outPage); +void MmFileCacheReleasePage(MM_FILE_CACHE_PAGE* page); + +// Returns a retained coherent cache page suitable for a shared writable mapping. +KSTATUS MmFileCacheGetWritablePage(FILE_OBJECT* file, uint64_t offset, + uint64_t fileSizeBefore, int needExistingBytes, + MM_FILE_CACHE_PAGE* outPage); + +// Marks one coherent cache page dirty after a shared writable mapping is written. +void MmFileCacheMarkDirty(FILE_OBJECT* file, uint64_t offset, uint32_t validBytes); + +// Writes into coherent cached file pages and marks them dirty. +KSTATUS MmFileCacheWrite(FILE_OBJECT* file, uint64_t offset, const void* buf, + size_t size, uint64_t fileSizeBefore, size_t* writtenOut); + +// Flushes dirty cached pages for one file. +KSTATUS MmFileCacheFlushFile(FILE_OBJECT* file); + +// Drops cached clean pages overlapping the byte range. Dirty pages are flushed +// by callers before destructive metadata operations. +void MmFileCacheInvalidate(FILE_OBJECT* file, uint64_t offset, size_t size); +void MmFileCacheInvalidateAll(FILE_OBJECT* file); + +// Reclaims one cache-owned page if possible. Safe to call under memory pressure; +// it never writes back because v1 stores only clean file data. +KSTATUS MmFileCacheDropOnePageForPressure(void); + +// Queues best-effort read-ahead for clean file pages. The queue owns a file +// reference until the worker has consumed the request. +KSTATUS MmFileCacheScheduleReadAhead(FILE_OBJECT* file, uint64_t offset, uint32_t pages); + +void MmFileCacheQueryStats(MM_FILE_CACHE_STATS* out); diff --git a/code/kernel/include/kernel/mm_pfn.h b/code/kernel/include/kernel/mm_pfn.h new file mode 100644 index 0000000..acbccc1 --- /dev/null +++ b/code/kernel/include/kernel/mm_pfn.h @@ -0,0 +1,134 @@ +// Physical frame database declarations. +// The PFN layer owns frame state, reference counts, pin counts, and allocation +// policy. Higher VMM layers should ask for pages by purpose instead of treating +// the physical allocator as an untyped bitmap. + +#pragma once + +#include +#include +#include +#include + +#define MM_PFN_INVALID_INDEX ((size_t)-1) + +typedef enum MM_PFN_STATE { + MmPfnStateInvalid = 0, + MmPfnStateReserved, + MmPfnStateFree, + MmPfnStateZeroed, + MmPfnStateActive, + MmPfnStateStandby, + MmPfnStateModified, +} MM_PFN_STATE; + +typedef enum MM_PFN_USAGE { + MmPfnUseNone = 0, + MmPfnUseKernel, + MmPfnUsePageTable, + MmPfnUseUser, + MmPfnUseCleanFileCache, + MmPfnUseImage, + MmPfnUseMappedFile, + MmPfnUseAnonymous, + MmPfnUseCow, + MmPfnUseSurface, + MmPfnUseDriver, + MmPfnUseDma, + MmPfnUseBootloader, + MmPfnUsePfnDatabase, + MmPfnUseMmio, +} MM_PFN_USAGE; + +typedef struct MM_PFN { + uint16_t State; + uint16_t Usage; + uint16_t RefCount; + uint16_t PinCount; + uint32_t Flags; + uint32_t MappedCount; + void* Owner; + uint64_t BackingPage; +} MM_PFN; + +typedef struct MM_PFN_STATS { + uint64_t TotalFrames; + uint64_t FreeFrames; + uint64_t UsedFrames; + + uint64_t ReservedFrames; + uint64_t ZeroedFrames; + uint64_t ActiveFrames; + uint64_t StandbyFrames; + uint64_t ModifiedFrames; + uint64_t PinnedFrames; + uint64_t EvictableFrames; + uint64_t NonEvictableFrames; + uint64_t ReclaimableFrames; + uint64_t AvailableFrames; + uint64_t MappedFrames; + + uint64_t KernelFrames; + uint64_t PageTableFrames; + uint64_t UserFrames; + uint64_t CleanFileCacheFrames; + uint64_t ActiveCleanFileCacheFrames; + uint64_t StandbyCleanFileCacheFrames; + uint64_t ImageFrames; + uint64_t ActiveImageFrames; + uint64_t StandbyImageFrames; + uint64_t MappedFileFrames; + uint64_t AnonymousFrames; + uint64_t CowFrames; + uint64_t SurfaceFrames; + uint64_t DriverFrames; + uint64_t DmaFrames; + uint64_t BootloaderFrames; + uint64_t PfnDatabaseFrames; + uint64_t MmioFrames; + + uint64_t AllocationCount; + uint64_t FreeCount; + uint64_t RetainCount; + uint64_t ReleaseCount; + uint64_t PinCount; + uint64_t UnpinCount; + uint64_t FailedAllocations; + uint64_t FailedContiguousAllocations; + + uint64_t FailedInvalidRange; + uint64_t FailedEmptyRange; + uint64_t FailedReserveDenied; + uint64_t FailedNoFrame; + + uint64_t FailedContiguousInvalidRequest; + uint64_t FailedContiguousReserveDenied; + uint64_t FailedContiguousNoRun; + + uint64_t ReclaimAttempts; + uint64_t ReclaimPagesRequested; + uint64_t ReclaimPagesSucceeded; + uint64_t ReclaimPagesFailed; + uint64_t ReclaimReentrantSkips; +} MM_PFN_STATS; + +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocate(MM_PFN_USAGE usage); +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocateBelow(PHYSICAL_ADDRESS maxExclusive, MM_PFN_USAGE usage); +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocateInRange(PHYSICAL_ADDRESS minInclusive, + PHYSICAL_ADDRESS maxExclusive, + MM_PFN_USAGE usage); +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocateContiguous(size_t count, MM_PFN_USAGE usage); +__declspec(dllexport) KSTATUS MmPfnFree(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnRetain(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnRelease(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnPin(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnUnpin(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnAddMapping(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnRemoveMapping(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnMoveToStandby(PHYSICAL_ADDRESS frame); +__declspec(dllexport) KSTATUS MmPfnActivate(PHYSICAL_ADDRESS frame); +__declspec(dllexport) MM_PFN* MmPfnGet(PHYSICAL_ADDRESS frame); +__declspec(dllexport) void* MmPfnToVirtual(PHYSICAL_ADDRESS frame); +size_t MmPfnIndex(PHYSICAL_ADDRESS frame); +PHYSICAL_ADDRESS MmPfnAddress(size_t pfn); +void MmPfnQueryStats(MM_PFN_STATS* out); diff --git a/code/kernel/include/kernel/mm_phys.h b/code/kernel/include/kernel/mm_phys.h index 998a843..cc44ff8 100644 --- a/code/kernel/include/kernel/mm_phys.h +++ b/code/kernel/include/kernel/mm_phys.h @@ -1,8 +1,9 @@ -// Kernel mm phys declarations shared by implementation files and kernel clients. +// Physical-memory declarations shared by the kernel and drivers. +// The physical layer publishes page-size constants, boot memory-map ingestion, +// and aggregate memory statistics. Frame ownership lives in the PFN database. #pragma once -// mm_phys.h - physical memory manager (PMM) -// Bitmap allocator over 4 KB frames, seeded from MEMORY_MAP. + #include #include #include @@ -22,22 +23,7 @@ typedef struct MM_PHYSICAL_STATS { uint64_t FreeRunHistogram[8]; } MM_PHYSICAL_STATS; -// Called once by KernelMain with the MEMORY_MAP from BOOT_INFO. -bool MmInitializePhysical(const MEMORY_MAP* map, - uint64_t physicalWindowBase); - -// Allocate / free a single 4 KB frame. -// MmAllocateFrame returns 0 on failure. -__declspec(dllexport) PHYSICAL_ADDRESS MmAllocateFrame(void); -PHYSICAL_ADDRESS MmAllocateFrameBelow(PHYSICAL_ADDRESS maxExclusive); -PHYSICAL_ADDRESS MmAllocateFrameInRange(PHYSICAL_ADDRESS minInclusive, - PHYSICAL_ADDRESS maxExclusive); -void MmFreeFrame(PHYSICAL_ADDRESS frame); - -// Allocate 'count' physically contiguous 4 KB frames. -// Returns 0 on failure. Use sparingly (DMA, page tables). -__declspec(dllexport) PHYSICAL_ADDRESS MmAllocateContiguous(size_t count); - -size_t MmFreeFrameCount(void); -size_t MmTotalFrameCount(void); -void MmQueryPhysicalStats(MM_PHYSICAL_STATS* out); +bool MmInitializePhysical(const MEMORY_MAP* map, uint64_t physicalWindowBase); +size_t MmFreeFrameCount(void); +size_t MmTotalFrameCount(void); +void MmQueryPhysicalStats(MM_PHYSICAL_STATS* out); diff --git a/code/kernel/include/kernel/mm_pte.h b/code/kernel/include/kernel/mm_pte.h new file mode 100644 index 0000000..f357884 --- /dev/null +++ b/code/kernel/include/kernel/mm_pte.h @@ -0,0 +1,105 @@ +// Kernel page-table manager declarations shared by the VMM and low-level kernel clients. +#pragma once + +#include +#include +#include +#include + +// x86-64 PTE bits exposed to the rest of the kernel. +#define PAGE_PRESENT (1ULL << 0) +#define PAGE_WRITE (1ULL << 1) +#define PAGE_USER (1ULL << 2) +#define PAGE_WRITETHROUGH (1ULL << 3) +#define PAGE_NOCACHE (1ULL << 4) +#define PAGE_ACCESSED (1ULL << 5) +#define PAGE_DIRTY (1ULL << 6) +#define PAGE_NX (1ULL << 63) + +// The physical direct map is a kernel-only alias used to reach page-table frames and early physical memory. +#define PHYS_MAP_BASE 0xFFFF880000000000ULL + + +// Page-table telemetry separates cumulative operations from the current number +// of 4 KiB leaf mappings owned by this layer. Huge boot mappings may exist but +// are not counted as leaves here. +typedef enum MM_PTE_QUERY_PURPOSE { + MmPteQueryGeneric = 0, + MmPteQueryUserProbe, + MmPteQueryHeap, + MmPteQueryVad, + MmPteQueryDebug, + MmPteQuerySelfTest, + MmPteQueryPurposeCount, +} MM_PTE_QUERY_PURPOSE; + +typedef struct MM_PTE_STATS { + uint64_t MapCalls; + uint64_t MapFailures; + uint64_t UnmapCalls; + uint64_t UnmapFailures; + uint64_t ProtectCalls; + uint64_t ProtectFailures; + uint64_t QueryCalls; + uint64_t QueryFailures; + uint64_t QueryByPurpose[MmPteQueryPurposeCount]; + uint64_t QueryFailuresByPurpose[MmPteQueryPurposeCount]; + uint64_t TranslateCalls; + uint64_t LocalFlushes; + uint64_t GlobalFlushes; + uint64_t ShootdownRequests; + uint64_t ShootdownTargetCpus; + uint64_t ShootdownSkippedNoTargets; + uint64_t ShootdownAcks; + uint64_t ShootdownTimeouts; + uint64_t NewMappingNoFlushes; + uint64_t ReplacedMappingFlushes; + uint64_t PageTablePagesAllocated; + uint64_t CurrentLeafMappings; +} MM_PTE_STATS; + +typedef enum MM_PTE_MAP_MODE { + MmPteMapFailIfPresent = 0, + MmPteMapReplaceExisting = 1, +} MM_PTE_MAP_MODE; + +// Initializes the page-table manager around the bootloader-created PML4. +void MmPteInitialize(PHYSICAL_ADDRESS bootPml4Phys); + +// Installs the shared PML4 into CR3. +void MmPteActivate(void); + +// Maps a 4 KiB page into the shared address space. +__declspec(dllexport) KSTATUS MmPteMapPage(uintptr_t virt, PHYSICAL_ADDRESS phys, uint64_t flags, MM_PTE_MAP_MODE mode); + +// Removes a 4 KiB mapping from the shared address space. +__declspec(dllexport) KSTATUS MmPteUnmapPage(uintptr_t virt); + +// Replaces the flags on every mapped 4 KiB page in the range while preserving the physical frame. +__declspec(dllexport) KSTATUS MmPteProtectRange(uintptr_t virt, size_t size, uint64_t flags); + +// Tests and clears the hardware accessed bit on one mapped 4 KiB leaf. +// Returns nonzero when the page had been accessed and was given a second chance. +__declspec(dllexport) int MmPteTestAndClearAccessed(uintptr_t virt); + +// Tests and clears the hardware dirty bit on one mapped 4 KiB leaf. +__declspec(dllexport) int MmPteTestAndClearDirty(uintptr_t virt); + +// Translates a virtual address through the current shared page tables. +__declspec(dllexport) PHYSICAL_ADDRESS MmPteTranslate(uintptr_t virt); + +// Returns the physical address and effective flags for a mapped virtual address. +__declspec(dllexport) KSTATUS MmPteQueryPage(uintptr_t virt, PHYSICAL_ADDRESS* physOut, uint64_t* flagsOut); +KSTATUS MmPteQueryPageFor(MM_PTE_QUERY_PURPOSE purpose, uintptr_t virt, PHYSICAL_ADDRESS* physOut, uint64_t* flagsOut); + +// Flushes one address from the local TLB. +void MmPteFlushLocal(uintptr_t virt); + +// Flushes a kernel mapping from every online CPU that shares the kernel page tables. +void MmPteFlushKernelAddress(uintptr_t virt); + +// Handles the fixed-vector NMI used for kernel TLB shootdowns. +int MmPteHandleShootdownNmi(void); + +// Returns page-table operation counters used by memory telemetry. +void MmPteQueryStats(MM_PTE_STATS* out); diff --git a/code/kernel/include/kernel/mm_swap.h b/code/kernel/include/kernel/mm_swap.h new file mode 100644 index 0000000..0b61c29 --- /dev/null +++ b/code/kernel/include/kernel/mm_swap.h @@ -0,0 +1,31 @@ +// Kernel swap manager declarations. +// Swap owns pagefile slot allocation and page-sized I/O for anonymous VAD pages. + +#pragma once +#include +#include +#include +#include +#include + +typedef struct MM_SWAP_STATS { + uint64_t Enabled; + uint64_t TotalSlots; + uint64_t UsedSlots; + uint64_t FreeSlots; + uint64_t HighWaterSlots; + uint64_t PageOuts; + uint64_t PageIns; + uint64_t PageOutFailures; + uint64_t PageInFailures; + uint64_t SlotAllocations; + uint64_t SlotFrees; + uint64_t SlotAllocationFailures; +} MM_SWAP_STATS; + +KSTATUS MmSwapInitialize(SYS_STRING_VIEW path, uint64_t sizeBytes); +int MmSwapIsEnabled(void); +KSTATUS MmSwapPageOut(PHYSICAL_ADDRESS frame, uint32_t* slotOut); +KSTATUS MmSwapPageIn(uint32_t slot, PHYSICAL_ADDRESS frame); +void MmSwapFreeSlot(uint32_t slot); +void MmSwapQueryStats(MM_SWAP_STATS* out); diff --git a/code/kernel/include/kernel/mm_virt.h b/code/kernel/include/kernel/mm_virt.h index 389d934..3fc229b 100644 --- a/code/kernel/include/kernel/mm_virt.h +++ b/code/kernel/include/kernel/mm_virt.h @@ -2,50 +2,43 @@ #pragma once // mm_virt.h - virtual memory manager (VMM) -// Manages the single shared PML4, kernel pool, and page-granular user VM regions. +// Manages the single shared PML4, kernel pool, and VAD-backed user VM ownership. #include #include #include +#include #include struct PROCESS; typedef struct PROCESS PROCESS; +struct FILE_OBJECT; +typedef struct FILE_OBJECT FILE_OBJECT; +struct IMAGE_SECTION_OBJECT; +typedef struct IMAGE_SECTION_OBJECT IMAGE_SECTION_OBJECT; -typedef enum MM_MAP_MODE { - MmMapFailIfPresent = 0, - MmMapReplaceExisting = 1, -} MM_MAP_MODE; +typedef enum MM_VAD_TYPE { + MmVadGeneric = 0, + MmVadHeap, + MmVadImage, + MmVadStack, + MmVadTeb, + MmVadPeb, + MmVadEnvironment, + MmVadMappedFile, + MmVadShared, +} MM_VAD_TYPE; -typedef enum USER_REGION_TYPE { - UserRegionGeneric = 0, - UserRegionHeap, - UserRegionImage, - UserRegionStack, - UserRegionTeb, - UserRegionPeb, - UserRegionEnvironment, - UserRegionMappedFile, - UserRegionShared, -} USER_REGION_TYPE; - -#define USER_REGION_GUARD_LOW 0x00000001u -#define USER_REGION_GUARD_HIGH 0x00000002u -#define USER_REGION_ZERO 0x00000004u - -// Page flags (x86-64 PTE bits). -#define PAGE_PRESENT (1ULL << 0) -#define PAGE_WRITE (1ULL << 1) -#define PAGE_USER (1ULL << 2) -#define PAGE_WRITETHROUGH (1ULL << 3) -#define PAGE_NOCACHE (1ULL << 4) -#define PAGE_NX (1ULL << 63) +#define MM_VAD_GUARD_LOW 0x00000001u +#define MM_VAD_GUARD_HIGH 0x00000002u +#define MM_VAD_ZERO 0x00000004u +#define MM_VAD_PREFETCH 0x00000008u +#define MM_VAD_SHARED_WRITE 0x00000010u // Virtual address space constants. #define USER_BASE 0x0000000000001000ULL #define USER_LIMIT 0x00007FFFFFFFF000ULL #define KERNEL_BASE 0xFFFF800000000000ULL -#define PHYS_MAP_BASE 0xFFFF880000000000ULL #define POOL_BASE 0xFFFFC00000000000ULL #define POOL_LIMIT 0xFFFFE00000000000ULL @@ -55,37 +48,6 @@ typedef enum USER_REGION_TYPE { // poolSize: initial reservation for kernel pool (bytes, page-aligned). void MmInitializeVirtual(PHYSICAL_ADDRESS bootPml4Phys, size_t poolSize); -// Install the shared PML4 into CR3. Called once at the end of KernelMain init. -void MmActivate(void); - -// Page mapping. -__declspec(dllexport) KSTATUS MmMapPage(uintptr_t virt, PHYSICAL_ADDRESS phys, uint64_t flags, MM_MAP_MODE mode); -__declspec(dllexport) KSTATUS MmUnmapPage(uintptr_t virt); - -// Modify protection flags on an already-mapped virtual range. -// 'flags' is the complete new set of PAGE_* bits - it replaces the old flags -// entirely (except the physical address, which is preserved). -// Returns STATUS_NOT_FOUND if any page in the range is not mapped. -__declspec(dllexport) KSTATUS MmSetPageProtect(uintptr_t virt, size_t size, uint64_t flags); - -// Walk the page tables to translate a virtual address to physical. -// Returns 0 if the address is not mapped. -__declspec(dllexport) PHYSICAL_ADDRESS MmGetPhysicalAddress(uintptr_t virt); - -// TLB/cache coherency hooks for the shared kernel address space. Local flushes -// use INVLPG directly; kernel mapping changes broadcast an NMI shootdown to -// other online CPUs and wait for acknowledgements. -void MmFlushTlbLocal(uintptr_t virt); -void MmFlushTlbForKernelMapping(uintptr_t virt); -int MmHandleTlbShootdownNmi(void); - - -// User/kernel copy boundary helpers -// MmQueryPage returns the physical address and effective PAGE_* bits for a -// mapped virtual address. The reported PAGE_USER/PAGE_WRITE bits account for -// every page-table level, not just the leaf entry. -__declspec(dllexport) KSTATUS MmQueryPage(uintptr_t virt, PHYSICAL_ADDRESS* physOut, uint64_t* flagsOut); - // Probe a user virtual range without copying it. These functions only accept // lower-half user mappings with PAGE_USER set; MmProbeUserWrite additionally // requires PAGE_WRITE. @@ -97,31 +59,96 @@ __declspec(dllexport) KSTATUS MmProbeUserWrite(void* ptr, size_t size); __declspec(dllexport) KSTATUS CopyFromUser(void* kernelDst, const void* userSrc, size_t size); __declspec(dllexport) KSTATUS CopyToUser(void* userDst, const void* kernelSrc, size_t size); +typedef struct MM_PINNED_USER_PAGE { + uintptr_t UserPage; + size_t PageOffset; + PHYSICAL_ADDRESS Frame; + void* KernelAddress; + uint64_t PageFlags; +} MM_PINNED_USER_PAGE; + +typedef struct MM_LOCKED_USER_BUFFER MM_LOCKED_USER_BUFFER; + +// Pins one user page and returns a kernel direct-map alias for the requested +// address. Callers may copy through KernelAddress + PageOffset while the page is +// pinned. The pin is short-lived: never sleep, do file I/O, or hold it across +// arbitrary callbacks. +__declspec(dllexport) KSTATUS MmPinUserPageForRead(PROCESS* process, const void* userAddress, MM_PINNED_USER_PAGE* out); +__declspec(dllexport) KSTATUS MmPinUserPageForWrite(PROCESS* process, void* userAddress, MM_PINNED_USER_PAGE* out); +__declspec(dllexport) void MmUnpinUserPage(MM_PINNED_USER_PAGE* page); + +// Locks a user buffer for asynchronous kernel access. The containing VADs stay +// alive and protected, and resident PFNs stay pinned until the buffer is +// unlocked. WriteAccess describes kernel writes to the user buffer. +__declspec(dllexport) KSTATUS MmLockUserBuffer(PROCESS* process, void* userAddress, size_t size, int writeAccess, MM_LOCKED_USER_BUFFER** out); +__declspec(dllexport) void MmUnlockUserBuffer(MM_LOCKED_USER_BUFFER* buffer); + // Kernel pool - ring 0 only, never PAGE_USER. // Best-fit free-list allocator, thread-safe via spinlock. +#define MM_POOL_TAG(a, b, c, d) \ + ((uint32_t)(uint8_t)(a) | ((uint32_t)(uint8_t)(b) << 8) | \ + ((uint32_t)(uint8_t)(c) << 16) | ((uint32_t)(uint8_t)(d) << 24)) + __declspec(dllexport) void* MmAllocatePool(size_t size); +__declspec(dllexport) void* MmAllocatePoolTag(size_t size, uint32_t tag); __declspec(dllexport) void MmFreePool(void* ptr); __declspec(dllexport) void* MmReallocatePool(void* ptr, size_t newSize); -// User virtual regions. -// The kernel has one shared user VA arena, but every user mapping is owned by -// a PROCESS so process teardown can reclaim leaked memory. Guard pages are -// reserved VA with no present PTE. -KSTATUS MmCreateUserRegion(PROCESS* process, size_t size, size_t alignment, - uint64_t pageFlags, uint32_t regionFlags, - USER_REGION_TYPE type, void** outBase); -KSTATUS MmEnsureUserRegionMapped(PROCESS* process, void* base, size_t size, uint64_t pageFlags); -KSTATUS MmProtectUserRegion(PROCESS* process, void* base, size_t size, uint64_t pageFlags); -KSTATUS MmDestroyUserRegion(PROCESS* process, void* base); -void MmDestroyAllUserRegions(PROCESS* process); -KSTATUS MmQueryUserRegion(PROCESS* process, const void* address, void** baseOut, size_t* sizeOut, uint64_t* pageFlagsOut, USER_REGION_TYPE* typeOut, uint32_t* regionFlagsOut); -void MmQueryProcessUserMemory(PROCESS* process, size_t* regionCountOut, size_t* reservedBytesOut, size_t* committedBytesOut); -size_t MmGetUserRegionSize(PROCESS* process, const void* base); -size_t MmGetUserRegionSizeByBase(const void* base); +// Virtual address descriptors. +// The single shared address space still has per-process VAD ownership so process +// teardown can reclaim leaked mappings. VADs track sparse per-page commitment, +// and committed pages are materialized by user faults or explicit prefaulting. +KSTATUS MmVadReserve(PROCESS* process, size_t size, size_t alignment, + uint64_t pageFlags, uint32_t vadFlags, + MM_VAD_TYPE type, void** outBase); +KSTATUS MmVadCommit(PROCESS* process, void* base, size_t size, + uint64_t pageFlags, uint32_t commitFlags); +KSTATUS MmVadCreateCommitted(PROCESS* process, size_t size, size_t alignment, + uint64_t pageFlags, uint32_t vadFlags, + MM_VAD_TYPE type, void** outBase); +KSTATUS MmVadCreateFileMapping(PROCESS* process, FILE_OBJECT* file, + uint64_t fileOffset, size_t size, + uint64_t pageFlags, uint32_t vadFlags, + void** outBase); +KSTATUS MmVadCreateImageSectionMapping(PROCESS* process, IMAGE_SECTION_OBJECT* section, + uint64_t pageFlags, uint32_t vadFlags, + void** outBase); +KSTATUS MmVadEnsureResident(PROCESS* process, void* base, size_t size, uint64_t accessFlags); +KSTATUS MmVadMakeImageRangePrivate(PROCESS* process, void* base, size_t size); +KSTATUS MmVadSyncFileMappings(FILE_OBJECT* file); +KSTATUS MmVadCreateCowView(PROCESS* sourceProcess, void* sourceBase, size_t size, + PROCESS* targetProcess, void** outBase); +KSTATUS MmVadCreateCowViewFromFrames(PROCESS* targetProcess, + const PHYSICAL_ADDRESS* frames, + size_t pageCount, + size_t payloadSize, + uint64_t pageFlags, + uint32_t vadFlags, + MM_VAD_TYPE type, + void** outBase); +KSTATUS MmVadCreateSharedViewFromFrames(PROCESS* targetProcess, + const PHYSICAL_ADDRESS* frames, + size_t pageCount, + size_t payloadSize, + uint64_t pageFlags, + uint32_t vadFlags, + MM_VAD_TYPE type, + void** outBase); +KSTATUS MmVadResolveUserPageFault(PROCESS* process, uintptr_t faultAddress, uint64_t errorCode); +KSTATUS MmVadResolvePageFaultByAddress(uintptr_t faultAddress, uint64_t errorCode); +KSTATUS MmVadTrimOnePageForTest(PROCESS* process, void** baseOut); +KSTATUS MmVadTrimOnePageForPressure(void); +KSTATUS MmVadProtect(PROCESS* process, void* base, size_t size, uint64_t pageFlags); +KSTATUS MmVadDestroy(PROCESS* process, void* base); +void MmVadDestroyAllForProcess(PROCESS* process); +KSTATUS MmVadQuery(PROCESS* process, const void* address, void** baseOut, size_t* sizeOut, uint64_t* pageFlagsOut, MM_VAD_TYPE* typeOut, uint32_t* vadFlagsOut); +void MmVadQueryProcessMemory(PROCESS* process, size_t* vadCountOut, size_t* reservedBytesOut, size_t* committedBytesOut); +size_t MmVadGetSize(PROCESS* process, const void* base); +size_t MmVadGetSizeByBase(const void* base); // Debug-only ownership/address diagnostics used by fault and launch paths. void MmDebugDescribeAddress(uintptr_t addr); -void MmDebugDumpUserRegions(PROCESS* process); +void MmVadDebugDumpProcess(PROCESS* process); __declspec(dllexport) void* MmAllocateUserMemoryForProcess(PROCESS* process, size_t size); __declspec(dllexport) void MmFreeUserMemoryForProcess(PROCESS* process, void* ptr); @@ -140,3 +167,40 @@ __declspec(dllexport) void MmFreeUserImage(void* ptr, size_t size); size_t MmGetKernelPoolUsage(void); size_t MmGetUserHeapUsage(void); + +// Demand-paging telemetry used by the shell stats command. +typedef struct MM_VAD_STATS { + uint64_t ResolveFaultCalls; + uint64_t ResolvedFaults; + uint64_t FailedFaults; + uint64_t ProtectionFaults; + uint64_t NoVadFaults; + uint64_t UncommittedFaults; + uint64_t NoAccessFaults; + uint64_t DemandZeroPages; + uint64_t ZeroPageMaps; + uint64_t ZeroPageCowSplits; + uint64_t CowFaults; + uint64_t CowPagesCopied; + uint64_t CowViewsCreated; + uint64_t CowPagesShared; + uint64_t EnsureResidentCalls; + uint64_t EnsureResidentPages; + uint64_t AlreadyResidentPages; + uint64_t FileBackedFaults; + uint64_t FileBackedReads; + uint64_t FileBackedReadFailures; + uint64_t FileBackedDiscards; + uint64_t FileBackedSharedWritableFaults; + uint64_t FileBackedDirtyFaults; + uint64_t FileBackedDirtySyncs; + uint64_t ImageBackedFaults; + uint64_t ImageBackedZeroFaults; + uint64_t ImageBackedPrivateFaults; + uint64_t ImageFixupPrivateCalls; + uint64_t ImageFixupPrivatePages; + uint64_t SwapPageOuts; + uint64_t SwapPageIns; +} MM_VAD_STATS; + +void MmVadQueryStats(MM_VAD_STATS* out); diff --git a/code/kernel/include/kernel/ob_object.h b/code/kernel/include/kernel/ob_object.h index 67b8bfc..ab09519 100644 --- a/code/kernel/include/kernel/ob_object.h +++ b/code/kernel/include/kernel/ob_object.h @@ -23,6 +23,10 @@ typedef enum OBJECT_TYPE { ObjectTypeDriver = 12, ObjectTypeUsbDevice = 13, ObjectTypeUsbPipe = 14, + ObjectTypeFileMapping = 15, + ObjectTypeSurface = 16, + ObjectTypeInputQueue = 17, + ObjectTypeImageSection = 18, } OBJECT_TYPE; typedef struct OBJECT { diff --git a/code/kernel/include/kernel/syscall_nr.h b/code/kernel/include/kernel/syscall_nr.h index a05a408..ba67364 100644 --- a/code/kernel/include/kernel/syscall_nr.h +++ b/code/kernel/include/kernel/syscall_nr.h @@ -11,13 +11,13 @@ #define SYS_EXIT_PROCESS 0 #define SYS_EXIT_THREAD 1 #define SYS_CREATE_PROCESS 2 -#define SYS_WAIT_FOR_PROCESS 3 +#define SYS_WAIT_SINGLE 3 #define SYS_GET_PROCESS_EXIT_CODE 4 #define SYS_KILL_PROCESS 5 #define SYS_GET_PROCESS_LIST 6 #define SYS_GET_PROCESS_PID 7 #define SYS_CREATE_THREAD 8 -#define SYS_WAIT_THREAD 9 +#define SYS_RETIRED_WAIT_THREAD 9 #define SYS_GET_CURRENT_TID 10 #define SYS_GET_CURRENT_PID 11 #define SYS_SET_THREAD_PRIORITY 12 @@ -31,17 +31,17 @@ #define SYS_QUERY_VM 20 #define SYS_CLOSE_HANDLE 21 #define SYS_CREATE_MUTEX 22 -#define SYS_ACQUIRE_MUTEX 23 +#define SYS_RETIRED_ACQUIRE_MUTEX 23 #define SYS_TRY_ACQUIRE_MUTEX 24 #define SYS_RELEASE_MUTEX 25 #define SYS_CREATE_SEM 26 -#define SYS_WAIT_SEM 27 +#define SYS_RETIRED_WAIT_SEM 27 #define SYS_TRY_WAIT_SEM 28 #define SYS_SIGNAL_SEM 29 #define SYS_CREATE_EVENT 30 #define SYS_SIGNAL_EVENT 31 #define SYS_CLEAR_EVENT 32 -#define SYS_WAIT_EVENT 33 +#define SYS_RETIRED_WAIT_EVENT 33 #define SYS_CREATE_QUEUE 34 #define SYS_SEND_QUEUE 35 #define SYS_RECV_QUEUE 36 @@ -74,36 +74,47 @@ #define SYS_FILL_CONSOLE_CHARACTER 63 #define SYS_FILL_CONSOLE_ATTRIBUTE 64 #define SYS_PUT_SCREEN_CHAR 65 -#define SYS_BLIT_SCREEN 66 -#define SYS_CLEAR_SCREEN 67 -#define SYS_GET_SCREEN_SIZE 68 -#define SYS_QUERY_DISPLAY_INFO 69 -#define SYS_POLL_KEYBOARD 70 -#define SYS_IS_KEY_DOWN 71 -#define SYS_READ_KEYBOARD_CHAR 72 -#define SYS_READ_KEY 73 -#define SYS_GET_KEY_MODIFIERS 74 -#define SYS_POLL_MOUSE 75 -#define SYS_READ_MOUSE 76 -#define SYS_GET_MOUSE_POS 77 -#define SYS_GET_TICK 78 -#define SYS_PRINT_DEBUG 79 -#define SYS_QUERY_TRAP_FRAME 80 -#define SYS_CREATE_SOCKET 81 -#define SYS_BIND_SOCKET 82 -#define SYS_LISTEN_SOCKET 83 -#define SYS_ACCEPT_SOCKET 84 -#define SYS_CONNECT_SOCKET 85 -#define SYS_SEND_SOCKET 86 -#define SYS_RECV_SOCKET 87 -#define SYS_SHUTDOWN_SOCKET 88 -#define SYS_SEND_TO_SOCKET 89 -#define SYS_RECV_FROM_SOCKET 90 -#define SYS_RESOLVE_HOST 91 -#define SYS_SET_SOCKET_OPTION 92 -#define SYS_GET_SOCKET_OPTION 93 -#define SYS_POLL 94 -#define SYS_GET_MEMORY_STATUS 95 -#define SYS_QUERY_KERNEL_STATS 96 -#define SYS_GET_SYSTEM_TIME 97 -#define SYS_MAX 98 +#define SYS_CLEAR_SCREEN 66 +#define SYS_GET_SCREEN_SIZE 67 +#define SYS_QUERY_DISPLAY_INFO 68 +#define SYS_POLL_KEYBOARD 69 +#define SYS_IS_KEY_DOWN 70 +#define SYS_READ_KEYBOARD_CHAR 71 +#define SYS_READ_KEY 72 +#define SYS_GET_KEY_MODIFIERS 73 +#define SYS_POLL_MOUSE 74 +#define SYS_READ_MOUSE 75 +#define SYS_GET_MOUSE_POS 76 +#define SYS_GET_TICK 77 +#define SYS_PRINT_DEBUG 78 +#define SYS_QUERY_TRAP_FRAME 79 +#define SYS_CREATE_SOCKET 80 +#define SYS_BIND_SOCKET 81 +#define SYS_LISTEN_SOCKET 82 +#define SYS_ACCEPT_SOCKET 83 +#define SYS_CONNECT_SOCKET 84 +#define SYS_SEND_SOCKET 85 +#define SYS_RECV_SOCKET 86 +#define SYS_SHUTDOWN_SOCKET 87 +#define SYS_SEND_TO_SOCKET 88 +#define SYS_RECV_FROM_SOCKET 89 +#define SYS_RESOLVE_HOST 90 +#define SYS_SET_SOCKET_OPTION 91 +#define SYS_GET_SOCKET_OPTION 92 +#define SYS_POLL 93 +#define SYS_GET_MEMORY_STATUS 94 +#define SYS_QUERY_KERNEL_STATS 95 +#define SYS_GET_SYSTEM_TIME 96 +#define SYS_CREATE_FILE_MAPPING 97 +#define SYS_MAP_FILE 98 +#define SYS_CREATE_SURFACE 99 +#define SYS_MAP_SURFACE 100 +#define SYS_QUERY_SURFACE 101 +#define SYS_PRESENT_SURFACE 102 +#define SYS_GET_THREAD_LIST 103 +#define SYS_READ_INPUT_EVENT 104 +#define SYS_PEEK_INPUT_EVENT 105 +#define SYS_WAIT_MANY 106 +#define SYS_OPEN_INPUT_QUEUE 107 +#define SYS_FLUSH_FILE 108 +#define SYS_MAX 109 diff --git a/code/kernel/io_device.c b/code/kernel/io_device.c index c44cf91..89aeb71 100644 --- a/code/kernel/io_device.c +++ b/code/kernel/io_device.c @@ -7,6 +7,7 @@ #include #include #include +#include #define MAX_DRIVER_OBJECTS 64 #define MAX_PDO_OBJECTS 128 @@ -443,6 +444,170 @@ static uint32_t gDiskChangeCallbackCount; static IO_DISK_ARRIVAL_CALLBACK_ENTRY gDiskArrivalCallbacks[MAX_DISK_CALLBACKS]; static uint32_t gDiskArrivalCallbackCount; +#define IO_DISK_READAHEAD_MAX_SECTORS 128u +#define IO_DISK_READAHEAD_TRIGGER_SCORE 2u +#define IO_DISK_READAHEAD_MIN_SECTORS 8u +#define IO_DISK_SECTOR_BYTES 512u + +typedef struct IO_DISK_READAHEAD_STATE { + SPINLOCK Lock; + uint8_t* Buffer; + uint8_t Valid; + uint8_t Loading; + uint64_t Generation; + uint64_t BaseLba; + uint32_t Count; + uint64_t NextLba; + uint32_t SequentialScore; + IO_DISK_STATS Stats; +} IO_DISK_READAHEAD_STATE; + +static IO_DISK_READAHEAD_STATE gDiskReadAhead[MAX_DISK_DEVICES]; + +// Updates a maximum statistic while holding the disk read-ahead lock. +static void IoDiskStatMaxLocked(uint64_t* field, uint64_t value) { + if (field && *field < value) *field = value; +} + +// Prepares the per-disk read-ahead buffer. +static void IoDiskReadAheadPrepare(uint8_t driveIndex) { + if (driveIndex >= MAX_DISK_DEVICES) + return; + + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[driveIndex]; + state->Lock = (SPINLOCK)SPINLOCK_INIT; + state->Valid = 0; + state->Loading = 0; + state->Generation = 0; + state->BaseLba = 0; + state->Count = 0; + state->NextLba = 0; + state->SequentialScore = 0; + memset(&state->Stats, 0, sizeof(state->Stats)); + + if (!state->Buffer) { + state->Buffer = (uint8_t*)MmAllocatePoolTag(IO_DISK_READAHEAD_MAX_SECTORS * IO_DISK_SECTOR_BYTES, + MM_POOL_TAG('d', 'r', 'a', 'h')); + if (!state->Buffer) + state->Stats.ReadaheadAllocFailures++; + } +} + +// Releases the per-disk read-ahead buffer. +static void IoDiskReadAheadRelease(uint8_t driveIndex) { + if (driveIndex >= MAX_DISK_DEVICES) + return; + + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[driveIndex]; + uint8_t* buffer = (uint8_t*)0; + for (;;) { + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + if (!state->Loading) { + buffer = state->Buffer; + state->Buffer = (uint8_t*)0; + state->Valid = 0; + state->Generation++; + state->Count = 0; + state->SequentialScore = 0; + KeReleaseSpinlock(&state->Lock, flags); + break; + } + KeReleaseSpinlock(&state->Lock, flags); + KeYield(); + } + + if (buffer) + MmFreePool(buffer); +} + +// Invalidates cached disk read-ahead data for one disk. +static void IoDiskReadAheadInvalidate(uint8_t driveIndex) { + if (driveIndex >= MAX_DISK_DEVICES) + return; + + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[driveIndex]; + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + state->Valid = 0; + state->Generation++; + state->Count = 0; + state->Stats.ReadaheadInvalidations++; + KeReleaseSpinlock(&state->Lock, flags); +} + +// Returns true when one range fully contains another range. +static bool IoDiskRangeContains(uint64_t outerBase, uint32_t outerCount, + uint64_t innerBase, uint32_t innerCount) { + uint64_t outerEnd = outerBase + outerCount; + uint64_t innerEnd = innerBase + innerCount; + if (outerEnd < outerBase || innerEnd < innerBase) + return false; + return innerBase >= outerBase && innerEnd <= outerEnd; +} + +// Attempts to satisfy a disk read from the per-disk read-ahead buffer. +static bool IoDiskReadAheadTryHit(uint8_t driveIndex, uint64_t lba, uint32_t count, void* buffer) { + if (driveIndex >= MAX_DISK_DEVICES || !buffer || count == 0) + return false; + + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[driveIndex]; + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + bool hit = state->Buffer && state->Valid && !state->Loading && + IoDiskRangeContains(state->BaseLba, state->Count, lba, count); + if (hit) { + size_t offset = (size_t)(lba - state->BaseLba) * IO_DISK_SECTOR_BYTES; + size_t bytes = (size_t)count * IO_DISK_SECTOR_BYTES; + memcpy(buffer, state->Buffer + offset, bytes); + state->Stats.ReadaheadHits++; + state->Stats.ReadaheadBytesServed += bytes; + } else { + state->Stats.ReadaheadMisses++; + } + KeReleaseSpinlock(&state->Lock, flags); + return hit; +} + +// Records request size and sequential access state for one disk read. +static uint32_t IoDiskReadAheadNoteRead(uint8_t driveIndex, uint64_t lba, uint32_t count) { + if (driveIndex >= MAX_DISK_DEVICES || count == 0) + return 0; + + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[driveIndex]; + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + state->Stats.ReadRequests++; + state->Stats.ReadSectors += count; + IoDiskStatMaxLocked(&state->Stats.MaxReadSectors, count); + + if (lba == state->NextLba) { + if (state->SequentialScore < 32) + state->SequentialScore++; + } else { + state->SequentialScore = 1; + } + state->NextLba = lba + count; + uint32_t score = state->SequentialScore; + if (score >= IO_DISK_READAHEAD_TRIGGER_SCORE) + state->Stats.ReadaheadSequentialDetections++; + KeReleaseSpinlock(&state->Lock, flags); + return score; +} + +// Records one disk write and invalidates stale read-ahead data. +static void IoDiskReadAheadNoteWrite(uint8_t driveIndex, uint32_t count) { + if (driveIndex >= MAX_DISK_DEVICES) + return; + + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[driveIndex]; + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + state->Stats.WriteRequests++; + state->Stats.WriteSectors += count; + IoDiskStatMaxLocked(&state->Stats.MaxWriteSectors, count); + state->Valid = 0; + state->Generation++; + state->Count = 0; + state->Stats.ReadaheadInvalidations++; + KeReleaseSpinlock(&state->Lock, flags); +} + // Implements the notify Disk Change operation. static void IoNotifyDiskChange(uint8_t driveIndex, DEVICE_OBJECT* device, IO_DEVICE_CHANGE change) { for (uint32_t i = 0; i < gDiskChangeCallbackCount; i++) { @@ -477,6 +642,8 @@ __declspec(dllexport) KSTATUS IoRegisterDiskDevice(uint8_t driveIndex, if (pnpStatus == STATUS_NOT_SUPPORTED) device->PnpState = DevicePnpStarted; + IoDiskReadAheadPrepare(driveIndex); + IoReferenceDeviceObject(device); gDiskDevices[driveIndex] = device; IoNotifyDiskChange(driveIndex, device, IoDeviceArrived); @@ -490,6 +657,8 @@ __declspec(dllexport) KSTATUS IoNotifyDiskSurpriseRemoved(uint8_t driveIndex) { DEVICE_OBJECT* dev = gDiskDevices[driveIndex]; if (!dev) return STATUS_NOT_FOUND; + IoDiskReadAheadInvalidate(driveIndex); + dev->PnpState = DevicePnpSurpriseRemoved; IoNotifyDiskChange(driveIndex, dev, IoDeviceSurpriseRemoved); (void)IoSendPnpIrpSync(dev, IRP_MN_SURPRISE_REMOVAL); @@ -518,6 +687,7 @@ __declspec(dllexport) KSTATUS IoUnregisterDiskDevice(uint8_t driveIndex) { (void)IoSendPnpIrpSync(dev, IRP_MN_REMOVE_DEVICE); gDiskDevices[driveIndex] = (DEVICE_OBJECT*)0; + IoDiskReadAheadRelease(driveIndex); IoNotifyDiskChange(driveIndex, dev, IoDeviceRemoved); IoDereferenceDeviceObject(dev); return STATUS_SUCCESS; @@ -632,6 +802,112 @@ __declspec(dllexport) KSTATUS IoCallDriver(DEVICE_OBJECT* device, IRP* irp) { return st; } +// Validates that a sector range fits the disk and IRP byte length. +static KSTATUS IoValidateDiskSectorRange(DEVICE_OBJECT* dev, uint64_t lba, uint32_t count) { + if (!dev || count == 0) return STATUS_INVALID_PARAMETER; + if (count > (0xffffffffu / IO_DISK_SECTOR_BYTES)) return STATUS_INVALID_PARAMETER; + if (lba > dev->u.Disk.SectorCount) return STATUS_INVALID_PARAMETER; + if ((uint64_t)count > dev->u.Disk.SectorCount - lba) return STATUS_INVALID_PARAMETER; + return STATUS_SUCCESS; +} + +// Submits one raw disk read IRP. +static KSTATUS IoReadDiskSectorsRaw(DEVICE_OBJECT* dev, uint64_t lba, + uint32_t count, void* buffer) { + if (!dev || !buffer || count == 0) return STATUS_INVALID_PARAMETER; + + IRP irp; + IoBuildReadWriteIrp(&irp, IRP_MJ_READ, lba * 512ull, count * 512u, buffer); + return IoSubmitIrpSync(dev, &irp); +} + +// Submits one raw disk write IRP. +static KSTATUS IoWriteDiskSectorsRaw(DEVICE_OBJECT* dev, uint64_t lba, + uint32_t count, void* buffer) { + if (!dev || !buffer || count == 0) return STATUS_INVALID_PARAMETER; + + IRP irp; + IoBuildReadWriteIrp(&irp, IRP_MJ_WRITE, lba * 512ull, count * 512u, buffer); + return IoSubmitIrpSync(dev, &irp); +} + +// Chooses a conservative read-ahead span from the current sequential score. +static uint32_t IoDiskChooseReadAheadSectors(uint32_t currentCount, uint32_t score) { + uint32_t sectors = currentCount; + if (sectors < IO_DISK_READAHEAD_MIN_SECTORS) + sectors = IO_DISK_READAHEAD_MIN_SECTORS; + if (score >= 8 && sectors < 128u) + sectors = 128u; + else if (score >= 4 && sectors < 64u) + sectors = 64u; + else if (score >= 2 && sectors < 32u) + sectors = 32u; + if (sectors > IO_DISK_READAHEAD_MAX_SECTORS) + sectors = IO_DISK_READAHEAD_MAX_SECTORS; + return sectors; +} + +// Loads the next sequential disk range into the read-ahead buffer. +static void IoDiskMaybeReadAhead(uint8_t driveIndex, DEVICE_OBJECT* dev, + uint64_t nextLba, uint32_t currentCount, uint32_t score) { + if (driveIndex >= MAX_DISK_DEVICES || !dev || score < IO_DISK_READAHEAD_TRIGGER_SCORE) + return; + + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[driveIndex]; + if (!state->Buffer) { + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + state->Stats.ReadaheadSuppressed++; + KeReleaseSpinlock(&state->Lock, flags); + return; + } + + uint32_t sectors = IoDiskChooseReadAheadSectors(currentCount, score); + uint64_t diskSectors = dev->u.Disk.SectorCount; + if (nextLba >= diskSectors) + return; + if ((uint64_t)sectors > diskSectors - nextLba) + sectors = (uint32_t)(diskSectors - nextLba); + if (sectors == 0) + return; + + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + bool covered = state->Valid && !state->Loading && + IoDiskRangeContains(state->BaseLba, state->Count, nextLba, sectors); + if (covered) { + KeReleaseSpinlock(&state->Lock, flags); + return; + } + if (state->Loading) { + state->Stats.ReadaheadSuppressed++; + KeReleaseSpinlock(&state->Lock, flags); + return; + } + uint64_t generation = state->Generation; + state->Loading = 1; + state->Valid = 0; + state->Count = 0; + KeReleaseSpinlock(&state->Lock, flags); + + KSTATUS st = IoReadDiskSectorsRaw(dev, nextLba, sectors, state->Buffer); + + flags = KeAcquireSpinlock(&state->Lock); + state->Loading = 0; + if (KSUCCESS(st) && generation == state->Generation) { + state->BaseLba = nextLba; + state->Count = sectors; + state->Valid = 1; + state->Stats.ReadaheadStores++; + state->Stats.ReadaheadSectorsLoaded += sectors; + IoDiskStatMaxLocked(&state->Stats.MaxReadaheadSectors, sectors); + } else { + state->Valid = 0; + state->Count = 0; + if (!KSUCCESS(st)) + state->Stats.ReadaheadReadFailures++; + } + KeReleaseSpinlock(&state->Lock, flags); +} + __declspec(dllexport) KSTATUS IoReadDiskSectors(uint8_t driveIndex, uint64_t lba, uint32_t count, @@ -641,9 +917,23 @@ __declspec(dllexport) KSTATUS IoReadDiskSectors(uint8_t driveIndex, DEVICE_OBJECT* dev = IoReferenceDiskDevice(driveIndex); if (!dev) return STATUS_NOT_FOUND; - IRP irp; - IoBuildReadWriteIrp(&irp, IRP_MJ_READ, lba * 512ull, count * 512u, buffer); - KSTATUS st = IoSubmitIrpSync(dev, &irp); + KSTATUS rangeStatus = IoValidateDiskSectorRange(dev, lba, count); + if (!KSUCCESS(rangeStatus)) { + IoDereferenceDeviceObject(dev); + return rangeStatus; + } + + if (IoDiskReadAheadTryHit(driveIndex, lba, count, buffer)) { + uint32_t score = IoDiskReadAheadNoteRead(driveIndex, lba, count); + IoDiskMaybeReadAhead(driveIndex, dev, lba + count, count, score); + IoDereferenceDeviceObject(dev); + return STATUS_SUCCESS; + } + + KSTATUS st = IoReadDiskSectorsRaw(dev, lba, count, buffer); + uint32_t score = IoDiskReadAheadNoteRead(driveIndex, lba, count); + if (KSUCCESS(st)) + IoDiskMaybeReadAhead(driveIndex, dev, lba + count, count, score); IoDereferenceDeviceObject(dev); return st; @@ -658,10 +948,50 @@ __declspec(dllexport) KSTATUS IoWriteDiskSectors(uint8_t driveIndex, DEVICE_OBJECT* dev = IoReferenceDiskDevice(driveIndex); if (!dev) return STATUS_NOT_FOUND; - IRP irp; - IoBuildReadWriteIrp(&irp, IRP_MJ_WRITE, lba * 512ull, count * 512u, (void*)buffer); - KSTATUS st = IoSubmitIrpSync(dev, &irp); + KSTATUS rangeStatus = IoValidateDiskSectorRange(dev, lba, count); + if (!KSUCCESS(rangeStatus)) { + IoDereferenceDeviceObject(dev); + return rangeStatus; + } + + KSTATUS st = IoWriteDiskSectorsRaw(dev, lba, count, (void*)buffer); + if (KSUCCESS(st)) + IoDiskReadAheadNoteWrite(driveIndex, count); IoDereferenceDeviceObject(dev); return st; } + +// Queries aggregate disk I/O statistics. +__declspec(dllexport) void IoQueryDiskStats(IO_DISK_STATS* out) { + if (!out) return; + memset(out, 0, sizeof(*out)); + + for (uint8_t i = 0; i < MAX_DISK_DEVICES; i++) { + IO_DISK_READAHEAD_STATE* state = &gDiskReadAhead[i]; + uintptr_t flags = KeAcquireSpinlock(&state->Lock); + if (gDiskDevices[i]) + out->RegisteredDisks++; + out->ReadRequests += state->Stats.ReadRequests; + out->ReadSectors += state->Stats.ReadSectors; + out->WriteRequests += state->Stats.WriteRequests; + out->WriteSectors += state->Stats.WriteSectors; + if (out->MaxReadSectors < state->Stats.MaxReadSectors) + out->MaxReadSectors = state->Stats.MaxReadSectors; + if (out->MaxWriteSectors < state->Stats.MaxWriteSectors) + out->MaxWriteSectors = state->Stats.MaxWriteSectors; + out->ReadaheadHits += state->Stats.ReadaheadHits; + out->ReadaheadMisses += state->Stats.ReadaheadMisses; + out->ReadaheadStores += state->Stats.ReadaheadStores; + out->ReadaheadInvalidations += state->Stats.ReadaheadInvalidations; + out->ReadaheadBytesServed += state->Stats.ReadaheadBytesServed; + out->ReadaheadSectorsLoaded += state->Stats.ReadaheadSectorsLoaded; + out->ReadaheadSequentialDetections += state->Stats.ReadaheadSequentialDetections; + out->ReadaheadSuppressed += state->Stats.ReadaheadSuppressed; + out->ReadaheadAllocFailures += state->Stats.ReadaheadAllocFailures; + out->ReadaheadReadFailures += state->Stats.ReadaheadReadFailures; + if (out->MaxReadaheadSectors < state->Stats.MaxReadaheadSectors) + out->MaxReadaheadSectors = state->Stats.MaxReadaheadSectors; + KeReleaseSpinlock(&state->Lock, flags); + } +} diff --git a/code/kernel/io_file.c b/code/kernel/io_file.c index a608942..e53c878 100644 --- a/code/kernel/io_file.c +++ b/code/kernel/io_file.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -13,6 +15,7 @@ #include #include #include +#include #define MAX_DRIVES 8 @@ -45,11 +48,15 @@ struct IO_ASYNC_FILE_REQUEST_OBJECT { uint8_t CancelRequested; FILE_OBJECT* File; // referenced until object destruction + EVENT* WaitEvent; // internal completion event EVENT* CompletionEvent; // optional, referenced until object destruction KAPC* CompletionApc; // caller-owned APC storage IO_STATUS_BLOCK* Iosb; // caller/user-owned completion block + IO_STATUS_BLOCK IosbSnapshot; + MM_LOCKED_USER_BUFFER* LockedBuffer; + MM_LOCKED_USER_BUFFER* LockedIosb; - void* Buffer; // caller/user-owned; must remain valid until completion/cancel + void* Buffer; // caller-owned; user buffers must be locked until completion/cancel size_t Size; uint64_t Offset; }; @@ -65,6 +72,10 @@ static void KiIoAsyncFileRequestDestroy(OBJECT* obj) { IO_ASYNC_FILE_REQUEST_OBJECT* req = CONTAINING_RECORD(obj, IO_ASYNC_FILE_REQUEST_OBJECT, Header); + if (req->WaitEvent) { + ObDereferenceObject(&req->WaitEvent->Header); + req->WaitEvent = (EVENT*)0; + } if (req->CompletionEvent) { ObDereferenceObject(&req->CompletionEvent->Header); req->CompletionEvent = (EVENT*)0; @@ -73,6 +84,14 @@ static void KiIoAsyncFileRequestDestroy(OBJECT* obj) { ObDereferenceObject(&req->File->Header); req->File = (FILE_OBJECT*)0; } + if (req->LockedBuffer) { + MmUnlockUserBuffer(req->LockedBuffer); + req->LockedBuffer = (MM_LOCKED_USER_BUFFER*)0; + } + if (req->LockedIosb) { + MmUnlockUserBuffer(req->LockedIosb); + req->LockedIosb = (MM_LOCKED_USER_BUFFER*)0; + } } static void KiIoCompleteAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req, @@ -80,6 +99,9 @@ static void KiIoCompleteAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req, size_t information) { if (!req) return; + req->IosbSnapshot.Status = status; + req->IosbSnapshot.Information = information; + uintptr_t flags = KeAcquireSpinlock(&gAsyncFileQueueLock); req->State = (status == STATUS_CANCELLED) ? IoAsyncStateCancelled : IoAsyncStateCompleted; KeReleaseSpinlock(&gAsyncFileQueueLock, flags); @@ -99,6 +121,18 @@ static void KiIoCompleteAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req, if (req->CompletionEvent) KeSignalEvent(req->CompletionEvent); + + if (req->WaitEvent) + KeSignalEvent(req->WaitEvent); + + if (req->LockedBuffer) { + MmUnlockUserBuffer(req->LockedBuffer); + req->LockedBuffer = (MM_LOCKED_USER_BUFFER*)0; + } + if (req->LockedIosb) { + MmUnlockUserBuffer(req->LockedIosb); + req->LockedIosb = (MM_LOCKED_USER_BUFFER*)0; + } } // Implements the io Async File Dequeue operation. @@ -226,10 +260,24 @@ static KSTATUS KiIoSubmitAsyncFileRequest(IO_ASYNC_FILE_OP op, IO_STATUS_BLOCK* iosb, EVENT* completionEvent, KAPC* completionApc, + MM_LOCKED_USER_BUFFER* lockedBuffer, + MM_LOCKED_USER_BUFFER* lockedIosb, IO_ASYNC_FILE_REQUEST_OBJECT** requestOut) { - if (!file || !iosb) return STATUS_INVALID_PARAMETER; - if (!buffer && size) return STATUS_INVALID_PARAMETER; - if (!KeCanWait()) return STATUS_INVALID_IRQL; + if (!file || !iosb) { + MmUnlockUserBuffer(lockedBuffer); + MmUnlockUserBuffer(lockedIosb); + return STATUS_INVALID_PARAMETER; + } + if (!buffer && size) { + MmUnlockUserBuffer(lockedBuffer); + MmUnlockUserBuffer(lockedIosb); + return STATUS_INVALID_PARAMETER; + } + if (!KeCanWait()) { + MmUnlockUserBuffer(lockedBuffer); + MmUnlockUserBuffer(lockedIosb); + return STATUS_INVALID_IRQL; + } iosb->Status = STATUS_PENDING; iosb->Information = 0; @@ -239,6 +287,8 @@ static KSTATUS KiIoSubmitAsyncFileRequest(IO_ASYNC_FILE_OP op, sizeof(IO_ASYNC_FILE_REQUEST_OBJECT)); if (!req) { iosb->Status = STATUS_NO_MEMORY; + MmUnlockUserBuffer(lockedBuffer); + MmUnlockUserBuffer(lockedIosb); return STATUS_NO_MEMORY; } @@ -247,10 +297,23 @@ static KSTATUS KiIoSubmitAsyncFileRequest(IO_ASYNC_FILE_OP op, req->Op = op; req->State = IoAsyncStateQueued; req->CancelRequested = 0; + KSTATUS waitStatus = KeEventCreate(EventManualReset, 0, &req->WaitEvent); + if (!KSUCCESS(waitStatus)) { + iosb->Status = waitStatus; + MmUnlockUserBuffer(lockedBuffer); + MmUnlockUserBuffer(lockedIosb); + ObDereferenceObject(&req->Header); + return waitStatus; + } + req->File = file; req->CompletionEvent = completionEvent; req->CompletionApc = completionApc; req->Iosb = iosb; + req->IosbSnapshot.Status = STATUS_PENDING; + req->IosbSnapshot.Information = 0; + req->LockedBuffer = lockedBuffer; + req->LockedIosb = lockedIosb; req->Buffer = buffer; req->Size = size; req->Offset = offset; @@ -288,6 +351,8 @@ static KSTATUS KiIoSubmitAsyncFileRequest(IO_ASYNC_FILE_OP op, ObDereferenceObject(&req->Header); // provider rejected synchronously iosb->Status = ns; + req->IosbSnapshot.Status = ns; + req->IosbSnapshot.Information = 0; ObDereferenceObject(&req->Header); // creator reference return ns; } @@ -295,6 +360,8 @@ static KSTATUS KiIoSubmitAsyncFileRequest(IO_ASYNC_FILE_OP op, KSTATUS s = KiIoEnsureAsyncFileIoStarted(); if (!KSUCCESS(s)) { iosb->Status = s; + req->IosbSnapshot.Status = s; + req->IosbSnapshot.Information = 0; ObDereferenceObject(&req->Header); return s; } @@ -323,10 +390,12 @@ __declspec(dllexport) KSTATUS IoReadFileAsyncKernel(FILE_OBJECT* file, void* buf IO_STATUS_BLOCK* iosb, EVENT* completionEvent, KAPC* completionApc, + MM_LOCKED_USER_BUFFER* lockedBuffer, + MM_LOCKED_USER_BUFFER* lockedIosb, IO_ASYNC_FILE_REQUEST_OBJECT** requestOut) { return KiIoSubmitAsyncFileRequest(IoAsyncFileRead, file, buf, size, offset, iosb, completionEvent, completionApc, - requestOut); + lockedBuffer, lockedIosb, requestOut); } __declspec(dllexport) KSTATUS IoWriteFileAsyncKernel(FILE_OBJECT* file, const void* buf, @@ -334,10 +403,12 @@ __declspec(dllexport) KSTATUS IoWriteFileAsyncKernel(FILE_OBJECT* file, const vo IO_STATUS_BLOCK* iosb, EVENT* completionEvent, KAPC* completionApc, + MM_LOCKED_USER_BUFFER* lockedBuffer, + MM_LOCKED_USER_BUFFER* lockedIosb, IO_ASYNC_FILE_REQUEST_OBJECT** requestOut) { return KiIoSubmitAsyncFileRequest(IoAsyncFileWrite, file, (void*)buf, size, offset, iosb, completionEvent, completionApc, - requestOut); + lockedBuffer, lockedIosb, requestOut); } // Implements the async File Request Object operation. @@ -345,6 +416,19 @@ __declspec(dllexport) OBJECT* IoAsyncFileRequestObject(IO_ASYNC_FILE_REQUEST_OBJ return req ? &req->Header : (OBJECT*)0; } + +// Returns the wait event owned by an async file request. +__declspec(dllexport) EVENT* IoGetAsyncFileRequestWaitEvent(IO_ASYNC_FILE_REQUEST_OBJECT* req) { + return req ? req->WaitEvent : (EVENT*)0; +} + +// Waits for an async file request to complete according to wait flags. +__declspec(dllexport) KSTATUS IoWaitAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req, uint32_t flags) { + if (!req || !req->WaitEvent) return STATUS_INVALID_PARAMETER; + if (flags & ~SYS_WAIT_FLAG_ALERTABLE) return STATUS_INVALID_PARAMETER; + return KeWaitForEvent(req->WaitEvent, (flags & SYS_WAIT_FLAG_ALERTABLE) ? 1 : 0); +} + // Cancels async File Request. __declspec(dllexport) KSTATUS IoCancelAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req) { if (!req) return STATUS_INVALID_PARAMETER; @@ -377,13 +461,8 @@ __declspec(dllexport) KSTATUS IoCancelAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJ __declspec(dllexport) KSTATUS IoQueryAsyncFileRequest(IO_ASYNC_FILE_REQUEST_OBJECT* req, IO_STATUS_BLOCK* iosbOut) { if (!req || !iosbOut) return STATUS_INVALID_PARAMETER; - if (req->Iosb) { - iosbOut->Status = req->Iosb->Status; - iosbOut->Information = req->Iosb->Information; - } else { - iosbOut->Status = STATUS_FAILURE; - iosbOut->Information = 0; - } + iosbOut->Status = req->IosbSnapshot.Status; + iosbOut->Information = req->IosbSnapshot.Information; return STATUS_SUCCESS; } @@ -622,10 +701,51 @@ __declspec(dllexport) bool IoFilesystemPresent(uint8_t volumeIndex) { return gVolumes[volumeIndex].Fs != (IO_FILESYSTEM*)0; } +// Aggregates filesystem traversal statistics across mounted volumes. +__declspec(dllexport) void IoQueryFilesystemStats(IO_FILESYSTEM_STATS* out) { + if (!out) return; + memset(out, 0, sizeof(*out)); + + for (uint8_t i = 0; i < MAX_DRIVES; i++) { + IO_FILESYSTEM* fs = gVolumes[i].Fs; + if (!fs || !fs->QueryStats) continue; + + IO_FILESYSTEM_STATS one; + memset(&one, 0, sizeof(one)); + fs->QueryStats(fs, &one); + + out->PathLookups += one.PathLookups; + out->PathCacheHits += one.PathCacheHits; + out->PathCacheMisses += one.PathCacheMisses; + out->PathCacheStores += one.PathCacheStores; + out->PathCacheInvalidations += one.PathCacheInvalidations; + if (one.PathCacheGeneration > out->PathCacheGeneration) + out->PathCacheGeneration = one.PathCacheGeneration; + out->DirectorySearches += one.DirectorySearches; + out->DirectoryClustersRead += one.DirectoryClustersRead; + out->DirectoryEntriesScanned += one.DirectoryEntriesScanned; + out->FatChainCacheLookups += one.FatChainCacheLookups; + out->FatChainCacheHits += one.FatChainCacheHits; + out->FatChainCacheMisses += one.FatChainCacheMisses; + out->FatChainCacheStores += one.FatChainCacheStores; + out->FatChainCacheInvalidations += one.FatChainCacheInvalidations; + out->FatChainWalkSteps += one.FatChainWalkSteps; + out->DataReadRuns += one.DataReadRuns; + out->DataReadSectors += one.DataReadSectors; + out->DataReadRunMerges += one.DataReadRunMerges; + if (one.MaxDataReadSpanSectors > out->MaxDataReadSpanSectors) + out->MaxDataReadSpanSectors = one.MaxDataReadSpanSectors; + } +} + // Destructor. static void KiFileDestroy(OBJECT* obj) { FILE_OBJECT* f = CONTAINING_RECORD(obj, FILE_OBJECT, Header); - if (f->Fs && f->FileCtx) f->Fs->Close(f->Fs, f->FileCtx); + if (f->Fs && f->FileCtx) { + (void)IoFlushFile(f); + MmFileCacheInvalidateAll(f); + f->Fs->Close(f->Fs, f->FileCtx); + } } // IoOpenFile. @@ -655,11 +775,73 @@ __declspec(dllexport) KSTATUS IoOpenFile(SYS_STRING_VIEW path, FILE_OBJECT **out f->Lock = (SPINLOCK)SPINLOCK_INIT; f->Size = fs->GetSize(fs, fileCtx); f->Offset = 0; + f->Flags = 0; + f->ReadaheadScore = 0; + f->ReadaheadNextOffset = 0; + f->ReadaheadIssuedUntil = 0; + f->ReadaheadSuppressedUntil = 0; *out = f; return STATUS_SUCCESS; } + +static uint64_t KiAlignUpPage64(uint64_t value) { + return (value + PAGE_SIZE - 1u) & ~(uint64_t)(PAGE_SIZE - 1u); +} + +static void KiIoMaybeScheduleReadAhead(FILE_OBJECT* file, uint64_t offset, size_t bytesRead, uint64_t fileSize) { + if (!file || bytesRead < 512) + return; + if ((file->Flags & IO_FILE_FLAG_NO_CACHE) != 0) + return; + + uint64_t end = offset + bytesRead; + uint64_t start = 0; + uint32_t pages = 0; + + uintptr_t flags = KeAcquireSpinlock(&file->Lock); + if (offset == file->ReadaheadNextOffset) { + if (file->ReadaheadScore < 32) + file->ReadaheadScore++; + } else { + file->ReadaheadScore = 1; + file->ReadaheadIssuedUntil = end; + } + file->ReadaheadNextOffset = end; + + if (end < file->ReadaheadSuppressedUntil) { + pages = 0; + } else if (file->ReadaheadScore >= 2 || bytesRead >= PAGE_SIZE) { + pages = file->ReadaheadScore < 4 ? 8u : (file->ReadaheadScore < 8 ? 16u : 32u); + start = KiAlignUpPage64(end); + if (start < file->ReadaheadIssuedUntil) + start = KiAlignUpPage64(file->ReadaheadIssuedUntil); + if (start >= fileSize) { + pages = 0; + } else { + uint64_t maxEnd = start + ((uint64_t)pages * PAGE_SIZE); + if (maxEnd > fileSize) + pages = (uint32_t)((KiAlignUpPage64(fileSize) - start) / PAGE_SIZE); + file->ReadaheadIssuedUntil = start + ((uint64_t)pages * PAGE_SIZE); + } + } + KeReleaseSpinlock(&file->Lock, flags); + + if (pages) { + KSTATUS ras = MmFileCacheScheduleReadAhead(file, start, pages); + if (ras == STATUS_QUEUE_FULL) { + flags = KeAcquireSpinlock(&file->Lock); + uint64_t suppressUntil = end + (1024u * 1024u); + if (suppressUntil < end || suppressUntil > fileSize) + suppressUntil = fileSize; + if (file->ReadaheadSuppressedUntil < suppressUntil) + file->ReadaheadSuppressedUntil = suppressUntil; + KeReleaseSpinlock(&file->Lock, flags); + } + } +} + // Explicit-offset file I/O. Async I/O is built on these routines so it does // not race the sequential FILE_OBJECT.Offset used by legacy read/write calls. __declspec(dllexport) KSTATUS IoReadFileAt(FILE_OBJECT* file, uint64_t offset, @@ -679,7 +861,50 @@ __declspec(dllexport) KSTATUS IoReadFileAt(FILE_OBJECT* file, uint64_t offset, size_t remaining = (size_t)(fileSize - offset); if (size > remaining) size = remaining; - return file->Fs->Read(file->Fs, file->FileCtx, offset, buf, size, bytesRead); + if ((file->Flags & IO_FILE_FLAG_NO_CACHE) || size < 512) + return file->Fs->Read(file->Fs, file->FileCtx, offset, buf, size, bytesRead); + + size_t total = 0; + uint8_t* dst = (uint8_t*)buf; + while (total < size) { + uint64_t absolute = offset + total; + uint64_t pageOffset = absolute & ~(uint64_t)(PAGE_SIZE - 1); + size_t inPage = (size_t)(absolute - pageOffset); + size_t chunk = PAGE_SIZE - inPage; + if (chunk > size - total) chunk = size - total; + + MM_FILE_CACHE_PAGE page; + KSTATUS s = MmFileCacheGetPage(file, pageOffset, &page); + if (!KSUCCESS(s)) { + if (total) { + *bytesRead = total; + return STATUS_SUCCESS; + } + return s; + } + + if (inPage >= page.ValidBytes) { + MmFileCacheReleasePage(&page); + break; + } + size_t available = page.ValidBytes - inPage; + if (chunk > available) chunk = available; + + void* srcPage = MmPfnToVirtual(page.Frame); + if (!srcPage) { + MmFileCacheReleasePage(&page); + return total ? STATUS_SUCCESS : STATUS_FAILURE; + } + memcpy(dst + total, (const uint8_t*)srcPage + inPage, chunk); + MmFileCacheReleasePage(&page); + + total += chunk; + if (chunk == 0) break; + } + + *bytesRead = total; + KiIoMaybeScheduleReadAhead(file, offset, total, fileSize); + return STATUS_SUCCESS; } __declspec(dllexport) KSTATUS IoWriteFileAt(FILE_OBJECT* file, uint64_t offset, @@ -691,10 +916,21 @@ __declspec(dllexport) KSTATUS IoWriteFileAt(FILE_OBJECT* file, uint64_t offset, if (!file->Fs->Write) return STATUS_NOT_SUPPORTED; if (size == 0) return STATUS_SUCCESS; + uintptr_t flags = KeAcquireSpinlock(&file->Lock); + uint64_t oldSize = file->Size; + KeReleaseSpinlock(&file->Lock, flags); + size_t written = 0; - KSTATUS s = file->Fs->Write(file->Fs, file->FileCtx, offset, buf, size, &written); + KSTATUS s; + if (file->Flags & IO_FILE_FLAG_NO_CACHE) { + s = file->Fs->Write(file->Fs, file->FileCtx, offset, buf, size, &written); + if (KSUCCESS(s) && written) + MmFileCacheInvalidate(file, offset, written); + } else { + s = MmFileCacheWrite(file, offset, buf, size, oldSize, &written); + } if (KSUCCESS(s)) { - uintptr_t flags = KeAcquireSpinlock(&file->Lock); + flags = KeAcquireSpinlock(&file->Lock); uint64_t end = offset + written; if (end > file->Size) file->Size = end; KeReleaseSpinlock(&file->Lock, flags); @@ -704,6 +940,16 @@ __declspec(dllexport) KSTATUS IoWriteFileAt(FILE_OBJECT* file, uint64_t offset, return s; } +// Flushes dirty mapped and cached pages for one file object. +__declspec(dllexport) KSTATUS IoFlushFile(FILE_OBJECT* file) { + if (!file) return STATUS_INVALID_PARAMETER; + if (!KeCanWait()) return STATUS_INVALID_IRQL; + + KSTATUS sync = MmVadSyncFileMappings(file); + KSTATUS flush = MmFileCacheFlushFile(file); + return KSUCCESS(sync) ? flush : sync; +} + // IoReadFile - sequential read at current offset. __declspec(dllexport) KSTATUS IoReadFile(FILE_OBJECT* file, void* buf, size_t size, size_t* bytesRead) { @@ -768,6 +1014,11 @@ __declspec(dllexport) KSTATUS IoCreateFile(SYS_STRING_VIEW path, FILE_OBJECT** o f->Lock = (SPINLOCK)SPINLOCK_INIT; f->Size = 0; f->Offset = 0; + f->Flags = 0; + f->ReadaheadScore = 0; + f->ReadaheadNextOffset = 0; + f->ReadaheadIssuedUntil = 0; + f->ReadaheadSuppressedUntil = 0; *out = f; return STATUS_SUCCESS; @@ -848,8 +1099,12 @@ __declspec(dllexport) KSTATUS IoTruncateFile(FILE_OBJECT* file, uint64_t newSize if (!KeCanWait()) return STATUS_INVALID_IRQL; if (!file->Fs->Truncate) return STATUS_NOT_SUPPORTED; - KSTATUS s = file->Fs->Truncate(file->Fs, file->FileCtx, newSize); + KSTATUS flush = IoFlushFile(file); + if (!KSUCCESS(flush)) return flush; + + KSTATUS s = file->Fs->Truncate(file->Fs, file->FileCtx, newSize, file->Flags); if (KSUCCESS(s)) { + MmFileCacheInvalidate(file, newSize, (size_t)-1); file->Size = newSize; // Pull the file pointer back if it now sits past EOF. if (file->Offset > file->Size) @@ -875,6 +1130,16 @@ __declspec(dllexport) uint64_t IoGetFileSize(FILE_OBJECT* file) { return size; } +// Queries the filesystem identity for a file object. +__declspec(dllexport) KSTATUS IoQueryFileIdentity(FILE_OBJECT* file, IO_FILE_IDENTITY* out) { + if (!file || !out) + return STATUS_INVALID_PARAMETER; + memset(out, 0, sizeof(*out)); + if (!file->Fs || !file->Fs->QueryIdentity) + return STATUS_NOT_SUPPORTED; + return file->Fs->QueryIdentity(file->Fs, file->FileCtx, out); +} + // Releases file. __declspec(dllexport) void IoCloseFile(FILE_OBJECT* file) { if (file) ObDereferenceObject(&file->Header); diff --git a/code/kernel/ke_apc.c b/code/kernel/ke_apc.c index 026dcc0..48f81d9 100644 --- a/code/kernel/ke_apc.c +++ b/code/kernel/ke_apc.c @@ -204,7 +204,7 @@ __declspec(dllexport) KSTATUS KeQueueUserApc(THREAD* thread, if (thread->State == ThreadDead) return STATUS_OBJECT_DEAD; - KUSER_APC* apc = (KUSER_APC*)MmAllocatePool(sizeof(KUSER_APC)); + KUSER_APC* apc = (KUSER_APC*)MmAllocatePoolTag(sizeof(KUSER_APC), MM_POOL_TAG('a', 'p', 'c', ' ')); if (!apc) return STATUS_NO_MEMORY; diff --git a/code/kernel/ke_debug.c b/code/kernel/ke_debug.c index 092fecb..38b0c66 100644 --- a/code/kernel/ke_debug.c +++ b/code/kernel/ke_debug.c @@ -1,20 +1,24 @@ // Kernel debugging and bugcheck support. This file owns panic reporting, process fault reporting, and diagnostic queries. #include +#include #include #include #include #include #include #pragma intrinsic(_disable) +#pragma intrinsic(_enable) #include #include #include #include #include #include +#include #include #include +#include #include int _fltused; // MSVC floating-point helper anchor @@ -22,15 +26,25 @@ int gDebugEnabled = 0; static uint32_t gDebugFlags = 0; void (*PrintChar)(char, uint32_t, uint32_t); -#define KE_DEBUG_LOG_BUFFER_SIZE (50u * 1024u) -#define KE_DEBUG_LOG_LINE_SIZE 1024u +#define KE_DEBUG_LOG_BUFFER_SIZE (50u * 1024u) +#define KE_DEBUG_SCREEN_BUFFER_SIZE (16u * 1024u) +#define KE_DEBUG_LOG_LINE_SIZE 1024u +#define KE_DEBUG_SCREEN_DRAIN_SIZE 512u static SPINLOCK gDebugPrintLock = SPINLOCK_INIT; static char* gDebugLogBuffer; static size_t gDebugLogSize; static size_t gDebugLogWrite; static size_t gDebugLogUsed; +static char* gDebugScreenBuffer; +static size_t gDebugScreenSize; +static size_t gDebugScreenRead; +static size_t gDebugScreenWrite; +static size_t gDebugScreenUsed; static volatile long gDebugLogFlushing; +static volatile long gDebugConsoleOwnsSerial; +static volatile long gDebugConsoleWorkReady; +static KWORK_ITEM gDebugConsoleWorkItem; // Implements the debug Log Append Locked operation. static void KiDebugLogAppendLocked(const char* text, size_t len) { @@ -55,6 +69,95 @@ static void KiDebugLogAppendLocked(const char* text, size_t len) { gDebugLogUsed += len; } +// Implements the debug Screen Append Locked operation. +static void KiDebugScreenAppendLocked(const char* text, size_t len) { + if (!gDebugScreenBuffer || gDebugScreenSize == 0 || !text || len == 0) + return; + + if (len >= gDebugScreenSize) { + text += len - (gDebugScreenSize - 1u); + len = gDebugScreenSize - 1u; + } + + while (gDebugScreenUsed + len > gDebugScreenSize) { + gDebugScreenRead = (gDebugScreenRead + 1u) % gDebugScreenSize; + gDebugScreenUsed--; + } + + size_t first = gDebugScreenSize - gDebugScreenWrite; + if (first > len) first = len; + memcpy(gDebugScreenBuffer + gDebugScreenWrite, text, first); + if (len > first) + memcpy(gDebugScreenBuffer, text + first, len - first); + + gDebugScreenWrite = (gDebugScreenWrite + len) % gDebugScreenSize; + gDebugScreenUsed += len; +} + +// Implements the debug Screen Drain Locked operation. +static size_t KiDebugScreenDrainLocked(char* out, size_t max) { + if (!out || max == 0 || !gDebugScreenBuffer || gDebugScreenSize == 0 || gDebugScreenUsed == 0) + return 0; + + size_t len = gDebugScreenUsed; + if (len > max) len = max; + + size_t first = gDebugScreenSize - gDebugScreenRead; + if (first > len) first = len; + memcpy(out, gDebugScreenBuffer + gDebugScreenRead, first); + if (len > first) + memcpy(out + first, gDebugScreenBuffer, len - first); + + gDebugScreenRead = (gDebugScreenRead + len) % gDebugScreenSize; + gDebugScreenUsed -= len; + return len; +} + +// Ensures the asynchronous screen ring exists before console output starts using it. +static bool KiDebugEnsureScreenBuffer(void) { + if (gDebugScreenBuffer && gDebugScreenSize != 0) + return true; + + char* buffer = (char*)MmAllocatePoolTag(KE_DEBUG_SCREEN_BUFFER_SIZE, MM_POOL_TAG('d', 's', 'c', 'r')); + if (!buffer) + return false; + memset(buffer, 0, KE_DEBUG_SCREEN_BUFFER_SIZE); + + bool installed = false; + uintptr_t flags = KeAcquireSpinlock(&gDebugPrintLock); + if (!gDebugScreenBuffer) { + gDebugScreenBuffer = buffer; + gDebugScreenSize = KE_DEBUG_SCREEN_BUFFER_SIZE; + gDebugScreenRead = 0; + gDebugScreenWrite = 0; + gDebugScreenUsed = 0; + installed = true; + } + KeReleaseSpinlock(&gDebugPrintLock, flags); + + if (!installed) + MmFreePool(buffer); + return true; +} + +// Implements the debug Serial Write Char operation. +static void KiDebugSerialWriteChar(char c) { + while ((__inbyte(0x3FD) & 0x20) == 0) { } + if (c == '\n') { + __outbyte(0x3F8, '\r'); + while ((__inbyte(0x3FD) & 0x20) == 0) { } + } + __outbyte(0x3F8, (uint8_t)c); +} + +// Implements the debug Serial Write Locked operation. +static void KiDebugSerialWriteLocked(const char* text, size_t len) { + if (!text) + return; + for (size_t i = 0; i < len; i++) + KiDebugSerialWriteChar(text[i]); +} + // Implements the debug Console Write operation. static void KiDebugConsoleWrite(const char* text, size_t len) { if (!PrintChar || !text) @@ -63,6 +166,28 @@ static void KiDebugConsoleWrite(const char* text, size_t len) { PrintChar(text[i], 0xCCCCCC, 0x012456); } +// Implements the debug Console Work operation. +static void KiDebugConsoleWork(void* context) { + (void)context; + char chunk[KE_DEBUG_SCREEN_DRAIN_SIZE]; + + for (;;) { + uintptr_t flags = KeAcquireSpinlock(&gDebugPrintLock); + size_t len = KiDebugScreenDrainLocked(chunk, sizeof(chunk)); + KeReleaseSpinlock(&gDebugPrintLock, flags); + + if (len == 0) + break; + KiDebugConsoleWrite(chunk, len); + } +} + +// Queues the debug Console Work operation if the passive work queue is ready. +static void KiDebugQueueConsoleWork(void) { + if (_InterlockedCompareExchange((volatile long*)&gDebugConsoleWorkReady, 1, 1) && KeIsWorkQueueStarted()) + (void)KeQueueWorkItem(&gDebugConsoleWorkItem); +} + static volatile long gPanicInProgress = 0; static volatile uintptr_t gPanicOwnerToken = 0; static volatile long gPanicStoppedCpuCount = 0; @@ -116,7 +241,7 @@ __declspec(dllexport) int KeHandleNmiIpi(void) { if (KiPanicIsActive()) KiPanicSecondaryHalt(); - if (MmHandleTlbShootdownNmi()) + if (MmPteHandleShootdownNmi()) return 1; return 0; @@ -124,6 +249,8 @@ __declspec(dllexport) int KeHandleNmiIpi(void) { // Implements the kernel Put Char operation. void KiKernelPutChar(char c) { + if (_InterlockedCompareExchange((volatile long*)&gDebugConsoleOwnsSerial, 1, 1)) + KiDebugSerialWriteChar(c); if (PrintChar) PrintChar(c, 0xCCCCCC, 0x012456); } @@ -135,8 +262,11 @@ void KeInitDebug(void (*PrintPtr)(char, uint32_t, uint32_t), uint32_t flags) { PrintChar = PrintPtr; printf_set_putchar(KiKernelPutChar); + if (!_InterlockedCompareExchange((volatile long*)&gDebugConsoleWorkReady, 1, 0)) + KeInitializeWorkItem(&gDebugConsoleWorkItem, KiDebugConsoleWork, (void*)0); + if ((flags & (KE_DEBUG_CONSOLE | KE_DEBUG_FILE_LOG)) && !gDebugLogBuffer) { - gDebugLogBuffer = (char*)MmAllocatePool(KE_DEBUG_LOG_BUFFER_SIZE); + gDebugLogBuffer = (char*)MmAllocatePoolTag(KE_DEBUG_LOG_BUFFER_SIZE, MM_POOL_TAG('d', 'l', 'o', 'g')); if (gDebugLogBuffer) { gDebugLogSize = KE_DEBUG_LOG_BUFFER_SIZE; gDebugLogWrite = 0; @@ -144,6 +274,9 @@ void KeInitDebug(void (*PrintPtr)(char, uint32_t, uint32_t), uint32_t flags) { memset(gDebugLogBuffer, 0, gDebugLogSize); } } + + if (flags & KE_DEBUG_CONSOLE) + (void)KiDebugEnsureScreenBuffer(); } // Returns debug Flags. @@ -157,6 +290,13 @@ void KeSetDebugConsoleWriter(void (*PrintPtr)(char, uint32_t, uint32_t)) { return; PrintChar = PrintPtr; printf_set_putchar(KiKernelPutChar); + + // After the display manager owns debug drawing, DbgPrint and user console + // writes mirror to COM1 themselves. The display writer is screen-only so + // asynchronous screen flushing does not duplicate serial bytes. + (void)KiDebugEnsureScreenBuffer(); + _InterlockedExchange((volatile long*)&gDebugConsoleOwnsSerial, 1); + KiDebugQueueConsoleWork(); } // Implements the dbg Print operation. @@ -179,17 +319,37 @@ __declspec(dllexport) void DbgPrint(const char* fmt, ...) { if (KiPanicIsActive()) { if (!KiPanicCurrentCpuOwnsReport()) KiPanicSecondaryHalt(); - if (gDebugFlags & KE_DEBUG_CONSOLE) + if (gDebugFlags & KE_DEBUG_CONSOLE) { + if (_InterlockedCompareExchange((volatile long*)&gDebugConsoleOwnsSerial, 1, 1)) + KiDebugSerialWriteLocked(line, len); KiDebugConsoleWrite(line, len); + } return; } + bool queueConsole = false; + bool writeConsoleSync = false; + uintptr_t flags = KeAcquireSpinlock(&gDebugPrintLock); + if (_InterlockedCompareExchange((volatile long*)&gDebugConsoleOwnsSerial, 1, 1)) + KiDebugSerialWriteLocked(line, len); if (!_InterlockedCompareExchange((volatile long*)&gDebugLogFlushing, 0, 0)) KiDebugLogAppendLocked(line, len); - if (gDebugFlags & KE_DEBUG_CONSOLE) - KiDebugConsoleWrite(line, len); + + if (gDebugFlags & KE_DEBUG_CONSOLE) { + if (KeIsWorkQueueStarted() && gDebugScreenBuffer) { + KiDebugScreenAppendLocked(line, len); + queueConsole = true; + } else { + writeConsoleSync = true; + } + } KeReleaseSpinlock(&gDebugPrintLock, flags); + + if (queueConsole) + KiDebugQueueConsoleWork(); + else if (writeConsoleSync) + KiDebugConsoleWrite(line, len); } // Flushes debug Log To File. @@ -242,6 +402,23 @@ __declspec(dllexport) KSTATUS KeFlushDebugLogToFile(SYS_STRING_VIEW path) { return (KSUCCESS(st) && totalWritten == used) ? STATUS_SUCCESS : st; } + +// Attempts to persist the debug log during a panic when the filesystem is usable. +static void KiTryFlushPanicDebugLog(void) { + if (!(gDebugFlags & KE_DEBUG_FILE_LOG)) + return; + if (!KeCanWait() || !KeSystemRootReady()) + return; + + char path[1024]; + if (!KeBuildSystemRootPath("kernel-panic.log", path, sizeof(path))) + return; + + KSTATUS st = KeFlushDebugLogToFile(SysSvFromCstr(path)); + if (!KSUCCESS(st)) + DbgPrint("debuglog: panic flush to %s failed st=0x%x\n", path, st); +} + // KePanic. __declspec(dllexport) _Noreturn void KePanic(int code, const char* fmt, ...) { KiBeginFatalReport(); @@ -279,15 +456,39 @@ __declspec(dllexport) _Noreturn void KePanic(int code, const char* fmt, ...) { va_end(va); } + KiTryFlushPanicDebugLog(); + for (;;) __halt(); } // KePrintConsole - write user-visible console text using the default console palette. -// Panic output keeps a distinct color so fatal paths remain visually obvious. +// User text mirrors to COM1 immediately, then queues expensive screen drawing +// through the passive debug-console worker when that path is available. __declspec(dllexport) void KePrintConsole(const char* buf, size_t len) { - if (!buf || !PrintChar) return; - for (size_t i = 0; i < len; i++) - PrintChar(buf[i], 0xCCCCCC, 0x012456); + if (!buf || len == 0) return; + + bool queueConsole = false; + bool writeConsoleSync = false; + bool canQueueScreen = PrintChar && KeIsWorkQueueStarted() && KiDebugEnsureScreenBuffer(); + + uintptr_t flags = KeAcquireSpinlock(&gDebugPrintLock); + if (_InterlockedCompareExchange((volatile long*)&gDebugConsoleOwnsSerial, 1, 1)) + KiDebugSerialWriteLocked(buf, len); + + if (PrintChar) { + if (!KiPanicIsActive() && canQueueScreen) { + KiDebugScreenAppendLocked(buf, len); + queueConsole = true; + } else { + writeConsoleSync = true; + } + } + KeReleaseSpinlock(&gDebugPrintLock, flags); + + if (queueConsole) + KiDebugQueueConsoleWork(); + else if (writeConsoleSync) + KiDebugConsoleWrite(buf, len); } // KeBugCheckProcess - user-mode fault handler @@ -613,8 +814,22 @@ static void KiDumpFaultFrame(PROCESS* proc, const KFAULT_FRAME* f) { // Prints crash Char. static void KiPrintCrashChar(char c) { + if (_InterlockedCompareExchange((volatile long*)&gDebugConsoleOwnsSerial, 1, 1)) + KiDebugSerialWriteChar(c); if (PrintChar) - PrintChar(c, 0xFFAA00, 0x000000); // amber - distinct from green debug / red panic + PrintChar(c, 0xFFAA00, 0x000000); +} + +static KSTATUS KiFaultExitStatus(int vector) { + switch (vector) { + case 0: return STATUS_PROCESS_DIVIDE_BY_ZERO; + case 3: return STATUS_PROCESS_BREAKPOINT; + case 6: return STATUS_PROCESS_ILLEGAL_OPCODE; + case 13: return STATUS_PROCESS_GENERAL_PROTECTION; + case 14: return STATUS_PROCESS_PAGE_FAULT; + case 17: return STATUS_PROCESS_ALIGNMENT_FAULT; + default: return STATUS_PROCESS_UNKNOWN_FAULT; + } } static void KiPrintCrashHeader(PROCESS* proc, int vector, uintptr_t rip, @@ -648,13 +863,23 @@ static void KiPrintCrashHeader(PROCESS* proc, int vector, uintptr_t rip, } // Implements the finish Faulted Process operation. -static _Noreturn void KiFinishFaultedProcess(PROCESS* proc, THREAD* thread) { +static _Noreturn void KiFinishFaultedProcess(PROCESS* proc, THREAD* thread, KSTATUS exitStatus) { printf_set_putchar(KiKernelPutChar); - if (proc) KeExitProcess(proc, -1); - if (thread) KeExitThread(thread, -1); + int exitCode = (int)exitStatus; + if (proc) (void)KeTerminateProcess(proc, exitCode); + if (thread) KeExitThread(thread, exitCode); - while (1) __halt(); + // KeExitThread normally switches away and never resumes this context. If + // a fault path re-enters here during scheduler bring-up or with no runnable + // peer yet, do not fall back to a local CLI/HLT dead stop: keep offering the + // dead thread to the scheduler with interrupts enabled so waiters such as + // the foreground shell can observe the process exit event. + while (true) { + _enable(); + KeYield(); + __halt(); + } } _Noreturn void KeBugCheckProcess(int vector, uintptr_t rip, uintptr_t errCode, uintptr_t cr2) { @@ -664,11 +889,11 @@ _Noreturn void KeBugCheckProcess(int vector, uintptr_t rip, uintptr_t errCode, u KiPrintCrashHeader(proc, vector, rip, errCode, cr2); KiDumpModules(proc); - KiFinishFaultedProcess(proc, thread); + KiFinishFaultedProcess(proc, thread, KiFaultExitStatus(vector)); } -// Implements the fault Dispatch Frame operation. -__declspec(dllexport) _Noreturn void KeFaultDispatchFrame(const KFAULT_FRAME* frame) { +// Dispatches a CPU fault frame and returns only when a user #PF was repaired. +__declspec(dllexport) KSTATUS KeFaultDispatchFrame(const KFAULT_FRAME* frame) { // Panic NMIs are broadcast to stop all other CPUs. Do not try to decode // the interrupted context on a secondary CPU once another CPU owns panic. if (KiPanicIsActive()) @@ -682,6 +907,12 @@ __declspec(dllexport) _Noreturn void KeFaultDispatchFrame(const KFAULT_FRAME* fr (unsigned long long)(uintptr_t)frame); } + if (frame->Vector == 14) { + KSTATUS handled = MmVadResolvePageFaultByAddress((uintptr_t)frame->Cr2, frame->ErrorCode); + if (KSUCCESS(handled)) + return STATUS_SUCCESS; + } + if ((frame->Cs & 3) == 0) { KiBeginFatalReport(); @@ -702,7 +933,7 @@ __declspec(dllexport) _Noreturn void KeFaultDispatchFrame(const KFAULT_FRAME* fr if (vector == 14) { MmDebugDescribeAddress((uintptr_t)frame->Cr2); if (frame->Rsp) MmDebugDescribeAddress((uintptr_t)frame->Rsp); - MmDebugDumpUserRegions(proc); + MmVadDebugDumpProcess(proc); } if (vector == 14) { @@ -743,7 +974,7 @@ __declspec(dllexport) _Noreturn void KeFaultDispatchFrame(const KFAULT_FRAME* fr KiDumpFaultFrame(proc, frame); KiDumpModules(proc); - KiFinishFaultedProcess(proc, thread); + KiFinishFaultedProcess(proc, thread, KiFaultExitStatus((int)frame->Vector)); } // KeFaultDispatch - single C entry point for all three IDT fault stubs. @@ -762,5 +993,6 @@ __declspec(dllexport) _Noreturn void KeFaultDispatch(int vector, uintptr_t rip, frame.Cr2 = cr2; frame.Rip = rip; frame.Cs = cs; - KeFaultDispatchFrame(&frame); + (void)KeFaultDispatchFrame(&frame); + while (1) __halt(); } diff --git a/code/kernel/ke_display.c b/code/kernel/ke_display.c index afc0cd7..bd211e6 100644 --- a/code/kernel/ke_display.c +++ b/code/kernel/ke_display.c @@ -12,6 +12,10 @@ #include #include #include +#include +#include +#include +#include #define DISPLAY_DEFAULT_FG 0x00CCCCCCu #define DISPLAY_DEFAULT_BG 0x00012456u @@ -25,6 +29,7 @@ #define DISPLAY_CONSOLE_MAX_ROWS 128 #define DISPLAY_CONSOLE_MAX_CELLS (DISPLAY_CONSOLE_MAX_COLS * DISPLAY_CONSOLE_MAX_ROWS) #define DISPLAY_CONSOLE_DEFAULT_ATTR 0x07u +#define DISPLAY_CURSOR_POS_INVALID (-2147483647 - 1) typedef struct BOOTFB_CONTEXT { const HAL_API* Hal; @@ -40,6 +45,11 @@ static const HAL_API* gDisplayHal; static DISPLAY_DEVICE* gDisplayList; static DISPLAY_DEVICE* gPrimaryDisplay; static SPINLOCK gDisplayLock = SPINLOCK_INIT; +static MUTEX* gDisplayProviderMutex; +static KWORK_ITEM gDisplayCursorWorkItem; +static volatile long gDisplayCursorWorkInitialized; +static bool gDisplayTransitionInProgress; +static uint64_t gDisplayGeneration; static DISPLAY_DEVICE gBootFbDevice; static BOOTFB_CONTEXT gBootFb; @@ -56,6 +66,11 @@ static int gCursorX; static int gCursorY; static int gCursorDrawX; static int gCursorDrawY; +static bool gCursorUpdatePending; +static uint64_t gCursorRequestGeneration; +static uint64_t gCursorAppliedGeneration; +static int gCursorCommittedX; +static int gCursorCommittedY; static int gConsoleX; static int gConsoleY; @@ -91,6 +106,69 @@ static const DISPLAY_DEVICE_OPS gBootFbOps = { }; +static bool KiDisplayOutputSuspended(void); +static void KiDisplayCursorWorker(void* context); +static void KiDisplayDirtyAll(void); + +static bool KiDisplayHasCurrentThread(void) { + return KeGetCurrentThread() != (THREAD*)0; +} + +static bool KiDisplayProviderCallbacksRequirePassive(void) { + return KiDisplayHasCurrentThread() && KeIsWorkQueueStarted(); +} + +static KSTATUS KiDisplayAcquireProviderMutex(bool* acquired) { + if (acquired) + *acquired = false; + + if (!gDisplayProviderMutex || !KiDisplayProviderCallbacksRequirePassive()) + return STATUS_SUCCESS; + + if (!KeCanWait()) + return STATUS_INVALID_IRQL; + + if (KeGetDebugFlags() & KE_DEBUG_DISPLAY_ASSERTS) + KeAssertIrql(PASSIVE_LEVEL); + + KSTATUS s = KeAcquireMutex(gDisplayProviderMutex); + if (KSUCCESS(s) && acquired) + *acquired = true; + return s; +} + +static void KiDisplayReleaseProviderMutex(bool acquired) { + if (acquired && gDisplayProviderMutex) + KeReleaseMutex(gDisplayProviderMutex); +} + +// Writes one character through the boot HAL framebuffer path when the display +// manager cannot safely call provider callbacks, such as during a panic. +static KSTATUS KiDisplayFallbackPrintChar(char c, uint32_t fg, uint32_t bg) { + const HAL_API* hal = gDisplayHal; + if (hal && hal->Screen.PrintChar) { + hal->Screen.PrintChar(c, fg, bg); + return STATUS_SUCCESS; + } + return STATUS_INVALID_IRQL; +} + +static void KiDisplayQueueCursorWork(void) { + if (!_InterlockedCompareExchange(&gDisplayCursorWorkInitialized, 1, 1)) + return; + if (KeIsWorkQueueStarted()) { + (void)KeQueueWorkItem(&gDisplayCursorWorkItem); + return; + } + + // During single-threaded early boot there is no worker thread yet. The + // boot framebuffer console still needs synchronous cursor setup; once the + // scheduler work queue exists all cursor commits are asynchronous. + if (!KiDisplayProviderCallbacksRequirePassive()) + KiDisplayCursorWorker(0); +} + + // Checks whether a primary display provider is available. static bool KiDisplayHasDevice(void) { return gPrimaryDisplay && gPrimaryDisplay->Ops; @@ -117,6 +195,136 @@ static bool KiDisplayHasCursorOps(void) { gPrimaryDisplay->Ops->ShowCursor; } + +static void KiDisplayCursorWorker(void* context) { + (void)context; + + bool providerHeld = false; + if (!KSUCCESS(KiDisplayAcquireProviderMutex(&providerHeld))) + return; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + int x = 0; + int y = 0; + bool visible = false; + bool drawn = false; + bool suspended = false; + uint64_t displayGeneration = 0; + uint64_t requestGeneration = 0; + int committedX = 0; + int committedY = 0; + bool hadPendingWork = false; + + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + hadPendingWork = gCursorUpdatePending || (gCursorRequestGeneration != gCursorAppliedGeneration); + if (!hadPendingWork) { + KeReleaseSpinlock(&gDisplayLock, flags); + KiDisplayReleaseProviderMutex(providerHeld); + return; + } + + gCursorUpdatePending = false; + if (KiDisplayHasDevice()) { + device = gPrimaryDisplay; + ops = gPrimaryDisplay->Ops; + caps = gPrimaryDisplay->Caps; + x = gCursorX; + y = gCursorY; + visible = gCursorVisible; + drawn = gCursorDrawn; + suspended = KiDisplayOutputSuspended() || gDisplayTransitionInProgress; + displayGeneration = gDisplayGeneration; + requestGeneration = gCursorRequestGeneration; + committedX = gCursorCommittedX; + committedY = gCursorCommittedY; + } + KeReleaseSpinlock(&gDisplayLock, flags); + + bool newDrawn = false; + bool positionCommitted = false; + int newCommittedX = committedX; + int newCommittedY = committedY; + KSTATUS status = STATUS_SUCCESS; + bool hasCursorOps = device && ops && ops->ShowCursor && + (caps & (DISPLAY_CAP_SOFTWARE_CURSOR | DISPLAY_CAP_HARDWARE_CURSOR)); + bool softwareCursor = hasCursorOps && (caps & DISPLAY_CAP_SOFTWARE_CURSOR) != 0; + bool hardwareCursor = hasCursorOps && (caps & DISPLAY_CAP_HARDWARE_CURSOR) != 0; + + if (hasCursorOps) { + if (!visible || suspended) { + if (drawn) + status = ops->ShowCursor(device, false); + newDrawn = false; + if (KSUCCESS(status)) { + positionCommitted = true; + newCommittedX = DISPLAY_CURSOR_POS_INVALID; + newCommittedY = DISPLAY_CURSOR_POS_INVALID; + } + } else if (hardwareCursor) { + // Hardware cursors are independent of framebuffer drawing. Commit + // position only when the logical cursor request changed or the + // provider has not yet been synchronized. This prevents stale or + // duplicate work items from hammering device cursor registers. + bool needPosition = !drawn || x != committedX || y != committedY; + if (needPosition && ops->SetCursorPosition) { + status = ops->SetCursorPosition(device, x, y); + if (KSUCCESS(status)) { + positionCommitted = true; + newCommittedX = x; + newCommittedY = y; + } + } else { + positionCommitted = true; + newCommittedX = committedX; + newCommittedY = committedY; + } + + if (KSUCCESS(status)) { + if (!drawn) + status = ops->ShowCursor(device, true); + newDrawn = KSUCCESS(status); + } + } else if (softwareCursor) { + if (drawn) + (void)ops->ShowCursor(device, false); + if (ops->SetCursorPosition) + (void)ops->SetCursorPosition(device, x, y); + status = ops->ShowCursor(device, true); + newDrawn = KSUCCESS(status); + if (newDrawn) { + positionCommitted = true; + newCommittedX = x; + newCommittedY = y; + } + } + } + + flags = KeAcquireSpinlock(&gDisplayLock); + if (displayGeneration == gDisplayGeneration && device == gPrimaryDisplay) { + gCursorDrawn = newDrawn; + if (positionCommitted) { + gCursorCommittedX = newCommittedX; + gCursorCommittedY = newCommittedY; + } + // Do not spin forever on provider failure. A later explicit cursor + // request, show/hide request, or display-generation change will retry. + gCursorAppliedGeneration = requestGeneration; + if (gCursorRequestGeneration != requestGeneration) + gCursorUpdatePending = true; + } else { + gCursorUpdatePending = true; + } + bool requeue = gCursorUpdatePending && !gDisplayTransitionInProgress; + KeReleaseSpinlock(&gDisplayLock, flags); + + KiDisplayReleaseProviderMutex(providerHeld); + + if (requeue) + KiDisplayQueueCursorWork(); +} + // Clips a rectangle to a display mode so providers never see negative or out-of-bounds regions. static bool KiDisplayClipRect(const DISPLAY_MODE* mode, DISPLAY_RECT* rect) { if (!mode || !rect || rect->W <= 0 || rect->H <= 0) @@ -141,6 +349,350 @@ static bool KiDisplayClipRect(const DISPLAY_MODE* mode, DISPLAY_RECT* rect) { return true; } + +// Snapshots the current primary under the manager spinlock. The caller must +// hold the provider mutex before invoking callbacks through the returned ops; +// KeDisplaySetPrimary takes the same mutex, so the provider object cannot be +// switched while the callback sequence is in progress. +static bool KiDisplaySnapshotPrimary(DISPLAY_DEVICE** outDevice, + const DISPLAY_DEVICE_OPS** outOps, + uint32_t* outCaps, + uint64_t* outGeneration) { + if (outDevice) *outDevice = (DISPLAY_DEVICE*)0; + if (outOps) *outOps = (const DISPLAY_DEVICE_OPS*)0; + if (outCaps) *outCaps = 0; + if (outGeneration) *outGeneration = 0; + + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + DISPLAY_DEVICE* device = KiDisplayHasDevice() ? gPrimaryDisplay : (DISPLAY_DEVICE*)0; + if (device) { + if (outDevice) *outDevice = device; + if (outOps) *outOps = device->Ops; + if (outCaps) *outCaps = device->Caps; + if (outGeneration) *outGeneration = gDisplayGeneration; + } + KeReleaseSpinlock(&gDisplayLock, flags); + return device != (DISPLAY_DEVICE*)0; +} + +static bool KiDisplayConsoleGridFromMode(const DISPLAY_MODE* mode, int* outCols, int* outRows) { + if (!mode) + return false; + + int cols = (int)mode->Width / FONT_W; + int rows = (int)mode->Height / FONT_H; + if (cols > DISPLAY_CONSOLE_MAX_COLS) cols = DISPLAY_CONSOLE_MAX_COLS; + if (rows > DISPLAY_CONSOLE_MAX_ROWS) rows = DISPLAY_CONSOLE_MAX_ROWS; + if (cols <= 0 || rows <= 0) + return false; + + if (outCols) *outCols = cols; + if (outRows) *outRows = rows; + return true; +} + +static void KiDisplayAddDirtyForModeLocked(const DISPLAY_MODE* mode, int x, int y, int w, int h) { + if (!mode || gDirtyAll || w <= 0 || h <= 0) + return; + + DISPLAY_RECT rect = { x, y, w, h }; + if (!KiDisplayClipRect(mode, &rect)) + return; + + if (rect.X == 0 && rect.Y == 0 && + rect.W == (int32_t)mode->Width && rect.H == (int32_t)mode->Height) { + KiDisplayDirtyAll(); + return; + } + + if (gDirtyRectCount >= DISPLAY_MAX_DIRTY_RECTS) { + KiDisplayDirtyAll(); + return; + } + + gDisplayStats.DirtyRectCount++; + gDirtyRects[gDirtyRectCount++] = rect; +} + +static void KiDisplayRecordBlitLocked(const DISPLAY_MODE* mode, int x, int y, int w, int h) { + gDisplayStats.BlitCount++; + gDisplayStats.BytesCopied += (uint64_t)(uint32_t)w * (uint64_t)(uint32_t)h * sizeof(uint32_t); + KiDisplayAddDirtyForModeLocked(mode, x, y, w, h); +} + +static void KiDisplayRecordFillLocked(const DISPLAY_MODE* mode, int x, int y, int w, int h) { + gDisplayStats.FillRectCount++; + gDisplayStats.BytesCopied += (uint64_t)(uint32_t)w * (uint64_t)(uint32_t)h * sizeof(uint32_t); + KiDisplayAddDirtyForModeLocked(mode, x, y, w, h); +} + +static void KiDisplayRecordCopyLocked(const DISPLAY_MODE* mode, int dstX, int dstY, int w, int h) { + gDisplayStats.CopyRectCount++; + gDisplayStats.BytesCopied += (uint64_t)(uint32_t)w * (uint64_t)(uint32_t)h * sizeof(uint32_t); + KiDisplayAddDirtyForModeLocked(mode, dstX, dstY, w, h); +} + +static KSTATUS KiDisplayFlushDirtyWithSnapshot(DISPLAY_DEVICE* device, const DISPLAY_DEVICE_OPS* ops) { + if (!device || !ops || !ops->Present) + return STATUS_SUCCESS; + + DISPLAY_RECT localRects[DISPLAY_MAX_DIRTY_RECTS]; + DISPLAY_RECT fullRect; + const DISPLAY_RECT* rects = localRects; + size_t count = 0; + bool dirtyAll = false; + + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device != gPrimaryDisplay || (!gDirtyAll && gDirtyRectCount == 0)) { + KeReleaseSpinlock(&gDisplayLock, flags); + return STATUS_SUCCESS; + } + + dirtyAll = gDirtyAll; + if (!dirtyAll) { + count = gDirtyRectCount; + if (count > DISPLAY_MAX_DIRTY_RECTS) + count = DISPLAY_MAX_DIRTY_RECTS; + for (size_t i = 0; i < count; i++) + localRects[i] = gDirtyRects[i]; + } + KeReleaseSpinlock(&gDisplayLock, flags); + + if (dirtyAll) { + DISPLAY_MODE mode; + if (!ops->GetMode) + return STATUS_NOT_SUPPORTED; + KSTATUS ms = ops->GetMode(device, &mode); + if (!KSUCCESS(ms)) + return ms; + fullRect.X = 0; + fullRect.Y = 0; + fullRect.W = (int32_t)mode.Width; + fullRect.H = (int32_t)mode.Height; + rects = &fullRect; + count = 1; + } + + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + gDisplayStats.PresentCount++; + gDisplayStats.PresentRectCount += count; + } + KeReleaseSpinlock(&gDisplayLock, flags); + + KSTATUS s = ops->Present(device, rects, count); + if (KSUCCESS(s)) { + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + gDirtyAll = false; + gDirtyRectCount = 0; + } + KeReleaseSpinlock(&gDisplayLock, flags); + } + return s; +} + +static bool KiDisplayHideCursorForDrawSnapshot(DISPLAY_DEVICE* device, + const DISPLAY_DEVICE_OPS* ops, + uint32_t caps) { + if (!device || !ops || !ops->ShowCursor || !(caps & DISPLAY_CAP_SOFTWARE_CURSOR)) + return false; + + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + bool hide = (device == gPrimaryDisplay) && gCursorDrawn; + if (hide) + gCursorDrawn = false; + KeReleaseSpinlock(&gDisplayLock, flags); + + if (hide) + (void)ops->ShowCursor(device, false); + return hide; +} + +static void KiDisplayRestoreCursorAfterDrawSnapshot(DISPLAY_DEVICE* device, + const DISPLAY_DEVICE_OPS* ops, + uint32_t caps) { + // Only software cursors are hidden/restored around framebuffer drawing. + // Hardware cursors are owned by the provider/device and must not be + // re-positioned after every blit/present; the coalesced cursor worker is + // the only path that commits hardware cursor movement. + if (!device || !ops || !ops->ShowCursor || !(caps & DISPLAY_CAP_SOFTWARE_CURSOR)) + return; + + int x = 0; + int y = 0; + bool shouldShow = false; + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + shouldShow = (device == gPrimaryDisplay) && + gCursorVisible && !gCursorDrawn && + !KiDisplayOutputSuspended() && + !gDisplayTransitionInProgress; + if (shouldShow) { + x = gCursorX; + y = gCursorY; + } + KeReleaseSpinlock(&gDisplayLock, flags); + + if (!shouldShow) + return; + + if (ops->SetCursorPosition) + (void)ops->SetCursorPosition(device, x, y); + KSTATUS s = ops->ShowCursor(device, true); + + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay && KSUCCESS(s)) { + gCursorDrawn = true; + gCursorCommittedX = x; + gCursorCommittedY = y; + gCursorAppliedGeneration = gCursorRequestGeneration; + gCursorUpdatePending = false; + } else if (device != gPrimaryDisplay) { + gCursorUpdatePending = true; + } + bool requeue = gCursorUpdatePending && !gDisplayTransitionInProgress; + KeReleaseSpinlock(&gDisplayLock, flags); + + if (requeue) + KiDisplayQueueCursorWork(); +} + +static KSTATUS KiDisplayGetProviderSnapshot(DISPLAY_DEVICE** outDevice, + const DISPLAY_DEVICE_OPS** outOps, + uint32_t* outCaps, + DISPLAY_MODE* outMode) { + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + + if (!KiDisplaySnapshotPrimary(&device, &ops, &caps, 0) || !device || !ops) + return STATUS_NOT_SUPPORTED; + if (!ops->GetMode) + return STATUS_NOT_SUPPORTED; + + KSTATUS s = ops->GetMode(device, outMode); + if (!KSUCCESS(s)) + return s; + + if (outDevice) *outDevice = device; + if (outOps) *outOps = ops; + if (outCaps) *outCaps = caps; + return STATUS_SUCCESS; +} + +static void KiDisplayBuildGlyphPixels(char c, uint32_t fg, uint32_t bg, uint32_t* outPixels) { + const uint8_t* glyph = Font8x16 + ((uint8_t)c * 16u); + for (int row = 0; row < FONT_H; row++) { + uint8_t bits = glyph[row]; + for (int col = 0; col < FONT_W; col++) + outPixels[row * FONT_W + col] = (bits & (0x80u >> col)) ? fg : bg; + } +} + +static KSTATUS KiDisplayBlitGlyphSnapshot(DISPLAY_DEVICE* device, + const DISPLAY_DEVICE_OPS* ops, + const DISPLAY_MODE* mode, + char c, + int x, + int y, + uint32_t fg, + uint32_t bg) { + if (!device || !ops || !ops->Blit || !mode) + return STATUS_NOT_SUPPORTED; + + uint32_t glyphPixels[FONT_W * FONT_H]; + KiDisplayBuildGlyphPixels(c, fg, bg, glyphPixels); + KSTATUS s = ops->Blit(device, x, y, FONT_W, FONT_H, glyphPixels); + if (KSUCCESS(s)) { + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) + KiDisplayRecordBlitLocked(mode, x, y, FONT_W, FONT_H); + KeReleaseSpinlock(&gDisplayLock, flags); + } + return s; +} + +static KSTATUS KiDisplayDrawBlitSnapshot(DISPLAY_DEVICE* device, + const DISPLAY_DEVICE_OPS* ops, + const DISPLAY_MODE* mode, + int x, + int y, + int w, + int h, + const uint32_t* pixels) { + if (!pixels || w <= 0 || h <= 0) + return STATUS_INVALID_PARAMETER; + if (!device || !ops || !ops->Blit || !mode) + return STATUS_NOT_SUPPORTED; + + KSTATUS s = ops->Blit(device, x, y, w, h, pixels); + if (KSUCCESS(s)) { + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) + KiDisplayRecordBlitLocked(mode, x, y, w, h); + KeReleaseSpinlock(&gDisplayLock, flags); + } + return s; +} + +static KSTATUS KiDisplayFillRectSnapshot(DISPLAY_DEVICE* device, + const DISPLAY_DEVICE_OPS* ops, + const DISPLAY_MODE* mode, + int x, + int y, + int w, + int h, + uint32_t color) { + if (w <= 0 || h <= 0) + return STATUS_INVALID_PARAMETER; + if (!device || !ops || !ops->FillRect || !mode) + return STATUS_NOT_SUPPORTED; + + KSTATUS s = ops->FillRect(device, x, y, w, h, color); + if (KSUCCESS(s)) { + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) + KiDisplayRecordFillLocked(mode, x, y, w, h); + KeReleaseSpinlock(&gDisplayLock, flags); + } + return s; +} + +static KSTATUS KiDisplayCopyRectSnapshot(DISPLAY_DEVICE* device, + const DISPLAY_DEVICE_OPS* ops, + const DISPLAY_MODE* mode, + int dstX, + int dstY, + int srcX, + int srcY, + int w, + int h) { + if (w <= 0 || h <= 0) + return STATUS_INVALID_PARAMETER; + if (!device || !ops || !ops->CopyRect || !mode) + return STATUS_NOT_SUPPORTED; + + KSTATUS s = ops->CopyRect(device, dstX, dstY, srcX, srcY, w, h); + if (KSUCCESS(s)) { + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) + KiDisplayRecordCopyLocked(mode, dstX, dstY, w, h); + KeReleaseSpinlock(&gDisplayLock, flags); + } + return s; +} + +static KSTATUS KiDisplayFinishProviderDraw(DISPLAY_DEVICE* device, + const DISPLAY_DEVICE_OPS* ops, + uint32_t caps, + KSTATUS drawStatus) { + KSTATUS presentStatus = STATUS_SUCCESS; + if (KSUCCESS(drawStatus)) + presentStatus = KiDisplayFlushDirtyWithSnapshot(device, ops); + KiDisplayRestoreCursorAfterDrawSnapshot(device, ops, caps); + return KSUCCESS(drawStatus) ? presentStatus : drawStatus; +} + // Implements the display Dirty All operation. static void KiDisplayDirtyAll(void) { gDisplayStats.DirtyAllCount++; @@ -437,6 +989,17 @@ void KeDisplayInitialize(const HAL_API* hal) { gCursorY = 0; gCursorDrawX = -1; gCursorDrawY = -1; + gCursorUpdatePending = false; + gCursorRequestGeneration = 0; + gCursorAppliedGeneration = 0; + gCursorCommittedX = DISPLAY_CURSOR_POS_INVALID; + gCursorCommittedY = DISPLAY_CURSOR_POS_INVALID; + gDisplayTransitionInProgress = false; + gDisplayGeneration = 1; + if (!gDisplayProviderMutex) + (void)KeMutexCreate(&gDisplayProviderMutex); + KeInitializeWorkItem(&gDisplayCursorWorkItem, KiDisplayCursorWorker, 0); + _InterlockedExchange(&gDisplayCursorWorkInitialized, 1); gConsoleX = 0; gConsoleY = 0; KiDisplayConsoleResetCellsLocked(); @@ -572,12 +1135,8 @@ DISPLAY_API KSTATUS KeDisplaySetPrimary(DISPLAY_DEVICE* device) { return STATUS_INVALID_PARAMETER; const char* name = device->Name ? device->Name : "?"; - KSTATUS result = STATUS_SUCCESS; - KSTATUS seedResult = STATUS_SUCCESS; - const char* seedOldName = NULL; uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - bool found = false; for (DISPLAY_DEVICE* it = gDisplayList; it; it = it->Next) { if (it == device) { @@ -591,51 +1150,96 @@ DISPLAY_API KSTATUS KeDisplaySetPrimary(DISPLAY_DEVICE* device) { } DISPLAY_DEVICE* oldDevice = gPrimaryDisplay; - bool cursorWasVisible = gCursorVisible; + bool oldCursorDrawn = gCursorDrawn; + gDisplayTransitionInProgress = true; + gDisplayGeneration++; + gCursorUpdatePending = gCursorVisible; + KeReleaseSpinlock(&gDisplayLock, flags); - if (oldDevice && gCursorDrawn && oldDevice->Ops && oldDevice->Ops->ShowCursor) - (void)oldDevice->Ops->ShowCursor(oldDevice, false); - - if (oldDevice && oldDevice != device) { - seedOldName = oldDevice->Name; - seedResult = KiDisplaySeedNewPrimary(oldDevice, device); + bool providerHeld = false; + KSTATUS result = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(result)) { + flags = KeAcquireSpinlock(&gDisplayLock); + gDisplayTransitionInProgress = false; + gDisplayGeneration++; + KeReleaseSpinlock(&gDisplayLock, flags); + KiDisplayQueueCursorWork(); + return result; } - gPrimaryDisplay = device; - gCursorDrawn = false; - gDirtyAll = false; - gDirtyRectCount = 0; + if (oldDevice && oldCursorDrawn && oldDevice->Ops && oldDevice->Ops->ShowCursor) + (void)oldDevice->Ops->ShowCursor(oldDevice, false); - if (device->Ops->Present) { + // Clean architecture policy: handoff does not read back/copy the old boot + // framebuffer. The new primary is cleared to the kernel console blue and + // user/session code redraws state above the provider boundary. + if (device->Ops->GetMode && device->Ops->FillRect) { DISPLAY_MODE mode; - if (KSUCCESS(device->Ops->GetMode(device, &mode))) { - DISPLAY_RECT full = { 0, 0, (int32_t)mode.Width, (int32_t)mode.Height }; - (void)device->Ops->Present(device, &full, 1); + KSTATUS modeStatus = device->Ops->GetMode(device, &mode); + if (KSUCCESS(modeStatus)) { + (void)device->Ops->FillRect(device, 0, 0, + (int)mode.Width, + (int)mode.Height, + DISPLAY_DEFAULT_BG); + if (device->Ops->Present) { + DISPLAY_RECT full = { 0, 0, (int32_t)mode.Width, (int32_t)mode.Height }; + (void)device->Ops->Present(device, &full, 1); + } } } - if (cursorWasVisible) - KiDisplayRestoreCursorAfterDraw(); + flags = KeAcquireSpinlock(&gDisplayLock); + gPrimaryDisplay = device; + gCursorDrawn = false; + gCursorCommittedX = DISPLAY_CURSOR_POS_INVALID; + gCursorCommittedY = DISPLAY_CURSOR_POS_INVALID; + gDirtyAll = false; + gDirtyRectCount = 0; + gDrawDepth = 0; + // The provider handoff clears the framebuffer instead of preserving boot + // text. Keep the console model coherent with that clear so subsequent + // kernel/debug output restarts at the top-left of the new primary. + KiDisplayConsoleResetCellsLocked(); + gConsoleX = 0; + gConsoleY = 0; + + gDisplayTransitionInProgress = false; + gDisplayGeneration++; + bool needCursor = gCursorVisible; + gCursorUpdatePending = needCursor; + if (needCursor) + gCursorRequestGeneration++; KeReleaseSpinlock(&gDisplayLock, flags); - if (!KSUCCESS(seedResult) && seedResult != STATUS_NOT_SUPPORTED) { - DbgPrint("Display: primary handoff seed %s -> %s failed st=0x%x\n", - seedOldName ? seedOldName : "?", name, seedResult); - } - if (KSUCCESS(result)) - DbgPrint("Display: primary is %s\n", name); + KiDisplayReleaseProviderMutex(providerHeld); + + if (needCursor) + KiDisplayQueueCursorWork(); + + DbgPrint("Display: primary is %s\n", name); return result; } DISPLAY_API KSTATUS KeDisplayGetMode(DISPLAY_MODE* outMode) { if (!outMode) return STATUS_INVALID_PARAMETER; + + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KSTATUS s = (!KiDisplayHasDevice() || !gPrimaryDisplay->Ops->GetMode) - ? STATUS_INVALID_PARAMETER - : gPrimaryDisplay->Ops->GetMode(gPrimaryDisplay, outMode); + DISPLAY_DEVICE* device = KiDisplayHasDevice() ? gPrimaryDisplay : (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = device ? device->Ops : (const DISPLAY_DEVICE_OPS*)0; KeReleaseSpinlock(&gDisplayLock, flags); + + s = (!device || !ops || !ops->GetMode) + ? STATUS_INVALID_PARAMETER + : ops->GetMode(device, outMode); + + KiDisplayReleaseProviderMutex(providerHeld); return s; } @@ -649,120 +1253,308 @@ DISPLAY_API KSTATUS KeDisplayGetSize(int* outWidth, int* outHeight) { return STATUS_SUCCESS; } -DISPLAY_API KSTATUS KeDisplayBlit(int x, int y, int w, int h, const uint32_t* pixels) { +DISPLAY_API KSTATUS KeDisplaySync(void) { + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KiDisplayBeginDraw(); - KSTATUS s = KiDisplayDrawBlitNoCursor(x, y, w, h, pixels); - KSTATUS ps = KiDisplayEndDraw(); + DISPLAY_DEVICE* device = KiDisplayHasDevice() ? gPrimaryDisplay : (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = device ? device->Ops : (const DISPLAY_DEVICE_OPS*)0; KeReleaseSpinlock(&gDisplayLock, flags); - return KSUCCESS(s) ? ps : s; + + if (!device || !ops || !ops->Sync) { + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_NOT_SUPPORTED; + } + + s = ops->Sync(device); + + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) + gDisplayStats.SyncCount++; + KeReleaseSpinlock(&gDisplayLock, flags); + + KiDisplayReleaseProviderMutex(providerHeld); + return s; +} + +DISPLAY_API KSTATUS KeDisplayQueryInfo(KE_DISPLAY_INFO* outInfo) { + if (!outInfo) + return STATUS_INVALID_PARAMETER; + + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + KE_DISPLAY_INFO info; + memset(&info, 0, sizeof(info)); + + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + + uint32_t count = 0; + for (DISPLAY_DEVICE* it = gDisplayList; it; it = it->Next) + count++; + info.DeviceCount = count; + info.ManagerStats = gDisplayStats; + + DISPLAY_DEVICE* device = KiDisplayHasDevice() ? gPrimaryDisplay : (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = device ? device->Ops : (const DISPLAY_DEVICE_OPS*)0; + if (device) { + const char* name = device->Name ? device->Name : "?"; + size_t len = strlen(name); + if (len >= sizeof(info.PrimaryName)) + len = sizeof(info.PrimaryName) - 1; + memcpy(info.PrimaryName, name, len); + info.PrimaryName[len] = '\0'; + info.Caps = device->Caps; + } + + KeReleaseSpinlock(&gDisplayLock, flags); + + if (!device || !ops) { + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_NOT_FOUND; + } + + if (ops->GetMode) { + s = ops->GetMode(device, &info.Mode); + if (!KSUCCESS(s)) { + KiDisplayReleaseProviderMutex(providerHeld); + return s; + } + } + + if (ops->QueryStats) + (void)ops->QueryStats(device, &info.DeviceStats); + + *outInfo = info; + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_SUCCESS; +} + +DISPLAY_API KSTATUS KeDisplayBlit(int x, int y, int w, int h, const uint32_t* pixels) { + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (KSUCCESS(s)) { + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + s = KiDisplayDrawBlitSnapshot(device, ops, &mode, x, y, w, h, pixels); + s = KiDisplayFinishProviderDraw(device, ops, caps, s); + } + + KiDisplayReleaseProviderMutex(providerHeld); + return s; +} + +DISPLAY_API KSTATUS KeDisplayBlitRows(int x, int y, int w, int h, KE_DISPLAY_ROW_SOURCE source, void* context, uint32_t* scratch) { + if (!source || !scratch || w <= 0 || h <= 0) + return STATUS_INVALID_PARAMETER; + + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (KSUCCESS(s)) { + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + for (int row = 0; row < h; ++row) { + s = source(context, row, scratch, (size_t)w); + if (!KSUCCESS(s)) + break; + s = KiDisplayDrawBlitSnapshot(device, ops, &mode, x, y + row, w, 1, scratch); + if (!KSUCCESS(s)) + break; + } + s = KiDisplayFinishProviderDraw(device, ops, caps, s); + } + + KiDisplayReleaseProviderMutex(providerHeld); + return s; } DISPLAY_API KSTATUS KeDisplayFillRect(int x, int y, int w, int h, uint32_t color) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KiDisplayBeginDraw(); - KSTATUS s = KiDisplayFillRectNoCursor(x, y, w, h, color); - KSTATUS ps = KiDisplayEndDraw(); - KeReleaseSpinlock(&gDisplayLock, flags); - return KSUCCESS(s) ? ps : s; -} - -// Implements the display Clear Locked operation. -static KSTATUS KiDisplayClearLocked(uint32_t color) { - DISPLAY_MODE mode; - if (!KiDisplayHasDevice() || !gPrimaryDisplay->Ops->GetMode) - return STATUS_NOT_SUPPORTED; - KSTATUS s = gPrimaryDisplay->Ops->GetMode(gPrimaryDisplay, &mode); + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); if (!KSUCCESS(s)) return s; - KiDisplayBeginDraw(); - s = KiDisplayFillRectNoCursor(0, 0, (int)mode.Width, (int)mode.Height, color); + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); if (KSUCCESS(s)) { - KiDisplayConsoleResetCellsLocked(); - gConsoleX = 0; - gConsoleY = 0; + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + s = KiDisplayFillRectSnapshot(device, ops, &mode, x, y, w, h, color); + s = KiDisplayFinishProviderDraw(device, ops, caps, s); } - KSTATUS ps = KiDisplayEndDraw(); - return KSUCCESS(s) ? ps : s; + + KiDisplayReleaseProviderMutex(providerHeld); + return s; } DISPLAY_API KSTATUS KeDisplayClear(uint32_t color) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KSTATUS s = KiDisplayClearLocked(color); - KeReleaseSpinlock(&gDisplayLock, flags); + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (KSUCCESS(s)) { + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + s = KiDisplayFillRectSnapshot(device, ops, &mode, 0, 0, (int)mode.Width, (int)mode.Height, color); + if (KSUCCESS(s)) { + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + KiDisplayConsoleResetCellsLocked(); + gConsoleX = 0; + gConsoleY = 0; + } + KeReleaseSpinlock(&gDisplayLock, flags); + } + s = KiDisplayFinishProviderDraw(device, ops, caps, s); + } + + KiDisplayReleaseProviderMutex(providerHeld); return s; } DISPLAY_API KSTATUS KeDisplayCopyRect(int dstX, int dstY, int srcX, int srcY, int w, int h) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KiDisplayBeginDraw(); - KSTATUS s = KiDisplayCopyRectNoCursor(dstX, dstY, srcX, srcY, w, h); - KSTATUS ps = KiDisplayEndDraw(); - KeReleaseSpinlock(&gDisplayLock, flags); - return KSUCCESS(s) ? ps : s; -} - -// Implements the display Scroll Locked operation. -static KSTATUS KiDisplayScrollLocked(int lines) { - if (lines <= 0) - return STATUS_SUCCESS; - - if (!KiDisplayHasDevice() || !gPrimaryDisplay->Ops->GetMode) - return STATUS_NOT_SUPPORTED; - DISPLAY_MODE mode; - KSTATUS s = gPrimaryDisplay->Ops->GetMode(gPrimaryDisplay, &mode); + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); if (!KSUCCESS(s)) return s; - if ((uint32_t)lines >= mode.Height) - return KiDisplayClearLocked(DISPLAY_DEFAULT_BG); - KiDisplayBeginDraw(); - s = KiDisplayCopyRectNoCursor(0, 0, 0, lines, (int)mode.Width, (int)(mode.Height - (uint32_t)lines)); + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); if (KSUCCESS(s)) { - s = KiDisplayFillRectNoCursor(0, (int)(mode.Height - (uint32_t)lines), - (int)mode.Width, lines, DISPLAY_DEFAULT_BG); + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + s = KiDisplayCopyRectSnapshot(device, ops, &mode, dstX, dstY, srcX, srcY, w, h); + s = KiDisplayFinishProviderDraw(device, ops, caps, s); } - KSTATUS ps = KiDisplayEndDraw(); - return KSUCCESS(s) ? ps : s; + + KiDisplayReleaseProviderMutex(providerHeld); + return s; } DISPLAY_API KSTATUS KeDisplayScroll(int lines) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KSTATUS s = KiDisplayScrollLocked(lines); - KeReleaseSpinlock(&gDisplayLock, flags); - return s; -} + if (lines <= 0) + return STATUS_SUCCESS; -// Implements the display Put Char Locked operation. -static KSTATUS KiDisplayPutCharLocked(char c, int x, int y, uint32_t fg, uint32_t bg) { - KiDisplayBeginDraw(); - KSTATUS s = KiDisplayDrawCharPixelsLocked(c, x, y, fg, bg); - KSTATUS ps = KiDisplayEndDraw(); - return KSUCCESS(s) ? ps : s; + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (KSUCCESS(s)) { + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + if ((uint32_t)lines >= mode.Height) { + s = KiDisplayFillRectSnapshot(device, ops, &mode, 0, 0, (int)mode.Width, (int)mode.Height, DISPLAY_DEFAULT_BG); + if (KSUCCESS(s)) { + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + KiDisplayConsoleResetCellsLocked(); + gConsoleX = 0; + gConsoleY = 0; + } + KeReleaseSpinlock(&gDisplayLock, flags); + } + } else { + s = KiDisplayCopyRectSnapshot(device, ops, &mode, 0, 0, 0, lines, + (int)mode.Width, (int)(mode.Height - (uint32_t)lines)); + if (KSUCCESS(s)) { + s = KiDisplayFillRectSnapshot(device, ops, &mode, 0, + (int)(mode.Height - (uint32_t)lines), + (int)mode.Width, lines, DISPLAY_DEFAULT_BG); + } + } + s = KiDisplayFinishProviderDraw(device, ops, caps, s); + } + + KiDisplayReleaseProviderMutex(providerHeld); + return s; } DISPLAY_API KSTATUS KeDisplayPutChar(char c, int x, int y, uint32_t fg, uint32_t bg) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KSTATUS s = KiDisplayPutCharLocked(c, x, y, fg, bg); - KeReleaseSpinlock(&gDisplayLock, flags); + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (KSUCCESS(s)) { + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + s = KiDisplayBlitGlyphSnapshot(device, ops, &mode, c, x, y, fg, bg); + s = KiDisplayFinishProviderDraw(device, ops, caps, s); + } + + KiDisplayReleaseProviderMutex(providerHeld); return s; } -DISPLAY_API KSTATUS KeDisplayPrintChar(char c, uint32_t fg, uint32_t bg) { - KiDisplaySerialWriteChar(c); +static KSTATUS KiDisplayPrintCharInternal(char c, uint32_t fg, uint32_t bg, bool mirrorSerial) { + if (mirrorSerial) + KiDisplaySerialWriteChar(c); - if (KiDisplayOutputSuspended()) + if (KiDisplayOutputSuspended()) { + if (!KeCanWait()) + return KiDisplayFallbackPrintChar(c, fg, bg); return STATUS_SUCCESS; + } - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + // Once the scheduler/work-queue era begins, display provider callbacks are + // PASSIVE_LEVEL-only. DbgPrint queues screen output through the work queue; + // panic and explicit console paths may still use the serial mirroring entry. + // A panic has interrupts disabled and cannot take the provider mutex, so it + // falls back to the boot framebuffer writer instead of disappearing from + // the screen while still printing to COM1. + if (KiDisplayProviderCallbacksRequirePassive() && !KeCanWait()) + return KiDisplayFallbackPrintChar(c, fg, bg); + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; DISPLAY_MODE mode; - KSTATUS s = (!KiDisplayHasDevice() || !gPrimaryDisplay->Ops->GetMode) - ? STATUS_NOT_SUPPORTED - : gPrimaryDisplay->Ops->GetMode(gPrimaryDisplay, &mode); + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); if (!KSUCCESS(s)) { + KiDisplayReleaseProviderMutex(providerHeld); const HAL_API* hal = gDisplayHal; - KeReleaseSpinlock(&gDisplayLock, flags); if (hal && hal->Screen.PrintChar) hal->Screen.PrintChar(c, fg, bg); return s; @@ -770,11 +1562,19 @@ DISPLAY_API KSTATUS KeDisplayPrintChar(char c, uint32_t fg, uint32_t bg) { int cols = 0; int rows = 0; - if (!KiDisplayGetConsoleGridLocked(&cols, &rows)) { - KeReleaseSpinlock(&gDisplayLock, flags); + if (!KiDisplayConsoleGridFromMode(&mode, &cols, &rows)) { + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_DEVICE_ERROR; } + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + + bool drawChar = false; + bool needScroll = false; + int drawCol = 0; + int drawRow = 0; + + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); if (c == '\n') { gConsoleX = 0; gConsoleY++; @@ -788,243 +1588,339 @@ DISPLAY_API KSTATUS KeDisplayPrintChar(char c, uint32_t fg, uint32_t bg) { gConsoleX = cols - 1; } } else { - s = KiDisplayPutCharLocked(c, gConsoleX * FONT_W, gConsoleY * FONT_H, fg, bg); - if (!KSUCCESS(s)) { - KeReleaseSpinlock(&gDisplayLock, flags); - return s; - } - if (gConsoleX >= 0 && gConsoleX < cols && gConsoleY >= 0 && gConsoleY < rows) { - size_t i = KiDisplayConsoleIndex(gConsoleX, gConsoleY); - gConsoleCells[i] = c; - gConsoleAttrs[i] = (uint8_t)gConsoleAttribute; - } + drawChar = true; + drawCol = gConsoleX; + drawRow = gConsoleY; if (++gConsoleX >= cols) { gConsoleX = 0; gConsoleY++; } } - if (gConsoleY >= rows) { - s = KiDisplayScrollLocked(FONT_H); - if (!KSUCCESS(s)) { + if (gConsoleY >= rows) + needScroll = true; + KeReleaseSpinlock(&gDisplayLock, flags); + + if (drawChar) { + s = KiDisplayBlitGlyphSnapshot(device, ops, &mode, c, drawCol * FONT_W, drawRow * FONT_H, fg, bg); + if (KSUCCESS(s)) { + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay && drawCol >= 0 && drawCol < cols && drawRow >= 0 && drawRow < rows) { + size_t i = KiDisplayConsoleIndex(drawCol, drawRow); + gConsoleCells[i] = c; + gConsoleAttrs[i] = (uint8_t)gConsoleAttribute; + } KeReleaseSpinlock(&gDisplayLock, flags); - return s; } - KiDisplayConsoleScrollCellsLocked(cols, rows, 1); - gConsoleY = rows - 1; + } else { + s = STATUS_SUCCESS; } - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; + if (KSUCCESS(s) && needScroll) { + s = KiDisplayCopyRectSnapshot(device, ops, &mode, 0, 0, 0, FONT_H, + (int)mode.Width, (int)(mode.Height - FONT_H)); + if (KSUCCESS(s)) { + s = KiDisplayFillRectSnapshot(device, ops, &mode, 0, + (int)(mode.Height - FONT_H), + (int)mode.Width, FONT_H, DISPLAY_DEFAULT_BG); + } + if (KSUCCESS(s)) { + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + KiDisplayConsoleScrollCellsLocked(cols, rows, 1); + gConsoleY = rows - 1; + if (gConsoleX < 0) gConsoleX = 0; + if (gConsoleX >= cols) gConsoleX = cols - 1; + } + KeReleaseSpinlock(&gDisplayLock, flags); + } + } + + s = KiDisplayFinishProviderDraw(device, ops, caps, s); + KiDisplayReleaseProviderMutex(providerHeld); + return s; +} + +DISPLAY_API KSTATUS KeDisplayPrintChar(char c, uint32_t fg, uint32_t bg) { + return KiDisplayPrintCharInternal(c, fg, bg, true); } // Implements the display Debug Print Char operation. void KeDisplayDebugPrintChar(char c, uint32_t fg, uint32_t bg) { - (void)KeDisplayPrintChar(c, fg, bg); + (void)KiDisplayPrintCharInternal(c, fg, bg, false); } DISPLAY_API KSTATUS KeDisplayGetConsoleInfo(KE_CONSOLE_INFO* outInfo) { if (!outInfo) return STATUS_INVALID_PARAMETER; - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - int cols = 0; - int rows = 0; - if (!KiDisplayGetConsoleGridLocked(&cols, &rows)) { - KeReleaseSpinlock(&gDisplayLock, flags); + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + (void)caps; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (!KSUCCESS(s)) { + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_NOT_SUPPORTED; } + int cols = 0; + int rows = 0; + if (!KiDisplayConsoleGridFromMode(&mode, &cols, &rows)) { + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_NOT_SUPPORTED; + } + + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); outInfo->Columns = (uint32_t)cols; outInfo->Rows = (uint32_t)rows; outInfo->CursorX = (uint32_t)gConsoleX; outInfo->CursorY = (uint32_t)gConsoleY; outInfo->Attribute = gConsoleAttribute; KeReleaseSpinlock(&gDisplayLock, flags); + + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_SUCCESS; } DISPLAY_API KSTATUS KeDisplaySetConsoleCursor(int col, int row) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + (void)caps; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (!KSUCCESS(s)) { + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_NOT_SUPPORTED; + } + int cols = 0; int rows = 0; - if (!KiDisplayGetConsoleGridLocked(&cols, &rows)) { - KeReleaseSpinlock(&gDisplayLock, flags); + if (!KiDisplayConsoleGridFromMode(&mode, &cols, &rows)) { + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_NOT_SUPPORTED; } if (col < 0 || row < 0 || col >= cols || row >= rows) { - KeReleaseSpinlock(&gDisplayLock, flags); + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_INVALID_PARAMETER; } + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); gConsoleX = col; gConsoleY = row; KeReleaseSpinlock(&gDisplayLock, flags); + + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_SUCCESS; } DISPLAY_API KSTATUS KeDisplayFillConsoleChars(char c, int col, int row, size_t length, size_t* writtenOut) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (writtenOut) *writtenOut = 0; + + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (!KSUCCESS(s)) { + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_NOT_SUPPORTED; + } + int cols = 0; int rows = 0; - if (!KiDisplayGetConsoleGridLocked(&cols, &rows)) { - KeReleaseSpinlock(&gDisplayLock, flags); + if (!KiDisplayConsoleGridFromMode(&mode, &cols, &rows)) { + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_NOT_SUPPORTED; } if (col < 0 || row < 0 || col >= cols || row >= rows) { - KeReleaseSpinlock(&gDisplayLock, flags); - if (writtenOut) *writtenOut = 0; + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_INVALID_PARAMETER; } + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + size_t written = 0; - KiDisplayBeginDraw(); - KSTATUS s = STATUS_SUCCESS; int x = col; int y = row; while (written < length && y < rows) { - size_t i = KiDisplayConsoleIndex(x, y); - uint16_t attr = gConsoleAttrs[i]; - s = KiDisplayDrawConsoleCellLocked(x, y, c, attr, true); + uint16_t attr = DISPLAY_CONSOLE_DEFAULT_ATTR; + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + size_t i = KiDisplayConsoleIndex(x, y); + attr = gConsoleAttrs[i]; + } + KeReleaseSpinlock(&gDisplayLock, flags); + + s = KiDisplayBlitGlyphSnapshot(device, ops, &mode, c, x * FONT_W, y * FONT_H, + KiDisplayConsoleFg(attr), KiDisplayConsoleBg(attr)); if (!KSUCCESS(s)) break; + + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + size_t i = KiDisplayConsoleIndex(x, y); + gConsoleCells[i] = c; + gConsoleAttrs[i] = (uint8_t)attr; + } + KeReleaseSpinlock(&gDisplayLock, flags); + written++; if (++x >= cols) { x = 0; y++; } } - KSTATUS ps = KiDisplayEndDraw(); - KeReleaseSpinlock(&gDisplayLock, flags); + KSTATUS fs = KiDisplayFinishProviderDraw(device, ops, caps, s); + KiDisplayReleaseProviderMutex(providerHeld); if (writtenOut) *writtenOut = written; - return KSUCCESS(s) ? ps : s; + return KSUCCESS(s) ? fs : s; } DISPLAY_API KSTATUS KeDisplayFillConsoleAttrs(uint16_t attr, int col, int row, size_t length, size_t* writtenOut) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (writtenOut) *writtenOut = 0; + + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (!KSUCCESS(s)) { + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_NOT_SUPPORTED; + } + int cols = 0; int rows = 0; - if (!KiDisplayGetConsoleGridLocked(&cols, &rows)) { - KeReleaseSpinlock(&gDisplayLock, flags); + if (!KiDisplayConsoleGridFromMode(&mode, &cols, &rows)) { + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_NOT_SUPPORTED; } if (col < 0 || row < 0 || col >= cols || row >= rows) { - KeReleaseSpinlock(&gDisplayLock, flags); - if (writtenOut) *writtenOut = 0; + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_INVALID_PARAMETER; } + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); + size_t written = 0; - KiDisplayBeginDraw(); - KSTATUS s = STATUS_SUCCESS; int x = col; int y = row; while (written < length && y < rows) { - size_t i = KiDisplayConsoleIndex(x, y); - char c = gConsoleCells[i]; - s = KiDisplayDrawConsoleCellLocked(x, y, c, attr, true); + char ch = ' '; + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + size_t i = KiDisplayConsoleIndex(x, y); + ch = gConsoleCells[i]; + } + KeReleaseSpinlock(&gDisplayLock, flags); + + s = KiDisplayBlitGlyphSnapshot(device, ops, &mode, ch, x * FONT_W, y * FONT_H, + KiDisplayConsoleFg(attr), KiDisplayConsoleBg(attr)); if (!KSUCCESS(s)) break; + + flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + size_t i = KiDisplayConsoleIndex(x, y); + gConsoleCells[i] = ch; + gConsoleAttrs[i] = (uint8_t)attr; + } + KeReleaseSpinlock(&gDisplayLock, flags); + written++; if (++x >= cols) { x = 0; y++; } } - KSTATUS ps = KiDisplayEndDraw(); - KeReleaseSpinlock(&gDisplayLock, flags); + KSTATUS fs = KiDisplayFinishProviderDraw(device, ops, caps, s); + KiDisplayReleaseProviderMutex(providerHeld); if (writtenOut) *writtenOut = written; - return KSUCCESS(s) ? ps : s; + return KSUCCESS(s) ? fs : s; } DISPLAY_API KSTATUS KeDisplayPresent(const DISPLAY_RECT* rects, size_t rectCount) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - if (!KiDisplayHasDevice() || !gPrimaryDisplay->Ops->Present) { - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_NOT_SUPPORTED; - } + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; - if (!rects || rectCount == 0) { - KSTATUS s = KiDisplayFlushDirty(); - KeReleaseSpinlock(&gDisplayLock, flags); + DISPLAY_DEVICE* device = (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = (const DISPLAY_DEVICE_OPS*)0; + uint32_t caps = 0; + DISPLAY_MODE mode; + s = KiDisplayGetProviderSnapshot(&device, &ops, &caps, &mode); + if (!KSUCCESS(s)) { + KiDisplayReleaseProviderMutex(providerHeld); return s; } - - bool hideCursor = gCursorDrawn && KiDisplayUsesSoftwareCursor(); - if (hideCursor) - KiDisplayHideCursorForDraw(); - - gDisplayStats.PresentCount++; - gDisplayStats.PresentRectCount += rectCount; - KSTATUS s = gPrimaryDisplay->Ops->Present(gPrimaryDisplay, rects, rectCount); - - if (hideCursor) - KiDisplayRestoreCursorAfterDraw(); - KeReleaseSpinlock(&gDisplayLock, flags); - return s; -} - -DISPLAY_API KSTATUS KeDisplaySync(void) { - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - if (!KiDisplayHasDevice() || !gPrimaryDisplay->Ops->Sync) { - KeReleaseSpinlock(&gDisplayLock, flags); + if (!ops->Present) { + KiDisplayReleaseProviderMutex(providerHeld); return STATUS_NOT_SUPPORTED; } - gDisplayStats.SyncCount++; - KSTATUS s = gPrimaryDisplay->Ops->Sync(gPrimaryDisplay); - KeReleaseSpinlock(&gDisplayLock, flags); - return s; -} -DISPLAY_API KSTATUS KeDisplayQueryInfo(KE_DISPLAY_INFO* outInfo) { - if (!outInfo) - return STATUS_INVALID_PARAMETER; + (void)KiDisplayHideCursorForDrawSnapshot(device, ops, caps); - uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - memset(outInfo, 0, sizeof(*outInfo)); - - uint32_t count = 0; - for (DISPLAY_DEVICE* it = gDisplayList; it; it = it->Next) - count++; - outInfo->DeviceCount = count; - outInfo->ManagerStats = gDisplayStats; - - if (!KiDisplayHasDevice()) { - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_NOT_FOUND; - } - - const char* name = gPrimaryDisplay->Name ? gPrimaryDisplay->Name : "?"; - size_t len = strlen(name); - if (len >= sizeof(outInfo->PrimaryName)) - len = sizeof(outInfo->PrimaryName) - 1; - memcpy(outInfo->PrimaryName, name, len); - outInfo->PrimaryName[len] = '\0'; - - outInfo->Caps = gPrimaryDisplay->Caps; - if (gPrimaryDisplay->Ops->GetMode) { - KSTATUS s = gPrimaryDisplay->Ops->GetMode(gPrimaryDisplay, &outInfo->Mode); - if (!KSUCCESS(s)) { - KeReleaseSpinlock(&gDisplayLock, flags); - return s; + if (!rects || rectCount == 0) { + s = KiDisplayFlushDirtyWithSnapshot(device, ops); + } else { + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); + if (device == gPrimaryDisplay) { + gDisplayStats.PresentCount++; + gDisplayStats.PresentRectCount += rectCount; } + KeReleaseSpinlock(&gDisplayLock, flags); + s = ops->Present(device, rects, rectCount); } - if (gPrimaryDisplay->Ops->QueryStats) - (void)gPrimaryDisplay->Ops->QueryStats(gPrimaryDisplay, &outInfo->DeviceStats); - - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; + KiDisplayRestoreCursorAfterDrawSnapshot(device, ops, caps); + KiDisplayReleaseProviderMutex(providerHeld); + return s; } DISPLAY_API KSTATUS KeDisplaySetCursorImage(int width, int height, int hotX, int hotY, const uint32_t* argbPixels) { + bool providerHeld = false; + KSTATUS s = KiDisplayAcquireProviderMutex(&providerHeld); + if (!KSUCCESS(s)) + return s; + uintptr_t flags = KeAcquireSpinlock(&gDisplayLock); - KSTATUS s = (!KiDisplayHasDevice() || !gPrimaryDisplay->Ops->SetCursorImage) - ? STATUS_NOT_SUPPORTED - : gPrimaryDisplay->Ops->SetCursorImage(gPrimaryDisplay, width, height, hotX, hotY, argbPixels); + DISPLAY_DEVICE* device = KiDisplayHasDevice() ? gPrimaryDisplay : (DISPLAY_DEVICE*)0; + const DISPLAY_DEVICE_OPS* ops = device ? device->Ops : (const DISPLAY_DEVICE_OPS*)0; KeReleaseSpinlock(&gDisplayLock, flags); + + if (!device || !ops || !ops->SetCursorImage) { + KiDisplayReleaseProviderMutex(providerHeld); + return STATUS_NOT_SUPPORTED; + } + + s = ops->SetCursorImage(device, width, height, hotX, hotY, argbPixels); + KiDisplayReleaseProviderMutex(providerHeld); return s; } @@ -1034,31 +1930,18 @@ DISPLAY_API KSTATUS KeDisplaySetCursorPosition(int x, int y) { gCursorY = y; gDisplayStats.CursorMoveCount++; - if (KiDisplayOutputSuspended()) { - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; + bool shouldQueue = !KiDisplayOutputSuspended() && + gCursorVisible && + KiDisplayHasCursorOps(); + if (shouldQueue) { + gCursorUpdatePending = true; + gCursorRequestGeneration++; } - - if (!gCursorVisible || !KiDisplayHasCursorOps()) { - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; - } - - if (gCursorDrawn && KiDisplayUsesSoftwareCursor()) - (void)gPrimaryDisplay->Ops->ShowCursor(gPrimaryDisplay, false); - - if (gPrimaryDisplay->Ops->SetCursorPosition) - (void)gPrimaryDisplay->Ops->SetCursorPosition(gPrimaryDisplay, x, y); - - if (gCursorDrawn && KiDisplayUsesHardwareCursor()) { - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; - } - - KSTATUS s = gPrimaryDisplay->Ops->ShowCursor(gPrimaryDisplay, true); - gCursorDrawn = KSUCCESS(s); KeReleaseSpinlock(&gDisplayLock, flags); - return s; + + if (shouldQueue) + KiDisplayQueueCursorWork(); + return STATUS_SUCCESS; } DISPLAY_API KSTATUS KeDisplayShowCursor(bool show) { @@ -1066,37 +1949,21 @@ DISPLAY_API KSTATUS KeDisplayShowCursor(bool show) { gCursorVisible = show; if (show) gDisplayStats.CursorShowCount++; else gDisplayStats.CursorHideCount++; - if (KiDisplayOutputSuspended()) { - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; - } - - if (!KiDisplayHasCursorOps()) { + bool shouldQueue = !KiDisplayOutputSuspended() && KiDisplayHasCursorOps(); + if (!shouldQueue && !show) { gCursorDrawn = false; - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; + gCursorCommittedX = DISPLAY_CURSOR_POS_INVALID; + gCursorCommittedY = DISPLAY_CURSOR_POS_INVALID; } - - if (!show) { - if (gCursorDrawn) - (void)gPrimaryDisplay->Ops->ShowCursor(gPrimaryDisplay, false); - gCursorDrawn = false; - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; + if (shouldQueue) { + gCursorUpdatePending = true; + gCursorRequestGeneration++; } - - if (gCursorDrawn) { - KeReleaseSpinlock(&gDisplayLock, flags); - return STATUS_SUCCESS; - } - - if (gPrimaryDisplay->Ops->SetCursorPosition) - (void)gPrimaryDisplay->Ops->SetCursorPosition(gPrimaryDisplay, gCursorX, gCursorY); - - KSTATUS s = gPrimaryDisplay->Ops->ShowCursor(gPrimaryDisplay, true); - gCursorDrawn = KSUCCESS(s); KeReleaseSpinlock(&gDisplayLock, flags); - return s; + + if (shouldQueue) + KiDisplayQueueCursorWork(); + return STATUS_SUCCESS; } // Implements the boot Fb Context operation. @@ -1331,6 +2198,7 @@ static void BootFbRestoreCursor(BOOTFB_CONTEXT* fb) { gCursorDrawX = -1; gCursorDrawY = -1; + gCursorUpdatePending = false; } // Implements the boot Fb Draw Cursor operation. diff --git a/code/kernel/ke_event.c b/code/kernel/ke_event.c index 458d7f6..179e10e 100644 --- a/code/kernel/ke_event.c +++ b/code/kernel/ke_event.c @@ -1,17 +1,100 @@ // Kernel event primitive. This file owns manual-reset event state and waiter wakeups. #include +#include #include #include #include #include #include #include +#include +#include + +// Multi-wait context shared by the stack wait blocks for one wait call. +typedef struct KE_EVENT_MULTI_WAIT_CONTEXT KE_EVENT_MULTI_WAIT_CONTEXT; + +typedef struct KE_EVENT_WAIT_BLOCK { + LIST_ENTRY Link; + KE_EVENT_MULTI_WAIT_CONTEXT* Context; + EVENT* Event; + uint32_t Index; + uint8_t Armed; +} KE_EVENT_WAIT_BLOCK; + +struct KE_EVENT_MULTI_WAIT_CONTEXT { + THREAD* Thread; + KE_EVENT_WAIT_BLOCK* Blocks; + uint32_t Count; + volatile long Completed; + KSTATUS Status; + uint32_t SelectedIndex; +}; // Implements the event Destroy operation. static void KiEventDestroy(OBJECT* obj) { (void)obj; } -// Implements the event Create operation. +// Completes a multi-wait block while the source event lock is held. +static void KiCompleteMultiWaitBlockLocked(KE_EVENT_WAIT_BLOCK* block) { + if (!block || !block->Armed) + return; + + ListRemove(&block->Link); + block->Armed = 0; + + KE_EVENT_MULTI_WAIT_CONTEXT* ctx = block->Context; + if (!ctx) + return; + + if (_InterlockedCompareExchange(&ctx->Completed, 1, 0) != 0) + return; + + ctx->Status = STATUS_SUCCESS; + ctx->SelectedIndex = block->Index; + KeUnblockThreadFromWaitListWithStatus(ctx->Thread, STATUS_SUCCESS); +} + +// Removes any wait blocks that are still linked after the wait completed. +static void KiCleanupMultiWaitBlocks(KE_EVENT_MULTI_WAIT_CONTEXT* ctx) { + if (!ctx || !ctx->Blocks) + return; + + for (uint32_t i = 0; i < ctx->Count; i++) { + KE_EVENT_WAIT_BLOCK* block = &ctx->Blocks[i]; + EVENT* event = block->Event; + if (!event) + continue; + + uintptr_t flags = KeAcquireSpinlock(&event->Lock); + if (block->Armed) { + ListRemove(&block->Link); + block->Armed = 0; + } + KeReleaseSpinlock(&event->Lock, flags); + } +} + +// Removes a multi-wait from every event list during abort or thread teardown. +static void KiRundownMultiWait(THREAD* thread, void* context, KSTATUS status) { + KE_EVENT_MULTI_WAIT_CONTEXT* ctx = (KE_EVENT_MULTI_WAIT_CONTEXT*)context; + if (!ctx) + return; + + if (_InterlockedCompareExchange(&ctx->Completed, 1, 0) == 0) { + ctx->Status = status; + ctx->SelectedIndex = 0; + } + + KiCleanupMultiWaitBlocks(ctx); + + // Abort wakes a truly blocked waiter. Thread teardown may call this after a + // signal already made the thread ready; in that case only the list cleanup + // above is needed. + if (thread && thread->State == ThreadWaiting) + KeUnblockThreadFromWaitListWithStatus(thread, status); +} + +// Creates event. __declspec(dllexport) KSTATUS KeEventCreate(EVENT_TYPE type, uint32_t initialState, EVENT **out) { if (!out) return STATUS_INVALID_PARAMETER; EVENT* e = (EVENT *)ObAllocateObject(ObjectTypeEvent, sizeof(EVENT)); @@ -21,6 +104,7 @@ __declspec(dllexport) KSTATUS KeEventCreate(EVENT_TYPE type, uint32_t initialSta e->Type = type; e->Signaled = initialState ? 1 : 0; LIST_INIT(&e->WaitList); + LIST_INIT(&e->MultiWaitList); *out = e; return STATUS_SUCCESS; } @@ -35,22 +119,39 @@ __declspec(dllexport) void KeSignalEvent(EVENT* event) { while (!LIST_IS_EMPTY(&event->WaitList)) { LIST_ENTRY* node = ListPopHead(&event->WaitList); THREAD* t = CONTAINING_RECORD(node, THREAD, SchedulerLink); + KeSetThreadWaitList(t, (LIST_ENTRY*)0); KeReleaseSpinlock(&event->Lock, flags); KeUnblockThreadWithStatus(t, STATUS_SUCCESS); flags = KeAcquireSpinlock(&event->Lock); } + while (!LIST_IS_EMPTY(&event->MultiWaitList)) { + LIST_ENTRY* node = event->MultiWaitList.Fwd; + KE_EVENT_WAIT_BLOCK* block = CONTAINING_RECORD(node, KE_EVENT_WAIT_BLOCK, Link); + KiCompleteMultiWaitBlockLocked(block); + } } else { - if (!LIST_IS_EMPTY(&event->WaitList)) { - // Auto-reset handoff: transfer the signal directly to exactly one - // waiter. Do not leave Signaled=1, because a later waiter can steal - // the disk/APC completion before the selected thread resumes. + while (!LIST_IS_EMPTY(&event->WaitList)) { event->Signaled = 0; LIST_ENTRY* node = ListPopHead(&event->WaitList); THREAD* t = CONTAINING_RECORD(node, THREAD, SchedulerLink); + KeSetThreadWaitList(t, (LIST_ENTRY*)0); KeReleaseSpinlock(&event->Lock, flags); KeUnblockThreadWithStatus(t, STATUS_SUCCESS); return; } + + while (!LIST_IS_EMPTY(&event->MultiWaitList)) { + LIST_ENTRY* node = event->MultiWaitList.Fwd; + KE_EVENT_WAIT_BLOCK* block = CONTAINING_RECORD(node, KE_EVENT_WAIT_BLOCK, Link); + KE_EVENT_MULTI_WAIT_CONTEXT* ctx = block->Context; + if (ctx && _InterlockedCompareExchange(&ctx->Completed, 0, 0) == 0) { + event->Signaled = 0; + KiCompleteMultiWaitBlockLocked(block); + KeReleaseSpinlock(&event->Lock, flags); + return; + } + KiCompleteMultiWaitBlockLocked(block); + } event->Signaled = 1; } KeReleaseSpinlock(&event->Lock, flags); @@ -64,7 +165,22 @@ __declspec(dllexport) void KeClearEvent(EVENT* event) { KeReleaseSpinlock(&event->Lock, flags); } -// Waits for for Event. +// Attempts to consume an event signal without blocking. +__declspec(dllexport) KSTATUS KeTryWaitEvent(EVENT* event) { + if (!event) return STATUS_INVALID_PARAMETER; + uintptr_t flags = KeAcquireSpinlock(&event->Lock); + if (!event->Signaled) { + KeReleaseSpinlock(&event->Lock, flags); + return STATUS_TIMEOUT; + } + + if (event->Type == EventAutoReset) + event->Signaled = 0; + KeReleaseSpinlock(&event->Lock, flags); + return STATUS_SUCCESS; +} + +// Waits for Event. __declspec(dllexport) KSTATUS KeWaitForEvent(EVENT* event, uint8_t alertable) { if (!event) return STATUS_INVALID_PARAMETER; if (!KeCanWait()) return STATUS_INVALID_IRQL; @@ -85,3 +201,85 @@ __declspec(dllexport) KSTATUS KeWaitForEvent(EVENT* event, uint8_t alertable) { return KeCommitBlockThread(self, alertable); } +// Waits for any of a set of events, returning the selected index. +__declspec(dllexport) KSTATUS KeWaitForMultipleEvents(EVENT** events, uint32_t count, + uint8_t alertable, uint64_t timeoutMs, + uint32_t* signaledIndexOut) { + if (!events || count == 0 || count > SYS_WAIT_MANY_MAX_HANDLES) + return STATUS_INVALID_PARAMETER; + if (!KeCanWait()) return STATUS_INVALID_IRQL; + if (alertable && KeAreKernelApcsDisabled()) return STATUS_INVALID_PARAMETER; + + for (uint32_t i = 0; i < count; i++) { + if (!events[i]) return STATUS_INVALID_PARAMETER; + KSTATUS s = KeTryWaitEvent(events[i]); + if (KSUCCESS(s)) { + if (signaledIndexOut) *signaledIndexOut = i; + return STATUS_SUCCESS; + } + } + + if (timeoutMs == 0) + return STATUS_TIMEOUT; + + KE_EVENT_WAIT_BLOCK stackBlocks[SYS_WAIT_MANY_MAX_HANDLES]; + KE_EVENT_MULTI_WAIT_CONTEXT ctx; + ctx.Thread = KeGetCurrentThread(); + ctx.Blocks = stackBlocks; + ctx.Count = count; + ctx.Completed = 0; + ctx.Status = STATUS_PENDING; + ctx.SelectedIndex = 0; + + for (uint32_t i = 0; i < count; i++) { + stackBlocks[i].Context = &ctx; + stackBlocks[i].Event = events[i]; + stackBlocks[i].Index = i; + stackBlocks[i].Armed = 0; + LIST_INIT(&stackBlocks[i].Link); + } + + KePrepareBlockThread(ctx.Thread, (OBJECT*)0, alertable); + KeSetThreadWaitRundown(ctx.Thread, KiRundownMultiWait, &ctx); + + if (timeoutMs != SYS_WAIT_TIMEOUT_INFINITE) { + KSTATUS ts = KeArmCurrentThreadTimeout(timeoutMs, STATUS_TIMEOUT); + if (!KSUCCESS(ts)) { + KeClearThreadWaitRundown(ctx.Thread); + KeUnblockThreadFromWaitListWithStatus(ctx.Thread, ts); + return ts; + } + } + + for (uint32_t i = 0; i < count; i++) { + EVENT* event = events[i]; + uintptr_t flags = KeAcquireSpinlock(&event->Lock); + if (_InterlockedCompareExchange(&ctx.Completed, 0, 0) == 0 && event->Signaled) { + if (event->Type == EventAutoReset) + event->Signaled = 0; + ctx.Status = STATUS_SUCCESS; + ctx.SelectedIndex = i; + _InterlockedExchange(&ctx.Completed, 1); + KeReleaseSpinlock(&event->Lock, flags); + KeUnblockThreadFromWaitListWithStatus(ctx.Thread, STATUS_SUCCESS); + break; + } + + if (_InterlockedCompareExchange(&ctx.Completed, 0, 0) == 0) { + stackBlocks[i].Armed = 1; + ListInsertTail(&event->MultiWaitList, &stackBlocks[i].Link); + } + KeReleaseSpinlock(&event->Lock, flags); + } + + KSTATUS waitStatus = KeCommitBlockThread(ctx.Thread, alertable); + if (_InterlockedCompareExchange(&ctx.Completed, 1, 0) == 0) + ctx.Status = waitStatus; + + KiCleanupMultiWaitBlocks(&ctx); + KeClearThreadWaitRundown(ctx.Thread); + + if (ctx.Status == STATUS_SUCCESS && signaledIndexOut) + *signaledIndexOut = ctx.SelectedIndex; + return ctx.Status; +} diff --git a/code/kernel/ke_input.c b/code/kernel/ke_input.c index b1bebc5..9201faf 100644 --- a/code/kernel/ke_input.c +++ b/code/kernel/ke_input.c @@ -8,6 +8,8 @@ #include #include #include +#include +#include #define KEY_QUEUE_SIZE 256u #define KEY_QUEUE_MASK (KEY_QUEUE_SIZE - 1u) @@ -32,6 +34,7 @@ static volatile uint32_t gModifiers; typedef struct INPUT_KEY_QUEUE_ENTRY { INPUT_KEY key; char ch; + uint32_t modifiers; } INPUT_KEY_QUEUE_ENTRY; static INPUT_KEY_QUEUE_ENTRY gKeyQueue[KEY_QUEUE_SIZE]; @@ -50,6 +53,8 @@ static volatile int gMouseBoundsH = 1080; static volatile uint8_t gMouseButtons; static volatile uint32_t gNextInputDeviceId = 1; +static EVENT* gKeyboardEvent; +static EVENT* gInputStateEvent; static char KiInputKeyToChar(INPUT_KEY key); @@ -77,14 +82,18 @@ static const KEY_CHAR_PAIR gCharMap[] = { { INPUT_KEY_KP_0, '0', '0' }, { INPUT_KEY_KP_PERIOD, '.', '.' }, }; -// Implements the input Queue Key operation. +// Queues a keyboard event and wakes one blocked reader when the event was accepted. static void KiInputQueueKey(INPUT_KEY key, char ch) { if (key == INPUT_KEY_NONE) return; int next = (gKeyTail + 1) & (int)KEY_QUEUE_MASK; if (next == gKeyHead) return; gKeyQueue[gKeyTail].key = key; gKeyQueue[gKeyTail].ch = ch; + gKeyQueue[gKeyTail].modifiers = gModifiers; gKeyTail = next; + + if (gKeyboardEvent) + KeSignalEvent(gKeyboardEvent); } // Implements the input Queue Has Space operation. @@ -211,6 +220,17 @@ void KeInputInitialize(const HAL_API* hal) { gMouseHead = gMouseTail = 0; gMouseButtons = 0; + if (!gKeyboardEvent) { + KSTATUS s = KeEventCreate(EventAutoReset, 0, &gKeyboardEvent); + if (!KSUCCESS(s)) + DbgPrint("Input: keyboard wait event create failed st=0x%x\n", s); + } + if (!gInputStateEvent) { + KSTATUS s = KeEventCreate(EventAutoReset, 0, &gInputStateEvent); + if (!KSUCCESS(s)) + DbgPrint("Input: state wait event create failed st=0x%x\n", s); + } + int w = 1920, h = 1080; DISPLAY_MODE mode; if (KSUCCESS(KeDisplayGetMode(&mode))) { @@ -252,6 +272,7 @@ __declspec(dllexport) void KeInputSubmitKey(const INPUT_KEY_EVENT* event) { gKeyDown[key] = 1u; KiInputSetModifier(key, true); + if (gInputStateEvent) KeSignalEvent(gInputStateEvent); KiInputQueueKey(key, KiInputKeyToChar(key)); KiInputStartRepeat(key, KiInputNowTicks()); } else { @@ -260,6 +281,7 @@ __declspec(dllexport) void KeInputSubmitKey(const INPUT_KEY_EVENT* event) { gKeyDown[key] = 0u; KiInputSetModifier(key, false); + if (gInputStateEvent) KeSignalEvent(gInputStateEvent); KiInputStopRepeat(key); } } @@ -341,27 +363,60 @@ INPUT_KEY KeInputKbPoll(void) { return (gKeyHead == gKeyTail) ? INPUT_KEY_NONE : gKeyQueue[gKeyHead].key; } + +// Waits until at least one keyboard event is available. +KSTATUS KeInputWaitKey(uint8_t alertable) { + if (KeInputKbPoll() != INPUT_KEY_NONE) + return STATUS_SUCCESS; + if (!gKeyboardEvent) + return STATUS_FAILURE; + + for (;;) { + KSTATUS s = KeWaitForEvent(gKeyboardEvent, alertable); + if (!KSUCCESS(s)) + return s; + if (KeInputKbPoll() != INPUT_KEY_NONE) + return STATUS_SUCCESS; + } +} + // Implements the input Is Key Down operation. bool KeInputIsKeyDown(INPUT_KEY key) { if (key == INPUT_KEY_NONE || key >= MAX_INPUT_KEYS) return false; return gKeyDown[key] != 0; } -INPUT_KEY KeInputReadKey(void) { +// Implements the input Peek Event operation. +bool KeInputPeekEvent(INPUT_KEY_RECORD* record) { + if (!record) return false; if (gKeyHead == gKeyTail) KiInputServiceRepeat(); - if (gKeyHead == gKeyTail) return INPUT_KEY_NONE; - INPUT_KEY key = gKeyQueue[gKeyHead].key; + if (gKeyHead == gKeyTail) return false; + + INPUT_KEY_QUEUE_ENTRY entry = gKeyQueue[gKeyHead]; + record->Key = entry.key; + record->Char = entry.ch; + record->Reserved = 0; + record->Modifiers = entry.modifiers; + return true; +} + +// Implements the input Read Event operation. +bool KeInputReadEvent(INPUT_KEY_RECORD* record) { + if (!KeInputPeekEvent(record)) return false; gKeyHead = (gKeyHead + 1) & (int)KEY_QUEUE_MASK; - return key; + return true; +} + +// Implements the input Read Key operation. +INPUT_KEY KeInputReadKey(void) { + INPUT_KEY_RECORD record; + return KeInputReadEvent(&record) ? record.Key : INPUT_KEY_NONE; } // Implements the input Read Char operation. char KeInputReadChar(void) { - if (gKeyHead == gKeyTail) KiInputServiceRepeat(); - if (gKeyHead == gKeyTail) return 0; - char ch = gKeyQueue[gKeyHead].ch; - gKeyHead = (gKeyHead + 1) & (int)KEY_QUEUE_MASK; - return ch; + INPUT_KEY_RECORD record; + return KeInputReadEvent(&record) ? record.Char : 0; } // Implements the input Get Modifiers operation. @@ -416,3 +471,32 @@ void KeInputMouseSetBounds(int w, int h) { gMouseBoundsH = h; KeInputMouseSetPosition(gMouseX, gMouseY); } + +// Destroys an input queue handle. +static void KiInputQueueDestroy(OBJECT* object) { + INPUT_QUEUE* queue = CONTAINING_RECORD(object, INPUT_QUEUE, Header); + if (queue->WaitEvent) { + ObDereferenceObject(&queue->WaitEvent->Header); + queue->WaitEvent = (EVENT*)0; + } +} + +// Opens the default keyboard input queue as a waitable object. +KSTATUS KeInputOpenQueue(INPUT_QUEUE** out) { + if (!out) return STATUS_INVALID_PARAMETER; + *out = (INPUT_QUEUE*)0; + if (!gInputStateEvent) return STATUS_FAILURE; + + INPUT_QUEUE* queue = (INPUT_QUEUE*)ObAllocateObject(ObjectTypeInputQueue, sizeof(INPUT_QUEUE)); + if (!queue) return STATUS_NO_MEMORY; + queue->Header.Destroy = KiInputQueueDestroy; + ObReferenceObject(&gInputStateEvent->Header); + queue->WaitEvent = gInputStateEvent; + *out = queue; + return STATUS_SUCCESS; +} + +// Returns the event that wakes when keyboard state changes. +EVENT* KeInputQueueWaitEvent(INPUT_QUEUE* queue) { + return queue ? queue->WaitEvent : (EVENT*)0; +} diff --git a/code/kernel/ke_irp.c b/code/kernel/ke_irp.c index b565caa..33ced20 100644 --- a/code/kernel/ke_irp.c +++ b/code/kernel/ke_irp.c @@ -138,7 +138,7 @@ static size_t IoIrpSize(uint8_t stackCount) { __declspec(dllexport) KSTATUS IoAllocateIrp(uint8_t stackCount, IRP** outIrp) { if (!outIrp) return STATUS_INVALID_PARAMETER; if (stackCount == 0) stackCount = 1; - IRP* irp = (IRP*)MmAllocatePool(IoIrpSize(stackCount)); + IRP* irp = (IRP*)MmAllocatePoolTag(IoIrpSize(stackCount), MM_POOL_TAG('i', 'r', 'p', ' ')); if (!irp) return STATUS_NO_MEMORY; IoInitializeIrp(irp, stackCount); *outIrp = irp; diff --git a/code/kernel/ke_mutex.c b/code/kernel/ke_mutex.c index 1d1b034..fe50c5e 100644 --- a/code/kernel/ke_mutex.c +++ b/code/kernel/ke_mutex.c @@ -12,7 +12,7 @@ static void KiMutexDestroy(OBJECT* obj) { (void)obj; } // Implements the mutex Create operation. -KSTATUS KeMutexCreate(MUTEX **out) { +__declspec(dllexport) KSTATUS KeMutexCreate(MUTEX **out) { if (!out) return STATUS_INVALID_PARAMETER; MUTEX* m = (MUTEX *)ObAllocateObject(ObjectTypeMutex, sizeof(MUTEX)); if (!m) return STATUS_NO_MEMORY; @@ -25,7 +25,7 @@ KSTATUS KeMutexCreate(MUTEX **out) { } // Implements the acquire Mutex operation. -KSTATUS KeAcquireMutex(MUTEX* mutex) { +__declspec(dllexport) KSTATUS KeAcquireMutex(MUTEX* mutex) { if (!mutex) return STATUS_INVALID_PARAMETER; if (!KeCanWait()) return STATUS_INVALID_IRQL; THREAD* self = KeGetCurrentThread(); @@ -45,7 +45,7 @@ KSTATUS KeAcquireMutex(MUTEX* mutex) { } // Implements the try Acquire Mutex operation. -KSTATUS KeTryAcquireMutex(MUTEX* mutex) { +__declspec(dllexport) KSTATUS KeTryAcquireMutex(MUTEX* mutex) { if (!mutex) return STATUS_INVALID_PARAMETER; THREAD* self = KeGetCurrentThread(); uintptr_t flags = KeAcquireSpinlock(&mutex->Lock); @@ -59,13 +59,14 @@ KSTATUS KeTryAcquireMutex(MUTEX* mutex) { } // Releases mutex. -void KeReleaseMutex(MUTEX* mutex) { +__declspec(dllexport) void KeReleaseMutex(MUTEX* mutex) { if (!mutex) return; uintptr_t flags = KeAcquireSpinlock(&mutex->Lock); if (!LIST_IS_EMPTY(&mutex->WaitList)) { LIST_ENTRY* node = ListPopHead(&mutex->WaitList); THREAD* waiter = CONTAINING_RECORD(node, THREAD, SchedulerLink); + KeSetThreadWaitList(waiter, (LIST_ENTRY*)0); // Direct handoff: the selected waiter owns the mutex when its wait // returns. Do not drop Owner to NULL, or another thread can steal it. mutex->Owner = waiter; diff --git a/code/kernel/ke_process.c b/code/kernel/ke_process.c index 4911578..4bfa1cc 100644 --- a/code/kernel/ke_process.c +++ b/code/kernel/ke_process.c @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -140,7 +142,7 @@ size_t KeCopyProcessInfo(struct SYS_PROCESS_INFO* infoOut, size_t maxCount) { if (p == system) info->Flags |= SYS_PROCESS_FLAG_SYSTEM; if (p == current) info->Flags |= SYS_PROCESS_FLAG_CURRENT; - MmQueryProcessUserMemory(p, &info->UserRegionCount, + MmVadQueryProcessMemory(p, &info->VadCount, &info->UserReservedBytes, &info->UserCommittedBytes); @@ -158,27 +160,128 @@ size_t KeCopyProcessInfo(struct SYS_PROCESS_INFO* infoOut, size_t maxCount) { return count; } -// Destructor (called by ObDereferenceObject when RefCount hits zero). -static void KiProcessDestroy(OBJECT* obj) { - PROCESS* proc = CONTAINING_RECORD(obj, PROCESS, Header); +// Converts a kernel thread state to the public ABI state. +static uint32_t KiThreadStateToSys(THREAD_STATE state) { + switch (state) { + case ThreadReady: return SYS_THREAD_STATE_READY; + case ThreadRunning: return SYS_THREAD_STATE_RUNNING; + case ThreadWaiting: return SYS_THREAD_STATE_WAITING; + case ThreadDead: return SYS_THREAD_STATE_DEAD; + default: return SYS_THREAD_STATE_DEAD; + } +} - if (proc == gSystemProcess) - gSystemProcess = (PROCESS*)0; +// Converts a kernel object type to the public wait-object type. +static uint32_t KiWaitObjectTypeToSys(OBJECT_TYPE type) { + switch (type) { + case ObjectTypeProcess: return SYS_THREAD_WAIT_PROCESS; + case ObjectTypeThread: return SYS_THREAD_WAIT_THREAD; + case ObjectTypeMutex: return SYS_THREAD_WAIT_MUTEX; + case ObjectTypeSemaphore: return SYS_THREAD_WAIT_SEMAPHORE; + case ObjectTypeEvent: return SYS_THREAD_WAIT_EVENT; + case ObjectTypeQueue: return SYS_THREAD_WAIT_QUEUE; + case ObjectTypeFile: return SYS_THREAD_WAIT_FILE; + case ObjectTypeDir: return SYS_THREAD_WAIT_DIR; + case ObjectTypeAsyncIo: return SYS_THREAD_WAIT_ASYNC_IO; + case ObjectTypeSocket: return SYS_THREAD_WAIT_SOCKET; + case ObjectTypeDevice: return SYS_THREAD_WAIT_DEVICE; + case ObjectTypeDriver: return SYS_THREAD_WAIT_DRIVER; + case ObjectTypeUsbDevice: return SYS_THREAD_WAIT_USB_DEVICE; + case ObjectTypeUsbPipe: return SYS_THREAD_WAIT_USB_PIPE; + case ObjectTypeFileMapping: return SYS_THREAD_WAIT_FILE_MAPPING; + case ObjectTypeSurface: return SYS_THREAD_WAIT_SURFACE; + default: return SYS_THREAD_WAIT_UNKNOWN; + } +} - // Release all remaining handles. - for (int i = 1; i < HANDLE_TABLE_SIZE; i++) { - if (proc->Handles[i].Object) { - ObDereferenceObject(proc->Handles[i].Object); +// Copies one process's thread telemetry into caller-provided storage. +KSTATUS KeCopyProcessThreadInfo(uint32_t pid, struct SYS_THREAD_INFO* infoOut, size_t maxCount, size_t* countOut) { + if (!countOut) return STATUS_INVALID_PARAMETER; + if (maxCount && !infoOut) return STATUS_INVALID_PARAMETER; + + PROCESS* process = KeGetProcessById(pid); + if (!process) return STATUS_NOT_FOUND; + + THREAD* current = KeGetCurrentThread(); + size_t count = 0; + + uintptr_t flags = KeAcquireSpinlock(&process->HandleLock); + LIST_ENTRY* head = &process->ThreadList; + for (LIST_ENTRY* node = head->Fwd; node != head; node = node->Fwd) { + THREAD* thread = CONTAINING_RECORD(node, THREAD, ProcessLink); + if (infoOut && count < maxCount) { + SYS_THREAD_INFO* info = &infoOut[count]; + memset(info, 0, sizeof(*info)); + info->Size = sizeof(*info); + info->ProcessId = process->ProcessId; + info->ThreadId = thread->ThreadId; + info->State = KiThreadStateToSys(thread->State); + info->Priority = (uint32_t)thread->Priority; + info->RunningCpu = thread->RunningCpu; + info->LastCpu = thread->LastCpu; + info->QuantumRemaining = thread->QuantumRemaining; + info->QuantumTicks = thread->QuantumTicks; + info->CpuBurstTicks = thread->CpuBurstTicks; + info->LastRunTicks = thread->LastRunTicks; + info->WaitObjectType = thread->WaitObject ? KiWaitObjectTypeToSys(thread->WaitObject->Type) : SYS_THREAD_WAIT_NONE; + info->WaitStatus = (uint32_t)thread->WaitStatus; + info->KernelApcDisable = thread->KernelApcDisable; + + if (thread == current) info->Flags |= SYS_THREAD_FLAG_CURRENT; + if (thread->IsIdleThread) info->Flags |= SYS_THREAD_FLAG_IDLE; + if (!thread->Teb) info->Flags |= SYS_THREAD_FLAG_KERNEL; + if (thread->Queued) info->Flags |= SYS_THREAD_FLAG_QUEUED; + if (thread->AlertableWait) info->Flags |= SYS_THREAD_FLAG_ALERTABLE_WAIT; + if (thread->KernelApcPending) info->Flags |= SYS_THREAD_FLAG_KERNEL_APC_PENDING; + if (thread->UserApcPending) info->Flags |= SYS_THREAD_FLAG_USER_APC_PENDING; + } + count++; + } + KeReleaseSpinlock(&process->HandleLock, flags); + + *countOut = count; + ObDereferenceObject(&process->Header); + return STATUS_SUCCESS; +} + +// Releases all kernel objects still reachable from a process handle table. +// This is part of process exit, not merely object destruction: a parent may keep +// a handle to an exited process for wait/query, but the dead process must not +// keep its own files, mappings, queues, sockets, or synchronization objects alive. +static void KiProcessReleaseAllHandles(PROCESS* proc) { + if (!proc) return; + + for (uint32_t i = 1; i < HANDLE_TABLE_SIZE; ++i) { + OBJECT* obj = (OBJECT*)0; + + uintptr_t flags = KeAcquireSpinlock(&proc->HandleLock); + obj = proc->Handles[i].Object; + if (obj) { proc->Handles[i].Object = (OBJECT*)0; proc->Handles[i].Rights = 0; proc->Handles[i].Flags = 0; } + KeReleaseSpinlock(&proc->HandleLock, flags); + + if (obj) + ObDereferenceObject(obj); } +} + +// Releases the process-owned address space and per-process kernel resources. +// Idempotent because the normal exit path and the final OBJECT destructor can +// both observe a process whose threads are already gone. +static void KiProcessRundownResources(PROCESS* proc) { + if (!proc) return; + if (_InterlockedExchange(&proc->ResourcesReleased, 1) != 0) + return; + + KiProcessReleaseAllHandles(proc); // Release every user VA region owned by this process. This includes the // main image, DLLs, TEBs/stacks that survived thread teardown, process - // environment strings, and leaked user allocations. - MmDestroyAllUserRegions(proc); + // environment strings, mmap views, and leaked user allocations. + MmVadDestroyAllForProcess(proc); proc->ImageBase = 0; proc->ImageSize = 0; proc->ModuleCount = 0; @@ -190,8 +293,21 @@ static void KiProcessDestroy(OBJECT* obj) { proc->UserCmdlineSize = 0; proc->UserCwdBase = 0; proc->UserCwdSize = 0; +} - // Release the exit event. +// Destructor (called by ObDereferenceObject when RefCount hits zero). +static void KiProcessDestroy(OBJECT* obj) { + PROCESS* proc = CONTAINING_RECORD(obj, PROCESS, Header); + + if (proc == gSystemProcess) + gSystemProcess = (PROCESS*)0; + + // If the process failed during launch, or no normal exit completion path + // ran, make sure its own handles and VADs are still reclaimed. + KiProcessRundownResources(proc); + + // Release the exit event only when the process object itself dies. External + // process handles may still wait/query an exited process after rundown. if (proc->ExitEvent) { ObDereferenceObject(&proc->ExitEvent->Header); proc->ExitEvent = (EVENT *)0; @@ -227,6 +343,7 @@ KSTATUS KeCreateProcess(const char* name, PROCESS **out) { proc->ExitCode = 0; proc->Terminating = 0; proc->Exited = 0; + proc->ResourcesReleased = 0; proc->ImageBase = 0; proc->ImageSize = 0; proc->UserPebBase = 0; @@ -240,10 +357,10 @@ KSTATUS KeCreateProcess(const char* name, PROCESS **out) { proc->ModuleCount = 0; proc->CurrentDirectory[0] = '\0'; proc->CurrentDirectoryLength = 0; - proc->CurrentDrive = 0; + proc->CurrentDrive = KeGetSystemRootDrive(); proc->CwdLock = (SPINLOCK)SPINLOCK_INIT; - LIST_INIT(&proc->UserRegions); - proc->UserRegionLock = (SPINLOCK)SPINLOCK_INIT; + LIST_INIT(&proc->VadList); + proc->VadLock = (SPINLOCK)SPINLOCK_INIT; // Slot 0 is the invalid handle - leave it NULL. memset(proc->Handles, 0, sizeof(proc->Handles)); @@ -427,7 +544,7 @@ fail: } // Process exit / termination. -static void KiCompleteProcessExitIfLastThread(PROCESS* process) { +void KeCompleteProcessExitIfLastThread(PROCESS* process) { if (!process) return; uintptr_t flags = KeAcquireSpinlock(&process->HandleLock); @@ -435,6 +552,7 @@ static void KiCompleteProcessExitIfLastThread(PROCESS* process) { KeReleaseSpinlock(&process->HandleLock, flags); if (threadCount == 0 && _InterlockedExchange(&process->Exited, 1) == 0) { + KiProcessRundownResources(process); if (process->ExitEvent) KeSignalEvent(process->ExitEvent); } } @@ -448,7 +566,7 @@ void KeExitProcess(PROCESS* process, int exitCode) { // The calling thread exits immediately after this in SysExitProcess. Do // not drop the process object here: each thread owns one process reference, // and KiThreadDestroy releases that reference after unlinking the thread. - KiCompleteProcessExitIfLastThread(process); + KeCompleteProcessExitIfLastThread(process); } // Implements the terminate Process operation. @@ -469,7 +587,15 @@ KSTATUS KeTerminateProcess(PROCESS* process, int exitCode) { if (t == self) continue; - (void)KeAlertThread(t); + // A terminating process must not be held open by worker threads that + // are blocked in non-alertable waits. User-mode DLLs such as Bink can + // leave IO/submit workers parked on semaphores; after the foreground + // thread crashes, those waiters would otherwise keep ThreadCount > 0 + // and prevent the process ExitEvent from waking the shell. + if (t->State == ThreadWaiting) + KeAbortThreadWait(t, STATUS_ALERTED); + else + (void)KeAlertThread(t); if (t->RunningCpu != KE_INVALID_CPU && t->RunningCpu != selfIndex && t->RunningCpu < gCpuCount) { KCPU* cpu = &gCpus[t->RunningCpu]; @@ -481,7 +607,7 @@ KSTATUS KeTerminateProcess(PROCESS* process, int exitCode) { } KeReleaseSpinlock(&process->HandleLock, flags); - KiCompleteProcessExitIfLastThread(process); + KeCompleteProcessExitIfLastThread(process); return STATUS_SUCCESS; } @@ -535,6 +661,8 @@ void KeCloseHandle(PROCESS* process, uint32_t handle) { process->Handles[handle].Rights = 0; process->Handles[handle].Flags = 0; KeReleaseSpinlock(&process->HandleLock, flags); + if (obj->Type == ObjectTypeFile) + (void)IoFlushFile((FILE_OBJECT*)obj); ObDereferenceObject(obj); return; } diff --git a/code/kernel/ke_sched.c b/code/kernel/ke_sched.c index 1504df4..67a64b4 100644 --- a/code/kernel/ke_sched.c +++ b/code/kernel/ke_sched.c @@ -17,6 +17,7 @@ #include static LIST_ENTRY gRunQueue[PriorityCount]; +static LIST_ENTRY gSleepList; static SPINLOCK gSchedLock; static uint32_t gAgingCounter = 0; @@ -305,6 +306,7 @@ static void KiIdleLoop(void) { // Initializes scheduler Global. void KiInitializeSchedulerGlobal(void) { for (int i = 0; i < PriorityCount; i++) LIST_INIT(&gRunQueue[i]); + LIST_INIT(&gSleepList); gSchedLock = (SPINLOCK)SPINLOCK_INIT; KSTATUS s = KeCreateProcess("idle", &gIdleProcess); @@ -349,6 +351,8 @@ KSTATUS KiCreateIdleThreadForCpu(KCPU* cpu) { idle->WaitStatus = STATUS_SUCCESS; idle->WaitListHead = (LIST_ENTRY*)0; idle->AlertableWait = 0; + idle->SleepUntilTick = 0; + idle->TimeoutStatus = STATUS_SUCCESS; idle->UserStackBase = (void*)0; idle->Teb = (void*)0; idle->TebSize = 0; @@ -358,7 +362,7 @@ KSTATUS KiCreateIdleThreadForCpu(KCPU* cpu) { LIST_INIT(&idle->ProcessLink); LIST_INIT(&idle->SchedulerLink); - void* kstack = MmAllocatePool(KSTACK_SIZE); + void* kstack = MmAllocatePoolTag(KSTACK_SIZE, MM_POOL_TAG('k', 's', 't', 'k')); if (!kstack) { ObDereferenceObject(&idle->Header); return STATUS_NO_MEMORY; @@ -621,12 +625,10 @@ void KiSchedulerNoteVoluntaryBlock(THREAD* thread) { } -// Implements the scheduler Wake Thread operation. -void KiSchedulerWakeThread(THREAD* thread, KSTATUS status) { +// Completes a wait while the scheduler lock is held. +static void KiWakeThreadLocked(THREAD* thread, KSTATUS status) { if (!thread) return; - uintptr_t flags = KeAcquireSpinlock(&gSchedLock); - // Complete the wait while holding the scheduler lock. On SMP a wake can // race the prepare->commit window on another CPU: the target thread is // already on the object's wait list and has State=ThreadWaiting, but it is @@ -634,7 +636,6 @@ void KiSchedulerWakeThread(THREAD* thread, KSTATUS status) { // That case must complete in-place, not enqueue the still-running thread. if (thread->State != ThreadWaiting) { thread->WaitStatus = status; - KeReleaseSpinlock(&gSchedLock, flags); return; } @@ -642,6 +643,8 @@ void KiSchedulerWakeThread(THREAD* thread, KSTATUS status) { thread->WaitStatus = status; thread->WaitListHead = (LIST_ENTRY*)0; thread->AlertableWait = 0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; thread->CpuBurstTicks = 0; thread->QuantaConsumed = 0; @@ -652,7 +655,6 @@ void KiSchedulerWakeThread(THREAD* thread, KSTATUS status) { // KeCommitBlockThread() will see State != ThreadWaiting and return the // completed wait status without ever entering the ready queue. thread->State = ThreadRunning; - KeReleaseSpinlock(&gSchedLock, flags); return; } @@ -662,10 +664,49 @@ void KiSchedulerWakeThread(THREAD* thread, KSTATUS status) { KiInsertReadyLocked(thread); KiPokeRunnableCpusLocked(thread); +} + +// Wakes a thread after first removing its scheduler-link wait membership. +void KeUnblockThreadFromWaitListWithStatus(THREAD* thread, KSTATUS status) { + if (!thread) return; + uintptr_t flags = KeAcquireSpinlock(&gSchedLock); + if (thread->State == ThreadWaiting && thread->WaitListHead) { + ListRemove(&thread->SchedulerLink); + thread->WaitListHead = (LIST_ENTRY*)0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; + } + KiWakeThreadLocked(thread, status); KeReleaseSpinlock(&gSchedLock, flags); } +// Implements the scheduler Wake Thread operation. +void KiSchedulerWakeThread(THREAD* thread, KSTATUS status) { + uintptr_t flags = KeAcquireSpinlock(&gSchedLock); + KiWakeThreadLocked(thread, status); + KeReleaseSpinlock(&gSchedLock, flags); +} + +// Wakes threads whose scheduler-tick sleep deadline has expired. +static void KiWakeExpiredSleepersLocked(uint64_t nowTick) { + LIST_ENTRY* node = gSleepList.Fwd; + while (node != &gSleepList) { + LIST_ENTRY* next = node->Fwd; + THREAD* thread = CONTAINING_RECORD(node, THREAD, SchedulerLink); + + if (thread->State == ThreadWaiting && + thread->WaitListHead == &gSleepList && + thread->SleepUntilTick <= nowTick) { + ListRemove(node); + thread->WaitListHead = (LIST_ENTRY*)0; + KiWakeThreadLocked(thread, thread->TimeoutStatus); + } + + node = next; + } +} + // Implements the scheduler Tick operation. void KeSchedulerTick(void) { KCPU* cpu = KeGetCurrentCpu(); @@ -677,6 +718,7 @@ void KeSchedulerTick(void) { KiArmPostSwitchUnlock(cur, flags); gSchedStats.Ticks++; + KiWakeExpiredSleepersLocked(gSchedStats.Ticks); int requestReschedule = 0; @@ -745,6 +787,90 @@ void KeSchedulerTick(void) { KiRequestReschedule(); } + +// Arms a timeout for the current thread after a wait primitive has prepared it to block. +KSTATUS KeArmCurrentThreadTimeout(uint64_t milliseconds, KSTATUS timeoutStatus) { + if (milliseconds == 0) + return STATUS_INVALID_PARAMETER; + if (!KeCanWait()) + return STATUS_INVALID_IRQL; + + THREAD* self = KeGetCurrentThread(); + if (!self || self->IsIdleThread) + return STATUS_INVALID_PARAMETER; + + uint64_t ticks = milliseconds / SCHED_TICK_MS; + if ((milliseconds % SCHED_TICK_MS) != 0) + ticks++; + if (ticks == 0) + ticks = 1; + + uintptr_t flags = KeAcquireSpinlock(&gSchedLock); + if (self->State != ThreadWaiting || self->WaitListHead != (LIST_ENTRY*)0 || + self->RunningCpu != KeGetCurrentCpu()->CpuIndex) { + KeReleaseSpinlock(&gSchedLock, flags); + return STATUS_INVALID_PARAMETER; + } + + uint64_t now = gSchedStats.Ticks; + uint64_t wakeTick = now + ticks; + if (wakeTick < now) + wakeTick = UINT64_MAX; + + self->WaitListHead = &gSleepList; + self->SleepUntilTick = wakeTick; + self->TimeoutStatus = timeoutStatus; + ListInsertTail(&gSleepList, &self->SchedulerLink); + KeReleaseSpinlock(&gSchedLock, flags); + return STATUS_SUCCESS; +} + +// Implements the delay Thread operation. +__declspec(dllexport) KSTATUS KeDelayThread(uint64_t milliseconds) { + if (milliseconds == 0) { + KeYield(); + return STATUS_SUCCESS; + } + + if (!KeCanWait()) + return STATUS_INVALID_IRQL; + + THREAD* self = KeGetCurrentThread(); + if (!self || self->IsIdleThread) + return STATUS_INVALID_PARAMETER; + + uint64_t ticks = milliseconds / SCHED_TICK_MS; + if ((milliseconds % SCHED_TICK_MS) != 0) + ticks++; + if (ticks == 0) + ticks = 1; + + uintptr_t flags = KeAcquireSpinlock(&gSchedLock); + + if (self->Queued || self->State != ThreadRunning || + self->RunningCpu != KeGetCurrentCpu()->CpuIndex) { + KeReleaseSpinlock(&gSchedLock, flags); + return STATUS_INVALID_PARAMETER; + } + + uint64_t now = gSchedStats.Ticks; + uint64_t wakeTick = now + ticks; + if (wakeTick < now) + wakeTick = UINT64_MAX; + + self->State = ThreadWaiting; + self->WaitObject = (OBJECT*)0; + self->WaitStatus = STATUS_PENDING; + self->WaitListHead = &gSleepList; + self->AlertableWait = 0; + self->SleepUntilTick = wakeTick; + self->TimeoutStatus = STATUS_SUCCESS; + ListInsertTail(&gSleepList, &self->SchedulerLink); + + KeReleaseSpinlock(&gSchedLock, flags); + return KeCommitBlockThread(self, 0); +} + // Implements the yield operation. __declspec(dllexport) void KeYield(void) { KeAssertIrql(PASSIVE_LEVEL); diff --git a/code/kernel/ke_selftest.c b/code/kernel/ke_selftest.c index c898f52..49061e3 100644 --- a/code/kernel/ke_selftest.c +++ b/code/kernel/ke_selftest.c @@ -14,10 +14,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include @@ -478,11 +481,118 @@ static void KiRunIrpKernelApcSelfTest(const HAL_API* hal) { -// Virtual memory mapping invariants. + +// Verifies that the PFN database tracks ownership, references, pins, and contiguous runs. +static void KiRunPfnDatabaseSelfTest(void) { + MM_PFN_STATS before; + MM_PFN_STATS after; + MmPfnQueryStats(&before); + + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseKernel); + if (!frame) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: failed to allocate kernel frame\n"); + + MM_PFN* pfn = MmPfnGet(frame); + if (!pfn || pfn->State != MmPfnStateActive || pfn->Usage != MmPfnUseKernel || pfn->RefCount != 1) + KePanic(KE_PANIC_CODE_SELFTEST, + "PFN selftest: allocated frame metadata invalid phys=%016llx\n", + (unsigned long long)frame); + + if (!KSUCCESS(MmPfnRetain(frame)) || pfn->RefCount != 2) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: retain failed ref=%u\n", pfn->RefCount); + if (!KSUCCESS(MmPfnRelease(frame)) || pfn->RefCount != 1) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: release failed ref=%u\n", pfn->RefCount); + + if (!KSUCCESS(MmPfnPin(frame)) || pfn->PinCount != 1) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: pin failed pin=%u\n", pfn->PinCount); + if (MmPfnFree(frame) != STATUS_ACCESS_DENIED) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: pinned frame was freed\n"); + if (!KSUCCESS(MmPfnUnpin(frame)) || pfn->PinCount != 0) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: unpin failed pin=%u\n", pfn->PinCount); + if (!KSUCCESS(MmPfnAddMapping(frame)) || pfn->MappedCount != 1) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: mapping add failed maps=%u\n", pfn->MappedCount); + if (MmPfnFree(frame) != STATUS_ACCESS_DENIED) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: mapped frame was freed\n"); + if (!KSUCCESS(MmPfnRemoveMapping(frame)) || pfn->MappedCount != 0) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: mapping remove failed maps=%u\n", pfn->MappedCount); + if (!KSUCCESS(MmPfnFree(frame))) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: free failed\n"); + + PHYSICAL_ADDRESS run = MmPfnAllocateContiguous(2, MmPfnUseDma); + if (!run) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: contiguous allocation failed\n"); + MM_PFN* p0 = MmPfnGet(run); + MM_PFN* p1 = MmPfnGet(run + PAGE_SIZE); + if (!p0 || !p1 || p0->Usage != MmPfnUseDma || p1->Usage != MmPfnUseDma) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: contiguous metadata invalid\n"); + MmPfnFree(run); + MmPfnFree(run + PAGE_SIZE); + + // Accounting buckets are part of the VM contract. A missing increment here + // usually means a new allocation path is using an anonymous usage class. + PHYSICAL_ADDRESS clean = MmPfnAllocate(MmPfnUseCleanFileCache); + PHYSICAL_ADDRESS image = MmPfnAllocate(MmPfnUseImage); + PHYSICAL_ADDRESS mapped = MmPfnAllocate(MmPfnUseMappedFile); + PHYSICAL_ADDRESS anon = MmPfnAllocate(MmPfnUseAnonymous); + PHYSICAL_ADDRESS cow = MmPfnAllocate(MmPfnUseCow); + PHYSICAL_ADDRESS surface = MmPfnAllocate(MmPfnUseSurface); + if (!clean || !image || !mapped || !anon || !cow || !surface) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: bucket allocation failed\n"); + + MM_PFN_STATS bucketStats; + MmPfnQueryStats(&bucketStats); + if (bucketStats.CleanFileCacheFrames < before.CleanFileCacheFrames + 1 || + bucketStats.ImageFrames < before.ImageFrames + 1 || + bucketStats.MappedFileFrames < before.MappedFileFrames + 1 || + bucketStats.AnonymousFrames < before.AnonymousFrames + 1 || + bucketStats.CowFrames < before.CowFrames + 1 || + bucketStats.SurfaceFrames < before.SurfaceFrames + 1) { + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: usage bucket accounting failed\n"); + } + + // Standby is the clean-page holding state used by later file-cache and image + // reclaim. It must not change the free count until the owning cache drops it. + if (!KSUCCESS(MmPfnMoveToStandby(clean))) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: clean standby transition failed\n"); + if (!KSUCCESS(MmPfnMoveToStandby(image))) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: image standby transition failed\n"); + + MM_PFN_STATS standbyStats; + MmPfnQueryStats(&standbyStats); + if (standbyStats.StandbyCleanFileCacheFrames < bucketStats.StandbyCleanFileCacheFrames + 1 || + standbyStats.StandbyImageFrames < bucketStats.StandbyImageFrames + 1 || + standbyStats.ReclaimableFrames < bucketStats.ReclaimableFrames + 2 || + standbyStats.FreeFrames != bucketStats.FreeFrames) { + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: standby accounting failed\n"); + } + + if (!KSUCCESS(MmPfnActivate(clean))) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: clean standby activation failed\n"); + if (!KSUCCESS(MmPfnActivate(image))) + KePanic(KE_PANIC_CODE_SELFTEST, "PFN selftest: image standby activation failed\n"); + + (void)MmPfnFree(clean); + (void)MmPfnFree(image); + (void)MmPfnFree(mapped); + (void)MmPfnFree(anon); + (void)MmPfnFree(cow); + (void)MmPfnFree(surface); + + MmPfnQueryStats(&after); + if (before.FreeFrames != after.FreeFrames) + KePanic(KE_PANIC_CODE_SELFTEST, + "PFN selftest: free count leak before=%llu after=%llu\n", + (unsigned long long)before.FreeFrames, + (unsigned long long)after.FreeFrames); + + DbgPrint("PFN selftest: metadata/refcount/pin/buckets/standby/contiguous PASS\n"); +} + +// Verifies the page-table manager invariants that the higher VMM will depend on. static void KiRunMemoryMappingSelfTest(void) { PHYSICAL_ADDRESS phys = 0; uint64_t flags = 0; - KSTATUS s = MmQueryPage(0, &phys, &flags); + KSTATUS s = MmPteQueryPageFor(MmPteQuerySelfTest, 0, &phys, &flags); if (s != STATUS_NOT_FOUND) { KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: virtual page zero is mapped phys=%016llx flags=%016llx status=%u\n", (unsigned long long)phys, @@ -490,7 +600,329 @@ static void KiRunMemoryMappingSelfTest(void) { (unsigned)s); } - DbgPrint("MM selftest: page zero unmapped\n"); + uintptr_t testVa = 0xFFFFA00000000000ULL; + s = MmPteQueryPageFor(MmPteQuerySelfTest, testVa, &phys, &flags); + if (s != STATUS_NOT_FOUND) { + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: test VA already mapped va=%016llx phys=%016llx flags=%016llx\n", + (unsigned long long)testVa, + (unsigned long long)phys, + (unsigned long long)flags); + } + + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseKernel); + if (!frame) + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: failed to allocate test frame\n"); + + s = MmPteMapPage(testVa, frame, PAGE_PRESENT | PAGE_WRITE | PAGE_NX, MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + MmPfnFree(frame); + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: MmPteMapPage failed status=%u\n", s); + } + + volatile uint64_t* probe = (volatile uint64_t*)testVa; + *probe = 0x1122334455667788ULL; + if (*probe != 0x1122334455667788ULL) + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: mapped test page did not retain data\n"); + + s = MmPteQueryPageFor(MmPteQuerySelfTest, testVa, &phys, &flags); + if (!KSUCCESS(s) || phys != frame || !(flags & PAGE_WRITE)) + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: query after map failed status=%u phys=%016llx frame=%016llx flags=%016llx\n", + s, + (unsigned long long)phys, + (unsigned long long)frame, + (unsigned long long)flags); + + s = MmPteProtectRange(testVa, PAGE_SIZE, PAGE_PRESENT | PAGE_NX); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: MmPteProtectRange failed status=%u\n", s); + + s = MmPteQueryPageFor(MmPteQuerySelfTest, testVa, &phys, &flags); + if (!KSUCCESS(s) || (flags & PAGE_WRITE)) + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: read-only protect failed status=%u flags=%016llx\n", + s, + (unsigned long long)flags); + + s = MmPteUnmapPage(testVa); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: MmPteUnmapPage failed status=%u\n", s); + + s = MmPteQueryPageFor(MmPteQuerySelfTest, testVa, &phys, &flags); + if (s != STATUS_NOT_FOUND) + KePanic(KE_PANIC_CODE_SELFTEST, "MM selftest: test VA still mapped after unmap status=%u\n", s); + + MmPfnFree(frame); + DbgPrint("MM selftest: PTE map/query/protect/unmap PASS\n"); +} + + +// Verifies sparse VAD commitment and NOACCESS protection metadata. +static void KiRunVadSparseSelfTest(void) { + PROCESS* process = KeGetCurrentProcess(); + if (!process) + process = KeGetSystemProcess(); + if (!process) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: no process context\n"); + + size_t beforeVadCount = 0; + size_t beforeReserved = 0; + size_t beforeCommitted = 0; + MmVadQueryProcessMemory(process, &beforeVadCount, &beforeReserved, &beforeCommitted); + + void* base = (void*)0; + KSTATUS s = MmVadReserve(process, + PAGE_SIZE * 4, + PAGE_SIZE, + PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, + MM_VAD_ZERO, + MmVadGeneric, + &base); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: reserve failed %u\n", s); + + uint8_t* p = (uint8_t*)base; + s = MmVadCommit(process, p, PAGE_SIZE, PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, MM_VAD_ZERO); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: first commit failed %u\n", s); + + s = MmVadCommit(process, p + PAGE_SIZE * 2, PAGE_SIZE, PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, MM_VAD_ZERO); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: sparse commit failed %u\n", s); + + void* queryBase = (void*)0; + size_t querySize = 0; + uint64_t queryFlags = 0; + s = MmVadQuery(process, p, &queryBase, &querySize, &queryFlags, (MM_VAD_TYPE*)0, (uint32_t*)0); + if (!KSUCCESS(s) || queryBase != p || querySize != PAGE_SIZE || !(queryFlags & PAGE_WRITE)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: first query failed status=%u base=%p size=%llu flags=%016llx\n", + s, queryBase, (unsigned long long)querySize, (unsigned long long)queryFlags); + + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)p, (PHYSICAL_ADDRESS*)0, (uint64_t*)0); + if (s != STATUS_NOT_FOUND) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: demand-zero page was mapped before access status=%u\n", s); + + s = MmVadEnsureResident(process, p, PAGE_SIZE, PAGE_WRITE | PAGE_USER); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: first demand-zero materialization failed %u\n", s); + s = MmVadEnsureResident(process, p + PAGE_SIZE * 2, PAGE_SIZE, PAGE_WRITE | PAGE_USER); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: sparse demand-zero materialization failed %u\n", s); + + s = MmVadQuery(process, p + PAGE_SIZE, &queryBase, &querySize, &queryFlags, (MM_VAD_TYPE*)0, (uint32_t*)0); + if (s != STATUS_NOT_FOUND) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: uncommitted hole queried as committed status=%u\n", s); + + p[0] = 0x5A; + p[PAGE_SIZE * 2] = 0xA5; + + s = MmVadProtect(process, p, PAGE_SIZE, 0); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: NOACCESS protect failed %u\n", s); + + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)p, (PHYSICAL_ADDRESS*)0, (uint64_t*)0); + if (s != STATUS_NOT_FOUND) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: NOACCESS page still had a PTE status=%u\n", s); + + s = MmVadQuery(process, p, &queryBase, &querySize, &queryFlags, (MM_VAD_TYPE*)0, (uint32_t*)0); + if (!KSUCCESS(s) || queryFlags != 0) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: NOACCESS query failed status=%u flags=%016llx\n", + s, (unsigned long long)queryFlags); + + s = MmVadProtect(process, p, PAGE_SIZE, PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: remap after NOACCESS failed %u\n", s); + if (p[0] != 0x5A || p[PAGE_SIZE * 2] != 0xA5) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: protected page contents were lost\n"); + + size_t afterVadCount = 0; + size_t afterReserved = 0; + size_t afterCommitted = 0; + MmVadQueryProcessMemory(process, &afterVadCount, &afterReserved, &afterCommitted); + if (afterVadCount != beforeVadCount + 1 || afterCommitted != beforeCommitted + PAGE_SIZE * 2) + KePanic(KE_PANIC_CODE_SELFTEST, + "VAD selftest: accounting mismatch before=%llu after=%llu vads=%llu/%llu\n", + (unsigned long long)beforeCommitted, + (unsigned long long)afterCommitted, + (unsigned long long)beforeVadCount, + (unsigned long long)afterVadCount); + + s = MmVadDestroy(process, base); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: destroy failed %u\n", s); + + MmVadQueryProcessMemory(process, &afterVadCount, &afterReserved, &afterCommitted); + if (afterVadCount != beforeVadCount || afterCommitted != beforeCommitted) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD selftest: accounting leaked after destroy\n"); + + DbgPrint("VAD selftest: sparse commit/protect/query/demand-zero PASS\n"); +} + + +// Verifies that writable copy-on-write views split before either alias can modify shared state. +static void KiRunVadCopyOnWriteSelfTest(void) { + PROCESS* process = KeGetCurrentProcess(); + if (!process) + process = KeGetSystemProcess(); + if (!process) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: no process context\n"); + + void* base = (void*)0; + KSTATUS s = MmVadCreateCommitted(process, + PAGE_SIZE, + PAGE_SIZE, + PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, + MM_VAD_ZERO, + MmVadGeneric, + &base); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: source allocation failed %u\n", s); + + s = MmVadEnsureResident(process, base, PAGE_SIZE, PAGE_WRITE | PAGE_USER); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: source materialization failed %u\n", s); + + uint8_t* source = (uint8_t*)base; + source[0] = 0x31; + + void* cowBase = (void*)0; + s = MmVadCreateCowView(process, base, PAGE_SIZE, process, &cowBase); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: COW view creation failed %u\n", s); + + PHYSICAL_ADDRESS sourcePhys = 0; + PHYSICAL_ADDRESS cowPhys = 0; + uint64_t sourceFlags = 0; + uint64_t cowFlags = 0; + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)base, &sourcePhys, &sourceFlags); + if (!KSUCCESS(s) || (sourceFlags & PAGE_WRITE)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: source was not write-protected status=%u flags=%016llx\n", + s, (unsigned long long)sourceFlags); + + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)cowBase, &cowPhys, &cowFlags); + if (!KSUCCESS(s) || (cowFlags & PAGE_WRITE) || cowPhys != sourcePhys) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: view was not shared read-only status=%u src=%016llx cow=%016llx flags=%016llx\n", + s, + (unsigned long long)sourcePhys, + (unsigned long long)cowPhys, + (unsigned long long)cowFlags); + + s = MmVadResolveUserPageFault(process, (uintptr_t)cowBase, 0x3); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: simulated write fault split failed %u\n", s); + + uint8_t* copy = (uint8_t*)cowBase; + copy[0] = 0x62; + if (source[0] != 0x31 || copy[0] != 0x62) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: copy isolation failed src=%02x cow=%02x\n", + source[0], copy[0]); + + PHYSICAL_ADDRESS splitPhys = 0; + uint64_t splitFlags = 0; + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)cowBase, &splitPhys, &splitFlags); + if (!KSUCCESS(s) || splitPhys == sourcePhys || !(splitFlags & PAGE_WRITE)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: split mapping wrong status=%u old=%016llx new=%016llx flags=%016llx\n", + s, + (unsigned long long)sourcePhys, + (unsigned long long)splitPhys, + (unsigned long long)splitFlags); + + (void)MmVadDestroy(process, cowBase); + (void)MmVadDestroy(process, base); + + void* zeroBase = (void*)0; + s = MmVadCreateCommitted(process, + PAGE_SIZE, + PAGE_SIZE, + PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, + MM_VAD_ZERO, + MmVadGeneric, + &zeroBase); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: zero-page VAD creation failed %u\n", s); + + s = MmVadEnsureResident(process, zeroBase, PAGE_SIZE, PAGE_USER); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: zero-page read materialization failed %u\n", s); + + PHYSICAL_ADDRESS zeroPhys = 0; + uint64_t zeroFlags = 0; + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)zeroBase, &zeroPhys, &zeroFlags); + if (!KSUCCESS(s) || (zeroFlags & PAGE_WRITE)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: zero page was not mapped read-only status=%u flags=%016llx\n", + s, (unsigned long long)zeroFlags); + + s = MmVadResolveUserPageFault(process, (uintptr_t)zeroBase, 0x3); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: zero-page split failed %u\n", s); + + PHYSICAL_ADDRESS privatePhys = 0; + uint64_t privateFlags = 0; + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)zeroBase, &privatePhys, &privateFlags); + if (!KSUCCESS(s) || privatePhys == zeroPhys || !(privateFlags & PAGE_WRITE)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: zero-page split mapping wrong status=%u old=%016llx new=%016llx flags=%016llx\n", + s, + (unsigned long long)zeroPhys, + (unsigned long long)privatePhys, + (unsigned long long)privateFlags); + + ((uint8_t*)zeroBase)[0] = 0x9C; + if (((uint8_t*)zeroBase)[0] != 0x9C) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD COW selftest: zero-page private write failed\n"); + + (void)MmVadDestroy(process, zeroBase); + DbgPrint("VAD COW selftest: shared views and zero-page split PASS\n"); +} + + +// Verifies that a private resident VAD page can move to swap and fault back in. +static void KiRunVadSwapSelfTest(void) { + if (!MmSwapIsEnabled()) { + DbgPrint("VAD swap selftest: SKIP, swap is disabled\n"); + return; + } + + PROCESS* process = KeGetCurrentProcess(); + if (!process) + process = KeGetSystemProcess(); + if (!process) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD swap selftest: no process context\n"); + + void* base = (void*)0; + KSTATUS s = MmVadCreateCommitted(process, + PAGE_SIZE, + PAGE_SIZE, + PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, + MM_VAD_ZERO, + MmVadGeneric, + &base); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD swap selftest: allocation failed %u\n", s); + + s = MmVadEnsureResident(process, base, PAGE_SIZE, PAGE_WRITE | PAGE_USER); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD swap selftest: materialization failed %u\n", s); + + uint8_t* p = (uint8_t*)base; + p[0] = 0xC1; + p[PAGE_SIZE - 1] = 0x5E; + + void* trimmed = (void*)0; + s = MmVadTrimOnePageForTest(process, &trimmed); + if (!KSUCCESS(s) || trimmed != base) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD swap selftest: trim failed status=%u trimmed=%p base=%p\n", s, trimmed, base); + + s = MmPteQueryPageFor(MmPteQuerySelfTest, (uintptr_t)base, (PHYSICAL_ADDRESS*)0, (uint64_t*)0); + if (s != STATUS_NOT_FOUND) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD swap selftest: swapped page still had a PTE status=%u\n", s); + + s = MmVadResolveUserPageFault(process, (uintptr_t)base, 0); + if (!KSUCCESS(s)) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD swap selftest: page-in fault failed %u\n", s); + + if (p[0] != 0xC1 || p[PAGE_SIZE - 1] != 0x5E) + KePanic(KE_PANIC_CODE_SELFTEST, "VAD swap selftest: contents changed after page-in %02x/%02x\n", p[0], p[PAGE_SIZE - 1]); + + (void)MmVadDestroy(process, base); + DbgPrint("VAD swap selftest: private pageout/pagein PASS\n"); } // String-view ABI self-test. @@ -614,6 +1046,8 @@ static void KiRunAsyncFileIoSelfTest(void) { &st.Iosb, st.CompletionEvent, &st.Apc, + (MM_LOCKED_USER_BUFFER*)0, + (MM_LOCKED_USER_BUFFER*)0, &readReq); if (s != STATUS_PENDING) KePanic(KE_PANIC_CODE_SELFTEST, "FILE/ASYNC selftest: submit returned %u\n", s); @@ -683,6 +1117,8 @@ static void KiRunAsyncFileIoSelfTest(void) { &wiosb, wev, (KAPC*)0, + (MM_LOCKED_USER_BUFFER*)0, + (MM_LOCKED_USER_BUFFER*)0, &writeReq); if (s != STATUS_PENDING) KePanic(KE_PANIC_CODE_SELFTEST, "FILE/ASYNC selftest: write submit returned %u\n", s); @@ -832,7 +1268,11 @@ void KiRunEarlyKernelSelfTests(void) { void KiRunKernelSelfTests(const HAL_API* hal) { DbgPrint("Kernel selftests: begin\n"); KiRunKernelApcSelfTest(); + KiRunPfnDatabaseSelfTest(); KiRunMemoryMappingSelfTest(); + KiRunVadSparseSelfTest(); + KiRunVadCopyOnWriteSelfTest(); + KiRunVadSwapSelfTest(); KiRunStringViewSelfTest(); KiRunIrpKernelApcSelfTest(hal); KiRunAsyncFileIoSelfTest(); diff --git a/code/kernel/ke_semaphore.c b/code/kernel/ke_semaphore.c index 01e7770..ec6c1ea 100644 --- a/code/kernel/ke_semaphore.c +++ b/code/kernel/ke_semaphore.c @@ -68,6 +68,7 @@ __declspec(dllexport) void KeSignalSemaphore(SEMAPHORE* sem) { // consuming a count later. LIST_ENTRY* node = ListPopHead(&sem->WaitList); THREAD* waiter = CONTAINING_RECORD(node, THREAD, SchedulerLink); + KeSetThreadWaitList(waiter, (LIST_ENTRY*)0); KeReleaseSpinlock(&sem->Lock, flags); KeUnblockThreadWithStatus(waiter, STATUS_SUCCESS); return; diff --git a/code/kernel/ke_smp.c b/code/kernel/ke_smp.c index a1ed696..a49898f 100644 --- a/code/kernel/ke_smp.c +++ b/code/kernel/ke_smp.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -141,15 +142,17 @@ static KSTATUS KiBuildApTrampoline(void) { DbgPrint("SMP: trying PMM AP trampoline range 0x%016llX-0x%016llX\n", (uint64_t)AP_TRAMPOLINE_PREFERRED_MIN, (uint64_t)AP_TRAMPOLINE_MAX_PHYS); - frame = MmAllocateFrameInRange(AP_TRAMPOLINE_PREFERRED_MIN, - AP_TRAMPOLINE_MAX_PHYS); + frame = MmPfnAllocateInRange(AP_TRAMPOLINE_PREFERRED_MIN, + AP_TRAMPOLINE_MAX_PHYS, + MmPfnUseKernel); } if (!frame) { DbgPrint("SMP: trying PMM AP trampoline range 0x%016llX-0x%016llX\n", (uint64_t)AP_TRAMPOLINE_FALLBACK_MIN, (uint64_t)AP_TRAMPOLINE_PREFERRED_MIN); - frame = MmAllocateFrameInRange(AP_TRAMPOLINE_FALLBACK_MIN, - AP_TRAMPOLINE_PREFERRED_MIN); + frame = MmPfnAllocateInRange(AP_TRAMPOLINE_FALLBACK_MIN, + AP_TRAMPOLINE_PREFERRED_MIN, + MmPfnUseKernel); } if (!frame) { DbgPrint("SMP: no free PMM frame available below 0x%016llX for AP trampoline\n", @@ -589,7 +592,7 @@ void KeBuildProcessorTopology(void) { // Creates ap Stack. static KSTATUS KiAllocateApStack(KCPU* cpu) { // AP kernel stacks do not need physically contiguous memory. Requiring - // MmAllocateContiguous(8) made SMP startup depend on a DMA-style run being + // MmPfnAllocateContiguous(8) made SMP startup depend on a DMA-style run being // available, which is fragile on real machines and unrelated to AP startup. size_t stackBytes = (size_t)AP_STACK_PAGES * PAGE_SIZE; void* stack = MmAllocatePool(stackBytes); @@ -967,12 +970,11 @@ KSTATUS KeRunApplicationProcessorSchedulerSelfTests(void) { cpuIndex, st); } - THREAD* thread = (THREAD*)0; st = KeCreateKernelThreadWithAffinity((uintptr_t)KiSmpApControlledWorker, (uintptr_t)arg, PriorityNormal, (1ULL << cpuIndex), - &thread); + (THREAD**)0); if (!KSUCCESS(st)) { KePanic(KE_PANIC_CODE_SELFTEST, "SMP scheduler selftest: AP%u worker create failed status=0x%x\n", diff --git a/code/kernel/ke_syscall.asm b/code/kernel/ke_syscall.asm index 168b0bd..6c01689 100644 --- a/code/kernel/ke_syscall.asm +++ b/code/kernel/ke_syscall.asm @@ -152,7 +152,7 @@ KeSyscallEntry ENDP ; also shows which register held the null target. ; ; KeFaultDispatchFrame signature (MS x64): -; void KeFaultDispatchFrame(const KFAULT_FRAME* frame) ; rcx +; KSTATUS KeFaultDispatchFrame(const KFAULT_FRAME* frame) ; rcx ; --------------------------------------------------------------------------- KFAULT_MAGIC EQU 3146544C5541464Bh ; "KFAULTF1" little-ish @@ -379,8 +379,45 @@ pf_kernel_frame: pf_call: and rsp, -16 sub rsp, FAULT_CALL_SHADOW + mov rbx, rax ; Keep the KFAULT_FRAME pointer across C. mov rcx, rax call KeFaultDispatchFrame + test eax, eax + jnz pf_unhandled_return + + mov rdx, rbx ; rdx = KFAULT_FRAME*. + mov rcx, [rdx + KF_Rcx] + mov r8, [rdx + KF_R8] + mov r9, [rdx + KF_R9] + mov rsi, [rdx + KF_Rsi] + mov rdi, [rdx + KF_Rdi] + mov rbp, [rdx + KF_Rbp] + mov r12, [rdx + KF_R12] + mov r13, [rdx + KF_R13] + mov r14, [rdx + KF_R14] + mov r15, [rdx + KF_R15] + mov rbx, [rdx + KF_Rbx] + + lea rsp, [rdx + KFAULT_SIZE + 24] ; Skip saved RAX/R11 and the CPU error code. + mov rax, [rdx + KF_Rax] + mov r11, [rdx + KF_R11] + mov r10, [rdx + KF_Cs] + test r10, 3 + jz pf_resume_no_swapgs + mov r10, [rdx + KF_R10] + mov rdx, [rdx + KF_Rdx] + swapgs + iretq +pf_resume_no_swapgs: + mov r10, [rdx + KF_R10] + mov rdx, [rdx + KF_Rdx] + iretq + +pf_unhandled_return: + cli +pf_unhandled_halt: + hlt + jmp pf_unhandled_halt KeFaultPF ENDP diff --git a/code/kernel/ke_syscall.c b/code/kernel/ke_syscall.c index 81fc6e4..530bcdc 100644 --- a/code/kernel/ke_syscall.c +++ b/code/kernel/ke_syscall.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #ifndef PROCESS_LAUNCH_DEBUG @@ -52,10 +53,15 @@ #define ProcDbg(...) ((void)0) #endif #include +#include +#include +#include +#include #include #include #include #include +#include #include #include #include @@ -95,6 +101,196 @@ typedef struct SYS_USER_ASYNC_APC_CONTEXT { void* UserContext; } SYS_USER_ASYNC_APC_CONTEXT; + +typedef struct FILE_MAPPING_OBJECT { + OBJECT Header; + FILE_OBJECT* File; + uint64_t MaximumSize; + uint64_t PageFlags; + uint32_t Protect; + uint32_t Flags; +} FILE_MAPPING_OBJECT; + +static void KiFileMappingDestroy(OBJECT* obj) { + FILE_MAPPING_OBJECT* mapping = CONTAINING_RECORD(obj, FILE_MAPPING_OBJECT, Header); + if (mapping->File) { + ObDereferenceObject(&mapping->File->Header); + mapping->File = (FILE_OBJECT*)0; + } +} + +typedef struct SURFACE_OBJECT { + OBJECT Header; + PROCESS* Owner; + uint32_t Width; + uint32_t Height; + uint32_t PitchBytes; + uint32_t Format; + uint32_t Flags; + size_t PayloadSize; + size_t PageCount; + uint8_t StatsRegistered; + uint64_t MapCount; + uint64_t PresentCount; + uint64_t PresentRectCount; + uint64_t PresentClippedRectCount; + uint64_t PresentEmptyClipCount; + uint64_t PresentRowsCopied; + uint64_t PresentBytesCopied; + PHYSICAL_ADDRESS* Frames; + void* UserBase; + uint32_t* RowScratch; + MUTEX* Lock; +} SURFACE_OBJECT; + +static SPINLOCK gSurfaceStatsLock = SPINLOCK_INIT; +static SYS_SURFACE_STATS gSurfaceStats; + +// Registers a surface in the global surface telemetry snapshot. +static void KiSurfaceStatsRegister(SURFACE_OBJECT* surface) { + if (!surface || surface->StatsRegistered) + return; + + uintptr_t flags = KeAcquireSpinlock(&gSurfaceStatsLock); + gSurfaceStats.CreatedSurfaces++; + gSurfaceStats.LiveSurfaces++; + gSurfaceStats.LivePages += surface->PageCount; + gSurfaceStats.LiveBytes += surface->PayloadSize; + surface->StatsRegistered = 1; + KeReleaseSpinlock(&gSurfaceStatsLock, flags); +} + +// Removes a surface from the global surface telemetry snapshot. +static void KiSurfaceStatsUnregister(SURFACE_OBJECT* surface) { + if (!surface || !surface->StatsRegistered) + return; + + uintptr_t flags = KeAcquireSpinlock(&gSurfaceStatsLock); + gSurfaceStats.DestroyedSurfaces++; + if (gSurfaceStats.LiveSurfaces) gSurfaceStats.LiveSurfaces--; + if (gSurfaceStats.LivePages >= surface->PageCount) gSurfaceStats.LivePages -= surface->PageCount; + else gSurfaceStats.LivePages = 0; + if (gSurfaceStats.LiveBytes >= surface->PayloadSize) gSurfaceStats.LiveBytes -= surface->PayloadSize; + else gSurfaceStats.LiveBytes = 0; + surface->StatsRegistered = 0; + KeReleaseSpinlock(&gSurfaceStatsLock, flags); +} + +// Records a successful surface map operation. +static void KiSurfaceStatsRecordMap(SURFACE_OBJECT* surface) { + if (!surface) + return; + + uintptr_t flags = KeAcquireSpinlock(&gSurfaceStatsLock); + surface->MapCount++; + gSurfaceStats.MapCalls++; + KeReleaseSpinlock(&gSurfaceStatsLock, flags); +} + +// Records a successful surface present operation. +static void KiSurfaceStatsRecordPresent(SURFACE_OBJECT* surface, uint64_t inputRects, uint64_t clippedRects, + uint64_t emptyClips, uint64_t rowsCopied, uint64_t bytesCopied) { + if (!surface) + return; + + uintptr_t flags = KeAcquireSpinlock(&gSurfaceStatsLock); + surface->PresentCount++; + surface->PresentRectCount += inputRects; + surface->PresentClippedRectCount += clippedRects; + surface->PresentEmptyClipCount += emptyClips; + surface->PresentRowsCopied += rowsCopied; + surface->PresentBytesCopied += bytesCopied; + gSurfaceStats.PresentCalls++; + gSurfaceStats.PresentRectInputs += inputRects; + gSurfaceStats.PresentClippedRects += clippedRects; + gSurfaceStats.PresentEmptyClips += emptyClips; + gSurfaceStats.PresentRowsCopied += rowsCopied; + gSurfaceStats.PresentBytesCopied += bytesCopied; + KeReleaseSpinlock(&gSurfaceStatsLock, flags); +} + +// Copies the global surface telemetry snapshot. +static void KiSurfaceStatsQuery(SYS_SURFACE_STATS* out) { + if (!out) + return; + + uintptr_t flags = KeAcquireSpinlock(&gSurfaceStatsLock); + *out = gSurfaceStats; + KeReleaseSpinlock(&gSurfaceStatsLock, flags); +} + +static void KiSurfaceDestroy(OBJECT* obj) { + SURFACE_OBJECT* surface = CONTAINING_RECORD(obj, SURFACE_OBJECT, Header); + + KiSurfaceStatsUnregister(surface); + + if (surface->Owner && surface->UserBase) { + (void)MmVadDestroy(surface->Owner, surface->UserBase); + surface->UserBase = (void*)0; + } + + if (surface->Lock) { + ObDereferenceObject(&surface->Lock->Header); + surface->Lock = (MUTEX*)0; + } + + if (surface->Frames) { + for (size_t i = 0; i < surface->PageCount; ++i) { + if (surface->Frames[i]) + (void)MmPfnRelease(surface->Frames[i]); + } + MmFreePool(surface->Frames); + surface->Frames = (PHYSICAL_ADDRESS*)0; + } + + if (surface->RowScratch) { + MmFreePool(surface->RowScratch); + surface->RowScratch = (uint32_t*)0; + } +} + +typedef struct SURFACE_ROW_CONTEXT { + SURFACE_OBJECT* Surface; + uint32_t SourceX; + uint32_t SourceY; +} SURFACE_ROW_CONTEXT; + +static KSTATUS KiSurfaceCopyRow(void* context, int rowIndex, uint32_t* scratch, size_t pixelCount) { + SURFACE_ROW_CONTEXT* ctx = (SURFACE_ROW_CONTEXT*)context; + if (!ctx || !ctx->Surface || !scratch || rowIndex < 0) + return STATUS_INVALID_PARAMETER; + + SURFACE_OBJECT* surface = ctx->Surface; + uint64_t row = (uint64_t)ctx->SourceY + (uint64_t)(uint32_t)rowIndex; + uint64_t offset = row * (uint64_t)surface->PitchBytes + (uint64_t)ctx->SourceX * sizeof(uint32_t); + uint64_t bytes = (uint64_t)pixelCount * sizeof(uint32_t); + if (offset > (uint64_t)surface->PayloadSize || bytes > (uint64_t)surface->PayloadSize - offset) + return STATUS_INVALID_PARAMETER; + + uint8_t* dst = (uint8_t*)scratch; + while (bytes) { + size_t frameIndex = (size_t)(offset / PAGE_SIZE); + size_t pageOffset = (size_t)(offset & (PAGE_SIZE - 1)); + if (frameIndex >= surface->PageCount || !surface->Frames[frameIndex]) + return STATUS_INVALID_PARAMETER; + + size_t chunk = PAGE_SIZE - pageOffset; + if (chunk > bytes) + chunk = (size_t)bytes; + + const uint8_t* src = (const uint8_t*)MmPfnToVirtual(surface->Frames[frameIndex]); + if (!src) + return STATUS_INVALID_PARAMETER; + memcpy(dst, src + pageOffset, chunk); + + dst += chunk; + offset += chunk; + bytes -= chunk; + } + + return STATUS_SUCCESS; +} + // Implements the syscall Init operation. void KeSyscallInit(const HAL_API* hal) { gHal = hal; @@ -141,9 +337,10 @@ static KSTATUS KiAllocHandleForCurrent(OBJECT* obj, uint32_t* handleOut) { } -#define SYSCALL_STRING_MAX 1024 +#define SYSCALL_STRING_MAX SYS_PATH_MAX #define SYSCALL_TEXT_MAX_BYTES (64ULL * 1024ULL) #define SYSCALL_IO_MAX_BYTES (16ULL * 1024ULL * 1024ULL) +#define SYSCALL_FILE_IO_CHUNK_BYTES (64ULL * 1024ULL) #define SYSCALL_BLIT_MAX_BYTES (16ULL * 1024ULL * 1024ULL) // Copies user Request. @@ -225,6 +422,13 @@ static int KiMulSize(size_t a, size_t b, size_t* out) { return 1; } +static size_t KiAlignUpSize(size_t value, size_t alignment) { + if (!alignment) + return value; + size_t mask = alignment - 1; + return (value + mask) & ~mask; +} + // Individual syscall handlers. // SYS_EXIT_PROCESS (0) a0 = exitCode. @@ -247,30 +451,263 @@ static uint64_t SyscallExitThread(uint64_t a0, uint64_t a1, uint64_t a2, while (true); } -// SYS_CREATE_THREAD (2) a0=entry a1=arg a2=priority a3=*tidOut. +// SYS_CREATE_THREAD a0=*SYS_CREATE_THREAD_REQUEST. +// Creates a runnable user thread and optionally returns both its waitable +// handle and diagnostic TID. The handle is the synchronization identity. static uint64_t SyscallCreateThread(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a4; (void)a5; + (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; + + SYS_CREATE_THREAD_REQUEST req; + KSTATUS s = KiCopyUserRequest(a0, &req, sizeof(req)); + if (!KSUCCESS(s)) return s; + PROCESS* proc = KeGetCurrentProcess(); if (!proc) return STATUS_FAILURE; - if (!a0) return STATUS_INVALID_PARAMETER; - if (a2 >= PriorityCount) return STATUS_INVALID_PARAMETER; + if (!req.Entry) return STATUS_INVALID_PARAMETER; + if (req.Priority >= PriorityCount) return STATUS_INVALID_PARAMETER; + if (req.Flags) return STATUS_INVALID_PARAMETER; + if (!req.ThreadHandleOut && !req.ThreadIdOut) return STATUS_INVALID_PARAMETER; - KSTATUS s = MmProbeUserRead((const void*)(uintptr_t)a0, 1); + s = MmProbeUserRead((const void*)req.Entry, 1); if (!KSUCCESS(s)) return s; - if (a3) { - s = MmProbeUserWrite((void*)(uintptr_t)a3, sizeof(uint32_t)); + if (req.ThreadHandleOut) { + s = MmProbeUserWrite(req.ThreadHandleOut, sizeof(*req.ThreadHandleOut)); + if (!KSUCCESS(s)) return s; + } + if (req.ThreadIdOut) { + s = MmProbeUserWrite(req.ThreadIdOut, sizeof(*req.ThreadIdOut)); if (!KSUCCESS(s)) return s; } THREAD* newt = NULL; - s = KeCreateThread(proc, (uintptr_t)a0, (uintptr_t)a1, - (THREAD_PRIORITY)a2, &newt); + s = KeCreateThread(proc, req.Entry, req.Argument, + (THREAD_PRIORITY)req.Priority, &newt); if (!KSUCCESS(s)) return s; - if (a3) return KiCopyU32ToUser(a3, newt->ThreadId); + + uint32_t threadHandle = 0; + if (req.ThreadHandleOut) { + s = KeAllocateHandleWithRights(proc, &newt->Header, + HANDLE_RIGHT_WAIT | HANDLE_RIGHT_QUERY, + &threadHandle); + if (!KSUCCESS(s)) { + ObDereferenceObject(&newt->Header); + return s; + } + } + + if (req.ThreadHandleOut) { + s = KiCopyU32ToUser((uint64_t)(uintptr_t)req.ThreadHandleOut, threadHandle); + if (!KSUCCESS(s)) { + KeCloseHandle(proc, threadHandle); + ObDereferenceObject(&newt->Header); + return s; + } + } + + if (req.ThreadIdOut) { + s = KiCopyU32ToUser((uint64_t)(uintptr_t)req.ThreadIdOut, newt->ThreadId); + if (!KSUCCESS(s)) { + if (threadHandle) KeCloseHandle(proc, threadHandle); + ObDereferenceObject(&newt->Header); + return s; + } + } + + ObDereferenceObject(&newt->Header); return STATUS_SUCCESS; } +static void KiFinishAlertableUserWait(uint8_t alertable) { + if (!alertable) return; + + THREAD* thread = KeGetCurrentThread(); + if (thread && thread->UserApcPending) + thread->UserApcDeliverable = 1; +} + +// Maps a waitable handle to its passive event backing object. +static KSTATUS KiHandleToWaitEvent(PROCESS* proc, uint32_t handle, EVENT** eventOut, OBJECT** objectOut) { + if (!proc || !eventOut || !objectOut) return STATUS_INVALID_PARAMETER; + *eventOut = (EVENT*)0; + *objectOut = (OBJECT*)0; + + OBJECT* obj = KeLookupHandleWithRights(proc, handle, HANDLE_RIGHT_WAIT); + if (!obj) return STATUS_INVALID_HANDLE; + + EVENT* event = (EVENT*)0; + switch (obj->Type) { + case ObjectTypeProcess: + event = CONTAINING_RECORD(obj, PROCESS, Header)->ExitEvent; + break; + case ObjectTypeThread: + event = CONTAINING_RECORD(obj, THREAD, Header)->ExitEvent; + break; + case ObjectTypeEvent: + event = CONTAINING_RECORD(obj, EVENT, Header); + break; + case ObjectTypeAsyncIo: + event = IoGetAsyncFileRequestWaitEvent((IO_ASYNC_FILE_REQUEST_OBJECT*)obj); + break; + case ObjectTypeInputQueue: + event = KeInputQueueWaitEvent(CONTAINING_RECORD(obj, INPUT_QUEUE, Header)); + break; + default: + ObDereferenceObject(obj); + return STATUS_NOT_SUPPORTED; + } + + if (!event) { + ObDereferenceObject(obj); + return STATUS_OBJECT_DEAD; + } + + *eventOut = event; + *objectOut = obj; + return STATUS_SUCCESS; +} + +// SYS_WAIT_SINGLE a0=handle a1=SYS_WAIT_FLAG_* a2=timeoutMs. +// Waits on one handle. Passive objects use the wait-many engine; mutexes and +// semaphores keep their native consuming semantics for single-object waits. +static uint64_t SyscallWaitSingle(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a3; (void)a4; (void)a5; + + uint32_t flags = (uint32_t)a1; + uint64_t timeoutMs = a2; + if (flags & ~SYS_WAIT_FLAG_ALERTABLE) return STATUS_INVALID_PARAMETER; + + PROCESS* proc = KeGetCurrentProcess(); + if (!proc) return STATUS_FAILURE; + + OBJECT* obj = KeLookupHandleWithRights(proc, (uint32_t)a0, HANDLE_RIGHT_WAIT); + if (!obj) return STATUS_INVALID_HANDLE; + + uint8_t alertable = (flags & SYS_WAIT_FLAG_ALERTABLE) ? 1 : 0; + KSTATUS s = STATUS_NOT_SUPPORTED; + + if (obj->Type == ObjectTypeMutex) { + if (alertable) s = STATUS_NOT_SUPPORTED; + else if (timeoutMs == 0) { + s = KeTryAcquireMutex(CONTAINING_RECORD(obj, MUTEX, Header)); + if (s == STATUS_FAILURE) s = STATUS_TIMEOUT; + } else if (timeoutMs == SYS_WAIT_TIMEOUT_INFINITE) { + s = KeAcquireMutex(CONTAINING_RECORD(obj, MUTEX, Header)); + } else { + s = STATUS_NOT_SUPPORTED; + } + ObDereferenceObject(obj); + return s; + } + + if (obj->Type == ObjectTypeSemaphore) { + if (alertable) s = STATUS_NOT_SUPPORTED; + else if (timeoutMs == 0) { + s = KeTrySemaphore(CONTAINING_RECORD(obj, SEMAPHORE, Header)); + if (s == STATUS_FAILURE) s = STATUS_TIMEOUT; + } else if (timeoutMs == SYS_WAIT_TIMEOUT_INFINITE) { + s = KeWaitSemaphore(CONTAINING_RECORD(obj, SEMAPHORE, Header)); + } else { + s = STATUS_NOT_SUPPORTED; + } + ObDereferenceObject(obj); + return s; + } + + ObDereferenceObject(obj); + + EVENT* event = (EVENT*)0; + OBJECT* ref = (OBJECT*)0; + s = KiHandleToWaitEvent(proc, (uint32_t)a0, &event, &ref); + if (!KSUCCESS(s)) return s; + + uint32_t selected = 0; + s = KeWaitForMultipleEvents(&event, 1, alertable, timeoutMs, &selected); + ObDereferenceObject(ref); + if (s != STATUS_TIMEOUT && s != STATUS_NOT_SUPPORTED) + KiFinishAlertableUserWait(alertable); + return s; +} + +// SYS_WAIT_MANY a0=*SYS_WAIT_MANY_REQUEST. +// Waits until any passive waitable handle becomes signaled. +static uint64_t SyscallWaitMany(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; + + SYS_WAIT_MANY_REQUEST req; + KSTATUS s = KiCopyUserRequest(a0, &req, sizeof(req)); + if (!KSUCCESS(s)) return s; + if (req.Size != sizeof(req)) return STATUS_INVALID_PARAMETER; + if (req.Flags & ~SYS_WAIT_FLAG_ALERTABLE) return STATUS_INVALID_PARAMETER; + if (!req.Handles || req.Count == 0 || req.Count > SYS_WAIT_MANY_MAX_HANDLES) + return STATUS_INVALID_PARAMETER; + + uint32_t handles[SYS_WAIT_MANY_MAX_HANDLES]; + size_t handleBytes = (size_t)req.Count * sizeof(handles[0]); + s = CopyFromUser(handles, req.Handles, handleBytes); + if (!KSUCCESS(s)) return s; + + PROCESS* proc = KeGetCurrentProcess(); + if (!proc) return STATUS_FAILURE; + + EVENT* events[SYS_WAIT_MANY_MAX_HANDLES]; + OBJECT* refs[SYS_WAIT_MANY_MAX_HANDLES]; + for (uint32_t i = 0; i < SYS_WAIT_MANY_MAX_HANDLES; i++) { + events[i] = (EVENT*)0; + refs[i] = (OBJECT*)0; + } + + for (uint32_t i = 0; i < req.Count; i++) { + s = KiHandleToWaitEvent(proc, handles[i], &events[i], &refs[i]); + if (!KSUCCESS(s)) { + for (uint32_t j = 0; j < i; j++) + ObDereferenceObject(refs[j]); + return s; + } + } + + uint8_t alertable = (req.Flags & SYS_WAIT_FLAG_ALERTABLE) ? 1 : 0; + uint32_t selected = 0; + s = KeWaitForMultipleEvents(events, req.Count, alertable, req.TimeoutMilliseconds, &selected); + + for (uint32_t i = 0; i < req.Count; i++) + ObDereferenceObject(refs[i]); + + if (KSUCCESS(s) && req.SignaledIndexOut) { + KSTATUS cs = KiCopyU32ToUser((uint64_t)(uintptr_t)req.SignaledIndexOut, selected); + if (!KSUCCESS(cs)) return cs; + } + + if (s != STATUS_TIMEOUT && s != STATUS_NOT_SUPPORTED) + KiFinishAlertableUserWait(alertable); + return s; +} + +// SYS_OPEN_INPUT_QUEUE a0=flags a1=*handleOut. +static uint64_t SyscallOpenInputQueue(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a2; (void)a3; (void)a4; (void)a5; + if (a0 != 0) return STATUS_INVALID_PARAMETER; + if (!a1) return STATUS_INVALID_PARAMETER; + + INPUT_QUEUE* queue = (INPUT_QUEUE*)0; + KSTATUS s = KeInputOpenQueue(&queue); + if (!KSUCCESS(s)) return s; + + uint32_t handle = 0; + s = KiAllocHandleForCurrentWithRights(&queue->Header, HANDLE_RIGHT_WAIT | HANDLE_RIGHT_QUERY, &handle); + if (!KSUCCESS(s)) return s; + + s = KiCopyHandleToUser(a1, handle); + if (!KSUCCESS(s)) { + PROCESS* proc = KeGetCurrentProcess(); + if (proc) KeCloseHandle(proc, handle); + } + return s; +} + // SYS_GET_CURRENT_TID (3). static uint64_t SyscallGetCurrentTid(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -299,11 +736,7 @@ static uint64_t SyscallYield(uint64_t a0, uint64_t a1, uint64_t a2, static uint64_t SyscallSleep(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - if (!gHal) return STATUS_FAILURE; - uint64_t deadline = gHal->Timer.QueryTicks() + ((uint64_t)a0 * 1000u); - while (gHal->Timer.QueryTicks() < deadline) - KeYield(); - return STATUS_SUCCESS; + return KeDelayThread(a0); } // SYS_SET_USER_APC_DISPATCHER a0 = dispatcher entry in syscall.dll/user image. @@ -391,24 +824,16 @@ static uint64_t SyscallVmAlloc(uint64_t a0, uint64_t a1, uint64_t a2, if (!process) return STATUS_FAILURE; void* base = (void*)0; - s = MmCreateUserRegion(process, pageCount * PAGE_SIZE, PAGE_SIZE, - pageFlags ? pageFlags : (PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX), - USER_REGION_ZERO, - UserRegionGeneric, + s = MmVadCreateCommitted(process, pageCount * PAGE_SIZE, PAGE_SIZE, + pageFlags, + MM_VAD_ZERO, + MmVadGeneric, &base); if (!KSUCCESS(s)) return s; - if (protect == SYS_VM_NOACCESS) { - s = MmProtectUserRegion(process, base, pageCount * PAGE_SIZE, 0); - if (!KSUCCESS(s)) { - MmDestroyUserRegion(process, base); - return s; - } - } - s = CopyToUser(baseOut, &base, sizeof(base)); if (!KSUCCESS(s)) { - MmDestroyUserRegion(process, base); + MmVadDestroy(process, base); return s; } return STATUS_SUCCESS; @@ -419,7 +844,7 @@ static uint64_t SyscallVmFree(uint64_t a0, uint64_t a1, uint64_t a2, (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; PROCESS* process = KeGetCurrentProcess(); if (!process || !a0) return STATUS_INVALID_PARAMETER; - return MmDestroyUserRegion(process, (void*)(uintptr_t)a0); + return MmVadDestroy(process, (void*)(uintptr_t)a0); } static uint64_t SyscallVmProtect(uint64_t a0, uint64_t a1, uint64_t a2, @@ -439,16 +864,16 @@ static uint64_t SyscallVmProtect(uint64_t a0, uint64_t a1, uint64_t a2, void* regionBase = (void*)0; size_t regionSize = 0; uint64_t oldFlags = 0; - USER_REGION_TYPE type = UserRegionGeneric; + MM_VAD_TYPE type = MmVadGeneric; uint32_t regionFlags = 0; - s = MmQueryUserRegion(process, (void*)(uintptr_t)a0, ®ionBase, ®ionSize, &oldFlags, &type, ®ionFlags); + s = MmVadQuery(process, (void*)(uintptr_t)a0, ®ionBase, ®ionSize, &oldFlags, &type, ®ionFlags); if (!KSUCCESS(s)) return s; uint32_t oldProtect = KiPageFlagsToVmProtect(oldFlags); s = CopyToUser((void*)(uintptr_t)a3, &oldProtect, sizeof(oldProtect)); if (!KSUCCESS(s)) return s; } - return MmProtectUserRegion(process, (void*)(uintptr_t)a0, (size_t)a1 * PAGE_SIZE, pageFlags); + return MmVadProtect(process, (void*)(uintptr_t)a0, (size_t)a1 * PAGE_SIZE, pageFlags); } static uint64_t SyscallVmQuery(uint64_t a0, uint64_t a1, uint64_t a2, @@ -462,9 +887,9 @@ static uint64_t SyscallVmQuery(uint64_t a0, uint64_t a1, uint64_t a2, void* base = (void*)0; size_t size = 0; uint64_t pageFlags = 0; - USER_REGION_TYPE type = UserRegionGeneric; + MM_VAD_TYPE type = MmVadGeneric; uint32_t regionFlags = 0; - KSTATUS s = MmQueryUserRegion(process, (void*)(uintptr_t)a0, &base, &size, &pageFlags, &type, ®ionFlags); + KSTATUS s = MmVadQuery(process, (void*)(uintptr_t)a0, &base, &size, &pageFlags, &type, ®ionFlags); if (!KSUCCESS(s)) return s; SYS_VM_REGION_INFO info; @@ -496,17 +921,6 @@ static uint64_t SyscallMutexCreate(uint64_t a0, uint64_t a1, uint64_t a2, return s; } -// SYS_ACQUIRE_MUTEX (12) a0 = handle. -static uint64_t SyscallMutexAcquire(uint64_t a0, uint64_t a1, uint64_t a2, - uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - MUTEX* mx = (MUTEX*)KiHandleToObject((uint32_t)a0, ObjectTypeMutex, HANDLE_RIGHT_WAIT); - if (!mx) return STATUS_NOT_FOUND; - KSTATUS s = KeAcquireMutex(mx); - ObDereferenceObject(&mx->Header); - return s; -} - // SYS_TRY_ACQUIRE_MUTEX (13) a0 = handle. static uint64_t SyscallMutexTryAcquire(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -548,17 +962,6 @@ static uint64_t SyscallSemCreate(uint64_t a0, uint64_t a1, uint64_t a2, return s; } -// SYS_WAIT_SEM (16) a0 = handle. -static uint64_t SyscallSemWait(uint64_t a0, uint64_t a1, uint64_t a2, - uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - SEMAPHORE* sem = (SEMAPHORE*)KiHandleToObject((uint32_t)a0, ObjectTypeSemaphore, HANDLE_RIGHT_WAIT); - if (!sem) return STATUS_NOT_FOUND; - KSTATUS s = KeWaitSemaphore(sem); - ObDereferenceObject(&sem->Header); - return s; -} - // SYS_TRY_WAIT_SEM (17) a0 = handle. static uint64_t SyscallSemTryWait(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -622,23 +1025,6 @@ static uint64_t SyscallEventClear(uint64_t a0, uint64_t a1, uint64_t a2, return STATUS_SUCCESS; } -// SYS_WAIT_EVENT a0 = handle, a1 = alertable. -static uint64_t SyscallEventWait(uint64_t a0, uint64_t a1, uint64_t a2, - uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a2; (void)a3; (void)a4; (void)a5; - EVENT* ev = (EVENT*)KiHandleToObject((uint32_t)a0, ObjectTypeEvent, HANDLE_RIGHT_WAIT); - if (!ev) return STATUS_NOT_FOUND; - uint8_t alertable = a1 ? 1 : 0; - KSTATUS s = KeWaitForEvent(ev, alertable); - if (alertable) { - THREAD* thread = KeGetCurrentThread(); - if (thread && thread->UserApcPending) - thread->UserApcDeliverable = 1; - } - ObDereferenceObject(&ev->Header); - return s; -} - // SYS_CREATE_QUEUE (23) a0 = *handleOut. static uint64_t SyscallQueueCreate(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -698,11 +1084,193 @@ static uint64_t SyscallQueueRecv(uint64_t a0, uint64_t a1, uint64_t a2, // on 'obj'. If that was the last reference, ObDereferenceObject runs the // type-specific Destroy callback (KiFileDestroy, etc.) and frees the pool memory. // No per-type close syscall needed or wanted. + +static uint64_t SyscallCreateFileMapping(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a5; + uint32_t fileHandle = (uint32_t)a0; + uint32_t protect = (uint32_t)a1; + uint32_t flags = (uint32_t)a2; + uint64_t maximumSize = a3; + uint32_t* handleOut = (uint32_t*)(uintptr_t)a4; + + if (!fileHandle || !handleOut) + return STATUS_INVALID_PARAMETER; + + uint64_t pageFlags = 0; + KSTATUS s = KiVmProtectToPageFlags(protect, &pageFlags); + if (!KSUCCESS(s)) return s; + if (!pageFlags) return STATUS_ACCESS_DENIED; + + uint32_t neededRights = HANDLE_RIGHT_READ | HANDLE_RIGHT_QUERY; + if (pageFlags & PAGE_WRITE) + neededRights |= HANDLE_RIGHT_WRITE; + + FILE_OBJECT* file = (FILE_OBJECT*)KiHandleToObject(fileHandle, ObjectTypeFile, neededRights); + if (!file) return STATUS_NOT_FOUND; + + if (maximumSize == 0) + maximumSize = IoGetFileSize(file); + + FILE_MAPPING_OBJECT* mapping = (FILE_MAPPING_OBJECT*)ObAllocateObject(ObjectTypeFileMapping, + sizeof(FILE_MAPPING_OBJECT)); + if (!mapping) { + ObDereferenceObject(&file->Header); + return STATUS_NO_MEMORY; + } + + mapping->Header.Destroy = KiFileMappingDestroy; + mapping->File = file; // KiHandleToObject returned the mapping object's file reference. + mapping->MaximumSize = maximumSize; + mapping->PageFlags = pageFlags; + mapping->Protect = protect; + mapping->Flags = flags; + + uint32_t h = 0; + s = KiAllocHandleForCurrentWithRights(&mapping->Header, + HANDLE_RIGHT_READ | HANDLE_RIGHT_QUERY, + &h); + if (!KSUCCESS(s)) + return s; + + s = KiCopyHandleToUser((uint64_t)(uintptr_t)handleOut, h); + if (!KSUCCESS(s)) { + PROCESS* proc = KeGetCurrentProcess(); + if (proc) KeCloseHandle(proc, h); + } + return s; +} + +static KSTATUS KiMapAccessToPageFlags(const FILE_MAPPING_OBJECT* mapping, + uint32_t access, + uint64_t* pageFlagsOut) { + if (!mapping || !pageFlagsOut) + return STATUS_INVALID_PARAMETER; + + uint64_t mappingFlags = mapping->PageFlags; + if (!mappingFlags) + return STATUS_ACCESS_DENIED; + + if (access == 0) + access = SYS_MAP_ACCESS_READ; + + if ((access & SYS_MAP_ACCESS_WRITE) && (access & SYS_MAP_ACCESS_COPY)) + return STATUS_INVALID_PARAMETER; + + if (((access & SYS_MAP_ACCESS_COPY) || (access & SYS_MAP_ACCESS_WRITE)) && + (mappingFlags & PAGE_WRITE) == 0) + return STATUS_ACCESS_DENIED; + + uint64_t flags = PAGE_PRESENT | PAGE_USER; + if ((access & SYS_MAP_ACCESS_COPY) || (access & SYS_MAP_ACCESS_WRITE)) + flags |= PAGE_WRITE; + + if (access & SYS_MAP_ACCESS_EXECUTE) { + if (mappingFlags & PAGE_NX) + return STATUS_ACCESS_DENIED; + } else { + flags |= PAGE_NX; + } + + *pageFlagsOut = flags; + return STATUS_SUCCESS; +} + +static uint64_t SyscallMapFile(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + uint32_t mappingHandle = (uint32_t)a0; + uint64_t offset = a1; + size_t size = (size_t)a2; + uint32_t access = (uint32_t)a3; + uint32_t flags = (uint32_t)a4; + void** baseOut = (void**)(uintptr_t)a5; + + if (!mappingHandle || !baseOut) + return STATUS_INVALID_PARAMETER; + if (offset & (PAGE_SIZE - 1)) + return STATUS_INVALID_PARAMETER; + + FILE_MAPPING_OBJECT* mapping = (FILE_MAPPING_OBJECT*)KiHandleToObject(mappingHandle, + ObjectTypeFileMapping, + HANDLE_RIGHT_READ); + if (!mapping) + return STATUS_NOT_FOUND; + + uint64_t maximumSize = mapping->MaximumSize; + if (maximumSize == 0) + maximumSize = IoGetFileSize(mapping->File); + + if (offset >= maximumSize) { + ObDereferenceObject(&mapping->Header); + return STATUS_INVALID_PARAMETER; + } + if (size == 0) { + uint64_t tail = maximumSize - offset; + if (tail > (uint64_t)((size_t)-1)) { + ObDereferenceObject(&mapping->Header); + return STATUS_NO_MEMORY; + } + size = (size_t)tail; + } + if (size == 0) { + ObDereferenceObject(&mapping->Header); + return STATUS_INVALID_PARAMETER; + } + if ((uint64_t)size > maximumSize - offset) { + ObDereferenceObject(&mapping->Header); + return STATUS_INVALID_PARAMETER; + } + + uint64_t pageFlags = 0; + KSTATUS s = KiMapAccessToPageFlags(mapping, access, &pageFlags); + if (!KSUCCESS(s)) { + ObDereferenceObject(&mapping->Header); + return s; + } + + PROCESS* process = KeGetCurrentProcess(); + if (!process) { + ObDereferenceObject(&mapping->Header); + return STATUS_FAILURE; + } + + uint32_t vadFlags = flags; + if (access & SYS_MAP_ACCESS_WRITE) + vadFlags |= MM_VAD_SHARED_WRITE; + + void* base = (void*)0; + s = MmVadCreateFileMapping(process, mapping->File, offset, size, + pageFlags, vadFlags, &base); + ObDereferenceObject(&mapping->Header); + if (!KSUCCESS(s)) + return s; + + s = CopyToUser(baseOut, &base, sizeof(base)); + if (!KSUCCESS(s)) { + MmVadDestroy(process, base); + return s; + } + return STATUS_SUCCESS; +} + static uint64_t SyscallCloseHandle(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; PROCESS* proc = KeGetCurrentProcess(); if (!proc) return STATUS_FAILURE; + + OBJECT* obj = KeLookupHandleWithRights(proc, (uint32_t)a0, 0); + if (obj) { + if (obj->Type == ObjectTypeFile) { + KSTATUS fs = IoFlushFile((FILE_OBJECT*)obj); + ObDereferenceObject(obj); + if (!KSUCCESS(fs)) + return fs; + } else { + ObDereferenceObject(obj); + } + } + KeCloseHandle(proc, (uint32_t)a0); return STATUS_SUCCESS; } @@ -739,70 +1307,165 @@ static uint64_t SyscallOpenFile(uint64_t a0, uint64_t a1, uint64_t a2, return s; } -// SYS_READ_FILE (28) a0=handle a1=buf a2=size a3=*bytesReadOut. +static void* KiAllocateSyscallIoBuffer(size_t requested, size_t* actualOut) { + if (actualOut) *actualOut = 0; + if (!requested) return NULL; + + size_t chunk = requested < SYSCALL_FILE_IO_CHUNK_BYTES ? requested : SYSCALL_FILE_IO_CHUNK_BYTES; + for (;;) { + void* buf = MmAllocatePoolTag(chunk, MM_POOL_TAG('s', 'i', 'o', ' ')); + if (buf) { + if (actualOut) *actualOut = chunk; + return buf; + } + if (chunk <= PAGE_SIZE) + break; + chunk >>= 1; + if (chunk < PAGE_SIZE) + chunk = PAGE_SIZE; + } + return NULL; +} + +// Imports and validates a synchronous file I/O request. +static KSTATUS KiImportSyncFileIoRequest(uint64_t arg, SYS_FILE_IO_REQUEST* req) { + KSTATUS s = KiCopyUserRequest(arg, req, sizeof(*req)); + if (!KSUCCESS(s)) + return s; + + if (req->Flags != 0) + return STATUS_INVALID_PARAMETER; + if (req->Size > SYSCALL_IO_MAX_BYTES) + return STATUS_INVALID_PARAMETER; + if (req->Size && !req->Buffer) + return STATUS_INVALID_PARAMETER; + + return STATUS_SUCCESS; +} + +// SYS_READ_FILE a0=*SYS_FILE_IO_REQUEST. static uint64_t SyscallReadFile(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a4; (void)a5; - size_t size = (size_t)a2; - if (size && !a1) return STATUS_INVALID_PARAMETER; - if (size > SYSCALL_IO_MAX_BYTES) return STATUS_INVALID_PARAMETER; + (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - if (size == 0) { - if (a3) return KiCopySizeToUser(a3, 0); + SYS_FILE_IO_REQUEST req; + KSTATUS s = KiImportSyncFileIoRequest(a0, &req); + if (!KSUCCESS(s)) + return s; + + if (req.Size == 0) { + if (req.TransferredOut) + return KiCopySizeToUser((uint64_t)(uintptr_t)req.TransferredOut, 0); return STATUS_SUCCESS; } - KSTATUS s = MmProbeUserWrite((void*)(uintptr_t)a1, size); - if (!KSUCCESS(s)) return s; + FILE_OBJECT* file = (FILE_OBJECT*)KiHandleToObject(req.FileHandle, ObjectTypeFile, HANDLE_RIGHT_READ); + if (!file) return STATUS_NOT_FOUND; - void* kbuf = MmAllocatePool(size); - if (!kbuf) return STATUS_NO_MEMORY; + size_t chunkSize = 0; + void* kbuf = KiAllocateSyscallIoBuffer(req.Size, &chunkSize); + if (!kbuf) { + ObDereferenceObject(&file->Header); + return STATUS_NO_MEMORY; + } - FILE_OBJECT* file = (FILE_OBJECT*)KiHandleToObject((uint32_t)a0, ObjectTypeFile, HANDLE_RIGHT_READ); - if (!file) { MmFreePool(kbuf); return STATUS_NOT_FOUND; } + uintptr_t user = (uintptr_t)req.Buffer; + size_t total = 0; + s = STATUS_SUCCESS; - size_t got = 0; - s = IoReadFile(file, kbuf, size, &got); + while (total < req.Size) { + size_t todo = req.Size - total; + if (todo > chunkSize) todo = chunkSize; + + if (user > (uintptr_t)-1 - total) { + s = STATUS_INVALID_PARAMETER; + break; + } + + void* userChunk = (void*)(user + total); + s = MmProbeUserWrite(userChunk, todo); + if (!KSUCCESS(s)) break; + + size_t got = 0; + s = IoReadFile(file, kbuf, todo, &got); + if (!KSUCCESS(s)) break; + + if (got) { + s = CopyToUser(userChunk, kbuf, got); + if (!KSUCCESS(s)) break; + total += got; + } + + if (got < todo) + break; + } + + MmFreePool(kbuf); ObDereferenceObject(&file->Header); - if (KSUCCESS(s)) - s = CopyToUser((void*)(uintptr_t)a1, kbuf, got); - MmFreePool(kbuf); - - if (KSUCCESS(s) && a3) - s = KiCopySizeToUser(a3, got); + if (req.TransferredOut) { + KSTATUS cs = KiCopySizeToUser((uint64_t)(uintptr_t)req.TransferredOut, total); + if (KSUCCESS(s)) s = cs; + } return s; } -// SYS_WRITE_FILE (29) a0=handle a1=buf a2=size a3=*bytesWrittenOut. +// SYS_WRITE_FILE a0=*SYS_FILE_IO_REQUEST. static uint64_t SyscallWriteFile(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a4; (void)a5; - size_t size = (size_t)a2; - if (size && !a1) return STATUS_INVALID_PARAMETER; - if (size > SYSCALL_IO_MAX_BYTES) return STATUS_INVALID_PARAMETER; + (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - if (size == 0) { - if (a3) return KiCopySizeToUser(a3, 0); + SYS_FILE_IO_REQUEST req; + KSTATUS s = KiImportSyncFileIoRequest(a0, &req); + if (!KSUCCESS(s)) + return s; + + if (req.Size == 0) { + if (req.TransferredOut) + return KiCopySizeToUser((uint64_t)(uintptr_t)req.TransferredOut, 0); return STATUS_SUCCESS; } - void* kbuf = MmAllocatePool(size); - if (!kbuf) return STATUS_NO_MEMORY; + FILE_OBJECT* file = (FILE_OBJECT*)KiHandleToObject(req.FileHandle, ObjectTypeFile, HANDLE_RIGHT_WRITE); + if (!file) return STATUS_NOT_FOUND; - KSTATUS s = CopyFromUser(kbuf, (const void*)(uintptr_t)a1, size); - if (!KSUCCESS(s)) { MmFreePool(kbuf); return s; } + size_t chunkSize = 0; + void* kbuf = KiAllocateSyscallIoBuffer(req.Size, &chunkSize); + if (!kbuf) { + ObDereferenceObject(&file->Header); + return STATUS_NO_MEMORY; + } - FILE_OBJECT* file = (FILE_OBJECT*)KiHandleToObject((uint32_t)a0, ObjectTypeFile, HANDLE_RIGHT_WRITE); - if (!file) { MmFreePool(kbuf); return STATUS_NOT_FOUND; } + uintptr_t user = (uintptr_t)req.Buffer; + size_t total = 0; + s = STATUS_SUCCESS; + + while (total < req.Size) { + size_t todo = req.Size - total; + if (todo > chunkSize) todo = chunkSize; + + if (user > (uintptr_t)-1 - total) { + s = STATUS_INVALID_PARAMETER; + break; + } + + s = CopyFromUser(kbuf, (const void*)(user + total), todo); + if (!KSUCCESS(s)) break; + + size_t written = 0; + s = IoWriteFile(file, kbuf, todo, &written); + if (!KSUCCESS(s)) break; + + total += written; + if (written < todo) + break; + } - size_t written = 0; - s = IoWriteFile(file, kbuf, size, &written); - ObDereferenceObject(&file->Header); MmFreePool(kbuf); + ObDereferenceObject(&file->Header); - if (a3) { - KSTATUS cs = KiCopySizeToUser(a3, written); + if (req.TransferredOut) { + KSTATUS cs = KiCopySizeToUser((uint64_t)(uintptr_t)req.TransferredOut, total); if (KSUCCESS(s)) s = cs; } return s; @@ -879,14 +1542,8 @@ static uint64_t SyscallAsyncFileCommon(uint64_t a0, bool write) { if (!req.Buffer && req.Size) return STATUS_INVALID_PARAMETER; if (req.Size > SYSCALL_IO_MAX_BYTES) return STATUS_INVALID_PARAMETER; - s = MmProbeUserWrite(req.Iosb, sizeof(*req.Iosb)); - if (!KSUCCESS(s)) return s; - - if (req.Size) { - s = write ? MmProbeUserRead(req.Buffer, req.Size) - : MmProbeUserWrite(req.Buffer, req.Size); - if (!KSUCCESS(s)) return s; - } + PROCESS* proc = KeGetCurrentProcess(); + if (!proc) return STATUS_FAILURE; if (req.RequestHandleOut) { s = MmProbeUserWrite(req.RequestHandleOut, sizeof(*req.RequestHandleOut)); @@ -916,7 +1573,7 @@ static uint64_t SyscallAsyncFileCommon(uint64_t a0, bool write) { return STATUS_INVALID_PARAMETER; } - apcCtx = (SYS_USER_ASYNC_APC_CONTEXT*)MmAllocatePool(sizeof(SYS_USER_ASYNC_APC_CONTEXT)); + apcCtx = (SYS_USER_ASYNC_APC_CONTEXT*)MmAllocatePoolTag(sizeof(SYS_USER_ASYNC_APC_CONTEXT), MM_POOL_TAG('u', 'a', 'p', 'c')); if (!apcCtx) { if (event) ObDereferenceObject(&event->Header); ObDereferenceObject(&file->Header); @@ -937,32 +1594,53 @@ static uint64_t SyscallAsyncFileCommon(uint64_t a0, bool write) { completionApc = &apcCtx->KernelApc; } + MM_LOCKED_USER_BUFFER* lockedBuffer = (MM_LOCKED_USER_BUFFER*)0; + MM_LOCKED_USER_BUFFER* lockedIosb = (MM_LOCKED_USER_BUFFER*)0; + + s = MmLockUserBuffer(proc, req.Iosb, sizeof(*req.Iosb), 1, &lockedIosb); + if (!KSUCCESS(s)) { + if (apcCtx) + SyscallUserAsyncApcContextFree(apcCtx); + if (event) ObDereferenceObject(&event->Header); + ObDereferenceObject(&file->Header); + return s; + } + + if (req.Size) { + s = MmLockUserBuffer(proc, req.Buffer, req.Size, write ? 0 : 1, &lockedBuffer); + if (!KSUCCESS(s)) { + MmUnlockUserBuffer(lockedIosb); + if (apcCtx) + SyscallUserAsyncApcContextFree(apcCtx); + if (event) ObDereferenceObject(&event->Header); + ObDereferenceObject(&file->Header); + return s; + } + } + IO_ASYNC_FILE_REQUEST_OBJECT* asyncReq = (IO_ASYNC_FILE_REQUEST_OBJECT*)0; if (write) { s = IoWriteFileAsyncKernel(file, req.Buffer, req.Size, req.Offset, (IO_STATUS_BLOCK*)req.Iosb, event, completionApc, + lockedBuffer, + lockedIosb, &asyncReq); } else { s = IoReadFileAsyncKernel(file, req.Buffer, req.Size, req.Offset, (IO_STATUS_BLOCK*)req.Iosb, event, completionApc, + lockedBuffer, + lockedIosb, &asyncReq); } + lockedBuffer = (MM_LOCKED_USER_BUFFER*)0; + lockedIosb = (MM_LOCKED_USER_BUFFER*)0; if (s == STATUS_PENDING && req.RequestHandleOut && asyncReq) { - PROCESS* proc = KeGetCurrentProcess(); - if (!proc) { - (void)IoCancelAsyncFileRequest(asyncReq); - ObDereferenceObject(IoAsyncFileRequestObject(asyncReq)); - if (event) ObDereferenceObject(&event->Header); - ObDereferenceObject(&file->Header); - return STATUS_FAILURE; - } - uint32_t requestHandle = 0; KSTATUS hs = KeAllocateHandleWithRights(proc, IoAsyncFileRequestObject(asyncReq), - HANDLE_RIGHT_QUERY | HANDLE_RIGHT_TERMINATE, + HANDLE_RIGHT_WAIT | HANDLE_RIGHT_QUERY | HANDLE_RIGHT_TERMINATE, &requestHandle); if (!KSUCCESS(hs)) { (void)IoCancelAsyncFileRequest(asyncReq); @@ -986,7 +1664,7 @@ static uint64_t SyscallAsyncFileCommon(uint64_t a0, bool write) { KSTATUS cs = CopyToUser(req.RequestHandleOut, &zeroHandle, sizeof(zeroHandle)); if (!KSUCCESS(cs)) { if (asyncReq) ObDereferenceObject(IoAsyncFileRequestObject(asyncReq)); - if (!KSUCCESS(s) && s != STATUS_PENDING && apcCtx) + if (s != STATUS_PENDING && apcCtx) SyscallUserAsyncApcContextFree(apcCtx); if (event) ObDereferenceObject(&event->Header); ObDereferenceObject(&file->Header); @@ -997,7 +1675,7 @@ static uint64_t SyscallAsyncFileCommon(uint64_t a0, bool write) { if (asyncReq) ObDereferenceObject(IoAsyncFileRequestObject(asyncReq)); // drop creator reference - if (!KSUCCESS(s) && s != STATUS_PENDING && apcCtx) + if (s != STATUS_PENDING && apcCtx) SyscallUserAsyncApcContextFree(apcCtx); if (event) ObDereferenceObject(&event->Header); @@ -1093,6 +1771,17 @@ static uint64_t SyscallTruncateFile(uint64_t a0, uint64_t a1, uint64_t a2, return s; } +// SYS_FLUSH_FILE a0=handle. +static uint64_t SyscallFlushFile(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; + FILE_OBJECT* file = (FILE_OBJECT*)KiHandleToObject((uint32_t)a0, ObjectTypeFile, HANDLE_RIGHT_WRITE); + if (!file) return STATUS_NOT_FOUND; + KSTATUS s = IoFlushFile(file); + ObDereferenceObject(&file->Header); + return s; +} + // SYS_GET_FILE_POS (33) a0=handle -> current seek offset. static uint64_t SyscallGetFilePos(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -1128,31 +1817,280 @@ static uint64_t SyscallGetFileSize(uint64_t a0, uint64_t a1, uint64_t a2, return sz; } -// SYS_BLIT_SCREEN (66) a0=x a1=y a2=w a3=h a4=*pixels. -static uint64_t SyscallBlitScreen(uint64_t a0, uint64_t a1, uint64_t a2, - uint64_t a3, uint64_t a4, uint64_t a5) { +// SYS_CREATE_SURFACE (99) a0=width a1=height a2=format a3=flags a4=*handleOut. +static uint64_t SyscallCreateSurface(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { (void)a5; - if (!a4) return STATUS_FAILURE; + uint32_t width = (uint32_t)a0; + uint32_t height = (uint32_t)a1; + uint32_t format = (uint32_t)a2; + uint32_t flags = (uint32_t)a3; + if (!width || !height || !a4) + return STATUS_INVALID_PARAMETER; + if (format != SYS_SURFACE_FORMAT_XRGB8888 || flags != SYS_SURFACE_FLAG_NONE) + return STATUS_NOT_SUPPORTED; - int w = (int)a2; - int h = (int)a3; - if (w <= 0 || h <= 0) return STATUS_INVALID_PARAMETER; - - size_t pixels = 0; - size_t bytes = 0; - if (!KiMulSize((size_t)w, (size_t)h, &pixels) || - !KiMulSize(pixels, sizeof(uint32_t), &bytes) || - bytes > SYSCALL_BLIT_MAX_BYTES) + DISPLAY_MODE mode; + KSTATUS s = KeDisplayGetMode(&mode); + if (!KSUCCESS(s)) + return s; + if (width > mode.Width || height > mode.Height) return STATUS_INVALID_PARAMETER; - uint32_t* kpixels = (uint32_t*)MmAllocatePool(bytes); - if (!kpixels) return STATUS_NO_MEMORY; + size_t rowBytes = 0; + if (!KiMulSize((size_t)width, sizeof(uint32_t), &rowBytes)) + return STATUS_INVALID_PARAMETER; + size_t pitchBytes = KiAlignUpSize(rowBytes, 64); + if (pitchBytes < rowBytes) + return STATUS_INVALID_PARAMETER; + + size_t payloadSize = 0; + if (!KiMulSize(pitchBytes, (size_t)height, &payloadSize)) + return STATUS_INVALID_PARAMETER; + size_t pageCount = (payloadSize + PAGE_SIZE - 1) / PAGE_SIZE; + if (!pageCount || pageCount > (SIZE_MAX / sizeof(PHYSICAL_ADDRESS))) + return STATUS_INVALID_PARAMETER; + + SURFACE_OBJECT* surface = (SURFACE_OBJECT*)ObAllocateObject(ObjectTypeSurface, sizeof(SURFACE_OBJECT)); + if (!surface) + return STATUS_NO_MEMORY; + surface->Header.Destroy = KiSurfaceDestroy; + surface->Owner = KeGetCurrentProcess(); + surface->Width = width; + surface->Height = height; + surface->PitchBytes = (uint32_t)pitchBytes; + surface->Format = format; + surface->Flags = flags; + surface->PayloadSize = payloadSize; + surface->PageCount = pageCount; + + surface->Frames = (PHYSICAL_ADDRESS*)MmAllocatePoolTag(pageCount * sizeof(PHYSICAL_ADDRESS), MM_POOL_TAG('s', 'f', 'r', 'm')); + surface->RowScratch = (uint32_t*)MmAllocatePoolTag(rowBytes, MM_POOL_TAG('s', 'r', 'o', 'w')); + s = KeMutexCreate(&surface->Lock); + if (!surface->Frames || !surface->RowScratch || !KSUCCESS(s)) { + ObDereferenceObject(&surface->Header); + return STATUS_NO_MEMORY; + } + memset(surface->Frames, 0, pageCount * sizeof(PHYSICAL_ADDRESS)); + + for (size_t i = 0; i < pageCount; ++i) { + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseSurface); + if (!frame) { + ObDereferenceObject(&surface->Header); + return STATUS_NO_MEMORY; + } + surface->Frames[i] = frame; + void* kva = MmPfnToVirtual(frame); + if (kva) + memset(kva, 0, PAGE_SIZE); + } + + uint32_t handle = 0; + s = KiAllocHandleForCurrentWithRights(&surface->Header, HANDLE_RIGHT_READ | HANDLE_RIGHT_WRITE | HANDLE_RIGHT_QUERY, &handle); + if (!KSUCCESS(s)) + return s; + + KiSurfaceStatsRegister(surface); + + s = CopyToUser((void*)(uintptr_t)a4, &handle, sizeof(handle)); + if (!KSUCCESS(s)) { + KeCloseHandle(KeGetCurrentProcess(), handle); + return s; + } + return STATUS_SUCCESS; +} + +// SYS_MAP_SURFACE (100) a0=surfaceHandle a1=*baseOut a2=*pitchBytesOut. +static uint64_t SyscallMapSurface(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a3; (void)a4; (void)a5; + if (!a1 || !a2) + return STATUS_INVALID_PARAMETER; + + SURFACE_OBJECT* surface = (SURFACE_OBJECT*)KiHandleToObject((uint32_t)a0, ObjectTypeSurface, HANDLE_RIGHT_READ | HANDLE_RIGHT_WRITE); + if (!surface) + return STATUS_INVALID_PARAMETER; + + PROCESS* current = KeGetCurrentProcess(); + if (surface->Owner != current) { + ObDereferenceObject(&surface->Header); + return STATUS_ACCESS_DENIED; + } + + KSTATUS s = KeAcquireMutex(surface->Lock); + if (!KSUCCESS(s)) { + ObDereferenceObject(&surface->Header); + return s; + } + + if (!surface->UserBase) { + s = MmVadCreateSharedViewFromFrames(current, surface->Frames, surface->PageCount, + surface->PayloadSize, + PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, + MM_VAD_ZERO, MmVadShared, &surface->UserBase); + } + + void* userBase = surface->UserBase; + uint32_t pitchBytes = surface->PitchBytes; + KeReleaseMutex(surface->Lock); - KSTATUS s = CopyFromUser(kpixels, (const void*)(uintptr_t)a4, bytes); if (KSUCCESS(s)) - s = KeDisplayBlit((int)a0, (int)a1, w, h, kpixels); + s = CopyToUser((void*)(uintptr_t)a1, &userBase, sizeof(userBase)); + if (KSUCCESS(s)) + s = CopyToUser((void*)(uintptr_t)a2, &pitchBytes, sizeof(pitchBytes)); + if (KSUCCESS(s)) + KiSurfaceStatsRecordMap(surface); - MmFreePool(kpixels); + ObDereferenceObject(&surface->Header); + return s; +} + +// SYS_QUERY_SURFACE (101) a0=surfaceHandle a1=*SYS_SURFACE_DESC. +static uint64_t SyscallQuerySurface(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a2; (void)a3; (void)a4; (void)a5; + if (!a1) + return STATUS_INVALID_PARAMETER; + + SURFACE_OBJECT* surface = (SURFACE_OBJECT*)KiHandleToObject((uint32_t)a0, ObjectTypeSurface, HANDLE_RIGHT_QUERY); + if (!surface) + return STATUS_INVALID_PARAMETER; + + if (surface->Owner != KeGetCurrentProcess()) { + ObDereferenceObject(&surface->Header); + return STATUS_ACCESS_DENIED; + } + + SYS_SURFACE_DESC desc; + memset(&desc, 0, sizeof(desc)); + + KSTATUS s = KeAcquireMutex(surface->Lock); + if (KSUCCESS(s)) { + desc.Width = surface->Width; + desc.Height = surface->Height; + desc.PitchBytes = surface->PitchBytes; + desc.Format = surface->Format; + desc.Base = surface->UserBase; + desc.PayloadBytes = surface->PayloadSize; + desc.PageCount = surface->PageCount; + KeReleaseMutex(surface->Lock); + + uintptr_t statsFlags = KeAcquireSpinlock(&gSurfaceStatsLock); + desc.MapCount = surface->MapCount; + desc.PresentCount = surface->PresentCount; + desc.PresentRectCount = surface->PresentRectCount; + desc.PresentClippedRectCount = surface->PresentClippedRectCount; + desc.PresentEmptyClipCount = surface->PresentEmptyClipCount; + desc.PresentRowsCopied = surface->PresentRowsCopied; + desc.PresentBytesCopied = surface->PresentBytesCopied; + KeReleaseSpinlock(&gSurfaceStatsLock, statsFlags); + + s = CopyToUser((void*)(uintptr_t)a1, &desc, sizeof(desc)); + } + + ObDereferenceObject(&surface->Header); + return s; +} + +static int KiClipSurfaceRect(const SURFACE_OBJECT* surface, const DISPLAY_RECT* in, DISPLAY_RECT* out) { + if (!surface || !out) + return 0; + DISPLAY_RECT r; + if (in) { + r = *in; + } else { + r.X = 0; + r.Y = 0; + r.W = (int32_t)surface->Width; + r.H = (int32_t)surface->Height; + } + if (r.W <= 0 || r.H <= 0) + return 0; + + int64_t x0 = r.X; + int64_t y0 = r.Y; + int64_t x1 = x0 + r.W; + int64_t y1 = y0 + r.H; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > surface->Width) x1 = surface->Width; + if (y1 > surface->Height) y1 = surface->Height; + if (x0 >= x1 || y0 >= y1) + return 0; + + out->X = (int32_t)x0; + out->Y = (int32_t)y0; + out->W = (int32_t)(x1 - x0); + out->H = (int32_t)(y1 - y0); + return 1; +} + +// SYS_PRESENT_SURFACE (102) a0=surfaceHandle a1=*DISPLAY_RECT[] a2=rectCount. +static uint64_t SyscallPresentSurface(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a3; (void)a4; (void)a5; + uint32_t rectCount = (uint32_t)a2; + if (rectCount > SYS_SURFACE_MAX_RECTS) + return STATUS_INVALID_PARAMETER; + + if (rectCount != 0 && !a1) + return STATUS_INVALID_PARAMETER; + + SURFACE_OBJECT* surface = (SURFACE_OBJECT*)KiHandleToObject((uint32_t)a0, ObjectTypeSurface, HANDLE_RIGHT_READ); + if (!surface) + return STATUS_INVALID_PARAMETER; + if (surface->Owner != KeGetCurrentProcess()) { + ObDereferenceObject(&surface->Header); + return STATUS_ACCESS_DENIED; + } + + DISPLAY_RECT rects[SYS_SURFACE_MAX_RECTS]; + if (rectCount == 0) { + rects[0].X = 0; + rects[0].Y = 0; + rects[0].W = (int32_t)surface->Width; + rects[0].H = (int32_t)surface->Height; + rectCount = 1; + } else { + KSTATUS cs = CopyFromUser(rects, (const void*)(uintptr_t)a1, (size_t)rectCount * sizeof(DISPLAY_RECT)); + if (!KSUCCESS(cs)) { + ObDereferenceObject(&surface->Header); + return cs; + } + } + + uint64_t inputRects = (uint64_t)a2; + uint64_t clippedRects = 0; + uint64_t emptyClips = 0; + uint64_t rowsCopied = 0; + uint64_t bytesCopied = 0; + + KSTATUS s = STATUS_SUCCESS; + for (uint32_t i = 0; i < rectCount; ++i) { + DISPLAY_RECT clipped; + if (!KiClipSurfaceRect(surface, &rects[i], &clipped)) { + emptyClips++; + continue; + } + + SURFACE_ROW_CONTEXT ctx; + ctx.Surface = surface; + ctx.SourceX = (uint32_t)clipped.X; + ctx.SourceY = (uint32_t)clipped.Y; + s = KeDisplayBlitRows(clipped.X, clipped.Y, clipped.W, clipped.H, + KiSurfaceCopyRow, &ctx, surface->RowScratch); + if (!KSUCCESS(s)) + break; + + clippedRects++; + rowsCopied += (uint64_t)(uint32_t)clipped.H; + bytesCopied += (uint64_t)(uint32_t)clipped.W * (uint64_t)(uint32_t)clipped.H * sizeof(uint32_t); + } + + if (KSUCCESS(s)) + KiSurfaceStatsRecordPresent(surface, inputRects, clippedRects, emptyClips, rowsCopied, bytesCopied); + + ObDereferenceObject(&surface->Header); return s; } @@ -1267,20 +2205,36 @@ static uint64_t SyscallKbIsKeyDown(uint64_t a0, uint64_t a1, uint64_t a2, return KeInputIsKeyDown((INPUT_KEY)a0) ? 1u : 0u; } -// SYS_READ_KEYBOARD_CHAR (72) -> char (blocks, yields while waiting). +// SYS_READ_KEYBOARD_CHAR (72) -> char. static uint64_t SyscallKbReadChar(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { (void)a0; (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - while (!KeInputKbPoll()) KeYield(); - return (uint64_t)(uint8_t)KeInputReadChar(); + + for (;;) { + char ch = KeInputReadChar(); + if (ch) + return (uint64_t)(uint8_t)ch; + + KSTATUS s = KeInputWaitKey(0); + if (!KSUCCESS(s)) + return 0; + } } -// SYS_READ_KEY (73) -> INPUT_KEY (blocks, yields while waiting). +// SYS_READ_KEY (73) -> INPUT_KEY. static uint64_t SyscallKbReadKey(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { (void)a0; (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - while (!KeInputKbPoll()) KeYield(); - return (uint64_t)KeInputReadKey(); + + for (;;) { + INPUT_KEY key = KeInputReadKey(); + if (key != INPUT_KEY_NONE) + return (uint64_t)key; + + KSTATUS s = KeInputWaitKey(0); + if (!KSUCCESS(s)) + return (uint64_t)INPUT_KEY_NONE; + } } // SYS_GET_KEY_MODIFIERS (74) -> uint32_t INPUT_MOD_* flags. @@ -1290,6 +2244,36 @@ static uint64_t SyscallKbGetModifiers(uint64_t a0, uint64_t a1, uint64_t a2, return (uint64_t)KeInputGetModifiers(); } +// SYS_READ_INPUT_EVENT (104) a0=*INPUT_KEY_RECORD -> KSTATUS. +static uint64_t SyscallKbReadInputEvent(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; + if (!a0) return STATUS_INVALID_PARAMETER; + + INPUT_KEY_RECORD record; + for (;;) { + if (KeInputReadEvent(&record)) + return CopyToUser((void*)(uintptr_t)a0, &record, sizeof(record)); + + KSTATUS s = KeInputWaitKey(0); + if (!KSUCCESS(s)) + return s; + } +} + +// SYS_PEEK_INPUT_EVENT (105) a0=*INPUT_KEY_RECORD -> KSTATUS. +static uint64_t SyscallKbPeekInputEvent(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; + if (!a0) return STATUS_INVALID_PARAMETER; + + INPUT_KEY_RECORD record; + if (!KeInputPeekEvent(&record)) + return STATUS_TIMEOUT; + + return CopyToUser((void*)(uintptr_t)a0, &record, sizeof(record)); +} + // SYS_POLL_MOUSE (75) -> 1 or 0. static uint64_t SyscallMousePoll(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -1423,18 +2407,11 @@ static uint64_t SyscallCreateProcess(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t size = IoGetFileSize(f); ProcDbg("CreateProcess: fileSize=%llu\n", size); - void* mem = MmAllocatePool(size); - if (!mem) { IoCloseFile(f); ProcDbg("CreateProcess: raw image pool alloc failed size=%llu\n", size); return STATUS_NO_MEMORY; } - - size_t got; - s = IoReadFile(f, mem, size, &got); - IoCloseFile(f); - if (!KSUCCESS(s)) { ProcDbg("CreateProcess: IoReadFile failed st=%d got=%llu\n", s, (unsigned long long)got); MmFreePool(mem); return s; } // Create the process object. PROCESS* proc; s = KeCreateProcess(pathBuf, &proc); - if (!KSUCCESS(s)) { ProcDbg("CreateProcess: KeCreateProcess failed st=%d\n", s); MmFreePool(mem); return s; } + if (!KSUCCESS(s)) { ProcDbg("CreateProcess: KeCreateProcess failed st=%d\n", s); IoCloseFile(f); return s; } ProcDbg("CreateProcess: created pid=%u name=%s\n", proc->ProcessId, proc->Name); // Inherit CWD from parent. @@ -1454,19 +2431,19 @@ static uint64_t SyscallCreateProcess(uint64_t a0, uint64_t a1, uint64_t a2, path, cmdlineView, SysSvFromParts(proc->CurrentDirectory, proc->CurrentDirectoryLength)); - if (!KSUCCESS(s)) { ProcDbg("CreateProcess: env setup failed st=%d\n", s); MmFreePool(mem); ObDereferenceObject(&proc->Header); return s; } + if (!KSUCCESS(s)) { ProcDbg("CreateProcess: env setup failed st=%d\n", s); IoCloseFile(f); ObDereferenceObject(&proc->Header); return s; } // Map the PE image into memory. uintptr_t imageBase = 0; - s = LdrLoadImageForProcess(proc, mem, size, &imageBase); - MmFreePool(mem); - if (!KSUCCESS(s)) { ProcDbg("CreateProcess: LdrLoadImage failed st=%d\n", s); ObDereferenceObject(&proc->Header); return s; } + s = LdrLoadImageFileForProcess(proc, f, &imageBase); + IoCloseFile(f); + if (!KSUCCESS(s)) { ProcDbg("CreateProcess: LdrLoadImageFileForProcess failed st=%d\n", s); ObDereferenceObject(&proc->Header); return s; } ProcDbg("CreateProcess: image mapped pid=%u base=%016llx size=%llx\n", proc->ProcessId, (unsigned long long)imageBase, (unsigned long long)LdrGetMappedImageSize(proc, imageBase)); s = LdrResolveImportsForProcess(proc, imageBase); if (!KSUCCESS(s)) { ProcDbg("CreateProcess: resolve imports failed st=%d pid=%u image=%016llx\n", s, proc->ProcessId, (unsigned long long)imageBase); - MmDebugDumpUserRegions(proc); + MmVadDebugDumpProcess(proc); ObDereferenceObject(&proc->Header); return s; } @@ -1475,7 +2452,7 @@ static uint64_t SyscallCreateProcess(uint64_t a0, uint64_t a1, uint64_t a2, s = LdrProtectImageForProcess(proc, imageBase, true); if (!KSUCCESS(s)) { ProcDbg("CreateProcess: protect image failed st=%d pid=%u\n", s, proc->ProcessId); - MmDebugDumpUserRegions(proc); + MmVadDebugDumpProcess(proc); ObDereferenceObject(&proc->Header); return s; } @@ -1503,16 +2480,18 @@ static uint64_t SyscallCreateProcess(uint64_t a0, uint64_t a1, uint64_t a2, s = KeCreateThread(proc, entryPoint, 0, PriorityNormal, &thread); if (!KSUCCESS(s)) { ProcDbg("CreateProcess: KeCreateThread failed st=%d pid=%u entry=%016llx\n", s, proc->ProcessId, (unsigned long long)entryPoint); - MmDebugDumpUserRegions(proc); + MmVadDebugDumpProcess(proc); KeCloseHandle(caller, handle); ObDereferenceObject(&proc->Header); return s; } - ProcDbg("CreateProcess: thread created pid=%u tid=%llu entry=%016llx handle=%u\n", proc->ProcessId, (unsigned long long)thread->ThreadId, (unsigned long long)entryPoint, handle); + uint32_t tid = thread->ThreadId; + ProcDbg("CreateProcess: thread created pid=%u tid=%llu entry=%016llx handle=%u\n", proc->ProcessId, (unsigned long long)tid, (unsigned long long)entryPoint, handle); + ObDereferenceObject(&thread->Header); s = KiCopyHandleToUser((uint64_t)(uintptr_t)req.HandleOut, handle); if (!KSUCCESS(s)) { - KeTerminateProcess(proc, -1); + KeTerminateProcess(proc, (int)STATUS_PROCESS_PAGE_FAULT); KeCloseHandle(caller, handle); ObDereferenceObject(&proc->Header); return s; @@ -1523,20 +2502,6 @@ static uint64_t SyscallCreateProcess(uint64_t a0, uint64_t a1, uint64_t a2, ObDereferenceObject(&proc->Header); return STATUS_SUCCESS; } -// SYS_WAIT_FOR_PROCESS (51) a0=handle -// Blocks until the target process signals its ExitEvent. -static uint64_t SyscallWaitForProcess(uint64_t a0, uint64_t a1, uint64_t a2, - uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - - PROCESS* proc = (PROCESS*)KiHandleToObject((uint32_t)a0, ObjectTypeProcess, HANDLE_RIGHT_WAIT); - if (!proc) return STATUS_INVALID_HANDLE; - - KSTATUS s = KeWaitForEvent(proc->ExitEvent, 0); - ObDereferenceObject(&proc->Header); - return s; -} - // SYS_GET_PROCESS_EXIT_CODE (52) a0=handle a1=*exitCodeOut. static uint64_t SyscallGetProcessExitCode(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -1677,48 +2642,6 @@ static uint64_t SyscallGetCwd(uint64_t a0, uint64_t a1, uint64_t a2, return STATUS_SUCCESS; } -// SYS_WAIT_THREAD (58) a0=tid -// Blocks the caller until the thread with the given TID exits. -// Searches the current process's thread list; cross-process join is not -// supported in v1 (POSIX pthread_join is always intra-process). -// Returns STATUS_NOT_FOUND if the TID is unknown (already reaped or never -// existed) - treat the same as a successful join at the POSIX layer. -static uint64_t SyscallWaitThread(uint64_t a0, uint64_t a1, uint64_t a2, - uint64_t a3, uint64_t a4, uint64_t a5) { - (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; - uint32_t targetTid = (uint32_t)a0; - - PROCESS* proc = KeGetCurrentProcess(); - if (!proc) return STATUS_FAILURE; - - // Walk the thread list under the spinlock to find the target, then bump - // its ExitEvent refcount so the event survives even if the thread exits - // and is destroyed while we wait. - EVENT* exitEvent = (EVENT*)0; - uintptr_t flags = KeAcquireSpinlock(&proc->HandleLock); - LIST_ENTRY* entry = proc->ThreadList.Fwd; - while (entry != &proc->ThreadList) { - THREAD* t = CONTAINING_RECORD(entry, THREAD, ProcessLink); - if (t->ThreadId == targetTid && t->ExitEvent) { - exitEvent = t->ExitEvent; - ObReferenceObject(&exitEvent->Header); // keep event alive - break; - } - entry = entry->Fwd; - } - KeReleaseSpinlock(&proc->HandleLock, flags); - - if (!exitEvent) { - // TID not found - either already exited and reaped, or invalid. - // Treat as success at the POSIX layer (join on an already-dead thread). - return STATUS_NOT_FOUND; - } - - KSTATUS s = KeWaitForEvent(exitEvent, 0); - ObDereferenceObject(&exitEvent->Header); - return s; -} - // SYS_GET_PROCESS_PID (59) a0=procHandle a1=*pidOut // Returns the numeric PID of a process identified by handle. // Used by posix_spawn to map the kernel handle back to a pid_t for waitpid. @@ -1968,7 +2891,7 @@ static uint64_t SyscallGetProcessList(uint64_t a0, uint64_t a1, uint64_t a2, size_t bytes = 0; if (!KiMulSize(toCopy, sizeof(SYS_PROCESS_INFO), &bytes)) return STATUS_INVALID_PARAMETER; - SYS_PROCESS_INFO* tmp = (SYS_PROCESS_INFO*)MmAllocatePool(bytes); + SYS_PROCESS_INFO* tmp = (SYS_PROCESS_INFO*)MmAllocatePoolTag(bytes, MM_POOL_TAG('s', 'y', 's', ' ')); if (!tmp) return STATUS_NO_MEMORY; size_t copied = KeCopyProcessInfo(tmp, toCopy); @@ -1983,6 +2906,45 @@ static uint64_t SyscallGetProcessList(uint64_t a0, uint64_t a1, uint64_t a2, return STATUS_SUCCESS; } +// SYS_GET_THREAD_LIST (103) a0=pid a1=*SYS_THREAD_INFO[] a2=maxCount a3=*countOut. +static uint64_t SyscallGetThreadList(uint64_t a0, uint64_t a1, uint64_t a2, + uint64_t a3, uint64_t a4, uint64_t a5) { + (void)a4; (void)a5; + uint32_t pid = (uint32_t)a0; + size_t maxCount = (size_t)a2; + + if (maxCount && !a1) return STATUS_INVALID_PARAMETER; + if (a3) { + KSTATUS s = MmProbeUserWrite((void*)(uintptr_t)a3, sizeof(size_t)); + if (!KSUCCESS(s)) return s; + } + + size_t total = 0; + KSTATUS s = KeCopyProcessThreadInfo(pid, NULL, 0, &total); + if (!KSUCCESS(s)) return s; + + size_t toCopy = total < maxCount ? total : maxCount; + if (toCopy) { + size_t bytes = 0; + if (!KiMulSize(toCopy, sizeof(SYS_THREAD_INFO), &bytes)) return STATUS_INVALID_PARAMETER; + + SYS_THREAD_INFO* tmp = (SYS_THREAD_INFO*)MmAllocatePoolTag(bytes, MM_POOL_TAG('s', 't', 'h', 'r')); + if (!tmp) return STATUS_NO_MEMORY; + + size_t copied = 0; + s = KeCopyProcessThreadInfo(pid, tmp, toCopy, &copied); + if (KSUCCESS(s)) { + if (copied < toCopy) toCopy = copied; + s = CopyToUser((void*)(uintptr_t)a1, tmp, toCopy * sizeof(SYS_THREAD_INFO)); + } + MmFreePool(tmp); + if (!KSUCCESS(s)) return s; + } + + if (a3) return KiCopySizeToUser(a3, total); + return STATUS_SUCCESS; +} + // SYS_RENAME_FILE (66) a0=*oldPath a1=*newPath. static uint64_t SyscallRenameFile(uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5) { @@ -2076,6 +3038,12 @@ typedef struct SYS_MEMORY_STATUS_K { uint64_t FreePhysicalBytes; uint64_t KernelPoolBytes; uint64_t UserHeapBytes; + uint64_t ReclaimablePhysicalBytes; + uint64_t AvailablePhysicalBytes; + uint64_t PinnedPhysicalBytes; + uint64_t NonEvictablePhysicalBytes; + uint64_t UserPhysicalBytes; + uint64_t PageTablePhysicalBytes; } SYS_MEMORY_STATUS_K; #define SYS_POLL_MAX_FDS 64u @@ -2224,7 +3192,7 @@ static uint64_t SyscallSend(uint64_t a0, uint64_t a1, uint64_t a2, return STATUS_SUCCESS; } - void* kbuf = MmAllocatePool(length); + void* kbuf = MmAllocatePoolTag(length, MM_POOL_TAG('s', 'y', 's', ' ')); if (!kbuf) return STATUS_NO_MEMORY; KSTATUS s = CopyFromUser(kbuf, (const void*)(uintptr_t)a1, length); @@ -2261,7 +3229,7 @@ static uint64_t SyscallRecv(uint64_t a0, uint64_t a1, uint64_t a2, KSTATUS s = MmProbeUserWrite((void*)(uintptr_t)a1, length); if (!KSUCCESS(s)) return s; - void* kbuf = MmAllocatePool(length); + void* kbuf = MmAllocatePoolTag(length, MM_POOL_TAG('s', 'y', 's', ' ')); if (!kbuf) return STATUS_NO_MEMORY; SOCKET_OBJECT* socket = (SOCKET_OBJECT*)KiHandleToObject((uint32_t)a0, ObjectTypeSocket, HANDLE_RIGHT_READ | HANDLE_RIGHT_WRITE); @@ -2313,7 +3281,7 @@ static uint64_t SyscallSendTo(uint64_t a0, uint64_t a1, uint64_t a2, void* kbuf = NULL; if (req.Length) { - kbuf = MmAllocatePool(req.Length); + kbuf = MmAllocatePoolTag(req.Length, MM_POOL_TAG('s', 'y', 's', ' ')); if (!kbuf) return STATUS_NO_MEMORY; s = CopyFromUser(kbuf, req.Buffer, req.Length); if (!KSUCCESS(s)) { MmFreePool(kbuf); return s; } @@ -2369,7 +3337,7 @@ static uint64_t SyscallRecvFrom(uint64_t a0, uint64_t a1, uint64_t a2, void* kbuf = NULL; if (req.Length) { - kbuf = MmAllocatePool(req.Length); + kbuf = MmAllocatePoolTag(req.Length, MM_POOL_TAG('s', 'y', 's', ' ')); if (!kbuf) return STATUS_NO_MEMORY; } @@ -2521,7 +3489,7 @@ static uint64_t SyscallPoll(uint64_t a0, uint64_t a1, uint64_t a2, return STATUS_INVALID_PARAMETER; size_t bytes = count * sizeof(SYS_POLLFD_K); - SYS_POLLFD_K* fds = (SYS_POLLFD_K*)MmAllocatePool(bytes); + SYS_POLLFD_K* fds = (SYS_POLLFD_K*)MmAllocatePoolTag(bytes, MM_POOL_TAG('s', 'y', 's', ' ')); if (!fds) return STATUS_NO_MEMORY; KSTATUS s = CopyFromUser(fds, (const void*)(uintptr_t)a0, bytes); @@ -2573,10 +3541,20 @@ static uint64_t SyscallGetMemoryStatus(uint64_t a0, uint64_t a1, uint64_t a2, (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; if (!a0) return STATUS_INVALID_PARAMETER; + MM_PFN_STATS pfn; + MmPfnQueryStats(&pfn); + SYS_MEMORY_STATUS_K status; - status.FreePhysicalBytes = MmFreeFrameCount() * PAGE_SIZE; + memset(&status, 0, sizeof(status)); + status.FreePhysicalBytes = pfn.FreeFrames * PAGE_SIZE; status.KernelPoolBytes = MmGetKernelPoolUsage(); status.UserHeapBytes = MmGetUserHeapUsage(); + status.ReclaimablePhysicalBytes = pfn.ReclaimableFrames * PAGE_SIZE; + status.AvailablePhysicalBytes = pfn.AvailableFrames * PAGE_SIZE; + status.PinnedPhysicalBytes = pfn.PinnedFrames * PAGE_SIZE; + status.NonEvictablePhysicalBytes = pfn.NonEvictableFrames * PAGE_SIZE; + status.UserPhysicalBytes = pfn.UserFrames * PAGE_SIZE; + status.PageTablePhysicalBytes = pfn.PageTableFrames * PAGE_SIZE; return CopyToUser((void*)(uintptr_t)a0, &status, sizeof(status)); } @@ -2661,6 +3639,267 @@ static uint64_t SyscallQueryKernelStats(uint64_t a0, uint64_t a1, uint64_t a2, return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); } + + case SYS_KERNEL_STATS_PFN: { + MM_PFN_STATS km; + SYS_PFN_STATS um; + MmPfnQueryStats(&km); + memset(&um, 0, sizeof(um)); + um.TotalFrames = km.TotalFrames; + um.FreeFrames = km.FreeFrames; + um.UsedFrames = km.UsedFrames; + um.ReservedFrames = km.ReservedFrames; + um.ZeroedFrames = km.ZeroedFrames; + um.ActiveFrames = km.ActiveFrames; + um.StandbyFrames = km.StandbyFrames; + um.ModifiedFrames = km.ModifiedFrames; + um.PinnedFrames = km.PinnedFrames; + um.EvictableFrames = km.EvictableFrames; + um.NonEvictableFrames = km.NonEvictableFrames; + um.ReclaimableFrames = km.ReclaimableFrames; + um.AvailableFrames = km.AvailableFrames; + um.MappedFrames = km.MappedFrames; + um.KernelFrames = km.KernelFrames; + um.PageTableFrames = km.PageTableFrames; + um.UserFrames = km.UserFrames; + um.CleanFileCacheFrames = km.CleanFileCacheFrames; + um.ActiveCleanFileCacheFrames = km.ActiveCleanFileCacheFrames; + um.StandbyCleanFileCacheFrames = km.StandbyCleanFileCacheFrames; + um.ImageFrames = km.ImageFrames; + um.ActiveImageFrames = km.ActiveImageFrames; + um.StandbyImageFrames = km.StandbyImageFrames; + um.MappedFileFrames = km.MappedFileFrames; + um.AnonymousFrames = km.AnonymousFrames; + um.CowFrames = km.CowFrames; + um.SurfaceFrames = km.SurfaceFrames; + um.DriverFrames = km.DriverFrames; + um.DmaFrames = km.DmaFrames; + um.BootloaderFrames = km.BootloaderFrames; + um.PfnDatabaseFrames = km.PfnDatabaseFrames; + um.MmioFrames = km.MmioFrames; + um.AllocationCount = km.AllocationCount; + um.FreeCount = km.FreeCount; + um.RetainCount = km.RetainCount; + um.ReleaseCount = km.ReleaseCount; + um.PinCount = km.PinCount; + um.UnpinCount = km.UnpinCount; + um.FailedAllocations = km.FailedAllocations; + um.FailedContiguousAllocations = km.FailedContiguousAllocations; + um.FailedInvalidRange = km.FailedInvalidRange; + um.FailedEmptyRange = km.FailedEmptyRange; + um.FailedReserveDenied = km.FailedReserveDenied; + um.FailedNoFrame = km.FailedNoFrame; + um.FailedContiguousInvalidRequest = km.FailedContiguousInvalidRequest; + um.FailedContiguousReserveDenied = km.FailedContiguousReserveDenied; + um.FailedContiguousNoRun = km.FailedContiguousNoRun; + um.ReclaimAttempts = km.ReclaimAttempts; + um.ReclaimPagesRequested = km.ReclaimPagesRequested; + um.ReclaimPagesSucceeded = km.ReclaimPagesSucceeded; + um.ReclaimPagesFailed = km.ReclaimPagesFailed; + um.ReclaimReentrantSkips = km.ReclaimReentrantSkips; + return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); + } + + case SYS_KERNEL_STATS_PTE: { + MM_PTE_STATS km; + SYS_PTE_STATS um; + MmPteQueryStats(&km); + memset(&um, 0, sizeof(um)); + um.MapCalls = km.MapCalls; + um.MapFailures = km.MapFailures; + um.UnmapCalls = km.UnmapCalls; + um.UnmapFailures = km.UnmapFailures; + um.ProtectCalls = km.ProtectCalls; + um.ProtectFailures = km.ProtectFailures; + um.QueryCalls = km.QueryCalls; + um.QueryFailures = km.QueryFailures; + for (size_t i = 0; i < 6 && i < MmPteQueryPurposeCount; i++) { + um.QueryByPurpose[i] = km.QueryByPurpose[i]; + um.QueryFailuresByPurpose[i] = km.QueryFailuresByPurpose[i]; + } + um.TranslateCalls = km.TranslateCalls; + um.LocalFlushes = km.LocalFlushes; + um.GlobalFlushes = km.GlobalFlushes; + um.ShootdownRequests = km.ShootdownRequests; + um.ShootdownTargetCpus = km.ShootdownTargetCpus; + um.ShootdownSkippedNoTargets = km.ShootdownSkippedNoTargets; + um.ShootdownAcks = km.ShootdownAcks; + um.ShootdownTimeouts = km.ShootdownTimeouts; + um.NewMappingNoFlushes = km.NewMappingNoFlushes; + um.ReplacedMappingFlushes = km.ReplacedMappingFlushes; + um.PageTablePagesAllocated = km.PageTablePagesAllocated; + um.CurrentLeafMappings = km.CurrentLeafMappings; + return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); + } + + case SYS_KERNEL_STATS_VAD: { + MM_VAD_STATS km; + SYS_VAD_STATS um; + MmVadQueryStats(&km); + memset(&um, 0, sizeof(um)); + um.ResolveFaultCalls = km.ResolveFaultCalls; + um.ResolvedFaults = km.ResolvedFaults; + um.FailedFaults = km.FailedFaults; + um.ProtectionFaults = km.ProtectionFaults; + um.NoVadFaults = km.NoVadFaults; + um.UncommittedFaults = km.UncommittedFaults; + um.NoAccessFaults = km.NoAccessFaults; + um.DemandZeroPages = km.DemandZeroPages; + um.ZeroPageMaps = km.ZeroPageMaps; + um.ZeroPageCowSplits = km.ZeroPageCowSplits; + um.CowFaults = km.CowFaults; + um.CowPagesCopied = km.CowPagesCopied; + um.CowViewsCreated = km.CowViewsCreated; + um.CowPagesShared = km.CowPagesShared; + um.EnsureResidentCalls = km.EnsureResidentCalls; + um.EnsureResidentPages = km.EnsureResidentPages; + um.AlreadyResidentPages = km.AlreadyResidentPages; + um.FileBackedFaults = km.FileBackedFaults; + um.FileBackedReads = km.FileBackedReads; + um.FileBackedReadFailures = km.FileBackedReadFailures; + um.FileBackedDiscards = km.FileBackedDiscards; + um.FileBackedSharedWritableFaults = km.FileBackedSharedWritableFaults; + um.FileBackedDirtyFaults = km.FileBackedDirtyFaults; + um.FileBackedDirtySyncs = km.FileBackedDirtySyncs; + um.ImageBackedFaults = km.ImageBackedFaults; + um.ImageBackedZeroFaults = km.ImageBackedZeroFaults; + um.ImageBackedPrivateFaults = km.ImageBackedPrivateFaults; + um.ImageFixupPrivateCalls = km.ImageFixupPrivateCalls; + um.ImageFixupPrivatePages = km.ImageFixupPrivatePages; + um.SwapPageOuts = km.SwapPageOuts; + um.SwapPageIns = km.SwapPageIns; + return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); + } + + + case SYS_KERNEL_STATS_FILE_CACHE: { + MM_FILE_CACHE_STATS km; + SYS_FILE_CACHE_STATS um; + MmFileCacheQueryStats(&km); + memset(&um, 0, sizeof(um)); + um.Lookups = km.Lookups; + um.Hits = km.Hits; + um.Misses = km.Misses; + um.Loads = km.Loads; + um.LoadFailures = km.LoadFailures; + um.RaceDiscards = km.RaceDiscards; + um.InflightWaits = km.InflightWaits; + um.Invalidations = km.Invalidations; + um.DirtyPages = km.DirtyPages; + um.MaxDirtyPages = km.MaxDirtyPages; + um.DirtyMarks = km.DirtyMarks; + um.WritebackRequests = km.WritebackRequests; + um.WritebackPages = km.WritebackPages; + um.WritebackBytes = km.WritebackBytes; + um.WritebackFailures = km.WritebackFailures; + um.FlushFileCalls = km.FlushFileCalls; + um.FlushFileFailures = km.FlushFileFailures; + um.MappedDirtySyncs = km.MappedDirtySyncs; + um.MappedDirtyPages = km.MappedDirtyPages; + um.Drops = km.Drops; + um.DropFailures = km.DropFailures; + um.StandbyTransitions = km.StandbyTransitions; + um.StandbyHits = km.StandbyHits; + um.ReadAheadRequests = km.ReadAheadRequests; + um.ReadAheadPagesRequested = km.ReadAheadPagesRequested; + um.ReadAheadPagesQueued = km.ReadAheadPagesQueued; + um.ReadAheadPagesLoaded = km.ReadAheadPagesLoaded; + um.ReadAheadPagesSkipped = km.ReadAheadPagesSkipped; + um.ReadAheadFailures = km.ReadAheadFailures; + um.ReadAheadPagesThrottled = km.ReadAheadPagesThrottled; + um.MaxReadAheadPagesPerRequest = km.MaxReadAheadPagesPerRequest; + um.BatchReads = km.BatchReads; + um.BatchPagesRequested = km.BatchPagesRequested; + um.BatchPagesLoaded = km.BatchPagesLoaded; + um.MaxBatchPagesRequested = km.MaxBatchPagesRequested; + um.MaxBatchPagesLoaded = km.MaxBatchPagesLoaded; + um.ResidentPages = km.ResidentPages; + um.ActivePages = km.ActivePages; + um.StandbyPages = km.StandbyPages; + um.MaxResidentPages = km.MaxResidentPages; + return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); + } + + case SYS_KERNEL_STATS_SURFACE: { + SYS_SURFACE_STATS stats; + memset(&stats, 0, sizeof(stats)); + KiSurfaceStatsQuery(&stats); + return KiReturnKernelStats(a1, (size_t)a2, a3, &stats, sizeof(stats)); + } + + case SYS_KERNEL_STATS_SWAP: { + MM_SWAP_STATS km; + SYS_SWAP_STATS um; + MmSwapQueryStats(&km); + memset(&um, 0, sizeof(um)); + um.Enabled = km.Enabled; + um.TotalSlots = km.TotalSlots; + um.UsedSlots = km.UsedSlots; + um.FreeSlots = km.FreeSlots; + um.HighWaterSlots = km.HighWaterSlots; + um.PageOuts = km.PageOuts; + um.PageIns = km.PageIns; + um.PageOutFailures = km.PageOutFailures; + um.PageInFailures = km.PageInFailures; + um.SlotAllocations = km.SlotAllocations; + um.SlotFrees = km.SlotFrees; + um.SlotAllocationFailures = km.SlotAllocationFailures; + return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); + } + + case SYS_KERNEL_STATS_FILESYSTEM: { + IO_FILESYSTEM_STATS km; + SYS_FILESYSTEM_STATS um; + IoQueryFilesystemStats(&km); + memset(&um, 0, sizeof(um)); + um.PathLookups = km.PathLookups; + um.PathCacheHits = km.PathCacheHits; + um.PathCacheMisses = km.PathCacheMisses; + um.PathCacheStores = km.PathCacheStores; + um.PathCacheInvalidations = km.PathCacheInvalidations; + um.PathCacheGeneration = km.PathCacheGeneration; + um.DirectorySearches = km.DirectorySearches; + um.DirectoryClustersRead = km.DirectoryClustersRead; + um.DirectoryEntriesScanned = km.DirectoryEntriesScanned; + um.FatChainCacheLookups = km.FatChainCacheLookups; + um.FatChainCacheHits = km.FatChainCacheHits; + um.FatChainCacheMisses = km.FatChainCacheMisses; + um.FatChainCacheStores = km.FatChainCacheStores; + um.FatChainCacheInvalidations = km.FatChainCacheInvalidations; + um.FatChainWalkSteps = km.FatChainWalkSteps; + um.DataReadRuns = km.DataReadRuns; + um.DataReadSectors = km.DataReadSectors; + um.DataReadRunMerges = km.DataReadRunMerges; + um.MaxDataReadSpanSectors = km.MaxDataReadSpanSectors; + return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); + } + + case SYS_KERNEL_STATS_DISK: { + IO_DISK_STATS km; + SYS_DISK_STATS um; + IoQueryDiskStats(&km); + memset(&um, 0, sizeof(um)); + um.RegisteredDisks = km.RegisteredDisks; + um.ReadRequests = km.ReadRequests; + um.ReadSectors = km.ReadSectors; + um.WriteRequests = km.WriteRequests; + um.WriteSectors = km.WriteSectors; + um.MaxReadSectors = km.MaxReadSectors; + um.MaxWriteSectors = km.MaxWriteSectors; + um.ReadaheadHits = km.ReadaheadHits; + um.ReadaheadMisses = km.ReadaheadMisses; + um.ReadaheadStores = km.ReadaheadStores; + um.ReadaheadInvalidations = km.ReadaheadInvalidations; + um.ReadaheadBytesServed = km.ReadaheadBytesServed; + um.ReadaheadSectorsLoaded = km.ReadaheadSectorsLoaded; + um.ReadaheadSequentialDetections = km.ReadaheadSequentialDetections; + um.ReadaheadSuppressed = km.ReadaheadSuppressed; + um.ReadaheadAllocFailures = km.ReadaheadAllocFailures; + um.ReadaheadReadFailures = km.ReadaheadReadFailures; + um.MaxReadaheadSectors = km.MaxReadaheadSectors; + return KiReturnKernelStats(a1, (size_t)a2, a3, &um, sizeof(um)); + } + case SYS_KERNEL_STATS_CPU: { SYS_CPU_STATS stats; memset(&stats, 0, sizeof(stats)); @@ -2681,6 +3920,7 @@ static uint64_t SyscallQueryKernelStats(uint64_t a0, uint64_t a1, uint64_t a2, if (cpu->IsBsp) out->Flags |= SYS_CPU_FLAG_BSP; if (cpu->IsPrimaryThread) out->Flags |= SYS_CPU_FLAG_PRIMARY; out->CurrentTid = cpu->CurrentThread ? cpu->CurrentThread->ThreadId : 0; + if (cpu->CurrentThread && cpu->CurrentThread->IsIdleThread) out->CurrentThreadFlags |= SYS_CPU_THREAD_FLAG_IDLE; out->SchedulerTicks = cpu->SchedulerTicks; out->ContextSwitches = cpu->ContextSwitches; } @@ -2701,13 +3941,13 @@ static const SYSCALL_HANDLER gSyscallTable[SYS_MAX] = { SyscallExitProcess, // 00 SYS_EXIT_PROCESS. SyscallExitThread, // 01 SYS_EXIT_THREAD. SyscallCreateProcess, // 02 SYS_CREATE_PROCESS. - SyscallWaitForProcess, // 03 SYS_WAIT_FOR_PROCESS. + SyscallWaitSingle, // 03 SYS_WAIT_SINGLE. SyscallGetProcessExitCode, // 04 SYS_GET_PROCESS_EXIT_CODE. SyscallKillProcess, // 05 SYS_KILL_PROCESS. SyscallGetProcessList, // 06 SYS_GET_PROCESS_LIST. SyscallGetProcessPid, // 07 SYS_GET_PROCESS_PID. SyscallCreateThread, // 08 SYS_CREATE_THREAD. - SyscallWaitThread, // 09 SYS_WAIT_THREAD. + 0, // 09 retired SYS_WAIT_THREAD. SyscallGetCurrentTid, // 10 SYS_GET_CURRENT_TID. SyscallGetCurrentPid, // 11 SYS_GET_CURRENT_PID. SyscallSetThreadPriority, // 12 SYS_SET_THREAD_PRIORITY. @@ -2721,17 +3961,17 @@ static const SYSCALL_HANDLER gSyscallTable[SYS_MAX] = { SyscallVmQuery, // 20 SYS_QUERY_VM. SyscallCloseHandle, // 21 SYS_CLOSE_HANDLE. SyscallMutexCreate, // 22 SYS_CREATE_MUTEX. - SyscallMutexAcquire, // 23 SYS_ACQUIRE_MUTEX. + 0, // 23 retired SYS_ACQUIRE_MUTEX. SyscallMutexTryAcquire, // 24 SYS_TRY_ACQUIRE_MUTEX. SyscallMutexRelease, // 25 SYS_RELEASE_MUTEX. SyscallSemCreate, // 26 SYS_CREATE_SEM. - SyscallSemWait, // 27 SYS_WAIT_SEM. + 0, // 27 retired SYS_WAIT_SEM. SyscallSemTryWait, // 28 SYS_TRY_WAIT_SEM. SyscallSemSignal, // 29 SYS_SIGNAL_SEM. SyscallEventCreate, // 30 SYS_CREATE_EVENT. SyscallEventSignal, // 31 SYS_SIGNAL_EVENT. SyscallEventClear, // 32 SYS_CLEAR_EVENT. - SyscallEventWait, // 33 SYS_WAIT_EVENT. + 0, // 33 retired SYS_WAIT_EVENT. SyscallQueueCreate, // 34 SYS_CREATE_QUEUE. SyscallQueueSend, // 35 SYS_SEND_QUEUE. SyscallQueueRecv, // 36 SYS_RECV_QUEUE. @@ -2764,38 +4004,49 @@ static const SYSCALL_HANDLER gSyscallTable[SYS_MAX] = { SyscallFillConsoleCharacter, // 63 SYS_FILL_CONSOLE_CHARACTER. SyscallFillConsoleAttribute, // 64 SYS_FILL_CONSOLE_ATTRIBUTE. SyscallPutScreenChar, // 65 SYS_PUT_SCREEN_CHAR. - SyscallBlitScreen, // 66 SYS_BLIT_SCREEN. - SyscallClearScreen, // 67 SYS_CLEAR_SCREEN. - SyscallGetScreenSize, // 68 SYS_GET_SCREEN_SIZE. - SyscallQueryDisplayInfo, // 69 SYS_QUERY_DISPLAY_INFO. - SyscallKbPoll, // 70 SYS_POLL_KEYBOARD. - SyscallKbIsKeyDown, // 71 SYS_IS_KEY_DOWN. - SyscallKbReadChar, // 72 SYS_READ_KEYBOARD_CHAR. - SyscallKbReadKey, // 73 SYS_READ_KEY. - SyscallKbGetModifiers, // 74 SYS_GET_KEY_MODIFIERS. - SyscallMousePoll, // 75 SYS_POLL_MOUSE. - SyscallMouseRead, // 76 SYS_READ_MOUSE. - SyscallMouseGetPos, // 77 SYS_GET_MOUSE_POS. - SyscallGetTick, // 78 SYS_GET_TICK. - SyscallDebugPrint, // 79 SYS_PRINT_DEBUG. - SyscallQueryTrapFrame, // 80 SYS_QUERY_TRAP_FRAME. - SyscallSocket, // 81 SYS_CREATE_SOCKET. - SyscallBind, // 82 SYS_BIND_SOCKET. - SyscallListen, // 83 SYS_LISTEN_SOCKET. - SyscallAccept, // 84 SYS_ACCEPT_SOCKET. - SyscallConnect, // 85 SYS_CONNECT_SOCKET. - SyscallSend, // 86 SYS_SEND_SOCKET. - SyscallRecv, // 87 SYS_RECV_SOCKET. - SyscallShutdown, // 88 SYS_SHUTDOWN_SOCKET. - SyscallSendTo, // 89 SYS_SEND_TO_SOCKET. - SyscallRecvFrom, // 90 SYS_RECV_FROM_SOCKET. - SyscallResolveHost, // 91 SYS_RESOLVE_HOST. - SyscallSetSocketOption, // 92 SYS_SET_SOCKET_OPTION. - SyscallGetSocketOption, // 93 SYS_GET_SOCKET_OPTION. - SyscallPoll, // 94 SYS_POLL. - SyscallGetMemoryStatus, // 95 SYS_GET_MEMORY_STATUS. - SyscallQueryKernelStats, // 96 SYS_QUERY_KERNEL_STATS. - SyscallGetSystemTime // 97 SYS_GET_SYSTEM_TIME. + SyscallClearScreen, // 66 SYS_CLEAR_SCREEN. + SyscallGetScreenSize, // 67 SYS_GET_SCREEN_SIZE. + SyscallQueryDisplayInfo, // 68 SYS_QUERY_DISPLAY_INFO. + SyscallKbPoll, // 69 SYS_POLL_KEYBOARD. + SyscallKbIsKeyDown, // 70 SYS_IS_KEY_DOWN. + SyscallKbReadChar, // 71 SYS_READ_KEYBOARD_CHAR. + SyscallKbReadKey, // 72 SYS_READ_KEY. + SyscallKbGetModifiers, // 73 SYS_GET_KEY_MODIFIERS. + SyscallMousePoll, // 74 SYS_POLL_MOUSE. + SyscallMouseRead, // 75 SYS_READ_MOUSE. + SyscallMouseGetPos, // 76 SYS_GET_MOUSE_POS. + SyscallGetTick, // 77 SYS_GET_TICK. + SyscallDebugPrint, // 78 SYS_PRINT_DEBUG. + SyscallQueryTrapFrame, // 79 SYS_QUERY_TRAP_FRAME. + SyscallSocket, // 80 SYS_CREATE_SOCKET. + SyscallBind, // 81 SYS_BIND_SOCKET. + SyscallListen, // 82 SYS_LISTEN_SOCKET. + SyscallAccept, // 83 SYS_ACCEPT_SOCKET. + SyscallConnect, // 84 SYS_CONNECT_SOCKET. + SyscallSend, // 85 SYS_SEND_SOCKET. + SyscallRecv, // 86 SYS_RECV_SOCKET. + SyscallShutdown, // 87 SYS_SHUTDOWN_SOCKET. + SyscallSendTo, // 88 SYS_SEND_TO_SOCKET. + SyscallRecvFrom, // 89 SYS_RECV_FROM_SOCKET. + SyscallResolveHost, // 90 SYS_RESOLVE_HOST. + SyscallSetSocketOption, // 91 SYS_SET_SOCKET_OPTION. + SyscallGetSocketOption, // 92 SYS_GET_SOCKET_OPTION. + SyscallPoll, // 93 SYS_POLL. + SyscallGetMemoryStatus, // 94 SYS_GET_MEMORY_STATUS. + SyscallQueryKernelStats, // 95 SYS_QUERY_KERNEL_STATS. + SyscallGetSystemTime, // 96 SYS_GET_SYSTEM_TIME. + SyscallCreateFileMapping, // 97 SYS_CREATE_FILE_MAPPING. + SyscallMapFile, // 98 SYS_MAP_FILE. + SyscallCreateSurface, // 99 SYS_CREATE_SURFACE. + SyscallMapSurface, // 100 SYS_MAP_SURFACE. + SyscallQuerySurface, // 101 SYS_QUERY_SURFACE. + SyscallPresentSurface, // 102 SYS_PRESENT_SURFACE. + SyscallGetThreadList, // 103 SYS_GET_THREAD_LIST. + SyscallKbReadInputEvent, // 104 SYS_READ_INPUT_EVENT. + SyscallKbPeekInputEvent, // 105 SYS_PEEK_INPUT_EVENT. + SyscallWaitMany, // 106 SYS_WAIT_MANY. + SyscallOpenInputQueue, // 107 SYS_OPEN_INPUT_QUEUE. + SyscallFlushFile, // 108 SYS_FLUSH_FILE. }; // KeSyscallDispatch is entered from ke_syscall.asm after the assembly stub has reordered registers into the C calling convention. diff --git a/code/kernel/ke_system_root.c b/code/kernel/ke_system_root.c new file mode 100644 index 0000000..37abd85 --- /dev/null +++ b/code/kernel/ke_system_root.c @@ -0,0 +1,97 @@ +// System root volume selection. This file keeps boot-volume identity in one place so loaders do not guess. + +#include +#include +#include +#include +#include +#include +#include + +static uint8_t gSystemRootDrive; +static bool gSystemRootReady; + +// Builds a drive-qualified path without depending on any process CWD. +static bool KiBuildDrivePath(uint8_t drive, const char* relative, char* out, size_t outSize) { + if (!relative || !out || drive >= KE_SYSTEM_ROOT_MAX_DRIVES) + return false; + + size_t relLen = strlen(relative); + if (relLen + 4 > outSize) + return false; + + out[0] = (char)('0' + drive); + out[1] = ':'; + out[2] = '/'; + memcpy(out + 3, relative, relLen + 1); + return true; +} + +// Returns true when the candidate volume looks like the volume that booted the kernel. +static bool KiDriveLooksLikeSystemRoot(uint8_t drive) { + static const char* required[] = { + "system/colonel.exe", + "system/hal.dll", + "drivers/drivers.ini", + }; + + char path[1024]; + for (size_t i = 0; i < sizeof(required) / sizeof(required[0]); i++) { + if (!KiBuildDrivePath(drive, required[i], path, sizeof(path))) + return false; + + FILE_OBJECT* file = NULL; + KSTATUS s = IoOpenFile(SysSvFromCstr(path), &file); + if (!KSUCCESS(s)) + return false; + IoCloseFile(file); + } + + return true; +} + +// Selects the mounted volume that contains the kernel/system tree. +KSTATUS KeInitializeSystemRootVolume(void) { + for (uint8_t drive = 0; drive < KE_SYSTEM_ROOT_MAX_DRIVES; drive++) { + if (!IoFilesystemPresent(drive)) + continue; + if (!KiDriveLooksLikeSystemRoot(drive)) + continue; + + gSystemRootDrive = drive; + gSystemRootReady = true; + DbgPrint("System root: drive=%u\n", drive); + return STATUS_SUCCESS; + } + + return STATUS_NOT_FOUND; +} + +// Returns whether the system root volume has been selected. +bool KeSystemRootReady(void) { + return gSystemRootReady; +} + +// Returns the selected system root drive, or zero before discovery. +uint8_t KeGetSystemRootDrive(void) { + return gSystemRootReady ? gSystemRootDrive : 0; +} + +// Builds an absolute path rooted at the selected system volume. +bool KeBuildSystemRootPath(const char* relative, char* out, size_t outSize) { + if (!gSystemRootReady) + return false; + return KiBuildDrivePath(gSystemRootDrive, relative, out, outSize); +} + +// Opens a file rooted at the selected system volume. +KSTATUS KeOpenSystemRootFile(const char* relative, FILE_OBJECT** out) { + if (!out) + return STATUS_INVALID_PARAMETER; + *out = NULL; + + char path[1024]; + if (!KeBuildSystemRootPath(relative, path, sizeof(path))) + return STATUS_NOT_FOUND; + return IoOpenFile(SysSvFromCstr(path), out); +} diff --git a/code/kernel/ke_thread.c b/code/kernel/ke_thread.c index f73200d..6e2b92d 100644 --- a/code/kernel/ke_thread.c +++ b/code/kernel/ke_thread.c @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -36,6 +38,11 @@ extern void KeThreadEntry(void); static void KiThreadDestroy(OBJECT* obj) { THREAD* thread = CONTAINING_RECORD(obj, THREAD, Header); + // A wait that uses auxiliary stack wait blocks must be removed before the + // kernel stack is released. A terminated thread may never return to the + // wait syscall to run normal cleanup. + KeRundownThreadWait(thread, STATUS_ALERTED); + KiRundownApcState(thread); if (thread->ExitEvent) { @@ -43,16 +50,16 @@ static void KiThreadDestroy(OBJECT* obj) { thread->ExitEvent = (EVENT*)0; } - PROCESS* ownerForUserRegions = thread->Owner; - if (ownerForUserRegions) { + PROCESS* ownerForVadList = thread->Owner; + if (ownerForVadList) { if (thread->Teb && thread->TebSize) { - MmFreeUserMemoryForProcess(ownerForUserRegions, thread->Teb); + MmFreeUserMemoryForProcess(ownerForVadList, thread->Teb); thread->Teb = (void*)0; thread->TebSize = 0; } if (thread->UserStackBase) { - MmFreeUserMemoryForProcess(ownerForUserRegions, thread->UserStackBase); + MmFreeUserMemoryForProcess(ownerForVadList, thread->UserStackBase); thread->UserStackBase = (void *)0; } } @@ -66,10 +73,13 @@ static void KiThreadDestroy(OBJECT* obj) { remaining = owner->ThreadCount; KeReleaseSpinlock(&owner->HandleLock, flags); - if (remaining == 0 && owner->Terminating && - // Implements the interlocked Exchange operation. - _InterlockedExchange(&owner->Exited, 1) == 0) { - if (owner->ExitEvent) KeSignalEvent(owner->ExitEvent); + if (remaining == 0) { + // The last thread leaving a process is process exit even if user + // code called ExitThread instead of ExitProcess. KeExitProcess / + // KeTerminateProcess set Terminating earlier; this covers the last + // thread voluntarily exiting without an explicit process-exit call. + _InterlockedExchange(&owner->Terminating, 1); + KeCompleteProcessExitIfLastThread(owner); } ObDereferenceObject(&owner->Header); @@ -111,11 +121,13 @@ __declspec(dllexport) KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, ui thread->WaitStatus = STATUS_SUCCESS; thread->WaitListHead = (LIST_ENTRY*)0; thread->AlertableWait = 0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; thread->SwitchLockCookie = 0x202ULL; KiInitializeApcState(thread); thread->TrapFrame = (KTRAP_FRAME*)0; - void* kstack = MmAllocatePool(KSTACK_SIZE); + void* kstack = MmAllocatePoolTag(KSTACK_SIZE, MM_POOL_TAG('k', 's', 't', 'k')); if (!kstack) { ObDereferenceObject(&thread->Header); return STATUS_NO_MEMORY; @@ -127,10 +139,10 @@ __declspec(dllexport) KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, ui KSTATUS s; void* ustack = (void*)0; - s = MmCreateUserRegion(owner, USTACK_SIZE, PAGE_SIZE, + s = MmVadCreateCommitted(owner, USTACK_SIZE, PAGE_SIZE, PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, - USER_REGION_ZERO | USER_REGION_GUARD_LOW | USER_REGION_GUARD_HIGH, - UserRegionStack, + MM_VAD_ZERO | MM_VAD_GUARD_LOW | MM_VAD_GUARD_HIGH, + MmVadStack, &ustack); if (!KSUCCESS(s) || !ustack) { MmFreePool(kstack); @@ -139,11 +151,19 @@ __declspec(dllexport) KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, ui return STATUS_NO_MEMORY; } thread->UserStackBase = ustack; - // Ring-3 starts at the image entry via IRET, not CALL, so there is no - // pushed return address. The x64 ABI expects RSP % 16 == 8 at function - // entry; bias the initial stack as if a call had happened so compiler - // generated aligned SSE spills (movaps) are safe. - uintptr_t userStackTop = (uintptr_t)ustack + USTACK_SIZE - 8; + // Ring-3 starts at the image entry via IRET, not CALL. The MS x64 ABI + // nevertheless lets callees immediately use the caller-provided 32-byte + // home/shadow area above RSP, and MSVC commonly emits a first instruction + // like "mov [rsp+8], rcx" in unoptimized test code. Bias the synthetic + // entry stack as if a CALL with a normal home area had happened: + // + // RSP % 16 == 8 at function entry + // [RSP + 0] fake return-address slot + // [RSP + 8..39] home/shadow space + // + // Without the 32-byte home area, the first spill lands in the high stack + // guard page and every freshly-created MSVC thread faults immediately. + uintptr_t userStackTop = (uintptr_t)ustack + USTACK_SIZE - 8 - 32; SYS_TEB* teb = (SYS_TEB*)MmAllocateUserMemoryForProcess(owner, sizeof(SYS_TEB)); if (!teb) { @@ -211,8 +231,16 @@ __declspec(dllexport) KSTATUS KeCreateThread(PROCESS* owner, uintptr_t entry, ui KeReleaseSpinlock(&owner->HandleLock, flags); KeInitThreadFpuContext(thread); - KeReadyThread(thread); + + // Hand the creator a short-lived reference before making the thread + // runnable. On SMP a new user thread can run and exit before + // KeCreateThread() returns; without this reference, callers that merely + // read ThreadId for logging or syscall output can race the scheduler's + // dead-thread dereference and touch freed pool. + ObReferenceObject(&thread->Header); *out = thread; + + KeReadyThread(thread); return STATUS_SUCCESS; } @@ -283,6 +311,8 @@ static KSTATUS KiCreateKernelThreadWithAffinity(uintptr_t fn, uintptr_t arg, thread->WaitStatus = STATUS_SUCCESS; thread->WaitListHead = (LIST_ENTRY*)0; thread->AlertableWait = 0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; thread->SwitchLockCookie = 0x202ULL; KiInitializeApcState(thread); thread->TrapFrame = (KTRAP_FRAME*)0; @@ -290,7 +320,7 @@ static KSTATUS KiCreateKernelThreadWithAffinity(uintptr_t fn, uintptr_t arg, thread->Teb = (void*)0; // no TEB; GS_BASE will be 0 in kernel ctx thread->TebSize = 0; - void* kstack = MmAllocatePool(KSTACK_SIZE); + void* kstack = MmAllocatePoolTag(KSTACK_SIZE, MM_POOL_TAG('k', 's', 't', 'k')); if (!kstack) { ObDereferenceObject(&thread->Header); return STATUS_NO_MEMORY; @@ -320,7 +350,16 @@ static KSTATUS KiCreateKernelThreadWithAffinity(uintptr_t fn, uintptr_t arg, owner->ThreadCount++; KeReleaseSpinlock(&owner->HandleLock, flags); - if (out) *out = thread; + // Hand the creator a short-lived reference before making the kernel + // thread runnable. Kernel workers can run, signal their completion event, + // exit, and be reaped on another CPU before KeCreateKernelThread()'s + // caller drops its returned pointer. The live scheduler reference remains + // separate and is released by KeSchedulerPostSwitch() after the dead + // thread has switched away. + if (out) { + ObReferenceObject(&thread->Header); + *out = thread; + } KeInitThreadFpuContext(thread); KeReadyThread(thread); @@ -352,6 +391,8 @@ void KePrepareBlockThread(THREAD* thread, OBJECT* waitObject, uint8_t alertable) thread->WaitStatus = STATUS_PENDING; thread->WaitListHead = (LIST_ENTRY*)0; thread->AlertableWait = alertable ? 1 : 0; + thread->WaitRundownRoutine = (KE_WAIT_RUNDOWN_ROUTINE)0; + thread->WaitRundownContext = (void*)0; } @@ -362,12 +403,41 @@ void KeSetThreadWaitList(THREAD* thread, LIST_ENTRY* waitListHead) { thread->WaitListHead = waitListHead; } +// Registers cleanup for waits with auxiliary wait blocks. +void KeSetThreadWaitRundown(THREAD* thread, KE_WAIT_RUNDOWN_ROUTINE routine, void* context) { + KeAssertIrqlAtOrBelow(DISPATCH_LEVEL); + if (!thread) return; + thread->WaitRundownRoutine = routine; + thread->WaitRundownContext = context; +} + +// Clears auxiliary wait cleanup after the owning wait has removed its blocks. +void KeClearThreadWaitRundown(THREAD* thread) { + KeAssertIrqlAtOrBelow(DISPATCH_LEVEL); + if (!thread) return; + thread->WaitRundownRoutine = (KE_WAIT_RUNDOWN_ROUTINE)0; + thread->WaitRundownContext = (void*)0; +} + +// Runs auxiliary wait cleanup for abort and thread teardown paths. +void KeRundownThreadWait(THREAD* thread, KSTATUS status) { + if (!thread || !thread->WaitRundownRoutine) return; + + KE_WAIT_RUNDOWN_ROUTINE routine = thread->WaitRundownRoutine; + void* context = thread->WaitRundownContext; + thread->WaitRundownRoutine = (KE_WAIT_RUNDOWN_ROUTINE)0; + thread->WaitRundownContext = (void*)0; + routine(thread, context, status); +} + // Implements the finish Wait operation. static void KiFinishWait(THREAD* thread, KSTATUS status) { thread->WaitObject = (OBJECT*)0; thread->WaitStatus = status; thread->WaitListHead = (LIST_ENTRY*)0; thread->AlertableWait = 0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; } // Implements the commit Block Thread operation. @@ -385,6 +455,8 @@ KSTATUS KeCommitBlockThread(THREAD* thread, uint8_t alertable) { if (thread->State != ThreadWaiting) { KSTATUS status = thread->WaitStatus; thread->AlertableWait = 0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; return status; } @@ -406,6 +478,8 @@ KSTATUS KeCommitBlockThread(THREAD* thread, uint8_t alertable) { KiDeliverKernelApcs(); thread->AlertableWait = 0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; return thread->WaitStatus; } @@ -423,6 +497,92 @@ void KeUnblockThreadWithStatus(THREAD* thread, KSTATUS status) { KiSchedulerWakeThread(thread, status); } +// Returns whether the scheduler link still belongs to an object wait list. +static int KiThreadWaitLinkIsLinked(const THREAD* thread) { + if (!thread) return 0; + return thread->SchedulerLink.Fwd != &thread->SchedulerLink || + thread->SchedulerLink.Bwd != &thread->SchedulerLink; +} + +// Removes a thread from one wait list while that wait object's lock is held. +static int KiRemoveThreadFromObjectWaitListLocked(THREAD* thread, LIST_ENTRY* waitListHead) { + if (!thread || !waitListHead) return 0; + if (thread->State != ThreadWaiting || thread->WaitListHead != waitListHead) + return 0; + + // The signaling side pops the wait-list entry before it wakes the thread. + // In that handoff window the scheduler link is already self-linked; abort + // must not remove it again or turn a successful mutex/semaphore handoff + // into an aborted wait for a thread that may already own the object. + if (!KiThreadWaitLinkIsLinked(thread)) + return 0; + + ListRemove(&thread->SchedulerLink); + thread->WaitListHead = (LIST_ENTRY*)0; + thread->SleepUntilTick = 0; + thread->TimeoutStatus = STATUS_SUCCESS; + return 1; +} + +// Removes a thread from an object-owned wait queue using that object's lock. +static int KiAbortObjectWait(THREAD* thread) { + if (!thread || !thread->WaitObject || !thread->WaitListHead) + return 0; + + OBJECT* object = thread->WaitObject; + switch (object->Type) { + case ObjectTypeEvent: { + EVENT* event = CONTAINING_RECORD(object, EVENT, Header); + uintptr_t flags = KeAcquireSpinlock(&event->Lock); + int removed = KiRemoveThreadFromObjectWaitListLocked(thread, &event->WaitList); + KeReleaseSpinlock(&event->Lock, flags); + return removed; + } + case ObjectTypeMutex: { + MUTEX* mutex = CONTAINING_RECORD(object, MUTEX, Header); + uintptr_t flags = KeAcquireSpinlock(&mutex->Lock); + int removed = KiRemoveThreadFromObjectWaitListLocked(thread, &mutex->WaitList); + KeReleaseSpinlock(&mutex->Lock, flags); + return removed; + } + case ObjectTypeSemaphore: { + SEMAPHORE* semaphore = CONTAINING_RECORD(object, SEMAPHORE, Header); + uintptr_t flags = KeAcquireSpinlock(&semaphore->Lock); + int removed = KiRemoveThreadFromObjectWaitListLocked(thread, &semaphore->WaitList); + KeReleaseSpinlock(&semaphore->Lock, flags); + return removed; + } + default: + return 0; + } +} + +// Aborts a thread's current wait regardless of whether the wait was marked +// alertable. Process teardown uses this for crash/kill paths: a background +// worker blocked forever on a semaphore must not keep the process exit event +// unsignaled after the foreground thread has faulted. Object wait-list removal +// must use the object's own lock; otherwise signal/abort races can leave stale +// scheduler links in an event, mutex, or semaphore wait queue. +void KeAbortThreadWait(THREAD* thread, KSTATUS status) { + if (!thread) return; + if (thread->State != ThreadWaiting) return; + + if (thread->WaitRundownRoutine) { + KeRundownThreadWait(thread, status); + return; + } + + if (KiAbortObjectWait(thread)) { + KeUnblockThreadWithStatus(thread, status); + return; + } + + // Sleep/timeouts and already-dequeued handoffs are owned by the scheduler. + // This path removes from the scheduler sleep list when present and otherwise + // completes the wait in place. + KeUnblockThreadFromWaitListWithStatus(thread, status); +} + // Implements the unblock Thread operation. void KeUnblockThread(THREAD* thread) { KeUnblockThreadWithStatus(thread, STATUS_SUCCESS); diff --git a/code/kernel/ke_work.c b/code/kernel/ke_work.c new file mode 100644 index 0000000..4f41b38 --- /dev/null +++ b/code/kernel/ke_work.c @@ -0,0 +1,117 @@ +// Kernel passive-level work queue. This file owns the generic mechanism used +// to move ISR/DPC-originated work to a sleepable system thread context. + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static SPINLOCK gWorkQueueLock = SPINLOCK_INIT; +static LIST_ENTRY gWorkQueueList; +static EVENT* gWorkQueueEvent; +static volatile long gWorkQueueListReady; +static volatile long gWorkQueueStartState; // 0=not started, 1=starting, 2=started. + +static void KiWorkQueueWorker(void* arg) { + (void)arg; + KeAssertIrql(PASSIVE_LEVEL); + + for (;;) { + (void)KeWaitForEvent(gWorkQueueEvent, 0); + + for (;;) { + KWORK_ITEM* item = (KWORK_ITEM*)0; + uintptr_t flags = KeAcquireSpinlock(&gWorkQueueLock); + if (!LIST_IS_EMPTY(&gWorkQueueList)) { + LIST_ENTRY* node = ListPopHead(&gWorkQueueList); + item = CONTAINING_RECORD(node, KWORK_ITEM, Link); + } + KeReleaseSpinlock(&gWorkQueueLock, flags); + + if (!item) + break; + + // Allow the item to be queued again while it is running. This keeps + // level-triggered users such as cursor updates naturally coalesced + // but does not lose an update that arrives during the callback. + _InterlockedExchange(&item->Queued, 0); + if (item->Routine) + item->Routine(item->Context); + } + } +} + +void KeInitializeWorkItem(KWORK_ITEM* item, KWORK_ROUTINE routine, void* context) { + if (!item) + return; + LIST_INIT(&item->Link); + item->Routine = routine; + item->Context = context; + item->Queued = 0; +} + +bool KeIsWorkQueueStarted(void) { + return _InterlockedCompareExchange(&gWorkQueueStartState, 2, 2) == 2; +} + +bool KeQueueWorkItem(KWORK_ITEM* item) { + if (!item || !item->Routine) + return false; + + if (_InterlockedCompareExchange(&item->Queued, 1, 0) != 0) + return false; + + if (!_InterlockedCompareExchange(&gWorkQueueListReady, 1, 1)) { + _InterlockedExchange(&item->Queued, 0); + return false; + } + + uintptr_t flags = KeAcquireSpinlock(&gWorkQueueLock); + ListInsertTail(&gWorkQueueList, &item->Link); + EVENT* event = gWorkQueueEvent; + KeReleaseSpinlock(&gWorkQueueLock, flags); + + if (event) + KeSignalEvent(event); + return true; +} + +KSTATUS KeStartWorkQueue(void) { + long state = _InterlockedCompareExchange(&gWorkQueueStartState, 1, 0); + if (state == 2) + return STATUS_SUCCESS; + if (state == 1) { + while (_InterlockedCompareExchange(&gWorkQueueStartState, 2, 2) == 1) + KeYield(); + return KeIsWorkQueueStarted() ? STATUS_SUCCESS : STATUS_FAILURE; + } + + LIST_INIT(&gWorkQueueList); + _InterlockedExchange(&gWorkQueueListReady, 1); + + KSTATUS s = KeEventCreate(EventAutoReset, 0, &gWorkQueueEvent); + if (KSUCCESS(s)) { + THREAD* worker = (THREAD*)0; + s = KeCreateKernelThread((uintptr_t)KiWorkQueueWorker, + 0, + PriorityNormal, + &worker); + // Persistent system worker; keep the creator reference for kernel life. + (void)worker; + } + + if (KSUCCESS(s)) { + _InterlockedExchange(&gWorkQueueStartState, 2); + KeSignalEvent(gWorkQueueEvent); + } else { + _InterlockedExchange(&gWorkQueueStartState, 0); + } + return s; +} diff --git a/code/kernel/kmain.c b/code/kernel/kmain.c index 94e526f..c47a259 100644 --- a/code/kernel/kmain.c +++ b/code/kernel/kmain.c @@ -7,7 +7,9 @@ #pragma intrinsic(_enable) #include #include +#include #include +#include #include #include #include @@ -28,6 +30,9 @@ #include #include #include +#include +#include +#include #include typedef struct { @@ -35,6 +40,90 @@ typedef struct { const HAL_API* hal; } SYSTEM_INIT_ARGS; +#define KI_SWAP_MIN_MB 4ull +#define KI_SWAP_MAX_MB 8192ull + +// Parses a required unsigned decimal integer from settings.ini. +// Configuration errors are treated as boot-time corruption instead of guessed defaults. +static uint64_t KiParseRequiredUInt64Setting(const char* section, const char* key) { + char value[64]; + KSTATUS s = KeGetSettingString(section, key, value, sizeof(value)); + if (!KSUCCESS(s)) { + KePanic(KE_PANIC_CODE_INIT, + "settings: missing required [%s] %s entry, status=0x%x\n", + section, + key, + s); + } + + uint64_t result = 0; + const char* p = value; + if (!*p) { + KePanic(KE_PANIC_CODE_INIT, + "settings: empty numeric value for [%s] %s\n", + section, + key); + } + + while (*p) { + if (*p < '0' || *p > '9') { + KePanic(KE_PANIC_CODE_INIT, + "settings: invalid numeric value for [%s] %s: %s\n", + section, + key, + value); + } + + uint64_t digit = (uint64_t)(*p - '0'); + if (result > (UINT64_MAX - digit) / 10ull) { + KePanic(KE_PANIC_CODE_INIT, + "settings: numeric value for [%s] %s is too large: %s\n", + section, + key, + value); + } + + result = result * 10ull + digit; + ++p; + } + + return result; +} + +// Initializes swap according to settings.ini. +// A size of zero is the explicit opt-out. Any missing or invalid size is a broken configuration. +static void KiInitializeSwapFromSettings(void) { + uint64_t swapMb = KiParseRequiredUInt64Setting("vm", "swap_size_mb"); + if (swapMb == 0) { + DbgPrint("swap: disabled by settings\n"); + return; + } + + if (swapMb < KI_SWAP_MIN_MB || swapMb > KI_SWAP_MAX_MB) { + KePanic(KE_PANIC_CODE_INIT, + "settings: [vm] swap_size_mb=%llu is outside the allowed range %llu..%llu MB\n", + (unsigned long long)swapMb, + (unsigned long long)KI_SWAP_MIN_MB, + (unsigned long long)KI_SWAP_MAX_MB); + } + + char pagefilePath[1024]; + if (!KeBuildSystemRootPath("pagefile.sys", pagefilePath, sizeof(pagefilePath))) { + KePanic(KE_PANIC_CODE_INIT, + "swap: failed to build system-root pagefile path\n"); + } + + uint64_t swapBytes = swapMb * 1024ull * 1024ull; + KSTATUS s = MmSwapInitialize(SysSvFromCstr(pagefilePath), swapBytes); + if (!KSUCCESS(s)) { + KePanic(KE_PANIC_CODE_INIT, + "swap: failed to initialize %llu MB pagefile at %s, status=0x%x\n", + (unsigned long long)swapMb, + pagefilePath, + s); + } +} + typedef KSTATUS (*DRIVER_ENTRY_FN)(const HAL_API *); // HAL pointer cached at boot for subsystems that still need platform services. @@ -68,7 +157,7 @@ static void KiReclaimBootloaderMemory(const BOOT_INFO* bootInfo) { for (uintptr_t page = base; page < end; page += PAGE_SIZE) { if (!KiPageInBootloaderRegion(bootInfo, (PHYSICAL_ADDRESS)page)) continue; - MmFreeFrame((PHYSICAL_ADDRESS)page); + MmPfnFree((PHYSICAL_ADDRESS)page); } } } @@ -86,6 +175,7 @@ static void KiResolveSystemImports(BOOT_INFO* bootInfo) { } } + // Phase Helper: Initialize dynamically loaded .sys drivers. static void KiDriversIniCb(const char* section, const char* key, const char* val, void* ud) { @@ -120,7 +210,7 @@ static void KiInitializeDrivers(BOOT_INFO* bootInfo, const HAL_API* hal) { // Map the PE image into kernel space. uintptr_t mappedBase = 0; - KSTATUS st = LdrLoadImage(rawPeBuffer, mod->ImageSize, &mappedBase, false); + KSTATUS st = LdrLoadKernelImage(rawPeBuffer, mod->ImageSize, &mappedBase); if (!KSUCCESS(st)) { DbgPrint("ERROR: Failed to map driver %s (0x%X)\n", mod->Name, st); continue; @@ -129,7 +219,9 @@ static void KiInitializeDrivers(BOOT_INFO* bootInfo, const HAL_API* hal) { // Update the struct so other modules can find it. mod->VirtBase = mappedBase; - // Resolve imports (can now see Kernel, HAL, and previous drivers). + // Resolve imports while the image is still writable. Kernel driver images + // stay writable until the loader grows real section-backed mappings; the + // loader still clears NX for the whole image before returning it. if (!KSUCCESS(LdrResolveImports(mappedBase))) { DbgPrint("ERROR: Failed to resolve imports for %s\n", mod->Name); continue; @@ -152,9 +244,24 @@ static void KiInitializeDrivers(BOOT_INFO* bootInfo, const HAL_API* hal) { KSTATUS s; FILE_OBJECT* f; - s = IoOpenFile(SYS_SV_LITERAL("drivers\\drivers.ini"), &f); + s = KeInitializeSystemRootVolume(); if (s != STATUS_SUCCESS) { - KePanic(KE_PANIC_CODE_INIT, "KiInitializeDrivers: Failed to open drivers.ini (status %d)\n", s); + KePanic(KE_PANIC_CODE_INIT, "KiInitializeDrivers: Failed to find system root volume (status %d)\n", s); + } + + char root[4]; + root[0] = (char)('0' + KeGetSystemRootDrive()); + root[1] = ':'; + root[2] = '/'; + root[3] = '\0'; + s = IoSetCwd(SysSvFromCstr(root)); + if (s != STATUS_SUCCESS) { + KePanic(KE_PANIC_CODE_INIT, "KiInitializeDrivers: Failed to set system CWD to %s (status %d)\n", root, s); + } + + s = KeOpenSystemRootFile("drivers\\drivers.ini", &f); + if (s != STATUS_SUCCESS) { + KePanic(KE_PANIC_CODE_INIT, "KiInitializeDrivers: Failed to open drivers.ini on system root (status %d)\n", s); } uint64_t size = IoGetFileSize(f) + 1; @@ -174,26 +281,27 @@ static void KiInitializeDrivers(BOOT_INFO* bootInfo, const HAL_API* hal) { for (uint32_t i = 0; i < arr_len(runtimeKeys); i++) { DbgPrint("Found runtime driver: %s\n", runtimeKeys[i]); - // Load the driver. + char path[1024]; + char relative[512]; size_t len = strlen(runtimeKeys[i]); - char* path = MmAllocatePool(len + 10); - if (!path) { - DbgPrint("KernelMain: Failed to allocate memory for driver path. How?\n"); + if (len + 9 > sizeof(relative)) { + DbgPrint("KiInitializeDrivers: runtime driver path too long: %s\n", runtimeKeys[i]); continue; } + memcpy(relative, "drivers\\", 8); + memcpy(relative + 8, runtimeKeys[i], len + 1); - // memcpy maneuvers. - memcpy(path, "drivers\\", 8); - memcpy(path + 8, runtimeKeys[i], len + 1); + if (!KeBuildSystemRootPath(relative, path, sizeof(path))) { + DbgPrint("KiInitializeDrivers: failed to build runtime driver path: %s\n", runtimeKeys[i]); + continue; + } s = IoOpenFile(SysSvFromCstr(path), &f); if (s != STATUS_SUCCESS) { DbgPrint("KiInitializeDrivers: Failed to open runtime driver: %s (%s)\n", runtimeKeys[i], path); - continue; // Skip this driver but keep going with others + continue; } - MmFreePool(path); - uint64_t drvSize = IoGetFileSize(f); void* drvMem = MmAllocatePool(drvSize); if (!KSUCCESS(IoReadFile(f, drvMem, drvSize, &got))) { @@ -204,7 +312,7 @@ static void KiInitializeDrivers(BOOT_INFO* bootInfo, const HAL_API* hal) { IoCloseFile(f); uintptr_t mappedBase = 0; - if (!KSUCCESS(LdrLoadImage(drvMem, drvSize, &mappedBase, false))) { // Load into kernel space + if (!KSUCCESS(LdrLoadKernelImage(drvMem, drvSize, &mappedBase))) { // Load into kernel space DbgPrint("KiInitializeDrivers: Failed to load runtime driver: %s\n", runtimeKeys[i]); MmFreePool(drvMem); continue; @@ -215,7 +323,6 @@ static void KiInitializeDrivers(BOOT_INFO* bootInfo, const HAL_API* hal) { DbgPrint("KiInitializeDrivers: Failed to resolve imports for runtime driver: %s\n", runtimeKeys[i]); continue; } - // Runtime modules may also export DDIs to drivers loaded later. Boot // drivers were already registered above; keep the module model uniform. LdrRegisterModule(runtimeKeys[i], mappedBase); @@ -250,28 +357,15 @@ static PROCESS* KiLaunchUserProcess(const char* filePath, const char* args) { KSTATUS s; FILE_OBJECT* f; - // Read the executable file into a raw buffer. - s = IoOpenFile(SysSvFromCstr(filePath), &f); + // Kernel-launched user images are rooted at the selected system volume. + s = KeOpenSystemRootFile(filePath, &f); if (s != STATUS_SUCCESS) { - DbgPrint("Failed to load file: \"%s\"!\n", filePath); + DbgPrint("Failed to load system file: \"%s\" (status %d)!\n", filePath, s); return 0; } uint64_t size = IoGetFileSize(f); - - void* mem = MmAllocatePool(size); - if (!mem) { - IoCloseFile(f); - return 0; - } - - size_t got = 0; - s = IoReadFile(f, mem, size, &got); - IoCloseFile(f); - if (!KSUCCESS(s)) { - MmFreePool(mem); - return 0; - } + (void)size; // Create the process object. PROCESS* proc; @@ -280,7 +374,7 @@ static PROCESS* KiLaunchUserProcess(const char* filePath, const char* args) { s = KeCreateProcess(filePath, &proc); if (!KSUCCESS(s)) { DbgPrint("Failed to create process. System halted.\n"); - MmFreePool(mem); + IoCloseFile(f); return 0; } DbgPrint("Created process (PID %u) with command line: %s\n", proc->ProcessId, (args && args[0]) ? args : filePath); @@ -291,17 +385,17 @@ static PROCESS* KiLaunchUserProcess(const char* filePath, const char* args) { SysSvFromCstr(proc->CurrentDirectory)); if (!KSUCCESS(s)) { DbgPrint("KiLaunchUserProcess: Failed to build user process environment. Status: %d\n", s); - MmFreePool(mem); + IoCloseFile(f); ObDereferenceObject(&proc->Header); return 0; } - // Parse the raw PE file and map it into executable memory. + // Map the executable through an image section backed by the open file. uintptr_t imageBase = 0; - s = LdrLoadImageForProcess(proc, mem, size, &imageBase); - MmFreePool(mem); + s = LdrLoadImageFileForProcess(proc, f, &imageBase); + IoCloseFile(f); if (!KSUCCESS(s)) { - DbgPrint("KiLaunchUserProcess: LdrLoadImage failed with status: %d\n", s); + DbgPrint("KiLaunchUserProcess: LdrLoadImageFileForProcess failed with status: %d\n", s); ObDereferenceObject(&proc->Header); return 0; } @@ -313,6 +407,13 @@ static PROCESS* KiLaunchUserProcess(const char* filePath, const char* args) { return 0; } + s = LdrProtectImageForProcess(proc, imageBase, true); + if (!KSUCCESS(s)) { + DbgPrint("KiLaunchUserProcess: Failed to protect EXE image for %s (status %d)\n", filePath, s); + ObDereferenceObject(&proc->Header); + return 0; + } + proc->ImageBase = imageBase; proc->ImageSize = LdrGetMappedImageSize(proc, imageBase); @@ -327,7 +428,9 @@ static PROCESS* KiLaunchUserProcess(const char* filePath, const char* args) { ObDereferenceObject(&proc->Header); return 0; } - DbgPrint("KiLaunchUserProcess: Thread created and queued!\n"); + uint32_t tid = thread->ThreadId; + DbgPrint("KiLaunchUserProcess: Thread created and queued! TID=%u\n", tid); + ObDereferenceObject(&thread->Header); // Caller owns the reference returned by KeCreateProcess. return proc; @@ -344,35 +447,39 @@ void SystemInitThread(void* arg) { KiInitializeDrivers(args->bootInfo, args->hal); KeTimeReloadSettings(); + KiInitializeSwapFromSettings(); + KiResolveSystemImports(args->bootInfo); KiReclaimBootloaderMemory(args->bootInfo); if (args->bootInfo->Debug & KE_DEBUG_SELFTEST) KiRunKernelSelfTests(args->hal); - // Launch User-Mode init.exe and restart it until it exits cleanly. - while (true) { - PROCESS* shellProc = KiLaunchUserProcess("system/init.exe", args->bootInfo->CommandLine); - if (!shellProc) { - DbgPrint("SystemInitThread: Failed to start system/init.exe; retrying.\n"); - KeYield(); - continue; - } - - KeWaitForEvent(shellProc->ExitEvent, 0); - int exitCode = shellProc->ExitCode; - DbgPrint("SystemInitThread: init.exe exited with code %d.\n", exitCode); - ObDereferenceObject(&shellProc->Header); - - if (exitCode == 0) break; - DbgPrint("SystemInitThread: Restarting init.exe.\n"); + // Launch User-Mode init.exe. + PROCESS* shellProc = KiLaunchUserProcess("system/init.exe", args->bootInfo->CommandLine); + if (!shellProc) { + DbgPrint("SystemInitThread: Failed to start system/init.exe.\n"); + KePanic(KE_PANIC_CODE_INIT, "SystemInitThread: Failed to start system/init.exe.\n"); } - DbgPrint("SystemInitThread: init.exe completed successfully; shutting down.\n"); + KeWaitForEvent(shellProc->ExitEvent, 0); + int exitCode = shellProc->ExitCode; + if (exitCode != 0) { + KePanic(KE_PANIC_CODE_INIT, "SystemInitThread: init.exe exited with code %d.\n", exitCode); + } + DbgPrint("SystemInitThread: init.exe exited with code %d.\n", exitCode); + ObDereferenceObject(&shellProc->Header); + if (args->bootInfo->Debug & (KE_DEBUG_CONSOLE | KE_DEBUG_FILE_LOG)) { - KSTATUS logStatus = KeFlushDebugLogToFile(SYS_SV_LITERAL("0:/kernel.log")); - if (!KSUCCESS(logStatus)) - DbgPrint("debuglog: shutdown flush to 0:/kernel.log failed st=0x%x\n", logStatus); + char logPath[1024]; + KSTATUS logStatus = STATUS_NOT_FOUND; + if (KeBuildSystemRootPath("kernel.log", logPath, sizeof(logPath))) { + logStatus = KeFlushDebugLogToFile(SysSvFromCstr(logPath)); + if (!KSUCCESS(logStatus)) + DbgPrint("debuglog: shutdown flush to %s failed st=0x%x\n", logPath, logStatus); + } else { + DbgPrint("debuglog: shutdown flush skipped; system root path unavailable\n"); + } } gKmainHal->Power.Shutdown(); MmFreePool(args); @@ -459,6 +566,11 @@ extern void KernelMain(BOOT_INFO* bootInfo) { KeInitializeScheduler(); + KSTATUS workStatus = KeStartWorkQueue(); + if (!KSUCCESS(workStatus)) { + KePanic(KE_PANIC_CODE_INIT, "workq: failed to start passive work queue, status=0x%x\n", workStatus); + } + KSTATUS smpSchedStatus = KeAdmitApplicationProcessorsToScheduler(); if (!KSUCCESS(smpSchedStatus)) { KePanic(KE_PANIC_CODE_INIT, "SMP: failed to admit APs to scheduler, status=0x%x\n", smpSchedStatus); @@ -471,11 +583,11 @@ extern void KernelMain(BOOT_INFO* bootInfo) { initArgs->bootInfo = bootInfo; initArgs->hal = hal; - // Spawn the Initialization Thread. - THREAD* initThread = (THREAD*)0; - KSTATUS st = KeCreateKernelThread((uintptr_t)SystemInitThread, - (uintptr_t)initArgs, - PriorityNormal, &initThread); + // Spawn the Initialization Thread. No creator reference is needed; the + // scheduler owns the live thread lifetime. + KSTATUS st = KeCreateKernelThread((uintptr_t)SystemInitThread, + (uintptr_t)initArgs, + PriorityNormal, (THREAD**)0); if (!KSUCCESS(st)) { KePanic(KE_PANIC_CODE_INIT, "KernelMain: Failed to create SystemInitThread (status 0x%x)\n", st); diff --git a/code/kernel/ldr_image.c b/code/kernel/ldr_image.c index fb97fc7..5fe64c1 100644 --- a/code/kernel/ldr_image.c +++ b/code/kernel/ldr_image.c @@ -3,11 +3,13 @@ #include #include #include +#include #include #include #include #include #include +#include #ifndef LDR_DEBUG_VERBOSE #define LDR_DEBUG_VERBOSE 0 @@ -155,6 +157,8 @@ typedef struct { #define IMAGE_REL_BASED_DIR64 10 #define IMAGE_SCN_CNT_CODE 0x00000020u +#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040u +#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080u #define IMAGE_SCN_MEM_EXECUTE 0x20000000u #define IMAGE_SCN_MEM_READ 0x40000000u #define IMAGE_SCN_MEM_WRITE 0x80000000u @@ -171,6 +175,11 @@ typedef struct { static MODULE_ENTRY gModules[MAX_MODULES]; static uint32_t gModuleCount = 0; +// Ensures a user image page is privately writable before the kernel patches it. +static KSTATUS KiEnsureUserImageWrite(PROCESS* process, bool user, uintptr_t address, size_t size) { + if (!user) return STATUS_SUCCESS; + return MmVadMakeImageRangePrivate(process, (void*)address, size); +} // Implements the canonical Module Name operation. static const char* KiCanonicalModuleName(const char* name) { @@ -310,10 +319,10 @@ size_t LdrGetImageSize(uintptr_t base) { size_t LdrGetMappedImageSize(PROCESS* process, uintptr_t base) { if (!base) return 0; if (process) { - size_t regionSize = MmGetUserRegionSize(process, (const void*)base); + size_t regionSize = MmVadGetSize(process, (const void*)base); if (regionSize) return regionSize; } - size_t globalRegionSize = MmGetUserRegionSizeByBase((const void*)base); + size_t globalRegionSize = MmVadGetSizeByBase((const void*)base); if (globalRegionSize) return globalRegionSize; return LdrGetImageSize(base); } @@ -327,13 +336,26 @@ static void KiFreeLoadedImageForProcess(PROCESS* process, uintptr_t imageBase, b else MmFreePool((void*)imageBase); } -// Releases loaded Image. -static void KiFreeLoadedImage(uintptr_t imageBase, bool user) { - KiFreeLoadedImageForProcess(KeGetCurrentProcess(), imageBase, user); +// Returns the final page flags for a PE section. +static uint64_t KiSectionPageProtection(const SECTION_HEADER* section, bool user) { + uint64_t prot = PAGE_PRESENT | (user ? PAGE_USER : 0); + + // Some old toolchains mark BSS-like sections as uninitialized data without + // setting MEM_WRITE. Treating those pages as read-only breaks programs that + // legitimately write their zero-fill storage. + if ((section->Characteristics & IMAGE_SCN_MEM_WRITE) || + (section->Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA)) { + prot |= PAGE_WRITE; + } + + if (!((section->Characteristics & IMAGE_SCN_MEM_EXECUTE) || + (section->Characteristics & IMAGE_SCN_CNT_CODE))) { + prot |= PAGE_NX; + } + return prot; } - -// Implements the ldr Protect Image For Process operation. +// Applies final page protections to a mapped PE image. KSTATUS LdrProtectImageForProcess(PROCESS* process, uintptr_t imageBase, bool user) { if (!imageBase) return STATUS_INVALID_PARAMETER; if (user && !process) return STATUS_INVALID_PARAMETER; @@ -353,6 +375,14 @@ KSTATUS LdrProtectImageForProcess(PROCESS* process, uintptr_t imageBase, bool us if (sectionTableOffset > mappedSize || sectionTableSize > mappedSize - sectionTableOffset) return STATUS_FAILURE; + size_t pageCount = (mappedSize + PAGE_SIZE - 1) / PAGE_SIZE; + uint64_t* pageProt = (uint64_t*)MmAllocatePoolTag(pageCount * sizeof(uint64_t), MM_POOL_TAG('i', 'p', 'r', 't')); + if (!pageProt) return STATUS_NO_MEMORY; + + uint64_t headerProt = PAGE_PRESENT | (user ? PAGE_USER : 0) | PAGE_NX; + for (size_t i = 0; i < pageCount; ++i) + pageProt[i] = headerProt; + SECTION_HEADER* sec = (SECTION_HEADER *)(imageBase + sectionTableOffset); for (int i = 0; i < pe->NumberOfSections; i++) { size_t protectSize = sec[i].VirtualSize ? sec[i].VirtualSize : sec[i].SizeOfRawData; @@ -368,35 +398,56 @@ KSTATUS LdrProtectImageForProcess(PROCESS* process, uintptr_t imageBase, bool us (unsigned long long)imageBase, (unsigned long long)mappedSize); continue; } - if (secEnd < secBase) return STATUS_NO_MEMORY; + if (secEnd < secBase) { + MmFreePool(pageProt); + return STATUS_NO_MEMORY; + } if (secEnd > mapEnd) { LdrDbg("LDR: protect clamping section %d end=%016llx to mapEnd=%016llx\n", i, (unsigned long long)secEnd, (unsigned long long)mapEnd); - protectSize = (size_t)(mapEnd - secBase); + secEnd = mapEnd; } - uint64_t prot = PAGE_PRESENT | (user ? PAGE_USER : 0); - if (sec[i].Characteristics & IMAGE_SCN_MEM_WRITE) - prot |= PAGE_WRITE; - if (!((sec[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) || - (sec[i].Characteristics & IMAGE_SCN_CNT_CODE))) - prot |= PAGE_NX; - - KSTATUS s; - if (user) { - s = MmProtectUserRegion(process, (void*)secBase, protectSize, prot); - } else { - s = MmSetPageProtect(secBase, protectSize, prot); - } - if (!KSUCCESS(s)) { - LdrDbg("LDR: protect failed section=%d va=%016llx size=%llx prot=%016llx st=%d image=%016llx mapped=%llx\n", - i, (unsigned long long)secBase, (unsigned long long)protectSize, - (unsigned long long)prot, s, - (unsigned long long)imageBase, (unsigned long long)mappedSize); - return s; + size_t firstPage = (size_t)((secBase - imageBase) / PAGE_SIZE); + size_t lastPage = (size_t)((secEnd - 1 - imageBase) / PAGE_SIZE); + uint64_t prot = KiSectionPageProtection(&sec[i], user); + for (size_t page = firstPage; page <= lastPage && page < pageCount; ++page) { + // Protection is page-granular. If sections ever share a page, the + // hardware page must allow the union of their access requirements. + if (prot & PAGE_WRITE) + pageProt[page] |= PAGE_WRITE; + if ((prot & PAGE_NX) == 0) + pageProt[page] &= ~PAGE_NX; } } - return STATUS_SUCCESS; + + size_t runStart = 0; + KSTATUS s = STATUS_SUCCESS; + while (runStart < pageCount) { + uint64_t prot = pageProt[runStart]; + size_t runEnd = runStart + 1; + while (runEnd < pageCount && pageProt[runEnd] == prot) + runEnd++; + + uintptr_t va = imageBase + runStart * PAGE_SIZE; + size_t bytes = (runEnd - runStart) * PAGE_SIZE; + if (user) + s = MmVadProtect(process, (void*)va, bytes, prot); + else + s = MmPteProtectRange(va, bytes, prot); + + if (!KSUCCESS(s)) { + LdrDbg("LDR: protect failed va=%016llx size=%llx prot=%016llx st=%d image=%016llx mapped=%llx\n", + (unsigned long long)va, (unsigned long long)bytes, + (unsigned long long)prot, s, + (unsigned long long)imageBase, (unsigned long long)mappedSize); + break; + } + runStart = runEnd; + } + + MmFreePool(pageProt); + return s; } @@ -408,7 +459,7 @@ uintptr_t LdrGetEntryPoint(uintptr_t base) { } // Base Relocation. -static KSTATUS LdrApplyRelocations(uintptr_t base, uintptr_t delta) { +static KSTATUS LdrApplyRelocationsForProcess(PROCESS* process, uintptr_t base, uintptr_t delta, bool user) { uint32_t peOffset = *(uint32_t *)(base + 0x3C); PE_HEADER* pe = (PE_HEADER *)(base + peOffset); OPT_HEADER64* opt = &pe->OptionalHeader; @@ -432,6 +483,8 @@ static KSTATUS LdrApplyRelocations(uintptr_t base, uintptr_t delta) { // For 64-bit PEs, we mostly care about DIR64 (Type 10). if (type == IMAGE_REL_BASED_DIR64) { uint64_t* ptr = (uint64_t *)(base + block->VirtualAddress + offset); + KSTATUS ws = KiEnsureUserImageWrite(process, user, (uintptr_t)ptr, sizeof(*ptr)); + if (!KSUCCESS(ws)) return ws; *ptr += delta; } } @@ -534,6 +587,8 @@ static KSTATUS LdrResolveImportsCommon( return STATUS_NOT_FOUND; } + KSTATUS ws = KiEnsureUserImageWrite(process, process != (PROCESS*)0, (uintptr_t)thunk, sizeof(*thunk)); + if (!KSUCCESS(ws)) return ws; *thunk = (uint64_t)addr; thunk++; @@ -566,6 +621,41 @@ KSTATUS LdrResolveImportsForProcess(PROCESS* process, uintptr_t imageBase) { ); } +// Loads a user image from an open file through the image-section object model. +KSTATUS LdrLoadImageFileForProcess(PROCESS* process, FILE_OBJECT* file, uintptr_t* baseOut) { + if (!process || !file || !baseOut) + return STATUS_INVALID_PARAMETER; + *baseOut = 0; + + IMAGE_SECTION_OBJECT* section = (IMAGE_SECTION_OBJECT*)0; + KSTATUS s = LdrCreateImageSectionFromFile(file, §ion); + if (!KSUCCESS(s)) + return s; + + void* view = (void*)0; + s = LdrMapImageSection(process, section, &view); + if (!KSUCCESS(s)) { + LdrDereferenceImageSection(section); + return s; + } + + const IMAGE_SECTION_METADATA* metadata = LdrImageSectionMetadata(section); + uintptr_t imageBase = (uintptr_t)view; + uintptr_t delta = imageBase - metadata->PreferredBase; + if (delta != 0) { + s = LdrApplyRelocationsForProcess(process, imageBase, delta, true); + if (!KSUCCESS(s)) { + KiFreeLoadedImageForProcess(process, imageBase, true); + LdrDereferenceImageSection(section); + return s; + } + } + + LdrDereferenceImageSection(section); + *baseOut = imageBase; + return STATUS_SUCCESS; +} + // Implements the ldr Register Process Module operation. static KSTATUS LdrRegisterProcessModule(PROCESS* process, const char* name, uintptr_t base) { if (!process || !name || !base) return STATUS_INVALID_PARAMETER; @@ -602,36 +692,27 @@ KSTATUS LdrLoadModuleForProcess(PROCESS* process, const char* filename, uintptr_ FILE_OBJECT* f; KSTATUS s = IoOpenFile(SysSvFromCstr(filename), &f); if (!KSUCCESS(s)) { - char path[260]; - memcpy(path, "0:/system/", 10); - memcpy(path + 10, filename, strnlen(filename, 248) + 1); - path[259] = '\0'; - s = IoOpenFile(SysSvFromCstr(path), &f); + char relative[260]; + size_t nameLen = strnlen(filename, sizeof(relative) - 8); + if (nameLen + 8 >= sizeof(relative)) + return STATUS_BUFFER_TOO_SMALL; + memcpy(relative, "system/", 7); + memcpy(relative + 7, filename, nameLen); + relative[7 + nameLen] = '\0'; + + s = KeOpenSystemRootFile(relative, &f); if (!KSUCCESS(s)) { - DbgPrint("LdrLoadModuleForProcess: Failed to open %s (%d)\n", filename, s); + DbgPrint("LdrLoadModuleForProcess: Failed to open %s on system root (%d)\n", filename, s); return s; } } uint64_t fileSize = IoGetFileSize(f); LdrDbg("LDR: load module pid=%u file=%s size=%llu\n", process->ProcessId, filename, fileSize); - void* mem = MmAllocatePool((size_t)fileSize); - if (!mem) { - IoCloseFile(f); - return STATUS_NO_MEMORY; - } - - size_t got = 0; - s = IoReadFile(f, mem, (size_t)fileSize, &got); - IoCloseFile(f); - if (!KSUCCESS(s)) { - MmFreePool(mem); - return s; - } uintptr_t imageBase = 0; - s = LdrLoadImageForProcess(process, mem, (size_t)fileSize, &imageBase); - MmFreePool(mem); + s = LdrLoadImageFileForProcess(process, f, &imageBase); + IoCloseFile(f); if (!KSUCCESS(s)) { DbgPrint("LDR: load image for module %s failed st=%d\n", filename, s); return s; @@ -665,10 +746,9 @@ KSTATUS LdrLoadModuleForProcess(PROCESS* process, const char* filename, uintptr_ return STATUS_SUCCESS; } -// LdrLoadImage - load a PE from a raw buffer into virtual space. -static KSTATUS KiLdrLoadImageCommon(PROCESS* process, const void* buffer, size_t size, uintptr_t* baseOut, bool user) { +// Loads a boot/kernel PE image from a raw memory buffer into kernel space. +static KSTATUS KiLdrLoadKernelImageFromBuffer(const void* buffer, size_t size, uintptr_t* baseOut) { if (!buffer || !size || !baseOut) return STATUS_INVALID_PARAMETER; - if (user && !process) return STATUS_INVALID_PARAMETER; if (size < 0x40) return STATUS_FAILURE; const uint8_t* bytes = (const uint8_t*)buffer; @@ -687,13 +767,13 @@ static KSTATUS KiLdrLoadImageCommon(PROCESS* process, const void* buffer, size_t uint16_t optionalMagic = *(const uint16_t *)(bytes + peOffset + 24); if (machine == IMAGE_FILE_MACHINE_I386 || optionalMagic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { - DbgPrint("LdrLoadImage: hard error: 32-bit PE images are not supported (machine=0x%X magic=0x%X)\n", + DbgPrint("LdrLoadKernelImage: hard error: 32-bit PE images are not supported (machine=0x%X magic=0x%X)\n", machine, optionalMagic); return STATUS_NOT_SUPPORTED; } if (machine != IMAGE_FILE_MACHINE_AMD64 || optionalMagic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { - DbgPrint("LdrLoadImage: unsupported PE image (machine=0x%X magic=0x%X)\n", + DbgPrint("LdrLoadKernelImage: unsupported PE image (machine=0x%X magic=0x%X)\n", machine, optionalMagic); return STATUS_NOT_SUPPORTED; } @@ -704,17 +784,16 @@ static KSTATUS KiLdrLoadImageCommon(PROCESS* process, const void* buffer, size_t KSTATUS sizeStatus = KiComputeRequiredImageSize(pe, size, &imageSize); if (!KSUCCESS(sizeStatus)) return sizeStatus; - LdrDbg("LDR: image header sizeOfImage=%x required=%llx preferred=%016llx sections=%u user=%d pid=%u\n", + LdrDbg("LDR: kernel image header sizeOfImage=%x required=%llx preferred=%016llx sections=%u\n", opt->SizeOfImage, (unsigned long long)imageSize, (unsigned long long)opt->ImageBase, - pe->NumberOfSections, - user ? 1 : 0, - process ? process->ProcessId : 0); - void* mem = user ? MmAllocateUserImageForProcess(process, imageSize) : MmAllocatePool(imageSize); + pe->NumberOfSections); + + void* mem = MmAllocatePoolTag(imageSize, MM_POOL_TAG('i', 'm', 'g', ' ')); if (!mem) { - DbgPrint("LDR: image allocation failed required=%llx user=%d pid=%u\n", - (unsigned long long)imageSize, user ? 1 : 0, process ? process->ProcessId : 0); + DbgPrint("LDR: kernel image allocation failed required=%llx\n", + (unsigned long long)imageSize); return STATUS_NO_MEMORY; } memset(mem, 0, imageSize); @@ -730,27 +809,37 @@ static KSTATUS KiLdrLoadImageCommon(PROCESS* process, const void* buffer, size_t memcpy(dst, src, copySize); } - uint64_t prot = PAGE_PRESENT | PAGE_WRITE | (user ? PAGE_USER : 0); - MmSetPageProtect((uintptr_t)mem + sec[i].VirtualAddress, + MmPteProtectRange((uintptr_t)mem + sec[i].VirtualAddress, sec[i].VirtualSize ? sec[i].VirtualSize : sec[i].SizeOfRawData, - prot); + PAGE_PRESENT | PAGE_WRITE); } uintptr_t delta = (uintptr_t)mem - opt->ImageBase; if (delta != 0) { - LdrApplyRelocations((uintptr_t)mem, delta); + KSTATUS rs = LdrApplyRelocationsForProcess((PROCESS*)0, (uintptr_t)mem, delta, false); + if (!KSUCCESS(rs)) { + MmFreePool(mem); + return rs; + } + } + + // Kernel drivers are still pool-backed images. Section protections cannot + // safely make pages read-only yet because imports and driver-owned writable + // image data are not separated from callback code by a section object. What + // must be true is simpler: every page in the loaded image must be executable + // enough for driver callbacks, including section-alignment slack near the + // end of the image. + KSTATUS xs = MmPteProtectRange((uintptr_t)mem, imageSize, PAGE_PRESENT | PAGE_WRITE); + if (!KSUCCESS(xs)) { + MmFreePool(mem); + return xs; } *baseOut = (uintptr_t)mem; return STATUS_SUCCESS; } -// Implements the ldr Load Image For Process operation. -KSTATUS LdrLoadImageForProcess(PROCESS* process, const void* buffer, size_t size, uintptr_t* baseOut) { - return KiLdrLoadImageCommon(process, buffer, size, baseOut, true); -} - -// Implements the ldr Load Image operation. -KSTATUS LdrLoadImage(const void* buffer, size_t size, uintptr_t* baseOut, bool user) { - return KiLdrLoadImageCommon(user ? KeGetCurrentProcess() : (PROCESS*)0, buffer, size, baseOut, user); +// Loads a kernel image from a bootloader or driver raw buffer. +KSTATUS LdrLoadKernelImage(const void* buffer, size_t size, uintptr_t* baseOut) { + return KiLdrLoadKernelImageFromBuffer(buffer, size, baseOut); } diff --git a/code/kernel/ldr_section.c b/code/kernel/ldr_section.c new file mode 100644 index 0000000..7324bfb --- /dev/null +++ b/code/kernel/ldr_section.c @@ -0,0 +1,736 @@ +// Image section objects. This file owns parsed PE image identity, sparse prototype pages, and image-section caching. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PE_SIGNATURE 0x00004550UL +#define IMAGE_FILE_MACHINE_AMD64 0x8664 +#define IMAGE_FILE_MACHINE_I386 0x014c +#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b +#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b +#define IMAGE_SCN_CNT_CODE 0x00000020u +#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040u +#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080u +#define IMAGE_SCN_MEM_EXECUTE 0x20000000u +#define IMAGE_SCN_MEM_READ 0x40000000u +#define IMAGE_SCN_MEM_WRITE 0x80000000u + +#define LDR_IMAGE_SECTION_CACHE_MAX 32u + +#pragma pack(push, 1) +typedef struct LDR_OPT_HEADER64 { + uint16_t Magic; + uint8_t MajorLinkerVersion; + uint8_t MinorLinkerVersion; + uint32_t SizeOfCode; + uint32_t SizeOfInitializedData; + uint32_t SizeOfUninitializedData; + uint32_t AddressOfEntryPoint; + uint32_t BaseOfCode; + uint64_t ImageBase; + uint32_t SectionAlignment; + uint32_t FileAlignment; + uint16_t MajorOSVersion; + uint16_t MinorOSVersion; + uint16_t MajorImageVersion; + uint16_t MinorImageVersion; + uint16_t MajorSubsystemVersion; + uint16_t MinorSubsystemVersion; + uint32_t Win32VersionValue; + uint32_t SizeOfImage; + uint32_t SizeOfHeaders; + uint32_t CheckSum; + uint16_t Subsystem; + uint16_t DllCharacteristics; + uint64_t SizeOfStackReserve; + uint64_t SizeOfStackCommit; + uint64_t SizeOfHeapReserve; + uint64_t SizeOfHeapCommit; + uint32_t LoaderFlags; + uint32_t NumberOfRvaAndSizes; + struct { uint32_t VirtualAddress; uint32_t Size; } DataDirectory[16]; +} LDR_OPT_HEADER64; + +typedef struct LDR_PE_HEADER { + uint32_t Signature; + uint16_t Machine; + uint16_t NumberOfSections; + uint32_t TimeDateStamp; + uint32_t PointerToSymbolTable; + uint32_t NumberOfSymbols; + uint16_t SizeOfOptionalHeader; + uint16_t Characteristics; + LDR_OPT_HEADER64 OptionalHeader; +} LDR_PE_HEADER; + +typedef struct LDR_SECTION_HEADER { + char Name[8]; + uint32_t VirtualSize; + uint32_t VirtualAddress; + uint32_t SizeOfRawData; + uint32_t PointerToRawData; + uint32_t PointerToRelocations; + uint32_t PointerToLinenumbers; + uint16_t NumberOfRelocations; + uint16_t NumberOfLinenumbers; + uint32_t Characteristics; +} LDR_SECTION_HEADER; +#pragma pack(pop) + +struct IMAGE_SECTION_OBJECT { + OBJECT Header; + IMAGE_SECTION_METADATA Metadata; + IMAGE_SECTION_DESCRIPTOR* Sections; + PHYSICAL_ADDRESS* Frames; + FILE_OBJECT* BackingFile; + size_t HeadersFileSize; + SPINLOCK Lock; +}; + +static IMAGE_SECTION_OBJECT* gImageSectionCache[LDR_IMAGE_SECTION_CACHE_MAX]; +static SPINLOCK gImageSectionCacheLock = SPINLOCK_INIT; + +// Aligns a PE image size to the hardware page size. +static size_t KiImageSectionAlignSize(size_t value) { + return (value + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); +} + +// Mixes filesystem identity into a compact cache key. +static uint64_t KiImageSectionIdentityKey(const IO_FILE_IDENTITY* identity) { + if (!identity) + return 0; + + uint64_t h = 1469598103934665603ULL; + h ^= identity->VolumeId; + h *= 1099511628211ULL; + h ^= identity->FileId; + h *= 1099511628211ULL; + h ^= identity->Generation; + h *= 1099511628211ULL; + h ^= identity->Size; + h *= 1099511628211ULL; + return h ? h : 1; +} + +// Converts PE section characteristics to the final user page protection union for that section. +static uint64_t KiImageSectionProtection(uint32_t characteristics) { + uint64_t prot = PAGE_PRESENT | PAGE_USER | PAGE_NX; + if (characteristics & IMAGE_SCN_MEM_WRITE) + prot |= PAGE_WRITE; + if (characteristics & IMAGE_SCN_MEM_EXECUTE) + prot &= ~PAGE_NX; + return prot; +} + +// Computes the required image mapping size after validating raw section extents. +static KSTATUS KiImageSectionComputeSize(const LDR_PE_HEADER* pe, size_t fileSize, size_t* outSize) { + if (!pe || !outSize) return STATUS_INVALID_PARAMETER; + + const LDR_OPT_HEADER64* opt = &pe->OptionalHeader; + size_t required = KiImageSectionAlignSize(opt->SizeOfImage); + size_t headers = KiImageSectionAlignSize(opt->SizeOfHeaders); + if (headers > required) required = headers; + + const LDR_SECTION_HEADER* sec = (const LDR_SECTION_HEADER*)((const uint8_t*)&pe->OptionalHeader + pe->SizeOfOptionalHeader); + for (uint32_t i = 0; i < pe->NumberOfSections; i++) { + size_t va = (size_t)sec[i].VirtualAddress; + size_t virt = (size_t)sec[i].VirtualSize; + size_t raw = (size_t)sec[i].SizeOfRawData; + size_t span = virt ? virt : raw; + if (raw > span) span = raw; + if (!span) continue; + + if (va > (size_t)-1 - span) return STATUS_NO_MEMORY; + size_t end = KiImageSectionAlignSize(va + span); + if (end > required) required = end; + + if (raw) { + size_t rawBase = (size_t)sec[i].PointerToRawData; + if (rawBase > fileSize || raw > fileSize - rawBase) + return STATUS_FAILURE; + } + } + + if (!required) return STATUS_FAILURE; + *outSize = required; + return STATUS_SUCCESS; +} + +// Parses and validates the PE header that backs an image section object. +static KSTATUS KiImageSectionParseHeader(const void* buffer, + size_t bufferSize, + size_t fileSize, + const LDR_PE_HEADER** peOut, + const LDR_SECTION_HEADER** sectionsOut, + size_t* imageSizeOut) { + if (!buffer || !bufferSize || !peOut || !sectionsOut || !imageSizeOut) + return STATUS_INVALID_PARAMETER; + if (bufferSize < 0x40) return STATUS_FAILURE; + + const uint8_t* bytes = (const uint8_t*)buffer; + uint32_t peOffset = *(const uint32_t*)(bytes + 0x3C); + if ((size_t)peOffset > bufferSize || bufferSize - (size_t)peOffset < 24) + return STATUS_FAILURE; + + const LDR_PE_HEADER* pe = (const LDR_PE_HEADER*)(bytes + peOffset); + if (pe->Signature != PE_SIGNATURE) + return STATUS_FAILURE; + + uint16_t optionalSize = pe->SizeOfOptionalHeader; + if (optionalSize < sizeof(LDR_OPT_HEADER64) || + (size_t)peOffset + 24 + optionalSize > bufferSize) { + return STATUS_FAILURE; + } + + uint16_t optionalMagic = pe->OptionalHeader.Magic; + if (pe->Machine == IMAGE_FILE_MACHINE_I386 || optionalMagic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { + DbgPrint("LdrImageSection: 32-bit PE images are not supported (machine=0x%X magic=0x%X)\n", + pe->Machine, optionalMagic); + return STATUS_NOT_SUPPORTED; + } + + if (pe->Machine != IMAGE_FILE_MACHINE_AMD64 || optionalMagic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { + DbgPrint("LdrImageSection: unsupported PE image (machine=0x%X magic=0x%X)\n", + pe->Machine, optionalMagic); + return STATUS_NOT_SUPPORTED; + } + + if (!pe->NumberOfSections) + return STATUS_FAILURE; + + const LDR_SECTION_HEADER* sections = (const LDR_SECTION_HEADER*)((const uint8_t*)&pe->OptionalHeader + pe->SizeOfOptionalHeader); + size_t sectionTableOffset = (size_t)peOffset + 24u + (size_t)pe->SizeOfOptionalHeader; + size_t sectionTableBytes = (size_t)pe->NumberOfSections * sizeof(LDR_SECTION_HEADER); + if (sectionTableOffset > bufferSize || sectionTableBytes > bufferSize - sectionTableOffset) + return STATUS_FAILURE; + + size_t imageSize = 0; + KSTATUS s = KiImageSectionComputeSize(pe, fileSize, &imageSize); + if (!KSUCCESS(s)) return s; + + *peOut = pe; + *sectionsOut = sections; + *imageSizeOut = imageSize; + return STATUS_SUCCESS; +} + +// Releases all resources owned by an image section object. +static void KiDestroyImageSectionObject(OBJECT* object) { + IMAGE_SECTION_OBJECT* section = (IMAGE_SECTION_OBJECT*)object; + if (!section) return; + + if (section->Frames) { + for (size_t i = 0; i < section->Metadata.PageCount; ++i) { + if (section->Frames[i]) + (void)MmPfnFree(section->Frames[i]); + } + MmFreePool(section->Frames); + section->Frames = (PHYSICAL_ADDRESS*)0; + } + + if (section->Sections) { + MmFreePool(section->Sections); + section->Sections = (IMAGE_SECTION_DESCRIPTOR*)0; + } + + if (section->BackingFile) { + IoCloseFile(section->BackingFile); + section->BackingFile = (FILE_OBJECT*)0; + } +} + +// Finds a cached image section while the image-section cache lock is held. +static IMAGE_SECTION_OBJECT* KiFindImageSectionLocked(const IO_FILE_IDENTITY* identity, uint64_t key) { + if (!identity || !key) + return (IMAGE_SECTION_OBJECT*)0; + + for (uint32_t i = 0; i < LDR_IMAGE_SECTION_CACHE_MAX; ++i) { + IMAGE_SECTION_OBJECT* section = gImageSectionCache[i]; + if (section && + section->Metadata.ContentKey == key && + section->Metadata.VolumeId == identity->VolumeId && + section->Metadata.FileId == identity->FileId && + section->Metadata.FileGeneration == identity->Generation && + section->Metadata.RawSize == identity->Size) { + return section; + } + } + return (IMAGE_SECTION_OBJECT*)0; +} + + +// Verifies that a cached image section is still backed by the same file generation. +static KSTATUS KiValidateImageSectionBackingFile(IMAGE_SECTION_OBJECT* section) { + if (!section || !section->BackingFile) + return STATUS_INVALID_PARAMETER; + + IO_FILE_IDENTITY identity; + KSTATUS s = IoQueryFileIdentity(section->BackingFile, &identity); + if (!KSUCCESS(s)) + return s; + + if (identity.VolumeId != section->Metadata.VolumeId || + identity.FileId != section->Metadata.FileId || + identity.Generation != section->Metadata.FileGeneration || + identity.Size != section->Metadata.RawSize) { + return STATUS_CHECKSUM_MISMATCH; + } + + return STATUS_SUCCESS; +} + +// Returns true when a virtual image page has initialized bytes in the backing file. +static bool KiImageSectionPageHasFileBytes(const IMAGE_SECTION_OBJECT* section, size_t pageIndex) { + if (!section || pageIndex >= section->Metadata.PageCount) + return false; + + size_t pageStart = pageIndex * PAGE_SIZE; + size_t pageEnd = pageStart + PAGE_SIZE; + if (pageStart < section->HeadersFileSize && pageEnd > 0) + return true; + + for (uint32_t i = 0; i < section->Metadata.SectionCount; ++i) { + const IMAGE_SECTION_DESCRIPTOR* desc = §ion->Sections[i]; + size_t raw = desc->RawDataSize; + if (desc->VirtualSize && desc->VirtualSize < raw) + raw = desc->VirtualSize; + if (!raw) + continue; + + size_t start = desc->VirtualAddress; + size_t end = start + raw; + if (end < start) + continue; + if (pageStart < end && pageEnd > start) + return true; + } + + return false; +} + +// Copies a file range into an image prototype page, leaving absent bytes zero-filled. +static KSTATUS KiReadImageSectionFileBytes(IMAGE_SECTION_OBJECT* section, + uint64_t fileOffset, + void* dst, + size_t size) { + if (!section || !section->BackingFile || (!dst && size)) + return STATUS_INVALID_PARAMETER; + if (!size) + return STATUS_SUCCESS; + + size_t got = 0; + KSTATUS s = IoReadFileAt(section->BackingFile, fileOffset, dst, size, &got); + if (!KSUCCESS(s)) + return s; + if (got != size) + return STATUS_FAILURE; + return STATUS_SUCCESS; +} + +// Fills one prototype frame from the image backing file. +static KSTATUS KiFillImageSectionFrameFromFile(IMAGE_SECTION_OBJECT* section, + size_t pageIndex, + PHYSICAL_ADDRESS frame) { + if (!section || !frame || pageIndex >= section->Metadata.PageCount) + return STATUS_INVALID_PARAMETER; + + uint8_t* page = (uint8_t*)MmPfnToVirtual(frame); + if (!page) + return STATUS_FAILURE; + memset(page, 0, PAGE_SIZE); + + size_t pageStart = pageIndex * PAGE_SIZE; + size_t pageEnd = pageStart + PAGE_SIZE; + + if (pageStart < section->HeadersFileSize) { + size_t copyStart = pageStart; + size_t copyEnd = pageEnd; + if (copyEnd > section->HeadersFileSize) + copyEnd = section->HeadersFileSize; + KSTATUS s = KiReadImageSectionFileBytes(section, + (uint64_t)copyStart, + page, + copyEnd - copyStart); + if (!KSUCCESS(s)) + return s; + } + + for (uint32_t i = 0; i < section->Metadata.SectionCount; ++i) { + const IMAGE_SECTION_DESCRIPTOR* desc = §ion->Sections[i]; + size_t raw = desc->RawDataSize; + if (desc->VirtualSize && desc->VirtualSize < raw) + raw = desc->VirtualSize; + if (!raw) + continue; + + size_t sectionStart = desc->VirtualAddress; + size_t sectionEnd = sectionStart + raw; + if (sectionEnd < sectionStart) + return STATUS_FAILURE; + if (pageStart >= sectionEnd || pageEnd <= sectionStart) + continue; + + size_t copyStart = pageStart > sectionStart ? pageStart : sectionStart; + size_t copyEnd = pageEnd < sectionEnd ? pageEnd : sectionEnd; + size_t dstOffset = copyStart - pageStart; + uint64_t srcOffset = (uint64_t)desc->RawDataOffset + (uint64_t)(copyStart - sectionStart); + + KSTATUS s = KiReadImageSectionFileBytes(section, + srcOffset, + page + dstOffset, + copyEnd - copyStart); + if (!KSUCCESS(s)) + return s; + } + + return STATUS_SUCCESS; +} + +// Ensures a prototype page exists for initialized image bytes. +static KSTATUS KiEnsureImageSectionFrame(IMAGE_SECTION_OBJECT* section, size_t pageIndex) { + if (!section || pageIndex >= section->Metadata.PageCount) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(§ion->Lock); + PHYSICAL_ADDRESS existing = section->Frames[pageIndex]; + KeReleaseSpinlock(§ion->Lock, flags); + if (existing) + return STATUS_SUCCESS; + + if (section->BackingFile && !KiImageSectionPageHasFileBytes(section, pageIndex)) + return STATUS_SUCCESS; + + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseImage); + if (!frame) + return STATUS_NO_MEMORY; + + void* va = MmPfnToVirtual(frame); + if (!va) { + MmPfnFree(frame); + return STATUS_FAILURE; + } + + KSTATUS s = STATUS_SUCCESS; + if (section->BackingFile) { + s = KiValidateImageSectionBackingFile(section); + if (KSUCCESS(s)) + s = KiFillImageSectionFrameFromFile(section, pageIndex, frame); + } else { + memset(va, 0, PAGE_SIZE); + } + if (!KSUCCESS(s)) { + MmPfnFree(frame); + return s; + } + + flags = KeAcquireSpinlock(§ion->Lock); + if (section->Frames[pageIndex]) { + KeReleaseSpinlock(§ion->Lock, flags); + MmPfnFree(frame); + return STATUS_SUCCESS; + } + section->Frames[pageIndex] = frame; + KeReleaseSpinlock(§ion->Lock, flags); + return STATUS_SUCCESS; +} + +// Builds an uncached image section object backed directly by an open file. +static KSTATUS KiBuildImageSectionObjectFromFile(FILE_OBJECT* file, + const void* headerBuffer, + size_t headerBufferSize, + const IO_FILE_IDENTITY* identity, + uint64_t key, + IMAGE_SECTION_OBJECT** sectionOut) { + if (!file || !headerBuffer || !headerBufferSize || !identity || !sectionOut) + return STATUS_INVALID_PARAMETER; + *sectionOut = (IMAGE_SECTION_OBJECT*)0; + + size_t fileSize = (size_t)identity->Size; + const LDR_PE_HEADER* pe = (const LDR_PE_HEADER*)0; + const LDR_SECTION_HEADER* rawSections = (const LDR_SECTION_HEADER*)0; + size_t imageSize = 0; + KSTATUS s = KiImageSectionParseHeader(headerBuffer, + headerBufferSize, + fileSize, + &pe, + &rawSections, + &imageSize); + if (!KSUCCESS(s)) + return s; + + size_t pageCount = (imageSize + PAGE_SIZE - 1) / PAGE_SIZE; + if (!pageCount || pageCount > ((size_t)-1 / sizeof(PHYSICAL_ADDRESS))) + return STATUS_FAILURE; + + IMAGE_SECTION_OBJECT* section = (IMAGE_SECTION_OBJECT*)ObAllocateObject(ObjectTypeImageSection, + sizeof(IMAGE_SECTION_OBJECT)); + if (!section) + return STATUS_NO_MEMORY; + section->Header.Destroy = KiDestroyImageSectionObject; + + PHYSICAL_ADDRESS* frames = (PHYSICAL_ADDRESS*)MmAllocatePoolTag(pageCount * sizeof(PHYSICAL_ADDRESS), + MM_POOL_TAG('i', 's', 'f', 'r')); + if (!frames) { + ObDereferenceObject(§ion->Header); + return STATUS_NO_MEMORY; + } + memset(frames, 0, pageCount * sizeof(PHYSICAL_ADDRESS)); + + IMAGE_SECTION_DESCRIPTOR* descriptors = (IMAGE_SECTION_DESCRIPTOR*)MmAllocatePoolTag( + (size_t)pe->NumberOfSections * sizeof(IMAGE_SECTION_DESCRIPTOR), + MM_POOL_TAG('i', 's', 'e', 'c')); + if (!descriptors) { + MmFreePool(frames); + ObDereferenceObject(§ion->Header); + return STATUS_NO_MEMORY; + } + memset(descriptors, 0, (size_t)pe->NumberOfSections * sizeof(IMAGE_SECTION_DESCRIPTOR)); + + section->Frames = frames; + section->Sections = descriptors; + section->BackingFile = file; + section->HeadersFileSize = pe->OptionalHeader.SizeOfHeaders; + section->Lock = (SPINLOCK)SPINLOCK_INIT; + ObReferenceObject(&file->Header); + + section->Metadata.ContentKey = key; + section->Metadata.VolumeId = identity->VolumeId; + section->Metadata.FileId = identity->FileId; + section->Metadata.FileGeneration = identity->Generation; + section->Metadata.PreferredBase = pe->OptionalHeader.ImageBase; + section->Metadata.EntryRva = pe->OptionalHeader.AddressOfEntryPoint; + section->Metadata.HeaderSize = pe->OptionalHeader.SizeOfHeaders; + section->Metadata.RawSize = fileSize; + section->Metadata.ImageSize = imageSize; + section->Metadata.PageCount = pageCount; + section->Metadata.SectionCount = pe->NumberOfSections; + + for (uint32_t i = 0; i < pe->NumberOfSections; ++i) { + IMAGE_SECTION_DESCRIPTOR* dst = §ion->Sections[i]; + memcpy(dst->Name, rawSections[i].Name, sizeof(rawSections[i].Name)); + dst->Name[8] = '\0'; + dst->VirtualAddress = rawSections[i].VirtualAddress; + dst->VirtualSize = rawSections[i].VirtualSize; + dst->RawDataOffset = rawSections[i].PointerToRawData; + dst->RawDataSize = rawSections[i].SizeOfRawData; + dst->Characteristics = rawSections[i].Characteristics; + dst->PageProtection = KiImageSectionProtection(rawSections[i].Characteristics); + } + + *sectionOut = section; + return STATUS_SUCCESS; +} + +// Creates or references an image section object backed by an open file. +KSTATUS LdrCreateImageSectionFromFile(FILE_OBJECT* file, + IMAGE_SECTION_OBJECT** sectionOut) { + if (!file || !sectionOut) + return STATUS_INVALID_PARAMETER; + *sectionOut = (IMAGE_SECTION_OBJECT*)0; + + IO_FILE_IDENTITY identity; + KSTATUS s = IoQueryFileIdentity(file, &identity); + if (!KSUCCESS(s)) + return s; + + size_t fileSize = (size_t)identity.Size; + if (!fileSize) + return STATUS_FAILURE; + + size_t firstRead = fileSize < 4096u ? fileSize : 4096u; + uint8_t stackHeader[4096]; + size_t got = 0; + s = IoReadFileAt(file, 0, stackHeader, firstRead, &got); + if (!KSUCCESS(s) || got != firstRead) + return KSUCCESS(s) ? STATUS_FAILURE : s; + if (firstRead < 0x40) + return STATUS_FAILURE; + + uint32_t peOffset = *(const uint32_t*)(stackHeader + 0x3C); + if ((size_t)peOffset > fileSize || fileSize - (size_t)peOffset < 24) + return STATUS_FAILURE; + + size_t minimumHeader = (size_t)peOffset + 24u; + if (minimumHeader > firstRead) { + if (minimumHeader > 65536u || minimumHeader > fileSize) + return STATUS_FAILURE; + void* tmp = MmAllocatePoolTag(minimumHeader, MM_POOL_TAG('i', 's', 'h', 'd')); + if (!tmp) + return STATUS_NO_MEMORY; + s = IoReadFileAt(file, 0, tmp, minimumHeader, &got); + if (!KSUCCESS(s) || got != minimumHeader) { + MmFreePool(tmp); + return KSUCCESS(s) ? STATUS_FAILURE : s; + } + uint16_t optSize = *(const uint16_t*)((const uint8_t*)tmp + peOffset + 20u); + size_t fullHeader = (size_t)peOffset + 24u + (size_t)optSize; + MmFreePool(tmp); + if (fullHeader > firstRead) { + firstRead = fullHeader; + } + } else { + uint16_t optSize = *(const uint16_t*)(stackHeader + peOffset + 20u); + size_t fullHeader = (size_t)peOffset + 24u + (size_t)optSize; + if (fullHeader > firstRead) + firstRead = fullHeader; + } + + if (firstRead > fileSize || firstRead > 65536u) + return STATUS_FAILURE; + + void* header = stackHeader; + bool heapHeader = false; + if (firstRead > sizeof(stackHeader)) { + header = MmAllocatePoolTag(firstRead, MM_POOL_TAG('i', 's', 'h', 'd')); + if (!header) + return STATUS_NO_MEMORY; + heapHeader = true; + s = IoReadFileAt(file, 0, header, firstRead, &got); + if (!KSUCCESS(s) || got != firstRead) { + MmFreePool(header); + return KSUCCESS(s) ? STATUS_FAILURE : s; + } + } + + const LDR_PE_HEADER* pe = (const LDR_PE_HEADER*)((const uint8_t*)header + peOffset); + size_t tableBytes = (size_t)pe->NumberOfSections * sizeof(LDR_SECTION_HEADER); + size_t tableEnd = (size_t)peOffset + 24u + (size_t)pe->SizeOfOptionalHeader + tableBytes; + if (tableEnd > firstRead) { + if (tableEnd > fileSize || tableEnd > 65536u) { + if (heapHeader) MmFreePool(header); + return STATUS_FAILURE; + } + void* bigger = MmAllocatePoolTag(tableEnd, MM_POOL_TAG('i', 's', 'h', 'd')); + if (!bigger) { + if (heapHeader) MmFreePool(header); + return STATUS_NO_MEMORY; + } + s = IoReadFileAt(file, 0, bigger, tableEnd, &got); + if (heapHeader) MmFreePool(header); + if (!KSUCCESS(s) || got != tableEnd) { + MmFreePool(bigger); + return KSUCCESS(s) ? STATUS_FAILURE : s; + } + header = bigger; + firstRead = tableEnd; + heapHeader = true; + } + + uint64_t key = KiImageSectionIdentityKey(&identity); + + uintptr_t flags = KeAcquireSpinlock(&gImageSectionCacheLock); + IMAGE_SECTION_OBJECT* cached = KiFindImageSectionLocked(&identity, key); + if (cached) { + ObReferenceObject(&cached->Header); + KeReleaseSpinlock(&gImageSectionCacheLock, flags); + if (heapHeader) MmFreePool(header); + *sectionOut = cached; + return STATUS_SUCCESS; + } + KeReleaseSpinlock(&gImageSectionCacheLock, flags); + + IMAGE_SECTION_OBJECT* built = (IMAGE_SECTION_OBJECT*)0; + s = KiBuildImageSectionObjectFromFile(file, header, firstRead, &identity, key, &built); + if (heapHeader) MmFreePool(header); + if (!KSUCCESS(s)) + return s; + + flags = KeAcquireSpinlock(&gImageSectionCacheLock); + cached = KiFindImageSectionLocked(&identity, key); + if (cached) { + ObReferenceObject(&cached->Header); + KeReleaseSpinlock(&gImageSectionCacheLock, flags); + ObDereferenceObject(&built->Header); + *sectionOut = cached; + return STATUS_SUCCESS; + } + + for (uint32_t i = 0; i < LDR_IMAGE_SECTION_CACHE_MAX; ++i) { + if (!gImageSectionCache[i]) { + ObReferenceObject(&built->Header); + gImageSectionCache[i] = built; + break; + } + } + KeReleaseSpinlock(&gImageSectionCacheLock, flags); + + *sectionOut = built; + return STATUS_SUCCESS; +} + +// Adds a reference to an existing image section object. +void LdrReferenceImageSection(IMAGE_SECTION_OBJECT* section) { + if (!section) return; + ObReferenceObject(§ion->Header); +} + +// Releases a reference returned by LdrCreateImageSectionFromFile. +void LdrDereferenceImageSection(IMAGE_SECTION_OBJECT* section) { + if (!section) return; + ObDereferenceObject(§ion->Header); +} + +// Returns immutable metadata for an image section object. +const IMAGE_SECTION_METADATA* LdrImageSectionMetadata(const IMAGE_SECTION_OBJECT* section) { + if (!section) return (const IMAGE_SECTION_METADATA*)0; + return §ion->Metadata; +} + +// Returns the immutable PE section table owned by the image section object. +const IMAGE_SECTION_DESCRIPTOR* LdrImageSectionDescriptors(const IMAGE_SECTION_OBJECT* section, + uint32_t* countOut) { + if (countOut) *countOut = section ? section->Metadata.SectionCount : 0; + if (!section) return (const IMAGE_SECTION_DESCRIPTOR*)0; + return section->Sections; +} + +// Retains one prototype page frame owned by an image section object. +KSTATUS LdrRetainImageSectionPage(IMAGE_SECTION_OBJECT* section, + size_t pageIndex, + PHYSICAL_ADDRESS* frameOut) { + if (!section || !frameOut) return STATUS_INVALID_PARAMETER; + *frameOut = 0; + if (pageIndex >= section->Metadata.PageCount) + return STATUS_INVALID_PARAMETER; + + KSTATUS es = KiEnsureImageSectionFrame(section, pageIndex); + if (!KSUCCESS(es)) + return es; + + uintptr_t flags = KeAcquireSpinlock(§ion->Lock); + PHYSICAL_ADDRESS frame = section->Frames[pageIndex]; + KeReleaseSpinlock(§ion->Lock, flags); + if (!frame) + return STATUS_SUCCESS; + + KSTATUS s = MmPfnRetain(frame); + if (!KSUCCESS(s)) + return s; + + *frameOut = frame; + return STATUS_SUCCESS; +} + +// Maps an image section into a process as an image-backed VAD. +KSTATUS LdrMapImageSection(PROCESS* process, + IMAGE_SECTION_OBJECT* section, + void** baseOut) { + if (!process || !section || !baseOut) return STATUS_INVALID_PARAMETER; + *baseOut = (void*)0; + + return MmVadCreateImageSectionMapping(process, + section, + PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, + MM_VAD_ZERO | MM_VAD_GUARD_LOW | MM_VAD_GUARD_HIGH, + baseOut); +} diff --git a/code/kernel/mm_file_cache.c b/code/kernel/mm_file_cache.c new file mode 100644 index 0000000..dabc914 --- /dev/null +++ b/code/kernel/mm_file_cache.c @@ -0,0 +1,1273 @@ +// Coherent file page cache. +// +// The cache owns one resident page per file/offset. Reads, buffered writes, and +// mapped-file faults converge on this page so writeback has a single source of +// truth. Dirty pages are flushed explicitly, by close, or by the background +// writeback worker under pressure. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MM_FILE_CACHE_BUCKETS 64u +#define MM_FILE_CACHE_SOFT_MAX_PAGES 512u +#define MM_FILE_CACHE_READAHEAD_MAX_REQUESTS 64u +#define MM_FILE_CACHE_READAHEAD_MAX_PAGES 32u +#define MM_FILE_CACHE_READAHEAD_QUEUE_PAGE_BUDGET 128u +#define MM_FILE_CACHE_READAHEAD_MIN_HEADROOM_PAGES 16u +#define MM_FILE_CACHE_BATCH_MAX_PAGES 16u +#define MM_FILE_CACHE_PRESSURE_BATCH_HEADROOM_PAGES 64u +#define MM_FILE_CACHE_DIRTY_HIGH_WATER_PAGES 128u +#define MM_FILE_CACHE_DIRTY_HARD_LIMIT_PAGES 192u +#define MM_FILE_CACHE_WRITEBACK_BATCH_PAGES 16u + +typedef struct MM_FILE_CACHE_ENTRY { + struct MM_FILE_CACHE_ENTRY* Next; + FILE_OBJECT* File; + uint64_t Offset; + PHYSICAL_ADDRESS Frame; + uint32_t ValidBytes; + uint8_t Referenced; + uint8_t Standby; + uint8_t Loading; + uint8_t Invalidated; + uint8_t Dirty; + uint8_t Writeback; + uint64_t DirtySequence; +} MM_FILE_CACHE_ENTRY; + +typedef struct MM_FILE_CACHE_READAHEAD_REQUEST { + LIST_ENTRY Link; + FILE_OBJECT* File; + uint64_t Offset; + uint32_t Pages; +} MM_FILE_CACHE_READAHEAD_REQUEST; + +static SPINLOCK gFileCacheLock = SPINLOCK_INIT; +static MM_FILE_CACHE_ENTRY* gFileCacheBuckets[MM_FILE_CACHE_BUCKETS]; +static uint64_t gFileCacheResidentPages; +static uint64_t gFileCacheActivePages; +static uint64_t gFileCacheStandbyPages; +static uint64_t gFileCacheDirtyPages; +static uint64_t gFileCacheMaxDirtyPages; +static uint64_t gFileCacheMaxResidentPages; +static MM_FILE_CACHE_STATS gFileCacheStats; + +static SPINLOCK gReadAheadLock = SPINLOCK_INIT; +static LIST_ENTRY gReadAheadQueue; +static SEMAPHORE* gReadAheadSemaphore; +static uint32_t gReadAheadQueuedRequests; +static uint32_t gReadAheadQueuedPages; +static int gReadAheadQueueReady; +static int gReadAheadStartState; + +static SPINLOCK gWritebackLock = SPINLOCK_INIT; +static SEMAPHORE* gWritebackSemaphore; +static int gWritebackStartState; + +static uint32_t KiFileCacheHash(FILE_OBJECT* file, uint64_t offset) { + uintptr_t f = (uintptr_t)file >> 4; + uint64_t p = offset >> 12; + return (uint32_t)((f ^ p ^ (p >> 11)) & (MM_FILE_CACHE_BUCKETS - 1u)); +} + +static MM_FILE_CACHE_ENTRY* KiFileCacheFindLocked(FILE_OBJECT* file, uint64_t offset, uint32_t bucket) { + for (MM_FILE_CACHE_ENTRY* e = gFileCacheBuckets[bucket]; e; e = e->Next) { + if (e->File == file && e->Offset == offset) + return e; + } + return (MM_FILE_CACHE_ENTRY*)0; +} + +static void KiFileCacheBumpResidentLocked(int delta, int standby) { + if (delta > 0) { + gFileCacheResidentPages += (uint64_t)delta; + if (standby) gFileCacheStandbyPages += (uint64_t)delta; + else gFileCacheActivePages += (uint64_t)delta; + if (gFileCacheResidentPages > gFileCacheMaxResidentPages) + gFileCacheMaxResidentPages = gFileCacheResidentPages; + } else if (delta < 0) { + uint64_t dec = (uint64_t)(-delta); + gFileCacheResidentPages = gFileCacheResidentPages > dec ? gFileCacheResidentPages - dec : 0; + if (standby) gFileCacheStandbyPages = gFileCacheStandbyPages > dec ? gFileCacheStandbyPages - dec : 0; + else gFileCacheActivePages = gFileCacheActivePages > dec ? gFileCacheActivePages - dec : 0; + } +} + +// Updates dirty-page accounting while the cache lock is held. +static void KiFileCacheSetDirtyLocked(MM_FILE_CACHE_ENTRY* e, int dirty) { + if (!e) + return; + if (dirty) { + if (!e->Dirty) { + e->Dirty = 1; + gFileCacheDirtyPages++; + gFileCacheStats.DirtyPages = gFileCacheDirtyPages; + if (gFileCacheDirtyPages > gFileCacheMaxDirtyPages) { + gFileCacheMaxDirtyPages = gFileCacheDirtyPages; + gFileCacheStats.MaxDirtyPages = gFileCacheMaxDirtyPages; + } + } + e->DirtySequence++; + gFileCacheStats.DirtyMarks++; + } else { + if (e->Dirty) { + e->Dirty = 0; + if (gFileCacheDirtyPages) + gFileCacheDirtyPages--; + gFileCacheStats.DirtyPages = gFileCacheDirtyPages; + } + } +} + +static void KiFileCacheMoveEntryToStandbyLocked(MM_FILE_CACHE_ENTRY* e) { + if (!e || e->Standby) + return; + e->Standby = 1; + if (gFileCacheActivePages) gFileCacheActivePages--; + gFileCacheStandbyPages++; + gFileCacheStats.StandbyTransitions++; +} + +static void KiFileCacheActivateEntryLocked(MM_FILE_CACHE_ENTRY* e) { + if (!e || !e->Standby) + return; + e->Standby = 0; + if (gFileCacheStandbyPages) gFileCacheStandbyPages--; + gFileCacheActivePages++; + gFileCacheStats.StandbyHits++; +} + +static void KiFileCacheFreeEntry(MM_FILE_CACHE_ENTRY* e) { + if (!e) return; + if (e->File) ObDereferenceObject(&e->File->Header); + if (e->Frame) (void)MmPfnRelease(e->Frame); + MmFreePool(e); +} + +static void KiFileCacheRemoveEntryLocked(uint32_t bucket, MM_FILE_CACHE_ENTRY* prev, MM_FILE_CACHE_ENTRY* e) { + if (!e) return; + if (prev) prev->Next = e->Next; + else gFileCacheBuckets[bucket] = e->Next; + if (e->Frame) + KiFileCacheBumpResidentLocked(-1, e->Standby); + if (e->Dirty) + KiFileCacheSetDirtyLocked(e, 0); +} + +static int KiFileCacheIsReclaimCandidateLocked(MM_FILE_CACHE_ENTRY* e, FILE_OBJECT* skipFile, uint64_t skipOffset) { + if (!e) return 0; + if (skipFile && e->File == skipFile && e->Offset == skipOffset) + return 0; + if (e->Loading || e->Invalidated || !e->Frame || e->Dirty || e->Writeback) + return 0; + + MM_PFN* pfn = MmPfnGet(e->Frame); + return pfn && pfn->PinCount == 0 && pfn->MappedCount == 0 && pfn->RefCount == 1; +} + +static KSTATUS KiFileCacheDemoteOnePage(FILE_OBJECT* skipFile, uint64_t skipOffset) { + // A cold clean page first moves to PFN standby so the cache can reactivate it + // without disk I/O if it becomes hot again before final pressure reclaim. + for (int pass = 0; pass < 2; ++pass) { + for (uint32_t b = 0; b < MM_FILE_CACHE_BUCKETS; ++b) { + MM_FILE_CACHE_ENTRY* demote = (MM_FILE_CACHE_ENTRY*)0; + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + for (MM_FILE_CACHE_ENTRY* e = gFileCacheBuckets[b]; e; e = e->Next) { + if (e->Standby || !KiFileCacheIsReclaimCandidateLocked(e, skipFile, skipOffset)) + continue; + + if (e->Referenced && pass == 0) { + e->Referenced = 0; + continue; + } + + demote = e; + break; + } + + if (demote) { + KSTATUS s = MmPfnMoveToStandby(demote->Frame); + if (KSUCCESS(s)) { + KiFileCacheMoveEntryToStandbyLocked(demote); + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_SUCCESS; + } + } + KeReleaseSpinlock(&gFileCacheLock, flags); + } + } + return STATUS_NOT_FOUND; +} + +static KSTATUS KiFileCacheDropOneStandbyPage(FILE_OBJECT* skipFile, uint64_t skipOffset) { + for (uint32_t b = 0; b < MM_FILE_CACHE_BUCKETS; ++b) { + MM_FILE_CACHE_ENTRY* victim = (MM_FILE_CACHE_ENTRY*)0; + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + MM_FILE_CACHE_ENTRY* prev = (MM_FILE_CACHE_ENTRY*)0; + for (MM_FILE_CACHE_ENTRY* e = gFileCacheBuckets[b]; e; prev = e, e = e->Next) { + if (!e->Standby || !KiFileCacheIsReclaimCandidateLocked(e, skipFile, skipOffset)) + continue; + + victim = e; + KiFileCacheRemoveEntryLocked(b, prev, e); + gFileCacheStats.Drops++; + break; + } + KeReleaseSpinlock(&gFileCacheLock, flags); + if (victim) { + KiFileCacheFreeEntry(victim); + return STATUS_SUCCESS; + } + } + return STATUS_NOT_FOUND; +} + +static KSTATUS KiFileCacheReclaimOnePage(FILE_OBJECT* skipFile, uint64_t skipOffset) { + KSTATUS s = KiFileCacheDropOneStandbyPage(skipFile, skipOffset); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + + s = KiFileCacheDemoteOnePage(skipFile, skipOffset); + if (KSUCCESS(s)) { + s = KiFileCacheDropOneStandbyPage(skipFile, skipOffset); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + } + + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + gFileCacheStats.DropFailures++; + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_NOT_FOUND; +} + + +static void KiFileCacheAddReadAheadStat(uint64_t* field, uint64_t value) { + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + *field += value; + KeReleaseSpinlock(&gFileCacheLock, flags); +} + +// Wakes the writeback worker after dirty pressure crosses the high watermark. +static void KiFileCacheSignalWriteback(void) { + if (gWritebackSemaphore) + KeSignalSemaphore(gWritebackSemaphore); +} + +// Updates a maximum counter while the file-cache stats lock is held. +static void KiFileCacheMaxStatLocked(uint64_t* field, uint64_t value) { + if (*field < value) + *field = value; +} + +static int KiFileCachePageExistsLocked(FILE_OBJECT* file, uint64_t offset) { + uint32_t bucket = KiFileCacheHash(file, offset); + return KiFileCacheFindLocked(file, offset, bucket) != (MM_FILE_CACHE_ENTRY*)0; +} + +// Finds the lowest dirty page for a file, or globally when file is NULL. +static MM_FILE_CACHE_ENTRY* KiFileCacheFindLowestDirtyLocked(FILE_OBJECT* file) { + MM_FILE_CACHE_ENTRY* best = (MM_FILE_CACHE_ENTRY*)0; + for (uint32_t b = 0; b < MM_FILE_CACHE_BUCKETS; ++b) { + for (MM_FILE_CACHE_ENTRY* e = gFileCacheBuckets[b]; e; e = e->Next) { + if (!e->Dirty || e->Writeback || e->Loading || e->Invalidated || !e->Frame) + continue; + if (file && e->File != file) + continue; + if (!best || e->Offset < best->Offset) + best = e; + } + } + return best; +} + +static MM_FILE_CACHE_READAHEAD_REQUEST* KiFileCacheReadAheadDequeue(void) { + uintptr_t flags = KeAcquireSpinlock(&gReadAheadLock); + LIST_ENTRY* node = ListPopHead(&gReadAheadQueue); + if (node) { + MM_FILE_CACHE_READAHEAD_REQUEST* req = CONTAINING_RECORD(node, MM_FILE_CACHE_READAHEAD_REQUEST, Link); + if (gReadAheadQueuedRequests) + gReadAheadQueuedRequests--; + if (gReadAheadQueuedPages >= req->Pages) + gReadAheadQueuedPages -= req->Pages; + else + gReadAheadQueuedPages = 0; + } + KeReleaseSpinlock(&gReadAheadLock, flags); + + if (!node) + return (MM_FILE_CACHE_READAHEAD_REQUEST*)0; + return CONTAINING_RECORD(node, MM_FILE_CACHE_READAHEAD_REQUEST, Link); +} + +static void KiFileCacheReadAheadWorker(void* arg) { + (void)arg; + KeAssertIrql(PASSIVE_LEVEL); + + for (;;) { + KSTATUS ws = KeWaitSemaphore(gReadAheadSemaphore); + if (!KSUCCESS(ws)) { + KeYield(); + continue; + } + + MM_FILE_CACHE_READAHEAD_REQUEST* req = KiFileCacheReadAheadDequeue(); + if (!req) + continue; + + for (uint32_t i = 0; i < req->Pages; ++i) { + uint64_t offset = req->Offset + ((uint64_t)i * PAGE_SIZE); + + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + if (gFileCacheResidentPages + MM_FILE_CACHE_READAHEAD_MIN_HEADROOM_PAGES >= + MM_FILE_CACHE_SOFT_MAX_PAGES) { + gFileCacheStats.ReadAheadPagesThrottled += (uint64_t)(req->Pages - i); + KeReleaseSpinlock(&gFileCacheLock, flags); + break; + } + int exists = KiFileCachePageExistsLocked(req->File, offset); + if (exists) + gFileCacheStats.ReadAheadPagesSkipped++; + KeReleaseSpinlock(&gFileCacheLock, flags); + if (exists) + continue; + + MM_FILE_CACHE_PAGE page; + KSTATUS s = MmFileCacheGetPage(req->File, offset, &page); + flags = KeAcquireSpinlock(&gFileCacheLock); + if (KSUCCESS(s)) + gFileCacheStats.ReadAheadPagesLoaded++; + else + gFileCacheStats.ReadAheadFailures++; + KeReleaseSpinlock(&gFileCacheLock, flags); + + if (!KSUCCESS(s)) + break; + MmFileCacheReleasePage(&page); + } + + ObDereferenceObject(&req->File->Header); + MmFreePool(req); + } +} + +static KSTATUS KiFileCacheEnsureReadAheadStarted(void) { + uintptr_t flags = KeAcquireSpinlock(&gReadAheadLock); + if (gReadAheadStartState == 2) { + KeReleaseSpinlock(&gReadAheadLock, flags); + return STATUS_SUCCESS; + } + if (gReadAheadStartState == 1) { + KeReleaseSpinlock(&gReadAheadLock, flags); + while (gReadAheadStartState == 1) + KeYield(); + return gReadAheadStartState == 2 ? STATUS_SUCCESS : STATUS_FAILURE; + } + + gReadAheadStartState = 1; + KeReleaseSpinlock(&gReadAheadLock, flags); + + LIST_INIT(&gReadAheadQueue); + gReadAheadQueuedRequests = 0; + gReadAheadQueuedPages = 0; + gReadAheadQueueReady = 1; + + KSTATUS s = KeSemaphoreCreate(0, 0x7fffffff, &gReadAheadSemaphore); + if (KSUCCESS(s)) { + THREAD* worker = (THREAD*)0; + s = KeCreateKernelThread((uintptr_t)KiFileCacheReadAheadWorker, + 0, + PriorityNormal, + &worker); + (void)worker; + } + + flags = KeAcquireSpinlock(&gReadAheadLock); + gReadAheadStartState = KSUCCESS(s) ? 2 : 0; + if (!KSUCCESS(s)) + gReadAheadQueueReady = 0; + KeReleaseSpinlock(&gReadAheadLock, flags); + return s; +} + +// Flushes the next dirty cache page for file, or globally when file is NULL. +static KSTATUS KiFileCacheFlushOneDirtyPage(FILE_OBJECT* file, int* didWork) { + if (didWork) *didWork = 0; + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + MM_FILE_CACHE_ENTRY* e = KiFileCacheFindLowestDirtyLocked(file); + if (!e) { + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_NOT_FOUND; + } + + KSTATUS rs = MmPfnRetain(e->Frame); + if (!KSUCCESS(rs)) { + gFileCacheStats.WritebackFailures++; + KeReleaseSpinlock(&gFileCacheLock, flags); + return rs; + } + + FILE_OBJECT* target = e->File; + ObReferenceObject(&target->Header); + PHYSICAL_ADDRESS frame = e->Frame; + uint64_t offset = e->Offset; + uint32_t validBytes = e->ValidBytes ? e->ValidBytes : PAGE_SIZE; + uint64_t sequence = e->DirtySequence; + e->Writeback = 1; + gFileCacheStats.WritebackRequests++; + KeReleaseSpinlock(&gFileCacheLock, flags); + + void* page = MmPfnToVirtual(frame); + size_t written = 0; + KSTATUS s = STATUS_FAILURE; + if (page && target->Fs && target->Fs->Write) + s = target->Fs->Write(target->Fs, target->FileCtx, offset, page, validBytes, &written); + if (KSUCCESS(s) && written != validBytes) + s = STATUS_FAILURE; + + flags = KeAcquireSpinlock(&gFileCacheLock); + uint32_t bucket = KiFileCacheHash(target, offset); + MM_FILE_CACHE_ENTRY* cur = KiFileCacheFindLocked(target, offset, bucket); + if (cur && cur->Frame == frame) { + if (KSUCCESS(s) && cur->Dirty && cur->DirtySequence == sequence) { + KiFileCacheSetDirtyLocked(cur, 0); + gFileCacheStats.WritebackPages++; + gFileCacheStats.WritebackBytes += validBytes; + } else if (!KSUCCESS(s)) { + gFileCacheStats.WritebackFailures++; + } + cur->Writeback = 0; + } + KeReleaseSpinlock(&gFileCacheLock, flags); + + (void)MmPfnRelease(frame); + ObDereferenceObject(&target->Header); + if (didWork) *didWork = 1; + return s; +} + +// Background writeback worker drains dirty cache pressure best-effort. +static void KiFileCacheWritebackWorker(void* arg) { + (void)arg; + KeAssertIrql(PASSIVE_LEVEL); + + for (;;) { + KSTATUS ws = KeWaitSemaphore(gWritebackSemaphore); + if (!KSUCCESS(ws)) { + KeYield(); + continue; + } + + for (uint32_t i = 0; i < MM_FILE_CACHE_WRITEBACK_BATCH_PAGES; ++i) { + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + int overHigh = gFileCacheDirtyPages >= MM_FILE_CACHE_DIRTY_HIGH_WATER_PAGES; + KeReleaseSpinlock(&gFileCacheLock, flags); + if (!overHigh) + break; + + int didWork = 0; + KSTATUS s = KiFileCacheFlushOneDirtyPage((FILE_OBJECT*)0, &didWork); + if (!didWork || !KSUCCESS(s)) + break; + } + } +} + +// Starts the writeback worker once. +static KSTATUS KiFileCacheEnsureWritebackStarted(void) { + uintptr_t flags = KeAcquireSpinlock(&gWritebackLock); + if (gWritebackStartState == 2) { + KeReleaseSpinlock(&gWritebackLock, flags); + return STATUS_SUCCESS; + } + if (gWritebackStartState == 1) { + KeReleaseSpinlock(&gWritebackLock, flags); + while (gWritebackStartState == 1) + KeYield(); + return gWritebackStartState == 2 ? STATUS_SUCCESS : STATUS_FAILURE; + } + + gWritebackStartState = 1; + KeReleaseSpinlock(&gWritebackLock, flags); + + KSTATUS s = KeSemaphoreCreate(0, 0x7fffffff, &gWritebackSemaphore); + if (KSUCCESS(s)) { + THREAD* worker = (THREAD*)0; + s = KeCreateKernelThread((uintptr_t)KiFileCacheWritebackWorker, + 0, + PriorityNormal, + &worker); + (void)worker; + } + + flags = KeAcquireSpinlock(&gWritebackLock); + gWritebackStartState = KSUCCESS(s) ? 2 : 0; + KeReleaseSpinlock(&gWritebackLock, flags); + return s; +} + +static size_t KiMinSize(size_t a, size_t b) { return a < b ? a : b; } + +static KSTATUS KiFileCacheTryHit(FILE_OBJECT* file, uint64_t aligned, MM_FILE_CACHE_PAGE* outPage, int waitForLoading) { + if (outPage) + memset(outPage, 0, sizeof(*outPage)); + + uint32_t bucket = KiFileCacheHash(file, aligned); + int counted = 0; + for (;;) { + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + if (!counted) { + gFileCacheStats.Lookups++; + counted = 1; + } + + MM_FILE_CACHE_ENTRY* hit = KiFileCacheFindLocked(file, aligned, bucket); + if (!hit) { + gFileCacheStats.Misses++; + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_NOT_FOUND; + } + + if (hit->Loading) { + if (!waitForLoading) { + gFileCacheStats.Misses++; + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_PENDING; + } + + gFileCacheStats.InflightWaits++; + KeReleaseSpinlock(&gFileCacheLock, flags); + KeYield(); + continue; + } + + if (hit->Invalidated || !hit->Frame) { + gFileCacheStats.Misses++; + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_NOT_FOUND; + } + + if (hit->Standby) { + KSTATUS as = MmPfnActivate(hit->Frame); + if (KSUCCESS(as)) + KiFileCacheActivateEntryLocked(hit); + } + + KSTATUS rs = STATUS_SUCCESS; + if (outPage) + rs = MmPfnRetain(hit->Frame); + if (KSUCCESS(rs)) { + hit->Referenced = 1; + gFileCacheStats.Hits++; + if (outPage) { + outPage->File = file; + outPage->Offset = aligned; + outPage->Frame = hit->Frame; + outPage->ValidBytes = hit->ValidBytes; + } + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_SUCCESS; + } + + // A retained cache entry should never point at an invalid frame, but do not + // return corrupt data if the PFN layer rejects it. + gFileCacheStats.Misses++; + KeReleaseSpinlock(&gFileCacheLock, flags); + return rs; + } +} + +static void KiFileCacheKeepBounded(FILE_OBJECT* skipFile, uint64_t skipOffset) { + for (;;) { + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + int over = gFileCacheResidentPages > MM_FILE_CACHE_SOFT_MAX_PAGES; + KeReleaseSpinlock(&gFileCacheLock, flags); + if (!over) break; + if (!KSUCCESS(KiFileCacheReclaimOnePage(skipFile, skipOffset))) break; + } +} + +static MM_FILE_CACHE_ENTRY* KiFileCacheAllocateLoadingEntry(FILE_OBJECT* file, uint64_t aligned) { + MM_FILE_CACHE_ENTRY* entry = (MM_FILE_CACHE_ENTRY*)MmAllocatePoolTag(sizeof(MM_FILE_CACHE_ENTRY), MM_POOL_TAG('p', 'c', 'a', 'c')); + if (!entry) + return (MM_FILE_CACHE_ENTRY*)0; + + memset(entry, 0, sizeof(*entry)); + entry->File = file; + entry->Offset = aligned; + entry->Referenced = 1; + entry->Loading = 1; + ObReferenceObject(&file->Header); + return entry; +} + +static void KiFileCacheFreeLoadingEntry(MM_FILE_CACHE_ENTRY* entry) { + if (!entry) + return; + entry->Frame = 0; + entry->Loading = 0; + KiFileCacheFreeEntry(entry); +} + +static int KiFileCacheUnlinkEntryLocked(MM_FILE_CACHE_ENTRY* entry) { + if (!entry) + return 0; + + uint32_t bucket = KiFileCacheHash(entry->File, entry->Offset); + MM_FILE_CACHE_ENTRY* prev = (MM_FILE_CACHE_ENTRY*)0; + for (MM_FILE_CACHE_ENTRY* e = gFileCacheBuckets[bucket]; e; prev = e, e = e->Next) { + if (e == entry) { + KiFileCacheRemoveEntryLocked(bucket, prev, e); + return 1; + } + } + return 0; +} + +static KSTATUS KiFileCacheReserveLoadingPage(FILE_OBJECT* file, uint64_t aligned, MM_FILE_CACHE_ENTRY** outEntry) { + if (outEntry) + *outEntry = (MM_FILE_CACHE_ENTRY*)0; + + MM_FILE_CACHE_ENTRY* entry = KiFileCacheAllocateLoadingEntry(file, aligned); + if (!entry) + return STATUS_NO_MEMORY; + + uint32_t bucket = KiFileCacheHash(file, aligned); + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + if (KiFileCacheFindLocked(file, aligned, bucket)) { + KeReleaseSpinlock(&gFileCacheLock, flags); + KiFileCacheFreeLoadingEntry(entry); + return STATUS_ALREADY_EXISTS; + } + + entry->Next = gFileCacheBuckets[bucket]; + gFileCacheBuckets[bucket] = entry; + KeReleaseSpinlock(&gFileCacheLock, flags); + + if (outEntry) + *outEntry = entry; + return STATUS_SUCCESS; +} + +static void KiFileCacheCancelLoadingPage(MM_FILE_CACHE_ENTRY* entry) { + if (!entry) + return; + + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + int unlinked = KiFileCacheUnlinkEntryLocked(entry); + KeReleaseSpinlock(&gFileCacheLock, flags); + + if (unlinked) + KiFileCacheFreeLoadingEntry(entry); +} + +static KSTATUS KiFileCacheCompleteLoadingPage(MM_FILE_CACHE_ENTRY* entry, + PHYSICAL_ADDRESS frame, + uint32_t validBytes, + MM_FILE_CACHE_PAGE* requestedPage, + uint64_t* publishedCount) { + if (!entry || !frame) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + if (entry->Invalidated) { + (void)KiFileCacheUnlinkEntryLocked(entry); + KeReleaseSpinlock(&gFileCacheLock, flags); + (void)MmPfnRelease(frame); + KiFileCacheFreeLoadingEntry(entry); + return STATUS_NOT_FOUND; + } + + entry->Frame = frame; + entry->ValidBytes = validBytes; + entry->Referenced = 1; + entry->Standby = 0; + entry->Loading = 0; + gFileCacheStats.Loads++; + KiFileCacheBumpResidentLocked(1, 0); + if (publishedCount) + (*publishedCount)++; + + if (requestedPage) { + KSTATUS rs = MmPfnRetain(frame); + if (!KSUCCESS(rs)) { + (void)KiFileCacheUnlinkEntryLocked(entry); + gFileCacheStats.LoadFailures++; + KeReleaseSpinlock(&gFileCacheLock, flags); + KiFileCacheFreeEntry(entry); + return rs; + } + requestedPage->File = entry->File; + requestedPage->Offset = entry->Offset; + requestedPage->Frame = frame; + requestedPage->ValidBytes = validBytes; + } + + KeReleaseSpinlock(&gFileCacheLock, flags); + return STATUS_SUCCESS; +} + + +static uint32_t KiFileCacheChooseForegroundBatchPages(void) { + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + uint64_t resident = gFileCacheResidentPages; + KeReleaseSpinlock(&gFileCacheLock, flags); + + if (resident >= MM_FILE_CACHE_SOFT_MAX_PAGES) + return 1; + + uint64_t headroom = MM_FILE_CACHE_SOFT_MAX_PAGES - resident; + if (headroom <= MM_FILE_CACHE_READAHEAD_MIN_HEADROOM_PAGES) + return 1; + if (headroom <= MM_FILE_CACHE_PRESSURE_BATCH_HEADROOM_PAGES) + return 2; + if (headroom <= (MM_FILE_CACHE_PRESSURE_BATCH_HEADROOM_PAGES * 2u)) + return 4; + if (headroom <= (MM_FILE_CACHE_PRESSURE_BATCH_HEADROOM_PAGES * 4u)) + return 8; + return MM_FILE_CACHE_BATCH_MAX_PAGES; +} + +static KSTATUS KiFileCacheLoadSpan(FILE_OBJECT* file, + uint64_t aligned, + uint32_t maxPages, + MM_FILE_CACHE_PAGE* requestedPage, + uint32_t* loadedPagesOut) { + if (loadedPagesOut) + *loadedPagesOut = 0; + if (!file || maxPages == 0) + return STATUS_INVALID_PARAMETER; + if (maxPages > MM_FILE_CACHE_BATCH_MAX_PAGES) + maxPages = MM_FILE_CACHE_BATCH_MAX_PAGES; + + uintptr_t statFlags = KeAcquireSpinlock(&gFileCacheLock); + gFileCacheStats.BatchPagesRequested += maxPages; + KiFileCacheMaxStatLocked(&gFileCacheStats.MaxBatchPagesRequested, maxPages); + KeReleaseSpinlock(&gFileCacheLock, statFlags); + + uintptr_t fileFlags = KeAcquireSpinlock(&file->Lock); + uint64_t fileSize = file->Size; + KeReleaseSpinlock(&file->Lock, fileFlags); + if (aligned >= fileSize) + return STATUS_SUCCESS; + + MM_FILE_CACHE_ENTRY* entries[MM_FILE_CACHE_BATCH_MAX_PAGES]; + PHYSICAL_ADDRESS frames[MM_FILE_CACHE_BATCH_MAX_PAGES]; + memset(entries, 0, sizeof(entries)); + memset(frames, 0, sizeof(frames)); + + uint32_t pages = 0; + KSTATUS reserveStatus = STATUS_SUCCESS; + while (pages < maxPages) { + uint64_t pageOffset = aligned + ((uint64_t)pages * PAGE_SIZE); + if (pageOffset >= fileSize) + break; + + reserveStatus = KiFileCacheReserveLoadingPage(file, pageOffset, &entries[pages]); + if (!KSUCCESS(reserveStatus)) + break; + pages++; + } + + if (pages == 0) { + if (requestedPage) + return KiFileCacheTryHit(file, aligned, requestedPage, 1); + return reserveStatus == STATUS_ALREADY_EXISTS ? STATUS_ALREADY_EXISTS : reserveStatus; + } + + uint8_t* readBuffer = (uint8_t*)MmAllocatePoolTag((size_t)pages * PAGE_SIZE, MM_POOL_TAG('f', 'c', 'b', 'l')); + if (!readBuffer) { + for (uint32_t i = 0; i < pages; ++i) + KiFileCacheCancelLoadingPage(entries[i]); + KiFileCacheAddReadAheadStat(&gFileCacheStats.LoadFailures, 1); + return STATUS_NO_MEMORY; + } + memset(readBuffer, 0, (size_t)pages * PAGE_SIZE); + + for (uint32_t i = 0; i < pages; ++i) { + frames[i] = MmPfnAllocate(MmPfnUseCleanFileCache); + if (!frames[i]) { + for (uint32_t j = 0; j < i; ++j) + (void)MmPfnRelease(frames[j]); + for (uint32_t j = 0; j < pages; ++j) + KiFileCacheCancelLoadingPage(entries[j]); + MmFreePool(readBuffer); + KiFileCacheAddReadAheadStat(&gFileCacheStats.LoadFailures, 1); + return STATUS_NO_MEMORY; + } + } + + size_t toRead = KiMinSize((size_t)pages * PAGE_SIZE, (size_t)(fileSize - aligned)); + size_t got = 0; + KSTATUS s = STATUS_NOT_SUPPORTED; + if (file->Fs && file->Fs->Read) + s = file->Fs->Read(file->Fs, file->FileCtx, aligned, readBuffer, toRead, &got); + if (!KSUCCESS(s)) { + for (uint32_t i = 0; i < pages; ++i) { + if (frames[i]) + (void)MmPfnRelease(frames[i]); + KiFileCacheCancelLoadingPage(entries[i]); + } + MmFreePool(readBuffer); + KiFileCacheAddReadAheadStat(&gFileCacheStats.LoadFailures, 1); + return s; + } + if (got > toRead) + got = toRead; + + uint64_t published = 0; + KSTATUS requestedStatus = STATUS_NOT_FOUND; + for (uint32_t i = 0; i < pages; ++i) { + uint64_t pageOffset = aligned + ((uint64_t)i * PAGE_SIZE); + size_t pageBase = (size_t)i * PAGE_SIZE; + if (pageBase >= got && pageOffset >= fileSize) { + (void)MmPfnRelease(frames[i]); + frames[i] = 0; + KiFileCacheCancelLoadingPage(entries[i]); + continue; + } + + uint32_t validBytes = 0; + if (pageBase < got) { + size_t avail = got - pageBase; + if (avail > PAGE_SIZE) avail = PAGE_SIZE; + validBytes = (uint32_t)avail; + } + + void* page = MmPfnToVirtual(frames[i]); + if (!page) { + (void)MmPfnRelease(frames[i]); + frames[i] = 0; + KiFileCacheCancelLoadingPage(entries[i]); + continue; + } + memcpy(page, readBuffer + pageBase, validBytes); + if (validBytes < PAGE_SIZE) + memset((uint8_t*)page + validBytes, 0, PAGE_SIZE - validBytes); + + MM_FILE_CACHE_PAGE* out = (pageOffset == aligned) ? requestedPage : (MM_FILE_CACHE_PAGE*)0; + KSTATUS ps = KiFileCacheCompleteLoadingPage(entries[i], frames[i], validBytes, out, &published); + if (KSUCCESS(ps)) { + if (pageOffset == aligned) + requestedStatus = STATUS_SUCCESS; + frames[i] = 0; + } else { + if (pageOffset == aligned) + requestedStatus = ps; + frames[i] = 0; + } + } + + MmFreePool(readBuffer); + + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + if (published) { + gFileCacheStats.BatchReads++; + gFileCacheStats.BatchPagesLoaded += published; + KiFileCacheMaxStatLocked(&gFileCacheStats.MaxBatchPagesLoaded, published); + } + KeReleaseSpinlock(&gFileCacheLock, flags); + + if (loadedPagesOut) + *loadedPagesOut = (uint32_t)published; + + KiFileCacheKeepBounded(file, aligned); + + if (requestedPage) { + if (KSUCCESS(requestedStatus)) + return STATUS_SUCCESS; + return KiFileCacheTryHit(file, aligned, requestedPage, 1); + } + return published ? STATUS_SUCCESS : STATUS_NOT_FOUND; +} + +// Returns a retained cache page suitable for in-place buffered writes. +static KSTATUS KiFileCacheGetWritablePage(FILE_OBJECT* file, + uint64_t aligned, + uint64_t oldFileSize, + int needExistingBytes, + MM_FILE_CACHE_PAGE* outPage) { + if (!file || !outPage) + return STATUS_INVALID_PARAMETER; + memset(outPage, 0, sizeof(*outPage)); + + KSTATUS s = KiFileCacheTryHit(file, aligned, outPage, 1); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + + if (needExistingBytes && aligned < oldFileSize) { + s = KiFileCacheLoadSpan(file, aligned, 1, outPage, (uint32_t*)0); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + if (s != STATUS_NOT_FOUND) + return s; + } + + MM_FILE_CACHE_ENTRY* entry = (MM_FILE_CACHE_ENTRY*)0; + s = KiFileCacheReserveLoadingPage(file, aligned, &entry); + if (s == STATUS_ALREADY_EXISTS) + return KiFileCacheTryHit(file, aligned, outPage, 1); + if (!KSUCCESS(s)) + return s; + + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseCleanFileCache); + if (!frame) { + KiFileCacheCancelLoadingPage(entry); + return STATUS_NO_MEMORY; + } + + void* page = MmPfnToVirtual(frame); + if (!page) { + (void)MmPfnRelease(frame); + KiFileCacheCancelLoadingPage(entry); + return STATUS_FAILURE; + } + memset(page, 0, PAGE_SIZE); + + uint32_t validBytes = PAGE_SIZE; + if (oldFileSize > aligned && oldFileSize - aligned < PAGE_SIZE) + validBytes = (uint32_t)(oldFileSize - aligned); + + s = KiFileCacheCompleteLoadingPage(entry, frame, validBytes, outPage, (uint64_t*)0); + if (!KSUCCESS(s)) + return s; + return STATUS_SUCCESS; +} + +// Marks one cache page dirty and updates its valid byte range. +static void KiFileCacheMarkPageDirty(FILE_OBJECT* file, uint64_t aligned, uint32_t validBytes) { + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + uint32_t bucket = KiFileCacheHash(file, aligned); + MM_FILE_CACHE_ENTRY* e = KiFileCacheFindLocked(file, aligned, bucket); + if (e) { + if (validBytes > e->ValidBytes) + e->ValidBytes = validBytes; + if (e->Standby) + KiFileCacheActivateEntryLocked(e); + KiFileCacheSetDirtyLocked(e, 1); + } + uint64_t dirty = gFileCacheDirtyPages; + KeReleaseSpinlock(&gFileCacheLock, flags); + + if (dirty >= MM_FILE_CACHE_DIRTY_HIGH_WATER_PAGES) { + if (KSUCCESS(KiFileCacheEnsureWritebackStarted())) + KiFileCacheSignalWriteback(); + } +} + + +KSTATUS MmFileCacheGetWritablePage(FILE_OBJECT* file, uint64_t offset, + uint64_t fileSizeBefore, int needExistingBytes, + MM_FILE_CACHE_PAGE* outPage) { + if (!outPage) + return STATUS_INVALID_PARAMETER; + uint64_t aligned = offset & ~(uint64_t)(PAGE_SIZE - 1); + return KiFileCacheGetWritablePage(file, aligned, fileSizeBefore, needExistingBytes, outPage); +} + +void MmFileCacheMarkDirty(FILE_OBJECT* file, uint64_t offset, uint32_t validBytes) { + uint64_t aligned = offset & ~(uint64_t)(PAGE_SIZE - 1); + if (validBytes > PAGE_SIZE) + validBytes = PAGE_SIZE; + if (validBytes == 0) + validBytes = PAGE_SIZE; + KiFileCacheMarkPageDirty(file, aligned, validBytes); + + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + gFileCacheStats.MappedDirtySyncs++; + gFileCacheStats.MappedDirtyPages++; + KeReleaseSpinlock(&gFileCacheLock, flags); +} + +KSTATUS MmFileCacheWrite(FILE_OBJECT* file, uint64_t offset, const void* buf, + size_t size, uint64_t fileSizeBefore, size_t* writtenOut) { + if (writtenOut) *writtenOut = 0; + if (!file || (!buf && size) || !writtenOut) + return STATUS_INVALID_PARAMETER; + if (!KeCanWait()) + return STATUS_INVALID_IRQL; + if (!file->Fs || !file->Fs->Write) + return STATUS_NOT_SUPPORTED; + if (size == 0) + return STATUS_SUCCESS; + + size_t total = 0; + const uint8_t* src = (const uint8_t*)buf; + while (total < size) { + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + uint64_t dirty = gFileCacheDirtyPages; + KeReleaseSpinlock(&gFileCacheLock, flags); + if (dirty >= MM_FILE_CACHE_DIRTY_HARD_LIMIT_PAGES) { + KSTATUS fs = MmFileCacheFlushFile(file); + if (!KSUCCESS(fs) && total == 0) + return fs; + } + + uint64_t absolute = offset + total; + if (absolute < offset) + break; + uint64_t aligned = absolute & ~(uint64_t)(PAGE_SIZE - 1); + size_t inPage = (size_t)(absolute - aligned); + size_t chunk = PAGE_SIZE - inPage; + if (chunk > size - total) + chunk = size - total; + + int coversFullPage = (inPage == 0 && chunk == PAGE_SIZE); + int needExisting = !coversFullPage; + MM_FILE_CACHE_PAGE page; + KSTATUS s = KiFileCacheGetWritablePage(file, aligned, fileSizeBefore, needExisting, &page); + if (!KSUCCESS(s)) + return total ? STATUS_SUCCESS : s; + + void* dst = MmPfnToVirtual(page.Frame); + if (!dst) { + MmFileCacheReleasePage(&page); + return total ? STATUS_SUCCESS : STATUS_FAILURE; + } + + memcpy((uint8_t*)dst + inPage, src + total, chunk); + uint32_t valid = (uint32_t)(inPage + chunk); + uint64_t pageEnd = aligned + valid; + uint64_t newEnd = offset + total + chunk; + if (pageEnd > newEnd) + valid = (uint32_t)(newEnd - aligned); + KiFileCacheMarkPageDirty(file, aligned, valid); + MmFileCacheReleasePage(&page); + + total += chunk; + } + + *writtenOut = total; + return STATUS_SUCCESS; +} + +KSTATUS MmFileCacheFlushFile(FILE_OBJECT* file) { + if (!file) + return STATUS_INVALID_PARAMETER; + if (!KeCanWait()) + return STATUS_INVALID_IRQL; + + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + gFileCacheStats.FlushFileCalls++; + KeReleaseSpinlock(&gFileCacheLock, flags); + + KSTATUS firstError = STATUS_SUCCESS; + for (;;) { + int didWork = 0; + KSTATUS s = KiFileCacheFlushOneDirtyPage(file, &didWork); + if (!didWork) + break; + if (!KSUCCESS(s) && KSUCCESS(firstError)) + firstError = s; + } + + if (!KSUCCESS(firstError)) { + flags = KeAcquireSpinlock(&gFileCacheLock); + gFileCacheStats.FlushFileFailures++; + KeReleaseSpinlock(&gFileCacheLock, flags); + } + return firstError; +} + +KSTATUS MmFileCacheGetPage(FILE_OBJECT* file, uint64_t offset, MM_FILE_CACHE_PAGE* outPage) { + if (!outPage) return STATUS_INVALID_PARAMETER; + memset(outPage, 0, sizeof(*outPage)); + if (!file) return STATUS_INVALID_PARAMETER; + if (!KeCanWait()) return STATUS_INVALID_IRQL; + + uint64_t aligned = offset & ~(uint64_t)(PAGE_SIZE - 1); + KSTATUS s = KiFileCacheTryHit(file, aligned, outPage, 1); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + return KiFileCacheLoadSpan(file, aligned, KiFileCacheChooseForegroundBatchPages(), outPage, (uint32_t*)0); +} + +void MmFileCacheReleasePage(MM_FILE_CACHE_PAGE* page) { + if (!page || !page->Frame) return; + (void)MmPfnRelease(page->Frame); + memset(page, 0, sizeof(*page)); +} + +void MmFileCacheInvalidate(FILE_OBJECT* file, uint64_t offset, size_t size) { + if (!file) return; + if (size == 0) return; + uint64_t start = offset & ~(uint64_t)(PAGE_SIZE - 1); + uint64_t end = offset + size; + if (end < offset || size == (size_t)-1) { + end = UINT64_MAX; + } else { + end = (end + PAGE_SIZE - 1) & ~(uint64_t)(PAGE_SIZE - 1); + } + + for (uint32_t b = 0; b < MM_FILE_CACHE_BUCKETS; ++b) { + for (;;) { + MM_FILE_CACHE_ENTRY* victim = (MM_FILE_CACHE_ENTRY*)0; + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + MM_FILE_CACHE_ENTRY* prev = (MM_FILE_CACHE_ENTRY*)0; + for (MM_FILE_CACHE_ENTRY* e = gFileCacheBuckets[b]; e; prev = e, e = e->Next) { + uint64_t pageEnd = e->Offset + PAGE_SIZE; + if (e->File == file && e->Offset < end && pageEnd > start) { + if (e->Loading) { + if (!e->Invalidated) { + e->Invalidated = 1; + gFileCacheStats.Invalidations++; + } + continue; + } + if (e->Dirty || e->Writeback) { + e->Invalidated = 1; + continue; + } + victim = e; + KiFileCacheRemoveEntryLocked(b, prev, e); + gFileCacheStats.Invalidations++; + break; + } + } + KeReleaseSpinlock(&gFileCacheLock, flags); + if (!victim) break; + KiFileCacheFreeEntry(victim); + } + } +} + +void MmFileCacheInvalidateAll(FILE_OBJECT* file) { + if (!file) return; + for (uint32_t b = 0; b < MM_FILE_CACHE_BUCKETS; ++b) { + for (;;) { + MM_FILE_CACHE_ENTRY* victim = (MM_FILE_CACHE_ENTRY*)0; + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + MM_FILE_CACHE_ENTRY* prev = (MM_FILE_CACHE_ENTRY*)0; + for (MM_FILE_CACHE_ENTRY* e = gFileCacheBuckets[b]; e; prev = e, e = e->Next) { + if (e->File == file) { + if (e->Loading) { + if (!e->Invalidated) { + e->Invalidated = 1; + gFileCacheStats.Invalidations++; + } + continue; + } + if (e->Dirty || e->Writeback) { + e->Invalidated = 1; + continue; + } + victim = e; + KiFileCacheRemoveEntryLocked(b, prev, e); + gFileCacheStats.Invalidations++; + break; + } + } + KeReleaseSpinlock(&gFileCacheLock, flags); + if (!victim) break; + KiFileCacheFreeEntry(victim); + } + } +} + +KSTATUS MmFileCacheDropOnePageForPressure(void) { + return KiFileCacheReclaimOnePage((FILE_OBJECT*)0, 0); +} + + +KSTATUS MmFileCacheScheduleReadAhead(FILE_OBJECT* file, uint64_t offset, uint32_t pages) { + if (!file || pages == 0) + return STATUS_INVALID_PARAMETER; + if (!KeCanWait()) + return STATUS_INVALID_IRQL; + + uint32_t requestedPages = pages; + if (pages > MM_FILE_CACHE_READAHEAD_MAX_PAGES) + pages = MM_FILE_CACHE_READAHEAD_MAX_PAGES; + + uintptr_t cacheFlags = KeAcquireSpinlock(&gFileCacheLock); + gFileCacheStats.ReadAheadPagesRequested += requestedPages; + KiFileCacheMaxStatLocked(&gFileCacheStats.MaxReadAheadPagesPerRequest, requestedPages); + uint64_t resident = gFileCacheResidentPages; + uint64_t headroom = resident < MM_FILE_CACHE_SOFT_MAX_PAGES + ? MM_FILE_CACHE_SOFT_MAX_PAGES - resident + : 0; + if (headroom <= MM_FILE_CACHE_READAHEAD_MIN_HEADROOM_PAGES) { + gFileCacheStats.ReadAheadPagesThrottled += pages; + KeReleaseSpinlock(&gFileCacheLock, cacheFlags); + return STATUS_QUEUE_FULL; + } + headroom -= MM_FILE_CACHE_READAHEAD_MIN_HEADROOM_PAGES; + if ((uint64_t)pages > headroom) + pages = (uint32_t)headroom; + KeReleaseSpinlock(&gFileCacheLock, cacheFlags); + if (pages == 0) + return STATUS_QUEUE_FULL; + + KSTATUS s = KiFileCacheEnsureReadAheadStarted(); + if (!KSUCCESS(s)) { + KiFileCacheAddReadAheadStat(&gFileCacheStats.ReadAheadFailures, 1); + return s; + } + + MM_FILE_CACHE_READAHEAD_REQUEST* req = + (MM_FILE_CACHE_READAHEAD_REQUEST*)MmAllocatePoolTag(sizeof(*req), MM_POOL_TAG('r', 'd', 'a', 'h')); + if (!req) { + KiFileCacheAddReadAheadStat(&gFileCacheStats.ReadAheadFailures, 1); + return STATUS_NO_MEMORY; + } + + LIST_INIT(&req->Link); + req->File = file; + req->Offset = offset & ~(uint64_t)(PAGE_SIZE - 1); + req->Pages = pages; + ObReferenceObject(&file->Header); + + uintptr_t flags = KeAcquireSpinlock(&gReadAheadLock); + if (!gReadAheadQueueReady || + gReadAheadQueuedRequests >= MM_FILE_CACHE_READAHEAD_MAX_REQUESTS || + gReadAheadQueuedPages >= MM_FILE_CACHE_READAHEAD_QUEUE_PAGE_BUDGET) { + KeReleaseSpinlock(&gReadAheadLock, flags); + ObDereferenceObject(&file->Header); + MmFreePool(req); + KiFileCacheAddReadAheadStat(&gFileCacheStats.ReadAheadPagesThrottled, pages); + return STATUS_QUEUE_FULL; + } + uint32_t queueHeadroom = MM_FILE_CACHE_READAHEAD_QUEUE_PAGE_BUDGET - gReadAheadQueuedPages; + if (pages > queueHeadroom) + pages = queueHeadroom; + req->Pages = pages; + + ListInsertTail(&gReadAheadQueue, &req->Link); + gReadAheadQueuedRequests++; + gReadAheadQueuedPages += pages; + KeReleaseSpinlock(&gReadAheadLock, flags); + + flags = KeAcquireSpinlock(&gFileCacheLock); + gFileCacheStats.ReadAheadRequests++; + gFileCacheStats.ReadAheadPagesQueued += pages; + KeReleaseSpinlock(&gFileCacheLock, flags); + + KeSignalSemaphore(gReadAheadSemaphore); + return STATUS_SUCCESS; +} + +void MmFileCacheQueryStats(MM_FILE_CACHE_STATS* out) { + if (!out) return; + uintptr_t flags = KeAcquireSpinlock(&gFileCacheLock); + MM_FILE_CACHE_STATS st = gFileCacheStats; + st.ResidentPages = gFileCacheResidentPages; + st.ActivePages = gFileCacheActivePages; + st.StandbyPages = gFileCacheStandbyPages; + st.DirtyPages = gFileCacheDirtyPages; + st.MaxDirtyPages = gFileCacheMaxDirtyPages; + st.MaxResidentPages = gFileCacheMaxResidentPages; + KeReleaseSpinlock(&gFileCacheLock, flags); + *out = st; +} diff --git a/code/kernel/mm_phys.c b/code/kernel/mm_phys.c index cf60293..5bdf0da 100644 --- a/code/kernel/mm_phys.c +++ b/code/kernel/mm_phys.c @@ -1,143 +1,305 @@ -// Physical-memory manager. This file owns frame accounting and boot memory-map consumption. +// Physical frame database. +// This file consumes the boot memory map and builds one PFN descriptor for each +// managed physical frame. The first implementation still allocates by scanning, +// but callers now get typed, reference-counted frames instead of anonymous +// bitmap bits. -// Bitmap allocator over 4 KB physical frames. One bitmap records which PFNs -// exist in usable RAM, and one bitmap records which usable PFNs are currently -// free. This keeps single-frame allocation simple while making contiguous -// allocation and memory visualization deterministic. -#include +#include #include +#include #include -// Physical-to-virtual translation via the direct map window. -static uint64_t gPhysBase = 0; // set by MmInitializePhysical +extern KSTATUS MmVadTrimOnePageForPressure(void); -// Implements the phys To Virt operation. +static uint64_t gPhysBase = 0; +static MM_PFN* gPfnDatabase = (MM_PFN*)0; +static size_t gMaxPfn = 0; +static size_t gTotalFrames = 0; +static size_t gFreeFrames = 0; +static SPINLOCK gPfnLock = SPINLOCK_INIT; +static uint64_t gPfnAllocations = 0; +static uint64_t gPfnFrees = 0; +static uint64_t gPfnRetains = 0; +static uint64_t gPfnReleases = 0; +static uint64_t gPfnPins = 0; +static uint64_t gPfnUnpins = 0; +static uint64_t gPfnFailedAllocations = 0; +static uint64_t gPfnFailedContiguousAllocations = 0; +static uint64_t gPfnFailedInvalidRange = 0; +static uint64_t gPfnFailedEmptyRange = 0; +static uint64_t gPfnFailedReserveDenied = 0; +static uint64_t gPfnFailedNoFrame = 0; +static uint64_t gPfnFailedContiguousInvalidRequest = 0; +static uint64_t gPfnFailedContiguousReserveDenied = 0; +static uint64_t gPfnFailedContiguousNoRun = 0; +static uint64_t gPfnReclaimAttempts = 0; +static uint64_t gPfnReclaimPagesRequested = 0; +static uint64_t gPfnReclaimPagesSucceeded = 0; +static uint64_t gPfnReclaimPagesFailed = 0; +static uint64_t gPfnReclaimReentrantSkips = 0; +static volatile int gPfnReclaiming = 0; + +static PHYSICAL_ADDRESS KiClaimPfnLocked(size_t pfn, MM_PFN_USAGE usage); + +#define MM_PFN_KERNEL_RESERVE_FRAMES 128u +#define MM_PFN_RECLAIM_LOW_WATER 512u +#define MM_PFN_RECLAIM_HIGH_WATER 768u + +// Converts a physical address into the direct-map alias used during early memory setup. static inline void* KiPhysToVirt(PHYSICAL_ADDRESS phys) { - return (void *)(phys + gPhysBase); + return (void*)(phys + gPhysBase); } -// Frame bitmap. - -typedef struct { - uint8_t* Usable; // bit = 1 when PFN belongs to usable RAM - uint8_t* Free; // bit = 1 when PFN is currently free - size_t BitmapBytes; - size_t MaxPfn; - size_t TotalFrames; // usable frames available to the allocator - size_t FreeFrames; - SPINLOCK Lock; -} FRAME_BITMAP; - -static FRAME_BITMAP gFrameBitmap; - -// Implements the align Up operation. +// Rounds a physical address up to the next whole frame boundary. static inline PHYSICAL_ADDRESS KiAlignUp(PHYSICAL_ADDRESS value) { return (value + PAGE_SIZE - 1) & PAGE_MASK; } -// Implements the align Down operation. +// Rounds a physical address down to the previous whole frame boundary. static inline PHYSICAL_ADDRESS KiAlignDown(PHYSICAL_ADDRESS value) { return value & PAGE_MASK; } -// Implements the bit Test operation. -static inline int KiBitTest(const uint8_t* bits, size_t index) { - return (bits[index >> 3] & (uint8_t)(1u << (index & 7))) != 0; +// Returns whether a boot memory type describes RAM that the PFN database should track. +static int KiMemoryTypeManaged(MEMORY_TYPE type) { + return type == MemoryUsable || type == MemoryBootloader || type == MemoryKernel; } -// Implements the bit Set operation. -static inline void KiBitSet(uint8_t* bits, size_t index) { - bits[index >> 3] |= (uint8_t)(1u << (index & 7)); +// Returns whether an allocation may consume the emergency kernel reserve. +static int KiPfnUsageMayUseReserve(MM_PFN_USAGE usage) { + return usage == MmPfnUseKernel || + usage == MmPfnUsePageTable || + usage == MmPfnUseDma || + usage == MmPfnUseBootloader || + usage == MmPfnUsePfnDatabase || + usage == MmPfnUseMmio; } -// Implements the bit Clear operation. -static inline void KiBitClear(uint8_t* bits, size_t index) { - bits[index >> 3] &= (uint8_t)~(1u << (index & 7)); +// Converts allocation purpose into the initial PFN state. +static MM_PFN_STATE KiAllocatedStateForUsage(MM_PFN_USAGE usage) { + (void)usage; + return MmPfnStateActive; } -// Implements the pfn Usable Locked operation. -static inline int KiPfnUsableLocked(size_t pfn) { - return pfn < gFrameBitmap.MaxPfn && KiBitTest(gFrameBitmap.Usable, pfn); +// Marks a PFN as used by a permanent structure that must never be returned to the free list. +static void KiReservePfnLocked(size_t pfn, MM_PFN_USAGE usage) { + if (pfn >= gMaxPfn) + return; + + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed) { + if (gFreeFrames) + gFreeFrames--; + } + + if (entry->State == MmPfnStateInvalid) + gTotalFrames++; + + entry->State = MmPfnStateActive; + entry->Usage = usage; + entry->RefCount = 1; + entry->PinCount = 1; + entry->Flags = 0; + entry->MappedCount = 0; + entry->Owner = (void*)0; + entry->BackingPage = 0; } -// Implements the pfn Free Locked operation. -static inline int KiPfnFreeLocked(size_t pfn) { - return KiPfnUsableLocked(pfn) && KiBitTest(gFrameBitmap.Free, pfn); -} +// Marks a page-aligned RAM range as managed by the PFN database. +static void KiRegisterManagedRange(PHYSICAL_ADDRESS base, + PHYSICAL_ADDRESS end, + MM_PFN_STATE state, + MM_PFN_USAGE usage) { + size_t firstPfn = (size_t)(base / PAGE_SIZE); + size_t count = (size_t)((end - base) / PAGE_SIZE); -// Implements the mark Usable Free Range operation. -static void KiMarkUsableFreeRange(size_t firstPfn, size_t count) { for (size_t i = 0; i < count; i++) { size_t pfn = firstPfn + i; - if (pfn >= gFrameBitmap.MaxPfn) break; - if (!KiBitTest(gFrameBitmap.Usable, pfn)) { - KiBitSet(gFrameBitmap.Usable, pfn); - gFrameBitmap.TotalFrames++; - } - if (!KiBitTest(gFrameBitmap.Free, pfn)) { - KiBitSet(gFrameBitmap.Free, pfn); - gFrameBitmap.FreeFrames++; + if (pfn >= gMaxPfn) + break; + + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateInvalid) + gTotalFrames++; + + if (entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed) { + if (gFreeFrames) + gFreeFrames--; } + + entry->State = state; + entry->Usage = usage; + entry->RefCount = (state == MmPfnStateFree || state == MmPfnStateZeroed) ? 0 : 1; + entry->PinCount = 0; + entry->Flags = 0; + entry->MappedCount = 0; + entry->Owner = (void*)0; + entry->BackingPage = 0; + + if (state == MmPfnStateFree || state == MmPfnStateZeroed) + gFreeFrames++; } } -// Releases usable Pfn. -static void KiRemoveUsablePfn(size_t pfn) { - if (pfn >= gFrameBitmap.MaxPfn) return; - if (KiBitTest(gFrameBitmap.Free, pfn)) { - KiBitClear(gFrameBitmap.Free, pfn); - if (gFrameBitmap.FreeFrames) gFrameBitmap.FreeFrames--; - } - if (KiBitTest(gFrameBitmap.Usable, pfn)) { - KiBitClear(gFrameBitmap.Usable, pfn); - if (gFrameBitmap.TotalFrames) gFrameBitmap.TotalFrames--; - } + +// Returns whether the future page-replacement code may consider this frame a candidate. +static int KiPfnEvictableLocked(const MM_PFN* entry) { + if (!entry || entry->PinCount) + return 0; + if (entry->State != MmPfnStateActive && entry->State != MmPfnStateStandby && entry->State != MmPfnStateModified) + return 0; + return entry->Usage == MmPfnUseUser || + entry->Usage == MmPfnUseCleanFileCache || + entry->Usage == MmPfnUseImage || + entry->Usage == MmPfnUseMappedFile || + entry->Usage == MmPfnUseAnonymous || + entry->Usage == MmPfnUseCow; } -// Initialisation. + + +// Returns whether a standby frame can be counted as immediately reclaimable. +static int KiPfnReclaimableLocked(const MM_PFN* entry) { + if (!entry || entry->PinCount || entry->MappedCount) + return 0; + if (entry->State != MmPfnStateStandby || entry->RefCount != 1) + return 0; + return entry->Usage == MmPfnUseCleanFileCache || entry->Usage == MmPfnUseImage; +} + +// Returns true when a PFN can satisfy a new allocation. +static int KiPfnAvailableLocked(size_t pfn) { + if (pfn >= gMaxPfn) + return 0; + MM_PFN* entry = &gPfnDatabase[pfn]; + return entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed; +} + +// Tries to create breathing room by swapping private user pages before an allocation fails. +static void KiPfnReclaimForPressure(void) { + if (!KeCanWait()) + return; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + if (gPfnReclaiming) { + gPfnReclaimReentrantSkips++; + KeReleaseSpinlock(&gPfnLock, flags); + return; + } + if (gFreeFrames >= MM_PFN_RECLAIM_LOW_WATER) { + KeReleaseSpinlock(&gPfnLock, flags); + return; + } + + gPfnReclaiming = 1; + gPfnReclaimAttempts++; + KeReleaseSpinlock(&gPfnLock, flags); + + for (uint32_t i = 0; i < 64; ++i) { + flags = KeAcquireSpinlock(&gPfnLock); + if (gFreeFrames >= MM_PFN_RECLAIM_HIGH_WATER) { + KeReleaseSpinlock(&gPfnLock, flags); + break; + } + gPfnReclaimPagesRequested++; + KeReleaseSpinlock(&gPfnLock, flags); + + KSTATUS s = MmVadTrimOnePageForPressure(); + + flags = KeAcquireSpinlock(&gPfnLock); + if (KSUCCESS(s)) + gPfnReclaimPagesSucceeded++; + else + gPfnReclaimPagesFailed++; + KeReleaseSpinlock(&gPfnLock, flags); + + if (!KSUCCESS(s)) + break; + } + + flags = KeAcquireSpinlock(&gPfnLock); + gPfnReclaiming = 0; + KeReleaseSpinlock(&gPfnLock, flags); +} + +// Claims a free PFN in the requested range while the PFN lock is held. +static PHYSICAL_ADDRESS KiClaimAvailableInRangeLocked(size_t firstPfn, size_t limitPfn, MM_PFN_USAGE usage) { + for (size_t pfn = limitPfn; pfn > firstPfn; pfn--) { + size_t candidate = pfn - 1; + if (KiPfnAvailableLocked(candidate)) + return KiClaimPfnLocked(candidate, usage); + } + return (PHYSICAL_ADDRESS)0; +} + +// Claims one free PFN for the requested usage. +static PHYSICAL_ADDRESS KiClaimPfnLocked(size_t pfn, MM_PFN_USAGE usage) { + MM_PFN* entry = &gPfnDatabase[pfn]; + entry->State = KiAllocatedStateForUsage(usage); + entry->Usage = usage; + entry->RefCount = 1; + entry->PinCount = (usage == MmPfnUsePageTable) ? 1 : 0; + entry->Flags = 0; + entry->MappedCount = 0; + entry->Owner = (void*)0; + entry->BackingPage = 0; + if (gFreeFrames) + gFreeFrames--; + gPfnAllocations++; + return MmPfnAddress(pfn); +} + +// Initializes the PFN database from the bootloader memory map. bool MmInitializePhysical(const MEMORY_MAP* map, uint64_t physicalWindowBase) { - if (!map) return false; + if (!map) + return false; + gPhysBase = physicalWindowBase; PHYSICAL_ADDRESS maxEnd = 0; for (uint32_t i = 0; i < map->RegionCount; i++) { const MEMORY_REGION* r = &map->Regions[i]; - if (r->Type != MemoryUsable) continue; + if (!KiMemoryTypeManaged(r->Type)) + continue; PHYSICAL_ADDRESS base = KiAlignUp(r->Base); PHYSICAL_ADDRESS end = r->Base + r->Length; - if (end < r->Base) end = (PHYSICAL_ADDRESS)-1; + if (end < r->Base) + end = (PHYSICAL_ADDRESS)-1; end = KiAlignDown(end); - if (end <= base) continue; - if (end > maxEnd) maxEnd = end; + if (end <= base) + continue; + if (end > maxEnd) + maxEnd = end; } - size_t maxPfn = (size_t)(maxEnd / PAGE_SIZE); - if (!maxPfn) return false; + gMaxPfn = (size_t)(maxEnd / PAGE_SIZE); + if (!gMaxPfn) + return false; - size_t bitmapBytes = (maxPfn + 7) / 8; - size_t metadataBytes = bitmapBytes * 2; - size_t metadataPages = (metadataBytes + PAGE_SIZE - 1) / PAGE_SIZE; - PHYSICAL_ADDRESS metadataSize = (PHYSICAL_ADDRESS)metadataPages * PAGE_SIZE; + size_t pfnBytes = gMaxPfn * sizeof(MM_PFN); + size_t pfnPages = (pfnBytes + PAGE_SIZE - 1) / PAGE_SIZE; + PHYSICAL_ADDRESS metadataSize = (PHYSICAL_ADDRESS)pfnPages * PAGE_SIZE; - // Keep allocator metadata out of low memory. The bitmap is much smaller - // than the old free-frame stack, so a first-fit search can otherwise choose - // a conventional-memory hole below 1 MiB. That range is still useful for - // AP startup/legacy scratch, and stealing it makes early boot failures - // unnecessarily silent because PMM initializes before normal debug output. const PHYSICAL_ADDRESS metadataMin = 0x100000ULL; PHYSICAL_ADDRESS metadataPhys = 0; int metadataFound = 0; + for (uint32_t i = 0; i < map->RegionCount; i++) { const MEMORY_REGION* r = &map->Regions[i]; - if (r->Type != MemoryUsable) continue; + if (r->Type != MemoryUsable) + continue; PHYSICAL_ADDRESS base = KiAlignUp(r->Base); - if (base < metadataMin) base = metadataMin; + if (base < metadataMin) + base = metadataMin; PHYSICAL_ADDRESS end = r->Base + r->Length; - if (end < r->Base) end = (PHYSICAL_ADDRESS)-1; + if (end < r->Base) + end = (PHYSICAL_ADDRESS)-1; end = KiAlignDown(end); - if (end <= base) continue; + if (end <= base) + continue; + if (end - base >= metadataSize) { metadataPhys = base; metadataFound = 1; @@ -145,155 +307,529 @@ bool MmInitializePhysical(const MEMORY_MAP* map, uint64_t physicalWindowBase) { } } - if (!metadataFound) return false; + if (!metadataFound) + return false; - gFrameBitmap.BitmapBytes = bitmapBytes; - gFrameBitmap.MaxPfn = maxPfn; - gFrameBitmap.TotalFrames = 0; - gFrameBitmap.FreeFrames = 0; - gFrameBitmap.Lock = (SPINLOCK)SPINLOCK_INIT; - gFrameBitmap.Usable = (uint8_t*)KiPhysToVirt(metadataPhys); - gFrameBitmap.Free = gFrameBitmap.Usable + bitmapBytes; + gPfnDatabase = (MM_PFN*)KiPhysToVirt(metadataPhys); + memset(gPfnDatabase, 0, metadataSize); + gTotalFrames = 0; + gFreeFrames = 0; + gPfnLock = (SPINLOCK)SPINLOCK_INIT; + gPfnAllocations = 0; + gPfnFrees = 0; + gPfnRetains = 0; + gPfnReleases = 0; + gPfnPins = 0; + gPfnUnpins = 0; + gPfnFailedAllocations = 0; + gPfnFailedContiguousAllocations = 0; + gPfnFailedInvalidRange = 0; + gPfnFailedEmptyRange = 0; + gPfnFailedReserveDenied = 0; + gPfnFailedNoFrame = 0; + gPfnFailedContiguousInvalidRequest = 0; + gPfnFailedContiguousReserveDenied = 0; + gPfnFailedContiguousNoRun = 0; + gPfnReclaimAttempts = 0; + gPfnReclaimPagesRequested = 0; + gPfnReclaimPagesSucceeded = 0; + gPfnReclaimPagesFailed = 0; + gPfnReclaimReentrantSkips = 0; - memset(gFrameBitmap.Usable, 0, metadataBytes); - - // Mark page-aligned usable RAM as free. Unaligned tails are ignored rather - // than handing malformed frame addresses to the rest of the kernel. for (uint32_t i = 0; i < map->RegionCount; i++) { const MEMORY_REGION* r = &map->Regions[i]; - if (r->Type != MemoryUsable) continue; + if (!KiMemoryTypeManaged(r->Type)) + continue; PHYSICAL_ADDRESS base = KiAlignUp(r->Base); PHYSICAL_ADDRESS end = r->Base + r->Length; - if (end < r->Base) end = (PHYSICAL_ADDRESS)-1; + if (end < r->Base) + end = (PHYSICAL_ADDRESS)-1; end = KiAlignDown(end); - if (end <= base) continue; + if (end <= base) + continue; - size_t firstPfn = (size_t)(base / PAGE_SIZE); - size_t count = (size_t)((end - base) / PAGE_SIZE); - KiMarkUsableFreeRange(firstPfn, count); + if (r->Type == MemoryUsable) { + KiRegisterManagedRange(base, end, MmPfnStateFree, MmPfnUseNone); + } else if (r->Type == MemoryBootloader) { + KiRegisterManagedRange(base, end, MmPfnStateActive, MmPfnUseBootloader); + } else { + KiRegisterManagedRange(base, end, MmPfnStateActive, MmPfnUseKernel); + for (size_t pfn = (size_t)(base / PAGE_SIZE); pfn < (size_t)(end / PAGE_SIZE) && pfn < gMaxPfn; pfn++) + gPfnDatabase[pfn].PinCount = 1; + } } - // PFN 0 is never a valid allocation result because 0 is the failure value. - KiRemoveUsablePfn(0); + // PFN 0 remains reserved because a zero physical address is the allocation failure value. + KiReservePfnLocked(0, MmPfnUseKernel); - // Hide the PMM metadata pages from the allocator entirely. They are not - // merely allocated; they are structural storage that must not become free if - // some caller accidentally passes their PFNs to MmFreeFrame(). + // The PFN database owns its storage permanently. It is structural metadata, + // not an allocation that can later be freed through a stale physical address. size_t metadataFirstPfn = (size_t)(metadataPhys / PAGE_SIZE); - for (size_t i = 0; i < metadataPages; i++) - KiRemoveUsablePfn(metadataFirstPfn + i); + for (size_t i = 0; i < pfnPages; i++) + KiReservePfnLocked(metadataFirstPfn + i, MmPfnUsePfnDatabase); return true; } -// Public API. -PHYSICAL_ADDRESS MmAllocateFrameInRange(PHYSICAL_ADDRESS minInclusive, - PHYSICAL_ADDRESS maxExclusive) { +// Converts a physical frame address to a PFN index. +size_t MmPfnIndex(PHYSICAL_ADDRESS frame) { + if (frame & (PAGE_SIZE - 1)) + return MM_PFN_INVALID_INDEX; + size_t pfn = (size_t)(frame / PAGE_SIZE); + if (pfn >= gMaxPfn) + return MM_PFN_INVALID_INDEX; + return pfn; +} + +// Converts a PFN index to a physical frame address. +PHYSICAL_ADDRESS MmPfnAddress(size_t pfn) { + return (PHYSICAL_ADDRESS)pfn * PAGE_SIZE; +} + +// Returns the PFN descriptor for a page-aligned physical frame. +__declspec(dllexport) MM_PFN* MmPfnGet(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return (MM_PFN*)0; + return &gPfnDatabase[pfn]; +} + + +// Returns the direct-map virtual address for a managed physical frame. +__declspec(dllexport) void* MmPfnToVirtual(PHYSICAL_ADDRESS frame) { + if (MmPfnIndex(frame) == MM_PFN_INVALID_INDEX) + return (void*)0; + return KiPhysToVirt(frame); +} + +// Allocates one physical frame for the requested kernel subsystem. +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocate(MM_PFN_USAGE usage) { + return MmPfnAllocateInRange(PAGE_SIZE, (PHYSICAL_ADDRESS)gMaxPfn * PAGE_SIZE, usage); +} + +// Allocates one physical frame below a physical address limit. +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocateBelow(PHYSICAL_ADDRESS maxExclusive, MM_PFN_USAGE usage) { + return MmPfnAllocateInRange(PAGE_SIZE, maxExclusive, usage); +} + +// Allocates one physical frame inside a physical address range. +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocateInRange(PHYSICAL_ADDRESS minInclusive, + PHYSICAL_ADDRESS maxExclusive, + MM_PFN_USAGE usage) { minInclusive = KiAlignUp(minInclusive); maxExclusive = KiAlignDown(maxExclusive); - if (maxExclusive <= minInclusive) return (PHYSICAL_ADDRESS)0; + if (maxExclusive <= minInclusive) { + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + gPfnFailedAllocations++; + gPfnFailedInvalidRange++; + KeReleaseSpinlock(&gPfnLock, flags); + return (PHYSICAL_ADDRESS)0; + } size_t firstPfn = (size_t)(minInclusive / PAGE_SIZE); size_t limitPfn = (size_t)(maxExclusive / PAGE_SIZE); - if (limitPfn > gFrameBitmap.MaxPfn) limitPfn = gFrameBitmap.MaxPfn; - if (firstPfn >= limitPfn) return (PHYSICAL_ADDRESS)0; - - uintptr_t flags = KeAcquireSpinlock(&gFrameBitmap.Lock); - - for (size_t pfn = limitPfn; pfn > firstPfn; pfn--) { - size_t candidate = pfn - 1; - if (KiPfnFreeLocked(candidate)) { - KiBitClear(gFrameBitmap.Free, candidate); - if (gFrameBitmap.FreeFrames) gFrameBitmap.FreeFrames--; - KeReleaseSpinlock(&gFrameBitmap.Lock, flags); - return (PHYSICAL_ADDRESS)candidate * PAGE_SIZE; - } + if (limitPfn > gMaxPfn) + limitPfn = gMaxPfn; + if (firstPfn >= limitPfn) { + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + gPfnFailedAllocations++; + gPfnFailedEmptyRange++; + KeReleaseSpinlock(&gPfnLock, flags); + return (PHYSICAL_ADDRESS)0; } - KeReleaseSpinlock(&gFrameBitmap.Lock, flags); + KiPfnReclaimForPressure(); + + int mayUseReserve = KiPfnUsageMayUseReserve(usage); + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + if (!mayUseReserve && gFreeFrames <= MM_PFN_KERNEL_RESERVE_FRAMES) { + KeReleaseSpinlock(&gPfnLock, flags); + } else { + PHYSICAL_ADDRESS phys = KiClaimAvailableInRangeLocked(firstPfn, limitPfn, usage); + if (phys) { + KeReleaseSpinlock(&gPfnLock, flags); + return phys; + } + KeReleaseSpinlock(&gPfnLock, flags); + } + + KiPfnReclaimForPressure(); + + flags = KeAcquireSpinlock(&gPfnLock); + if (!mayUseReserve && gFreeFrames <= MM_PFN_KERNEL_RESERVE_FRAMES) { + gPfnFailedAllocations++; + gPfnFailedReserveDenied++; + KeReleaseSpinlock(&gPfnLock, flags); + return (PHYSICAL_ADDRESS)0; + } + + PHYSICAL_ADDRESS phys = KiClaimAvailableInRangeLocked(firstPfn, limitPfn, usage); + if (phys) { + KeReleaseSpinlock(&gPfnLock, flags); + return phys; + } + gPfnFailedAllocations++; + gPfnFailedNoFrame++; + KeReleaseSpinlock(&gPfnLock, flags); return (PHYSICAL_ADDRESS)0; } -PHYSICAL_ADDRESS MmAllocateFrameBelow(PHYSICAL_ADDRESS maxExclusive) { - return MmAllocateFrameInRange(PAGE_SIZE, maxExclusive); -} - -// Creates frame. -__declspec(dllexport) PHYSICAL_ADDRESS MmAllocateFrame(void) { - return MmAllocateFrameInRange(PAGE_SIZE, (PHYSICAL_ADDRESS)gFrameBitmap.MaxPfn * PAGE_SIZE); -} - -// Releases frame. -void MmFreeFrame(PHYSICAL_ADDRESS frame) { - if (!frame || (frame & (PAGE_SIZE - 1))) return; - size_t pfn = (size_t)(frame / PAGE_SIZE); - - uintptr_t flags = KeAcquireSpinlock(&gFrameBitmap.Lock); - if (KiPfnUsableLocked(pfn) && !KiBitTest(gFrameBitmap.Free, pfn)) { - KiBitSet(gFrameBitmap.Free, pfn); - gFrameBitmap.FreeFrames++; +// Allocates a physically contiguous run for DMA or hardware structures. +__declspec(dllexport) PHYSICAL_ADDRESS MmPfnAllocateContiguous(size_t count, MM_PFN_USAGE usage) { + if (!count || count > gMaxPfn) { + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + gPfnFailedContiguousAllocations++; + gPfnFailedContiguousInvalidRequest++; + KeReleaseSpinlock(&gPfnLock, flags); + return (PHYSICAL_ADDRESS)0; } - KeReleaseSpinlock(&gFrameBitmap.Lock, flags); -} -// Creates contiguous. -__declspec(dllexport) PHYSICAL_ADDRESS MmAllocateContiguous(size_t count) { - if (!count || count > gFrameBitmap.MaxPfn) return (PHYSICAL_ADDRESS)0; + if (!KiPfnUsageMayUseReserve(usage)) + KiPfnReclaimForPressure(); - uintptr_t flags = KeAcquireSpinlock(&gFrameBitmap.Lock); + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + if (!KiPfnUsageMayUseReserve(usage) && gFreeFrames < MM_PFN_KERNEL_RESERVE_FRAMES + count) { + gPfnFailedContiguousAllocations++; + gPfnFailedContiguousReserveDenied++; + KeReleaseSpinlock(&gPfnLock, flags); + return (PHYSICAL_ADDRESS)0; + } size_t runStart = 0; size_t runLength = 0; - for (size_t pfn = 1; pfn < gFrameBitmap.MaxPfn; pfn++) { - if (KiPfnFreeLocked(pfn)) { - if (!runLength) runStart = pfn; + + for (size_t pfn = 1; pfn < gMaxPfn; pfn++) { + if (KiPfnAvailableLocked(pfn)) { + if (!runLength) + runStart = pfn; runLength++; if (runLength == count) { for (size_t i = 0; i < count; i++) - KiBitClear(gFrameBitmap.Free, runStart + i); - if (gFrameBitmap.FreeFrames >= count) gFrameBitmap.FreeFrames -= count; - else gFrameBitmap.FreeFrames = 0; - KeReleaseSpinlock(&gFrameBitmap.Lock, flags); - return (PHYSICAL_ADDRESS)runStart * PAGE_SIZE; + KiClaimPfnLocked(runStart + i, usage); + KeReleaseSpinlock(&gPfnLock, flags); + return MmPfnAddress(runStart); } } else { runLength = 0; } } - KeReleaseSpinlock(&gFrameBitmap.Lock, flags); + gPfnFailedContiguousAllocations++; + gPfnFailedContiguousNoRun++; + KeReleaseSpinlock(&gPfnLock, flags); return (PHYSICAL_ADDRESS)0; } -// Releases frame Count. +// Increments the reference count on an allocated frame. +__declspec(dllexport) KSTATUS MmPfnRetain(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateInvalid || entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + if (entry->RefCount == UINT16_MAX) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_FAILURE; + } + entry->RefCount++; + gPfnRetains++; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + +// Releases a reference and returns the frame to the free list when the last owner leaves. +__declspec(dllexport) KSTATUS MmPfnRelease(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateInvalid || entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + if (entry->PinCount) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_ACCESS_DENIED; + } + gPfnReleases++; + if (entry->RefCount > 1) { + entry->RefCount--; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; + } + if (entry->MappedCount) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_ACCESS_DENIED; + } + + entry->State = MmPfnStateFree; + entry->Usage = MmPfnUseNone; + entry->RefCount = 0; + entry->PinCount = 0; + entry->Flags = 0; + entry->MappedCount = 0; + entry->Owner = (void*)0; + entry->BackingPage = 0; + gFreeFrames++; + gPfnFrees++; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + +// Frees a frame that is expected to have a single owner. +__declspec(dllexport) KSTATUS MmPfnFree(PHYSICAL_ADDRESS frame) { + return MmPfnRelease(frame); +} + +// Pins a frame so later page replacement cannot evict it. +__declspec(dllexport) KSTATUS MmPfnPin(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateInvalid || entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + if (entry->PinCount == UINT16_MAX) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_FAILURE; + } + entry->PinCount++; + gPfnPins++; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + +// Removes one pin from a frame. +__declspec(dllexport) KSTATUS MmPfnUnpin(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (!entry->PinCount) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + entry->PinCount--; + gPfnUnpins++; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + +// Records that a VAD now maps this frame into a user address space. +__declspec(dllexport) KSTATUS MmPfnAddMapping(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateInvalid || entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + if (entry->MappedCount == UINT32_MAX) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_FAILURE; + } + entry->MappedCount++; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + +// Removes one user VAD mapping reference from a frame. +__declspec(dllexport) KSTATUS MmPfnRemoveMapping(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateInvalid || entry->State == MmPfnStateFree || entry->State == MmPfnStateZeroed || + entry->MappedCount == 0) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + entry->MappedCount--; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + + + +// Moves a clean, unshared frame to standby so pressure can reclaim it later. +__declspec(dllexport) KSTATUS MmPfnMoveToStandby(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State != MmPfnStateActive || entry->PinCount || entry->MappedCount || entry->RefCount != 1) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_ACCESS_DENIED; + } + if (entry->Usage != MmPfnUseCleanFileCache && entry->Usage != MmPfnUseImage) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + + entry->State = MmPfnStateStandby; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + +// Reactivates a standby frame after a cache or image lookup found it still useful. +__declspec(dllexport) KSTATUS MmPfnActivate(PHYSICAL_ADDRESS frame) { + size_t pfn = MmPfnIndex(frame); + if (pfn == MM_PFN_INVALID_INDEX) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State != MmPfnStateStandby || !entry->RefCount) { + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_INVALID_PARAMETER; + } + + entry->State = MmPfnStateActive; + KeReleaseSpinlock(&gPfnLock, flags); + return STATUS_SUCCESS; +} + +// Returns the number of frames that are immediately available for allocation. size_t MmFreeFrameCount(void) { - uintptr_t flags = KeAcquireSpinlock(&gFrameBitmap.Lock); - size_t count = gFrameBitmap.FreeFrames; - KeReleaseSpinlock(&gFrameBitmap.Lock, flags); + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + size_t count = gFreeFrames; + KeReleaseSpinlock(&gPfnLock, flags); return count; } -// Implements the total Frame Count operation. +// Returns the number of RAM frames managed by the PFN database. size_t MmTotalFrameCount(void) { - return gFrameBitmap.TotalFrames; + return gTotalFrames; } -// Returns physical Stats. + +// Returns detailed PFN state counters for VMM self-tests and diagnostics. +void MmPfnQueryStats(MM_PFN_STATS* out) { + if (!out) + return; + + MM_PFN_STATS stats; + memset(&stats, 0, sizeof(stats)); + + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + stats.TotalFrames = (uint64_t)gTotalFrames; + stats.FreeFrames = (uint64_t)gFreeFrames; + stats.UsedFrames = stats.TotalFrames - stats.FreeFrames; + + for (size_t pfn = 0; pfn < gMaxPfn; pfn++) { + const MM_PFN* entry = &gPfnDatabase[pfn]; + if (entry->State == MmPfnStateInvalid) + continue; + switch ((MM_PFN_STATE)entry->State) { + case MmPfnStateReserved: stats.ReservedFrames++; break; + case MmPfnStateZeroed: stats.ZeroedFrames++; break; + case MmPfnStateActive: stats.ActiveFrames++; break; + case MmPfnStateStandby: stats.StandbyFrames++; break; + case MmPfnStateModified: stats.ModifiedFrames++; break; + default: break; + } + if (entry->PinCount) + stats.PinnedFrames++; + if (entry->MappedCount) + stats.MappedFrames++; + if (KiPfnEvictableLocked(entry)) + stats.EvictableFrames++; + else if (entry->State != MmPfnStateFree && entry->State != MmPfnStateZeroed) + stats.NonEvictableFrames++; + if (KiPfnReclaimableLocked(entry)) + stats.ReclaimableFrames++; + + switch ((MM_PFN_USAGE)entry->Usage) { + case MmPfnUseKernel: stats.KernelFrames++; break; + case MmPfnUsePageTable: stats.PageTableFrames++; break; + case MmPfnUseUser: stats.UserFrames++; break; + case MmPfnUseCleanFileCache: + stats.CleanFileCacheFrames++; + if (entry->State == MmPfnStateStandby) stats.StandbyCleanFileCacheFrames++; + else if (entry->State == MmPfnStateActive) stats.ActiveCleanFileCacheFrames++; + break; + case MmPfnUseImage: + stats.ImageFrames++; + if (entry->State == MmPfnStateStandby) stats.StandbyImageFrames++; + else if (entry->State == MmPfnStateActive) stats.ActiveImageFrames++; + break; + case MmPfnUseMappedFile: stats.MappedFileFrames++; break; + case MmPfnUseAnonymous: stats.AnonymousFrames++; break; + case MmPfnUseCow: stats.CowFrames++; break; + case MmPfnUseSurface: stats.SurfaceFrames++; break; + case MmPfnUseDriver: stats.DriverFrames++; break; + case MmPfnUseDma: stats.DmaFrames++; break; + case MmPfnUseBootloader: stats.BootloaderFrames++; break; + case MmPfnUsePfnDatabase: stats.PfnDatabaseFrames++; break; + case MmPfnUseMmio: stats.MmioFrames++; break; + default: break; + } + } + + stats.AvailableFrames = stats.FreeFrames + stats.ReclaimableFrames; + + stats.AllocationCount = gPfnAllocations; + stats.FreeCount = gPfnFrees; + stats.RetainCount = gPfnRetains; + stats.ReleaseCount = gPfnReleases; + stats.PinCount = gPfnPins; + stats.UnpinCount = gPfnUnpins; + stats.FailedAllocations = gPfnFailedAllocations; + stats.FailedContiguousAllocations = gPfnFailedContiguousAllocations; + stats.FailedInvalidRange = gPfnFailedInvalidRange; + stats.FailedEmptyRange = gPfnFailedEmptyRange; + stats.FailedReserveDenied = gPfnFailedReserveDenied; + stats.FailedNoFrame = gPfnFailedNoFrame; + stats.FailedContiguousInvalidRequest = gPfnFailedContiguousInvalidRequest; + stats.FailedContiguousReserveDenied = gPfnFailedContiguousReserveDenied; + stats.FailedContiguousNoRun = gPfnFailedContiguousNoRun; + stats.ReclaimAttempts = gPfnReclaimAttempts; + stats.ReclaimPagesRequested = gPfnReclaimPagesRequested; + stats.ReclaimPagesSucceeded = gPfnReclaimPagesSucceeded; + stats.ReclaimPagesFailed = gPfnReclaimPagesFailed; + stats.ReclaimReentrantSkips = gPfnReclaimReentrantSkips; + + KeReleaseSpinlock(&gPfnLock, flags); + *out = stats; +} + +// Returns the PMM counters expected by the existing stats syscall. void MmQueryPhysicalStats(MM_PHYSICAL_STATS* out) { - if (!out) return; + if (!out) + return; MM_PHYSICAL_STATS stats; memset(&stats, 0, sizeof(stats)); - uintptr_t flags = KeAcquireSpinlock(&gFrameBitmap.Lock); - - stats.TotalFrames = (uint64_t)gFrameBitmap.TotalFrames; - stats.FreeFrames = (uint64_t)gFrameBitmap.FreeFrames; + uintptr_t flags = KeAcquireSpinlock(&gPfnLock); + stats.TotalFrames = (uint64_t)gTotalFrames; + stats.FreeFrames = (uint64_t)gFreeFrames; stats.UsedFrames = stats.TotalFrames - stats.FreeFrames; uint64_t run = 0; - for (size_t pfn = 1; pfn < gFrameBitmap.MaxPfn; pfn++) { - if (KiPfnFreeLocked(pfn)) { + for (size_t pfn = 1; pfn < gMaxPfn; pfn++) { + if (KiPfnAvailableLocked(pfn)) { run++; continue; } @@ -328,7 +864,6 @@ void MmQueryPhysicalStats(MM_PHYSICAL_STATS* out) { else stats.FreeRunHistogram[7]++; } - KeReleaseSpinlock(&gFrameBitmap.Lock, flags); + KeReleaseSpinlock(&gPfnLock, flags); *out = stats; } - diff --git a/code/kernel/mm_pte.c b/code/kernel/mm_pte.c new file mode 100644 index 0000000..6e79e45 --- /dev/null +++ b/code/kernel/mm_pte.c @@ -0,0 +1,575 @@ +// Kernel page-table manager. This file owns x86-64 PTE manipulation, TLB invalidation, and shared-CR3 activation. + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +// Page-table frames are reached through the physical direct map while the kernel still owns one shared CR3. +static uint64_t gPhysBase = PHYS_MAP_BASE; + +// Converts a physical address to the kernel physical-window alias used for page-table editing. +static inline void* KiPhysToVirt(PHYSICAL_ADDRESS p) { + return (void *)(p + gPhysBase); +} +// Returns the entry pointer for a page-table frame that is already mapped in the physical window. +static inline uint64_t* KiPtePtr(PHYSICAL_ADDRESS tablePhys, unsigned idx) { + return (uint64_t *)KiPhysToVirt(tablePhys) + idx; +} + +// The bootloader builds the initial PML4, and this layer owns all later edits to it. +static PHYSICAL_ADDRESS gPml4Phys = 0; +static SPINLOCK gPageTableLock = SPINLOCK_INIT; + +static volatile long gTlbShootdownLock = 0; +static volatile long gTlbShootdownActive = 0; +static volatile long gTlbShootdownAcks = 0; +static uintptr_t gTlbShootdownVirt = 0; +static MM_PTE_STATS gPteStats; + +#define TLB_SHOOTDOWN_TIMEOUT_SPINS 20000000u + +// These macros extract the hardware page-table index for each level. +#define PML4_IDX(va) (((va) >> 39) & 0x1FF) +#define PDPT_IDX(va) (((va) >> 30) & 0x1FF) +#define PDT_IDX(va) (((va) >> 21) & 0x1FF) +#define PT_IDX(va) (((va) >> 12) & 0x1FF) + +#define PAGE_PS (1ULL << 7) +#define PAGE_FLAGS_MASK (0xFFFULL | PAGE_NX) +#define PAGE_FRAME_4K_MASK 0x000FFFFFFFFFF000ULL +#define PAGE_FRAME_2M_MASK 0x000FFFFFFFE00000ULL +#define PAGE_FRAME_1G_MASK 0x000FFFFFC0000000ULL + +// Bumps a telemetry counter. These counters are diagnostic only; exactness is +// less important than avoiding extra locks in hot page-table paths. +static inline void KiPteStatInc(uint64_t* counter) { + (*counter)++; +} + +// Clamps query attribution so corrupt callers cannot index outside telemetry arrays. +static MM_PTE_QUERY_PURPOSE KiNormalizeQueryPurpose(MM_PTE_QUERY_PURPOSE purpose) { + if ((uint32_t)purpose >= (uint32_t)MmPteQueryPurposeCount) + return MmPteQueryGeneric; + return purpose; +} + +// Ensures that one parent entry points to a child page table. +// New child tables are zeroed before publication so no stale mappings become visible. +static PHYSICAL_ADDRESS KiEnsureTable(PHYSICAL_ADDRESS parentPhys, + unsigned index, uint64_t extraFlags) { + uint64_t* entry = KiPtePtr(parentPhys, index); + if (*entry & PAGE_PRESENT) { + *entry |= extraFlags; + return *entry & PAGE_FRAME_4K_MASK; + } + PHYSICAL_ADDRESS childPhys = MmPfnAllocate(MmPfnUsePageTable); + if (!childPhys) return 0; + KiPteStatInc(&gPteStats.PageTablePagesAllocated); + memset(KiPhysToVirt(childPhys), 0, PAGE_SIZE); + *entry = childPhys | PAGE_PRESENT | PAGE_WRITE | extraFlags; + return childPhys; +} + + +// Initializes the page-table manager around the bootloader-created PML4. +void MmPteInitialize(PHYSICAL_ADDRESS bootPml4Phys) { + gPml4Phys = bootPml4Phys; + gPageTableLock = (SPINLOCK)SPINLOCK_INIT; + gTlbShootdownLock = 0; + gTlbShootdownActive = 0; + gTlbShootdownAcks = 0; + gTlbShootdownVirt = 0; + memset(&gPteStats, 0, sizeof(gPteStats)); +} + +// Installs the shared PML4 into CR3. +void MmPteActivate(void) { + __writecr3(gPml4Phys); +} + +// Flushes one virtual address from the local TLB. +void MmPteFlushLocal(uintptr_t virt) { + KiPteStatInc(&gPteStats.LocalFlushes); + __invlpg((void*)virt); +} + +// Acquires the shootdown state lock without disabling interrupts. +static void KiAcquireTlbShootdownLock(void) { + // Do not use the normal spinlock here: it masks interrupts while spinning. + // A CPU spinning with IF=0 on the shootdown lock could not acknowledge a + // concurrent fixed-vector TLB IPI, creating the exact deadlock this lock is + // meant to avoid. + while (_InterlockedCompareExchange(&gTlbShootdownLock, 1, 0) != 0) + _mm_pause(); + _ReadWriteBarrier(); +} + +// Releases the shootdown state lock after all shared fields are stable. +static void KiReleaseTlbShootdownLock(void) { + _ReadWriteBarrier(); + _InterlockedExchange(&gTlbShootdownLock, 0); +} + +// Flushes one kernel mapping on every online CPU that shares this PML4. +void MmPteFlushKernelAddress(uintptr_t virt) { + KiPteStatInc(&gPteStats.GlobalFlushes); + MmPteFlushLocal(virt); + + if (gCpuCount <= 1) { + KiPteStatInc(&gPteStats.ShootdownSkippedNoTargets); + return; + } + + KCPU* self = KeGetCurrentCpu(); + uint32_t selfIndex = self ? self->CpuIndex : KE_INVALID_CPU; + + KiAcquireTlbShootdownLock(); + + long targets = 0; + gTlbShootdownVirt = virt; + _InterlockedExchange(&gTlbShootdownAcks, 0); + _ReadWriteBarrier(); + _InterlockedExchange(&gTlbShootdownActive, 1); + _ReadWriteBarrier(); + + for (uint32_t i = 0; i < gCpuCount; i++) { + KCPU* cpu = &gCpus[i]; + if (i == selfIndex) + continue; + if (!cpu->Present || !cpu->Started || !cpu->Online) + continue; + targets++; + KiPteStatInc(&gPteStats.ShootdownTargetCpus); + HalApicSendTlbShootdownIpi(cpu->LapicId); + } + + if (!targets) { + KiPteStatInc(&gPteStats.ShootdownSkippedNoTargets); + _InterlockedExchange(&gTlbShootdownActive, 0); + _ReadWriteBarrier(); + KiReleaseTlbShootdownLock(); + return; + } + + KiPteStatInc(&gPteStats.ShootdownRequests); + + for (uint32_t spin = 0; targets > 0 && spin < TLB_SHOOTDOWN_TIMEOUT_SPINS; spin++) { + if (_InterlockedCompareExchange(&gTlbShootdownAcks, 0, 0) >= targets) + break; + _mm_pause(); + } + + long acks = _InterlockedCompareExchange(&gTlbShootdownAcks, 0, 0); + _InterlockedExchange(&gTlbShootdownActive, 0); + _ReadWriteBarrier(); + KiReleaseTlbShootdownLock(); + + gPteStats.ShootdownAcks += (uint64_t)acks; + if (acks < targets) { + KiPteStatInc(&gPteStats.ShootdownTimeouts); + KePanic(KE_PANIC_CODE_GENERIC, + "TLB shootdown timeout: va=0x%016llX targets=%ld acks=%ld selfCpu=%u\n", + (uint64_t)virt, targets, acks, selfIndex); + } +} + +// Handles an incoming shootdown request on the current CPU. +static int KiHandleTlbShootdown(void) { + if (_InterlockedCompareExchange(&gTlbShootdownActive, 0, 0) == 0) + return 0; + + uintptr_t va = gTlbShootdownVirt; + _ReadWriteBarrier(); + MmPteFlushLocal(va); + _ReadWriteBarrier(); + _InterlockedIncrement(&gTlbShootdownAcks); + return 1; +} + +// Handles the NMI vector reserved for TLB shootdowns. +int MmPteHandleShootdownNmi(void) { + return KiHandleTlbShootdown(); +} + +// Maps or replaces one 4 KiB leaf PTE in the shared page tables. +__declspec(dllexport) KSTATUS MmPteMapPage(uintptr_t virt, PHYSICAL_ADDRESS phys, uint64_t flags, MM_PTE_MAP_MODE mode) { + KiPteStatInc(&gPteStats.MapCalls); + unsigned userFlag = (flags & PAGE_USER) ? PAGE_USER : 0; + + uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); + + PHYSICAL_ADDRESS pdpt = KiEnsureTable(gPml4Phys, PML4_IDX(virt), userFlag); + if (!pdpt) { + KiPteStatInc(&gPteStats.MapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_NO_MEMORY; + } + PHYSICAL_ADDRESS pdt = KiEnsureTable(pdpt, PDPT_IDX(virt), userFlag); + if (!pdt) { + KiPteStatInc(&gPteStats.MapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_NO_MEMORY; + } + PHYSICAL_ADDRESS pt = KiEnsureTable(pdt, PDT_IDX(virt), userFlag); + if (!pt) { + KiPteStatInc(&gPteStats.MapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_NO_MEMORY; + } + + uint64_t* pte = KiPtePtr(pt, PT_IDX(virt)); + if ((*pte & PAGE_PRESENT) && mode == MmPteMapFailIfPresent) { + KiPteStatInc(&gPteStats.MapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_ALREADY_EXISTS; + } + int wasPresent = ((*pte & PAGE_PRESENT) != 0); + *pte = phys | flags; + if (!wasPresent) { + KiPteStatInc(&gPteStats.CurrentLeafMappings); + KiPteStatInc(&gPteStats.NewMappingNoFlushes); + } + + KeReleaseSpinlock(&gPageTableLock, lockFlags); + + if (wasPresent) { + KiPteStatInc(&gPteStats.ReplacedMappingFlushes); + MmPteFlushKernelAddress(virt); + } + return STATUS_SUCCESS; +} + + +// Removes one 4 KiB leaf PTE from the shared page tables. +__declspec(dllexport) KSTATUS MmPteUnmapPage(uintptr_t virt) { + KiPteStatInc(&gPteStats.UnmapCalls); + uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); + + uint64_t* e4 = KiPtePtr(gPml4Phys, PML4_IDX(virt)); + if (!(*e4 & PAGE_PRESENT)) { + KiPteStatInc(&gPteStats.UnmapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_NOT_FOUND; + } + uint64_t* e3 = KiPtePtr(*e4 & PAGE_FRAME_4K_MASK, PDPT_IDX(virt)); + if (!(*e3 & PAGE_PRESENT)) { + KiPteStatInc(&gPteStats.UnmapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_NOT_FOUND; + } + uint64_t* e2 = KiPtePtr(*e3 & PAGE_FRAME_4K_MASK, PDT_IDX(virt)); + if (!(*e2 & PAGE_PRESENT)) { + KiPteStatInc(&gPteStats.UnmapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_NOT_FOUND; + } + uint64_t* pte = KiPtePtr(*e2 & PAGE_FRAME_4K_MASK, PT_IDX(virt)); + if (!(*pte & PAGE_PRESENT)) { + KiPteStatInc(&gPteStats.UnmapFailures); + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return STATUS_NOT_FOUND; + } + *pte = 0; + if (gPteStats.CurrentLeafMappings) + gPteStats.CurrentLeafMappings--; + + KeReleaseSpinlock(&gPageTableLock, lockFlags); + MmPteFlushKernelAddress(virt); + return STATUS_SUCCESS; +} + + +// Replaces leaf flags for a 4 KiB range and refuses huge pages instead of silently corrupting them. +__declspec(dllexport) KSTATUS MmPteProtectRange(uintptr_t virt, size_t size, uint64_t flags) { + KiPteStatInc(&gPteStats.ProtectCalls); + if (!size) return STATUS_SUCCESS; + + // The protection operation is page-granular even when callers pass an interior address. + uintptr_t base = virt & PAGE_MASK; + uintptr_t end = (virt + size + PAGE_SIZE - 1) & PAGE_MASK; + + uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); + KSTATUS status = STATUS_SUCCESS; + uintptr_t flushEnd = base; + + for (uintptr_t va = base; va < end; va += PAGE_SIZE) { + uint64_t* e4 = KiPtePtr(gPml4Phys, PML4_IDX(va)); + if (!(*e4 & PAGE_PRESENT)) { + status = STATUS_NOT_FOUND; + break; + } + uint64_t* e3 = KiPtePtr(*e4 & PAGE_FRAME_4K_MASK, PDPT_IDX(va)); + if (!(*e3 & PAGE_PRESENT)) { + status = STATUS_NOT_FOUND; + break; + } + if (*e3 & PAGE_PS) { + status = STATUS_NOT_SUPPORTED; + break; + } + uint64_t* e2 = KiPtePtr(*e3 & PAGE_FRAME_4K_MASK, PDT_IDX(va)); + if (!(*e2 & PAGE_PRESENT)) { + status = STATUS_NOT_FOUND; + break; + } + if (*e2 & PAGE_PS) { + status = STATUS_NOT_SUPPORTED; + break; + } + uint64_t* pte = KiPtePtr(*e2 & PAGE_FRAME_4K_MASK, PT_IDX(va)); + if (!(*pte & PAGE_PRESENT)) { + status = STATUS_NOT_FOUND; + break; + } + + // The physical frame stays attached while access and cache attributes change. + PHYSICAL_ADDRESS phys = *pte & PAGE_FRAME_4K_MASK; + *pte = phys | flags; + flushEnd = va + PAGE_SIZE; + } + + if (!KSUCCESS(status)) + KiPteStatInc(&gPteStats.ProtectFailures); + + KeReleaseSpinlock(&gPageTableLock, lockFlags); + + for (uintptr_t va = base; va < flushEnd; va += PAGE_SIZE) + MmPteFlushKernelAddress(va); + + return status; +} + + +// Tests and clears the hardware accessed bit on a mapped 4 KiB leaf. This is +// used by the pressure trimmer as a second-chance test for clean file-backed +// pages. If the page was accessed recently, reclaim skips it once and flushes +// the TLB entry so future accesses can set PAGE_ACCESSED again. +__declspec(dllexport) int MmPteTestAndClearAccessed(uintptr_t virt) { + if (!gPml4Phys) + return 0; + + uintptr_t va = virt & PAGE_MASK; + int wasAccessed = 0; + + uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); + + uint64_t* e4 = KiPtePtr(gPml4Phys, PML4_IDX(va)); + if (!(*e4 & PAGE_PRESENT)) + goto out; + uint64_t* e3 = KiPtePtr(*e4 & PAGE_FRAME_4K_MASK, PDPT_IDX(va)); + if (!(*e3 & PAGE_PRESENT) || (*e3 & PAGE_PS)) + goto out; + uint64_t* e2 = KiPtePtr(*e3 & PAGE_FRAME_4K_MASK, PDT_IDX(va)); + if (!(*e2 & PAGE_PRESENT) || (*e2 & PAGE_PS)) + goto out; + uint64_t* pte = KiPtePtr(*e2 & PAGE_FRAME_4K_MASK, PT_IDX(va)); + if (!(*pte & PAGE_PRESENT)) + goto out; + + if (*pte & PAGE_ACCESSED) { + *pte &= ~PAGE_ACCESSED; + wasAccessed = 1; + } + +out: + KeReleaseSpinlock(&gPageTableLock, lockFlags); + + if (wasAccessed) + MmPteFlushKernelAddress(va); + return wasAccessed; +} + +// Tests and clears the hardware dirty bit on a mapped 4 KiB leaf. +__declspec(dllexport) int MmPteTestAndClearDirty(uintptr_t virt) { + if (!gPml4Phys) + return 0; + + uintptr_t va = virt & PAGE_MASK; + int wasDirty = 0; + + uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); + + uint64_t* e4 = KiPtePtr(gPml4Phys, PML4_IDX(va)); + if (!(*e4 & PAGE_PRESENT)) + goto out; + uint64_t* e3 = KiPtePtr(*e4 & PAGE_FRAME_4K_MASK, PDPT_IDX(va)); + if (!(*e3 & PAGE_PRESENT) || (*e3 & PAGE_PS)) + goto out; + uint64_t* e2 = KiPtePtr(*e3 & PAGE_FRAME_4K_MASK, PDT_IDX(va)); + if (!(*e2 & PAGE_PRESENT) || (*e2 & PAGE_PS)) + goto out; + uint64_t* pte = KiPtePtr(*e2 & PAGE_FRAME_4K_MASK, PT_IDX(va)); + if (!(*pte & PAGE_PRESENT)) + goto out; + + if (*pte & PAGE_DIRTY) { + *pte &= ~PAGE_DIRTY; + wasDirty = 1; + } + +out: + KeReleaseSpinlock(&gPageTableLock, lockFlags); + + if (wasDirty) + MmPteFlushKernelAddress(va); + return wasDirty; +} + +// Translates a mapped virtual address to a physical address. +__declspec(dllexport) PHYSICAL_ADDRESS MmPteTranslate(uintptr_t virt) { + KiPteStatInc(&gPteStats.TranslateCalls); + if (!gPml4Phys) return 0; + + PHYSICAL_ADDRESS result = 0; + uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); + + uint64_t pml4_idx = (virt >> 39) & 0x1FF; + uint64_t pdpt_idx = (virt >> 30) & 0x1FF; + uint64_t pd_idx = (virt >> 21) & 0x1FF; + uint64_t pt_idx = (virt >> 12) & 0x1FF; + + uint64_t* pml4 = KiPhysToVirt(gPml4Phys); + if (!(pml4[pml4_idx] & PAGE_PRESENT)) goto out; + + uint64_t* pdpt = KiPhysToVirt(pml4[pml4_idx] & PAGE_FRAME_4K_MASK); + if (!(pdpt[pdpt_idx] & PAGE_PRESENT)) goto out; + + // A present PDPT leaf maps a 1 GiB huge page. + if (pdpt[pdpt_idx] & PAGE_PS) { + result = (pdpt[pdpt_idx] & PAGE_FRAME_1G_MASK) + (virt & 0x3FFFFFFF); + goto out; + } + + uint64_t* pd = KiPhysToVirt(pdpt[pdpt_idx] & PAGE_FRAME_4K_MASK); + if (!(pd[pd_idx] & PAGE_PRESENT)) goto out; + + // A present PDT leaf maps a 2 MiB huge page. + if (pd[pd_idx] & PAGE_PS) { + result = (pd[pd_idx] & PAGE_FRAME_2M_MASK) + (virt & 0x1FFFFF); + goto out; + } + + uint64_t* pt = KiPhysToVirt(pd[pd_idx] & PAGE_FRAME_4K_MASK); + if (!(pt[pt_idx] & PAGE_PRESENT)) goto out; + + // A normal PT leaf maps a 4 KiB page. + result = (pt[pt_idx] & PAGE_FRAME_4K_MASK) + (virt & 0xFFF); + +out: + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return result; +} + +// Queries one virtual address through the shared page-table hierarchy. +KSTATUS MmPteQueryPageFor(MM_PTE_QUERY_PURPOSE purpose, + uintptr_t virt, + PHYSICAL_ADDRESS* physOut, + uint64_t* flagsOut) { + purpose = KiNormalizeQueryPurpose(purpose); + KiPteStatInc(&gPteStats.QueryCalls); + KiPteStatInc(&gPteStats.QueryByPurpose[purpose]); + if (!gPml4Phys) { + KiPteStatInc(&gPteStats.QueryFailures); + KiPteStatInc(&gPteStats.QueryFailuresByPurpose[purpose]); + return STATUS_NOT_FOUND; + } + + KSTATUS status = STATUS_NOT_FOUND; + uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); + + uint64_t* pml4 = (uint64_t*)KiPhysToVirt(gPml4Phys); + uint64_t e4 = pml4[PML4_IDX(virt)]; + if (!(e4 & PAGE_PRESENT)) goto out; + + uint64_t effectiveWrite = e4 & PAGE_WRITE; + uint64_t effectiveUser = e4 & PAGE_USER; + uint64_t effectiveNx = e4 & PAGE_NX; + + uint64_t* pdpt = (uint64_t*)KiPhysToVirt(e4 & PAGE_FRAME_4K_MASK); + uint64_t e3 = pdpt[PDPT_IDX(virt)]; + if (!(e3 & PAGE_PRESENT)) goto out; + effectiveWrite &= e3 & PAGE_WRITE; + effectiveUser &= e3 & PAGE_USER; + effectiveNx |= e3 & PAGE_NX; + + if (e3 & PAGE_PS) { + if (physOut) + *physOut = (e3 & PAGE_FRAME_1G_MASK) + (virt & 0x3FFFFFFFULL); + if (flagsOut) { + uint64_t flags = (e3 & PAGE_FLAGS_MASK) | effectiveNx; + if (!effectiveWrite) flags &= ~PAGE_WRITE; + if (!effectiveUser) flags &= ~PAGE_USER; + *flagsOut = flags; + } + status = STATUS_SUCCESS; + goto out; + } + + uint64_t* pd = (uint64_t*)KiPhysToVirt(e3 & PAGE_FRAME_4K_MASK); + uint64_t e2 = pd[PDT_IDX(virt)]; + if (!(e2 & PAGE_PRESENT)) goto out; + effectiveWrite &= e2 & PAGE_WRITE; + effectiveUser &= e2 & PAGE_USER; + effectiveNx |= e2 & PAGE_NX; + + if (e2 & PAGE_PS) { + if (physOut) + *physOut = (e2 & PAGE_FRAME_2M_MASK) + (virt & 0x1FFFFFULL); + if (flagsOut) { + uint64_t flags = (e2 & PAGE_FLAGS_MASK) | effectiveNx; + if (!effectiveWrite) flags &= ~PAGE_WRITE; + if (!effectiveUser) flags &= ~PAGE_USER; + *flagsOut = flags; + } + status = STATUS_SUCCESS; + goto out; + } + + uint64_t* pt = (uint64_t*)KiPhysToVirt(e2 & PAGE_FRAME_4K_MASK); + uint64_t e1 = pt[PT_IDX(virt)]; + if (!(e1 & PAGE_PRESENT)) goto out; + effectiveWrite &= e1 & PAGE_WRITE; + effectiveUser &= e1 & PAGE_USER; + effectiveNx |= e1 & PAGE_NX; + + if (physOut) + *physOut = (e1 & PAGE_FRAME_4K_MASK) + (virt & 0xFFFULL); + if (flagsOut) { + uint64_t flags = (e1 & PAGE_FLAGS_MASK) | effectiveNx; + if (!effectiveWrite) flags &= ~PAGE_WRITE; + if (!effectiveUser) flags &= ~PAGE_USER; + *flagsOut = flags; + } + status = STATUS_SUCCESS; + +out: + if (!KSUCCESS(status)) { + KiPteStatInc(&gPteStats.QueryFailures); + KiPteStatInc(&gPteStats.QueryFailuresByPurpose[purpose]); + } + KeReleaseSpinlock(&gPageTableLock, lockFlags); + return status; +} + +// Queries one virtual address without caller-specific attribution. +__declspec(dllexport) KSTATUS MmPteQueryPage(uintptr_t virt, + PHYSICAL_ADDRESS* physOut, + uint64_t* flagsOut) { + return MmPteQueryPageFor(MmPteQueryGeneric, virt, physOut, flagsOut); +} + +// Returns page-table telemetry for diagnostics. +void MmPteQueryStats(MM_PTE_STATS* out) { + if (!out) + return; + *out = gPteStats; +} + + diff --git a/code/kernel/mm_swap.c b/code/kernel/mm_swap.c new file mode 100644 index 0000000..be1fb39 --- /dev/null +++ b/code/kernel/mm_swap.c @@ -0,0 +1,243 @@ +// Kernel swap manager. +// The first backend is a fixed-size pagefile accessed through VFS offset I/O. +// Kernel memory and filesystem code remain non-pageable, so this layer is safe +// to use before the VMM has direct block-extents for pageout. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct MM_SWAP_STATE { + SPINLOCK Lock; + FILE_OBJECT* File; + uint8_t* Bitmap; + uint32_t SlotCount; + uint32_t UsedSlots; + uint32_t HighWaterSlots; + int Enabled; + MM_SWAP_STATS Stats; +} MM_SWAP_STATE; + +static MM_SWAP_STATE gSwap = { .Lock = SPINLOCK_INIT }; + +// Returns whether a swap slot is marked in use. +static int KiSwapTestSlot(uint32_t slot) { + return (gSwap.Bitmap[slot >> 3] & (uint8_t)(1u << (slot & 7))) != 0; +} + +// Marks a swap slot as allocated or free. +static void KiSwapSetSlot(uint32_t slot, int used) { + uint8_t mask = (uint8_t)(1u << (slot & 7)); + if (used) + gSwap.Bitmap[slot >> 3] |= mask; + else + gSwap.Bitmap[slot >> 3] &= (uint8_t)~mask; +} + +// Allocates one pagefile slot. +static KSTATUS KiSwapAllocateSlot(uint32_t* slotOut) { + if (!slotOut) return STATUS_INVALID_PARAMETER; + *slotOut = 0; + + uintptr_t flags = KeAcquireSpinlock(&gSwap.Lock); + if (!gSwap.Enabled || !gSwap.Bitmap || !gSwap.SlotCount) { + KeReleaseSpinlock(&gSwap.Lock, flags); + return STATUS_NOT_SUPPORTED; + } + + for (uint32_t slot = 0; slot < gSwap.SlotCount; ++slot) { + if (!KiSwapTestSlot(slot)) { + KiSwapSetSlot(slot, 1); + gSwap.UsedSlots++; + if (gSwap.UsedSlots > gSwap.HighWaterSlots) + gSwap.HighWaterSlots = gSwap.UsedSlots; + gSwap.Stats.SlotAllocations++; + *slotOut = slot; + KeReleaseSpinlock(&gSwap.Lock, flags); + return STATUS_SUCCESS; + } + } + + gSwap.Stats.SlotAllocationFailures++; + KeReleaseSpinlock(&gSwap.Lock, flags); + return STATUS_NO_MEMORY; +} + +// Initializes the fixed pagefile backend. +KSTATUS MmSwapInitialize(SYS_STRING_VIEW path, uint64_t sizeBytes) { + if (!path.Data || !path.Length || sizeBytes < PAGE_SIZE) + return STATUS_INVALID_PARAMETER; + if (sizeBytes & (PAGE_SIZE - 1)) + sizeBytes = (sizeBytes + PAGE_SIZE - 1) & ~(uint64_t)(PAGE_SIZE - 1); + + uintptr_t flags = KeAcquireSpinlock(&gSwap.Lock); + if (gSwap.Enabled) { + KeReleaseSpinlock(&gSwap.Lock, flags); + return STATUS_SUCCESS; + } + KeReleaseSpinlock(&gSwap.Lock, flags); + + FILE_OBJECT* file = (FILE_OBJECT*)0; + KSTATUS s = IoOpenFile(path, &file); + if (!KSUCCESS(s)) { + s = IoCreateFile(path, &file); + if (!KSUCCESS(s)) + return s; + } + + // Swap slots are never read before MmSwapPageOut overwrites the whole + // page, so the filesystem may allocate backing clusters without zeroing + // them. Normal files still get zero-on-extend semantics. + file->Flags |= IO_FILE_FLAG_NO_CACHE | IO_FILE_FLAG_SKIP_ZERO_EXTEND; + + s = IoTruncateFile(file, sizeBytes); + if (!KSUCCESS(s)) { + IoCloseFile(file); + return s; + } + + uint64_t slots64 = sizeBytes / PAGE_SIZE; + if (!slots64 || slots64 > 0xFFFFFFFFull) { + IoCloseFile(file); + return STATUS_INVALID_PARAMETER; + } + + uint32_t slotCount = (uint32_t)slots64; + size_t bitmapBytes = (slotCount + 7u) / 8u; + uint8_t* bitmap = (uint8_t*)MmAllocatePoolTag(bitmapBytes, MM_POOL_TAG('s', 'w', 'a', 'p')); + if (!bitmap) { + IoCloseFile(file); + return STATUS_NO_MEMORY; + } + memset(bitmap, 0, bitmapBytes); + + flags = KeAcquireSpinlock(&gSwap.Lock); + if (gSwap.Enabled) { + KeReleaseSpinlock(&gSwap.Lock, flags); + MmFreePool(bitmap); + IoCloseFile(file); + return STATUS_SUCCESS; + } + + gSwap.File = file; + gSwap.Bitmap = bitmap; + gSwap.SlotCount = slotCount; + gSwap.UsedSlots = 0; + gSwap.HighWaterSlots = 0; + gSwap.Enabled = 1; + memset(&gSwap.Stats, 0, sizeof(gSwap.Stats)); + gSwap.Stats.Enabled = 1; + gSwap.Stats.TotalSlots = slotCount; + gSwap.Stats.FreeSlots = slotCount; + KeReleaseSpinlock(&gSwap.Lock, flags); + + DbgPrint("swap: initialized %u slots (%llu KiB) at %v\n", + slotCount, + (unsigned long long)(sizeBytes / 1024), + path); + return STATUS_SUCCESS; +} + +// Returns whether swap is available for pageout. +int MmSwapIsEnabled(void) { + return gSwap.Enabled; +} + +// Writes one physical page to a newly allocated swap slot. +KSTATUS MmSwapPageOut(PHYSICAL_ADDRESS frame, uint32_t* slotOut) { + if (!slotOut || !frame) + return STATUS_INVALID_PARAMETER; + *slotOut = 0; + if (!gSwap.Enabled || !gSwap.File) + return STATUS_NOT_SUPPORTED; + + uint32_t slot = 0; + KSTATUS s = KiSwapAllocateSlot(&slot); + if (!KSUCCESS(s)) { + gSwap.Stats.PageOutFailures++; + return s; + } + + void* page = MmPfnToVirtual(frame); + if (!page) { + MmSwapFreeSlot(slot); + gSwap.Stats.PageOutFailures++; + return STATUS_FAILURE; + } + + size_t written = 0; + s = IoWriteFileAt(gSwap.File, (uint64_t)slot * PAGE_SIZE, page, PAGE_SIZE, &written); + if (!KSUCCESS(s) || written != PAGE_SIZE) { + MmSwapFreeSlot(slot); + gSwap.Stats.PageOutFailures++; + return KSUCCESS(s) ? STATUS_DEVICE_ERROR : s; + } + + gSwap.Stats.PageOuts++; + *slotOut = slot; + return STATUS_SUCCESS; +} + +// Reads one swap slot into an already allocated physical page. +KSTATUS MmSwapPageIn(uint32_t slot, PHYSICAL_ADDRESS frame) { + if (!frame) + return STATUS_INVALID_PARAMETER; + if (!gSwap.Enabled || !gSwap.File) + return STATUS_NOT_SUPPORTED; + + uintptr_t flags = KeAcquireSpinlock(&gSwap.Lock); + int valid = slot < gSwap.SlotCount && KiSwapTestSlot(slot); + KeReleaseSpinlock(&gSwap.Lock, flags); + if (!valid) { + gSwap.Stats.PageInFailures++; + return STATUS_NOT_FOUND; + } + + void* page = MmPfnToVirtual(frame); + if (!page) { + gSwap.Stats.PageInFailures++; + return STATUS_FAILURE; + } + + size_t read = 0; + KSTATUS s = IoReadFileAt(gSwap.File, (uint64_t)slot * PAGE_SIZE, page, PAGE_SIZE, &read); + if (!KSUCCESS(s) || read != PAGE_SIZE) { + gSwap.Stats.PageInFailures++; + return KSUCCESS(s) ? STATUS_DEVICE_ERROR : s; + } + + gSwap.Stats.PageIns++; + return STATUS_SUCCESS; +} + +// Releases a swap slot after the owning VAD page no longer references it. +void MmSwapFreeSlot(uint32_t slot) { + uintptr_t flags = KeAcquireSpinlock(&gSwap.Lock); + if (gSwap.Enabled && slot < gSwap.SlotCount && KiSwapTestSlot(slot)) { + KiSwapSetSlot(slot, 0); + if (gSwap.UsedSlots) + gSwap.UsedSlots--; + gSwap.Stats.SlotFrees++; + } + KeReleaseSpinlock(&gSwap.Lock, flags); +} + +// Copies swap telemetry for user diagnostics. +void MmSwapQueryStats(MM_SWAP_STATS* out) { + if (!out) return; + uintptr_t flags = KeAcquireSpinlock(&gSwap.Lock); + MM_SWAP_STATS stats = gSwap.Stats; + stats.Enabled = gSwap.Enabled ? 1 : 0; + stats.TotalSlots = gSwap.SlotCount; + stats.UsedSlots = gSwap.UsedSlots; + stats.FreeSlots = gSwap.SlotCount >= gSwap.UsedSlots ? gSwap.SlotCount - gSwap.UsedSlots : 0; + stats.HighWaterSlots = gSwap.HighWaterSlots; + KeReleaseSpinlock(&gSwap.Lock, flags); + *out = stats; +} diff --git a/code/kernel/mm_virt.c b/code/kernel/mm_virt.c index 716a47b..4fbd846 100644 --- a/code/kernel/mm_virt.c +++ b/code/kernel/mm_virt.c @@ -1,414 +1,36 @@ -// Kernel virtual-memory manager. This file owns kernel page-table construction, mapping attributes, and direct-map maintenance. - -// Page tables: -// One PML4 shared by all processes. -// Upper half (bit 47 set) has U/S = 0 - ring-3 access always faults. -// Lower half pages are PAGE_USER. +// Kernel virtual-memory manager. This file owns kernel pool allocation, vad bookkeeping, and user-copy validation. +// Page-table construction lives in mm_pte.c so the higher VMM can grow toward VADs, faults, COW, and swap without mixing those policies with raw PTE edits. // Kernel pool: -// A boundary-tag (header + footer) free-list allocator starting at POOL_BASE. -// Pages are committed on demand from the PMM. +// A boundary-tag allocator starts at POOL_BASE and commits pages on demand. -// User heap: -// A single best-fit free list in the lower half. -// Page-granular lower-half regions owned by PROCESS metadata. Sub-page -// allocation is a user-mode runtime concern. +// User mappings: +// VADs separate reservation from commitment. Committed pages become resident +// when a user fault or kernel prefault path materializes them. #include #include #include #include +#include +#include +#include +#include +#include +#include +#include #include -#include #include #include #include -#include +#include -// Helpers - physical window access. -static uint64_t gPhysBase = PHYS_MAP_BASE; - -// Implements the phys To Virt operation. -static inline void* KiPhysToVirt(PHYSICAL_ADDRESS p) { - return (void *)(p + gPhysBase); -} -// Implements the pte Ptr operation. -static inline uint64_t* KiPtePtr(PHYSICAL_ADDRESS tablePhys, unsigned idx) { - return (uint64_t *)KiPhysToVirt(tablePhys) + idx; -} - -// PML4 / page-table helpers. -static PHYSICAL_ADDRESS gPml4Phys = 0; -static SPINLOCK gPageTableLock = SPINLOCK_INIT; - -static volatile long gTlbShootdownLock = 0; -static volatile long gTlbShootdownActive = 0; -static volatile long gTlbShootdownAcks = 0; -static uintptr_t gTlbShootdownVirt = 0; - -#define TLB_SHOOTDOWN_TIMEOUT_SPINS 20000000u - -// Extract the page-table index for each level from a virtual address. -#define PML4_IDX(va) (((va) >> 39) & 0x1FF) -#define PDPT_IDX(va) (((va) >> 30) & 0x1FF) -#define PDT_IDX(va) (((va) >> 21) & 0x1FF) -#define PT_IDX(va) (((va) >> 12) & 0x1FF) - -#define PAGE_PS (1ULL << 7) -#define PAGE_FLAGS_MASK (0xFFFULL | PAGE_NX) -#define PAGE_FRAME_4K_MASK 0x000FFFFFFFFFF000ULL -#define PAGE_FRAME_2M_MASK 0x000FFFFFFFE00000ULL -#define PAGE_FRAME_1G_MASK 0x000FFFFFC0000000ULL - -// Ensure a page-table entry at 'parentPhys[index]' points to a child table. -// Allocates and zeros a new frame if the entry is not present. -// Returns the physical address of the child table, or 0 on OOM. -static PHYSICAL_ADDRESS KiEnsureTable(PHYSICAL_ADDRESS parentPhys, - unsigned index, uint64_t extraFlags) { - uint64_t* entry = KiPtePtr(parentPhys, index); - if (*entry & PAGE_PRESENT) { - *entry |= extraFlags; - return *entry & PAGE_FRAME_4K_MASK; - } - PHYSICAL_ADDRESS childPhys = MmAllocateFrame(); - if (!childPhys) return 0; - memset(KiPhysToVirt(childPhys), 0, PAGE_SIZE); - *entry = childPhys | PAGE_PRESENT | PAGE_WRITE | extraFlags; - return childPhys; -} - -// TLB/cache coherency hooks. -void MmFlushTlbLocal(uintptr_t virt) { - __invlpg((void*)virt); -} - -// Implements the acquire Tlb Shootdown Lock operation. -static void KiAcquireTlbShootdownLock(void) { - // Do not use the normal spinlock here: it masks interrupts while spinning. - // A CPU spinning with IF=0 on the shootdown lock could not acknowledge a - // concurrent fixed-vector TLB IPI, creating the exact deadlock this lock is - // meant to avoid. - while (_InterlockedCompareExchange(&gTlbShootdownLock, 1, 0) != 0) - _mm_pause(); - _ReadWriteBarrier(); -} - -// Releases tlb Shootdown Lock. -static void KiReleaseTlbShootdownLock(void) { - _ReadWriteBarrier(); - _InterlockedExchange(&gTlbShootdownLock, 0); -} - -// Flushes tlb For Kernel Mapping. -void MmFlushTlbForKernelMapping(uintptr_t virt) { - MmFlushTlbLocal(virt); - - if (gCpuCount <= 1) - return; - - KCPU* self = KeGetCurrentCpu(); - uint32_t selfIndex = self ? self->CpuIndex : KE_INVALID_CPU; - - KiAcquireTlbShootdownLock(); - - long targets = 0; - gTlbShootdownVirt = virt; - _InterlockedExchange(&gTlbShootdownAcks, 0); - _ReadWriteBarrier(); - _InterlockedExchange(&gTlbShootdownActive, 1); - _ReadWriteBarrier(); - - for (uint32_t i = 0; i < gCpuCount; i++) { - KCPU* cpu = &gCpus[i]; - if (i == selfIndex) - continue; - if (!cpu->Present || !cpu->Started || !cpu->Online) - continue; - targets++; - HalApicSendTlbShootdownIpi(cpu->LapicId); - } - - for (uint32_t spin = 0; targets > 0 && spin < TLB_SHOOTDOWN_TIMEOUT_SPINS; spin++) { - if (_InterlockedCompareExchange(&gTlbShootdownAcks, 0, 0) >= targets) - break; - _mm_pause(); - } - - long acks = _InterlockedCompareExchange(&gTlbShootdownAcks, 0, 0); - _InterlockedExchange(&gTlbShootdownActive, 0); - _ReadWriteBarrier(); - KiReleaseTlbShootdownLock(); - - if (acks < targets) { - KePanic(KE_PANIC_CODE_GENERIC, - "TLB shootdown timeout: va=0x%016llX targets=%ld acks=%ld selfCpu=%u\n", - (uint64_t)virt, targets, acks, selfIndex); - } -} - -// Handles tlb Shootdown. -static int KiHandleTlbShootdown(void) { - if (_InterlockedCompareExchange(&gTlbShootdownActive, 0, 0) == 0) - return 0; - - uintptr_t va = gTlbShootdownVirt; - _ReadWriteBarrier(); - MmFlushTlbLocal(va); - _ReadWriteBarrier(); - _InterlockedIncrement(&gTlbShootdownAcks); - return 1; -} - -// Handles tlb Shootdown Nmi. -int MmHandleTlbShootdownNmi(void) { - return KiHandleTlbShootdown(); -} - -// Public mapping. -__declspec(dllexport) KSTATUS MmMapPage(uintptr_t virt, PHYSICAL_ADDRESS phys, uint64_t flags, MM_MAP_MODE mode) { - unsigned userFlag = (flags & PAGE_USER) ? PAGE_USER : 0; - - uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); - - PHYSICAL_ADDRESS pdpt = KiEnsureTable(gPml4Phys, PML4_IDX(virt), userFlag); - if (!pdpt) { - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return STATUS_NO_MEMORY; - } - PHYSICAL_ADDRESS pdt = KiEnsureTable(pdpt, PDPT_IDX(virt), userFlag); - if (!pdt) { - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return STATUS_NO_MEMORY; - } - PHYSICAL_ADDRESS pt = KiEnsureTable(pdt, PDT_IDX(virt), userFlag); - if (!pt) { - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return STATUS_NO_MEMORY; - } - - uint64_t* pte = KiPtePtr(pt, PT_IDX(virt)); - if ((*pte & PAGE_PRESENT) && mode == MmMapFailIfPresent) { - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return STATUS_ALREADY_EXISTS; - } - *pte = phys | flags; - - KeReleaseSpinlock(&gPageTableLock, lockFlags); - MmFlushTlbForKernelMapping(virt); - return STATUS_SUCCESS; -} - - -// Releases page. -__declspec(dllexport) KSTATUS MmUnmapPage(uintptr_t virt) { - uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); - - uint64_t* e4 = KiPtePtr(gPml4Phys, PML4_IDX(virt)); - if (!(*e4 & PAGE_PRESENT)) { - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return STATUS_NOT_FOUND; - } - uint64_t* e3 = KiPtePtr(*e4 & PAGE_FRAME_4K_MASK, PDPT_IDX(virt)); - if (!(*e3 & PAGE_PRESENT)) { - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return STATUS_NOT_FOUND; - } - uint64_t* e2 = KiPtePtr(*e3 & PAGE_FRAME_4K_MASK, PDT_IDX(virt)); - if (!(*e2 & PAGE_PRESENT)) { - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return STATUS_NOT_FOUND; - } - uint64_t* pte = KiPtePtr(*e2 & PAGE_FRAME_4K_MASK, PT_IDX(virt)); - *pte = 0; - - KeReleaseSpinlock(&gPageTableLock, lockFlags); - MmFlushTlbForKernelMapping(virt); - return STATUS_SUCCESS; -} - - -// Changes page attributes for 4 KiB mappings and refuses huge-page leaves that cannot be edited in place. -__declspec(dllexport) KSTATUS MmSetPageProtect(uintptr_t virt, size_t size, uint64_t flags) { - if (!size) return STATUS_SUCCESS; - - // Round down to page boundary, round size up. - uintptr_t base = virt & PAGE_MASK; - uintptr_t end = (virt + size + PAGE_SIZE - 1) & PAGE_MASK; - - uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); - KSTATUS status = STATUS_SUCCESS; - uintptr_t flushEnd = base; - - for (uintptr_t va = base; va < end; va += PAGE_SIZE) { - uint64_t* e4 = KiPtePtr(gPml4Phys, PML4_IDX(va)); - if (!(*e4 & PAGE_PRESENT)) { - status = STATUS_NOT_FOUND; - break; - } - uint64_t* e3 = KiPtePtr(*e4 & PAGE_FRAME_4K_MASK, PDPT_IDX(va)); - if (!(*e3 & PAGE_PRESENT)) { - status = STATUS_NOT_FOUND; - break; - } - if (*e3 & PAGE_PS) { - status = STATUS_NOT_SUPPORTED; - break; - } - uint64_t* e2 = KiPtePtr(*e3 & PAGE_FRAME_4K_MASK, PDT_IDX(va)); - if (!(*e2 & PAGE_PRESENT)) { - status = STATUS_NOT_FOUND; - break; - } - if (*e2 & PAGE_PS) { - status = STATUS_NOT_SUPPORTED; - break; - } - uint64_t* pte = KiPtePtr(*e2 & PAGE_FRAME_4K_MASK, PT_IDX(va)); - if (!(*pte & PAGE_PRESENT)) { - status = STATUS_NOT_FOUND; - break; - } - - // Preserve the physical address, replace everything else. - PHYSICAL_ADDRESS phys = *pte & PAGE_FRAME_4K_MASK; - *pte = phys | flags; - flushEnd = va + PAGE_SIZE; - } - - KeReleaseSpinlock(&gPageTableLock, lockFlags); - - for (uintptr_t va = base; va < flushEnd; va += PAGE_SIZE) - MmFlushTlbForKernelMapping(va); - - return status; -} - - -// Returns physical Address. -__declspec(dllexport) PHYSICAL_ADDRESS MmGetPhysicalAddress(uintptr_t virt) { - if (!gPml4Phys) return 0; // Not initialized - - PHYSICAL_ADDRESS result = 0; - uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); - - uint64_t pml4_idx = (virt >> 39) & 0x1FF; - uint64_t pdpt_idx = (virt >> 30) & 0x1FF; - uint64_t pd_idx = (virt >> 21) & 0x1FF; - uint64_t pt_idx = (virt >> 12) & 0x1FF; - - uint64_t* pml4 = KiPhysToVirt(gPml4Phys); - if (!(pml4[pml4_idx] & PAGE_PRESENT)) goto out; - - uint64_t* pdpt = KiPhysToVirt(pml4[pml4_idx] & PAGE_FRAME_4K_MASK); - if (!(pdpt[pdpt_idx] & PAGE_PRESENT)) goto out; - - // Check for 1GB huge pages (bit 7 - PS). - if (pdpt[pdpt_idx] & PAGE_PS) { - result = (pdpt[pdpt_idx] & PAGE_FRAME_1G_MASK) + (virt & 0x3FFFFFFF); - goto out; - } - - uint64_t* pd = KiPhysToVirt(pdpt[pdpt_idx] & PAGE_FRAME_4K_MASK); - if (!(pd[pd_idx] & PAGE_PRESENT)) goto out; - - // Check for 2MB huge pages (bit 7 - PS). - if (pd[pd_idx] & PAGE_PS) { - result = (pd[pd_idx] & PAGE_FRAME_2M_MASK) + (virt & 0x1FFFFF); - goto out; - } - - uint64_t* pt = KiPhysToVirt(pd[pd_idx] & PAGE_FRAME_4K_MASK); - if (!(pt[pt_idx] & PAGE_PRESENT)) goto out; - - // Extract the physical frame and add the offset within the page. - result = (pt[pt_idx] & PAGE_FRAME_4K_MASK) + (virt & 0xFFF); - -out: - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return result; -} - - - -// Page query and user/kernel copy boundary helpers. -__declspec(dllexport) KSTATUS MmQueryPage(uintptr_t virt, - PHYSICAL_ADDRESS* physOut, - uint64_t* flagsOut) { - if (!gPml4Phys) return STATUS_NOT_FOUND; - - KSTATUS status = STATUS_NOT_FOUND; - uintptr_t lockFlags = KeAcquireSpinlock(&gPageTableLock); - - uint64_t* pml4 = (uint64_t*)KiPhysToVirt(gPml4Phys); - uint64_t e4 = pml4[PML4_IDX(virt)]; - if (!(e4 & PAGE_PRESENT)) goto out; - - uint64_t effectiveWrite = e4 & PAGE_WRITE; - uint64_t effectiveUser = e4 & PAGE_USER; - uint64_t effectiveNx = e4 & PAGE_NX; - - uint64_t* pdpt = (uint64_t*)KiPhysToVirt(e4 & PAGE_FRAME_4K_MASK); - uint64_t e3 = pdpt[PDPT_IDX(virt)]; - if (!(e3 & PAGE_PRESENT)) goto out; - effectiveWrite &= e3 & PAGE_WRITE; - effectiveUser &= e3 & PAGE_USER; - effectiveNx |= e3 & PAGE_NX; - - if (e3 & PAGE_PS) { - if (physOut) - *physOut = (e3 & PAGE_FRAME_1G_MASK) + (virt & 0x3FFFFFFFULL); - if (flagsOut) { - uint64_t flags = (e3 & PAGE_FLAGS_MASK) | effectiveNx; - if (!effectiveWrite) flags &= ~PAGE_WRITE; - if (!effectiveUser) flags &= ~PAGE_USER; - *flagsOut = flags; - } - status = STATUS_SUCCESS; - goto out; - } - - uint64_t* pd = (uint64_t*)KiPhysToVirt(e3 & PAGE_FRAME_4K_MASK); - uint64_t e2 = pd[PDT_IDX(virt)]; - if (!(e2 & PAGE_PRESENT)) goto out; - effectiveWrite &= e2 & PAGE_WRITE; - effectiveUser &= e2 & PAGE_USER; - effectiveNx |= e2 & PAGE_NX; - - if (e2 & PAGE_PS) { - if (physOut) - *physOut = (e2 & PAGE_FRAME_2M_MASK) + (virt & 0x1FFFFFULL); - if (flagsOut) { - uint64_t flags = (e2 & PAGE_FLAGS_MASK) | effectiveNx; - if (!effectiveWrite) flags &= ~PAGE_WRITE; - if (!effectiveUser) flags &= ~PAGE_USER; - *flagsOut = flags; - } - status = STATUS_SUCCESS; - goto out; - } - - uint64_t* pt = (uint64_t*)KiPhysToVirt(e2 & PAGE_FRAME_4K_MASK); - uint64_t e1 = pt[PT_IDX(virt)]; - if (!(e1 & PAGE_PRESENT)) goto out; - effectiveWrite &= e1 & PAGE_WRITE; - effectiveUser &= e1 & PAGE_USER; - effectiveNx |= e1 & PAGE_NX; - - if (physOut) - *physOut = (e1 & PAGE_FRAME_4K_MASK) + (virt & 0xFFFULL); - if (flagsOut) { - uint64_t flags = (e1 & PAGE_FLAGS_MASK) | effectiveNx; - if (!effectiveWrite) flags &= ~PAGE_WRITE; - if (!effectiveUser) flags &= ~PAGE_USER; - *flagsOut = flags; - } - status = STATUS_SUCCESS; - -out: - KeReleaseSpinlock(&gPageTableLock, lockFlags); - return status; -} +static KSTATUS KiVadEnsureResidentRange(PROCESS* process, uintptr_t start, size_t size, uint64_t accessFlags); +static KSTATUS KiCopyFromUserPaged(void* kernelDst, const void* userSrc, size_t size); +static KSTATUS KiCopyToUserPaged(void* userDst, const void* kernelSrc, size_t size); +static PHYSICAL_ADDRESS gVadZeroPageFrame; +// User/kernel copy boundary helpers. // Implements the user Range Valid operation. static int KiUserRangeValid(uintptr_t addr, size_t size) { if (size == 0) return 1; @@ -418,7 +40,7 @@ static int KiUserRangeValid(uintptr_t addr, size_t size) { return 1; } -// Probes user Range. +// Probes user Range and materializes demand-zero pages before the kernel copies through it. static KSTATUS KiProbeUserRange(uintptr_t addr, size_t size, uint64_t requiredFlags) { if (!KiUserRangeValid(addr, size)) return STATUS_ACCESS_DENIED; @@ -426,22 +48,11 @@ static KSTATUS KiProbeUserRange(uintptr_t addr, size_t size, uint64_t requiredFl if (size == 0) return STATUS_SUCCESS; - uintptr_t va = addr & PAGE_MASK; - uintptr_t last = (addr + size - 1) & PAGE_MASK; + PROCESS* process = KeGetCurrentProcess(); + if (!process) + return STATUS_ACCESS_DENIED; - for (;;) { - uint64_t flags = 0; - KSTATUS s = MmQueryPage(va, (PHYSICAL_ADDRESS*)0, &flags); - if (!KSUCCESS(s)) - return STATUS_ACCESS_DENIED; - if ((flags & requiredFlags) != requiredFlags) - return STATUS_ACCESS_DENIED; - if (va == last) - break; - va += PAGE_SIZE; - } - - return STATUS_SUCCESS; + return KiVadEnsureResidentRange(process, addr, size, requiredFlags); } // Probes user Read. @@ -454,26 +65,14 @@ __declspec(dllexport) KSTATUS MmProbeUserWrite(void* ptr, size_t size) { return KiProbeUserRange((uintptr_t)ptr, size, PAGE_PRESENT | PAGE_USER | PAGE_WRITE); } -// Copies from User. +// Copies from a user buffer through page-sized, pinned segments. __declspec(dllexport) KSTATUS CopyFromUser(void* kernelDst, const void* userSrc, size_t size) { - if (!kernelDst && size) return STATUS_INVALID_PARAMETER; - - KSTATUS s = MmProbeUserRead(userSrc, size); - if (!KSUCCESS(s)) return s; - - if (size) memcpy(kernelDst, userSrc, size); - return STATUS_SUCCESS; + return KiCopyFromUserPaged(kernelDst, userSrc, size); } -// Copies to User. +// Copies to a user buffer through page-sized, pinned segments. __declspec(dllexport) KSTATUS CopyToUser(void* userDst, const void* kernelSrc, size_t size) { - if (!kernelSrc && size) return STATUS_INVALID_PARAMETER; - - KSTATUS s = MmProbeUserWrite(userDst, size); - if (!KSUCCESS(s)) return s; - - if (size) memcpy(userDst, kernelSrc, size); - return STATUS_SUCCESS; + return KiCopyToUserPaged(userDst, kernelSrc, size); } // Boundary-tag allocator (used for both pool and user heap) @@ -483,12 +82,33 @@ __declspec(dllexport) KSTATUS CopyToUser(void* userDst, const void* kernelSrc, s #define BLOCK_MAGIC 0xB10CB10CUL +#define BLOCK_STATE_USED 0u +#define BLOCK_STATE_FREE 1u +#define BLOCK_STATE_QUARANTINE 2u + +#define BLOCK_FLAG_CANARY 0x00000001u +#define BLOCK_FLAG_POISONED 0x00000002u + +#define POOL_TAIL_CANARY_BASE 0xC0FFEE1234ABCDEFULL +#define POOL_POISON_FREE 0xDD +#define POOL_QUARANTINE_SLOTS 256u +#define POOL_TAG_DEFAULT 0x6C6F6F70u /* 'pool' */ + typedef struct BLOCK_HDR { uint32_t Magic; - uint32_t Free; // 1 = free, 0 = used - size_t Size; // usable bytes (excludes header + footer) + uint32_t Free; // BLOCK_STATE_* + size_t Size; // usable allocator bytes (excludes header + footer) struct BLOCK_HDR* PrevFree; struct BLOCK_HDR* NextFree; + + // Debug metadata. Present in all builds so boot.ini bits can enable + // diagnostics without changing allocator layout or ABI. + size_t RequestedSize; + uint32_t Tag; + uint32_t Flags; + uint64_t Sequence; + uintptr_t AllocCaller; + uintptr_t FreeCaller; } BLOCK_HDR; typedef struct BLOCK_FTR { @@ -519,43 +139,143 @@ static inline BLOCK_FTR* KiPrevFtr(BLOCK_HDR* h) { return (BLOCK_FTR *)((char *)h - FTR_SIZE); } -typedef struct HEAP { +typedef struct HEAP HEAP; + +struct HEAP { BLOCK_HDR* FreeList; // doubly-linked list of free blocks uintptr_t Start; // virtual base of the arena uintptr_t Brk; // current committed end (page-aligned) uintptr_t Limit; // hard upper bound uint64_t MapFlags; // PAGE_* flags for new committed pages SPINLOCK Lock; -} HEAP; + int DecommitTailOnFree; -// Implements the heap Ensure Mapped operation. -static int KiHeapEnsureMapped(HEAP* h, uintptr_t start, size_t size) { - if (!h || !size) return 1; - if (start < h->Start) return 0; - if (start > (uintptr_t)-1 - (size - 1)) return 0; + uint64_t NextSequence; + BLOCK_HDR* Quarantine[POOL_QUARANTINE_SLOTS]; + size_t QuarantineCursor; + size_t QuarantineCount; +}; - uintptr_t begin = start & PAGE_MASK; - uintptr_t end = (start + size + PAGE_SIZE - 1) & PAGE_MASK; - if (end < begin || end > h->Limit) return 0; +static inline uint32_t KiPoolDebugFlags(void) { + return KeGetDebugFlags() & (KE_DEBUG_POOL_CANARY | + KE_DEBUG_POOL_POISON | + KE_DEBUG_POOL_QUARANTINE | + KE_DEBUG_POOL_CALLERS); +} - for (uintptr_t va = begin; va < end; va += PAGE_SIZE) { - PHYSICAL_ADDRESS phys = 0; - uint64_t flags = 0; - KSTATUS qs = MmQueryPage(va, &phys, &flags); - if (KSUCCESS(qs) && (flags & PAGE_PRESENT)) - continue; +static inline uint64_t KiPoolTailCanaryValue(const BLOCK_HDR* blk) { + return POOL_TAIL_CANARY_BASE ^ + ((uint64_t)(uintptr_t)blk << 13) ^ + ((uint64_t)blk->RequestedSize << 1) ^ + blk->Sequence; +} - PHYSICAL_ADDRESS frame = MmAllocateFrame(); - if (!frame) return 0; - KSTATUS ms = MmMapPage(va, frame, h->MapFlags, MmMapFailIfPresent); - if (!KSUCCESS(ms)) { - MmFreeFrame(frame); - return 0; - } - memset((void*)va, 0, PAGE_SIZE); - DbgPrint("MM: repaired missing heap page va=%016llx\n", (unsigned long long)va); +static inline uint8_t* KiBlockUserBase(BLOCK_HDR* blk) { + return (uint8_t*)blk + HDR_SIZE; +} + +static inline uint64_t* KiBlockTailCanary(BLOCK_HDR* blk) { + return (uint64_t*)(void*)(KiBlockUserBase(blk) + blk->RequestedSize); +} + +static const char* KiPoolStateName(uint32_t state) { + switch (state) { + case BLOCK_STATE_USED: return "used"; + case BLOCK_STATE_FREE: return "free"; + case BLOCK_STATE_QUARANTINE: return "quarantine"; + default: return "bad-state"; + } +} + +static _Noreturn void KiPoolPanic(const char* reason, HEAP* h, BLOCK_HDR* blk, const void* ptr) { + KePanic(KE_PANIC_CODE_GENERIC, + "MM: pool corruption: %s ptr=%p hdr=%p heap=[%016llx,%016llx) magic=%08x state=%u(%s) size=%llu req=%llu tag=%08x flags=%08x seq=%llu alloc=%p free=%p\n", + reason ? reason : "unknown", + ptr, + blk, + (unsigned long long)(h ? h->Start : 0), + (unsigned long long)(h ? h->Brk : 0), + blk ? blk->Magic : 0, + blk ? blk->Free : 0, + blk ? KiPoolStateName(blk->Free) : "", + (unsigned long long)(blk ? blk->Size : 0), + (unsigned long long)(blk ? blk->RequestedSize : 0), + blk ? blk->Tag : 0, + blk ? blk->Flags : 0, + (unsigned long long)(blk ? blk->Sequence : 0), + blk ? (void*)blk->AllocCaller : (void*)0, + blk ? (void*)blk->FreeCaller : (void*)0); +} + +static void KiPoolValidateFooterLocked(HEAP* h, BLOCK_HDR* blk, const char* where) { + if (!h || !blk) + KiPoolPanic(where, h, blk, blk ? KiBlockUserBase(blk) : (void*)0); + if (blk->Magic != BLOCK_MAGIC) + KiPoolPanic(where, h, blk, KiBlockUserBase(blk)); + if ((uintptr_t)blk < h->Start || (uintptr_t)blk >= h->Brk) + KiPoolPanic("block outside heap", h, blk, KiBlockUserBase(blk)); + if (blk->Size > (size_t)(h->Brk - (uintptr_t)blk - OVERHEAD)) + KiPoolPanic("block size outside heap", h, blk, KiBlockUserBase(blk)); + BLOCK_FTR* ftr = KiHdrToFtr(blk); + if ((uintptr_t)ftr + FTR_SIZE > h->Brk) + KiPoolPanic("footer outside heap", h, blk, KiBlockUserBase(blk)); + if (ftr->Magic != BLOCK_MAGIC || ftr->Size != blk->Size || ftr->Free != blk->Free) + KiPoolPanic(where, h, blk, KiBlockUserBase(blk)); +} + +static void KiPoolWriteTailCanary(BLOCK_HDR* blk) { + if (!blk || !(blk->Flags & BLOCK_FLAG_CANARY)) + return; + *KiBlockTailCanary(blk) = KiPoolTailCanaryValue(blk); +} + +static void KiPoolValidateTailCanaryLocked(HEAP* h, BLOCK_HDR* blk, const char* where) { + if (!blk || !(blk->Flags & BLOCK_FLAG_CANARY)) + return; + if (blk->RequestedSize + sizeof(uint64_t) > blk->Size) + KiPoolPanic("invalid canary layout", h, blk, KiBlockUserBase(blk)); + uint64_t actual = *KiBlockTailCanary(blk); + uint64_t expect = KiPoolTailCanaryValue(blk); + if (actual != expect) + KiPoolPanic(where, h, blk, KiBlockUserBase(blk)); +} + +static void KiPoolPoisonFreedPayload(BLOCK_HDR* blk) { + if (!blk) + return; + size_t bytes = blk->RequestedSize; + if (bytes > blk->Size) + bytes = blk->Size; + memset(KiBlockUserBase(blk), POOL_POISON_FREE, bytes); + blk->Flags |= BLOCK_FLAG_POISONED; + KiPoolWriteTailCanary(blk); +} + +static void KiPoolValidateFreedPoisonLocked(HEAP* h, BLOCK_HDR* blk, const char* where) { + if (!blk || !(blk->Flags & BLOCK_FLAG_POISONED)) + return; + size_t bytes = blk->RequestedSize; + if (bytes > blk->Size) + bytes = blk->Size; + const uint8_t* p = KiBlockUserBase(blk); + for (size_t i = 0; i < bytes; i++) { + if (p[i] != POOL_POISON_FREE) + KiPoolPanic(where, h, blk, KiBlockUserBase(blk)); + } +} + +// Rolls back a failed heap grow attempt. KiHeapGrow maps pages before the +// new free block is linked into the allocator; if a later frame allocation or +// page-table allocation fails, those earlier mappings must be returned or the +// pool silently leaks physical frames and leaves stale mappings below Brk. +static void KiHeapRollbackGrow(uintptr_t start, uintptr_t end) { + for (uintptr_t va = start; va < end; va += PAGE_SIZE) { + PHYSICAL_ADDRESS frame = MmPteTranslate(va); + if (!frame) + continue; + if (KSUCCESS(MmPteUnmapPage(va))) + (void)MmPfnFree(frame); } - return 1; } // Extend heap by at least 'need' bytes (rounded up to page boundary). @@ -563,33 +283,71 @@ static int KiHeapGrow(HEAP* h, size_t need) { size_t grow = (need + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); if (h->Brk + grow > h->Limit) return 0; - for (uintptr_t va = h->Brk; va < h->Brk + grow; va += PAGE_SIZE) { - PHYSICAL_ADDRESS frame = MmAllocateFrame(); - if (!frame) return 0; - if (!KSUCCESS(MmMapPage(va, frame, h->MapFlags, MmMapFailIfPresent))) { - MmFreeFrame(frame); + uintptr_t start = h->Brk; + uintptr_t mappedEnd = start; + for (uintptr_t va = start; va < start + grow; va += PAGE_SIZE) { + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseKernel); + if (!frame) { + KiHeapRollbackGrow(start, mappedEnd); return 0; } + if (!KSUCCESS(MmPteMapPage(va, frame, h->MapFlags, MmPteMapFailIfPresent))) { + MmPfnFree(frame); + KiHeapRollbackGrow(start, mappedEnd); + return 0; + } + mappedEnd = va + PAGE_SIZE; memset((void *)va, 0, PAGE_SIZE); } - if (!KiHeapEnsureMapped(h, h->Brk, grow)) - return 0; + // The grow path owns the only transition from reserved heap VA to mapped heap + // pages, so a successful loop above is the heap's committed-frontier proof. + // Re-querying every heap page on later allocations only burns page-table locks. + // Missing pool pages are corruption now, not something the allocator repairs. + + uintptr_t oldBrk = h->Brk; + uintptr_t newBrk = oldBrk + grow; + + // If the current tail block is already free, extend it in-place instead of + // leaving adjacent free blocks. This matters once tail decommit is enabled: + // a later grow should rebuild the same tail block, not fragment the arena + // into many page-sized free runs. + if (oldBrk > h->Start) { + BLOCK_FTR* prevFtr = (BLOCK_FTR*)(oldBrk - FTR_SIZE); + if (prevFtr->Magic == BLOCK_MAGIC && prevFtr->Free == BLOCK_STATE_FREE) { + BLOCK_HDR* prev = KiFtrToHdr(prevFtr); + if ((uintptr_t)prev >= h->Start && prev->Magic == BLOCK_MAGIC && prev->Free == BLOCK_STATE_FREE) { + prev->Size += grow; + h->Brk = newBrk; + KiHdrToFtr(prev)->Magic = BLOCK_MAGIC; + KiHdrToFtr(prev)->Free = BLOCK_STATE_FREE; + KiHdrToFtr(prev)->Size = prev->Size; + return 1; + } + } + } // Make the new pages a single free block. - BLOCK_HDR* blk = (BLOCK_HDR *)h->Brk; + BLOCK_HDR* blk = (BLOCK_HDR *)oldBrk; blk->Magic = BLOCK_MAGIC; - blk->Free = 1; + blk->Free = BLOCK_STATE_FREE; blk->Size = grow - OVERHEAD; blk->PrevFree = (BLOCK_HDR *)0; blk->NextFree = h->FreeList; + blk->RequestedSize = 0; + blk->Tag = 0; + blk->Flags = 0; + blk->Sequence = 0; + blk->AllocCaller = 0; + blk->FreeCaller = 0; if (h->FreeList) h->FreeList->PrevFree = blk; h->FreeList = blk; + + h->Brk = newBrk; KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; - KiHdrToFtr(blk)->Free = 1; + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; KiHdrToFtr(blk)->Size = blk->Size; - h->Brk += grow; return 1; } @@ -603,41 +361,201 @@ static void KiFreeListRemove(HEAP* h, BLOCK_HDR* blk) { // Implements the heap Try Decommit Tail operation. static void KiHeapTryDecommitTail(HEAP* h, BLOCK_HDR* blk) { - (void)h; - if (!blk || blk->Free == 0) return; + if (!h || !blk || blk->Free == BLOCK_STATE_USED) return; - // Do not decommit kernel-pool tail pages for now. The pool allocator keeps - // free-list nodes and boundary tags inside the managed arena; shrinking the - // mapped tail while large launch-failure cleanup is walking/freeing many - // objects can leave stale metadata pointers into unmapped pages. Physical - // pages are still reclaimed for user regions by MmDestroyUserRegion(); pool - // pages remain committed until we have a dedicated, audited heap shrinker. + // Do not eagerly decommit the kernel pool on every free. Large transient + // syscall bounce buffers, especially SysBlitScreen's frame-sized buffer, + // are often allocated/freed once per frame. If the tail block is unmapped + // immediately, the next frame has to map the same pages again, producing + // massive page-table churn and global TLB shootdowns. Keep the committed + // pool high-water resident during normal operation; add an explicit idle or + // pressure pool trim later if needed. + if (!h->DecommitTailOnFree) { + KiHdrToFtr(blk)->Size = blk->Size; + KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; + return; + } + + uintptr_t blockBase = (uintptr_t)blk; + uintptr_t oldBrk = h->Brk; + + // Only the last free block can return committed heap pages. Interior free + // blocks must remain mapped because the allocator's boundary tags and free + // list links are stored inside those blocks. + if ((uintptr_t)KiNextBlock(blk) != oldBrk) { + KiHdrToFtr(blk)->Size = blk->Size; + KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; + return; + } + + // Keep enough mapped space for the tail block's header, footer, and a small + // usable payload, then decommit whole pages above that. The footer moves to + // the new Brk so future coalescing can still walk the boundary tag chain. + uintptr_t minEnd = blockBase + MIN_BLOCK; + uintptr_t newBrk = (minEnd + PAGE_SIZE - 1) & ~(uintptr_t)(PAGE_SIZE - 1); + + if (newBrk >= oldBrk) { + KiHdrToFtr(blk)->Size = blk->Size; + KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; + return; + } + + for (uintptr_t va = newBrk; va < oldBrk; va += PAGE_SIZE) { + PHYSICAL_ADDRESS frame = MmPteTranslate(va); + if (!frame) + continue; + if (KSUCCESS(MmPteUnmapPage(va))) + (void)MmPfnFree(frame); + } + + h->Brk = newBrk; + blk->Size = newBrk - blockBase - OVERHEAD; KiHdrToFtr(blk)->Size = blk->Size; KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; - KiHdrToFtr(blk)->Free = 1; + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; +} + +// Releases a previously quarantined block into the real free list. Caller holds h->Lock. +static void KiHeapReleaseFreeBlockLocked(HEAP* h, BLOCK_HDR* blk) { + if (!h || !blk) return; + + KiPoolValidateFooterLocked(h, blk, "release free footer corrupt"); + KiPoolValidateTailCanaryLocked(h, blk, "release free tail canary corrupt"); + KiPoolValidateFreedPoisonLocked(h, blk, "use-after-free payload write detected"); + + blk->Free = BLOCK_STATE_FREE; + blk->RequestedSize = 0; + blk->Tag = 0; + blk->Flags = 0; + blk->AllocCaller = 0; + // Keep FreeCaller/Sequence for diagnostics until the block is reused. + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; + KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; + KiHdrToFtr(blk)->Size = blk->Size; + + // Coalesce with next block if it is really free. Quarantined neighbors are + // intentionally not coalesced; their payload must remain intact for UAF checks. + BLOCK_HDR* next = KiNextBlock(blk); + if ((uintptr_t)next < h->Brk) { + KiPoolValidateFooterLocked(h, next, "next footer corrupt while freeing"); + if (next->Free == BLOCK_STATE_FREE) { + KiFreeListRemove(h, next); + blk->Size += OVERHEAD + next->Size; + KiHdrToFtr(blk)->Size = blk->Size; + KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; + } + } + + // Coalesce with previous block if it is really free. + if ((uintptr_t)blk > h->Start) { + BLOCK_FTR* prevFtr = KiPrevFtr(blk); + if (prevFtr->Magic == BLOCK_MAGIC) { + BLOCK_HDR* prev = KiFtrToHdr(prevFtr); + KiPoolValidateFooterLocked(h, prev, "previous footer corrupt while freeing"); + if (prev->Free == BLOCK_STATE_FREE) { + KiFreeListRemove(h, prev); + prev->Size += OVERHEAD + blk->Size; + KiHdrToFtr(prev)->Size = prev->Size; + KiHdrToFtr(prev)->Magic = BLOCK_MAGIC; + KiHdrToFtr(prev)->Free = BLOCK_STATE_FREE; + blk = prev; + } + } + } + + // Insert at head of free list. + blk->PrevFree = (BLOCK_HDR *)0; + blk->NextFree = h->FreeList; + if (h->FreeList) h->FreeList->PrevFree = blk; + h->FreeList = blk; + + KiHdrToFtr(blk)->Free = BLOCK_STATE_FREE; + KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; + KiHdrToFtr(blk)->Size = blk->Size; + + KiHeapTryDecommitTail(h, blk); +} + +static void KiHeapReleaseQuarantineSlotLocked(HEAP* h, size_t index) { + if (!h || index >= POOL_QUARANTINE_SLOTS) + return; + BLOCK_HDR* old = h->Quarantine[index]; + if (!old) + return; + h->Quarantine[index] = (BLOCK_HDR*)0; + if (h->QuarantineCount) + h->QuarantineCount--; + if (old->Free != BLOCK_STATE_QUARANTINE) + KiPoolPanic("quarantine slot state corrupt", h, old, KiBlockUserBase(old)); + KiHeapReleaseFreeBlockLocked(h, old); +} + +static void KiHeapDrainQuarantineLocked(HEAP* h) { + if (!h) + return; + for (size_t i = 0; i < POOL_QUARANTINE_SLOTS; i++) + KiHeapReleaseQuarantineSlotLocked(h, i); + h->QuarantineCursor = 0; +} + +static void KiHeapQuarantineBlockLocked(HEAP* h, BLOCK_HDR* blk) { + if (!h || !blk) + return; + size_t idx = h->QuarantineCursor % POOL_QUARANTINE_SLOTS; + KiHeapReleaseQuarantineSlotLocked(h, idx); + + blk->Free = BLOCK_STATE_QUARANTINE; + KiHdrToFtr(blk)->Free = BLOCK_STATE_QUARANTINE; + KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; + KiHdrToFtr(blk)->Size = blk->Size; + + h->Quarantine[idx] = blk; + h->QuarantineCursor = (idx + 1u) % POOL_QUARANTINE_SLOTS; + h->QuarantineCount++; } // Implements the heap Alloc operation. -static void* KiHeapAlloc(HEAP* h, size_t size) { - if (!size) return (void *)0; +static void* KiHeapAllocEx(HEAP* h, size_t requestedSize, uint32_t tag, uintptr_t caller) { + if (!requestedSize) return (void *)0; + + uint32_t dbg = KiPoolDebugFlags(); + size_t size = requestedSize; + if (dbg & KE_DEBUG_POOL_CANARY) { + if (size > (size_t)-1 - sizeof(uint64_t)) + return (void*)0; + size += sizeof(uint64_t); + } size = (size + 7) & ~7ULL; // 8-byte align uintptr_t flags = KeAcquireSpinlock(&h->Lock); - if (h->Brk > h->Start && !KiHeapEnsureMapped(h, h->Start, h->Brk - h->Start)) { - KeReleaseSpinlock(&h->Lock, flags); - return (void*)0; - } - // Best-fit search. BLOCK_HDR* best = (BLOCK_HDR *)0; for (BLOCK_HDR* cur = h->FreeList; cur; cur = cur->NextFree) { + KiPoolValidateFooterLocked(h, cur, "free list block corrupt"); + if (cur->Free != BLOCK_STATE_FREE) + KiPoolPanic("free list contains non-free block", h, cur, KiBlockUserBase(cur)); if (cur->Size >= size) { if (!best || cur->Size < best->Size) best = cur; if (best->Size == size) break; } } + if (!best && h->QuarantineCount) { + KiHeapDrainQuarantineLocked(h); + for (BLOCK_HDR* cur = h->FreeList; cur; cur = cur->NextFree) { + KiPoolValidateFooterLocked(h, cur, "free list block corrupt after quarantine drain"); + if (cur->Size >= size) { + if (!best || cur->Size < best->Size) best = cur; + if (best->Size == size) break; + } + } + } + if (!best) { if (!KiHeapGrow(h, size + OVERHEAD)) { KeReleaseSpinlock(&h->Lock, flags); @@ -650,6 +568,7 @@ static void* KiHeapAlloc(HEAP* h, size_t size) { KeReleaseSpinlock(&h->Lock, flags); return (void *)0; } + KiPoolValidateFooterLocked(h, best, "grown free block corrupt"); } KiFreeListRemove(h, best); @@ -658,130 +577,176 @@ static void* KiHeapAlloc(HEAP* h, size_t size) { if (best->Size >= size + OVERHEAD + sizeof(void*)) { BLOCK_HDR* rem = (BLOCK_HDR *)((char *)best + HDR_SIZE + size + FTR_SIZE); rem->Magic = BLOCK_MAGIC; - rem->Free = 1; + rem->Free = BLOCK_STATE_FREE; rem->Size = best->Size - size - OVERHEAD; rem->PrevFree = (BLOCK_HDR *)0; rem->NextFree = h->FreeList; + rem->RequestedSize = 0; + rem->Tag = 0; + rem->Flags = 0; + rem->Sequence = 0; + rem->AllocCaller = 0; + rem->FreeCaller = 0; if (h->FreeList) h->FreeList->PrevFree = rem; h->FreeList = rem; KiHdrToFtr(rem)->Magic = BLOCK_MAGIC; - KiHdrToFtr(rem)->Free = 1; + KiHdrToFtr(rem)->Free = BLOCK_STATE_FREE; KiHdrToFtr(rem)->Size = rem->Size; best->Size = size; } - best->Free = 0; - KiHdrToFtr(best)->Free = 0; + best->Free = BLOCK_STATE_USED; + best->RequestedSize = requestedSize; + best->Tag = tag ? tag : POOL_TAG_DEFAULT; + best->Flags = 0; + best->Sequence = ++h->NextSequence; + best->AllocCaller = (dbg & KE_DEBUG_POOL_CALLERS) ? caller : 0; + best->FreeCaller = 0; + if (dbg & KE_DEBUG_POOL_CANARY) + best->Flags |= BLOCK_FLAG_CANARY; + + KiHdrToFtr(best)->Free = BLOCK_STATE_USED; KiHdrToFtr(best)->Magic = BLOCK_MAGIC; KiHdrToFtr(best)->Size = best->Size; - memset((char *)best + HDR_SIZE, 0, size); + memset(KiBlockUserBase(best), 0, requestedSize); + if (best->Size > requestedSize) + memset(KiBlockUserBase(best) + requestedSize, 0, best->Size - requestedSize); + KiPoolWriteTailCanary(best); + KeReleaseSpinlock(&h->Lock, flags); - return (char *)best + HDR_SIZE; + return KiBlockUserBase(best); +} + +static void* KiHeapAlloc(HEAP* h, size_t size) { + return KiHeapAllocEx(h, size, POOL_TAG_DEFAULT, (uintptr_t)_ReturnAddress()); } // Implements the heap Free operation. -static void KiHeapFree(HEAP* h, void* ptr) { +static void KiHeapFreeEx(HEAP* h, void* ptr, uintptr_t caller) { if (!ptr) return; - BLOCK_HDR* blk = (BLOCK_HDR *)((char *)ptr - HDR_SIZE); - if (blk->Magic != BLOCK_MAGIC || blk->Free) return; // double-free / corrupt + + uintptr_t u = (uintptr_t)ptr; + if (!h || u < h->Start + HDR_SIZE || u >= h->Brk) + KiPoolPanic("invalid free pointer outside heap", h, (BLOCK_HDR*)0, ptr); + + BLOCK_HDR* blk = (BLOCK_HDR *)(u - HDR_SIZE); uintptr_t flags = KeAcquireSpinlock(&h->Lock); - blk->Free = 1; + if (blk->Magic != BLOCK_MAGIC) + KiPoolPanic("invalid free pointer/bad header magic", h, blk, ptr); + KiPoolValidateFooterLocked(h, blk, "allocated footer corrupt on free"); + if (blk->Free == BLOCK_STATE_FREE) + KiPoolPanic("double free", h, blk, ptr); + if (blk->Free == BLOCK_STATE_QUARANTINE) + KiPoolPanic("double free/quarantined UAF", h, blk, ptr); + if (blk->Free != BLOCK_STATE_USED) + KiPoolPanic("invalid block state on free", h, blk, ptr); - // Coalesce with next block if it is free. - BLOCK_HDR* next = KiNextBlock(blk); - if ((uintptr_t)next < h->Brk && next->Magic == BLOCK_MAGIC && next->Free) { - KiFreeListRemove(h, next); - blk->Size += OVERHEAD + next->Size; - KiHdrToFtr(blk)->Size = blk->Size; - KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; - KiHdrToFtr(blk)->Free = 1; + KiPoolValidateTailCanaryLocked(h, blk, "tail canary corrupt on free"); + + uint32_t dbg = KiPoolDebugFlags(); + blk->FreeCaller = (dbg & KE_DEBUG_POOL_CALLERS) ? caller : 0; + if (dbg & KE_DEBUG_POOL_POISON) + KiPoolPoisonFreedPayload(blk); + + if (dbg & KE_DEBUG_POOL_QUARANTINE) { + KiHeapQuarantineBlockLocked(h, blk); + } else { + KiHeapReleaseFreeBlockLocked(h, blk); } - // Coalesce with previous block if it is free. - if ((uintptr_t)blk > h->Start) { - BLOCK_FTR* prevFtr = KiPrevFtr(blk); - if (prevFtr->Magic == BLOCK_MAGIC && prevFtr->Free) { - BLOCK_HDR* prev = KiFtrToHdr(prevFtr); - KiFreeListRemove(h, prev); - prev->Size += OVERHEAD + blk->Size; - KiHdrToFtr(prev)->Size = prev->Size; - KiHdrToFtr(prev)->Magic = BLOCK_MAGIC; - KiHdrToFtr(prev)->Free = 1; - blk = prev; - } - } - - // Insert at head of free list. - blk->PrevFree = (BLOCK_HDR *)0; - blk->NextFree = h->FreeList; - if (h->FreeList) h->FreeList->PrevFree = blk; - h->FreeList = blk; - - KiHdrToFtr(blk)->Free = 1; - KiHdrToFtr(blk)->Magic = BLOCK_MAGIC; - - KiHeapTryDecommitTail(h, blk); - KeReleaseSpinlock(&h->Lock, flags); } +static void KiHeapFree(HEAP* h, void* ptr) { + KiHeapFreeEx(h, ptr, (uintptr_t)_ReturnAddress()); +} + // Implements the heap Realloc operation. -static void* KiHeapRealloc(HEAP* h, void* ptr, size_t new_size) { +static void* KiHeapReallocEx(HEAP* h, void* ptr, size_t newSize, uintptr_t caller) { // If ptr is NULL, realloc acts like malloc. if (!ptr) { - return KiHeapAlloc(h, new_size); + return KiHeapAllocEx(h, newSize, POOL_TAG_DEFAULT, caller); } // If size is 0, realloc acts like free. - if (new_size == 0) { - KiHeapFree(h, ptr); + if (newSize == 0) { + KiHeapFreeEx(h, ptr, caller); return (void*)0; } - // Retrieve the header of the existing block. - BLOCK_HDR* blk = (BLOCK_HDR *)((char *)ptr - HDR_SIZE); + uintptr_t u = (uintptr_t)ptr; + if (!h || u < h->Start + HDR_SIZE || u >= h->Brk) + KiPoolPanic("invalid realloc pointer outside heap", h, (BLOCK_HDR*)0, ptr); - // Sanity checks to prevent operating on corrupted or already-freed pointers. - if (blk->Magic != BLOCK_MAGIC || blk->Free) { - return (void*)0; - } + BLOCK_HDR* blk = (BLOCK_HDR *)(u - HDR_SIZE); - // Align the requested size (matches KiHeapAlloc 8-byte alignment). - size_t aligned_size = (new_size + 7) & ~7ULL; + uintptr_t flags = KeAcquireSpinlock(&h->Lock); + if (blk->Magic != BLOCK_MAGIC) + KiPoolPanic("invalid realloc pointer/bad header magic", h, blk, ptr); + KiPoolValidateFooterLocked(h, blk, "allocated footer corrupt on realloc"); + if (blk->Free != BLOCK_STATE_USED) + KiPoolPanic("realloc of non-allocated block", h, blk, ptr); + KiPoolValidateTailCanaryLocked(h, blk, "tail canary corrupt on realloc"); - // If the current block is already big enough, we can reuse it in-place. - // (Note: To save space, you could optionally split the block here if - // the new size is significantly smaller, similar to KiHeapAlloc's logic). - if (blk->Size >= aligned_size) { + size_t oldRequested = blk->RequestedSize; + size_t oldCopy = oldRequested < newSize ? oldRequested : newSize; + if (newSize <= oldRequested) { + blk->RequestedSize = newSize; + if (blk->Flags & BLOCK_FLAG_CANARY) + KiPoolWriteTailCanary(blk); + KeReleaseSpinlock(&h->Lock, flags); return ptr; } + KeReleaseSpinlock(&h->Lock, flags); - // The block is too small. Allocate a new, larger block. - void* new_ptr = KiHeapAlloc(h, aligned_size); - if (!new_ptr) { - return (void*)0; // Out of memory, original block is left untouched - } - memset(new_ptr, 0, aligned_size); + void* newPtr = KiHeapAllocEx(h, newSize, POOL_TAG_DEFAULT, caller); + if (!newPtr) + return (void*)0; + memcpy(newPtr, ptr, oldCopy); + KiHeapFreeEx(h, ptr, caller); + return newPtr; +} - // Copy the old data to the new block. - // We only copy up to the old block's usable size. - memcpy(new_ptr, ptr, blk->Size); - - // Free the old block. - KiHeapFree(h, ptr); - - return new_ptr; +static void* KiHeapRealloc(HEAP* h, void* ptr, size_t newSize) { + return KiHeapReallocEx(h, ptr, newSize, (uintptr_t)_ReturnAddress()); } // Kernel pool. static HEAP gPoolHeap; -// User virtual region arena. -typedef struct USER_VA_REGION { +// VAD arena. +typedef enum MM_VAD_PAGE_STATE { + MmVadPageFree = 0, + MmVadPageCommitted = 1, +} MM_VAD_PAGE_STATE; + +#define MM_VAD_PAGE_COW 0x00000001u +#define MM_VAD_PAGE_ZERO_PAGE 0x00000002u +#define MM_VAD_PAGE_SWAPPED 0x00000004u +#define MM_VAD_PAGE_SHARED_FRAME 0x00000008u +#define MM_VAD_PAGE_PAGING 0x00000010u +#define MM_VAD_PAGE_FILE_BACKED 0x00000020u +#define MM_VAD_PAGE_IMAGE_BACKED 0x00000040u +#define MM_VAD_PAGE_IMAGE_PRIVATE 0x00000080u +#define MM_VAD_PAGE_IMAGE_FIXUP 0x00000100u +#define MM_VAD_PAGE_FILE_SHARED_WRITE 0x00000200u +#define MM_VAD_PAGE_FILE_DIRTY_TRACK 0x00000400u + +typedef struct MM_VAD_PAGE { + uint64_t PageFlags; + PHYSICAL_ADDRESS Frame; + uint8_t State; + uint8_t Resident; + uint16_t Reserved; + uint32_t Flags; + uint32_t SwapSlot; +} MM_VAD_PAGE; + +typedef struct MM_VAD { LIST_ENTRY ProcessLink; LIST_ENTRY GlobalLink; PROCESS* Owner; @@ -790,97 +755,132 @@ typedef struct USER_VA_REGION { size_t ReservationSize; uintptr_t Base; size_t Size; - size_t CommittedSize; + size_t PayloadSize; + size_t PageCount; + size_t CommittedPages; uint64_t PageFlags; - USER_REGION_TYPE Type; + MM_VAD_TYPE Type; uint32_t Flags; -} USER_VA_REGION; + uint32_t LockCount; + MM_VAD_PAGE* Pages; -static LIST_ENTRY gUserRegionList; -static SPINLOCK gUserRegionLock = SPINLOCK_INIT; -static int gUserRegionListReady = 0; + FILE_OBJECT* FileObject; + uint64_t FileOffset; + size_t FileLength; + uint32_t FileMappingFlags; -#define USER_REGION_ARENA_BASE 0x0000100000000000ULL + IMAGE_SECTION_OBJECT* ImageSection; +} MM_VAD; -// Module init. +static LIST_ENTRY gVadList; +static SPINLOCK gVadLock = SPINLOCK_INIT; +static int gVadListReady = 0; +static MM_VAD_STATS gVadStats; + +#define MM_VAD_ARENA_BASE 0x0000100000000000ULL + + +// Initializes the VAD list after the shared page table and kernel pool are ready. +static void KiVadInitialize(void) { + LIST_INIT(&gVadList); + gVadLock = (SPINLOCK)SPINLOCK_INIT; + memset(&gVadStats, 0, sizeof(gVadStats)); + gVadListReady = 1; +} + +// Initializes the shared page tables, kernel pool, and user VAD arena. void MmInitializeVirtual(PHYSICAL_ADDRESS bootPml4Phys, size_t poolSize) { - gPml4Phys = bootPml4Phys; - gPageTableLock = (SPINLOCK)SPINLOCK_INIT; + MmPteInitialize(bootPml4Phys); - // Kernel pool heap. gPoolHeap.FreeList = (BLOCK_HDR *)0; gPoolHeap.Start = POOL_BASE; gPoolHeap.Brk = POOL_BASE; gPoolHeap.Limit = POOL_LIMIT; gPoolHeap.MapFlags = PAGE_PRESENT | PAGE_WRITE | PAGE_NX; gPoolHeap.Lock = (SPINLOCK)SPINLOCK_INIT; + gPoolHeap.DecommitTailOnFree = 0; + gPoolHeap.NextSequence = 0; + gPoolHeap.QuarantineCursor = 0; + gPoolHeap.QuarantineCount = 0; + memset(gPoolHeap.Quarantine, 0, sizeof(gPoolHeap.Quarantine)); - // Pre-commit the requested pool size. if (poolSize) { poolSize = (poolSize + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); KiHeapGrow(&gPoolHeap, poolSize); } - // User VA arena. One global lower-half virtual-address bucket, with - // per-process ownership tracked by USER_VA_REGION descriptors. - LIST_INIT(&gUserRegionList); - gUserRegionLock = (SPINLOCK)SPINLOCK_INIT; - gUserRegionListReady = 1; + KiVadInitialize(); + + gVadZeroPageFrame = MmPfnAllocate(MmPfnUseAnonymous); + if (!gVadZeroPageFrame) { + KePanic(KE_PANIC_CODE_INIT, "MM: failed to allocate global zero page.\n"); + } + memset(MmPfnToVirtual(gVadZeroPageFrame), 0, PAGE_SIZE); + (void)MmPfnPin(gVadZeroPageFrame); } -// Implements the activate operation. -void MmActivate(void) { - __writecr3(gPml4Phys); + +// Allocates zeroed kernel-pool memory from the non-user heap. +__declspec(dllexport) void* MmAllocatePool(size_t size) { + return KiHeapAllocEx(&gPoolHeap, size, POOL_TAG_DEFAULT, (uintptr_t)_ReturnAddress()); } -// Public pool API. -__declspec(dllexport) void* MmAllocatePool(size_t size) { return KiHeapAlloc(&gPoolHeap, size); } -// Releases pool. -__declspec(dllexport) void MmFreePool(void* ptr) { KiHeapFree(&gPoolHeap, ptr); } -// Implements the reallocate Pool operation. -__declspec(dllexport) void* MmReallocatePool(void* ptr, size_t newSize) { return KiHeapRealloc(&gPoolHeap, ptr, newSize); } +// Allocates zeroed kernel-pool memory with a diagnostic tag. +__declspec(dllexport) void* MmAllocatePoolTag(size_t size, uint32_t tag) { + return KiHeapAllocEx(&gPoolHeap, size, tag, (uintptr_t)_ReturnAddress()); +} -// User virtual region API. +// Releases kernel-pool memory back to the non-user heap. +__declspec(dllexport) void MmFreePool(void* ptr) { + KiHeapFreeEx(&gPoolHeap, ptr, (uintptr_t)_ReturnAddress()); +} + +// Reallocates kernel-pool memory while preserving existing contents. +__declspec(dllexport) void* MmReallocatePool(void* ptr, size_t newSize) { + return KiHeapReallocEx(&gPoolHeap, ptr, newSize, (uintptr_t)_ReturnAddress()); +} + +// VAD utility helpers. static inline uintptr_t KiAlignUpPtr(uintptr_t v, size_t align) { if (align < PAGE_SIZE) align = PAGE_SIZE; if (align & (align - 1)) return 0; return (v + (uintptr_t)align - 1) & ~((uintptr_t)align - 1); } -// Implements the align Up Size operation. +// Aligns a size to a power-of-two boundary and rejects invalid alignments. static inline size_t KiAlignUpSize(size_t v, size_t align) { if (align < PAGE_SIZE) align = PAGE_SIZE; if (align & (align - 1)) return 0; return (v + align - 1) & ~(align - 1); } -// Implements the ranges Overlap operation. +// Returns whether two half-open address ranges intersect. static int KiRangesOverlap(uintptr_t aBase, size_t aSize, uintptr_t bBase, size_t bSize) { uintptr_t aEnd = aBase + aSize; uintptr_t bEnd = bBase + bSize; return aBase < bEnd && bBase < aEnd; } -// Returns user Va Range. -static KSTATUS KiFindUserVaRange(size_t reservationSize, size_t alignment, uintptr_t* baseOut) { +// Finds a free range in the global user VA arena. +static KSTATUS KiFindVadRange(size_t reservationSize, size_t alignment, uintptr_t* baseOut) { if (!baseOut || !reservationSize) return STATUS_INVALID_PARAMETER; - if (!gUserRegionListReady) return STATUS_FAILURE; + if (!gVadListReady) return STATUS_FAILURE; - uintptr_t candidate = KiAlignUpPtr(USER_REGION_ARENA_BASE, alignment); + uintptr_t candidate = KiAlignUpPtr(MM_VAD_ARENA_BASE, alignment); if (!candidate) return STATUS_INVALID_PARAMETER; for (;;) { - if (candidate < USER_REGION_ARENA_BASE || reservationSize > (size_t)(USER_LIMIT - candidate)) + if (candidate < MM_VAD_ARENA_BASE || reservationSize > (size_t)(USER_LIMIT - candidate)) return STATUS_NO_MEMORY; uintptr_t next = candidate; int collided = 0; - for (LIST_ENTRY* node = gUserRegionList.Fwd; node != &gUserRegionList; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, GlobalLink); - if (KiRangesOverlap(candidate, reservationSize, r->ReservationBase, r->ReservationSize)) { - uintptr_t end = r->ReservationBase + r->ReservationSize; + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, GlobalLink); + if (KiRangesOverlap(candidate, reservationSize, vad->ReservationBase, vad->ReservationSize)) { + uintptr_t end = vad->ReservationBase + vad->ReservationSize; next = KiAlignUpPtr(end, alignment); if (!next || next <= candidate) return STATUS_NO_MEMORY; collided = 1; @@ -896,131 +896,1092 @@ static KSTATUS KiFindUserVaRange(size_t reservationSize, size_t alignment, uintp } } -// Returns user Region Locked. -static USER_VA_REGION* KiFindUserRegionLocked(PROCESS* process, const void* base) { - if (!process || !base) return (USER_VA_REGION*)0; - uintptr_t b = (uintptr_t)base; - for (LIST_ENTRY* node = process->UserRegions.Fwd; node != &process->UserRegions; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, ProcessLink); - if (r->Base == b) return r; - } - return (USER_VA_REGION*)0; +// Returns the page index for an address inside the VAD payload. +static size_t KiVadPageIndex(const MM_VAD* vad, uintptr_t va) { + return (size_t)((va - vad->Base) / PAGE_SIZE); } -// Returns user Region Containing Locked. -static USER_VA_REGION* KiFindUserRegionContainingLocked(PROCESS* process, uintptr_t base, size_t size) { - if (!process || !base || !size) return (USER_VA_REGION*)0; - if (base > (uintptr_t)-1 - (size - 1)) return (USER_VA_REGION*)0; +// Returns the page count covered by a page-aligned half-open range. +static size_t KiVadRangePageCount(uintptr_t alignedStart, uintptr_t alignedEnd) { + return (size_t)((alignedEnd - alignedStart) / PAGE_SIZE); +} + +// Finds a VAD by its public base address while the owning process VAD lock is held. +static MM_VAD* KiFindVadByBaseLocked(PROCESS* process, const void* base) { + if (!process || !base) return (MM_VAD*)0; + uintptr_t b = (uintptr_t)base; + for (LIST_ENTRY* node = process->VadList.Fwd; node != &process->VadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, ProcessLink); + if (vad->Base == b) return vad; + } + return (MM_VAD*)0; +} + +// Finds a VAD whose reserved payload fully contains the supplied range. +static MM_VAD* KiFindReservedVadContainingLocked(PROCESS* process, uintptr_t base, size_t size) { + if (!process || !base || !size) return (MM_VAD*)0; + if (base > (uintptr_t)-1 - (size - 1)) return (MM_VAD*)0; uintptr_t end = base + size; - for (LIST_ENTRY* node = process->UserRegions.Fwd; node != &process->UserRegions; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, ProcessLink); - uintptr_t regionEnd = r->Base + r->CommittedSize; - if (base >= r->Base && end <= regionEnd) return r; + for (LIST_ENTRY* node = process->VadList.Fwd; node != &process->VadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, ProcessLink); + uintptr_t vadEnd = vad->Base + vad->PayloadSize; + if (base >= vad->Base && end <= vadEnd) return vad; } - return (USER_VA_REGION*)0; + return (MM_VAD*)0; } -// Releases user Region Payload. -static void KiUnmapUserRegionPayload(USER_VA_REGION* r) { - if (!r) return; - uintptr_t end = r->Base + r->CommittedSize; - for (uintptr_t va = r->Base; va < end; va += PAGE_SIZE) { - PHYSICAL_ADDRESS phys = MmGetPhysicalAddress(va); - if (!phys) continue; - if (KSUCCESS(MmUnmapPage(va))) { - MmFreeFrame(phys); +// Returns whether every page in the aligned range is committed in the VAD metadata. +static int KiVadRangeCommitted(const MM_VAD* vad, uintptr_t alignedStart, uintptr_t alignedEnd) { + if (!vad || alignedStart < vad->Base || alignedEnd > vad->Base + vad->PayloadSize) + return 0; + size_t first = KiVadPageIndex(vad, alignedStart); + size_t count = KiVadRangePageCount(alignedStart, alignedEnd); + if (first > vad->PageCount || count > vad->PageCount - first) + return 0; + + for (size_t i = 0; i < count; ++i) { + if (vad->Pages[first + i].State != MmVadPageCommitted) + return 0; + } + return 1; +} + +static uint64_t KiVadHardwarePageFlags(const MM_VAD_PAGE* page); +static KSTATUS KiVadMapPageTracked(uintptr_t va, PHYSICAL_ADDRESS frame, uint64_t flags, MM_PTE_MAP_MODE mode); +static KSTATUS KiVadUnmapPageTracked(uintptr_t va, PHYSICAL_ADDRESS frame); + +static KSTATUS KiVadMarkSharedFilePageDirty(MM_VAD* vad, size_t index, int writeProtect) { + if (!vad || index >= vad->PageCount || !vad->FileObject) + return STATUS_INVALID_PARAMETER; + + MM_VAD_PAGE* page = &vad->Pages[index]; + if ((page->Flags & MM_VAD_PAGE_FILE_BACKED) == 0 || + (page->Flags & MM_VAD_PAGE_FILE_SHARED_WRITE) == 0) + return STATUS_NOT_FOUND; + + uint64_t pageDelta = (uint64_t)index * (uint64_t)PAGE_SIZE; + uint64_t fileOffset = vad->FileOffset + pageDelta; + if (fileOffset < vad->FileOffset) + return STATUS_INVALID_PARAMETER; + + uint32_t validBytes = PAGE_SIZE; + if (pageDelta >= (uint64_t)vad->FileLength) { + validBytes = PAGE_SIZE; + } else if ((uint64_t)vad->FileLength - pageDelta < PAGE_SIZE) { + validBytes = (uint32_t)((uint64_t)vad->FileLength - pageDelta); + if (validBytes == 0) + validBytes = PAGE_SIZE; + } + + MmFileCacheMarkDirty(vad->FileObject, fileOffset, validBytes); + + uint64_t end = fileOffset + validBytes; + uintptr_t flags = KeAcquireSpinlock(&vad->FileObject->Lock); + if (end > vad->FileObject->Size) + vad->FileObject->Size = end; + KeReleaseSpinlock(&vad->FileObject->Lock, flags); + + if (writeProtect && page->Resident && page->Frame && page->PageFlags) { + uintptr_t va = vad->Base + index * PAGE_SIZE; + page->Flags |= MM_VAD_PAGE_FILE_DIRTY_TRACK; + (void)MmPteProtectRange(va, PAGE_SIZE, KiVadHardwarePageFlags(page)); + } + + gVadStats.FileBackedDirtySyncs++; + return STATUS_SUCCESS; +} + +static void KiVadSyncSharedFilePageBeforeRelease(MM_VAD* vad, size_t index) { + if (!vad || index >= vad->PageCount) + return; + MM_VAD_PAGE* page = &vad->Pages[index]; + if ((page->Flags & MM_VAD_PAGE_FILE_SHARED_WRITE) == 0) + return; + uintptr_t va = vad->Base + index * PAGE_SIZE; + if (page->Resident && page->Frame && MmPteTestAndClearDirty(va)) + (void)KiVadMarkSharedFilePageDirty(vad, index, 0); +} + +// Marks a page uncommitted and releases any physical frame it still owns. +static void KiVadReleasePage(MM_VAD* vad, size_t index) { + if (!vad || index >= vad->PageCount) return; + MM_VAD_PAGE* page = &vad->Pages[index]; + uintptr_t va = vad->Base + index * PAGE_SIZE; + + KiVadSyncSharedFilePageBeforeRelease(vad, index); + + if (page->Resident && page->PageFlags) { + (void)KiVadUnmapPageTracked(va, page->Frame); + } + if (page->Resident && page->Frame && !(page->Flags & MM_VAD_PAGE_ZERO_PAGE)) { + (void)MmPfnRelease(page->Frame); + } + if ((page->Flags & MM_VAD_PAGE_SWAPPED) && page->SwapSlot != 0xFFFFFFFFu) { + MmSwapFreeSlot(page->SwapSlot); + } + + if (page->State == MmVadPageCommitted && vad->CommittedPages) + vad->CommittedPages--; + + page->PageFlags = 0; + page->Frame = 0; + page->State = MmVadPageFree; + page->Resident = 0; + page->Flags = 0; + page->SwapSlot = 0xFFFFFFFFu; +} + +// Unmaps all resident pages in a VAD and releases their PFNs. +static void KiUnmapVadPayload(MM_VAD* vad) { + if (!vad || !vad->Pages) return; + for (size_t i = 0; i < vad->PageCount; ++i) { + if (vad->Pages[i].State == MmVadPageCommitted) + KiVadReleasePage(vad, i); + } +} + +// Returns the hardware PTE flags used for a resident VAD page. +static uint64_t KiVadHardwarePageFlags(const MM_VAD_PAGE* page) { + if (!page || !page->PageFlags) + return 0; + uint64_t flags = page->PageFlags; + if (page->Flags & MM_VAD_PAGE_COW) + flags &= ~PAGE_WRITE; + if (page->Flags & MM_VAD_PAGE_FILE_DIRTY_TRACK) + flags &= ~PAGE_WRITE; + return flags; +} + +// Maps a user VAD PTE and records the mapping in the PFN database. +static KSTATUS KiVadMapPageTracked(uintptr_t va, PHYSICAL_ADDRESS frame, uint64_t flags, MM_PTE_MAP_MODE mode) { + KSTATUS s = MmPteMapPage(va, frame, flags, mode); + if (!KSUCCESS(s)) + return s; + + s = MmPfnAddMapping(frame); + if (!KSUCCESS(s)) { + (void)MmPteUnmapPage(va); + return s; + } + return STATUS_SUCCESS; +} + +// Unmaps a user VAD PTE and removes the matching PFN mapping reference. +static KSTATUS KiVadUnmapPageTracked(uintptr_t va, PHYSICAL_ADDRESS frame) { + KSTATUS s = MmPteUnmapPage(va); + if (KSUCCESS(s)) + (void)MmPfnRemoveMapping(frame); + return s; +} + +// Returns whether the committed page allows the requested fault or probe access. +static KSTATUS KiVadCheckPageAccess(const MM_VAD_PAGE* page, int wantWrite, int wantExecute) { + if (!page || page->State != MmVadPageCommitted) + return STATUS_NOT_FOUND; + if (!page->PageFlags) + return STATUS_ACCESS_DENIED; + if ((page->PageFlags & PAGE_USER) == 0) + return STATUS_ACCESS_DENIED; + if (wantWrite && (page->PageFlags & PAGE_WRITE) == 0) + return STATUS_ACCESS_DENIED; + if (wantExecute && (page->PageFlags & PAGE_NX)) + return STATUS_ACCESS_DENIED; + return STATUS_SUCCESS; +} + +// Waits for another CPU/thread to finish a page-in/page-out transition on a VAD page. +// The pager performs filesystem I/O outside the VAD lock; this transient bit keeps +// concurrent faults from treating half-transitioned metadata as either resident or +// fully swapped. +static void KiVadWaitForPagingComplete(MM_VAD_PAGE* page) { + while (page && (_InterlockedCompareExchange((volatile long*)&page->Flags, 0, 0) & + MM_VAD_PAGE_PAGING)) + KeYield(); +} + +// Atomically claims the transient paging bit. This is deliberately not a spinlock: +// a page-in/page-out may allocate frames or do filesystem I/O, so waiters must yield +// instead of holding IRQL-raised locks. Without this CAS, two CPUs faulting the same +// swapped/demand-zero page can both allocate/read/map it; one map fails and rolls +// metadata back to SWAPPED while the other has already published a valid PTE. That +// leaves stale or freed swap slots behind and corrupts user data under pressure. +static int KiVadTryBeginPaging(MM_VAD_PAGE* page) { + if (!page) return 0; + + for (;;) { + uint32_t oldFlags = (uint32_t)_InterlockedCompareExchange((volatile long*)&page->Flags, 0, 0); + if (oldFlags & MM_VAD_PAGE_PAGING) + return 0; + uint32_t newFlags = oldFlags | MM_VAD_PAGE_PAGING; + if ((uint32_t)_InterlockedCompareExchange((volatile long*)&page->Flags, + (long)newFlags, + (long)oldFlags) == oldFlags) + return 1; + _mm_pause(); + } +} + +static void KiVadEndPaging(MM_VAD_PAGE* page) { + if (!page) return; + + for (;;) { + uint32_t oldFlags = (uint32_t)_InterlockedCompareExchange((volatile long*)&page->Flags, 0, 0); + uint32_t newFlags = oldFlags & ~MM_VAD_PAGE_PAGING; + if ((uint32_t)_InterlockedCompareExchange((volatile long*)&page->Flags, + (long)newFlags, + (long)oldFlags) == oldFlags) + return; + _mm_pause(); + } +} + +// Reads one page of a file-backed VAD into a freshly allocated user frame. +// Bytes beyond EOF or beyond the requested mapping length are zero-filled, +// matching the useful mmap/CreateFileMapping contract without materializing the +// whole file up front. +static KSTATUS KiVadFillFileBackedPage(MM_VAD* vad, size_t pageIndex, PHYSICAL_ADDRESS frame) { + if (!vad || !vad->FileObject || !frame || pageIndex >= vad->PageCount) + return STATUS_INVALID_PARAMETER; + + void* pageVa = MmPfnToVirtual(frame); + if (!pageVa) + return STATUS_FAILURE; + memset(pageVa, 0, PAGE_SIZE); + + uint64_t pageDelta = (uint64_t)pageIndex * (uint64_t)PAGE_SIZE; + if (pageDelta >= (uint64_t)vad->FileLength) + return STATUS_SUCCESS; + + size_t remaining = vad->FileLength - (size_t)pageDelta; + size_t toRead = remaining < PAGE_SIZE ? remaining : PAGE_SIZE; + + uint64_t fileOffset = vad->FileOffset + pageDelta; + if (fileOffset < vad->FileOffset) + return STATUS_INVALID_PARAMETER; + + size_t got = 0; + KSTATUS s = IoReadFileAt(vad->FileObject, fileOffset, pageVa, toRead, &got); + if (!KSUCCESS(s)) { + gVadStats.FileBackedReadFailures++; + return s; + } + gVadStats.FileBackedReads++; + if (got < toRead) + memset((uint8_t*)pageVa + got, 0, toRead - got); + return STATUS_SUCCESS; +} + + +// Materializes one image-backed prototype page or zero-fill page for an image VAD. +static KSTATUS KiVadFillImageBackedPage(MM_VAD* vad, size_t pageIndex, int wantWrite) { + if (!vad || !vad->ImageSection || pageIndex >= vad->PageCount) + return STATUS_INVALID_PARAMETER; + + MM_VAD_PAGE* page = &vad->Pages[pageIndex]; + uintptr_t va = vad->Base + pageIndex * PAGE_SIZE; + + PHYSICAL_ADDRESS frame = 0; + KSTATUS s = LdrRetainImageSectionPage(vad->ImageSection, pageIndex, &frame); + if (!KSUCCESS(s)) + return s; + + if (frame) { + page->Frame = frame; + page->Resident = 1; + page->Flags &= ~(MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SWAPPED); + page->Flags |= MM_VAD_PAGE_IMAGE_BACKED | MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_COW; + page->SwapSlot = 0xFFFFFFFFu; + + s = KiVadMapPageTracked(va, frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + page->Frame = 0; + page->Resident = 0; + page->Flags &= ~(MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_COW); + (void)MmPfnRelease(frame); + return s; + } + + gVadStats.ImageBackedFaults++; + return STATUS_SUCCESS; + } + + if (!wantWrite && gVadZeroPageFrame) { + page->Frame = gVadZeroPageFrame; + page->Resident = 1; + page->Flags |= MM_VAD_PAGE_IMAGE_BACKED | MM_VAD_PAGE_ZERO_PAGE; + if (page->PageFlags & PAGE_WRITE) + page->Flags |= MM_VAD_PAGE_COW; + page->SwapSlot = 0xFFFFFFFFu; + + s = KiVadMapPageTracked(va, page->Frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + page->Frame = 0; + page->Resident = 0; + page->Flags &= ~(MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_COW); + return s; + } + + gVadStats.ImageBackedZeroFaults++; + return STATUS_SUCCESS; + } + + frame = MmPfnAllocate(MmPfnUseImage); + if (!frame) + return STATUS_NO_MEMORY; + + void* pageVa = MmPfnToVirtual(frame); + if (!pageVa) { + MmPfnFree(frame); + return STATUS_FAILURE; + } + memset(pageVa, 0, PAGE_SIZE); + + page->Frame = frame; + page->Resident = 1; + page->Flags &= ~(MM_VAD_PAGE_COW | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_SWAPPED); + page->Flags |= MM_VAD_PAGE_IMAGE_BACKED; + page->SwapSlot = 0xFFFFFFFFu; + + s = KiVadMapPageTracked(va, frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + page->Frame = 0; + page->Resident = 0; + MmPfnFree(frame); + return s; + } + + gVadStats.ImageBackedPrivateFaults++; + return STATUS_SUCCESS; +} + +// Materializes one committed VAD page as either the shared zero page or a private frame. +static KSTATUS KiVadEnsurePageResident(MM_VAD* vad, size_t pageIndex, int wantWrite, int wantExecute) { + if (!vad || pageIndex >= vad->PageCount) + return STATUS_NOT_FOUND; + + MM_VAD_PAGE* page = &vad->Pages[pageIndex]; +retry_after_paging: + KiVadWaitForPagingComplete(page); + + KSTATUS s = KiVadCheckPageAccess(page, wantWrite, wantExecute); + if (!KSUCCESS(s)) + return s; + + uintptr_t va = vad->Base + pageIndex * PAGE_SIZE; + if (page->Resident) { + gVadStats.AlreadyResidentPages++; + return STATUS_SUCCESS; + } + + if (!KiVadTryBeginPaging(page)) + goto retry_after_paging; + + // Re-check after claiming the transition bit. Another CPU may have resolved + // the page between our initial wait/access check and the CAS above. + s = KiVadCheckPageAccess(page, wantWrite, wantExecute); + if (!KSUCCESS(s)) { + KiVadEndPaging(page); + return s; + } + if (page->Resident) { + KiVadEndPaging(page); + gVadStats.AlreadyResidentPages++; + return STATUS_SUCCESS; + } + + if ((page->Flags & MM_VAD_PAGE_SWAPPED) && page->SwapSlot != 0xFFFFFFFFu) { + uint32_t oldSlot = page->SwapSlot; + + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseAnonymous); + if (!frame) { + KiVadEndPaging(page); + return STATUS_NO_MEMORY; + } + + KSTATUS swapStatus = MmSwapPageIn(oldSlot, frame); + if (!KSUCCESS(swapStatus)) { + MmPfnFree(frame); + KiVadEndPaging(page); + return swapStatus; + } + + page->Frame = frame; + page->Resident = 1; + page->Flags &= ~MM_VAD_PAGE_SWAPPED; + page->SwapSlot = 0xFFFFFFFFu; + + KSTATUS mapStatus = KiVadMapPageTracked(va, frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(mapStatus)) { + page->Frame = 0; + page->Resident = 0; + page->Flags |= MM_VAD_PAGE_SWAPPED; + page->SwapSlot = oldSlot; + KiVadEndPaging(page); + MmPfnFree(frame); + return mapStatus; + } + + KiVadEndPaging(page); + MmSwapFreeSlot(oldSlot); + gVadStats.SwapPageIns++; + return STATUS_SUCCESS; + } + + if (page->Flags & MM_VAD_PAGE_IMAGE_BACKED) { + s = KiVadFillImageBackedPage(vad, pageIndex, wantWrite); + KiVadEndPaging(page); + return s; + } + + if (page->Flags & MM_VAD_PAGE_FILE_BACKED) { + PHYSICAL_ADDRESS frame = 0; + MM_FILE_CACHE_PAGE cachePage; + memset(&cachePage, 0, sizeof(cachePage)); + + uint64_t fileOffset = vad->FileOffset + (uint64_t)pageIndex * (uint64_t)PAGE_SIZE; + if (fileOffset < vad->FileOffset) { + KiVadEndPaging(page); + return STATUS_INVALID_PARAMETER; + } + + if (page->Flags & MM_VAD_PAGE_FILE_SHARED_WRITE) { + uintptr_t f = KeAcquireSpinlock(&vad->FileObject->Lock); + uint64_t oldSize = vad->FileObject->Size; + KeReleaseSpinlock(&vad->FileObject->Lock, f); + int needExisting = fileOffset < oldSize; + s = MmFileCacheGetWritablePage(vad->FileObject, fileOffset, oldSize, needExisting, &cachePage); + if (!KSUCCESS(s)) { + gVadStats.FileBackedReadFailures++; + KiVadEndPaging(page); + return s; + } + frame = cachePage.Frame; + page->Flags |= MM_VAD_PAGE_FILE_DIRTY_TRACK; + gVadStats.FileBackedSharedWritableFaults++; + } else if ((page->PageFlags & PAGE_WRITE) == 0) { + s = MmFileCacheGetPage(vad->FileObject, fileOffset, &cachePage); + if (!KSUCCESS(s)) { + gVadStats.FileBackedReadFailures++; + KiVadEndPaging(page); + return s; + } + frame = cachePage.Frame; + } else { + frame = MmPfnAllocate(MmPfnUseMappedFile); + if (!frame) { + KiVadEndPaging(page); + return STATUS_NO_MEMORY; + } + + s = KiVadFillFileBackedPage(vad, pageIndex, frame); + if (!KSUCCESS(s)) { + KiVadEndPaging(page); + MmPfnFree(frame); + return s; + } + } + + page->Frame = frame; + page->Resident = 1; + page->Flags &= ~(MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_SWAPPED); + page->SwapSlot = 0xFFFFFFFFu; + + s = KiVadMapPageTracked(va, frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + page->Frame = 0; + page->Resident = 0; + KiVadEndPaging(page); + if (cachePage.Frame) MmFileCacheReleasePage(&cachePage); + else MmPfnFree(frame); + return s; + } + + KiVadEndPaging(page); + gVadStats.FileBackedFaults++; + return STATUS_SUCCESS; + } + + if (!wantWrite && gVadZeroPageFrame) { + uint32_t savedFlags = page->Flags; + page->Frame = gVadZeroPageFrame; + page->Resident = 1; + page->Flags |= MM_VAD_PAGE_ZERO_PAGE; + if (page->PageFlags & PAGE_WRITE) + page->Flags |= MM_VAD_PAGE_COW; + + s = KiVadMapPageTracked(va, page->Frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + page->Frame = 0; + page->Resident = 0; + page->Flags = savedFlags; + KiVadEndPaging(page); + return s; + } + + KiVadEndPaging(page); + gVadStats.ZeroPageMaps++; + return STATUS_SUCCESS; + } + + PHYSICAL_ADDRESS frame = MmPfnAllocate(MmPfnUseAnonymous); + if (!frame) { + KiVadEndPaging(page); + return STATUS_NO_MEMORY; + } + + void* pageVa = MmPfnToVirtual(frame); + if (!pageVa) { + KiVadEndPaging(page); + MmPfnFree(frame); + return STATUS_FAILURE; + } + memset(pageVa, 0, PAGE_SIZE); + + page->Frame = frame; + page->Resident = 1; + page->Flags &= ~(MM_VAD_PAGE_COW | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_SWAPPED); + page->SwapSlot = 0xFFFFFFFFu; + + s = KiVadMapPageTracked(va, frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + page->Frame = 0; + page->Resident = 0; + KiVadEndPaging(page); + MmPfnFree(frame); + return s; + } + + KiVadEndPaging(page); + gVadStats.DemandZeroPages++; + return STATUS_SUCCESS; +} + +// Splits a copy-on-write page so this VAD receives a private writable frame. +static KSTATUS KiVadBreakCopyOnWrite(MM_VAD* vad, size_t pageIndex) { + if (!vad || pageIndex >= vad->PageCount) + return STATUS_NOT_FOUND; + + MM_VAD_PAGE* page = &vad->Pages[pageIndex]; + if (page->State != MmVadPageCommitted || !(page->Flags & MM_VAD_PAGE_COW)) + return STATUS_ACCESS_DENIED; + if (!page->Resident || !page->Frame) + return STATUS_NOT_FOUND; + if ((page->PageFlags & PAGE_WRITE) == 0) + return STATUS_ACCESS_DENIED; + + PHYSICAL_ADDRESS newFrame = MmPfnAllocate(MmPfnUseCow); + if (!newFrame) + return STATUS_NO_MEMORY; + + void* newPage = MmPfnToVirtual(newFrame); + if (!newPage) { + MmPfnFree(newFrame); + return STATUS_FAILURE; + } + + if (page->Flags & MM_VAD_PAGE_ZERO_PAGE) { + memset(newPage, 0, PAGE_SIZE); + gVadStats.ZeroPageCowSplits++; + } else { + void* oldPage = MmPfnToVirtual(page->Frame); + if (!oldPage) { + MmPfnFree(newFrame); + return STATUS_FAILURE; + } + memcpy(newPage, oldPage, PAGE_SIZE); + } + + uintptr_t va = vad->Base + pageIndex * PAGE_SIZE; + KSTATUS s = KiVadUnmapPageTracked(va, page->Frame); + if (!KSUCCESS(s)) { + MmPfnFree(newFrame); + return s; + } + + s = KiVadMapPageTracked(va, newFrame, page->PageFlags, MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + (void)KiVadMapPageTracked(va, page->Frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + MmPfnFree(newFrame); + return s; + } + + if (!(page->Flags & MM_VAD_PAGE_ZERO_PAGE)) + (void)MmPfnRelease(page->Frame); + page->Frame = newFrame; + page->Flags &= ~(MM_VAD_PAGE_COW | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_IMAGE_BACKED); + page->Resident = 1; + gVadStats.CowPagesCopied++; + return STATUS_SUCCESS; +} + +// Ensures that every committed page in a range is resident and accessible. +static KSTATUS KiVadEnsureResidentRange(PROCESS* process, uintptr_t start, size_t size, uint64_t accessFlags) { + if (!process || !size) + return STATUS_INVALID_PARAMETER; + if (start > (uintptr_t)-1 - (size - 1)) + return STATUS_NO_MEMORY; + + uintptr_t alignedStart = start & PAGE_MASK; + uintptr_t alignedEnd = (start + size + PAGE_SIZE - 1) & PAGE_MASK; + if (alignedEnd <= alignedStart) + return STATUS_NO_MEMORY; + + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindReservedVadContainingLocked(process, alignedStart, alignedEnd - alignedStart); + KeReleaseSpinlock(&process->VadLock, procFlags); + if (!vad) + return STATUS_NOT_FOUND; + + int wantWrite = (accessFlags & PAGE_WRITE) ? 1 : 0; + int wantExecute = (accessFlags & PAGE_NX) ? 1 : 0; + size_t first = KiVadPageIndex(vad, alignedStart); + size_t count = KiVadRangePageCount(alignedStart, alignedEnd); + if (first > vad->PageCount || count > vad->PageCount - first) + return STATUS_ACCESS_DENIED; + + gVadStats.EnsureResidentCalls++; + for (size_t i = 0; i < count; ++i) { + KSTATUS s = KiVadEnsurePageResident(vad, first + i, wantWrite, wantExecute); + if (!KSUCCESS(s)) + return s; + if (wantWrite && (vad->Pages[first + i].Flags & MM_VAD_PAGE_COW)) { + s = KiVadBreakCopyOnWrite(vad, first + i); + if (!KSUCCESS(s)) + return s; + } + gVadStats.EnsureResidentPages++; + } + + return STATUS_SUCCESS; +} + +// Makes an image VAD range private before the loader patches relocations or imports. +KSTATUS MmVadMakeImageRangePrivate(PROCESS* process, void* base, size_t size) { + KeAssertIrql(PASSIVE_LEVEL); + if (!process || !base || !size) + return STATUS_INVALID_PARAMETER; + + uintptr_t start = (uintptr_t)base; + if (start > (uintptr_t)-1 - (size - 1)) + return STATUS_NO_MEMORY; + + uintptr_t alignedStart = start & PAGE_MASK; + uintptr_t alignedEnd = (start + size + PAGE_SIZE - 1) & PAGE_MASK; + if (alignedEnd <= alignedStart) + return STATUS_NO_MEMORY; + + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindReservedVadContainingLocked(process, alignedStart, alignedEnd - alignedStart); + KeReleaseSpinlock(&process->VadLock, procFlags); + if (!vad) + return STATUS_NOT_FOUND; + if (vad->Type != MmVadImage) + return STATUS_INVALID_PARAMETER; + + size_t first = KiVadPageIndex(vad, alignedStart); + size_t count = KiVadRangePageCount(alignedStart, alignedEnd); + if (first > vad->PageCount || count > vad->PageCount - first) + return STATUS_ACCESS_DENIED; + + gVadStats.ImageFixupPrivateCalls++; + for (size_t i = 0; i < count; ++i) { + MM_VAD_PAGE* page = &vad->Pages[first + i]; + + KSTATUS s = KiVadEnsurePageResident(vad, first + i, 1, 0); + if (!KSUCCESS(s)) + return s; + + if (page->Flags & MM_VAD_PAGE_COW) { + s = KiVadBreakCopyOnWrite(vad, first + i); + if (!KSUCCESS(s)) + return s; + } + + if ((page->Flags & MM_VAD_PAGE_IMAGE_PRIVATE) == 0) { + page->Flags |= MM_VAD_PAGE_IMAGE_PRIVATE | MM_VAD_PAGE_IMAGE_FIXUP; + page->Flags &= ~(MM_VAD_PAGE_IMAGE_BACKED | MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_ZERO_PAGE); + gVadStats.ImageFixupPrivatePages++; + } else { + page->Flags |= MM_VAD_PAGE_IMAGE_FIXUP; } } + + return STATUS_SUCCESS; } -KSTATUS MmCreateUserRegion(PROCESS* process, size_t size, size_t alignment, - uint64_t pageFlags, uint32_t regionFlags, - USER_REGION_TYPE type, void** outBase) { +// Pins the user page that contains the supplied address after materializing it. +// The returned KernelAddress is a direct-map alias for the physical frame, so +// callers do not continue touching the original user VA after validation. This +// closes the unmap-after-pin race and is the primitive future zero-copy-ish I/O +// paths should build on. +static KSTATUS KiVadPinUserPage(PROCESS* process, uintptr_t address, + uint64_t accessFlags, MM_PINNED_USER_PAGE* out) { + if (!process || !out) + return STATUS_INVALID_PARAMETER; + memset(out, 0, sizeof(*out)); + + if (!KiUserRangeValid(address, 1)) + return STATUS_ACCESS_DENIED; + + uintptr_t pageBase = address & PAGE_MASK; + KSTATUS s = KiVadEnsureResidentRange(process, pageBase, PAGE_SIZE, accessFlags | PAGE_USER); + if (!KSUCCESS(s)) + return s; + + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindReservedVadContainingLocked(process, pageBase, PAGE_SIZE); + PHYSICAL_ADDRESS frame = 0; + uint64_t pageFlags = 0; + if (vad) { + size_t index = KiVadPageIndex(vad, pageBase); + if (index < vad->PageCount) { + MM_VAD_PAGE* page = &vad->Pages[index]; + if (page->State == MmVadPageCommitted && page->Resident && page->Frame && page->PageFlags) { + frame = page->Frame; + pageFlags = page->PageFlags; + } + } + } + KeReleaseSpinlock(&process->VadLock, procFlags); + + if (!frame) + return STATUS_NOT_FOUND; + + s = MmPfnPin(frame); + if (!KSUCCESS(s)) + return s; + + procFlags = KeAcquireSpinlock(&process->VadLock); + vad = KiFindReservedVadContainingLocked(process, pageBase, PAGE_SIZE); + int stillValid = 0; + if (vad) { + size_t index = KiVadPageIndex(vad, pageBase); + if (index < vad->PageCount) { + MM_VAD_PAGE* page = &vad->Pages[index]; + stillValid = page->State == MmVadPageCommitted && + page->Resident && + page->Frame == frame && + page->PageFlags != 0 && + ((accessFlags & PAGE_WRITE) == 0 || (page->PageFlags & PAGE_WRITE)); + if (stillValid) + pageFlags = page->PageFlags; + } + } + KeReleaseSpinlock(&process->VadLock, procFlags); + + if (!stillValid) { + (void)MmPfnUnpin(frame); + return STATUS_NOT_FOUND; + } + + out->UserPage = pageBase; + out->PageOffset = (size_t)(address & (PAGE_SIZE - 1)); + out->Frame = frame; + out->KernelAddress = MmPfnToVirtual(frame); + out->PageFlags = pageFlags; + if (!out->KernelAddress) { + MmUnpinUserPage(out); + return STATUS_FAILURE; + } + return STATUS_SUCCESS; +} + +__declspec(dllexport) KSTATUS MmPinUserPageForRead(PROCESS* process, const void* userAddress, MM_PINNED_USER_PAGE* out) { + return KiVadPinUserPage(process, (uintptr_t)userAddress, PAGE_PRESENT | PAGE_USER, out); +} + +__declspec(dllexport) KSTATUS MmPinUserPageForWrite(PROCESS* process, void* userAddress, MM_PINNED_USER_PAGE* out) { + return KiVadPinUserPage(process, (uintptr_t)userAddress, PAGE_PRESENT | PAGE_USER | PAGE_WRITE, out); +} + +__declspec(dllexport) void MmUnpinUserPage(MM_PINNED_USER_PAGE* page) { + if (!page || !page->Frame) + return; + (void)MmPfnUnpin(page->Frame); + memset(page, 0, sizeof(*page)); +} + +typedef struct MM_LOCKED_USER_BUFFER { + PROCESS* Process; + void* UserAddress; + size_t Size; + size_t PageCount; + int WriteAccess; + MM_PINNED_USER_PAGE Pages[1]; +} MM_LOCKED_USER_BUFFER; + +// Returns the per-page VAD lock array that follows the pinned-page array. +static MM_VAD** KiLockedUserBufferVads(MM_LOCKED_USER_BUFFER* buffer) { + return buffer ? (MM_VAD**)(buffer->Pages + buffer->PageCount) : (MM_VAD**)0; +} + +// Adds one outstanding user-buffer lock to the VAD that owns a page. +static KSTATUS KiVadAddUserBufferPageLock(PROCESS* process, uintptr_t pageBase, MM_VAD** vadOut) { + if (vadOut) *vadOut = (MM_VAD*)0; + if (!process || !vadOut) + return STATUS_INVALID_PARAMETER; + + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindReservedVadContainingLocked(process, pageBase, PAGE_SIZE); + if (!vad) { + KeReleaseSpinlock(&process->VadLock, procFlags); + return STATUS_NOT_FOUND; + } + if (vad->LockCount == UINT32_MAX) { + KeReleaseSpinlock(&process->VadLock, procFlags); + return STATUS_FAILURE; + } + vad->LockCount++; + *vadOut = vad; + KeReleaseSpinlock(&process->VadLock, procFlags); + return STATUS_SUCCESS; +} + +// Removes one outstanding user-buffer lock from a VAD. +static void KiVadRemoveUserBufferPageLock(PROCESS* process, MM_VAD* vad) { + if (!process || !vad) + return; + + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + if (vad->LockCount) + vad->LockCount--; + KeReleaseSpinlock(&process->VadLock, procFlags); +} + +// Locks a user buffer for asynchronous kernel access. +__declspec(dllexport) KSTATUS MmLockUserBuffer(PROCESS* process, + void* userAddress, + size_t size, + int writeAccess, + MM_LOCKED_USER_BUFFER** out) { + if (!out) + return STATUS_INVALID_PARAMETER; + *out = (MM_LOCKED_USER_BUFFER*)0; + + if (size == 0) + return STATUS_SUCCESS; + if (!process || !userAddress) + return STATUS_INVALID_PARAMETER; + + uintptr_t start = (uintptr_t)userAddress; + if (!KiUserRangeValid(start, size)) + return STATUS_ACCESS_DENIED; + + uintptr_t alignedStart = start & PAGE_MASK; + uintptr_t alignedEnd = (start + size + PAGE_SIZE - 1) & PAGE_MASK; + if (alignedEnd <= alignedStart) + return STATUS_NO_MEMORY; + + size_t pageCount = KiVadRangePageCount(alignedStart, alignedEnd); + if (!pageCount) + return STATUS_INVALID_PARAMETER; + if (pageCount > (((size_t)-1) - sizeof(MM_LOCKED_USER_BUFFER)) / + (sizeof(MM_PINNED_USER_PAGE) + sizeof(MM_VAD*))) + return STATUS_NO_MEMORY; + + size_t allocSize = sizeof(MM_LOCKED_USER_BUFFER) + + (pageCount - 1) * sizeof(MM_PINNED_USER_PAGE) + + pageCount * sizeof(MM_VAD*); + MM_LOCKED_USER_BUFFER* buffer = (MM_LOCKED_USER_BUFFER*)MmAllocatePoolTag(allocSize, MM_POOL_TAG('u', 'l', 'o', 'k')); + if (!buffer) + return STATUS_NO_MEMORY; + + memset(buffer, 0, allocSize); + buffer->Process = process; + buffer->UserAddress = userAddress; + buffer->Size = size; + buffer->PageCount = pageCount; + buffer->WriteAccess = writeAccess ? 1 : 0; + ObReferenceObject(&process->Header); + + MM_VAD** vads = KiLockedUserBufferVads(buffer); + KSTATUS s = STATUS_SUCCESS; + size_t lockedPages = 0; + for (size_t i = 0; i < pageCount; ++i) { + uintptr_t pageBase = alignedStart + i * PAGE_SIZE; + s = KiVadAddUserBufferPageLock(process, pageBase, &vads[i]); + if (!KSUCCESS(s)) + break; + + s = writeAccess + ? MmPinUserPageForWrite(process, (void*)pageBase, &buffer->Pages[i]) + : MmPinUserPageForRead(process, (const void*)pageBase, &buffer->Pages[i]); + if (!KSUCCESS(s)) { + KiVadRemoveUserBufferPageLock(process, vads[i]); + vads[i] = (MM_VAD*)0; + break; + } + lockedPages++; + } + + if (!KSUCCESS(s)) { + for (size_t i = 0; i < lockedPages; ++i) + MmUnpinUserPage(&buffer->Pages[i]); + for (size_t i = 0; i < pageCount; ++i) + KiVadRemoveUserBufferPageLock(process, vads[i]); + ObDereferenceObject(&process->Header); + MmFreePool(buffer); + return s; + } + + *out = buffer; + return STATUS_SUCCESS; +} + +// Unlocks a user buffer locked for asynchronous kernel access. +__declspec(dllexport) void MmUnlockUserBuffer(MM_LOCKED_USER_BUFFER* buffer) { + if (!buffer) + return; + + MM_VAD** vads = KiLockedUserBufferVads(buffer); + for (size_t i = 0; i < buffer->PageCount; ++i) + MmUnpinUserPage(&buffer->Pages[i]); + for (size_t i = 0; i < buffer->PageCount; ++i) + KiVadRemoveUserBufferPageLock(buffer->Process, vads[i]); + + if (buffer->Process) { + ObDereferenceObject(&buffer->Process->Header); + buffer->Process = (PROCESS*)0; + } + MmFreePool(buffer); +} + +// Copies user memory one page at a time while the current page is pinned. +// The pin is short-lived and exists only to keep pressure reclaim from removing +// the PTE while the kernel is executing the memcpy. +static KSTATUS KiCopyUserPaged(void* dst, const void* src, size_t size, int toUser) { + if (size == 0) + return STATUS_SUCCESS; + if (!dst || !src) + return STATUS_INVALID_PARAMETER; + + uintptr_t user = toUser ? (uintptr_t)dst : (uintptr_t)src; + if (!KiUserRangeValid(user, size)) + return STATUS_ACCESS_DENIED; + + PROCESS* process = KeGetCurrentProcess(); + if (!process) + return STATUS_ACCESS_DENIED; + + uint8_t* d = (uint8_t*)dst; + const uint8_t* s = (const uint8_t*)src; + size_t done = 0; + uint64_t access = PAGE_PRESENT | PAGE_USER | (toUser ? PAGE_WRITE : 0); + + while (done < size) { + uintptr_t userAddress = user + done; + size_t pageOffset = (size_t)(userAddress & (PAGE_SIZE - 1)); + size_t chunk = PAGE_SIZE - pageOffset; + if (chunk > size - done) + chunk = size - done; + + MM_PINNED_USER_PAGE pinned; + KSTATUS st = toUser + ? MmPinUserPageForWrite(process, (void*)userAddress, &pinned) + : MmPinUserPageForRead(process, (const void*)userAddress, &pinned); + if (!KSUCCESS(st)) + return st; + + uint8_t* kernelPage = (uint8_t*)pinned.KernelAddress; + if (toUser) + memcpy(kernelPage + pinned.PageOffset, s + done, chunk); + else + memcpy(d + done, kernelPage + pinned.PageOffset, chunk); + + MmUnpinUserPage(&pinned); + done += chunk; + } + + return STATUS_SUCCESS; +} + +// Copies from user memory after pinning each source page for the duration of the copy. +static KSTATUS KiCopyFromUserPaged(void* kernelDst, const void* userSrc, size_t size) { + return KiCopyUserPaged(kernelDst, userSrc, size, 0); +} + +// Copies to user memory after pinning each destination page for the duration of the copy. +static KSTATUS KiCopyToUserPaged(void* userDst, const void* kernelSrc, size_t size) { + return KiCopyUserPaged(userDst, kernelSrc, size, 1); +} + + +// Reserves a VAD in the global user VA arena without committing physical pages. +KSTATUS MmVadReserve(PROCESS* process, size_t size, size_t alignment, + uint64_t pageFlags, uint32_t vadFlags, + MM_VAD_TYPE type, void** outBase) { KeAssertIrql(PASSIVE_LEVEL); if (!process || !outBase || !size) return STATUS_INVALID_PARAMETER; *outBase = (void*)0; - if (!(pageFlags & PAGE_USER)) pageFlags |= PAGE_USER; - if (!(pageFlags & PAGE_PRESENT)) pageFlags |= PAGE_PRESENT; - size_t payloadSize = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); if (payloadSize < size) return STATUS_NO_MEMORY; - size_t guardLow = (regionFlags & USER_REGION_GUARD_LOW) ? PAGE_SIZE : 0; - size_t guardHigh = (regionFlags & USER_REGION_GUARD_HIGH) ? PAGE_SIZE : 0; + size_t guardLow = (vadFlags & MM_VAD_GUARD_LOW) ? PAGE_SIZE : 0; + size_t guardHigh = (vadFlags & MM_VAD_GUARD_HIGH) ? PAGE_SIZE : 0; size_t reservationSize = guardLow + payloadSize + guardHigh; if (reservationSize < payloadSize) return STATUS_NO_MEMORY; if (alignment < PAGE_SIZE) alignment = PAGE_SIZE; if (alignment & (alignment - 1)) return STATUS_INVALID_PARAMETER; - USER_VA_REGION* r = (USER_VA_REGION*)MmAllocatePool(sizeof(USER_VA_REGION)); - if (!r) return STATUS_NO_MEMORY; - memset(r, 0, sizeof(*r)); - LIST_INIT(&r->ProcessLink); - LIST_INIT(&r->GlobalLink); + size_t pageCount = payloadSize / PAGE_SIZE; + if (!pageCount) return STATUS_INVALID_PARAMETER; + + if (pageFlags && !(pageFlags & PAGE_USER)) pageFlags |= PAGE_USER; + + MM_VAD* vad = (MM_VAD*)MmAllocatePoolTag(sizeof(MM_VAD), MM_POOL_TAG('v', 'a', 'd', ' ')); + if (!vad) return STATUS_NO_MEMORY; + memset(vad, 0, sizeof(*vad)); + + MM_VAD_PAGE* pages = (MM_VAD_PAGE*)MmAllocatePoolTag(pageCount * sizeof(MM_VAD_PAGE), MM_POOL_TAG('v', 'p', 'g', 's')); + if (!pages) { + MmFreePool(vad); + return STATUS_NO_MEMORY; + } + memset(pages, 0, pageCount * sizeof(MM_VAD_PAGE)); + + LIST_INIT(&vad->ProcessLink); + LIST_INIT(&vad->GlobalLink); - uintptr_t lockFlags = KeAcquireSpinlock(&gUserRegionLock); uintptr_t reservationBase = 0; - KSTATUS s = KiFindUserVaRange(reservationSize, alignment, &reservationBase); + uintptr_t globalFlags = KeAcquireSpinlock(&gVadLock); + KSTATUS s = KiFindVadRange(reservationSize, alignment, &reservationBase); if (KSUCCESS(s)) { - r->Owner = process; - r->ReservationBase = reservationBase; - r->ReservationSize = reservationSize; - r->Base = reservationBase + guardLow; - r->Size = size; - r->CommittedSize = payloadSize; - r->PageFlags = pageFlags; - r->Type = type; - r->Flags = regionFlags; - ListInsertTail(&gUserRegionList, &r->GlobalLink); + vad->Owner = process; + vad->ReservationBase = reservationBase; + vad->ReservationSize = reservationSize; + vad->Base = reservationBase + guardLow; + vad->Size = size; + vad->PayloadSize = payloadSize; + vad->PageCount = pageCount; + vad->CommittedPages = 0; + vad->PageFlags = pageFlags; + vad->Type = type; + vad->Flags = vadFlags; + vad->Pages = pages; + ListInsertTail(&gVadList, &vad->GlobalLink); } - KeReleaseSpinlock(&gUserRegionLock, lockFlags); + KeReleaseSpinlock(&gVadLock, globalFlags); if (!KSUCCESS(s)) { - MmFreePool(r); + MmFreePool(pages); + MmFreePool(vad); return s; } - uintptr_t va = r->Base; - for (; va < r->Base + payloadSize; va += PAGE_SIZE) { - PHYSICAL_ADDRESS frame = MmAllocateFrame(); - if (!frame) { - s = STATUS_NO_MEMORY; - break; - } - s = MmMapPage(va, frame, pageFlags, MmMapFailIfPresent); - if (!KSUCCESS(s)) { - MmFreeFrame(frame); - break; - } - } + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + ListInsertTail(&process->VadList, &vad->ProcessLink); + KeReleaseSpinlock(&process->VadLock, procFlags); - if (!KSUCCESS(s)) { - r->CommittedSize = (va > r->Base) ? (size_t)(va - r->Base) : 0; - KiUnmapUserRegionPayload(r); - lockFlags = KeAcquireSpinlock(&gUserRegionLock); - ListRemove(&r->GlobalLink); - KeReleaseSpinlock(&gUserRegionLock, lockFlags); - MmFreePool(r); - return s; - } - - if (regionFlags & USER_REGION_ZERO) { - memset((void*)r->Base, 0, payloadSize); - } - - uintptr_t procFlags = KeAcquireSpinlock(&process->UserRegionLock); - ListInsertTail(&process->UserRegions, &r->ProcessLink); - KeReleaseSpinlock(&process->UserRegionLock, procFlags); - - *outBase = (void*)r->Base; + *outBase = (void*)vad->Base; return STATUS_SUCCESS; } -// Implements the ensure User Region Mapped operation. -KSTATUS MmEnsureUserRegionMapped(PROCESS* process, void* base, size_t size, uint64_t pageFlags) { +// Commits pages in an existing VAD without allocating PFNs until access. +KSTATUS MmVadCommit(PROCESS* process, void* base, size_t size, + uint64_t pageFlags, uint32_t commitFlags) { KeAssertIrql(PASSIVE_LEVEL); if (!process || !base || !size) return STATUS_INVALID_PARAMETER; @@ -1031,51 +1992,845 @@ KSTATUS MmEnsureUserRegionMapped(PROCESS* process, void* base, size_t size, uint uintptr_t alignedEnd = (start + size + PAGE_SIZE - 1) & PAGE_MASK; if (alignedEnd <= alignedStart) return STATUS_NO_MEMORY; - uintptr_t procFlags = KeAcquireSpinlock(&process->UserRegionLock); - USER_VA_REGION* r = KiFindUserRegionContainingLocked(process, alignedStart, alignedEnd - alignedStart); - uintptr_t regionBase = r ? r->Base : 0; - uintptr_t regionEnd = r ? (r->Base + r->CommittedSize) : 0; - uint64_t defaultFlags = r ? r->PageFlags : 0; - KeReleaseSpinlock(&process->UserRegionLock, procFlags); + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindReservedVadContainingLocked(process, alignedStart, alignedEnd - alignedStart); + KeReleaseSpinlock(&process->VadLock, procFlags); - if (!r) { - DbgPrint("MM: ensure user mapping failed: pid=%u range=%016llx-%016llx not inside owned committed region\n", + if (!vad) { + DbgPrint("MM: commit failed: pid=%u range=%016llx-%016llx not inside reserved VAD\n", process->ProcessId, (unsigned long long)alignedStart, (unsigned long long)alignedEnd); return STATUS_NOT_FOUND; } - uint64_t flags = pageFlags ? pageFlags : defaultFlags; - flags |= PAGE_PRESENT | PAGE_USER; + uint64_t flags = pageFlags ? pageFlags : vad->PageFlags; + if (flags) flags |= PAGE_PRESENT | PAGE_USER; - uint32_t committed = 0; - for (uintptr_t va = alignedStart; va < alignedEnd; va += PAGE_SIZE) { - if (va < regionBase || va >= regionEnd) return STATUS_ACCESS_DENIED; + size_t first = KiVadPageIndex(vad, alignedStart); + size_t count = KiVadRangePageCount(alignedStart, alignedEnd); + if (first > vad->PageCount || count > vad->PageCount - first) + return STATUS_ACCESS_DENIED; - PHYSICAL_ADDRESS phys = 0; - uint64_t existingFlags = 0; - KSTATUS qs = MmQueryPage(va, &phys, &existingFlags); - if (KSUCCESS(qs) && (existingFlags & PAGE_PRESENT)) + uint8_t* newlyCommitted = (uint8_t*)MmAllocatePool(count ? count : 1); + if (!newlyCommitted) return STATUS_NO_MEMORY; + memset(newlyCommitted, 0, count ? count : 1); + + KSTATUS s = STATUS_SUCCESS; + for (size_t i = 0; i < count; ++i) { + size_t pageIndex = first + i; + MM_VAD_PAGE* page = &vad->Pages[pageIndex]; + + if (page->State == MmVadPageCommitted) { + continue; + } + + page->State = MmVadPageCommitted; + page->PageFlags = flags; + page->Frame = 0; + page->Resident = 0; + page->Flags = 0; + page->SwapSlot = 0xFFFFFFFFu; + vad->CommittedPages++; + newlyCommitted[i] = 1; + } + + if ((commitFlags & MM_VAD_PREFETCH) && flags) { + s = KiVadEnsureResidentRange(process, alignedStart, alignedEnd - alignedStart, flags); + } + + if (!KSUCCESS(s)) { + for (size_t i = 0; i < count; ++i) { + if (newlyCommitted[i]) + KiVadReleasePage(vad, first + i); + } + } + + MmFreePool(newlyCommitted); + return s; +} + +// Reserves and commits a VAD in one step. +KSTATUS MmVadCreateCommitted(PROCESS* process, size_t size, size_t alignment, + uint64_t pageFlags, uint32_t vadFlags, + MM_VAD_TYPE type, void** outBase) { + if (!outBase) return STATUS_INVALID_PARAMETER; + *outBase = (void*)0; + + void* base = (void*)0; + KSTATUS s = MmVadReserve(process, size, alignment, pageFlags, vadFlags, type, &base); + if (!KSUCCESS(s)) return s; + + s = MmVadCommit(process, base, size, pageFlags, vadFlags); + if (!KSUCCESS(s)) { + MmVadDestroy(process, base); + return s; + } + + *outBase = base; + return STATUS_SUCCESS; +} + +// Creates a demand-paged file-backed user mapping. The VAD owns a reference to +// the FILE_OBJECT so the caller may close the original file handle immediately +// after a view is established. Pages are read from the file on first fault and +// then behave like ordinary private user pages: if memory pressure trims them +// after they are resident, the swap path preserves later private modifications. +KSTATUS MmVadCreateFileMapping(PROCESS* process, FILE_OBJECT* file, + uint64_t fileOffset, size_t size, + uint64_t pageFlags, uint32_t vadFlags, + void** outBase) { + KeAssertIrql(PASSIVE_LEVEL); + if (!process || !file || !size || !outBase) + return STATUS_INVALID_PARAMETER; + *outBase = (void*)0; + + if (fileOffset & (PAGE_SIZE - 1)) + return STATUS_INVALID_PARAMETER; + + size_t alignedSize = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + if (!alignedSize || alignedSize < size) + return STATUS_NO_MEMORY; + + if (pageFlags && !(pageFlags & PAGE_USER)) + pageFlags |= PAGE_USER; + pageFlags |= PAGE_PRESENT | PAGE_USER; + + void* base = (void*)0; + KSTATUS s = MmVadReserve(process, alignedSize, PAGE_SIZE, pageFlags, + vadFlags, MmVadMappedFile, &base); + if (!KSUCCESS(s)) + return s; + + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindVadByBaseLocked(process, base); + KeReleaseSpinlock(&process->VadLock, procFlags); + if (!vad) { + (void)MmVadDestroy(process, base); + return STATUS_NOT_FOUND; + } + + ObReferenceObject(&file->Header); + vad->FileObject = file; + vad->FileOffset = fileOffset; + vad->FileLength = size; + vad->FileMappingFlags = vadFlags; + + for (size_t i = 0; i < vad->PageCount; ++i) { + MM_VAD_PAGE* page = &vad->Pages[i]; + page->State = MmVadPageCommitted; + page->PageFlags = pageFlags; + page->Frame = 0; + page->Resident = 0; + page->Flags = MM_VAD_PAGE_FILE_BACKED; + if (vadFlags & MM_VAD_SHARED_WRITE) + page->Flags |= MM_VAD_PAGE_FILE_SHARED_WRITE | MM_VAD_PAGE_FILE_DIRTY_TRACK; + page->SwapSlot = 0xFFFFFFFFu; + vad->CommittedPages++; + } + + *outBase = base; + return STATUS_SUCCESS; +} + + +// Creates an image-backed VAD whose pages fault from a shared image section object. +KSTATUS MmVadCreateImageSectionMapping(PROCESS* process, IMAGE_SECTION_OBJECT* section, + uint64_t pageFlags, uint32_t vadFlags, + void** outBase) { + KeAssertIrql(PASSIVE_LEVEL); + if (!process || !section || !outBase) + return STATUS_INVALID_PARAMETER; + *outBase = (void*)0; + + const IMAGE_SECTION_METADATA* metadata = LdrImageSectionMetadata(section); + if (!metadata || !metadata->ImageSize || !metadata->PageCount) + return STATUS_INVALID_PARAMETER; + + pageFlags |= PAGE_PRESENT | PAGE_USER; + + void* base = (void*)0; + KSTATUS s = MmVadReserve(process, metadata->ImageSize, PAGE_SIZE, pageFlags, + vadFlags, MmVadImage, &base); + if (!KSUCCESS(s)) + return s; + + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindVadByBaseLocked(process, base); + KeReleaseSpinlock(&process->VadLock, procFlags); + if (!vad) { + (void)MmVadDestroy(process, base); + return STATUS_NOT_FOUND; + } + + LdrReferenceImageSection(section); + vad->ImageSection = section; + + for (size_t i = 0; i < vad->PageCount; ++i) { + MM_VAD_PAGE* page = &vad->Pages[i]; + page->State = MmVadPageCommitted; + page->PageFlags = pageFlags; + page->Frame = 0; + page->Resident = 0; + page->Flags = MM_VAD_PAGE_IMAGE_BACKED; + page->SwapSlot = 0xFFFFFFFFu; + vad->CommittedPages++; + } + + *outBase = base; + return STATUS_SUCCESS; +} + +// Materializes committed VAD pages before the kernel or a recovered user fault touches them. +KSTATUS MmVadEnsureResident(PROCESS* process, void* base, size_t size, uint64_t accessFlags) { + if (!process || !base || !size) + return STATUS_INVALID_PARAMETER; + return KiVadEnsureResidentRange(process, (uintptr_t)base, size, accessFlags | PAGE_USER); +} + +// Creates a private copy-on-write view of a resident committed VAD range. +KSTATUS MmVadCreateCowView(PROCESS* sourceProcess, void* sourceBase, size_t size, + PROCESS* targetProcess, void** outBase) { + KeAssertIrql(PASSIVE_LEVEL); + if (!sourceProcess || !sourceBase || !targetProcess || !outBase || !size) + return STATUS_INVALID_PARAMETER; + *outBase = (void*)0; + + uintptr_t start = (uintptr_t)sourceBase; + if (start & (PAGE_SIZE - 1)) + return STATUS_INVALID_PARAMETER; + size_t alignedSize = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + if (!alignedSize || alignedSize < size) + return STATUS_NO_MEMORY; + + KSTATUS s = KiVadEnsureResidentRange(sourceProcess, start, alignedSize, PAGE_USER); + if (!KSUCCESS(s)) + return s; + + uintptr_t srcFlags = KeAcquireSpinlock(&sourceProcess->VadLock); + MM_VAD* sourceVad = KiFindReservedVadContainingLocked(sourceProcess, start, alignedSize); + KeReleaseSpinlock(&sourceProcess->VadLock, srcFlags); + if (!sourceVad || !KiVadRangeCommitted(sourceVad, start, start + alignedSize)) + return STATUS_NOT_FOUND; + + void* targetBase = (void*)0; + s = MmVadReserve(targetProcess, alignedSize, PAGE_SIZE, sourceVad->PageFlags, sourceVad->Flags, sourceVad->Type, &targetBase); + if (!KSUCCESS(s)) + return s; + + uintptr_t dstFlags = KeAcquireSpinlock(&targetProcess->VadLock); + MM_VAD* targetVad = KiFindVadByBaseLocked(targetProcess, targetBase); + KeReleaseSpinlock(&targetProcess->VadLock, dstFlags); + if (!targetVad) { + (void)MmVadDestroy(targetProcess, targetBase); + return STATUS_NOT_FOUND; + } + + uintptr_t dst = (uintptr_t)targetBase; + size_t first = KiVadPageIndex(sourceVad, start); + size_t count = alignedSize / PAGE_SIZE; + + for (size_t i = 0; i < count; ++i) { + MM_VAD_PAGE* srcPage = &sourceVad->Pages[first + i]; + if (srcPage->State != MmVadPageCommitted || !srcPage->Resident || !srcPage->Frame) { + s = STATUS_NOT_FOUND; + break; + } + + s = MmPfnRetain(srcPage->Frame); + if (!KSUCCESS(s)) + break; + + MM_VAD_PAGE* dstPage = &targetVad->Pages[i]; + dstPage->State = MmVadPageCommitted; + dstPage->PageFlags = srcPage->PageFlags; + dstPage->Frame = srcPage->Frame; + dstPage->Resident = 1; + dstPage->Flags = MM_VAD_PAGE_SHARED_FRAME | ((srcPage->PageFlags & PAGE_WRITE) ? MM_VAD_PAGE_COW : 0); + dstPage->SwapSlot = 0xFFFFFFFFu; + targetVad->CommittedPages++; + + if (dstPage->Flags) { + srcPage->Flags |= MM_VAD_PAGE_COW | MM_VAD_PAGE_SHARED_FRAME; + if (srcPage->Resident && srcPage->PageFlags) + (void)MmPteProtectRange(sourceVad->Base + (first + i) * PAGE_SIZE, PAGE_SIZE, KiVadHardwarePageFlags(srcPage)); + } + + s = KiVadMapPageTracked(dst + i * PAGE_SIZE, dstPage->Frame, KiVadHardwarePageFlags(dstPage), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) + break; + + gVadStats.CowPagesShared++; + } + + if (!KSUCCESS(s)) { + (void)MmVadDestroy(targetProcess, targetBase); + return s; + } + + gVadStats.CowViewsCreated++; + *outBase = targetBase; + return STATUS_SUCCESS; +} + +// Creates a COW view from loader-owned template frames. +KSTATUS MmVadCreateCowViewFromFrames(PROCESS* targetProcess, + const PHYSICAL_ADDRESS* frames, + size_t pageCount, + size_t payloadSize, + uint64_t pageFlags, + uint32_t vadFlags, + MM_VAD_TYPE type, + void** outBase) { + KeAssertIrql(PASSIVE_LEVEL); + if (!targetProcess || !frames || !pageCount || !payloadSize || !outBase) + return STATUS_INVALID_PARAMETER; + *outBase = (void*)0; + + size_t alignedSize = (payloadSize + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + if (!alignedSize || alignedSize / PAGE_SIZE > pageCount) + return STATUS_INVALID_PARAMETER; + + void* targetBase = (void*)0; + KSTATUS s = MmVadReserve(targetProcess, alignedSize, PAGE_SIZE, pageFlags, vadFlags, type, &targetBase); + if (!KSUCCESS(s)) + return s; + + uintptr_t dstFlags = KeAcquireSpinlock(&targetProcess->VadLock); + MM_VAD* targetVad = KiFindVadByBaseLocked(targetProcess, targetBase); + KeReleaseSpinlock(&targetProcess->VadLock, dstFlags); + if (!targetVad) { + (void)MmVadDestroy(targetProcess, targetBase); + return STATUS_NOT_FOUND; + } + + uintptr_t dst = (uintptr_t)targetBase; + size_t count = alignedSize / PAGE_SIZE; + for (size_t i = 0; i < count; ++i) { + MM_VAD_PAGE* page = &targetVad->Pages[i]; + page->State = MmVadPageCommitted; + page->PageFlags = pageFlags | PAGE_PRESENT | PAGE_USER; + page->Frame = frames[i]; + page->Resident = frames[i] ? 1 : 0; + page->Flags = (frames[i] ? MM_VAD_PAGE_SHARED_FRAME : 0) | ((page->PageFlags & PAGE_WRITE) ? MM_VAD_PAGE_COW : 0); + page->SwapSlot = 0xFFFFFFFFu; + targetVad->CommittedPages++; + + if (!frames[i]) continue; - PHYSICAL_ADDRESS frame = MmAllocateFrame(); - if (!frame) return STATUS_NO_MEMORY; + s = MmPfnRetain(frames[i]); + if (!KSUCCESS(s)) + break; - KSTATUS ms = MmMapPage(va, frame, flags, MmMapFailIfPresent); - if (!KSUCCESS(ms)) { - MmFreeFrame(frame); - return ms; + s = KiVadMapPageTracked(dst + i * PAGE_SIZE, page->Frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + (void)MmPfnRelease(frames[i]); + break; } - memset((void*)va, 0, PAGE_SIZE); - committed++; + + gVadStats.CowPagesShared++; } + if (!KSUCCESS(s)) { + (void)MmVadDestroy(targetProcess, targetBase); + return s; + } + + gVadStats.CowViewsCreated++; + *outBase = targetBase; return STATUS_SUCCESS; } -// Implements the protect User Region operation. -KSTATUS MmProtectUserRegion(PROCESS* process, void* base, size_t size, uint64_t pageFlags) { +KSTATUS MmVadCreateSharedViewFromFrames(PROCESS* targetProcess, + const PHYSICAL_ADDRESS* frames, + size_t pageCount, + size_t payloadSize, + uint64_t pageFlags, + uint32_t vadFlags, + MM_VAD_TYPE type, + void** outBase) { + KeAssertIrql(PASSIVE_LEVEL); + if (!targetProcess || !frames || !pageCount || !payloadSize || !outBase) + return STATUS_INVALID_PARAMETER; + *outBase = (void*)0; + + size_t alignedSize = (payloadSize + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1); + if (!alignedSize || alignedSize / PAGE_SIZE > pageCount) + return STATUS_INVALID_PARAMETER; + + pageFlags |= PAGE_PRESENT | PAGE_USER; + + void* targetBase = (void*)0; + KSTATUS s = MmVadReserve(targetProcess, alignedSize, PAGE_SIZE, pageFlags, + vadFlags, type, &targetBase); + if (!KSUCCESS(s)) + return s; + + uintptr_t dstFlags = KeAcquireSpinlock(&targetProcess->VadLock); + MM_VAD* targetVad = KiFindVadByBaseLocked(targetProcess, targetBase); + KeReleaseSpinlock(&targetProcess->VadLock, dstFlags); + if (!targetVad) { + (void)MmVadDestroy(targetProcess, targetBase); + return STATUS_NOT_FOUND; + } + + uintptr_t dst = (uintptr_t)targetBase; + size_t count = alignedSize / PAGE_SIZE; + for (size_t i = 0; i < count; ++i) { + if (!frames[i]) { + s = STATUS_INVALID_PARAMETER; + break; + } + + s = MmPfnRetain(frames[i]); + if (!KSUCCESS(s)) + break; + + MM_VAD_PAGE* page = &targetVad->Pages[i]; + page->State = MmVadPageCommitted; + page->PageFlags = pageFlags; + page->Frame = frames[i]; + page->Resident = 1; + page->Flags = MM_VAD_PAGE_SHARED_FRAME; + page->SwapSlot = 0xFFFFFFFFu; + targetVad->CommittedPages++; + + s = KiVadMapPageTracked(dst + i * PAGE_SIZE, page->Frame, + KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + if (!KSUCCESS(s)) { + page->PageFlags = 0; + page->Frame = 0; + page->Resident = 0; + page->Flags = 0; + page->State = MmVadPageFree; + page->SwapSlot = 0xFFFFFFFFu; + if (targetVad->CommittedPages) + targetVad->CommittedPages--; + (void)MmPfnRelease(frames[i]); + break; + } + + gVadStats.CowPagesShared++; + } + + if (!KSUCCESS(s)) { + (void)MmVadDestroy(targetProcess, targetBase); + return s; + } + + gVadStats.CowViewsCreated++; + *outBase = targetBase; + return STATUS_SUCCESS; +} + +// Returns whether a resident file-backed page can be discarded instead of +// swapped. This is safe only while the page is still clean by construction: +// read-only mappings cannot have been modified, and the backing file can +// repopulate the page on the next fault. Writable/private file mappings still +// fall back to the swap path because this VMM does not yet track dirty bits. +static int KiVadPageCanDiscardCleanFile(const MM_VAD* vad, const MM_VAD_PAGE* page) { + if (!vad || !page || vad->Type != MmVadMappedFile) + return 0; + if (!vad->FileObject || page->State != MmVadPageCommitted) + return 0; + if (!page->Resident || !page->Frame || !page->PageFlags) + return 0; + if ((page->Flags & MM_VAD_PAGE_FILE_BACKED) == 0) + return 0; + if (page->PageFlags & PAGE_WRITE) + return 0; + if (page->Flags & (MM_VAD_PAGE_COW | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | + MM_VAD_PAGE_SWAPPED | MM_VAD_PAGE_PAGING)) + return 0; + return 1; +} + +static int KiVadPageCanDiscardCleanFileAfterPagingClaim(const MM_VAD* vad, const MM_VAD_PAGE* page) { + if (!vad || !page || vad->Type != MmVadMappedFile) + return 0; + if (!vad->FileObject || page->State != MmVadPageCommitted) + return 0; + if (!page->Resident || !page->Frame || !page->PageFlags) + return 0; + if ((page->Flags & MM_VAD_PAGE_FILE_BACKED) == 0) + return 0; + if (page->PageFlags & PAGE_WRITE) + return 0; + if (page->Flags & (MM_VAD_PAGE_COW | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | + MM_VAD_PAGE_SWAPPED)) + return 0; + return (page->Flags & MM_VAD_PAGE_PAGING) != 0; +} + +// Returns whether a resident VAD page is safe to move to swap. Clean read-only +// file-backed pages are intentionally excluded; the pressure trimmer discards +// them and reloads them from the file instead of wasting swap slots. +static int KiVadPageCanSwap(const MM_VAD_PAGE* page) { + if (!page || page->State != MmVadPageCommitted) + return 0; + if (!page->Resident || !page->Frame || !page->PageFlags) + return 0; + if ((page->Flags & MM_VAD_PAGE_FILE_BACKED) && ((page->PageFlags & PAGE_WRITE) == 0)) + return 0; + if (page->Flags & MM_VAD_PAGE_FILE_SHARED_WRITE) + return 0; + if (page->Flags & (MM_VAD_PAGE_COW | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | + MM_VAD_PAGE_SWAPPED | MM_VAD_PAGE_PAGING)) + return 0; + return 1; +} + +static int KiVadPageCanSwapAfterPagingClaim(const MM_VAD_PAGE* page) { + if (!page || page->State != MmVadPageCommitted) + return 0; + if (!page->Resident || !page->Frame || !page->PageFlags) + return 0; + if ((page->Flags & MM_VAD_PAGE_FILE_BACKED) && ((page->PageFlags & PAGE_WRITE) == 0)) + return 0; + if (page->Flags & MM_VAD_PAGE_FILE_SHARED_WRITE) + return 0; + if (page->Flags & (MM_VAD_PAGE_COW | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | + MM_VAD_PAGE_SWAPPED)) + return 0; + if ((page->Flags & MM_VAD_PAGE_PAGING) == 0) + return 0; + return 1; +} + +// Discards one resident clean file-backed page. No swap slot is allocated; the +// VAD keeps its FILE_BACKED state and refaults the page from the file later. +static KSTATUS KiVadDiscardCleanFilePage(MM_VAD* vad, size_t pageIndex, void** baseOut) { + if (baseOut) *baseOut = (void*)0; + if (!vad || pageIndex >= vad->PageCount) + return STATUS_INVALID_PARAMETER; + + MM_VAD_PAGE* page = &vad->Pages[pageIndex]; + if (!KiVadPageCanDiscardCleanFile(vad, page)) + return STATUS_NOT_FOUND; + + PHYSICAL_ADDRESS frame = page->Frame; + MM_PFN* pfn = MmPfnGet(frame); + if (!pfn || pfn->PinCount) + return STATUS_NOT_FOUND; + + uintptr_t va = vad->Base + pageIndex * PAGE_SIZE; + + // Second chance for hot mapped-file pages. Dropping the active video/WAD + // working set is much worse than swapping cold anonymous pages: it causes + // repeated file reads, page-table churn, and global TLB shootdowns. Clear + // the hardware accessed bit and skip the page once; if it stays cold until + // a later pressure scan, it may be discarded then. + if (MmPteTestAndClearAccessed(va)) + return STATUS_NOT_FOUND; + + if (!KiVadTryBeginPaging(page)) + return STATUS_NOT_FOUND; + + if (!KiVadPageCanDiscardCleanFileAfterPagingClaim(vad, page) || page->Frame != frame) { + KiVadEndPaging(page); + return STATUS_NOT_FOUND; + } + + KSTATUS s = MmPfnPin(frame); + if (!KSUCCESS(s)) { + KiVadEndPaging(page); + return s; + } + + s = KiVadUnmapPageTracked(va, frame); + if (!KSUCCESS(s)) { + KiVadEndPaging(page); + (void)MmPfnUnpin(frame); + return s; + } + + page->Frame = 0; + page->Resident = 0; + page->Flags &= ~(MM_VAD_PAGE_SWAPPED | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_COW); + page->Flags |= MM_VAD_PAGE_FILE_BACKED; + page->SwapSlot = 0xFFFFFFFFu; + KiVadEndPaging(page); + + (void)MmPfnUnpin(frame); + (void)MmPfnRelease(frame); + if (vad->FileObject) { + uint64_t fileOffset = vad->FileOffset + (uint64_t)pageIndex * (uint64_t)PAGE_SIZE; + MmFileCacheInvalidate(vad->FileObject, fileOffset, PAGE_SIZE); + } + + gVadStats.FileBackedDiscards++; + if (baseOut) *baseOut = (void*)va; + return STATUS_SUCCESS; +} + +// Moves one private resident VAD page to the swap backend. +static KSTATUS KiVadTrimPageToSwap(MM_VAD* vad, size_t pageIndex, void** baseOut) { + if (baseOut) *baseOut = (void*)0; + if (!vad || pageIndex >= vad->PageCount) + return STATUS_INVALID_PARAMETER; + + MM_VAD_PAGE* page = &vad->Pages[pageIndex]; + if (!KiVadPageCanSwap(page)) + return STATUS_NOT_FOUND; + + PHYSICAL_ADDRESS frame = page->Frame; + MM_PFN* pfn = MmPfnGet(frame); + if (!pfn || pfn->RefCount != 1 || pfn->PinCount) + return STATUS_NOT_FOUND; + + // Claim the transient paging bit atomically before pinning/removing the + // user PTE. This serializes page-out against another page-out and against + // a concurrent fault on the same VAD page without holding a spinlock across + // pagefile I/O. + if (!KiVadTryBeginPaging(page)) + return STATUS_NOT_FOUND; + + if (!KiVadPageCanSwapAfterPagingClaim(page) || page->Frame != frame) { + KiVadEndPaging(page); + return STATUS_NOT_FOUND; + } + + KSTATUS s = MmPfnPin(frame); + if (!KSUCCESS(s)) { + KiVadEndPaging(page); + return s; + } + + uintptr_t va = vad->Base + pageIndex * PAGE_SIZE; + s = KiVadUnmapPageTracked(va, frame); + if (!KSUCCESS(s)) { + KiVadEndPaging(page); + (void)MmPfnUnpin(frame); + return s; + } + + page->Resident = 0; + + uint32_t slot = 0; + s = MmSwapPageOut(frame, &slot); + if (!KSUCCESS(s)) { + page->Resident = 1; + KSTATUS mapStatus = KiVadMapPageTracked(va, frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent); + KiVadEndPaging(page); + (void)MmPfnUnpin(frame); + return KSUCCESS(mapStatus) ? s : mapStatus; + } + + page->Frame = 0; + page->Flags |= MM_VAD_PAGE_SWAPPED; + page->SwapSlot = slot; + KiVadEndPaging(page); + + (void)MmPfnUnpin(frame); + (void)MmPfnRelease(frame); + + gVadStats.SwapPageOuts++; + if (baseOut) *baseOut = (void*)va; + return STATUS_SUCCESS; +} + +// Finds one private resident page in a process and swaps it out. +static KSTATUS KiVadTrimOnePageInProcess(PROCESS* process, void** baseOut) { + if (baseOut) *baseOut = (void*)0; + if (!process) return STATUS_INVALID_PARAMETER; + + for (LIST_ENTRY* node = process->VadList.Fwd; node != &process->VadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, ProcessLink); + for (size_t i = 0; i < vad->PageCount; ++i) { + KSTATUS s = KiVadTrimPageToSwap(vad, i, baseOut); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + } + } + return STATUS_NOT_FOUND; +} + +// Moves one private resident user page to the swap backend for self-tests. +KSTATUS MmVadTrimOnePageForTest(PROCESS* process, void** baseOut) { + KeAssertIrql(PASSIVE_LEVEL); + if (!MmSwapIsEnabled()) return STATUS_NOT_SUPPORTED; + return KiVadTrimOnePageInProcess(process, baseOut); +} + +// Reclaims one resident user page from any process during allocation pressure. +// Prefer clean file-backed pages because they can be dropped and re-read without +// consuming swap. Fall back to anonymous/private swap only when no such page is +// available. +KSTATUS MmVadTrimOnePageForPressure(void) { + if (!gVadListReady) return STATUS_NOT_SUPPORTED; + + KSTATUS cacheDrop = MmFileCacheDropOnePageForPressure(); + if (KSUCCESS(cacheDrop)) + return STATUS_SUCCESS; + + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, GlobalLink); + for (size_t i = 0; i < vad->PageCount; ++i) { + KSTATUS s = KiVadDiscardCleanFilePage(vad, i, (void**)0); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + } + } + + if (!MmSwapIsEnabled()) return STATUS_NOT_SUPPORTED; + + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, GlobalLink); + for (size_t i = 0; i < vad->PageCount; ++i) { + KSTATUS s = KiVadTrimPageToSwap(vad, i, (void**)0); + if (KSUCCESS(s)) + return STATUS_SUCCESS; + } + } + return STATUS_NOT_FOUND; +} + +KSTATUS MmVadSyncFileMappings(FILE_OBJECT* file) { + if (!file) + return STATUS_INVALID_PARAMETER; + + uintptr_t flags = KeAcquireSpinlock(&gVadLock); + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, GlobalLink); + if (vad->Type != MmVadMappedFile || vad->FileObject != file || !vad->Pages) + continue; + + for (size_t i = 0; i < vad->PageCount; ++i) { + MM_VAD_PAGE* page = &vad->Pages[i]; + if ((page->Flags & MM_VAD_PAGE_FILE_SHARED_WRITE) == 0) + continue; + if (!page->Resident || !page->Frame || !page->PageFlags) + continue; + + uintptr_t va = vad->Base + i * PAGE_SIZE; + int wasDirty = MmPteTestAndClearDirty(va); + if (wasDirty) + (void)KiVadMarkSharedFilePageDirty(vad, i, 0); + + if ((page->Flags & MM_VAD_PAGE_FILE_DIRTY_TRACK) == 0) { + page->Flags |= MM_VAD_PAGE_FILE_DIRTY_TRACK; + (void)MmPteProtectRange(va, PAGE_SIZE, KiVadHardwarePageFlags(page)); + } + } + } + KeReleaseSpinlock(&gVadLock, flags); + return STATUS_SUCCESS; +} + +// Resolves a user-mode page fault against committed VAD metadata. +// Resolves a user-mode page fault against committed VAD metadata. +KSTATUS MmVadResolveUserPageFault(PROCESS* process, uintptr_t faultAddress, uint64_t errorCode) { + gVadStats.ResolveFaultCalls++; + + if (!process) + return STATUS_ACCESS_DENIED; + if (!KiUserRangeValid(faultAddress, 1)) { + gVadStats.NoVadFaults++; + gVadStats.FailedFaults++; + return STATUS_ACCESS_DENIED; + } + + uintptr_t va = faultAddress & PAGE_MASK; + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindReservedVadContainingLocked(process, va, PAGE_SIZE); + KeReleaseSpinlock(&process->VadLock, procFlags); + if (!vad) { + gVadStats.NoVadFaults++; + gVadStats.FailedFaults++; + return STATUS_NOT_FOUND; + } + + size_t index = KiVadPageIndex(vad, va); + if (index >= vad->PageCount || vad->Pages[index].State != MmVadPageCommitted) { + gVadStats.UncommittedFaults++; + gVadStats.FailedFaults++; + return STATUS_NOT_FOUND; + } + if (!vad->Pages[index].PageFlags) { + gVadStats.NoAccessFaults++; + gVadStats.FailedFaults++; + return STATUS_ACCESS_DENIED; + } + + int presentFault = (errorCode & 0x1) ? 1 : 0; + int wantWrite = (errorCode & 0x2) ? 1 : 0; + int wantExecute = (errorCode & 0x10) ? 1 : 0; + if (presentFault) { + if (wantWrite && + (vad->Pages[index].Flags & MM_VAD_PAGE_FILE_SHARED_WRITE) && + (vad->Pages[index].Flags & MM_VAD_PAGE_FILE_DIRTY_TRACK)) { + KSTATUS dirty = KiVadMarkSharedFilePageDirty(vad, index, 0); + if (KSUCCESS(dirty)) { + vad->Pages[index].Flags &= ~MM_VAD_PAGE_FILE_DIRTY_TRACK; + KSTATUS ps = MmPteProtectRange(va, PAGE_SIZE, KiVadHardwarePageFlags(&vad->Pages[index])); + if (KSUCCESS(ps)) { + gVadStats.FileBackedDirtyFaults++; + gVadStats.ResolvedFaults++; + return STATUS_SUCCESS; + } + dirty = ps; + } + gVadStats.ProtectionFaults++; + gVadStats.FailedFaults++; + return dirty; + } + if (wantWrite && (vad->Pages[index].Flags & MM_VAD_PAGE_COW)) { + KSTATUS cow = KiVadBreakCopyOnWrite(vad, index); + if (KSUCCESS(cow)) { + gVadStats.CowFaults++; + gVadStats.ResolvedFaults++; + return STATUS_SUCCESS; + } + } + gVadStats.ProtectionFaults++; + gVadStats.FailedFaults++; + return STATUS_ACCESS_DENIED; + } + + KSTATUS s = KiVadEnsurePageResident(vad, index, wantWrite, wantExecute); + if (!KSUCCESS(s)) { + gVadStats.FailedFaults++; + return s; + } + + gVadStats.ResolvedFaults++; + return STATUS_SUCCESS; +} + + +// Resolves a page fault by looking up the owning VAD in the global user arena. +// The single address space intentionally allows the kernel and other processes +// to touch any lower-half mapping, so fault ownership is derived from the VAD +// that owns the virtual address rather than from the currently running process. +KSTATUS MmVadResolvePageFaultByAddress(uintptr_t faultAddress, uint64_t errorCode) { + if (!KiUserRangeValid(faultAddress, 1)) + return STATUS_ACCESS_DENIED; + if (!gVadListReady) + return STATUS_NOT_SUPPORTED; + + uintptr_t va = faultAddress & PAGE_MASK; + PROCESS* owner = (PROCESS*)0; + + uintptr_t flags = KeAcquireSpinlock(&gVadLock); + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, GlobalLink); + uintptr_t end = vad->Base + vad->PayloadSize; + if (va >= vad->Base && va < end) { + owner = vad->Owner; + break; + } + } + KeReleaseSpinlock(&gVadLock, flags); + + if (!owner) + return STATUS_NOT_FOUND; + + return MmVadResolveUserPageFault(owner, faultAddress, errorCode); +} + +// Changes page protections on a committed VAD range. +KSTATUS MmVadProtect(PROCESS* process, void* base, size_t size, uint64_t pageFlags) { KeAssertIrql(PASSIVE_LEVEL); if (!process || !base || !size) return STATUS_INVALID_PARAMETER; @@ -1086,240 +2841,279 @@ KSTATUS MmProtectUserRegion(PROCESS* process, void* base, size_t size, uint64_t uintptr_t alignedEnd = (start + size + PAGE_SIZE - 1) & PAGE_MASK; if (alignedEnd <= alignedStart) return STATUS_NO_MEMORY; - uintptr_t procFlags = KeAcquireSpinlock(&process->UserRegionLock); - USER_VA_REGION* r = KiFindUserRegionContainingLocked(process, alignedStart, alignedEnd - alignedStart); - uintptr_t regionBase = r ? r->Base : 0; - uintptr_t regionEnd = r ? (r->Base + r->CommittedSize) : 0; - KeReleaseSpinlock(&process->UserRegionLock, procFlags); + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindReservedVadContainingLocked(process, alignedStart, alignedEnd - alignedStart); + KeReleaseSpinlock(&process->VadLock, procFlags); - if (!r) { - DbgPrint("MM: protect user region failed: pid=%u range=%016llx-%016llx not owned/committed\n", + if (!vad || !KiVadRangeCommitted(vad, alignedStart, alignedEnd)) { + DbgPrint("MM: protect VAD failed: pid=%u range=%016llx-%016llx not committed\n", process->ProcessId, (unsigned long long)alignedStart, (unsigned long long)alignedEnd); return STATUS_NOT_FOUND; } + if (vad->LockCount) + return STATUS_ACCESS_DENIED; - if (pageFlags == 0) { - for (uintptr_t va = alignedStart; va < alignedEnd; va += PAGE_SIZE) { - if (va < regionBase || va >= regionEnd) return STATUS_ACCESS_DENIED; - PHYSICAL_ADDRESS phys = MmGetPhysicalAddress(va); - if (phys && KSUCCESS(MmUnmapPage(va))) - MmFreeFrame(phys); - } - return STATUS_SUCCESS; - } + uint64_t finalFlags = pageFlags ? (pageFlags | PAGE_PRESENT | PAGE_USER) : 0; + size_t first = KiVadPageIndex(vad, alignedStart); + size_t count = KiVadRangePageCount(alignedStart, alignedEnd); - uint64_t finalFlags = pageFlags | PAGE_PRESENT | PAGE_USER; + for (size_t i = 0; i < count; ++i) { + size_t pageIndex = first + i; + MM_VAD_PAGE* page = &vad->Pages[pageIndex]; + uintptr_t va = vad->Base + pageIndex * PAGE_SIZE; - for (uintptr_t va = alignedStart; va < alignedEnd; va += PAGE_SIZE) { - if (va < regionBase || va >= regionEnd) { - DbgPrint("MM: protect user region hit guard/outside page pid=%u va=%016llx region=%016llx-%016llx\n", - process->ProcessId, - (unsigned long long)va, - (unsigned long long)regionBase, - (unsigned long long)regionEnd); - return STATUS_ACCESS_DENIED; + if (!finalFlags) { + if (page->Resident && page->PageFlags) + (void)KiVadUnmapPageTracked(va, page->Frame); + page->PageFlags = 0; + continue; } - PHYSICAL_ADDRESS phys = 0; - uint64_t existingFlags = 0; - KSTATUS qs = MmQueryPage(va, &phys, &existingFlags); - if (!KSUCCESS(qs) || !(existingFlags & PAGE_PRESENT)) { - PHYSICAL_ADDRESS frame = MmAllocateFrame(); - if (!frame) return STATUS_NO_MEMORY; - KSTATUS ms = MmMapPage(va, frame, PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, MmMapFailIfPresent); - if (!KSUCCESS(ms)) { - MmFreeFrame(frame); - return ms; - } - memset((void*)va, 0, PAGE_SIZE); - DbgPrint("MM: committed missing user page during protect pid=%u va=%016llx\n", - process->ProcessId, (unsigned long long)va); + uint64_t oldFlags = page->PageFlags; + page->PageFlags = finalFlags; + if ((finalFlags & PAGE_WRITE) == 0 && + (page->Flags & (MM_VAD_PAGE_SHARED_FRAME | MM_VAD_PAGE_ZERO_PAGE | MM_VAD_PAGE_IMAGE_BACKED)) == 0) { + page->Flags &= ~MM_VAD_PAGE_COW; } - KSTATUS ps = MmSetPageProtect(va, PAGE_SIZE, finalFlags); - if (!KSUCCESS(ps)) { - PHYSICAL_ADDRESS dbgPhys = 0; - uint64_t dbgFlags = 0; - KSTATUS dbg = MmQueryPage(va, &dbgPhys, &dbgFlags); - DbgPrint("MM: protect user page failed pid=%u va=%016llx st=%d query=%d phys=%016llx flags=%016llx final=%016llx\n", - process->ProcessId, - (unsigned long long)va, - ps, dbg, - (unsigned long long)dbgPhys, - (unsigned long long)dbgFlags, - (unsigned long long)finalFlags); - return ps; + if (page->Resident) { + uint64_t hardwareFlags = KiVadHardwarePageFlags(page); + KSTATUS s = oldFlags + ? MmPteProtectRange(va, PAGE_SIZE, hardwareFlags) + : KiVadMapPageTracked(va, page->Frame, hardwareFlags, MmPteMapFailIfPresent); + if (!KSUCCESS(s)) return s; } } return STATUS_SUCCESS; } -// Releases user Region. -KSTATUS MmDestroyUserRegion(PROCESS* process, void* base) { +// Destroys a VAD and releases all resident payload pages. +KSTATUS MmVadDestroy(PROCESS* process, void* base) { KeAssertIrql(PASSIVE_LEVEL); if (!process || !base) return STATUS_INVALID_PARAMETER; - uintptr_t procFlags = KeAcquireSpinlock(&process->UserRegionLock); - USER_VA_REGION* r = KiFindUserRegionLocked(process, base); - if (!r) { - KeReleaseSpinlock(&process->UserRegionLock, procFlags); + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindVadByBaseLocked(process, base); + if (!vad) { + KeReleaseSpinlock(&process->VadLock, procFlags); return STATUS_NOT_FOUND; } - ListRemove(&r->ProcessLink); - KeReleaseSpinlock(&process->UserRegionLock, procFlags); + if (vad->LockCount) { + KeReleaseSpinlock(&process->VadLock, procFlags); + return STATUS_ACCESS_DENIED; + } + ListRemove(&vad->ProcessLink); + KeReleaseSpinlock(&process->VadLock, procFlags); - uintptr_t globalFlags = KeAcquireSpinlock(&gUserRegionLock); - ListRemove(&r->GlobalLink); - KeReleaseSpinlock(&gUserRegionLock, globalFlags); + uintptr_t globalFlags = KeAcquireSpinlock(&gVadLock); + ListRemove(&vad->GlobalLink); + KeReleaseSpinlock(&gVadLock, globalFlags); - KiUnmapUserRegionPayload(r); - MmFreePool(r); + KiUnmapVadPayload(vad); + if (vad->FileObject) { + ObDereferenceObject(&vad->FileObject->Header); + vad->FileObject = (FILE_OBJECT*)0; + } + if (vad->ImageSection) { + LdrDereferenceImageSection(vad->ImageSection); + vad->ImageSection = (IMAGE_SECTION_OBJECT*)0; + } + MmFreePool(vad->Pages); + MmFreePool(vad); return STATUS_SUCCESS; } -// Releases all User Regions. -void MmDestroyAllUserRegions(PROCESS* process) { +// Destroys every VAD owned by a process during teardown. +void MmVadDestroyAllForProcess(PROCESS* process) { KeAssertIrql(PASSIVE_LEVEL); if (!process) return; for (;;) { - uintptr_t procFlags = KeAcquireSpinlock(&process->UserRegionLock); - LIST_ENTRY* node = ListPopHead(&process->UserRegions); - KeReleaseSpinlock(&process->UserRegionLock, procFlags); - if (!node) break; + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + LIST_ENTRY* node = (LIST_ENTRY*)0; + int sawLocked = 0; + for (LIST_ENTRY* it = process->VadList.Fwd; it != &process->VadList; it = it->Fwd) { + MM_VAD* candidate = CONTAINING_RECORD(it, MM_VAD, ProcessLink); + if (candidate->LockCount) { + sawLocked = 1; + continue; + } + node = it; + ListRemove(node); + break; + } + KeReleaseSpinlock(&process->VadLock, procFlags); + if (!node) { + if (sawLocked) { + KeYield(); + continue; + } + break; + } - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, ProcessLink); - uintptr_t globalFlags = KeAcquireSpinlock(&gUserRegionLock); - ListRemove(&r->GlobalLink); - KeReleaseSpinlock(&gUserRegionLock, globalFlags); - KiUnmapUserRegionPayload(r); - MmFreePool(r); + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, ProcessLink); + uintptr_t globalFlags = KeAcquireSpinlock(&gVadLock); + ListRemove(&vad->GlobalLink); + KeReleaseSpinlock(&gVadLock, globalFlags); + KiUnmapVadPayload(vad); + if (vad->FileObject) { + ObDereferenceObject(&vad->FileObject->Header); + vad->FileObject = (FILE_OBJECT*)0; + } + if (vad->ImageSection) { + LdrDereferenceImageSection(vad->ImageSection); + vad->ImageSection = (IMAGE_SECTION_OBJECT*)0; + } + MmFreePool(vad->Pages); + MmFreePool(vad); } } -KSTATUS MmQueryUserRegion(PROCESS* process, const void* address, void** baseOut, size_t* sizeOut, - uint64_t* pageFlagsOut, USER_REGION_TYPE* typeOut, uint32_t* regionFlagsOut) { +// Queries the committed VAD range containing an address. +KSTATUS MmVadQuery(PROCESS* process, const void* address, void** baseOut, size_t* sizeOut, + uint64_t* pageFlagsOut, MM_VAD_TYPE* typeOut, uint32_t* vadFlagsOut) { if (!process || !address) return STATUS_INVALID_PARAMETER; uintptr_t addr = (uintptr_t)address; - uintptr_t procFlags = KeAcquireSpinlock(&process->UserRegionLock); - for (LIST_ENTRY* node = process->UserRegions.Fwd; node != &process->UserRegions; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, ProcessLink); - uintptr_t end = r->Base + r->CommittedSize; - if (addr >= r->Base && addr < end) { - if (baseOut) *baseOut = (void*)r->Base; - if (sizeOut) *sizeOut = r->CommittedSize; - if (pageFlagsOut) { - PHYSICAL_ADDRESS phys = 0; - uint64_t flags = 0; - KSTATUS qs = MmQueryPage(addr & PAGE_MASK, &phys, &flags); - *pageFlagsOut = KSUCCESS(qs) ? flags : 0; - } - if (typeOut) *typeOut = r->Type; - if (regionFlagsOut) *regionFlagsOut = r->Flags; - KeReleaseSpinlock(&process->UserRegionLock, procFlags); - return STATUS_SUCCESS; + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + for (LIST_ENTRY* node = process->VadList.Fwd; node != &process->VadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, ProcessLink); + if (addr < vad->Base || addr >= vad->Base + vad->PayloadSize) + continue; + + size_t index = KiVadPageIndex(vad, addr & PAGE_MASK); + if (index >= vad->PageCount || vad->Pages[index].State != MmVadPageCommitted) { + KeReleaseSpinlock(&process->VadLock, procFlags); + return STATUS_NOT_FOUND; } + + uint64_t flags = vad->Pages[index].PageFlags; + size_t runStart = index; + while (runStart > 0 && + vad->Pages[runStart - 1].State == MmVadPageCommitted && + vad->Pages[runStart - 1].PageFlags == flags) { + runStart--; + } + + size_t runEnd = index + 1; + while (runEnd < vad->PageCount && + vad->Pages[runEnd].State == MmVadPageCommitted && + vad->Pages[runEnd].PageFlags == flags) { + runEnd++; + } + + if (baseOut) *baseOut = (void*)(vad->Base + runStart * PAGE_SIZE); + if (sizeOut) *sizeOut = (runEnd - runStart) * PAGE_SIZE; + if (pageFlagsOut) *pageFlagsOut = flags; + if (typeOut) *typeOut = vad->Type; + if (vadFlagsOut) *vadFlagsOut = vad->Flags; + KeReleaseSpinlock(&process->VadLock, procFlags); + return STATUS_SUCCESS; } - KeReleaseSpinlock(&process->UserRegionLock, procFlags); + KeReleaseSpinlock(&process->VadLock, procFlags); return STATUS_NOT_FOUND; } - -// Returns process User Memory. -void MmQueryProcessUserMemory(PROCESS* process, size_t* regionCountOut, size_t* reservedBytesOut, size_t* committedBytesOut) { - size_t regions = 0; +// Returns per-process VAD accounting for ps and memory telemetry. +void MmVadQueryProcessMemory(PROCESS* process, size_t* vadCountOut, size_t* reservedBytesOut, size_t* committedBytesOut) { + size_t vads = 0; size_t reserved = 0; size_t committed = 0; if (process) { - uintptr_t flags = KeAcquireSpinlock(&process->UserRegionLock); - for (LIST_ENTRY* node = process->UserRegions.Fwd; node != &process->UserRegions; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, ProcessLink); - regions++; - reserved += r->ReservationSize; - committed += r->CommittedSize; + uintptr_t flags = KeAcquireSpinlock(&process->VadLock); + for (LIST_ENTRY* node = process->VadList.Fwd; node != &process->VadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, ProcessLink); + vads++; + reserved += vad->ReservationSize; + committed += vad->CommittedPages * PAGE_SIZE; } - KeReleaseSpinlock(&process->UserRegionLock, flags); + KeReleaseSpinlock(&process->VadLock, flags); } - if (regionCountOut) *regionCountOut = regions; + if (vadCountOut) *vadCountOut = vads; if (reservedBytesOut) *reservedBytesOut = reserved; if (committedBytesOut) *committedBytesOut = committed; } -// Returns user Region Size. -size_t MmGetUserRegionSize(PROCESS* process, const void* base) { +// Returns VAD and demand-fault telemetry. +void MmVadQueryStats(MM_VAD_STATS* out) { + if (!out) return; + *out = gVadStats; +} + +// Returns the requested payload size for a VAD base. +size_t MmVadGetSize(PROCESS* process, const void* base) { if (!process || !base) return 0; - uintptr_t procFlags = KeAcquireSpinlock(&process->UserRegionLock); - USER_VA_REGION* r = KiFindUserRegionLocked(process, base); - size_t size = r ? r->Size : 0; - KeReleaseSpinlock(&process->UserRegionLock, procFlags); + uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock); + MM_VAD* vad = KiFindVadByBaseLocked(process, base); + size_t size = vad ? vad->Size : 0; + KeReleaseSpinlock(&process->VadLock, procFlags); return size; } -// Returns user Region Size By Base. -size_t MmGetUserRegionSizeByBase(const void* base) { +// Returns the requested payload size for a VAD base without knowing the owner. +size_t MmVadGetSizeByBase(const void* base) { if (!base) return 0; uintptr_t b = (uintptr_t)base; - uintptr_t flags = KeAcquireSpinlock(&gUserRegionLock); - for (LIST_ENTRY* node = gUserRegionList.Fwd; node != &gUserRegionList; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, GlobalLink); - if (r->Base == b) { - size_t size = r->Size; - KeReleaseSpinlock(&gUserRegionLock, flags); + uintptr_t flags = KeAcquireSpinlock(&gVadLock); + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* vad = CONTAINING_RECORD(node, MM_VAD, GlobalLink); + if (vad->Base == b) { + size_t size = vad->Size; + KeReleaseSpinlock(&gVadLock, flags); return size; } } - KeReleaseSpinlock(&gUserRegionLock, flags); + KeReleaseSpinlock(&gVadLock, flags); return 0; } - -// Implements the user Region Type Name operation. -static const char* KiUserRegionTypeName(USER_REGION_TYPE type) { +// Implements the VAD Type Name operation. +static const char* KiVadTypeName(MM_VAD_TYPE type) { switch (type) { - case UserRegionGeneric: return "generic"; - case UserRegionHeap: return "heap"; - case UserRegionImage: return "image"; - case UserRegionStack: return "stack"; - case UserRegionTeb: return "teb"; - case UserRegionPeb: return "peb"; - case UserRegionEnvironment: return "env"; - case UserRegionMappedFile: return "mapped"; - case UserRegionShared: return "shared"; - default: return "?"; + case MmVadGeneric: return "generic"; + case MmVadHeap: return "heap"; + case MmVadImage: return "image"; + case MmVadStack: return "stack"; + case MmVadTeb: return "teb"; + case MmVadPeb: return "peb"; + case MmVadEnvironment: return "env"; + case MmVadMappedFile: return "mapped"; + case MmVadShared: return "shared"; + default: return "?"; } } -// Implements the debug Dump User Regions operation. -void MmDebugDumpUserRegions(PROCESS* process) { +// Implements the debug Dump VADs operation. +void MmVadDebugDumpProcess(PROCESS* process) { if (!process) { - DbgPrint("MM: user regions: \n"); + DbgPrint("MM: VADs: \n"); return; } - DbgPrint("MM: user regions for pid=%u name=%s:\n", process->ProcessId, process->Name); - uintptr_t flags = KeAcquireSpinlock(&process->UserRegionLock); + DbgPrint("MM: VADs for pid=%u name=%s:\n", process->ProcessId, process->Name); + uintptr_t flags = KeAcquireSpinlock(&process->VadLock); uint32_t count = 0; - for (LIST_ENTRY* node = process->UserRegions.Fwd; node != &process->UserRegions; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, ProcessLink); - DbgPrint(" #%u %-7s res=%016llx-%016llx base=%016llx size=%llx committed=%llx flags=%x pte=%llx\n", + for (LIST_ENTRY* node = process->VadList.Fwd; node != &process->VadList; node = node->Fwd) { + MM_VAD* r = CONTAINING_RECORD(node, MM_VAD, ProcessLink); + DbgPrint(" #%u %-7s res=%016llx-%016llx base=%016llx size=%llx committed=%llx/%llx flags=%x pte=%llx\n", count, - KiUserRegionTypeName(r->Type), + KiVadTypeName(r->Type), (unsigned long long)r->ReservationBase, (unsigned long long)(r->ReservationBase + r->ReservationSize), (unsigned long long)r->Base, (unsigned long long)r->Size, - (unsigned long long)r->CommittedSize, + (unsigned long long)(r->CommittedPages * PAGE_SIZE), + (unsigned long long)r->PayloadSize, r->Flags, (unsigned long long)r->PageFlags); count++; } - KeReleaseSpinlock(&process->UserRegionLock, flags); + KeReleaseSpinlock(&process->VadLock, flags); if (!count) DbgPrint(" \n"); } @@ -1351,14 +3145,34 @@ static void KiDebugDescribePoolAddress(uintptr_t addr) { uintptr_t data = hdr + HDR_SIZE; uintptr_t end = data + cur->Size; uintptr_t ftr = end; + BLOCK_FTR* footer = KiHdrToFtr(cur); + if ((uintptr_t)footer + FTR_SIZE > gPoolHeap.Brk || + footer->Magic != BLOCK_MAGIC || footer->Size != cur->Size || footer->Free != cur->Free) { + DbgPrint("MM: pool walk corrupt footer hdr=%p state=%u size=%llx footer=%p fmagic=%08x fstate=%u fsize=%llx\n", + cur, + cur->Free, + (unsigned long long)cur->Size, + footer, + ((uintptr_t)footer + FTR_SIZE <= gPoolHeap.Brk) ? footer->Magic : 0, + ((uintptr_t)footer + FTR_SIZE <= gPoolHeap.Brk) ? footer->Free : 0, + ((uintptr_t)footer + FTR_SIZE <= gPoolHeap.Brk) ? (unsigned long long)footer->Size : 0ULL); + break; + } if (addr >= hdr && addr < ftr + FTR_SIZE) { - DbgPrint("MM: address %016llx in pool block hdr=%016llx data=%016llx-%016llx size=%llx free=%u\n", + DbgPrint("MM: address %016llx in pool block hdr=%016llx data=%016llx-%016llx size=%llx req=%llx state=%u(%s) tag=%08x flags=%08x seq=%llu alloc=%p free=%p\n", (unsigned long long)addr, (unsigned long long)hdr, (unsigned long long)data, (unsigned long long)end, (unsigned long long)cur->Size, - cur->Free); + (unsigned long long)cur->RequestedSize, + cur->Free, + KiPoolStateName(cur->Free), + cur->Tag, + cur->Flags, + (unsigned long long)cur->Sequence, + (void*)cur->AllocCaller, + (void*)cur->FreeCaller); break; } if (KiNextBlock(cur) <= cur) { @@ -1373,7 +3187,7 @@ static void KiDebugDescribePoolAddress(uintptr_t addr) { void MmDebugDescribeAddress(uintptr_t addr) { PHYSICAL_ADDRESS phys = 0; uint64_t flags = 0; - KSTATUS qs = MmQueryPage(addr, &phys, &flags); + KSTATUS qs = MmPteQueryPageFor(MmPteQueryDebug, addr, &phys, &flags); DbgPrint("MM: describe va=%016llx query=%d phys=%016llx flags=%016llx\n", (unsigned long long)addr, qs, @@ -1385,26 +3199,39 @@ void MmDebugDescribeAddress(uintptr_t addr) { return; } - if (addr >= USER_REGION_ARENA_BASE && addr < USER_LIMIT) { - uintptr_t gl = KeAcquireSpinlock(&gUserRegionLock); - for (LIST_ENTRY* node = gUserRegionList.Fwd; node != &gUserRegionList; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, GlobalLink); + if (addr >= MM_VAD_ARENA_BASE && addr < USER_LIMIT) { + uintptr_t gl = KeAcquireSpinlock(&gVadLock); + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* r = CONTAINING_RECORD(node, MM_VAD, GlobalLink); if (addr >= r->ReservationBase && addr < r->ReservationBase + r->ReservationSize) { - DbgPrint("MM: address belongs to pid=%u name=%s region=%s res=%016llx-%016llx base=%016llx size=%llx flags=%x\n", + const char* pageState = "guard"; + if (addr >= r->Base && addr < r->Base + r->PayloadSize) { + size_t pageIndex = KiVadPageIndex(r, addr & PAGE_MASK); + if (pageIndex < r->PageCount) { + MM_VAD_PAGE* page = &r->Pages[pageIndex]; + if (page->State == MmVadPageCommitted) + pageState = page->PageFlags ? "mapped" : "noaccess"; + else + pageState = "reserved"; + } + } + + DbgPrint("MM: address belongs to pid=%u name=%s vad=%s page=%s res=%016llx-%016llx base=%016llx size=%llx flags=%x\n", r->Owner ? r->Owner->ProcessId : 0, r->Owner ? r->Owner->Name : "?", - KiUserRegionTypeName(r->Type), + KiVadTypeName(r->Type), + pageState, (unsigned long long)r->ReservationBase, (unsigned long long)(r->ReservationBase + r->ReservationSize), (unsigned long long)r->Base, (unsigned long long)r->Size, r->Flags); - KeReleaseSpinlock(&gUserRegionLock, gl); + KeReleaseSpinlock(&gVadLock, gl); return; } } - KeReleaseSpinlock(&gUserRegionLock, gl); - DbgPrint("MM: address is in user arena but not reserved by any USER_VA_REGION\n"); + KeReleaseSpinlock(&gVadLock, gl); + DbgPrint("MM: address is in user arena but not reserved by any MM_VAD\n"); } } @@ -1412,18 +3239,24 @@ void MmDebugDescribeAddress(uintptr_t addr) { __declspec(dllexport) void* MmAllocateUserMemoryForProcess(PROCESS* process, size_t size) { void* base = (void*)0; if (!process || !size) return (void*)0; - KSTATUS s = MmCreateUserRegion(process, size, PAGE_SIZE, + KSTATUS s = MmVadCreateCommitted(process, size, PAGE_SIZE, PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, - USER_REGION_ZERO, - UserRegionHeap, + MM_VAD_ZERO, + MmVadHeap, &base); - return KSUCCESS(s) ? base : (void*)0; + if (!KSUCCESS(s)) return (void*)0; + s = MmVadEnsureResident(process, base, size, PAGE_WRITE | PAGE_USER); + if (!KSUCCESS(s)) { + (void)MmVadDestroy(process, base); + return (void*)0; + } + return base; } // Releases user Memory For Process. __declspec(dllexport) void MmFreeUserMemoryForProcess(PROCESS* process, void* ptr) { if (!process || !ptr) return; - (void)MmDestroyUserRegion(process, ptr); + (void)MmVadDestroy(process, ptr); } // Implements the reallocate User Memory For Process operation. @@ -1435,7 +3268,7 @@ __declspec(dllexport) void* MmReallocateUserMemoryForProcess(PROCESS* process, v return (void*)0; } - size_t oldSize = MmGetUserRegionSize(process, ptr); + size_t oldSize = MmVadGetSize(process, ptr); if (!oldSize) return (void*)0; if (newSize <= oldSize) return ptr; @@ -1468,19 +3301,20 @@ __declspec(dllexport) void* MmReallocateUserMemory(void* ptr, size_t newSize) { __declspec(dllexport) void* MmAllocateUserImageForProcess(PROCESS* process, size_t size) { void* base = (void*)0; if (!process || !size) return (void*)0; - KSTATUS s = MmCreateUserRegion(process, size, PAGE_SIZE, + KSTATUS s = MmVadCreateCommitted(process, size, PAGE_SIZE, PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX, - USER_REGION_ZERO | USER_REGION_GUARD_LOW | USER_REGION_GUARD_HIGH, - UserRegionImage, + MM_VAD_ZERO | MM_VAD_GUARD_LOW | MM_VAD_GUARD_HIGH, + MmVadImage, &base); - return KSUCCESS(s) ? base : (void*)0; + if (!KSUCCESS(s)) return (void*)0; + return base; } // Releases user Image For Process. __declspec(dllexport) void MmFreeUserImageForProcess(PROCESS* process, void* ptr, size_t size) { (void)size; if (!process || !ptr) return; - (void)MmDestroyUserRegion(process, ptr); + (void)MmVadDestroy(process, ptr); } // Creates user Image. @@ -1504,12 +3338,12 @@ size_t MmGetKernelPoolUsage(void) { // Returns user Heap Usage. size_t MmGetUserHeapUsage(void) { size_t total = 0; - if (!gUserRegionListReady) return 0; - uintptr_t flags = KeAcquireSpinlock(&gUserRegionLock); - for (LIST_ENTRY* node = gUserRegionList.Fwd; node != &gUserRegionList; node = node->Fwd) { - USER_VA_REGION* r = CONTAINING_RECORD(node, USER_VA_REGION, GlobalLink); - total += r->CommittedSize; + if (!gVadListReady) return 0; + uintptr_t flags = KeAcquireSpinlock(&gVadLock); + for (LIST_ENTRY* node = gVadList.Fwd; node != &gVadList; node = node->Fwd) { + MM_VAD* r = CONTAINING_RECORD(node, MM_VAD, GlobalLink); + total += r->CommittedPages * PAGE_SIZE; } - KeReleaseSpinlock(&gUserRegionLock, flags); + KeReleaseSpinlock(&gVadLock, flags); return total; } diff --git a/code/kernel/ob_object.c b/code/kernel/ob_object.c index 5276b51..eacfef0 100644 --- a/code/kernel/ob_object.c +++ b/code/kernel/ob_object.c @@ -5,11 +5,36 @@ #include #include -// Creates object. +// Returns a diagnostic pool tag for the object type. +static uint32_t KiObjectPoolTag(OBJECT_TYPE type) { + switch (type) { + case ObjectTypeProcess: return MM_POOL_TAG('p', 'r', 'o', 'c'); + case ObjectTypeThread: return MM_POOL_TAG('t', 'h', 'r', 'd'); + case ObjectTypeMutex: return MM_POOL_TAG('m', 't', 'x', ' '); + case ObjectTypeSemaphore: return MM_POOL_TAG('s', 'e', 'm', ' '); + case ObjectTypeEvent: return MM_POOL_TAG('e', 'v', 'n', 't'); + case ObjectTypeQueue: return MM_POOL_TAG('q', 'u', 'e', 'u'); + case ObjectTypeFile: return MM_POOL_TAG('f', 'i', 'l', 'e'); + case ObjectTypeDir: return MM_POOL_TAG('d', 'i', 'r', ' '); + case ObjectTypeAsyncIo: return MM_POOL_TAG('a', 'i', 'o', ' '); + case ObjectTypeSocket: return MM_POOL_TAG('s', 'o', 'c', 'k'); + case ObjectTypeDevice: return MM_POOL_TAG('d', 'e', 'v', ' '); + case ObjectTypeDriver: return MM_POOL_TAG('d', 'r', 'v', 'r'); + case ObjectTypeUsbDevice: return MM_POOL_TAG('u', 's', 'b', 'd'); + case ObjectTypeUsbPipe: return MM_POOL_TAG('u', 's', 'b', 'p'); + case ObjectTypeFileMapping: return MM_POOL_TAG('m', 'a', 'p', 'f'); + case ObjectTypeSurface: return MM_POOL_TAG('s', 'u', 'r', 'f'); + case ObjectTypeInputQueue: return MM_POOL_TAG('i', 'n', 'p', 'q'); + case ObjectTypeImageSection: return MM_POOL_TAG('i', 'm', 's', 'e'); + default: return MM_POOL_TAG('o', 'b', 'j', ' '); + } +} + +// Creates a zeroed, tagged object with one initial reference. __declspec(dllexport) OBJECT* ObAllocateObject(OBJECT_TYPE type, size_t size) { if (size < sizeof(OBJECT)) size = sizeof(OBJECT); - OBJECT* obj = (OBJECT *)MmAllocatePool(size); + OBJECT* obj = (OBJECT *)MmAllocatePoolTag(size, KiObjectPoolTag(type)); if (!obj) return (OBJECT *)0; memset(obj, 0, size); diff --git a/code/public/ntlite/ddi/hal_api.h b/code/public/ntlite/ddi/hal_api.h index bad5824..89a527a 100644 --- a/code/public/ntlite/ddi/hal_api.h +++ b/code/public/ntlite/ddi/hal_api.h @@ -120,7 +120,7 @@ typedef struct HAL_INIT_PARAMS { void* (*AllocatePool)(size_t size); } HAL_INIT_PARAMS; -#define HAL_API_VERSION 5 +#define HAL_API_VERSION // do not change during development __declspec(dllexport) const HAL_API* HalGetApi(void); __declspec(dllexport) void HalRegisterScheduler(void (*pKeSchedulerTick)(void)); diff --git a/code/public/ntlite/ddi/io_driver.h b/code/public/ntlite/ddi/io_driver.h index 8f17fc5..e04a951 100644 --- a/code/public/ntlite/ddi/io_driver.h +++ b/code/public/ntlite/ddi/io_driver.h @@ -179,6 +179,27 @@ typedef void (*IO_DISK_ARRIVAL_CALLBACK)(uint8_t driveIndex, DEVICE_OBJECT* device, void* context); +typedef struct IO_DISK_STATS { + uint64_t RegisteredDisks; + uint64_t ReadRequests; + uint64_t ReadSectors; + uint64_t WriteRequests; + uint64_t WriteSectors; + uint64_t MaxReadSectors; + uint64_t MaxWriteSectors; + uint64_t ReadaheadHits; + uint64_t ReadaheadMisses; + uint64_t ReadaheadStores; + uint64_t ReadaheadInvalidations; + uint64_t ReadaheadBytesServed; + uint64_t ReadaheadSectorsLoaded; + uint64_t ReadaheadSequentialDetections; + uint64_t ReadaheadSuppressed; + uint64_t ReadaheadAllocFailures; + uint64_t ReadaheadReadFailures; + uint64_t MaxReadaheadSectors; +} IO_DISK_STATS; + __declspec(dllexport) KSTATUS IoRegisterDiskDevice(uint8_t driveIndex, DEVICE_OBJECT* device); __declspec(dllexport) KSTATUS IoNotifyDiskSurpriseRemoved(uint8_t driveIndex); @@ -203,3 +224,4 @@ __declspec(dllexport) KSTATUS IoWriteDiskSectors(uint8_t driveIndex, uint64_t lba, uint32_t count, const void* buffer); +__declspec(dllexport) void IoQueryDiskStats(IO_DISK_STATS* out); diff --git a/code/public/ntlite/ddi/pci.h b/code/public/ntlite/ddi/pci.h index 0836a6a..0a828d3 100644 --- a/code/public/ntlite/ddi/pci.h +++ b/code/public/ntlite/ddi/pci.h @@ -118,3 +118,6 @@ PCI_API KSTATUS PciGetInterrupt(DEVICE_OBJECT* pdo, PCI_API KSTATUS PciEnableMsi(DEVICE_OBJECT* pdo, uint8_t irqSlot); + +PCI_API KSTATUS PciEnableMsiAuto(DEVICE_OBJECT* pdo, + uint8_t* outIrqSlot); diff --git a/code/public/ntlite/display.h b/code/public/ntlite/display.h index c2dda13..5854e74 100644 --- a/code/public/ntlite/display.h +++ b/code/public/ntlite/display.h @@ -32,6 +32,43 @@ typedef struct DISPLAY_RECT { int32_t H; } DISPLAY_RECT; + +#define SYS_SURFACE_FORMAT_XRGB8888 1u +#define SYS_SURFACE_FLAG_NONE 0u +#define SYS_SURFACE_MAX_RECTS 64u + +typedef struct SYS_SURFACE_DESC { + uint32_t Width; + uint32_t Height; + uint32_t PitchBytes; + uint32_t Format; + void* Base; + uint64_t PayloadBytes; + uint64_t PageCount; + uint64_t MapCount; + uint64_t PresentCount; + uint64_t PresentRectCount; + uint64_t PresentClippedRectCount; + uint64_t PresentEmptyClipCount; + uint64_t PresentRowsCopied; + uint64_t PresentBytesCopied; +} SYS_SURFACE_DESC; + +typedef struct SYS_SURFACE_STATS { + uint64_t CreatedSurfaces; + uint64_t DestroyedSurfaces; + uint64_t LiveSurfaces; + uint64_t LivePages; + uint64_t LiveBytes; + uint64_t MapCalls; + uint64_t PresentCalls; + uint64_t PresentRectInputs; + uint64_t PresentClippedRects; + uint64_t PresentEmptyClips; + uint64_t PresentRowsCopied; + uint64_t PresentBytesCopied; +} SYS_SURFACE_STATS; + typedef struct DISPLAY_STATS { uint64_t BlitCount; uint64_t FillRectCount; diff --git a/code/public/ntlite/input.h b/code/public/ntlite/input.h index 198d6a3..ef3f2cd 100644 --- a/code/public/ntlite/input.h +++ b/code/public/ntlite/input.h @@ -136,6 +136,13 @@ typedef struct INPUT_KEY_EVENT { bool Pressed; } INPUT_KEY_EVENT; +typedef struct INPUT_KEY_RECORD { + INPUT_KEY Key; + char Char; + uint8_t Reserved; + uint32_t Modifiers; +} INPUT_KEY_RECORD; + typedef struct INPUT_MOUSE_EVENT { int dx; int dy; diff --git a/code/public/ntlite/status.h b/code/public/ntlite/status.h index 026d965..db3386d 100644 --- a/code/public/ntlite/status.h +++ b/code/public/ntlite/status.h @@ -45,4 +45,26 @@ typedef uint32_t KSTATUS; // --- Execution level (0x0060) ---. #define STATUS_INVALID_IRQL ((KSTATUS)0x0060) +// --- Process termination reasons ---. +// Low values are reserved for ordinary KSTATUS returns. Abnormal process +// termination uses a high-bit private range so exit statuses cannot collide +// with normal kernel/API result codes. +// +// Layout: +// 31 : set => abnormal/private status +// 30..24 : subsystem group +// 23..0 : group-local reason +// +// Group 0x61 is process termination/fault status. +#define STATUS_PRIVATE_BASE ((KSTATUS)0x80000000u) +#define STATUS_PROCESS_BASE ((KSTATUS)0xE1000000u) +#define STATUS_PROCESS_KILLED ((KSTATUS)(STATUS_PROCESS_BASE | 0x0000u)) +#define STATUS_PROCESS_PAGE_FAULT ((KSTATUS)(STATUS_PROCESS_BASE | 0x0010u)) +#define STATUS_PROCESS_GENERAL_PROTECTION ((KSTATUS)(STATUS_PROCESS_BASE | 0x0011u)) +#define STATUS_PROCESS_ILLEGAL_OPCODE ((KSTATUS)(STATUS_PROCESS_BASE | 0x0012u)) +#define STATUS_PROCESS_DIVIDE_BY_ZERO ((KSTATUS)(STATUS_PROCESS_BASE | 0x0013u)) +#define STATUS_PROCESS_BREAKPOINT ((KSTATUS)(STATUS_PROCESS_BASE | 0x0014u)) +#define STATUS_PROCESS_ALIGNMENT_FAULT ((KSTATUS)(STATUS_PROCESS_BASE | 0x0015u)) +#define STATUS_PROCESS_UNKNOWN_FAULT ((KSTATUS)(STATUS_PROCESS_BASE | 0x001Fu)) + #define KSUCCESS(s) ((s) == STATUS_SUCCESS) diff --git a/code/public/ntlite/syscall.h b/code/public/ntlite/syscall.h index 29db87b..314c01d 100644 --- a/code/public/ntlite/syscall.h +++ b/code/public/ntlite/syscall.h @@ -18,7 +18,11 @@ #define SYS_STATUS_PENDING 4u #define SYS_STATUS_ALERTED 5u #define SYS_STATUS_INVALID_PARAMETER 0x10u +#define SYS_STATUS_INVALID_HANDLE 0x11u +#define SYS_STATUS_BUFFER_TOO_SMALL 0x12u +#define SYS_STATUS_NOT_SUPPORTED 0x02u #define SYS_STATUS_NO_MEMORY 0x20u +#define SYS_STATUS_ACCESS_DENIED 0x21u #define SYS_STATUS_KERNEL_APC 6u #define SYS_STATUS_USER_APC 7u #define SYS_STATUS_CANCELLED 0x33u @@ -30,6 +34,7 @@ #define SYS_TEB_MAGIC 0x30424554u // "TEB0" #define SYS_ENV_VERSION 1u #define SYS_CWD_MAX 520u +#define SYS_PATH_MAX 1024u #ifndef SYS_USER_ENV_TYPES_DEFINED #define SYS_USER_ENV_TYPES_DEFINED 1 @@ -52,9 +57,7 @@ typedef struct SYS_TEB { uint32_t ThreadId; uint32_t Magic; - // Native per-thread extension pointer. The kernel leaves this NULL; - // user-mode personalities/libraries may hang their own thread-local - // state here without baking subsystem semantics into SYS_TEB. + // =The kernel leaves this NULL, userspace can modify and use. void* SubsystemData; } SYS_TEB; @@ -80,6 +83,27 @@ typedef void (*SYS_USER_APC_ROUTINE)(void* normalContext, SYS_IO_STATUS_BLOCK* iosb, uint64_t information); +typedef struct SYS_FILE_IO_REQUEST { + uint32_t FileHandle; + uint32_t Flags; + void* Buffer; + size_t Size; + size_t* TransferredOut; +} SYS_FILE_IO_REQUEST; + +static inline SYS_FILE_IO_REQUEST SysMakeFileIoRequest(uint32_t fileHandle, + const void* buffer, + size_t size, + size_t* transferredOut) { + SYS_FILE_IO_REQUEST request; + request.FileHandle = fileHandle; + request.Flags = 0; + request.Buffer = (void*)(uintptr_t)buffer; + request.Size = size; + request.TransferredOut = transferredOut; + return request; +} + typedef struct SYS_ASYNC_FILE_REQUEST { uint32_t FileHandle; uint32_t EventHandle; // optional; 0 means no event signal @@ -181,11 +205,40 @@ typedef struct SYS_NAMED_QUEUE_OWNER_REQUEST { uint32_t* PidOut; } SYS_NAMED_QUEUE_OWNER_REQUEST; +#define SYS_WAIT_FLAG_NONE 0x00000000u +#define SYS_WAIT_FLAG_ALERTABLE 0x00000001u +#define SYS_WAIT_TIMEOUT_INFINITE UINT64_MAX +#define SYS_WAIT_MANY_MAX_HANDLES 32u + +typedef struct SYS_WAIT_MANY_REQUEST { + uint32_t Size; + uint32_t Flags; + const uint32_t* Handles; + uint32_t Count; + uint32_t* SignaledIndexOut; + uint64_t TimeoutMilliseconds; +} SYS_WAIT_MANY_REQUEST; + +typedef struct SYS_CREATE_THREAD_REQUEST { + uintptr_t Entry; + uintptr_t Argument; + uint32_t Priority; + uint32_t Flags; + uint32_t* ThreadHandleOut; + uint32_t* ThreadIdOut; +} SYS_CREATE_THREAD_REQUEST; + // Process & thread lifecycle (0-14). void SysExitProcess(int exitCode); void SysExitThread(int exitCode); uint32_t SysCreateProcess(const SYS_CREATE_PROCESS_REQUEST* request); -uint32_t SysWaitForProcess(uint32_t handle); +uint32_t SysWaitSingle(uint32_t handle, uint32_t flags, uint64_t timeoutMilliseconds); +uint32_t SysWaitMany(const SYS_WAIT_MANY_REQUEST* request); +uint32_t SysOpenInputQueue(uint32_t flags, uint32_t* handleOut); + +static inline uint32_t SysWait(uint32_t handle, uint32_t flags) { + return SysWaitSingle(handle, flags, SYS_WAIT_TIMEOUT_INFINITE); +} uint32_t SysGetProcessExitCode(uint32_t handle, int* exitCodeOut); uint32_t SysKillProcess(uint32_t pid, int exitCode); @@ -200,6 +253,61 @@ uint32_t SysKillProcess(uint32_t pid, int exitCode); #define SYS_PROCESS_FLAG_CURRENT 0x00000002u #define SYS_PROCESS_FLAG_TERMINATING 0x00000004u #define SYS_PROCESS_FLAG_EXITED 0x00000008u +#define SYS_PROCESS_FLAG_IDLE 0x00000010u + +#define SYS_THREAD_STATE_READY 0u +#define SYS_THREAD_STATE_RUNNING 1u +#define SYS_THREAD_STATE_WAITING 2u +#define SYS_THREAD_STATE_DEAD 3u + +#define SYS_THREAD_FLAG_CURRENT 0x00000001u +#define SYS_THREAD_FLAG_IDLE 0x00000002u +#define SYS_THREAD_FLAG_KERNEL 0x00000004u +#define SYS_THREAD_FLAG_QUEUED 0x00000008u +#define SYS_THREAD_FLAG_ALERTABLE_WAIT 0x00000010u +#define SYS_THREAD_FLAG_KERNEL_APC_PENDING 0x00000020u +#define SYS_THREAD_FLAG_USER_APC_PENDING 0x00000040u + +#define SYS_THREAD_WAIT_NONE 0u +#define SYS_THREAD_WAIT_PROCESS 1u +#define SYS_THREAD_WAIT_THREAD 2u +#define SYS_THREAD_WAIT_MUTEX 3u +#define SYS_THREAD_WAIT_SEMAPHORE 4u +#define SYS_THREAD_WAIT_EVENT 5u +#define SYS_THREAD_WAIT_QUEUE 6u +#define SYS_THREAD_WAIT_FILE 7u +#define SYS_THREAD_WAIT_DIR 8u +#define SYS_THREAD_WAIT_ASYNC_IO 9u +#define SYS_THREAD_WAIT_SOCKET 10u +#define SYS_THREAD_WAIT_DEVICE 11u +#define SYS_THREAD_WAIT_DRIVER 12u +#define SYS_THREAD_WAIT_USB_DEVICE 13u +#define SYS_THREAD_WAIT_USB_PIPE 14u +#define SYS_THREAD_WAIT_FILE_MAPPING 15u +#define SYS_THREAD_WAIT_SURFACE 16u +#define SYS_THREAD_WAIT_UNKNOWN 0xffffffffu + +typedef struct SYS_THREAD_INFO { + uint32_t Size; + uint32_t ProcessId; + uint32_t ThreadId; + uint32_t State; + + uint32_t Priority; + uint32_t Flags; + uint32_t RunningCpu; + uint32_t LastCpu; + + uint32_t QuantumRemaining; + uint32_t QuantumTicks; + uint32_t CpuBurstTicks; + uint32_t LastRunTicks; + + uint32_t WaitObjectType; + uint32_t WaitStatus; + int32_t KernelApcDisable; + uint32_t Reserved0; +} SYS_THREAD_INFO; typedef struct SYS_PROCESS_INFO { uint32_t Size; @@ -219,7 +327,7 @@ typedef struct SYS_PROCESS_INFO { size_t ImageSize; uintptr_t PebBase; - size_t UserRegionCount; + size_t VadCount; size_t UserReservedBytes; size_t UserCommittedBytes; @@ -229,9 +337,9 @@ typedef struct SYS_PROCESS_INFO { uint32_t SysGetProcessList(SYS_PROCESS_INFO* processesOut, size_t maxCount, size_t* countOut); uint32_t SysGetProcessPid(uint32_t handle, uint32_t* pidOut); +uint32_t SysGetThreadList(uint32_t pid, SYS_THREAD_INFO* threadsOut, size_t maxCount, size_t* countOut); -uint32_t SysCreateThread(uintptr_t entry, uintptr_t arg, uint32_t priority, uint32_t* tidOut); -uint32_t SysWaitThread(uint32_t tid); +uint32_t SysCreateThread(const SYS_CREATE_THREAD_REQUEST* request); uint32_t SysGetCurrentTid(void); uint32_t SysGetCurrentPid(void); uint32_t SysSetThreadPriority(uint32_t priority); @@ -266,23 +374,32 @@ uint32_t SysAllocVm(void* preferredBase, size_t pageCount, uint32_t protect, uin uint32_t SysFreeVm(void* base); uint32_t SysProtectVm(void* base, size_t pageCount, uint32_t protect, uint32_t* oldProtectOut); uint32_t SysQueryVm(void* address, SYS_VM_REGION_INFO* out); + +#define SYS_MAP_ACCESS_READ 0x00000001u +#define SYS_MAP_ACCESS_WRITE 0x00000002u +#define SYS_MAP_ACCESS_COPY 0x00000004u +#define SYS_MAP_ACCESS_EXECUTE 0x00000008u +#define SYS_MAP_FLAG_NONE 0x00000000u + +uint32_t SysCreateFileMapping(uint32_t fileHandle, uint32_t protect, uint32_t flags, + uint64_t maximumSize, uint32_t* mappingHandleOut); +uint32_t SysMapFile(uint32_t mappingHandle, uint64_t offset, size_t size, + uint32_t access, uint32_t flags, void** baseOut); uint32_t SysCloseHandle(uint32_t handle); // Synchronization primitives (19-31). uint32_t SysCreateMutex(uint32_t* handleOut); -uint32_t SysAcquireMutex(uint32_t handle); uint32_t SysTryAcquireMutex(uint32_t handle); uint32_t SysReleaseMutex(uint32_t handle); uint32_t SysCreateSem(int32_t initial, int32_t max, uint32_t* handleOut); -uint32_t SysWaitSem(uint32_t handle); uint32_t SysTryWaitSem(uint32_t handle); uint32_t SysSignalSem(uint32_t handle); uint32_t SysCreateEvent(uint32_t type, uint32_t initialState, uint32_t* handleOut); uint32_t SysSignalEvent(uint32_t handle); uint32_t SysClearEvent(uint32_t handle); -uint32_t SysWaitEvent(uint32_t handle, uint32_t alertable); + // Queues and named queues (32-38). uint32_t SysCreateQueue(uint32_t* handleOut); @@ -296,8 +413,8 @@ uint32_t SysGetNamedQueueOwnerPid(const SYS_NAMED_QUEUE_OWNER_REQUEST* request); // Files, directories, and current directory (39-56). uint32_t SysOpenFile(const SYS_PATH_HANDLE_REQUEST* request); uint32_t SysCreateFile(const SYS_PATH_HANDLE_REQUEST* request); -uint32_t SysReadFile(uint32_t handle, void* buf, size_t size, size_t* bytesRead); -uint32_t SysWriteFile(uint32_t handle, const void* buf, size_t size, size_t* written); +uint32_t SysReadFile(const SYS_FILE_IO_REQUEST* request); +uint32_t SysWriteFile(const SYS_FILE_IO_REQUEST* request); uint32_t SysReadFileAsync(const SYS_ASYNC_FILE_REQUEST* request); uint32_t SysWriteFileAsync(const SYS_ASYNC_FILE_REQUEST* request); uint32_t SysCancelIo(uint32_t asyncRequestHandle); @@ -305,6 +422,7 @@ uint32_t SysSeekFile(uint32_t handle, uint64_t offset); uint64_t SysGetFilePos(uint32_t handle); uint64_t SysGetFileSize(uint32_t handle); uint32_t SysTruncateFile(uint32_t handle, uint64_t newSize); +uint32_t SysFlushFile(uint32_t handle); uint32_t SysDeleteFile(const SYS_PATH_REQUEST* request); uint32_t SysRenameFile(const SYS_RENAME_FILE_REQUEST* request); @@ -330,7 +448,10 @@ uint32_t SysSetConsoleCursor(int x, int y); uint32_t SysFillConsoleCharacter(char c, int x, int y, size_t length, size_t* writtenOut); uint32_t SysFillConsoleAttribute(uint16_t attr, int x, int y, size_t length, size_t* writtenOut); uint32_t SysPutScreenChar(char c, int x, int y, uint32_t fg, uint32_t bg); -uint32_t SysBlitScreen(int x, int y, int w, int h, const uint32_t* pixels); +uint32_t SysCreateSurface(uint32_t width, uint32_t height, uint32_t format, uint32_t flags, uint32_t* handleOut); +uint32_t SysMapSurface(uint32_t surfaceHandle, void** baseOut, uint32_t* pitchBytesOut); +uint32_t SysQuerySurface(uint32_t surfaceHandle, SYS_SURFACE_DESC* descOut); +uint32_t SysPresentSurface(uint32_t surfaceHandle, const DISPLAY_RECT* rects, uint32_t rectCount); uint32_t SysClearScreen(uint32_t color); uint32_t SysGetScreenSize(int* widthOut, int* heightOut); uint32_t SysQueryDisplayInfo(SYS_DISPLAY_INFO* infoOut); @@ -341,6 +462,8 @@ uint64_t SysIsKeyDown(INPUT_KEY key); char SysReadKeyboardChar(void); INPUT_KEY SysReadKey(void); uint32_t SysGetKeyModifiers(void); +uint32_t SysReadInputEvent(INPUT_KEY_RECORD* recordOut); +uint32_t SysPeekInputEvent(INPUT_KEY_RECORD* recordOut); uint64_t SysPollMouse(void); uint64_t SysReadMouse(int* dx, int* dy, uint8_t* buttons); @@ -437,11 +560,25 @@ typedef struct SYS_MEMORY_STATUS { uint64_t FreePhysicalBytes; uint64_t KernelPoolBytes; uint64_t UserHeapBytes; + uint64_t ReclaimablePhysicalBytes; + uint64_t AvailablePhysicalBytes; + uint64_t PinnedPhysicalBytes; + uint64_t NonEvictablePhysicalBytes; + uint64_t UserPhysicalBytes; + uint64_t PageTablePhysicalBytes; } SYS_MEMORY_STATUS; #define SYS_KERNEL_STATS_SCHEDULER 1u #define SYS_KERNEL_STATS_PMM 2u #define SYS_KERNEL_STATS_CPU 3u +#define SYS_KERNEL_STATS_PFN 4u +#define SYS_KERNEL_STATS_PTE 5u +#define SYS_KERNEL_STATS_VAD 6u +#define SYS_KERNEL_STATS_SWAP 7u +#define SYS_KERNEL_STATS_FILE_CACHE 8u +#define SYS_KERNEL_STATS_SURFACE 9u +#define SYS_KERNEL_STATS_FILESYSTEM 10u +#define SYS_KERNEL_STATS_DISK 11u #define SYS_KERNEL_STATS_MAX_CPUS 32u @@ -469,11 +606,15 @@ typedef struct SYS_PMM_STATS { uint64_t FreeRunHistogram[8]; } SYS_PMM_STATS; +#define SYS_CPU_THREAD_FLAG_IDLE 0x00000001u + typedef struct SYS_CPU_STAT { uint32_t CpuIndex; uint32_t LapicId; uint32_t Flags; uint32_t CurrentTid; + uint32_t CurrentThreadFlags; + uint32_t Reserved0; uint64_t SchedulerTicks; uint64_t ContextSwitches; } SYS_CPU_STAT; @@ -484,6 +625,228 @@ typedef struct SYS_CPU_STATS { SYS_CPU_STAT Cpu[SYS_KERNEL_STATS_MAX_CPUS]; } SYS_CPU_STATS; +typedef struct SYS_PFN_STATS { + uint64_t TotalFrames; + uint64_t FreeFrames; + uint64_t UsedFrames; + + uint64_t ReservedFrames; + uint64_t ZeroedFrames; + uint64_t ActiveFrames; + uint64_t StandbyFrames; + uint64_t ModifiedFrames; + uint64_t PinnedFrames; + uint64_t EvictableFrames; + uint64_t NonEvictableFrames; + uint64_t ReclaimableFrames; + uint64_t AvailableFrames; + uint64_t MappedFrames; + + uint64_t KernelFrames; + uint64_t PageTableFrames; + uint64_t UserFrames; + uint64_t CleanFileCacheFrames; + uint64_t ActiveCleanFileCacheFrames; + uint64_t StandbyCleanFileCacheFrames; + uint64_t ImageFrames; + uint64_t ActiveImageFrames; + uint64_t StandbyImageFrames; + uint64_t MappedFileFrames; + uint64_t AnonymousFrames; + uint64_t CowFrames; + uint64_t SurfaceFrames; + uint64_t DriverFrames; + uint64_t DmaFrames; + uint64_t BootloaderFrames; + uint64_t PfnDatabaseFrames; + uint64_t MmioFrames; + + uint64_t AllocationCount; + uint64_t FreeCount; + uint64_t RetainCount; + uint64_t ReleaseCount; + uint64_t PinCount; + uint64_t UnpinCount; + uint64_t FailedAllocations; + uint64_t FailedContiguousAllocations; + + uint64_t FailedInvalidRange; + uint64_t FailedEmptyRange; + uint64_t FailedReserveDenied; + uint64_t FailedNoFrame; + + uint64_t FailedContiguousInvalidRequest; + uint64_t FailedContiguousReserveDenied; + uint64_t FailedContiguousNoRun; + + uint64_t ReclaimAttempts; + uint64_t ReclaimPagesRequested; + uint64_t ReclaimPagesSucceeded; + uint64_t ReclaimPagesFailed; + uint64_t ReclaimReentrantSkips; +} SYS_PFN_STATS; + +typedef struct SYS_VAD_STATS { + uint64_t ResolveFaultCalls; + uint64_t ResolvedFaults; + uint64_t FailedFaults; + uint64_t ProtectionFaults; + uint64_t NoVadFaults; + uint64_t UncommittedFaults; + uint64_t NoAccessFaults; + uint64_t DemandZeroPages; + uint64_t ZeroPageMaps; + uint64_t ZeroPageCowSplits; + uint64_t CowFaults; + uint64_t CowPagesCopied; + uint64_t CowViewsCreated; + uint64_t CowPagesShared; + uint64_t EnsureResidentCalls; + uint64_t EnsureResidentPages; + uint64_t AlreadyResidentPages; + uint64_t FileBackedFaults; + uint64_t FileBackedReads; + uint64_t FileBackedReadFailures; + uint64_t FileBackedDiscards; + uint64_t FileBackedSharedWritableFaults; + uint64_t FileBackedDirtyFaults; + uint64_t FileBackedDirtySyncs; + uint64_t ImageBackedFaults; + uint64_t ImageBackedZeroFaults; + uint64_t ImageBackedPrivateFaults; + uint64_t ImageFixupPrivateCalls; + uint64_t ImageFixupPrivatePages; + uint64_t SwapPageOuts; + uint64_t SwapPageIns; +} SYS_VAD_STATS; + + +typedef struct SYS_FILE_CACHE_STATS { + uint64_t Lookups; + uint64_t Hits; + uint64_t Misses; + uint64_t Loads; + uint64_t LoadFailures; + uint64_t RaceDiscards; + uint64_t InflightWaits; + uint64_t Invalidations; + uint64_t DirtyPages; + uint64_t MaxDirtyPages; + uint64_t DirtyMarks; + uint64_t WritebackRequests; + uint64_t WritebackPages; + uint64_t WritebackBytes; + uint64_t WritebackFailures; + uint64_t FlushFileCalls; + uint64_t FlushFileFailures; + uint64_t MappedDirtySyncs; + uint64_t MappedDirtyPages; + uint64_t Drops; + uint64_t DropFailures; + uint64_t StandbyTransitions; + uint64_t StandbyHits; + uint64_t ReadAheadRequests; + uint64_t ReadAheadPagesRequested; + uint64_t ReadAheadPagesQueued; + uint64_t ReadAheadPagesLoaded; + uint64_t ReadAheadPagesSkipped; + uint64_t ReadAheadFailures; + uint64_t ReadAheadPagesThrottled; + uint64_t MaxReadAheadPagesPerRequest; + uint64_t BatchReads; + uint64_t BatchPagesRequested; + uint64_t BatchPagesLoaded; + uint64_t MaxBatchPagesRequested; + uint64_t MaxBatchPagesLoaded; + uint64_t ResidentPages; + uint64_t ActivePages; + uint64_t StandbyPages; + uint64_t MaxResidentPages; +} SYS_FILE_CACHE_STATS; + +typedef struct SYS_FILESYSTEM_STATS { + uint64_t PathLookups; + uint64_t PathCacheHits; + uint64_t PathCacheMisses; + uint64_t PathCacheStores; + uint64_t PathCacheInvalidations; + uint64_t PathCacheGeneration; + uint64_t DirectorySearches; + uint64_t DirectoryClustersRead; + uint64_t DirectoryEntriesScanned; + uint64_t FatChainCacheLookups; + uint64_t FatChainCacheHits; + uint64_t FatChainCacheMisses; + uint64_t FatChainCacheStores; + uint64_t FatChainCacheInvalidations; + uint64_t FatChainWalkSteps; + uint64_t DataReadRuns; + uint64_t DataReadSectors; + uint64_t DataReadRunMerges; + uint64_t MaxDataReadSpanSectors; +} SYS_FILESYSTEM_STATS; + +typedef struct SYS_DISK_STATS { + uint64_t RegisteredDisks; + uint64_t ReadRequests; + uint64_t ReadSectors; + uint64_t WriteRequests; + uint64_t WriteSectors; + uint64_t MaxReadSectors; + uint64_t MaxWriteSectors; + uint64_t ReadaheadHits; + uint64_t ReadaheadMisses; + uint64_t ReadaheadStores; + uint64_t ReadaheadInvalidations; + uint64_t ReadaheadBytesServed; + uint64_t ReadaheadSectorsLoaded; + uint64_t ReadaheadSequentialDetections; + uint64_t ReadaheadSuppressed; + uint64_t ReadaheadAllocFailures; + uint64_t ReadaheadReadFailures; + uint64_t MaxReadaheadSectors; +} SYS_DISK_STATS; + +typedef struct SYS_SWAP_STATS { + uint64_t Enabled; + uint64_t TotalSlots; + uint64_t UsedSlots; + uint64_t FreeSlots; + uint64_t HighWaterSlots; + uint64_t PageOuts; + uint64_t PageIns; + uint64_t PageOutFailures; + uint64_t PageInFailures; + uint64_t SlotAllocations; + uint64_t SlotFrees; + uint64_t SlotAllocationFailures; +} SYS_SWAP_STATS; + +typedef struct SYS_PTE_STATS { + uint64_t MapCalls; + uint64_t MapFailures; + uint64_t UnmapCalls; + uint64_t UnmapFailures; + uint64_t ProtectCalls; + uint64_t ProtectFailures; + uint64_t QueryCalls; + uint64_t QueryFailures; + uint64_t QueryByPurpose[6]; + uint64_t QueryFailuresByPurpose[6]; + uint64_t TranslateCalls; + uint64_t LocalFlushes; + uint64_t GlobalFlushes; + uint64_t ShootdownRequests; + uint64_t ShootdownTargetCpus; + uint64_t ShootdownSkippedNoTargets; + uint64_t ShootdownAcks; + uint64_t ShootdownTimeouts; + uint64_t NewMappingNoFlushes; + uint64_t ReplacedMappingFlushes; + uint64_t PageTablePagesAllocated; + uint64_t CurrentLeafMappings; +} SYS_PTE_STATS; + #define SYS_CPU_FLAG_PRESENT 0x00000001u #define SYS_CPU_FLAG_STARTED 0x00000002u #define SYS_CPU_FLAG_PARKED 0x00000004u diff --git a/code/settings.ini b/code/settings.ini index 8d64eff..d72ae7b 100644 --- a/code/settings.ini +++ b/code/settings.ini @@ -24,3 +24,7 @@ daylight_saving=auto hardware_clock=utc ; Used when timezone is a custom/fixed policy; minutes east of UTC. standard_offset_minutes=60 + +[vm] +; 0 disables swap. Nonzero swap must be between 4 MB and 8192 MB. +swap_size_mb=32 diff --git a/code/user/apps/doom/doom.c b/code/user/apps/doom/doom.c index cc41b7c..a6f7188 100644 --- a/code/user/apps/doom/doom.c +++ b/code/user/apps/doom/doom.c @@ -88,7 +88,8 @@ static int doom_read_wrapper(void* handle, void* buf, int count) if (!handle || count <= 0) return 0; DOOM_FILE_HANDLE* f = (DOOM_FILE_HANDLE*)handle; size_t read = 0; - SysReadFile(f->Handle, buf, (size_t)count, &read); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(f->Handle, buf, (size_t)count, &read); + SysReadFile(&req); f->Pos += read; if (f->Pos > f->Size) f->Size = f->Pos; return (int)read; @@ -99,7 +100,8 @@ static int doom_write_wrapper(void* handle, const void* buf, int count) if (!handle || count <= 0) return 0; DOOM_FILE_HANDLE* f = (DOOM_FILE_HANDLE*)handle; size_t written = 0; - SysWriteFile(f->Handle, buf, (size_t)count, &written); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(f->Handle, buf, (size_t)count, &written); + SysWriteFile(&req); f->Pos += written; if (f->Pos > f->Size) f->Size = f->Pos; return (int)written; @@ -262,7 +264,7 @@ static void poll_input(void) } // --------------------------------------------------------------------------- -// Render — Performs lightweight software upscale and blits to the screen +// Render — writes directly into a mapped kernel surface and presents it. // --------------------------------------------------------------------------- #define DOOM_W 320 #define DOOM_H 200 @@ -270,32 +272,65 @@ static void poll_input(void) #define DISP_W (DOOM_W * SCALE) #define DISP_H (DOOM_H * SCALE) -static uint32_t g_blit_buf[DISP_W * DISP_H]; +static uint32_t g_surface_handle; +static uint32_t* g_surface_pixels; +static uint32_t g_surface_pitch_pixels; + +static void init_surface(void) +{ + uint32_t s = SysCreateSurface(DISP_W, DISP_H, SYS_SURFACE_FORMAT_XRGB8888, + SYS_SURFACE_FLAG_NONE, &g_surface_handle); + if (s != SYS_STATUS_SUCCESS || !g_surface_handle) { + doom_print_wrapper("doom: SysCreateSurface failed\n"); + SysExitProcess(1); + } + + void* base = 0; + uint32_t pitchBytes = 0; + s = SysMapSurface(g_surface_handle, &base, &pitchBytes); + if (s != SYS_STATUS_SUCCESS || !base || pitchBytes < DISP_W * sizeof(uint32_t)) { + doom_print_wrapper("doom: SysMapSurface failed\n"); + SysExitProcess(1); + } + + g_surface_pixels = (uint32_t*)base; + g_surface_pitch_pixels = pitchBytes / sizeof(uint32_t); +} static void render_frame(const uint8_t* rgba) { const uint32_t* src = (const uint32_t*)rgba; - - // Software upscale loop mapping 320x200 buffer directly to scaled window buffer + uint32_t* dstBase = g_surface_pixels; + uint32_t dstPitch = g_surface_pitch_pixels; + + if (!dstBase || dstPitch < DISP_W) { + doom_print_wrapper("doom: surface not initialized\n"); + SysExitProcess(1); + } + for (int y = 0; y < DOOM_H; y++) { for (int x = 0; x < DOOM_W; x++) { uint32_t p = src[y * DOOM_W + x]; - - // RGBA -> BGRA conversion (standard for linear x86/64 framebuffers) + + // RGBA -> BGRA/XRGB8888 conversion for the current display path. uint32_t bgra = (p & 0xFF00FF00u) | ((p & 0x00FF0000u) >> 16) | ((p & 0x000000FFu) << 16); - // Populate SCALE * SCALE pixel block + uint32_t* block = dstBase + (size_t)(y * SCALE) * dstPitch + (size_t)(x * SCALE); for (int sy = 0; sy < SCALE; sy++) { for (int sx = 0; sx < SCALE; sx++) { - g_blit_buf[(y * SCALE + sy) * DISP_W + (x * SCALE + sx)] = bgra; + block[(size_t)sy * dstPitch + sx] = bgra; } } } } - SysBlitScreen(0, 0, DISP_W, DISP_H, g_blit_buf); + DISPLAY_RECT rect = { 0, 0, DISP_W, DISP_H }; + uint32_t code = SysPresentSurface(g_surface_handle, &rect, 1); + if (code != SYS_STATUS_SUCCESS) { + SysExitProcess(code); + } } static int build_argv_from_command_line(const char* cmdline, char* storage, size_t storageSize, @@ -373,6 +408,7 @@ int main(void) char argvStorage[512]; const char* argv[32]; int argc = build_argv_from_command_line(cmdline, argvStorage, sizeof(argvStorage), argv, 32); + init_surface(); doom_init(argc, argv, 0); do { SYS_WRITE_CONSOLE_REQUEST req = { SYS_SV_LITERAL("doom: entering game loop\n") }; SysWriteConsole(&req); } while (0); diff --git a/code/user/apps/edit/edit.c b/code/user/apps/edit/edit.c index dec31ef..6579ace 100644 --- a/code/user/apps/edit/edit.c +++ b/code/user/apps/edit/edit.c @@ -380,12 +380,14 @@ static void save_file(void) { for (int i = 0; i < gLineCount; i++) { size_t written = 0; if (gLines[i].len) { - st = SysWriteFile(h, gLines[i].text, (size_t)gLines[i].len, &written); + SYS_FILE_IO_REQUEST writeReq = SysMakeFileIoRequest(h, gLines[i].text, (size_t)gLines[i].len, &written); + st = SysWriteFile(&writeReq); if (st != 0) { SysCloseHandle(h); msg_set("save failed: write error"); return; } } if (i + 1 < gLineCount) { const char nl = '\n'; - st = SysWriteFile(h, &nl, 1, &written); + SYS_FILE_IO_REQUEST newlineReq = SysMakeFileIoRequest(h, &nl, 1, &written); + st = SysWriteFile(&newlineReq); if (st != 0) { SysCloseHandle(h); msg_set("save failed: write error"); return; } } } @@ -418,7 +420,9 @@ static void load_file(void) { char buf[512]; size_t got = 0; bool truncated = false; - while (SysReadFile(h, buf, sizeof(buf), &got) == 0 && got > 0) { + for (;;) { + SYS_FILE_IO_REQUEST readReq = SysMakeFileIoRequest(h, buf, sizeof(buf), &got); + if (SysReadFile(&readReq) != 0 || got == 0) break; for (size_t i = 0; i < got; i++) { char c = buf[i]; if (c == '\r') continue; diff --git a/code/user/apps/memtop/memtop.c b/code/user/apps/memtop/memtop.c new file mode 100644 index 0000000..72ea855 --- /dev/null +++ b/code/user/apps/memtop/memtop.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include + +static int u64_to_string(uint64_t n, char* buf) { + int i = 0; + if (n == 0) { + buf[i++] = '0'; + buf[i] = '\0'; + return i; + } + char rev[24]; + int r_idx = 0; + while (n > 0) { + rev[r_idx++] = (char)('0' + (n % 10)); + n /= 10; + } + while (r_idx > 0) { + buf[i++] = rev[--r_idx]; + } + buf[i] = '\0'; + return i; +} + +int main(void) { + SYS_CONSOLE_INFO console; + SYS_MEMORY_STATUS mem; + char metricsBuf[128]; + + while (true) { + if (SysGetConsoleInfo(&console) == SYS_STATUS_SUCCESS && + SysGetMemoryStatus(&mem) == SYS_STATUS_SUCCESS) { + + // If the system isn't reporting columns correctly, default to a safe standard width + int columns = (console.Columns > 0) ? (int)console.Columns : 80; + + char freeStr[24], poolStr[24], heapStr[24]; + u64_to_string(mem.FreePhysicalBytes / 1024u, freeStr); + u64_to_string(mem.KernelPoolBytes / 1024u, poolStr); + u64_to_string(mem.UserHeapBytes / 1024u, heapStr); + + metricsBuf[0] = '\0'; + strcat(metricsBuf, "[ Free: "); + strcat(metricsBuf, freeStr); + strcat(metricsBuf, "K | Pool: "); + strcat(metricsBuf, poolStr); + strcat(metricsBuf, "K | Heap: "); + strcat(metricsBuf, heapStr); + strcat(metricsBuf, "K ]"); + + size_t textLen = strlen(metricsBuf); + int targetX = columns - (int)textLen; + int targetY = 0; + + if (targetX >= 0) { + size_t written = 0; + + // Loop through the string and use the console's dedicated text filling API + // This utilizes the console's native font configuration instead of drawing raw screen bits + for (size_t i = 0; i < textLen; i++) { + SysFillConsoleCharacter(metricsBuf[i], targetX + (int)i, targetY, 1, &written); + } + } + } + + SysSleep(500); + } + return 0; +} \ No newline at end of file diff --git a/code/user/apps/shell_native/shell_native.c b/code/user/apps/shell_native/shell_native.c index 5d638cb..c36b796 100644 --- a/code/user/apps/shell_native/shell_native.c +++ b/code/user/apps/shell_native/shell_native.c @@ -5,9 +5,13 @@ #include #include #include +#include #define MAX_CMD 256 +#define SHELL_FOREGROUND_POLL_MS 30u #define MAX_PROCS 64 // Maximum number of processes we will fetch at once +#define MAX_THREADS 128 +#define SHELL_PROMPT "> " // Output helpers. static void print(const char* s) { SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; SysWriteConsole(&req); } @@ -62,6 +66,26 @@ static void printInt(int32_t n) { else { printUInt((uint32_t)n); } } +// Parses an unsigned decimal integer. +static bool parseUInt(const char* text, uint32_t* out) { + if (!text || !out) return false; + while (*text == ' ' || *text == '\t') text++; + if (*text < '0' || *text > '9') return false; + + uint32_t value = 0; + while (*text >= '0' && *text <= '9') { + uint32_t digit = (uint32_t)(*text - '0'); + if (value > (0xffffffffu - digit) / 10u) return false; + value = value * 10u + digit; + text++; + } + + while (*text == ' ' || *text == '\t') text++; + if (*text != '\0') return false; + *out = value; + return true; +} + // Prints u Int64. static void printUInt64(uint64_t n) { if (n == 0) { print("0"); return; } @@ -103,6 +127,21 @@ static void printUInt64Width(uint64_t n, size_t width) { printUInt64(n); } +// Names nt-lite process-abort exit statuses. +static const char* processExitStatusName(uint32_t code) { + switch (code) { + case STATUS_PROCESS_KILLED: return "killed"; + case STATUS_PROCESS_PAGE_FAULT: return "page-fault"; + case STATUS_PROCESS_GENERAL_PROTECTION: return "general-protection"; + case STATUS_PROCESS_ILLEGAL_OPCODE: return "illegal-opcode"; + case STATUS_PROCESS_DIVIDE_BY_ZERO: return "divide-by-zero"; + case STATUS_PROCESS_BREAKPOINT: return "breakpoint"; + case STATUS_PROCESS_ALIGNMENT_FAULT: return "alignment-fault"; + case STATUS_PROCESS_UNKNOWN_FAULT: return "unknown-fault"; + default: return NULL; + } +} + // Prints hex32. static void printHex32(uint32_t v) { static const char hex[] = "0123456789abcdef"; @@ -140,18 +179,399 @@ static void printStringWidth(const char* s, size_t width) { if (width > len) printSpaces(width - len); } -// Line editor. -static void readLine(char* buf, int len) { - int pos = 0; - while (true) { - char c = SysReadKeyboardChar(); - if (!c) { SysYield(); continue; } - if (c == '\r' || c == '\n') { print("\n"); break; } - if ((c == '\b' || c == 127) && pos > 0) { pos--; print("\b \b"); continue; } - if (c < 0x20 || c == 127) continue; - if (pos < len - 1) { buf[pos++] = c; char echo[2] = {c, '\0'}; print(echo); } +#define SHELL_HISTORY_DEPTH 16 + +static char gHistory[SHELL_HISTORY_DEPTH][MAX_CMD]; +static size_t gHistoryCount; + +static const char* const gShellBuiltinCommands[] = { + "cd", + "clear", + "crash", + "date", + "del", + "dir", + "display", + "exit", + "gpu", + "help", + "kill", + "mem", + "ps", + "stats", + "telemetry", + "threads", + "time", + "type", + "write", +}; + +// Converts a key event into the shell's printable character model. +static char shellKeyToChar(INPUT_KEY key, uint32_t modifiers) { + bool shifted = (modifiers & INPUT_MOD_SHIFT) != 0; + bool caps = (modifiers & INPUT_MOD_CAPS_LOCK) != 0; + + if (key >= INPUT_KEY_A && key <= INPUT_KEY_Z) { + char c = (char)('a' + (key - INPUT_KEY_A)); + if (shifted ^ caps) c = (char)(c - 'a' + 'A'); + return c; } - buf[pos] = '\0'; + + if (key >= INPUT_KEY_1 && key <= INPUT_KEY_9) { + static const char shiftedDigits[] = "!@#$%^&*("; + char normal = (char)('1' + (key - INPUT_KEY_1)); + return shifted ? shiftedDigits[key - INPUT_KEY_1] : normal; + } + + switch (key) { + case INPUT_KEY_0: return shifted ? ')' : '0'; + case INPUT_KEY_MINUS: return shifted ? '_' : '-'; + case INPUT_KEY_EQUAL: return shifted ? '+' : '='; + case INPUT_KEY_LEFT_BRACKET: return shifted ? '{' : '['; + case INPUT_KEY_RIGHT_BRACKET: return shifted ? '}' : ']'; + case INPUT_KEY_BACKSLASH: return shifted ? '|' : '\\'; + case INPUT_KEY_SEMICOLON: return shifted ? ':' : ';'; + case INPUT_KEY_APOSTROPHE: return shifted ? '"' : '\''; + case INPUT_KEY_GRAVE: return shifted ? '~' : '`'; + case INPUT_KEY_COMMA: return shifted ? '<' : ','; + case INPUT_KEY_PERIOD: return shifted ? '>' : '.'; + case INPUT_KEY_SLASH: return shifted ? '?' : '/'; + case INPUT_KEY_SPACE: return ' '; + case INPUT_KEY_TAB: return '\t'; + case INPUT_KEY_KP_DIVIDE: return '/'; + case INPUT_KEY_KP_MULTIPLY: return '*'; + case INPUT_KEY_KP_MINUS: return '-'; + case INPUT_KEY_KP_PLUS: return '+'; + case INPUT_KEY_KP_0: return '0'; + case INPUT_KEY_KP_1: return '1'; + case INPUT_KEY_KP_2: return '2'; + case INPUT_KEY_KP_3: return '3'; + case INPUT_KEY_KP_4: return '4'; + case INPUT_KEY_KP_5: return '5'; + case INPUT_KEY_KP_6: return '6'; + case INPUT_KEY_KP_7: return '7'; + case INPUT_KEY_KP_8: return '8'; + case INPUT_KEY_KP_9: return '9'; + case INPUT_KEY_KP_PERIOD: return '.'; + default: return 0; + } +} + +// Adds a non-empty command to the in-memory history ring. +static void shellHistoryAdd(const char* cmd) { + if (!cmd || !*cmd) return; + if (gHistoryCount > 0 && strcmp(gHistory[gHistoryCount - 1], cmd) == 0) return; + + if (gHistoryCount == SHELL_HISTORY_DEPTH) { + memmove(gHistory[0], gHistory[1], (SHELL_HISTORY_DEPTH - 1) * MAX_CMD); + gHistoryCount--; + } + + strncpy(gHistory[gHistoryCount], cmd, MAX_CMD - 1); + gHistory[gHistoryCount][MAX_CMD - 1] = '\0'; + gHistoryCount++; +} + +// Replaces the visible line while keeping the cursor at the end. +static void shellEditorReplaceLine(char* buf, int len, int* pos, const char* text) { + int oldPos = *pos; + for (int i = 0; i < oldPos; i++) print("\b"); + for (int i = 0; i < oldPos; i++) print(" "); + for (int i = 0; i < oldPos; i++) print("\b"); + + int newPos = 0; + if (text) { + while (text[newPos] && newPos < len - 1) { + buf[newPos] = text[newPos]; + newPos++; + } + } + buf[newPos] = '\0'; + *pos = newPos; + print(buf); +} + +// Returns true when a character separates shell tokens. +static bool shellIsTokenSeparator(char c) { + return c == ' ' || c == '\t'; +} + +// Returns true when a character separates path components. +static bool shellIsPathSeparator(char c) { + return c == '/' || c == '\\'; +} + +// Returns true when the token already names an explicit path. +static bool shellTokenHasPathSeparator(const char* token, size_t length) { + for (size_t i = 0; i < length; i++) { + if (shellIsPathSeparator(token[i])) return true; + } + return false; +} + +// Returns true when name starts with prefix. +static bool shellNameStartsWith(const char* name, const char* prefix, size_t prefixLength) { + if (!name || !prefix) return false; + for (size_t i = 0; i < prefixLength; i++) { + if (name[i] == '\0' || name[i] != prefix[i]) return false; + } + return true; +} + +// Extends an in-progress common prefix with one completion candidate. +static void shellUpdateCommonPrefix(char* common, size_t commonSize, const char* name, uint32_t matchCount) { + if (!common || commonSize == 0 || !name) return; + if (matchCount == 0) { + strncpy(common, name, commonSize - 1); + common[commonSize - 1] = '\0'; + return; + } + + size_t i = 0; + while (common[i] && name[i] && common[i] == name[i]) i++; + common[i] = '\0'; +} + +// Appends text to the edited line and echoes it. +static bool shellEditorAppend(char* buf, int len, int* pos, const char* text) { + if (!buf || len <= 0 || !pos || !text) return false; + + bool changed = false; + while (*text && *pos < len - 1) { + buf[*pos] = *text; + (*pos)++; + buf[*pos] = '\0'; + char echo[2] = { *text, '\0' }; + print(echo); + text++; + changed = true; + } + return changed; +} + +// Prints one completion candidate. +static void shellPrintCompletionCandidate(const char* name, bool directory) { + print(" "); + print(name); + if (directory) print("/"); + print("\n"); +} + +// Tracks completion candidates without storing the full candidate list. +typedef struct SHELL_COMPLETION_STATE { + uint32_t MatchCount; + bool LastIsDirectory; + bool LastAddsSpace; + char Common[SYS_DIR_NAME_MAX]; +} SHELL_COMPLETION_STATE; + +// Records one completion candidate. +static void shellRecordCompletion(SHELL_COMPLETION_STATE* state, const char* name, bool directory, bool addsSpace) { + if (!state || !name || !*name) return; + shellUpdateCommonPrefix(state->Common, sizeof(state->Common), name, state->MatchCount); + state->MatchCount++; + state->LastIsDirectory = directory; + state->LastAddsSpace = addsSpace; +} + +// Collects builtin command matches for a command-position token. +static void shellCollectBuiltinCompletions(const char* prefix, size_t prefixLength, + SHELL_COMPLETION_STATE* state, + bool printMatches) { + size_t count = sizeof(gShellBuiltinCommands) / sizeof(gShellBuiltinCommands[0]); + for (size_t i = 0; i < count; i++) { + const char* name = gShellBuiltinCommands[i]; + if (!shellNameStartsWith(name, prefix, prefixLength)) continue; + if (printMatches) shellPrintCompletionCandidate(name, false); + else shellRecordCompletion(state, name, false, true); + } +} + +// Splits a token into a directory to scan and a filename prefix to match. +static bool shellSplitCompletionPath(const char* token, size_t tokenLength, + char* dir, size_t dirSize, + const char** prefixOut, size_t* prefixLengthOut) { + if (!token || !dir || dirSize == 0 || !prefixOut || !prefixLengthOut) return false; + + size_t lastSeparator = (size_t)-1; + for (size_t i = 0; i < tokenLength; i++) { + if (shellIsPathSeparator(token[i])) lastSeparator = i; + } + + if (lastSeparator == (size_t)-1) { + if (dirSize < 2) return false; + dir[0] = '.'; + dir[1] = '\0'; + *prefixOut = token; + *prefixLengthOut = tokenLength; + return true; + } + + size_t dirLength = lastSeparator; + if (lastSeparator == 0) dirLength = 1; + if (lastSeparator >= 2 && token[1] == ':' && shellIsPathSeparator(token[2])) dirLength = lastSeparator + 1; + if (dirLength + 1 > dirSize) return false; + + memcpy(dir, token, dirLength); + dir[dirLength] = '\0'; + *prefixOut = token + lastSeparator + 1; + *prefixLengthOut = tokenLength - lastSeparator - 1; + return true; +} + +// Collects filesystem path matches using the existing directory enumeration ABI. +static void shellCollectPathCompletions(const char* token, size_t tokenLength, + SHELL_COMPLETION_STATE* state, + bool printMatches) { + char dir[MAX_CMD]; + const char* prefix = token; + size_t prefixLength = tokenLength; + + if (!shellSplitCompletionPath(token, tokenLength, dir, sizeof(dir), &prefix, &prefixLength)) return; + + uint32_t hDir = 0; + if (open_dir(dir, &hDir) != 0) return; + + SYS_DIR_ENTRY entry; + while (SysReadDir(hDir, &entry) == 0) { + if (!entry.Name[0]) continue; + if (!shellNameStartsWith(entry.Name, prefix, prefixLength)) continue; + + bool directory = (entry.Attr & 0x10u) != 0; + if (printMatches) shellPrintCompletionCandidate(entry.Name, directory); + else shellRecordCompletion(state, entry.Name, directory, true); + } + + SysCloseHandle(hDir); +} + +// Redraws the prompt and current edit buffer after a completion list. +static void shellRedrawPromptAndLine(const char* buf) { + print(SHELL_PROMPT); + print(buf ? buf : ""); +} + +// Completes the token at the end of the current line. +static void shellCompleteLine(char* buf, int len, int* pos) { + if (!buf || len <= 0 || !pos || *pos < 0) return; + + size_t cursor = (size_t)*pos; + size_t tokenStart = cursor; + while (tokenStart > 0 && !shellIsTokenSeparator(buf[tokenStart - 1])) tokenStart--; + + const char* token = buf + tokenStart; + size_t tokenLength = cursor - tokenStart; + bool commandPosition = tokenStart == 0; + bool explicitPath = shellTokenHasPathSeparator(token, tokenLength); + + char completionDir[MAX_CMD]; + const char* namePrefix = token; + size_t namePrefixLength = tokenLength; + (void)shellSplitCompletionPath(token, tokenLength, completionDir, sizeof(completionDir), &namePrefix, &namePrefixLength); + + SHELL_COMPLETION_STATE state; + memset(&state, 0, sizeof(state)); + + if (commandPosition && !explicitPath) { + shellCollectBuiltinCompletions(token, tokenLength, &state, false); + } + shellCollectPathCompletions(token, tokenLength, &state, false); + + if (state.MatchCount == 0) return; + + size_t commonLength = strlen(state.Common); + bool expanded = commonLength > namePrefixLength; + if (expanded) { + shellEditorAppend(buf, len, pos, state.Common + namePrefixLength); + } + + if (state.MatchCount == 1) { + if (state.LastIsDirectory) { + if (*pos < len - 1 && *pos > 0 && !shellIsPathSeparator(buf[*pos - 1])) shellEditorAppend(buf, len, pos, "/"); + } else if (state.LastAddsSpace) { + if (*pos < len - 1 && (*pos == 0 || buf[*pos - 1] != ' ')) shellEditorAppend(buf, len, pos, " "); + } + return; + } + + if (expanded) return; + + print("\n"); + if (commandPosition && !explicitPath) { + shellCollectBuiltinCompletions(token, tokenLength, NULL, true); + } + shellCollectPathCompletions(token, tokenLength, NULL, true); + shellRedrawPromptAndLine(buf); +} + +// Reads one editable command line from the keyboard. +static bool readLine(char* buf, int len) { + int pos = 0; + size_t historyCursor = gHistoryCount; + if (len <= 0) return false; + buf[0] = '\0'; + + while (true) { + INPUT_KEY_RECORD input; + if (SysReadInputEvent(&input) != 0) { SysYield(); continue; } + + INPUT_KEY key = input.Key; + if (key == INPUT_KEY_NONE) { SysYield(); continue; } + + uint32_t modifiers = input.Modifiers; + if ((modifiers & INPUT_MOD_CTRL) && key == INPUT_KEY_C) { + buf[0] = '\0'; + print("^C\n"); + return true; + } + + if (key == INPUT_KEY_ENTER || key == INPUT_KEY_KP_ENTER) { + print("\n"); + break; + } + + if (key == INPUT_KEY_BACKSPACE) { + if (pos > 0) { + pos--; + buf[pos] = '\0'; + print("\b \b"); + } + continue; + } + + if (key == INPUT_KEY_UP) { + if (historyCursor > 0) { + historyCursor--; + shellEditorReplaceLine(buf, len, &pos, gHistory[historyCursor]); + } + continue; + } + + if (key == INPUT_KEY_DOWN) { + if (historyCursor < gHistoryCount) { + historyCursor++; + shellEditorReplaceLine(buf, len, &pos, + historyCursor < gHistoryCount ? gHistory[historyCursor] : ""); + } + continue; + } + + if (key == INPUT_KEY_TAB) { + shellCompleteLine(buf, len, &pos); + continue; + } + + char c = input.Char ? input.Char : shellKeyToChar(key, modifiers); + if (c < 0x20 || c == 127) continue; + if (pos < len - 1) { + buf[pos++] = c; + buf[pos] = '\0'; + char echo[2] = { c, '\0' }; + print(echo); + } + } + + return false; } // Implements the trim operation. @@ -170,8 +590,10 @@ static void cmd_help() { " mem - Show memory usage\n" " time - Show system wall-clock time\n" " display - Show display provider and counters\n" - " stats - Show scheduler/CPU/PMM telemetry\n" + " stats - Show scheduler/CPU/MM telemetry\n" " ps - List processes\n" + " threads - Show one process's thread statuses\n" + " kill - Terminate a process\n" " dir [path] - List directory contents\n" " cd - Change directory\n" " type - Display file contents\n" @@ -209,6 +631,14 @@ static void cmd_stats(void) { SYS_SCHEDULER_STATS sched; SYS_PMM_STATS pmm; SYS_CPU_STATS cpu; + SYS_PFN_STATS pfn; + SYS_VAD_STATS vad; + SYS_SWAP_STATS swap; + SYS_FILE_CACHE_STATS fileCache; + SYS_FILESYSTEM_STATS filesystem; + SYS_DISK_STATS disk; + SYS_PTE_STATS pte; + SYS_SURFACE_STATS surface; uint32_t status; status = SysQueryKernelStats(SYS_KERNEL_STATS_SCHEDULER, &sched, sizeof(sched), NULL); @@ -235,6 +665,70 @@ static void cmd_stats(void) { return; } + status = SysQueryKernelStats(SYS_KERNEL_STATS_PFN, &pfn, sizeof(pfn), NULL); + if (status != 0) { + print("stats: pfn query failed: "); + printUInt(status); + print("\n"); + return; + } + + status = SysQueryKernelStats(SYS_KERNEL_STATS_VAD, &vad, sizeof(vad), NULL); + if (status != 0) { + print("stats: vad query failed: "); + printUInt(status); + print("\n"); + return; + } + + status = SysQueryKernelStats(SYS_KERNEL_STATS_SWAP, &swap, sizeof(swap), NULL); + if (status != 0) { + print("stats: swap query failed: "); + printUInt(status); + print("\n"); + return; + } + + status = SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, &fileCache, sizeof(fileCache), NULL); + if (status != 0) { + print("stats: file cache query failed: "); + printUInt(status); + print("\n"); + return; + } + + status = SysQueryKernelStats(SYS_KERNEL_STATS_FILESYSTEM, &filesystem, sizeof(filesystem), NULL); + if (status != 0) { + print("stats: filesystem query failed: "); + printUInt(status); + print("\n"); + return; + } + + status = SysQueryKernelStats(SYS_KERNEL_STATS_DISK, &disk, sizeof(disk), NULL); + if (status != 0) { + print("stats: disk query failed: "); + printUInt(status); + print("\n"); + return; + } + + status = SysQueryKernelStats(SYS_KERNEL_STATS_PTE, &pte, sizeof(pte), NULL); + if (status != 0) { + print("stats: pte query failed: "); + printUInt(status); + print("\n"); + return; + } + + status = SysQueryKernelStats(SYS_KERNEL_STATS_SURFACE, &surface, sizeof(surface), NULL); + if (status != 0) { + print("stats: surface query failed: "); + printUInt(status); + print("\n"); + return; + } + print("Scheduler:\n"); print(" ticks: "); printUInt64(sched.Ticks); print("\n"); print(" context switches: "); printUInt64(sched.ContextSwitches); print("\n"); @@ -256,7 +750,12 @@ static void cmd_stats(void) { SYS_CPU_STAT* c = &cpu.Cpu[i]; print(" cpu"); printUInt(c->CpuIndex); print(" lapic="); printUInt(c->LapicId); - print(" tid="); printUInt(c->CurrentTid); + print(" current="); + if (c->CurrentThreadFlags & SYS_CPU_THREAD_FLAG_IDLE) { + print("idle/"); printUInt(c->CpuIndex); + } else { + print("tid/"); printUInt(c->CurrentTid); + } print(" ticks="); printUInt64(c->SchedulerTicks); print(" switches="); printUInt64(c->ContextSwitches); print(" flags="); printCpuFlags(c->Flags); @@ -269,6 +768,8 @@ static void cmd_stats(void) { print(" free: "); printPagesKiB(pmm.FreeFrames); print(" (frames="); printUInt64(pmm.FreeFrames); print(")\n"); print(" largest free run: "); printPagesKiB(pmm.LargestFreeRun); print(" (frames="); printUInt64(pmm.LargestFreeRun); print(")\n"); print(" free runs: "); printUInt64(pmm.FreeRunCount); print("\n"); + print(" available+reclaim: "); printSizeKiB(pfn.AvailableFrames * 4096ull); + print(" (reclaimable="); printSizeKiB(pfn.ReclaimableFrames * 4096ull); print(")\n"); print(" run histogram:\n"); print(" 1: "); printUInt64(pmm.FreeRunHistogram[0]); print("\n"); print(" 2-3: "); printUInt64(pmm.FreeRunHistogram[1]); print("\n"); @@ -278,6 +779,236 @@ static void cmd_stats(void) { print(" 64-255: "); printUInt64(pmm.FreeRunHistogram[5]); print("\n"); print(" 256-1023: "); printUInt64(pmm.FreeRunHistogram[6]); print("\n"); print(" 1024+: "); printUInt64(pmm.FreeRunHistogram[7]); print("\n"); + + print("PFN database:\n"); + print(" states: active="); printUInt64(pfn.ActiveFrames); + print(" free="); printUInt64(pfn.FreeFrames); + print(" zeroed="); printUInt64(pfn.ZeroedFrames); + print(" standby="); printUInt64(pfn.StandbyFrames); + print(" modified="); printUInt64(pfn.ModifiedFrames); + print(" reserved="); printUInt64(pfn.ReservedFrames); print("\n"); + print(" eviction: evictable="); printUInt64(pfn.EvictableFrames); + print(" reclaimable="); printUInt64(pfn.ReclaimableFrames); + print(" available="); printUInt64(pfn.AvailableFrames); + print(" mapped="); printUInt64(pfn.MappedFrames); + print(" non-evictable="); printUInt64(pfn.NonEvictableFrames); + print(" pinned="); printUInt64(pfn.PinnedFrames); print("\n"); + print(" usage frames: kernel="); printUInt64(pfn.KernelFrames); + print(" pt="); printUInt64(pfn.PageTableFrames); + print(" user="); printUInt64(pfn.UserFrames); + print(" driver="); printUInt64(pfn.DriverFrames); + print(" dma="); printUInt64(pfn.DmaFrames); + print(" boot="); printUInt64(pfn.BootloaderFrames); + print(" pfn-db="); printUInt64(pfn.PfnDatabaseFrames); + print(" mmio="); printUInt64(pfn.MmioFrames); print("\n"); + print(" buckets: clean-file="); printUInt64(pfn.CleanFileCacheFrames); + print(" (active="); printUInt64(pfn.ActiveCleanFileCacheFrames); + print(" standby="); printUInt64(pfn.StandbyCleanFileCacheFrames); print(")"); + print(" image="); printUInt64(pfn.ImageFrames); + print(" (active="); printUInt64(pfn.ActiveImageFrames); + print(" standby="); printUInt64(pfn.StandbyImageFrames); print(")"); + print(" mapped-file="); printUInt64(pfn.MappedFileFrames); + print(" anon="); printUInt64(pfn.AnonymousFrames); + print(" cow="); printUInt64(pfn.CowFrames); + print(" surface="); printUInt64(pfn.SurfaceFrames); print("\n"); + print(" operations: alloc="); printUInt64(pfn.AllocationCount); + print(" free="); printUInt64(pfn.FreeCount); + print(" retain="); printUInt64(pfn.RetainCount); + print(" release="); printUInt64(pfn.ReleaseCount); + print(" pin="); printUInt64(pfn.PinCount); + print(" unpin="); printUInt64(pfn.UnpinCount); print("\n"); + print(" failures: alloc="); printUInt64(pfn.FailedAllocations); + print(" contiguous="); printUInt64(pfn.FailedContiguousAllocations); print("\n"); + print(" alloc reasons: invalid-range="); printUInt64(pfn.FailedInvalidRange); + print(" empty-range="); printUInt64(pfn.FailedEmptyRange); + print(" reserve-denied="); printUInt64(pfn.FailedReserveDenied); + print(" no-frame="); printUInt64(pfn.FailedNoFrame); print("\n"); + print(" contig reasons: invalid="); printUInt64(pfn.FailedContiguousInvalidRequest); + print(" reserve-denied="); printUInt64(pfn.FailedContiguousReserveDenied); + print(" no-run="); printUInt64(pfn.FailedContiguousNoRun); print("\n"); + print(" reclaim: attempts="); printUInt64(pfn.ReclaimAttempts); + print(" requested="); printUInt64(pfn.ReclaimPagesRequested); + print(" succeeded="); printUInt64(pfn.ReclaimPagesSucceeded); + print(" failed="); printUInt64(pfn.ReclaimPagesFailed); + print(" reentrant-skips="); printUInt64(pfn.ReclaimReentrantSkips); print("\n"); + + print("VAD faults:\n"); + print(" resolve: calls="); printUInt64(vad.ResolveFaultCalls); + print(" resolved="); printUInt64(vad.ResolvedFaults); + print(" failed="); printUInt64(vad.FailedFaults); print("\n"); + print(" failure reasons: protection="); printUInt64(vad.ProtectionFaults); + print(" no-vad="); printUInt64(vad.NoVadFaults); + print(" uncommitted="); printUInt64(vad.UncommittedFaults); + print(" noaccess="); printUInt64(vad.NoAccessFaults); print("\n"); + print(" materialization: demand-zero="); printUInt64(vad.DemandZeroPages); + print(" zero-maps="); printUInt64(vad.ZeroPageMaps); + print(" zero-splits="); printUInt64(vad.ZeroPageCowSplits); + print(" ensure-calls="); printUInt64(vad.EnsureResidentCalls); + print(" ensure-pages="); printUInt64(vad.EnsureResidentPages); + print(" already-resident="); printUInt64(vad.AlreadyResidentPages); print("\n"); + print(" cow: faults="); printUInt64(vad.CowFaults); + print(" copied="); printUInt64(vad.CowPagesCopied); + print(" views="); printUInt64(vad.CowViewsCreated); + print(" shared-pages="); printUInt64(vad.CowPagesShared); print("\n"); + print(" file-backed: faults="); printUInt64(vad.FileBackedFaults); + print(" reads="); printUInt64(vad.FileBackedReads); + print(" read-fail="); printUInt64(vad.FileBackedReadFailures); + print(" discards="); printUInt64(vad.FileBackedDiscards); print("\n"); + print(" mapped-file: shared-faults="); printUInt64(vad.FileBackedSharedWritableFaults); + print(" dirty-faults="); printUInt64(vad.FileBackedDirtyFaults); + print(" dirty-syncs="); printUInt64(vad.FileBackedDirtySyncs); print("\n"); + print(" image-backed: faults="); printUInt64(vad.ImageBackedFaults); + print(" zero-faults="); printUInt64(vad.ImageBackedZeroFaults); + print(" private-faults="); printUInt64(vad.ImageBackedPrivateFaults); print("\n"); + print(" image fixups: calls="); printUInt64(vad.ImageFixupPrivateCalls); + print(" private-pages="); printUInt64(vad.ImageFixupPrivatePages); print("\n"); + print(" swap-backed: page-outs="); printUInt64(vad.SwapPageOuts); + print(" page-ins="); printUInt64(vad.SwapPageIns); print("\n"); + + print("Swap:\n"); + print(" enabled: "); printUInt64(swap.Enabled); print("\n"); + print(" slots: total="); printUInt64(swap.TotalSlots); + print(" used="); printUInt64(swap.UsedSlots); + print(" free="); printUInt64(swap.FreeSlots); + print(" high-water="); printUInt64(swap.HighWaterSlots); print("\n"); + print(" io: out="); printUInt64(swap.PageOuts); + print(" in="); printUInt64(swap.PageIns); + print(" out-fail="); printUInt64(swap.PageOutFailures); + print(" in-fail="); printUInt64(swap.PageInFailures); print("\n"); + print(" slots ops: alloc="); printUInt64(swap.SlotAllocations); + print(" free="); printUInt64(swap.SlotFrees); + print(" alloc-fail="); printUInt64(swap.SlotAllocationFailures); print("\n"); + + print("File cache:\n"); + print(" lookups: total="); printUInt64(fileCache.Lookups); + print(" hits="); printUInt64(fileCache.Hits); + print(" misses="); printUInt64(fileCache.Misses); print("\n"); + print(" loads: ok="); printUInt64(fileCache.Loads); + print(" fail="); printUInt64(fileCache.LoadFailures); + print(" races="); printUInt64(fileCache.RaceDiscards); + print(" waits="); printUInt64(fileCache.InflightWaits); print("\n"); + print(" lifetime: resident="); printUInt64(fileCache.ResidentPages); + print(" active="); printUInt64(fileCache.ActivePages); + print(" standby="); printUInt64(fileCache.StandbyPages); + print(" max="); printUInt64(fileCache.MaxResidentPages); + print(" invalidations="); printUInt64(fileCache.Invalidations); print("\n"); + print(" dirty/writeback: dirty="); printUInt64(fileCache.DirtyPages); + print(" max-dirty="); printUInt64(fileCache.MaxDirtyPages); + print(" marks="); printUInt64(fileCache.DirtyMarks); + print(" wb-req="); printUInt64(fileCache.WritebackRequests); + print(" wb-pages="); printUInt64(fileCache.WritebackPages); + print(" wb-bytes="); printUInt64(fileCache.WritebackBytes); + print(" wb-fail="); printUInt64(fileCache.WritebackFailures); print("\n"); + print(" mapped dirty: syncs="); printUInt64(fileCache.MappedDirtySyncs); + print(" pages="); printUInt64(fileCache.MappedDirtyPages); print("\n"); + print(" flushes: calls="); printUInt64(fileCache.FlushFileCalls); + print(" fail="); printUInt64(fileCache.FlushFileFailures); print("\n"); + print(" reclaim: drops="); printUInt64(fileCache.Drops); + print(" standby="); printUInt64(fileCache.StandbyTransitions); + print(" standby-hits="); printUInt64(fileCache.StandbyHits); + print(" drop-fail="); printUInt64(fileCache.DropFailures); print("\n"); + print(" read-ahead: requests="); printUInt64(fileCache.ReadAheadRequests); + print(" requested="); printUInt64(fileCache.ReadAheadPagesRequested); + print(" queued="); printUInt64(fileCache.ReadAheadPagesQueued); + print(" loaded="); printUInt64(fileCache.ReadAheadPagesLoaded); + print(" skipped="); printUInt64(fileCache.ReadAheadPagesSkipped); + print(" fail="); printUInt64(fileCache.ReadAheadFailures); + print(" throttled="); printUInt64(fileCache.ReadAheadPagesThrottled); print("\n"); + print(" read-ahead max: pages/request="); printUInt64(fileCache.MaxReadAheadPagesPerRequest); print("\n"); + print(" batch fills: reads="); printUInt64(fileCache.BatchReads); + print(" requested="); printUInt64(fileCache.BatchPagesRequested); + print(" loaded="); printUInt64(fileCache.BatchPagesLoaded); + print(" max-request="); printUInt64(fileCache.MaxBatchPagesRequested); + print(" max-loaded="); printUInt64(fileCache.MaxBatchPagesLoaded); print("\n"); + + print("Filesystem:\n"); + print(" path cache: lookups="); printUInt64(filesystem.PathLookups); + print(" hits="); printUInt64(filesystem.PathCacheHits); + print(" misses="); printUInt64(filesystem.PathCacheMisses); + print(" stores="); printUInt64(filesystem.PathCacheStores); print("\n"); + print(" invalidation: count="); printUInt64(filesystem.PathCacheInvalidations); + print(" generation="); printUInt64(filesystem.PathCacheGeneration); print("\n"); + print(" directory search: searches="); printUInt64(filesystem.DirectorySearches); + print(" clusters="); printUInt64(filesystem.DirectoryClustersRead); + print(" entries="); printUInt64(filesystem.DirectoryEntriesScanned); print("\n"); + print(" FAT chain cache: lookups="); printUInt64(filesystem.FatChainCacheLookups); + print(" hits="); printUInt64(filesystem.FatChainCacheHits); + print(" misses="); printUInt64(filesystem.FatChainCacheMisses); + print(" stores="); printUInt64(filesystem.FatChainCacheStores); print("\n"); + print(" FAT chain work: invalidations="); printUInt64(filesystem.FatChainCacheInvalidations); + print(" fat-steps="); printUInt64(filesystem.FatChainWalkSteps); print("\n"); + print(" data read spans: runs="); printUInt64(filesystem.DataReadRuns); + print(" sectors="); printUInt64(filesystem.DataReadSectors); + print(" merges="); printUInt64(filesystem.DataReadRunMerges); + print(" max-sectors="); printUInt64(filesystem.MaxDataReadSpanSectors); print("\n"); + + print("Disk I/O:\n"); + print(" devices: registered="); printUInt64(disk.RegisteredDisks); print("\n"); + print(" reads: requests="); printUInt64(disk.ReadRequests); + print(" sectors="); printUInt64(disk.ReadSectors); + print(" max-sectors="); printUInt64(disk.MaxReadSectors); print("\n"); + print(" writes: requests="); printUInt64(disk.WriteRequests); + print(" sectors="); printUInt64(disk.WriteSectors); + print(" max-sectors="); printUInt64(disk.MaxWriteSectors); print("\n"); + print(" read-ahead: hits="); printUInt64(disk.ReadaheadHits); + print(" misses="); printUInt64(disk.ReadaheadMisses); + print(" stores="); printUInt64(disk.ReadaheadStores); + print(" sectors="); printUInt64(disk.ReadaheadSectorsLoaded); + print(" max-sectors="); printUInt64(disk.MaxReadaheadSectors); print("\n"); + print(" read-ahead misc: served-bytes="); printUInt64(disk.ReadaheadBytesServed); + print(" seq="); printUInt64(disk.ReadaheadSequentialDetections); + print(" invalidations="); printUInt64(disk.ReadaheadInvalidations); + print(" suppressed="); printUInt64(disk.ReadaheadSuppressed); + print(" alloc-fail="); printUInt64(disk.ReadaheadAllocFailures); + print(" read-fail="); printUInt64(disk.ReadaheadReadFailures); print("\n"); + + print("Surfaces:\n"); + print(" lifetime: created="); printUInt64(surface.CreatedSurfaces); + print(" destroyed="); printUInt64(surface.DestroyedSurfaces); + print(" live="); printUInt64(surface.LiveSurfaces); print("\n"); + print(" live memory: pages="); printUInt64(surface.LivePages); + print(" bytes="); printUInt64(surface.LiveBytes); print("\n"); + print(" maps/presents: maps="); printUInt64(surface.MapCalls); + print(" presents="); printUInt64(surface.PresentCalls); + print(" input-rects="); printUInt64(surface.PresentRectInputs); print("\n"); + print(" present work: clipped="); printUInt64(surface.PresentClippedRects); + print(" empty="); printUInt64(surface.PresentEmptyClips); + print(" rows="); printUInt64(surface.PresentRowsCopied); + print(" bytes="); printUInt64(surface.PresentBytesCopied); print("\n"); + + print("Page tables:\n"); + print(" leaf mappings: "); printUInt64(pte.CurrentLeafMappings); print("\n"); + print(" page-table pages: "); printUInt64(pte.PageTablePagesAllocated); print("\n"); + print(" map/unmap: map="); printUInt64(pte.MapCalls); + print(" failed="); printUInt64(pte.MapFailures); + print(" unmap="); printUInt64(pte.UnmapCalls); + print(" failed="); printUInt64(pte.UnmapFailures); print("\n"); + print(" protect: calls="); printUInt64(pte.ProtectCalls); + print(" failed="); printUInt64(pte.ProtectFailures); print("\n"); + print(" query: total="); printUInt64(pte.QueryCalls); + print(" failed="); printUInt64(pte.QueryFailures); + print(" translate="); printUInt64(pte.TranslateCalls); print("\n"); + print(" generic: "); printUInt64(pte.QueryByPurpose[0]); + print(" failed="); printUInt64(pte.QueryFailuresByPurpose[0]); print("\n"); + print(" user-probe: "); printUInt64(pte.QueryByPurpose[1]); + print(" failed="); printUInt64(pte.QueryFailuresByPurpose[1]); print("\n"); + print(" heap: "); printUInt64(pte.QueryByPurpose[2]); + print(" failed="); printUInt64(pte.QueryFailuresByPurpose[2]); print("\n"); + print(" vad: "); printUInt64(pte.QueryByPurpose[3]); + print(" failed="); printUInt64(pte.QueryFailuresByPurpose[3]); print("\n"); + print(" debug: "); printUInt64(pte.QueryByPurpose[4]); + print(" failed="); printUInt64(pte.QueryFailuresByPurpose[4]); print("\n"); + print(" self-test: "); printUInt64(pte.QueryByPurpose[5]); + print(" failed="); printUInt64(pte.QueryFailuresByPurpose[5]); print("\n"); + print(" tlb: local="); printUInt64(pte.LocalFlushes); + print(" global="); printUInt64(pte.GlobalFlushes); + print(" new-map-skip="); printUInt64(pte.NewMappingNoFlushes); + print(" replace-flush="); printUInt64(pte.ReplacedMappingFlushes); print("\n"); + print(" shootdowns: requests="); printUInt64(pte.ShootdownRequests); + print(" targets="); printUInt64(pte.ShootdownTargetCpus); + print(" skipped="); printUInt64(pte.ShootdownSkippedNoTargets); + print(" acks="); printUInt64(pte.ShootdownAcks); + print(" timeouts="); printUInt64(pte.ShootdownTimeouts); print("\n"); } @@ -369,11 +1100,23 @@ static void cmd_mem(void) { } print("Memory Status:\n"); - print(" Free physical: "); + print(" Free physical: "); printSizeKiB(st.FreePhysicalBytes); - print("\n Kernel pool: "); + print("\n Reclaimable: "); + printSizeKiB(st.ReclaimablePhysicalBytes); + print("\n Available physical: "); + printSizeKiB(st.AvailablePhysicalBytes); + print("\n Pinned physical: "); + printSizeKiB(st.PinnedPhysicalBytes); + print("\n Non-evictable: "); + printSizeKiB(st.NonEvictablePhysicalBytes); + print("\n User frames: "); + printSizeKiB(st.UserPhysicalBytes); + print("\n Page tables: "); + printSizeKiB(st.PageTablePhysicalBytes); + print("\n Kernel pool: "); printSizeKiB(st.KernelPoolBytes); - print("\n User heap: "); + print("\n User heap: "); printSizeKiB(st.UserHeapBytes); print("\n"); } @@ -388,6 +1131,57 @@ static const char* processStateName(uint32_t state) { } } +// Names public thread states. +static const char* threadStateName(uint32_t state) { + switch (state) { + case SYS_THREAD_STATE_READY: return "ready"; + case SYS_THREAD_STATE_RUNNING: return "run"; + case SYS_THREAD_STATE_WAITING: return "wait"; + case SYS_THREAD_STATE_DEAD: return "dead"; + default: return "?"; + } +} + +// Names public wait object types. +static const char* threadWaitName(uint32_t waitType) { + switch (waitType) { + case SYS_THREAD_WAIT_NONE: return "-"; + case SYS_THREAD_WAIT_PROCESS: return "process"; + case SYS_THREAD_WAIT_THREAD: return "thread"; + case SYS_THREAD_WAIT_MUTEX: return "mutex"; + case SYS_THREAD_WAIT_SEMAPHORE: return "sem"; + case SYS_THREAD_WAIT_EVENT: return "event"; + case SYS_THREAD_WAIT_QUEUE: return "queue"; + case SYS_THREAD_WAIT_FILE: return "file"; + case SYS_THREAD_WAIT_DIR: return "dir"; + case SYS_THREAD_WAIT_ASYNC_IO: return "async"; + case SYS_THREAD_WAIT_SOCKET: return "socket"; + case SYS_THREAD_WAIT_DEVICE: return "device"; + case SYS_THREAD_WAIT_DRIVER: return "driver"; + case SYS_THREAD_WAIT_USB_DEVICE: return "usbdev"; + case SYS_THREAD_WAIT_USB_PIPE: return "usbpipe"; + case SYS_THREAD_WAIT_FILE_MAPPING: return "mapping"; + case SYS_THREAD_WAIT_SURFACE: return "surface"; + default: return "unknown"; + } +} + +// Prints thread status flags. +static void printThreadFlags(uint32_t flags) { + char buf[8]; + size_t n = 0; + if (flags & SYS_THREAD_FLAG_CURRENT) buf[n++] = '*'; + if (flags & SYS_THREAD_FLAG_IDLE) buf[n++] = 'I'; + if (flags & SYS_THREAD_FLAG_KERNEL) buf[n++] = 'K'; + if (flags & SYS_THREAD_FLAG_QUEUED) buf[n++] = 'Q'; + if (flags & SYS_THREAD_FLAG_ALERTABLE_WAIT) buf[n++] = 'A'; + if (flags & SYS_THREAD_FLAG_KERNEL_APC_PENDING) buf[n++] = 'k'; + if (flags & SYS_THREAD_FLAG_USER_APC_PENDING) buf[n++] = 'u'; + if (n == 0) buf[n++] = '-'; + buf[n] = '\0'; + printStringWidth(buf, 7); +} + // Implements the cmd ps operation. static void cmd_ps(void) { SYS_PROCESS_INFO procs[MAX_PROCS]; @@ -403,14 +1197,16 @@ static void cmd_ps(void) { printUInt((uint32_t)count); print("):\n"); print(" PID State Thr Hnd Mod UserKiB ImageKiB Flags Name\n"); + print(" Flags: S=system I=idle *=current T=terminating X=exited.\n"); size_t displayCount = count < MAX_PROCS ? count : MAX_PROCS; for (size_t i = 0; i < displayCount; i++) { SYS_PROCESS_INFO* p = &procs[i]; - char flags[5]; + char flags[6]; size_t flagLen = 0; if (p->Flags & SYS_PROCESS_FLAG_SYSTEM) flags[flagLen++] = 'S'; + if (p->Flags & SYS_PROCESS_FLAG_IDLE) flags[flagLen++] = 'I'; if (p->Flags & SYS_PROCESS_FLAG_CURRENT) flags[flagLen++] = '*'; if (p->Flags & SYS_PROCESS_FLAG_TERMINATING) flags[flagLen++] = 'T'; if (p->Flags & SYS_PROCESS_FLAG_EXITED) flags[flagLen++] = 'X'; @@ -449,6 +1245,86 @@ static void cmd_ps(void) { } } +// Implements the cmd threads operation. +static void cmd_threads(const char* args) { + uint32_t pid = 0; + if (!parseUInt(args, &pid)) { + print("threads: usage: threads \n"); + return; + } + + SYS_THREAD_INFO threads[MAX_THREADS]; + size_t count = 0; + uint32_t status = SysGetThreadList(pid, threads, MAX_THREADS, &count); + if (status != 0) { + print("threads: query failed status="); + printUInt(status); + print("\n"); + return; + } + + print("Threads for PID "); + printUInt(pid); + print(" (Count: "); + printUInt((uint32_t)count); + print("):\n"); + print(" TID State Pri CPU LastCPU QRem QTick Burst Wait WStat Flags\n"); + print(" Flags: *=current I=idle K=kernel Q=queued A=alertable k=kernel-apc u=user-apc.\n"); + + size_t displayCount = count < MAX_THREADS ? count : MAX_THREADS; + for (size_t i = 0; i < displayCount; i++) { + SYS_THREAD_INFO* t = &threads[i]; + print(" "); + printUIntWidth(t->ThreadId, 3); + print(" "); + printStringWidth(threadStateName(t->State), 5); + print(" "); + printUIntWidth(t->Priority, 3); + print(" "); + if (t->RunningCpu == 0xffffffffu) printStringWidth("-", 3); + else printUIntWidth(t->RunningCpu, 3); + print(" "); + if (t->LastCpu == 0xffffffffu) printStringWidth("-", 7); + else printUIntWidth(t->LastCpu, 7); + print(" "); + printUIntWidth(t->QuantumRemaining, 4); + print(" "); + printUIntWidth(t->QuantumTicks, 5); + print(" "); + printUIntWidth(t->CpuBurstTicks, 5); + print(" "); + printStringWidth(threadWaitName(t->WaitObjectType), 8); + print(" "); + printUIntWidth(t->WaitStatus, 5); + print(" "); + printThreadFlags(t->Flags); + print("\n"); + } + + if (count > MAX_THREADS) { + print(" ... and "); + printUInt((uint32_t)(count - MAX_THREADS)); + print(" more threads.\n"); + } +} + +static void cmd_kill(const char* args) { + uint32_t pid = 0; + if (!parseUInt(args, &pid) || pid == 0) { print("kill: usage: kill \n"); return; } + uint32_t status = SysKillProcess(pid, (int)STATUS_PROCESS_KILLED); + if (status != 0) { + print("shell: failed to kill process "); + printUInt(pid); + print(", error code: "); + printUInt(status); + print("\n"); + } else { + print("Process "); + printUInt(pid); + print(" terminated successfully.\n"); + } +} + // Implements the cmd write operation. static void cmd_write(const char* args) { char filename[MAX_CMD]; int i = 0; @@ -460,7 +1336,8 @@ static void cmd_write(const char* args) { uint32_t hFile; if (create_file(filename, &hFile) != 0) { print("write: could not create file\n"); return; } size_t written; - SysWriteFile(hFile, args + i + 1, strlen(args + i + 1), &written); + SYS_FILE_IO_REQUEST writeReq = SysMakeFileIoRequest(hFile, args + i + 1, strlen(args + i + 1), &written); + SysWriteFile(&writeReq); SysCloseHandle(hFile); print("File written successfully.\n"); } @@ -479,22 +1356,16 @@ static void cmd_dir(const char* path) { while (SysReadDir(hDir, &ke) == 0) { printDirTime(&ke.LastWriteTime); - // Corrected Attribute Check: 0x10 is the Directory bit in FAT. if (ke.Attr & 0x10) { print(" "); + print(ke.Name); } else { print(" "); - } - - print(ke.Name); - - // Optional: print file size for non-directories. - if (!(ke.Attr & 0x10)) { + print(ke.Name); print(" ("); - printUInt(ke.FileSize); + printUInt64(ke.FileSize); print(" bytes)"); } - print("\n"); } SysCloseHandle(hDir); @@ -505,7 +1376,9 @@ static void cmd_type(const char* filename) { uint32_t hFile; if (open_file(filename, &hFile) != 0) { print("type: cannot open file\n"); return; } char buffer[512]; size_t bytesRead; - while (SysReadFile(hFile, buffer, sizeof(buffer) - 1, &bytesRead) == 0 && bytesRead > 0) { + for (;;) { + SYS_FILE_IO_REQUEST readReq = SysMakeFileIoRequest(hFile, buffer, sizeof(buffer) - 1, &bytesRead); + if (SysReadFile(&readReq) != 0 || bytesRead == 0) break; buffer[bytesRead] = '\0'; print(buffer); } SysCloseHandle(hFile); print("\n"); @@ -533,6 +1406,72 @@ static bool extractProgramPath(const char* cmdline, char* out, size_t outSize) { return pos != 0; } +// Consumes queued Ctrl-C input without taking ordinary foreground stdin. +static bool shellConsumeQueuedCtrlC(void) { + for (int i = 0; i < 4; i++) { + INPUT_KEY_RECORD input; + if (SysPeekInputEvent(&input) != 0) return false; + + if ((input.Modifiers & INPUT_MOD_CTRL) && input.Key == INPUT_KEY_C) { + (void)SysReadInputEvent(&input); + return true; + } + + if (input.Key == INPUT_KEY_LEFT_CTRL || input.Key == INPUT_KEY_RIGHT_CTRL) { + (void)SysReadInputEvent(&input); + continue; + } + + return false; + } + + return false; +} + +// Waits for a foreground process while watching Ctrl-C without stealing ordinary stdin. +static uint32_t waitForegroundProcess(uint32_t hProc) { + uint32_t pid = 0; + bool havePid = SysGetProcessPid(hProc, &pid) == 0 && pid != 0; + bool sentBreak = false; + + uint32_t inputQueue = 0; + if (SysOpenInputQueue(0, &inputQueue) != 0) + return SysWait(hProc, SYS_WAIT_FLAG_NONE); + + uint32_t handles[2] = { hProc, inputQueue }; + uint32_t index = 0; + SYS_WAIT_MANY_REQUEST waitReq; + waitReq.Size = sizeof(waitReq); + waitReq.Flags = SYS_WAIT_FLAG_NONE; + waitReq.Handles = handles; + waitReq.Count = 2; + waitReq.SignaledIndexOut = &index; + waitReq.TimeoutMilliseconds = SYS_WAIT_TIMEOUT_INFINITE; + + for (;;) { + index = 0; + uint32_t status = SysWaitMany(&waitReq); + if (status != SYS_STATUS_SUCCESS) { + SysCloseHandle(inputQueue); + return status; + } + + if (index == 0) { + SysCloseHandle(inputQueue); + return status; + } + + uint32_t modifiers = SysGetKeyModifiers(); + if (((modifiers & INPUT_MOD_CTRL) && SysIsKeyDown(INPUT_KEY_C)) || shellConsumeQueuedCtrlC()) { + if (!sentBreak) { + print("^C\n"); + if (havePid) (void)SysKillProcess(pid, (int)STATUS_PROCESS_KILLED); + sentBreak = true; + } + } + } +} + // Run one command. static int runCommand(const char* cmd) { if (!cmd || *cmd == '\0') return 0; @@ -582,6 +1521,8 @@ static int runCommand(const char* cmd) { if (strcmp(cmd, "display") == 0 || strcmp(cmd, "gpu") == 0) { cmd_display(); return 0; } if (strcmp(cmd, "stats") == 0 || strcmp(cmd, "telemetry") == 0) { cmd_stats(); return 0; } if (strcmp(cmd, "ps") == 0) { cmd_ps(); return 0; } + if (strncmp(cmd, "threads ", 8) == 0) { cmd_threads(cmd + 8); return 0; } + if (strncmp(cmd, "kill ", 5) == 0) { cmd_kill(cmd + 5); return 0; } if (strcmp(cmd, "dir") == 0) { cmd_dir("."); return 0; } if (strncmp(cmd, "dir ", 4) == 0) { cmd_dir(cmd + 4); return 0; } if (strncmp(cmd, "cd ", 3) == 0) { if (set_cwd(cmd + 3) != 0) print("cd: failed\n"); return 0; } @@ -589,15 +1530,64 @@ static int runCommand(const char* cmd) { if (strncmp(cmd, "type ", 5) == 0) { cmd_type(cmd + 5); return 0; } if (strncmp(cmd, "write ", 6) == 0) { cmd_write(cmd + 6); return 0; } + // Make a local copy of the command line so we can mutate it safely if needed + char modifiedCmd[MAX_CMD]; + strncpy(modifiedCmd, cmd, sizeof(modifiedCmd) - 1); + modifiedCmd[sizeof(modifiedCmd) - 1] = '\0'; + + // Parse background indicator execution flag (&) + bool background = false; + size_t cmdLen = strlen(modifiedCmd); + if (cmdLen > 0 && modifiedCmd[cmdLen - 1] == '&') { + background = true; + modifiedCmd[cmdLen - 1] = '\0'; // Strip the ampersand + + // Clean up any trailing whitespace left before the ampersand + cmdLen = strlen(modifiedCmd); + while (cmdLen > 0 && (modifiedCmd[cmdLen - 1] == ' ' || modifiedCmd[cmdLen - 1] == '\t')) { + modifiedCmd[cmdLen - 1] = '\0'; + cmdLen--; + } + } + char program[MAX_CMD]; - if (!extractProgramPath(cmd, program, sizeof(program))) return 0; + if (!extractProgramPath(modifiedCmd, program, sizeof(program))) return 0; uint32_t hProc; - if (create_process(program, &hProc, cmd) == 0) { - SysWaitForProcess(hProc); - SysCloseHandle(hProc); - while (SysPollKeyboard()) (void)SysReadKeyboardChar(); - } else { print("shell: launch failed\n"); } + if (create_process(program, &hProc, modifiedCmd) == 0) { + if (background) { + // Background mode: Clean up the handle immediately and skip waiting. + print("[Launched in background, handle: "); + printUInt(hProc); + print("]\n"); + SysCloseHandle(hProc); + } else { + // Foreground mode: Wait for completion and flush standard keyboard stream. + uint32_t waitStatus = waitForegroundProcess(hProc); + if (waitStatus != SYS_STATUS_SUCCESS) { + print("shell: wait failed: "); + printUInt(waitStatus); + print("\n"); + } + print("Process exited with code: "); + int exitCode; + SysGetProcessExitCode(hProc, &exitCode); + printInt(exitCode); + const char* exitName = processExitStatusName((uint32_t)exitCode); + if (exitName) { + print(" (0x"); + printHex32((uint32_t)exitCode); + print(" "); + print(exitName); + print(")"); + } + print("\n"); + SysCloseHandle(hProc); + while (SysPollKeyboard()) (void)SysReadKey(); + } + } else { + print("shell: launch failed\n"); + } return 0; } @@ -611,9 +1601,11 @@ int main(void) { print("\n"); print("shell: ready\n"); while (true) { - print("> "); - readLine(lineBuf, MAX_CMD); - if (runCommand(trim(lineBuf))) break; + print(SHELL_PROMPT); + if (readLine(lineBuf, MAX_CMD)) continue; + char* cmd = trim(lineBuf); + shellHistoryAdd(cmd); + if (runCommand(cmd)) break; } print("shell: goodbye\n"); SysExitProcess(0); diff --git a/code/user/apps/shell_win32/shell_win32.c b/code/user/apps/shell_win32/shell_win32.c index 3f3c017..cdff8fa 100644 --- a/code/user/apps/shell_win32/shell_win32.c +++ b/code/user/apps/shell_win32/shell_win32.c @@ -147,6 +147,8 @@ static int runCommand(const char* cmd) { WaitForSingleObject(pi.hProcess, INFINITE); if (pi.hThread && pi.hThread != INVALID_HANDLE_VALUE) CloseHandle(pi.hThread); + if (pi.hProcess && pi.hProcess != INVALID_HANDLE_VALUE) + CloseHandle(pi.hProcess); FlushConsoleInputBuffer(hIn); } else { print("shell: launch failed\n"); diff --git a/code/user/apps/video/video.c b/code/user/apps/video/video.c index eb49abf..e91cba6 100644 --- a/code/user/apps/video/video.c +++ b/code/user/apps/video/video.c @@ -1,110 +1,231 @@ -// User application entry points for video. +// Native NTLite Bink video player. +// +// This application intentionally uses the native syscall ABI directly. Do not +// include here; Win32 compatibility belongs in the Win32 personality +// layer, not in native apps. -#include #include #include #include #include +#include #include "bink.h" -// Implements the command Line Second Token operation. -static const char* commandLineSecondToken(const char* cmdline, char* out, size_t outSize) { - if (!out || outSize == 0) return "video.bk2"; - strcpy(out, "video.bk2"); - if (!cmdline) return out; +#define VIDEO_DEFAULT_PATH "video.bk2" - while (*cmdline == ' ' || *cmdline == '\t') cmdline++; - if (*cmdline == '\0') return out; +static void print_span(const char* s, size_t len) { + if (!s || !len) return; + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromParts(s, len) }; + SysWriteConsole(&req); +} - if (*cmdline == '"') { - cmdline++; - while (*cmdline && *cmdline != '"') cmdline++; - if (*cmdline == '"') cmdline++; +static void print(const char* s) { + if (!s) return; + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { + print("0"); + return; + } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i) { + char c = buf[--i]; + print_span(&c, 1); + } +} + +static void fail_status(const char* what, uint32_t status) { + print("video: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + for (;;) SysExitProcess(1); +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static SYS_STRING_VIEW command_line_view(void) { + SYS_PEB* peb = SysGetPeb(); + if (!peb || peb->Magic != SYS_PEB_MAGIC) + return SYS_SV_NULL; + return peb->CommandLine; +} + +static int sv_is_space(char c) { + return c == ' ' || c == '\t'; +} + +static SYS_STRING_VIEW skip_one_token(SYS_STRING_VIEW s) { + size_t i = 0; + while (i < s.Length && sv_is_space(s.Data[i])) + i++; + + if (i < s.Length && s.Data[i] == '"') { + i++; + while (i < s.Length && s.Data[i] != '"') + i++; + if (i < s.Length && s.Data[i] == '"') + i++; } else { - while (*cmdline && *cmdline != ' ' && *cmdline != '\t') cmdline++; + while (i < s.Length && !sv_is_space(s.Data[i])) + i++; } - while (*cmdline == ' ' || *cmdline == '\t') cmdline++; - if (*cmdline == '\0') return out; + while (i < s.Length && sv_is_space(s.Data[i])) + i++; - size_t pos = 0; - if (*cmdline == '"') { - cmdline++; - while (*cmdline && *cmdline != '"' && pos + 1 < outSize) - out[pos++] = *cmdline++; + return SysSvFromParts(s.Data + i, s.Length - i); +} + +static SYS_STRING_VIEW command_line_second_token(SYS_STRING_VIEW cmdline) { + SYS_STRING_VIEW rest = skip_one_token(cmdline); + if (!rest.Data || rest.Length == 0) + return SYS_SV_LITERAL(VIDEO_DEFAULT_PATH); + + size_t begin = 0; + size_t end = 0; + if (rest.Data[0] == '"') { + begin = 1; + end = begin; + while (end < rest.Length && rest.Data[end] != '"') + end++; } else { - while (*cmdline && *cmdline != ' ' && *cmdline != '\t' && pos + 1 < outSize) - out[pos++] = *cmdline++; + begin = 0; + end = 0; + while (end < rest.Length && !sv_is_space(rest.Data[end])) + end++; } - out[pos] = '\0'; + + if (end <= begin) + return SYS_SV_LITERAL(VIDEO_DEFAULT_PATH); + + return SysSvFromParts(rest.Data + begin, end - begin); +} + +static const char* path_to_cstr(SYS_STRING_VIEW path, char* out, size_t outSize) { + if (!out || outSize == 0) + return VIDEO_DEFAULT_PATH; + + if (!path.Data || path.Length == 0) + path = SYS_SV_LITERAL(VIDEO_DEFAULT_PATH); + + size_t n = path.Length; + if (n + 1 > outSize) + n = outSize - 1; + + memcpy(out, path.Data, n); + out[n] = '\0'; return out; } -// Runs the requested operation. +static uint32_t open_file_view(SYS_STRING_VIEW path, uint32_t* handleOut) { + SYS_PATH_HANDLE_REQUEST req = { path, handleOut }; + return SysOpenFile(&req); +} + int main(void) { - const char* cmdline = GetCommandLineA(); - char videoPath[260]; - const char* path = commandLineSecondToken(cmdline, videoPath, sizeof(videoPath)); + SYS_STRING_VIEW cmdline = command_line_view(); + SYS_STRING_VIEW pathView = command_line_second_token(cmdline); - void* h = GetStdHandle(STD_OUTPUT_HANDLE); - WriteConsoleA(h, "video: starting\n", 16, 0, 0); - - WriteConsoleA(h, cmdline, (unsigned int)strlen(cmdline), 0, 0); - WriteConsoleA(h, "\n", 1, 0, 0); - - void* f = CreateFileA(path, 0, 0, 0, 3, 0, 0); - if (!f || f == (void*)(unsigned long long)0xFFFFFFFF) { // fuckass - WriteConsoleA(h, "video: failed to open input file\n", 33, 0, 0); - ExitProcess(1); + print("video: starting\n"); + if (cmdline.Data && cmdline.Length) { + print_span(cmdline.Data, cmdline.Length); + print("\n"); } - unsigned long s = GetFileSize(f, 0); - if (!s || s == 0xFFFFFFFF) { - WriteConsoleA(h, "video: GetFileSize failed!\n", 27, 0, 0); - ExitProcess(1); - } - void* buf = HeapAlloc(0, 0, s); - if (!buf) { - WriteConsoleA(h, "video: HeapAlloc failed\n", 24, 0, 0); - ExitProcess(1); + uint32_t file = 0; + uint32_t status = open_file_view(pathView, &file); + if (status != SYS_STATUS_SUCCESS) { + char pathBuf[260]; + print("video: failed to open input file: "); + print(path_to_cstr(pathView, pathBuf, sizeof(pathBuf))); + print("\n"); + fail_status("SysOpenFile", status); } - int r = ReadFile(f, buf, s, 0, 0); - if (!r) { - WriteConsoleA(h, "video: ReadFile failed\n", 23, 0, 0); - ExitProcess(1); + uint64_t fileSize = SysGetFileSize(file); + if (fileSize == 0) { + SysCloseHandle(file); + print("video: input file is empty\n"); + exit_now(1); } - WriteConsoleA(h, "video: input file loaded\n", 25, 0, 0); - - CloseHandle(h); - HBINK b = BinkOpen(buf, BINKFROMMEMORY); + uint32_t mapping = 0; + status = SysCreateFileMapping(file, SYS_VM_READ, SYS_MAP_FLAG_NONE, fileSize, &mapping); + if (status != SYS_STATUS_SUCCESS || !mapping) { + SysCloseHandle(file); + fail_status("SysCreateFileMapping", status); + } + + void* buf = 0; + status = SysMapFile(mapping, 0, (size_t)fileSize, + SYS_MAP_ACCESS_READ, SYS_MAP_FLAG_NONE, &buf); + if (status != SYS_STATUS_SUCCESS || !buf) { + SysCloseHandle(mapping); + SysCloseHandle(file); + fail_status("SysMapFile", status); + } + + // The mapped VAD owns the file reference now; keep only the mapped pointer + // alive while Bink is open. This avoids a second whole-file heap allocation + // and lets the VM fault file pages in on demand. + SysCloseHandle(mapping); + SysCloseHandle(file); + + print("video: input file mapped\n"); + + HBINK b = BinkOpen((const char*)buf, BINKFROMMEMORY); if (!b) { - WriteConsoleA(h, "video: BinkOpen failed: ", 24, 0, 0); + print("video: BinkOpen failed: "); const char* err = BinkGetError(); - WriteConsoleA(h, err, (unsigned int)strlen(err), 0, 0); - WriteConsoleA(h, "\n", 1, 0, 0); - ExitProcess(1); + print(err ? err : "unknown"); + print("\n"); + SysFreeVm(buf); + exit_now(1); } - uint32_t width = b->Width; + uint32_t width = b->Width; uint32_t height = b->Height; - - uint32_t* fb = HeapAlloc(0, 0, width * height * sizeof(uint32_t)); - if (!fb) { - WriteConsoleA(h, "video: framebuffer alloc failed\n", 32, 0, 0); - ExitProcess(1); + if (!width || !height) { + print("video: invalid video dimensions\n"); + BinkClose(b); + SysFreeVm(buf); + exit_now(1); } - // Prepare the Win32 BITMAPINFO structure. - BITMAPINFO bmi; - memset(&bmi, 0, sizeof(bmi)); - bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - bmi.bmiHeader.biWidth = width; - bmi.bmiHeader.biHeight = -(LONG)height; // Negative means top-down drawing! - bmi.bmiHeader.biPlanes = 1; - bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biCompression = BI_RGB; + uint32_t surface = 0; + status = SysCreateSurface(width, height, SYS_SURFACE_FORMAT_XRGB8888, + SYS_SURFACE_FLAG_NONE, &surface); + if (status != SYS_STATUS_SUCCESS || !surface) { + BinkClose(b); + SysFreeVm(buf); + fail_status("SysCreateSurface", status); + } + + void* surfaceBase = 0; + uint32_t pitchBytes = 0; + status = SysMapSurface(surface, &surfaceBase, &pitchBytes); + if (status != SYS_STATUS_SUCCESS || !surfaceBase || pitchBytes < width * sizeof(uint32_t)) { + SysCloseHandle(surface); + BinkClose(b); + SysFreeVm(buf); + fail_status("SysMapSurface", status); + } + + print("video: native surface ready\n"); + + DISPLAY_RECT fullFrame = { 0, 0, (int32_t)width, (int32_t)height }; while (1) { if (BinkWait(b)) { @@ -120,33 +241,26 @@ int main(void) { continue; } - BinkCopyToBuffer(b, fb, width * 4, height, 0, 0, - BINKSURFACE32); + BinkCopyToBuffer(b, surfaceBase, pitchBytes, height, 0, 0, BINKSURFACE32); - // Welcome to the GDI pipeline! - PAINTSTRUCT ps; - // Using NULL/0 as the HWND since we don't have a real window handle yet. - HDC hdc = BeginPaint((HWND)0, &ps); - - StretchDIBits( - hdc, - 0, 0, width, height, // Destination rect - 0, 0, width, height, // Source rect - fb, - &bmi, - DIB_RGB_COLORS, - SRCCOPY - ); - - EndPaint((HWND)0, &ps); + status = SysPresentSurface(surface, &fullFrame, 1); + if (status != SYS_STATUS_SUCCESS) { + SysCloseHandle(surface); + BinkClose(b); + SysFreeVm(buf); + fail_status("SysPresentSurface", status); + } if (b->FrameNum == b->Frames) break; BinkNextFrame(b); } - - WriteConsoleA(h, "video: goodbye\n", 15, 0, 0); - ExitProcess(0); + SysCloseHandle(surface); + BinkClose(b); + SysFreeVm(buf); + + print("video: goodbye\n"); + exit_now(0); } diff --git a/code/user/personalities/posix/posix.h b/code/user/personalities/posix/posix.h index c3cfdab..f1d772c 100644 --- a/code/user/personalities/posix/posix.h +++ b/code/user/personalities/posix/posix.h @@ -72,12 +72,27 @@ extern int errno; // defined in posix_fd.c; single global in v1 #define SEEK_CUR 1 #define SEEK_END 2 +// mmap() protection and mapping flags. +#define PROT_NONE 0x0 +#define PROT_READ 0x1 +#define PROT_WRITE 0x2 +#define PROT_EXEC 0x4 +#define MAP_SHARED 0x01 +#define MAP_PRIVATE 0x02 +#define MAP_FAILED ((void*)(intptr_t)-1) +#define MS_SYNC 0x04 +#define MS_ASYNC 0x01 +#define MS_INVALIDATE 0x02 + // Scalar types. typedef int32_t pid_t; typedef int32_t off_t; typedef uint32_t mode_t; typedef int64_t ssize_t; +__declspec(dllexport) void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset); +__declspec(dllexport) int munmap(void* addr, size_t length); +__declspec(dllexport) int msync(void* addr, size_t length, int flags); // Sockets. typedef uint16_t sa_family_t; @@ -188,7 +203,7 @@ __declspec(dllexport) int getaddrinfo(const char* node, const char* service, con __declspec(dllexport) void freeaddrinfo(struct addrinfo* res); // pthread types. -typedef struct { uint32_t Tid; } pthread_t; +typedef struct { uint32_t Handle; uint32_t Tid; } pthread_t; typedef struct { uint32_t Handle; } pthread_mutex_t; typedef void* pthread_attr_t; // unused in v1 typedef void* pthread_mutexattr_t; diff --git a/code/user/personalities/posix/posix_io.c b/code/user/personalities/posix/posix_io.c index 6579da5..38b1323 100644 --- a/code/user/personalities/posix/posix_io.c +++ b/code/user/personalities/posix/posix_io.c @@ -81,7 +81,8 @@ __declspec(dllexport) ssize_t read(int fd, void* buf, size_t count) { return (ssize_t)count; case FD_FILE: { size_t got = 0; - uint32_t s = SysReadFile(e->KHandle, buf, count, &got); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(e->KHandle, buf, count, &got); + uint32_t s = SysReadFile(&req); if (s != 0 && got == 0) { errno = PosixStatusToErrno(s); return -1; } return (ssize_t)got; } @@ -115,7 +116,8 @@ __declspec(dllexport) ssize_t write(int fd, const void* buf, size_t count) { return (ssize_t)count; case FD_FILE: { size_t written = 0; - uint32_t s = SysWriteFile(e->KHandle, buf, count, &written); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(e->KHandle, buf, count, &written); + uint32_t s = SysWriteFile(&req); if (s != 0 && written == 0) { errno = PosixStatusToErrno(s); return -1; } return (ssize_t)written; } diff --git a/code/user/personalities/posix/posix_mman.c b/code/user/personalities/posix/posix_mman.c new file mode 100644 index 0000000..562371a --- /dev/null +++ b/code/user/personalities/posix/posix_mman.c @@ -0,0 +1,85 @@ +// POSIX compatibility support for mmap-style file mappings. + +#include +#include +#include +#include "posix.h" +#include "posix_fd.h" + +static uint32_t PosixProtToSysMapAccess(int prot, int flags) { + uint32_t access = 0; + if (prot & PROT_EXEC) access |= SYS_MAP_ACCESS_EXECUTE; + if (prot & PROT_WRITE) { + access |= (flags & MAP_SHARED) ? SYS_MAP_ACCESS_WRITE : SYS_MAP_ACCESS_COPY; + } else if (flags & MAP_PRIVATE) { + access |= SYS_MAP_ACCESS_COPY; + } + if (prot & PROT_READ) access |= SYS_MAP_ACCESS_READ; + if (!access) access = SYS_MAP_ACCESS_READ; + return access; +} + +__declspec(dllexport) void* mmap(void* addr, size_t length, int prot, + int flags, int fd, off_t offset) { + if (addr || length == 0 || offset < 0 || ((uint64_t)offset & 0xFFFu)) { + errno = EINVAL; + return MAP_FAILED; + } + + if (!(flags & MAP_PRIVATE) && !(flags & MAP_SHARED)) { + errno = EINVAL; + return MAP_FAILED; + } + + uint32_t pid = SysGetCurrentPid(); + FD_ENTRY* e = FdGet(pid, fd); + if (!e) return MAP_FAILED; + if (e->Type != FD_FILE) { + errno = EBADF; + return MAP_FAILED; + } + + uint64_t maxSize = (uint64_t)offset + (uint64_t)length; + if (maxSize < (uint64_t)offset) { + errno = EINVAL; + return MAP_FAILED; + } + + uint32_t protect = (prot & PROT_WRITE) ? SYS_VM_READWRITE : SYS_VM_READ; + if (prot & PROT_EXEC) + protect = (prot & PROT_WRITE) ? SYS_VM_EXECUTE_READWRITE : SYS_VM_EXECUTE_READ; + + uint32_t mapping = 0; + uint32_t s = SysCreateFileMapping(e->KHandle, protect, SYS_MAP_FLAG_NONE, + maxSize, &mapping); + if (s != SYS_STATUS_SUCCESS) { + errno = PosixStatusToErrno(s); + return MAP_FAILED; + } + + void* base = (void*)0; + s = SysMapFile(mapping, (uint64_t)offset, length, + PosixProtToSysMapAccess(prot, flags), + SYS_MAP_FLAG_NONE, &base); + SysCloseHandle(mapping); + if (s != SYS_STATUS_SUCCESS) { + errno = PosixStatusToErrno(s); + return MAP_FAILED; + } + return base; +} + +__declspec(dllexport) int munmap(void* addr, size_t length) { + (void)length; + if (!addr) { errno = EINVAL; return -1; } + uint32_t s = SysFreeVm(addr); + if (s != SYS_STATUS_SUCCESS) { errno = PosixStatusToErrno(s); return -1; } + return 0; +} + +__declspec(dllexport) int msync(void* addr, size_t length, int flags) { + (void)addr; + (void)length; + (void)flags; + return 0; +} diff --git a/code/user/personalities/posix/posix_proc.c b/code/user/personalities/posix/posix_proc.c index 5c377fb..dec224a 100644 --- a/code/user/personalities/posix/posix_proc.c +++ b/code/user/personalities/posix/posix_proc.c @@ -126,7 +126,7 @@ __declspec(dllexport) pid_t waitpid(pid_t pid, int* status, int options) { uint32_t handle = ProcTableFind(pid); if (!handle) { errno = ESRCH; return -1; } - SysWaitForProcess(handle); + SysWait(handle, SYS_WAIT_FLAG_NONE); int exitCode = 0; SysGetProcessExitCode(handle, &exitCode); diff --git a/code/user/personalities/posix/posix_thread.c b/code/user/personalities/posix/posix_thread.c index 998e3a2..6334509 100644 --- a/code/user/personalities/posix/posix_thread.c +++ b/code/user/personalities/posix/posix_thread.c @@ -84,28 +84,41 @@ __declspec(dllexport) int pthread_create(pthread_t* t, const pthread_attr_t* att tr->Pid = pid; uint32_t tid = 0; - uint32_t s = SysCreateThread((uint64_t)PthreadTrampoline, - (uint64_t)tr, 1, &tid); + uint32_t handle = 0; + SYS_CREATE_THREAD_REQUEST req = { + (uintptr_t)PthreadTrampoline, + (uintptr_t)tr, + 1, + 0, + &handle, + &tid + }; + uint32_t s = SysCreateThread(&req); if (s != 0) { free(tr); return EAGAIN; } // The trampoline allocates the retval slot itself after it knows its real // TID, avoiding the creator/new-thread race. - if (t) t->Tid = tid; + if (t) { + t->Handle = handle; + t->Tid = tid; + } else { + SysCloseHandle(handle); + } return 0; } // Implements the pthread join operation. __declspec(dllexport) int pthread_join(pthread_t t, void** retval) { uint32_t pid = SysGetCurrentPid(); - uint32_t s = SysWaitThread(t.Tid); - // STATUS_NOT_FOUND (0x22) means already dead - treat as success. - if (s != 0 && s != 0x0022) return ESRCH; + uint32_t s = SysWait(t.Handle, SYS_WAIT_FLAG_NONE); + if (s != 0) return ESRCH; PTHREAD_SLOT* slot = PthreadSlotFind(pid, t.Tid); if (retval) *retval = slot ? slot->RetVal : (void*)0; if (slot) slot->Used = false; + SysCloseHandle(t.Handle); return 0; } @@ -138,7 +151,7 @@ __declspec(dllexport) int pthread_mutex_destroy(pthread_mutex_t* m) { // Implements the pthread mutex lock operation. __declspec(dllexport) int pthread_mutex_lock(pthread_mutex_t* m) { - uint32_t s = SysAcquireMutex(m->Handle); + uint32_t s = SysWait(m->Handle, SYS_WAIT_FLAG_NONE); return s == 0 ? 0 : EINVAL; } diff --git a/code/user/personalities/win32/gdi32.c b/code/user/personalities/win32/gdi32.c index 504fe96..fc73f7e 100644 --- a/code/user/personalities/win32/gdi32.c +++ b/code/user/personalities/win32/gdi32.c @@ -63,101 +63,16 @@ static int* srcX_map = NULL; static int currentDestCapacity = 0; static int currentXMapCapacity = 0; -__declspec(dllexport) int StretchDIBits( - HDC hdc, - int xDest, int yDest, int DestWidth, int DestHeight, +__declspec(dllexport) int StretchDIBits(HDC hdc, int xDest, int yDest, int DestWidth, int DestHeight, int xSrc, int ySrc, int SrcWidth, int SrcHeight, const void *lpBits, const BITMAPINFO *lpbmi, UINT iUsage, DWORD rop) { - (void)hdc; (void)lpbmi; (void)iUsage; (void)rop; + (void)hdc; (void)xDest; (void)yDest; (void)DestWidth; (void)DestHeight; + (void)xSrc; (void)ySrc; (void)SrcWidth; (void)SrcHeight; + (void)lpBits; (void)lpbmi; (void)iUsage; (void)rop; - // Safety check to prevent division by zero. - if (DestWidth <= 0 || DestHeight <= 0 || SrcWidth <= 0 || SrcHeight <= 0) { - return 0; - } - - // Handle trivial case (1:1 copy) to bypass allocation and scaling overhead. - if (DestWidth == SrcWidth && DestHeight == SrcHeight && xSrc == 0 && ySrc == 0) { - SysBlitScreen(xDest, yDest, DestWidth, DestHeight, (const uint32_t*)lpBits); - return DestHeight; - } - - const uint32_t* srcPixels = (const uint32_t*)lpBits; - - // Only reallocate if the new size is LARGER than what we already have. - int requiredPixels = DestWidth * DestHeight; - if (requiredPixels > currentDestCapacity) { - free(destBuffer); - destBuffer = (uint32_t*)malloc(requiredPixels * sizeof(uint32_t)); - currentDestCapacity = requiredPixels; - } - - if (DestWidth > currentXMapCapacity) { - free(srcX_map); - srcX_map = (int*)malloc(DestWidth * sizeof(int)); - currentXMapCapacity = DestWidth; - } - - for (int dx = 0; dx < DestWidth; ++dx) { - // Nearest neighbor coordinate mapping. - srcX_map[dx] = xSrc + (dx * SrcWidth) / DestWidth; - - // Clamp to safe bounds. - if (srcX_map[dx] < 0) srcX_map[dx] = 0; - if (srcX_map[dx] >= SrcWidth) srcX_map[dx] = SrcWidth - 1; - } - - // Process the image row by row. - for (int dy = 0; dy < DestHeight; ++dy) { - // Map destination Y to source Y. - int sy = ySrc + (dy * SrcHeight) / DestHeight; - if (sy < 0) sy = 0; - if (sy >= SrcHeight) sy = SrcHeight - 1; - - // Assumes stride == SrcWidth (tightly packed 32bpp). - const uint32_t* srcRow = srcPixels + (sy * SrcWidth); - uint32_t* dstRow = destBuffer + (dy * DestWidth); - - int dx = 0; - -#if defined(__SSE2__) || defined(_M_X64) - // Process 4 pixels (16 bytes) per loop iteration using SSE2. - for (; dx <= DestWidth - 4; dx += 4) { - // Gather 4 source pixels using our precomputed X map. - // Note: _mm_set_epi32 takes arguments in reverse order (3, 2, 1, 0). - __m128i pixels = _mm_set_epi32( - srcRow[srcX_map[dx + 3]], - srcRow[srcX_map[dx + 2]], - srcRow[srcX_map[dx + 1]], - srcRow[srcX_map[dx + 0]] - ); - // Issue a single 128-bit unaligned store. - _mm_storeu_si128((__m128i*)&dstRow[dx], pixels); - } -#elif defined(__ARM_NEON) || defined(__ARM_NEON__) || defined(_M_ARM64) - // Process 4 pixels per loop iteration using ARM NEON. - for (; dx <= DestWidth - 4; dx += 4) { - // Initialize a vector and populate lanes (avoids stack-memory hop). - uint32x4_t pixels = vdupq_n_u32(0); - pixels = vsetq_lane_u32(srcRow[srcX_map[dx + 0]], pixels, 0); - pixels = vsetq_lane_u32(srcRow[srcX_map[dx + 1]], pixels, 1); - pixels = vsetq_lane_u32(srcRow[srcX_map[dx + 2]], pixels, 2); - pixels = vsetq_lane_u32(srcRow[srcX_map[dx + 3]], pixels, 3); - - // Issue a single 128-bit store. - vst1q_u32(&dstRow[dx], pixels); - } -#endif - - // Epilogue: Handle remaining pixels if DestWidth isn't a multiple of 4. - for (; dx < DestWidth; ++dx) { - dstRow[dx] = srcRow[srcX_map[dx]]; - } - } - - // Direct mapping to kernel screen blit using the stretched buffer. - SysBlitScreen(xDest, yDest, DestWidth, DestHeight, destBuffer); - - return DestHeight; + // There is no implicit display blit ABI. Native graphics clients own a + // surface and present it explicitly. + return 0; } diff --git a/code/user/personalities/win32/kernel32.c b/code/user/personalities/win32/kernel32.c index 649472b..af89d31 100644 --- a/code/user/personalities/win32/kernel32.c +++ b/code/user/personalities/win32/kernel32.c @@ -27,6 +27,8 @@ #include #include +extern size_t _malloc_usable_size(const void* ptr); + // Types. typedef void* HANDLE; typedef unsigned long DWORD; @@ -58,6 +60,7 @@ typedef struct _CONSOLE_SCREEN_BUFFER_INFO { #define MEM_RELEASE 0x00008000UL #define MEM_FREE 0x00010000UL #define MEM_PRIVATE 0x00020000UL +#define MEM_MAPPED 0x00040000UL #define PAGE_NOACCESS 0x01UL #define PAGE_READONLY 0x02UL #define PAGE_READWRITE 0x04UL @@ -65,6 +68,13 @@ typedef struct _CONSOLE_SCREEN_BUFFER_INFO { #define PAGE_EXECUTE 0x10UL #define PAGE_EXECUTE_READ 0x20UL #define PAGE_EXECUTE_READWRITE 0x40UL +#define SEC_COMMIT 0x08000000UL + +#define FILE_MAP_COPY 0x00000001UL +#define FILE_MAP_WRITE 0x00000002UL +#define FILE_MAP_READ 0x00000004UL +#define FILE_MAP_EXECUTE 0x00000020UL +#define FILE_MAP_ALL_ACCESS 0x000F001FUL typedef struct _MEMORY_BASIC_INFORMATION { void* BaseAddress; @@ -90,6 +100,26 @@ static uint32_t WinProtectToSysVm(DWORD protect) { } } +static uint32_t WinFileMappingProtectToSysVm(DWORD protect) { + // Strip section attributes; the low byte carries PAGE_* protection. + DWORD pageProtect = protect & 0xFFu; + if (pageProtect == PAGE_WRITECOPY) + return SYS_VM_READ; // MapViewOfFile(FILE_MAP_COPY) asks for the private writable view. + return WinProtectToSysVm(pageProtect ? pageProtect : PAGE_READONLY); +} + +static uint32_t WinFileMapAccessToSys(DWORD desiredAccess) { + uint32_t access = 0; + if (desiredAccess & FILE_MAP_EXECUTE) access |= SYS_MAP_ACCESS_EXECUTE; + if (desiredAccess & FILE_MAP_COPY) access |= SYS_MAP_ACCESS_COPY; + if (desiredAccess & FILE_MAP_WRITE) access |= SYS_MAP_ACCESS_WRITE; + if (desiredAccess & FILE_MAP_READ) access |= SYS_MAP_ACCESS_READ; + if ((desiredAccess & FILE_MAP_ALL_ACCESS) == FILE_MAP_ALL_ACCESS) + access |= SYS_MAP_ACCESS_READ | SYS_MAP_ACCESS_COPY; + if (!access) access = SYS_MAP_ACCESS_READ; + return access; +} + // Implements the protect Vm To Win operation. static DWORD SysProtectVmToWin(uint32_t protect) { switch (protect) { @@ -153,24 +183,9 @@ typedef struct _WIN32_FIND_DATAA { static inline HANDLE KH_TO_HANDLE(uint32_t kh) { return (HANDLE)(uintptr_t)kh; } static inline uint32_t HANDLE_TO_KH(HANDLE h) { return (uint32_t)(uintptr_t)h; } -// Thread IDs are not kernel handles. Keep Win32 thread pseudo-handles out of -// the low numeric handle namespace so CloseHandle/WaitForSingleObject cannot -// accidentally operate on an unrelated file/process handle with the same value. -#define WIN32_THREAD_HANDLE_TAG ((uintptr_t)1ULL << 63) -#define WIN32_MUTEX_SENTINEL ((uintptr_t)0x4D555458UL) // 'MUTX' -#define WIN32_SEMA_SENTINEL ((uintptr_t)0x53454D41UL) // 'SEMA' -static inline HANDLE TID_TO_THREAD_HANDLE(uint32_t tid) { - return (HANDLE)(WIN32_THREAD_HANDLE_TAG | (uintptr_t)tid); -} -static inline BOOL IS_THREAD_HANDLE(HANDLE h) { - return h && h != INVALID_HANDLE_VALUE && (((uintptr_t)h & WIN32_THREAD_HANDLE_TAG) != 0); -} -static inline uint32_t THREAD_HANDLE_TO_TID(HANDLE h) { - return (uint32_t)((uintptr_t)h & 0xFFFFFFFFu); -} -static inline BOOL IS_NOOP_SYNC_HANDLE(HANDLE h) { +static inline BOOL IS_VALID_KERNEL_HANDLE(HANDLE h) { uintptr_t v = (uintptr_t)h; - return v == WIN32_MUTEX_SENTINEL || v == WIN32_SEMA_SENTINEL; + return v != 0 && v != (uintptr_t)INVALID_HANDLE_VALUE; } // Path translator. @@ -268,11 +283,14 @@ static void K32DebugPrintA(const char* s) { } #define WIN32_TLS_SLOTS 64 +#define FLS_MAX_SLOTS 64 +#define FLS_OUT_OF_INDEXES 0xFFFFFFFFUL typedef struct WIN32_THREAD_ENV { DWORD LastError; DWORD Reserved0; void* TlsSlots[WIN32_TLS_SLOTS]; + void* FlsSlots[FLS_MAX_SLOTS]; } WIN32_THREAD_ENV; static WIN32_THREAD_ENV gFallbackThreadEnv; @@ -418,11 +436,27 @@ __declspec(dllexport) uint64_t GetTickCount64(void) { } // Memory (heap argument is ignored - single user-mode heap). +#define HEAP_ZERO_MEMORY 0x00000008UL +#define HEAP_REALLOC_IN_PLACE_ONLY 0x00000010UL + __declspec(dllexport) void* HeapAlloc(void* heap, DWORD flags, size_t size) { - (void)heap; (void)flags; + (void)heap; if (!size) return NULL; + void* ptr = malloc(size); - if (!ptr) gLastError = 8; // ERROR_NOT_ENOUGH_MEMORY + if (!ptr) { + gLastError = 8; // ERROR_NOT_ENOUGH_MEMORY + return NULL; + } + + // Bink and the Microsoft CRT routinely allocate internal state with + // HEAP_ZERO_MEMORY. Ignoring this flag only works accidentally while the + // heap keeps receiving fresh demand-zero pages; under low memory / swap the + // allocator reuses dirty freed blocks and stale pitch/pointer fields can + // corrupt the decoder. + if (flags & HEAP_ZERO_MEMORY) + memset(ptr, 0, size); + return ptr; } @@ -436,9 +470,28 @@ __declspec(dllexport) BOOL HeapFree(void* heap, DWORD flags, void* ptr) { __declspec(dllexport) void* HeapReAlloc(void* heap, DWORD flags, void* ptr, size_t size) { - (void)heap; (void)flags; + (void)heap; + + if (flags & HEAP_REALLOC_IN_PLACE_ONLY) { + // The user heap does not expose an in-place grow primitive. Preserve + // Windows semantics by failing instead of silently moving the block. + size_t oldSize = _malloc_usable_size(ptr); + if (oldSize != (size_t)-1 && oldSize >= size) + return ptr; + gLastError = 8; // ERROR_NOT_ENOUGH_MEMORY + return NULL; + } + + size_t oldSize = ptr ? _malloc_usable_size(ptr) : 0; void* newptr = realloc(ptr, size); - if (!newptr && size) gLastError = 8; + if (!newptr && size) { + gLastError = 8; + return NULL; + } + + if (newptr && (flags & HEAP_ZERO_MEMORY) && oldSize != (size_t)-1 && size > oldSize) + memset((unsigned char*)newptr + oldSize, 0, size - oldSize); + return newptr; } @@ -508,10 +561,88 @@ __declspec(dllexport) size_t VirtualQuery(const void* lpAddress, lpBuffer->RegionSize = info.PageCount * 4096u; lpBuffer->State = MEM_COMMIT; lpBuffer->Protect = protect; - lpBuffer->Type = MEM_PRIVATE; + lpBuffer->Type = (info.Type == 7u) ? MEM_MAPPED : MEM_PRIVATE; // 7 == MmVadMappedFile return sizeof(MEMORY_BASIC_INFORMATION); } +__declspec(dllexport) HANDLE CreateFileMappingA(HANDLE hFile, + void* lpFileMappingAttributes, + DWORD flProtect, + DWORD dwMaximumSizeHigh, + DWORD dwMaximumSizeLow, + const char* lpName) { + (void)lpFileMappingAttributes; + (void)lpName; + if (!hFile || hFile == INVALID_HANDLE_VALUE) { + gLastError = 6; // ERROR_INVALID_HANDLE + return NULL; + } + + uint64_t maximumSize = ((uint64_t)dwMaximumSizeHigh << 32) | (uint64_t)dwMaximumSizeLow; + uint32_t kh = 0; + uint32_t s = SysCreateFileMapping(HANDLE_TO_KH(hFile), + WinFileMappingProtectToSysVm(flProtect), + SYS_MAP_FLAG_NONE, + maximumSize, + &kh); + if (s != SYS_STATUS_SUCCESS) { + gLastError = (s == SYS_STATUS_NOT_SUPPORTED) ? 120 : ((s == SYS_STATUS_NO_MEMORY) ? 8 : 87); + return NULL; + } + return KH_TO_HANDLE(kh); +} + +__declspec(dllexport) HANDLE CreateFileMappingW(HANDLE hFile, + void* lpFileMappingAttributes, + DWORD flProtect, + DWORD dwMaximumSizeHigh, + DWORD dwMaximumSizeLow, + const unsigned short* lpName) { + (void)lpName; + return CreateFileMappingA(hFile, lpFileMappingAttributes, flProtect, + dwMaximumSizeHigh, dwMaximumSizeLow, NULL); +} + +__declspec(dllexport) void* MapViewOfFile(HANDLE hFileMappingObject, + DWORD dwDesiredAccess, + DWORD dwFileOffsetHigh, + DWORD dwFileOffsetLow, + size_t dwNumberOfBytesToMap) { + if (!hFileMappingObject || hFileMappingObject == INVALID_HANDLE_VALUE) { + gLastError = 6; + return NULL; + } + + uint64_t offset = ((uint64_t)dwFileOffsetHigh << 32) | (uint64_t)dwFileOffsetLow; + void* base = NULL; + uint32_t s = SysMapFile(HANDLE_TO_KH(hFileMappingObject), + offset, + dwNumberOfBytesToMap, + WinFileMapAccessToSys(dwDesiredAccess), + SYS_MAP_FLAG_NONE, + &base); + if (s != SYS_STATUS_SUCCESS) { + gLastError = (s == SYS_STATUS_NOT_SUPPORTED) ? 120 : ((s == SYS_STATUS_NO_MEMORY) ? 8 : 87); + return NULL; + } + return base; +} + +__declspec(dllexport) BOOL UnmapViewOfFile(const void* lpBaseAddress) { + if (!lpBaseAddress) { gLastError = 87; return FALSE; } + uint32_t s = SysFreeVm((void*)lpBaseAddress); + if (s != SYS_STATUS_SUCCESS) { gLastError = 87; return FALSE; } + return TRUE; +} + +__declspec(dllexport) BOOL FlushViewOfFile(const void* lpBaseAddress, size_t dwNumberOfBytesToFlush) { + (void)dwNumberOfBytesToFlush; + if (!lpBaseAddress) { gLastError = 87; return FALSE; } + // Native flush is file-handle based for now; unmap/close also harvests dirty + // PTEs, so FlushViewOfFile remains a successful cache-coherency hint. + return TRUE; +} + // --- Win32 API ---. // GetAsyncKeyState implementation over the canonical input key namespace. @@ -720,7 +851,7 @@ __declspec(dllexport) HANDLE CreateFileA(const char* name, // GetFileSize - fills optional high-DWORD pointer, returns low DWORD. // Returns 0xFFFFFFFF on error, matching Win32 behaviour. __declspec(dllexport) DWORD GetFileSize(HANDLE hFile, DWORD* fileSizeHigh) { - if (!hFile || hFile == INVALID_HANDLE_VALUE || IS_THREAD_HANDLE(hFile)) { gLastError = 6; return 0xFFFFFFFFUL; } + if (!hFile || hFile == INVALID_HANDLE_VALUE) { gLastError = 6; return 0xFFFFFFFFUL; } uint64_t sz = SysGetFileSize(HANDLE_TO_KH(hFile)); if (fileSizeHigh) *fileSizeHigh = (DWORD)(sz >> 32); return (DWORD)(sz & 0xFFFFFFFFULL); @@ -731,25 +862,27 @@ __declspec(dllexport) BOOL ReadFile(HANDLE hFile, void* buf, DWORD toRead, DWORD* bytesRead, void* overlapped) { (void)overlapped; if (bytesRead) *bytesRead = 0; - if (!hFile || hFile == INVALID_HANDLE_VALUE || IS_THREAD_HANDLE(hFile) || !buf) { + if (!hFile || hFile == INVALID_HANDLE_VALUE || !buf) { gLastError = 6; return FALSE; } size_t got = 0; - uint32_t s = SysReadFile(HANDLE_TO_KH(hFile), buf, (size_t)toRead, &got); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(HANDLE_TO_KH(hFile), buf, (size_t)toRead, &got); + uint32_t s = SysReadFile(&req); if (bytesRead) *bytesRead = (DWORD)got; - // Harden callers that accidentally look past *bytesRead. EOF/short reads - // are legal for files, but consuming stale stack/heap bytes after them makes - // the failure mode look like random game-data corruption. - if (got < (size_t)toRead) - memset((unsigned char*)buf + got, 0, (size_t)toRead - got); - if (s != 0) { gLastError = 1; return FALSE; } + + // Harden callers that accidentally look past *bytesRead. EOF/short reads + // are legal for files, but do not touch the caller's remaining buffer after + // a failed read because that can fault in megabytes under low memory. + if (got < (size_t)toRead) + memset((unsigned char*)buf + got, 0, (size_t)toRead - got); + return TRUE; } @@ -758,9 +891,10 @@ __declspec(dllexport) BOOL ReadFile(HANDLE hFile, void* buf, DWORD toRead, __declspec(dllexport) BOOL WriteFile(HANDLE hFile, const void* buf, DWORD toWrite, DWORD* bytesWritten, void* overlapped) { (void)overlapped; - if (!hFile || hFile == INVALID_HANDLE_VALUE || IS_THREAD_HANDLE(hFile) || !buf) { gLastError = 6; return FALSE; } + if (!hFile || hFile == INVALID_HANDLE_VALUE || !buf) { gLastError = 6; return FALSE; } size_t written = 0; - uint32_t s = SysWriteFile(HANDLE_TO_KH(hFile), buf, (size_t)toWrite, &written); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(HANDLE_TO_KH(hFile), buf, (size_t)toWrite, &written); + uint32_t s = SysWriteFile(&req); if (bytesWritten) *bytesWritten = (DWORD)written; if (s != 0) { gLastError = 29; return FALSE; } // ERROR_WRITE_FAULT return TRUE; @@ -770,7 +904,7 @@ __declspec(dllexport) BOOL WriteFile(HANDLE hFile, const void* buf, DWORD toWrit // Returns new position low DWORD, or 0xFFFFFFFF on error. __declspec(dllexport) DWORD SetFilePointer(HANDLE hFile, long dist, long* distHigh, DWORD method) { - if (!hFile || hFile == INVALID_HANDLE_VALUE || IS_THREAD_HANDLE(hFile)) { + if (!hFile || hFile == INVALID_HANDLE_VALUE) { gLastError = 6; return 0xFFFFFFFFUL; } @@ -813,7 +947,7 @@ __declspec(dllexport) DWORD SetFilePointer(HANDLE hFile, long dist, // SetEndOfFile - truncate or extend the file to the current seek position. __declspec(dllexport) BOOL SetEndOfFile(HANDLE hFile) { - if (!hFile || hFile == INVALID_HANDLE_VALUE || IS_THREAD_HANDLE(hFile)) { gLastError = 6; return FALSE; } + if (!hFile || hFile == INVALID_HANDLE_VALUE) { gLastError = 6; return FALSE; } uint32_t kh = HANDLE_TO_KH(hFile); uint64_t pos = SysGetFilePos(kh); if (pos == (uint64_t)-1) { gLastError = 6; return FALSE; } @@ -834,14 +968,10 @@ __declspec(dllexport) BOOL DeleteFileA(const char* lpFileName) { return TRUE; } -// CloseHandle - unified close for files, mutexes, semaphores, events, queues. +// CloseHandle - unified close for files, threads, mutexes, semaphores, events, queues, and processes. __declspec(dllexport) BOOL CloseHandle(HANDLE hObject) { if (!hObject || hObject == INVALID_HANDLE_VALUE) { gLastError = 6; return FALSE; } - // Our thread/sentinel sync handles are not entries in the kernel handle table. - // There is nothing to close; the thread object is reaped by the scheduler. - if (IS_THREAD_HANDLE(hObject) || IS_NOOP_SYNC_HANDLE(hObject)) return TRUE; - SysCloseHandle(HANDLE_TO_KH(hObject)); return TRUE; } @@ -870,8 +1000,11 @@ __declspec(dllexport) BOOL HeapDestroy(HANDLE hHeap) { // Implements the heap Size operation. __declspec(dllexport) size_t HeapSize(HANDLE heap, DWORD flags, const void* ptr) { - (void)heap; (void)flags; (void)ptr; - return (size_t)-1; // Win32 returns (SIZE_T)-1 on error / unsupported + (void)heap; (void)flags; + size_t size = _malloc_usable_size(ptr); + if (size == (size_t)-1) + gLastError = 87; // ERROR_INVALID_PARAMETER + return size; } __declspec(dllexport) BOOL HeapSetInformation(HANDLE HeapHandle, @@ -1050,9 +1183,29 @@ __declspec(dllexport) DWORD GetModuleFileNameA(HANDLE hModule, // Implements the terminate Process operation. __declspec(dllexport) BOOL TerminateProcess(HANDLE hProcess, unsigned int uExitCode) { - (void)hProcess; - SysExitProcess((int)uExitCode); - while (true) ; + if (hProcess == GetCurrentProcess()) { + SysExitProcess((int)uExitCode); + while (true) ; + } + + if (!hProcess) { + gLastError = 6; // ERROR_INVALID_HANDLE + return FALSE; + } + + uint32_t pid = 0; + uint32_t s = SysGetProcessPid(HANDLE_TO_KH(hProcess), &pid); + if (s != 0 || pid == 0) { + gLastError = 6; // ERROR_INVALID_HANDLE + return FALSE; + } + + s = SysKillProcess(pid, (int)uExitCode); + if (s != 0) { + gLastError = 5; // ERROR_ACCESS_DENIED / generic failure for now + return FALSE; + } + return TRUE; } // STARTUPINFOA - only cb matters for our stub. @@ -1099,35 +1252,42 @@ __declspec(dllexport) HANDLE CreateThread(void* lpThreadAttributes, DWORD* lpThreadId) { (void)lpThreadAttributes; (void)dwStackSize; (void)dwCreationFlags; uint32_t tid = 0; - uint32_t s = SysCreateThread((uint64_t)(uintptr_t)lpStartAddress, - (uint64_t)(uintptr_t)lpParameter, - 0, &tid); + uint32_t kh = 0; + SYS_CREATE_THREAD_REQUEST req = { + (uintptr_t)lpStartAddress, + (uintptr_t)lpParameter, + 0, + 0, + &kh, + &tid + }; + uint32_t s = SysCreateThread(&req); if (s != 0) { gLastError = 11; return INVALID_HANDLE_VALUE; } if (lpThreadId) *lpThreadId = (DWORD)tid; - return TID_TO_THREAD_HANDLE(tid); + return KH_TO_HANDLE(kh); } -// WaitForSingleObject - blocks on process handles via SysWaitForProcess; -// thread and other handle types are not yet waitable so return immediately. +// WaitForSingleObject - blocks on native waitable handles. A zero timeout is +// only a probe for mutexes/semaphores until native timeout waits exist. __declspec(dllexport) DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds) { - (void)dwMilliseconds; - if (!hHandle || hHandle == INVALID_HANDLE_VALUE) { gLastError = 6; return 0xFFFFFFFFUL; } - - if (IS_THREAD_HANDLE(hHandle)) { - uint32_t tid = THREAD_HANDLE_TO_TID(hHandle); - uint32_t s = SysWaitThread(tid); - if (s != 0 && s != 0x0022) { gLastError = 6; return 0xFFFFFFFFUL; } - return WAIT_OBJECT_0; - } - - if (IS_NOOP_SYNC_HANDLE(hHandle)) return WAIT_OBJECT_0; + if (!IS_VALID_KERNEL_HANDLE(hHandle)) { gLastError = 6; return 0xFFFFFFFFUL; } uint32_t kh = HANDLE_TO_KH(hHandle); - if (kh) { - uint32_t s = SysWaitForProcess(kh); - if (s != 0) { gLastError = 6; return 0xFFFFFFFFUL; } + if (dwMilliseconds == 0) { + uint32_t s = SysTryAcquireMutex(kh); + if (s == 0) return WAIT_OBJECT_0; + + s = SysTryWaitSem(kh); + if (s == 0) return WAIT_OBJECT_0; + + return WAIT_TIMEOUT; } - return WAIT_OBJECT_0; + + uint32_t s = SysWait(kh, SYS_WAIT_FLAG_NONE); + if (s == 0) return WAIT_OBJECT_0; + + gLastError = 6; + return 0xFFFFFFFFUL; } @@ -1250,7 +1410,7 @@ __declspec(dllexport) BOOL CreateProcessA( __declspec(dllexport) BOOL GetExitCodeProcess(HANDLE hProcess, DWORD* lpExitCode) { if (!lpExitCode) { gLastError = 87; return FALSE; } uint32_t kh = HANDLE_TO_KH(hProcess); - if (!kh || IS_THREAD_HANDLE(hProcess)) { gLastError = 6; return FALSE; } // ERROR_INVALID_HANDLE + if (!kh) { gLastError = 6; return FALSE; } // ERROR_INVALID_HANDLE int code = 0; uint32_t s = SysGetProcessExitCode(kh, &code); if (s != 0) { gLastError = 6; return FALSE; } @@ -1791,57 +1951,123 @@ __declspec(dllexport) DWORD GetCurrentDirectoryA(DWORD nBufferLength, char* lpBu __declspec(dllexport) HANDLE CreateMutexA(void* lpMutexAttributes, BOOL bInitialOwner, const char* lpName) { - (void)lpMutexAttributes; (void)bInitialOwner; (void)lpName; - return (HANDLE)WIN32_MUTEX_SENTINEL; + (void)lpMutexAttributes; (void)lpName; + uint32_t kh = 0; + uint32_t s = SysCreateMutex(&kh); + if (s != 0 || !kh) { gLastError = 8; return NULL; } + if (bInitialOwner) { + s = SysWait(kh, SYS_WAIT_FLAG_NONE); + if (s != 0) { + SysCloseHandle(kh); + gLastError = 1; + return NULL; + } + } + return KH_TO_HANDLE(kh); } // Releases mutex. __declspec(dllexport) BOOL ReleaseMutex(HANDLE hMutex) { - (void)hMutex; + if (!IS_VALID_KERNEL_HANDLE(hMutex)) { gLastError = 6; return FALSE; } + uint32_t s = SysReleaseMutex(HANDLE_TO_KH(hMutex)); + if (s != 0) { gLastError = 6; return FALSE; } return TRUE; } __declspec(dllexport) HANDLE CreateSemaphoreA(void* lpSemaphoreAttributes, long lInitialCount, long lMaximumCount, const char* lpName) { - (void)lpSemaphoreAttributes; (void)lInitialCount; - (void)lMaximumCount; (void)lpName; - return (HANDLE)WIN32_SEMA_SENTINEL; + (void)lpSemaphoreAttributes; (void)lpName; + uint32_t kh = 0; + uint32_t s = SysCreateSem((int32_t)lInitialCount, (int32_t)lMaximumCount, &kh); + if (s != 0 || !kh) { gLastError = 8; return NULL; } + return KH_TO_HANDLE(kh); } __declspec(dllexport) BOOL ReleaseSemaphore(HANDLE hSemaphore, long lReleaseCount, long* lpPreviousCount) { - (void)hSemaphore; (void)lReleaseCount; + if (!IS_VALID_KERNEL_HANDLE(hSemaphore) || lReleaseCount < 0) { gLastError = 6; return FALSE; } if (lpPreviousCount) *lpPreviousCount = 0; + for (long i = 0; i < lReleaseCount; ++i) { + uint32_t s = SysSignalSem(HANDLE_TO_KH(hSemaphore)); + if (s != 0) { gLastError = 6; return FALSE; } + } return TRUE; } -// Critical Sections & Condition Variables -// Single-threaded: all locks are no-ops. - -// Minimal opaque types; callers allocate these from their Windows headers - -// we just need the pointer to be accepted by our stubs. +// Critical Sections & Condition Variables. +// Match the size of Win64 RTL_CRITICAL_SECTION (5 pointer-sized fields) while +// using the storage as a small recursive spin lock. Bink uses these from its +// worker/copy paths, so the old no-op implementation allowed internal state +// races as soon as CreateThread started working. typedef struct { uintptr_t _opaque[5]; } CRITICAL_SECTION; typedef struct { void* _ptr; } CONDITION_VARIABLE; +#define K32_CS_LOCK 0 +#define K32_CS_OWNER 1 +#define K32_CS_RECURSION 2 + +static volatile long* K32CsLockWord(CRITICAL_SECTION* cs) { + return (volatile long*)&cs->_opaque[K32_CS_LOCK]; +} + +static DWORD K32CurrentTidOrOne(void) { + DWORD tid = (DWORD)SysGetCurrentTid(); + return tid ? tid : 1; +} + // Initializes critical Section. __declspec(dllexport) void InitializeCriticalSection(CRITICAL_SECTION* lpCS) { - (void)lpCS; + if (!lpCS) return; + memset(lpCS, 0, sizeof(*lpCS)); } __declspec(dllexport) BOOL InitializeCriticalSectionAndSpinCount(CRITICAL_SECTION* lpCS, DWORD dwSpinCount) { - (void)lpCS; (void)dwSpinCount; + (void)dwSpinCount; + InitializeCriticalSection(lpCS); return TRUE; } // Releases critical Section. __declspec(dllexport) void DeleteCriticalSection(CRITICAL_SECTION* lpCS) { (void)lpCS; } // Implements the enter Critical Section operation. -__declspec(dllexport) void EnterCriticalSection(CRITICAL_SECTION* lpCS) { (void)lpCS; } +__declspec(dllexport) void EnterCriticalSection(CRITICAL_SECTION* lpCS) { + if (!lpCS) return; + DWORD tid = K32CurrentTidOrOne(); + if ((DWORD)lpCS->_opaque[K32_CS_OWNER] == tid) { + lpCS->_opaque[K32_CS_RECURSION]++; + return; + } + while (_InterlockedCompareExchange(K32CsLockWord(lpCS), 1, 0) != 0) + _mm_pause(); + lpCS->_opaque[K32_CS_OWNER] = tid; + lpCS->_opaque[K32_CS_RECURSION] = 1; +} // Implements the leave Critical Section operation. -__declspec(dllexport) void LeaveCriticalSection(CRITICAL_SECTION* lpCS) { (void)lpCS; } +__declspec(dllexport) void LeaveCriticalSection(CRITICAL_SECTION* lpCS) { + if (!lpCS) return; + DWORD tid = K32CurrentTidOrOne(); + if ((DWORD)lpCS->_opaque[K32_CS_OWNER] != tid || lpCS->_opaque[K32_CS_RECURSION] == 0) + return; + lpCS->_opaque[K32_CS_RECURSION]--; + if (lpCS->_opaque[K32_CS_RECURSION] == 0) { + lpCS->_opaque[K32_CS_OWNER] = 0; + _ReadWriteBarrier(); + _InterlockedExchange(K32CsLockWord(lpCS), 0); + } +} // Implements the try Enter Critical Section operation. __declspec(dllexport) BOOL TryEnterCriticalSection(CRITICAL_SECTION* lpCS) { - (void)lpCS; return TRUE; + if (!lpCS) return FALSE; + DWORD tid = K32CurrentTidOrOne(); + if ((DWORD)lpCS->_opaque[K32_CS_OWNER] == tid) { + lpCS->_opaque[K32_CS_RECURSION]++; + return TRUE; + } + if (_InterlockedCompareExchange(K32CsLockWord(lpCS), 1, 0) != 0) + return FALSE; + lpCS->_opaque[K32_CS_OWNER] = tid; + lpCS->_opaque[K32_CS_RECURSION] = 1; + return TRUE; } // Initializes condition Variable. @@ -2357,14 +2583,12 @@ __declspec(dllexport) int GetLocaleInfoA(unsigned int Locale, DWORD LCType, return 0; } -// Fiber Local Storage (FLS) -// We implement a minimal 64-slot static table so callers that use FLS for -// thread-local-ish state don't immediately fail. +// Fiber Local Storage (FLS). +// Allocation state is process-global, but the values themselves are per-thread, +// matching the contract that Bink and the CRT expect when worker threads are +// enabled. The old single global value array let unrelated threads overwrite +// each other's decoder-local state. -#define FLS_MAX_SLOTS 64 -#define FLS_OUT_OF_INDEXES 0xFFFFFFFFUL - -static void* gFlsSlots[FLS_MAX_SLOTS]; static int gFlsUsed[FLS_MAX_SLOTS]; // 0 = free, 1 = allocated // Implements the fls Alloc operation. @@ -2372,8 +2596,8 @@ __declspec(dllexport) DWORD FlsAlloc(void* lpCallback) { (void)lpCallback; for (int i = 0; i < FLS_MAX_SLOTS; i++) { if (!gFlsUsed[i]) { - gFlsUsed[i] = 1; - gFlsSlots[i] = NULL; + gFlsUsed[i] = 1; + K32ThreadEnv()->FlsSlots[i] = NULL; return (DWORD)i; } } @@ -2387,7 +2611,7 @@ __declspec(dllexport) void* FlsGetValue(DWORD dwFlsIndex) { gLastError = 87; // ERROR_INVALID_PARAMETER return NULL; } - return gFlsSlots[dwFlsIndex]; + return K32ThreadEnv()->FlsSlots[dwFlsIndex]; } // Implements the fls Set Value operation. @@ -2396,7 +2620,7 @@ __declspec(dllexport) BOOL FlsSetValue(DWORD dwFlsIndex, void* lpFlsData) { gLastError = 87; return FALSE; } - gFlsSlots[dwFlsIndex] = lpFlsData; + K32ThreadEnv()->FlsSlots[dwFlsIndex] = lpFlsData; return TRUE; } @@ -2406,8 +2630,8 @@ __declspec(dllexport) BOOL FlsFree(DWORD dwFlsIndex) { gLastError = 87; return FALSE; } - gFlsUsed[dwFlsIndex] = 0; - gFlsSlots[dwFlsIndex] = NULL; + gFlsUsed[dwFlsIndex] = 0; + K32ThreadEnv()->FlsSlots[dwFlsIndex] = NULL; return TRUE; } diff --git a/code/user/runtime/heap.c b/code/user/runtime/heap.c index 629a180..7b1d323 100644 --- a/code/user/runtime/heap.c +++ b/code/user/runtime/heap.c @@ -210,6 +210,29 @@ void* _calloc(size_t count, size_t size) { return ptr; } + +// Returns the usable payload size of an allocation made by this heap. +size_t _malloc_usable_size(const void* ptr) { + if (!ptr) return (size_t)-1; + + HeapLock(); + HEAP_SEGMENT* seg = SegmentForPointer((void*)ptr); + if (!seg) { + HeapUnlock(); + return (size_t)-1; + } + + HEAP_BLOCK* block = PayloadBlock((void*)ptr); + if (block->Magic != USER_HEAP_MAGIC || block->Free) { + HeapUnlock(); + return (size_t)-1; + } + + size_t size = block->Size; + HeapUnlock(); + return size; +} + // Implements the realloc operation. void* _realloc(void* ptr, size_t newSize) { if (!ptr) return _malloc(newSize); diff --git a/code/user/runtime/heap_exports.c b/code/user/runtime/heap_exports.c index a4d128c..339c8d8 100644 --- a/code/user/runtime/heap_exports.c +++ b/code/user/runtime/heap_exports.c @@ -12,6 +12,7 @@ void* _malloc(size_t size); void _free(void* ptr); void* _calloc(size_t count, size_t size); void* _realloc(void* ptr, size_t size); +size_t _malloc_usable_size(const void* ptr); HEAP_EXPORT void* malloc(size_t size) { return _malloc(size); @@ -28,3 +29,7 @@ HEAP_EXPORT void* calloc(size_t count, size_t size) { HEAP_EXPORT void* realloc(void* ptr, size_t size) { return _realloc(ptr, size); } + +HEAP_EXPORT size_t malloc_usable_size(const void* ptr) { + return _malloc_usable_size(ptr); +} diff --git a/code/user/syscall/syscall_nr.inc b/code/user/syscall/syscall_nr.inc index a88f1fc..f839a86 100644 --- a/code/user/syscall/syscall_nr.inc +++ b/code/user/syscall/syscall_nr.inc @@ -3,13 +3,13 @@ SYS_EXIT_PROCESS EQU 0 SYS_EXIT_THREAD EQU 1 SYS_CREATE_PROCESS EQU 2 -SYS_WAIT_FOR_PROCESS EQU 3 +SYS_WAIT_SINGLE EQU 3 SYS_GET_PROCESS_EXIT_CODE EQU 4 SYS_KILL_PROCESS EQU 5 SYS_GET_PROCESS_LIST EQU 6 SYS_GET_PROCESS_PID EQU 7 SYS_CREATE_THREAD EQU 8 -SYS_WAIT_THREAD EQU 9 +SYS_RETIRED_WAIT_THREAD EQU 9 SYS_GET_CURRENT_TID EQU 10 SYS_GET_CURRENT_PID EQU 11 SYS_SET_THREAD_PRIORITY EQU 12 @@ -23,17 +23,17 @@ SYS_PROTECT_VM EQU 19 SYS_QUERY_VM EQU 20 SYS_CLOSE_HANDLE EQU 21 SYS_CREATE_MUTEX EQU 22 -SYS_ACQUIRE_MUTEX EQU 23 +SYS_RETIRED_ACQUIRE_MUTEX EQU 23 SYS_TRY_ACQUIRE_MUTEX EQU 24 SYS_RELEASE_MUTEX EQU 25 SYS_CREATE_SEM EQU 26 -SYS_WAIT_SEM EQU 27 +SYS_RETIRED_WAIT_SEM EQU 27 SYS_TRY_WAIT_SEM EQU 28 SYS_SIGNAL_SEM EQU 29 SYS_CREATE_EVENT EQU 30 SYS_SIGNAL_EVENT EQU 31 SYS_CLEAR_EVENT EQU 32 -SYS_WAIT_EVENT EQU 33 +SYS_RETIRED_WAIT_EVENT EQU 33 SYS_CREATE_QUEUE EQU 34 SYS_SEND_QUEUE EQU 35 SYS_RECV_QUEUE EQU 36 @@ -66,36 +66,47 @@ SYS_SET_CONSOLE_CURSOR EQU 62 SYS_FILL_CONSOLE_CHARACTER EQU 63 SYS_FILL_CONSOLE_ATTRIBUTE EQU 64 SYS_PUT_SCREEN_CHAR EQU 65 -SYS_BLIT_SCREEN EQU 66 -SYS_CLEAR_SCREEN EQU 67 -SYS_GET_SCREEN_SIZE EQU 68 -SYS_QUERY_DISPLAY_INFO EQU 69 -SYS_POLL_KEYBOARD EQU 70 -SYS_IS_KEY_DOWN EQU 71 -SYS_READ_KEYBOARD_CHAR EQU 72 -SYS_READ_KEY EQU 73 -SYS_GET_KEY_MODIFIERS EQU 74 -SYS_POLL_MOUSE EQU 75 -SYS_READ_MOUSE EQU 76 -SYS_GET_MOUSE_POS EQU 77 -SYS_GET_TICK EQU 78 -SYS_PRINT_DEBUG EQU 79 -SYS_QUERY_TRAP_FRAME EQU 80 -SYS_CREATE_SOCKET EQU 81 -SYS_BIND_SOCKET EQU 82 -SYS_LISTEN_SOCKET EQU 83 -SYS_ACCEPT_SOCKET EQU 84 -SYS_CONNECT_SOCKET EQU 85 -SYS_SEND_SOCKET EQU 86 -SYS_RECV_SOCKET EQU 87 -SYS_SHUTDOWN_SOCKET EQU 88 -SYS_SEND_TO_SOCKET EQU 89 -SYS_RECV_FROM_SOCKET EQU 90 -SYS_RESOLVE_HOST EQU 91 -SYS_SET_SOCKET_OPTION EQU 92 -SYS_GET_SOCKET_OPTION EQU 93 -SYS_POLL EQU 94 -SYS_GET_MEMORY_STATUS EQU 95 -SYS_QUERY_KERNEL_STATS EQU 96 -SYS_GET_SYSTEM_TIME EQU 97 -SYS_MAX EQU 98 +SYS_CLEAR_SCREEN EQU 66 +SYS_GET_SCREEN_SIZE EQU 67 +SYS_QUERY_DISPLAY_INFO EQU 68 +SYS_POLL_KEYBOARD EQU 69 +SYS_IS_KEY_DOWN EQU 70 +SYS_READ_KEYBOARD_CHAR EQU 71 +SYS_READ_KEY EQU 72 +SYS_GET_KEY_MODIFIERS EQU 73 +SYS_POLL_MOUSE EQU 74 +SYS_READ_MOUSE EQU 75 +SYS_GET_MOUSE_POS EQU 76 +SYS_GET_TICK EQU 77 +SYS_PRINT_DEBUG EQU 78 +SYS_QUERY_TRAP_FRAME EQU 79 +SYS_CREATE_SOCKET EQU 80 +SYS_BIND_SOCKET EQU 81 +SYS_LISTEN_SOCKET EQU 82 +SYS_ACCEPT_SOCKET EQU 83 +SYS_CONNECT_SOCKET EQU 84 +SYS_SEND_SOCKET EQU 85 +SYS_RECV_SOCKET EQU 86 +SYS_SHUTDOWN_SOCKET EQU 87 +SYS_SEND_TO_SOCKET EQU 88 +SYS_RECV_FROM_SOCKET EQU 89 +SYS_RESOLVE_HOST EQU 90 +SYS_SET_SOCKET_OPTION EQU 91 +SYS_GET_SOCKET_OPTION EQU 92 +SYS_POLL EQU 93 +SYS_GET_MEMORY_STATUS EQU 94 +SYS_QUERY_KERNEL_STATS EQU 95 +SYS_GET_SYSTEM_TIME EQU 96 +SYS_CREATE_FILE_MAPPING EQU 97 +SYS_MAP_FILE EQU 98 +SYS_CREATE_SURFACE EQU 99 +SYS_MAP_SURFACE EQU 100 +SYS_QUERY_SURFACE EQU 101 +SYS_PRESENT_SURFACE EQU 102 +SYS_GET_THREAD_LIST EQU 103 +SYS_READ_INPUT_EVENT EQU 104 +SYS_PEEK_INPUT_EVENT EQU 105 +SYS_WAIT_MANY EQU 106 +SYS_OPEN_INPUT_QUEUE EQU 107 +SYS_FLUSH_FILE EQU 108 +SYS_MAX EQU 109 diff --git a/code/user/syscall/syscall_stubs.asm b/code/user/syscall/syscall_stubs.asm index 5eb9c13..76b2489 100644 --- a/code/user/syscall/syscall_stubs.asm +++ b/code/user/syscall/syscall_stubs.asm @@ -12,7 +12,8 @@ ; User-mode callers follow standard MS x64: args 1-4 in RCX/RDX/R8/R9, ; args 5+ on the stack above the home area ([rsp+28h], [rsp+30h], ...). ; -; SYSCALL_STUB2 - 4 or fewer args: pure register passing, no stack read. +; SYSCALL_STUB2/3 - 4 or fewer args: pure register passing, no stack read. +; SYSCALL_STUB5 - exactly 5 args: loads [rsp+28h] -> R10 only. ; SYSCALL_STUB6 - 5 or 6 args: loads [rsp+28h] -> R10 and [rsp+30h] -> R11. ; ; Handle lifecycle: @@ -44,6 +45,27 @@ Sys&name PROC EXPORT Sys&name ENDP ENDM +; --------------------------------------------------------------------------- +SYSCALL_STUB3 MACRO name, nr +PUBLIC Sys&name +Sys&name PROC EXPORT + mov rax, nr + int 40h + ret +Sys&name ENDP +ENDM + +; --------------------------------------------------------------------------- +SYSCALL_STUB5 MACRO name, nr +PUBLIC Sys&name +Sys&name PROC EXPORT + mov r10, [rsp+28h] ; 5th argument + mov rax, nr + int 40h + ret +Sys&name ENDP +ENDM + ; --------------------------------------------------------------------------- SYSCALL_STUB6 MACRO name, nr PUBLIC Sys&name @@ -62,14 +84,16 @@ ENDM SYSCALL_STUB2 ExitProcess, SYS_EXIT_PROCESS SYSCALL_STUB2 ExitThread, SYS_EXIT_THREAD SYSCALL_STUB2 CreateProcess, SYS_CREATE_PROCESS -SYSCALL_STUB2 WaitForProcess, SYS_WAIT_FOR_PROCESS +SYSCALL_STUB3 WaitSingle, SYS_WAIT_SINGLE +SYSCALL_STUB2 WaitMany, SYS_WAIT_MANY +SYSCALL_STUB2 OpenInputQueue, SYS_OPEN_INPUT_QUEUE SYSCALL_STUB2 GetProcessExitCode, SYS_GET_PROCESS_EXIT_CODE SYSCALL_STUB2 KillProcess, SYS_KILL_PROCESS SYSCALL_STUB2 GetProcessList, SYS_GET_PROCESS_LIST SYSCALL_STUB2 GetProcessPid, SYS_GET_PROCESS_PID +SYSCALL_STUB2 GetThreadList, SYS_GET_THREAD_LIST SYSCALL_STUB2 CreateThread, SYS_CREATE_THREAD -SYSCALL_STUB2 WaitThread, SYS_WAIT_THREAD SYSCALL_STUB2 GetCurrentTid, SYS_GET_CURRENT_TID SYSCALL_STUB2 GetCurrentPid, SYS_GET_CURRENT_PID SYSCALL_STUB2 SetThreadPriority, SYS_SET_THREAD_PRIORITY @@ -120,19 +144,16 @@ SYSCALL_STUB2 CloseHandle, SYS_CLOSE_HANDLE ; Synchronization objects ; --------------------------------------------------------------------------- SYSCALL_STUB2 CreateMutex, SYS_CREATE_MUTEX -SYSCALL_STUB2 AcquireMutex, SYS_ACQUIRE_MUTEX SYSCALL_STUB2 TryAcquireMutex, SYS_TRY_ACQUIRE_MUTEX SYSCALL_STUB2 ReleaseMutex, SYS_RELEASE_MUTEX SYSCALL_STUB2 CreateSem, SYS_CREATE_SEM -SYSCALL_STUB2 WaitSem, SYS_WAIT_SEM SYSCALL_STUB2 TryWaitSem, SYS_TRY_WAIT_SEM SYSCALL_STUB2 SignalSem, SYS_SIGNAL_SEM SYSCALL_STUB2 CreateEvent, SYS_CREATE_EVENT SYSCALL_STUB2 SignalEvent, SYS_SIGNAL_EVENT SYSCALL_STUB2 ClearEvent, SYS_CLEAR_EVENT -SYSCALL_STUB2 WaitEvent, SYS_WAIT_EVENT ; --------------------------------------------------------------------------- ; Queues and named queues @@ -159,6 +180,7 @@ SYSCALL_STUB2 SeekFile, SYS_SEEK_FILE SYSCALL_STUB2 GetFilePos, SYS_GET_FILE_POS SYSCALL_STUB2 GetFileSize, SYS_GET_FILE_SIZE SYSCALL_STUB2 TruncateFile, SYS_TRUNCATE_FILE +SYSCALL_STUB2 FlushFile, SYS_FLUSH_FILE SYSCALL_STUB2 DeleteFile, SYS_DELETE_FILE SYSCALL_STUB2 RenameFile, SYS_RENAME_FILE @@ -178,7 +200,10 @@ SYSCALL_STUB2 SetConsoleCursor, SYS_SET_CONSOLE_CURSOR SYSCALL_STUB6 FillConsoleCharacter, SYS_FILL_CONSOLE_CHARACTER SYSCALL_STUB6 FillConsoleAttribute, SYS_FILL_CONSOLE_ATTRIBUTE SYSCALL_STUB6 PutScreenChar, SYS_PUT_SCREEN_CHAR -SYSCALL_STUB6 BlitScreen, SYS_BLIT_SCREEN +SYSCALL_STUB5 CreateSurface, SYS_CREATE_SURFACE +SYSCALL_STUB2 MapSurface, SYS_MAP_SURFACE +SYSCALL_STUB2 QuerySurface, SYS_QUERY_SURFACE +SYSCALL_STUB2 PresentSurface, SYS_PRESENT_SURFACE SYSCALL_STUB2 ClearScreen, SYS_CLEAR_SCREEN SYSCALL_STUB2 GetScreenSize, SYS_GET_SCREEN_SIZE SYSCALL_STUB2 QueryDisplayInfo, SYS_QUERY_DISPLAY_INFO @@ -191,6 +216,8 @@ SYSCALL_STUB2 IsKeyDown, SYS_IS_KEY_DOWN SYSCALL_STUB2 ReadKeyboardChar, SYS_READ_KEYBOARD_CHAR SYSCALL_STUB2 ReadKey, SYS_READ_KEY SYSCALL_STUB2 GetKeyModifiers, SYS_GET_KEY_MODIFIERS +SYSCALL_STUB2 ReadInputEvent, SYS_READ_INPUT_EVENT +SYSCALL_STUB2 PeekInputEvent, SYS_PEEK_INPUT_EVENT SYSCALL_STUB2 PollMouse, SYS_POLL_MOUSE SYSCALL_STUB2 ReadMouse, SYS_READ_MOUSE @@ -224,5 +251,7 @@ SYSCALL_STUB2 Poll, SYS_POLL SYSCALL_STUB2 GetMemoryStatus, SYS_GET_MEMORY_STATUS SYSCALL_STUB2 QueryKernelStats, SYS_QUERY_KERNEL_STATS SYSCALL_STUB2 GetSystemTime, SYS_GET_SYSTEM_TIME +SYSCALL_STUB5 CreateFileMapping, SYS_CREATE_FILE_MAPPING +SYSCALL_STUB6 MapFile, SYS_MAP_FILE END diff --git a/code/user/system/init/init.c b/code/user/system/init/init.c index 25545cd..d77b468 100644 --- a/code/user/system/init/init.c +++ b/code/user/system/init/init.c @@ -76,9 +76,9 @@ void _start(void) } debug_print("[init] waiting\n"); - s = SysWaitForProcess(h); + s = SysWait(h, SYS_WAIT_FLAG_NONE); if (s != STATUS_SUCCESS) - debug_print("[init] WARNING: SysWaitForProcess returned error\n"); + debug_print("[init] WARNING: SysWait returned error\n"); int exitCode = 0; if (SysGetProcessExitCode(h, &exitCode) != STATUS_SUCCESS) { diff --git a/code/user/tests/async_apc/async_apc.c b/code/user/tests/async_apc/async_apc.c index f45639f..8304931 100644 --- a/code/user/tests/async_apc/async_apc.c +++ b/code/user/tests/async_apc/async_apc.c @@ -15,10 +15,35 @@ static void print(const char* s) { SysWriteConsole(&req); } -// Opens file. -static uint32_t open_file(const char* path, uint32_t* handleOut) { - SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(path), handleOut }; - return SysOpenFile(&req); +// Opens the first available executable payload for async I/O. +// Tests are usually launched from /apps, so a bare "system/init.exe" +// resolves as /apps/system/init.exe and fails. Prefer the test binary itself, +// then fall back to absolute/drive-qualified paths. +static uint32_t open_test_file(uint32_t* handleOut, const char** pathOut) { + static const char* paths[] = { + "async_apc.exe", + "/apps/async_apc.exe", + "1:/apps/async_apc.exe", + "0:/apps/async_apc.exe", + "/system/init.exe", + "0:/system/init.exe", + "1:/system/init.exe", + "system/init.exe", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysOpenFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *handleOut = h; + if (pathOut) + *pathOut = paths[i]; + return SYS_STATUS_SUCCESS; + } + } + + return SYS_STATUS_NOT_FOUND; } // Prints hex8. @@ -74,7 +99,8 @@ int main(void) { } uint32_t file = 0; - s = open_file("system/init.exe", &file); + const char* openedPath = 0; + s = open_test_file(&file, &openedPath); if (s != SYS_STATUS_SUCCESS) { print("async_apc: open failed status="); print_u64(s); @@ -82,6 +108,10 @@ int main(void) { SysExitProcess(1); } + print("async_apc: using "); + print(openedPath ? openedPath : ""); + print("\n"); + uint32_t event = 0; s = SysCreateEvent(EVENT_AUTO_RESET, 0, &event); if (s != SYS_STATUS_SUCCESS) { @@ -125,7 +155,7 @@ int main(void) { } while (!state.Delivered) { - s = SysWaitEvent(event, 1); + s = SysWait(event, SYS_WAIT_FLAG_ALERTABLE); print("async_apc: alertable wait returned "); print_u64(s); print("\n"); diff --git a/code/user/tests/async_cancel/async_cancel.c b/code/user/tests/async_cancel/async_cancel.c index 0df69c7..6ea12ed 100644 --- a/code/user/tests/async_cancel/async_cancel.c +++ b/code/user/tests/async_cancel/async_cancel.c @@ -14,10 +14,35 @@ static void print(const char* s) { SysWriteConsole(&req); } -// Opens file. -static uint32_t open_file(const char* path, uint32_t* handleOut) { - SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(path), handleOut }; - return SysOpenFile(&req); +// Opens the first available executable payload for async I/O. +// Tests are usually launched from /apps, so a bare "system/init.exe" +// resolves as /apps/system/init.exe and fails. Prefer the test binary itself, +// then fall back to absolute/drive-qualified paths. +static uint32_t open_test_file(uint32_t* handleOut, const char** pathOut) { + static const char* paths[] = { + "async_cancel.exe", + "/apps/async_cancel.exe", + "1:/apps/async_cancel.exe", + "0:/apps/async_cancel.exe", + "/system/init.exe", + "0:/system/init.exe", + "1:/system/init.exe", + "system/init.exe", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysOpenFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *handleOut = h; + if (pathOut) + *pathOut = paths[i]; + return SYS_STATUS_SUCCESS; + } + } + + return SYS_STATUS_NOT_FOUND; } // Prints u64. @@ -46,12 +71,17 @@ int main(void) { uint32_t event = 0; uint32_t request = 0; - uint32_t s = open_file("system/init.exe", &file); + const char* openedPath = 0; + uint32_t s = open_test_file(&file, &openedPath); if (s != SYS_STATUS_SUCCESS) { print("async_cancel: open failed status="); print_u64(s); print("\n"); SysExitProcess(1); } + print("async_cancel: using "); + print(openedPath ? openedPath : ""); + print("\n"); + s = SysCreateEvent(EVENT_AUTO_RESET, 0, &event); if (s != SYS_STATUS_SUCCESS) { print("async_cancel: event create failed status="); print_u64(s); print("\n"); @@ -90,7 +120,7 @@ int main(void) { print("async_cancel: cancel returned "); print_u64(cs); print("\n"); while (iosb.Status == SYS_STATUS_PENDING) { - s = SysWaitEvent(event, 1); + s = SysWait(event, SYS_WAIT_FLAG_ALERTABLE); print("async_cancel: wait returned "); print_u64(s); print("\n"); if (s != SYS_STATUS_SUCCESS && s != SYS_STATUS_KERNEL_APC && s != SYS_STATUS_USER_APC && s != SYS_STATUS_ALERTED) break; diff --git a/code/user/tests/dirty_writeback/dirty_writeback.c b/code/user/tests/dirty_writeback/dirty_writeback.c new file mode 100644 index 0000000..91e4ef4 --- /dev/null +++ b/code/user/tests/dirty_writeback/dirty_writeback.c @@ -0,0 +1,227 @@ +// Dirty file-cache writeback regression test. +// Verifies buffered writes dirty cache pages, explicit flush writes them back, and close flushes synchronously. + +#include +#include +#include +#include + +#define PAGE_SIZE_BYTES 4096u +#define TEST_SIZE (PAGE_SIZE_BYTES * 4u) +#define REWRITE_SIZE (PAGE_SIZE_BYTES * 2u) +#define CLOSE_SIZE (PAGE_SIZE_BYTES * 3u) + +static uint8_t gPatternA[TEST_SIZE]; +static uint8_t gPatternB[TEST_SIZE]; +static uint8_t gReadback[TEST_SIZE]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { + print("0"); + return; + } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c[2] = { buf[i], 0 }; + print(c); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("dirty_writeback: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("dirty_writeback: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static uint32_t query_cache(SYS_FILE_CACHE_STATS* out) { + memset(out, 0, sizeof(*out)); + return SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, out, sizeof(*out), 0); +} + +static void delete_path(const char* path) { + SYS_PATH_REQUEST req = { SysSvFromCstr(path) }; + (void)SysDeleteFile(&req); +} + +static uint32_t create_temp(uint32_t* fileOut, const char** pathOut) { + static const char* paths[] = { + "dirty_writeback.tmp", + "/dirty_writeback.tmp", + "1:/dirty_writeback.tmp", + "0:/dirty_writeback.tmp", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + delete_path(paths[i]); + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysCreateFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *fileOut = h; + if (pathOut) *pathOut = paths[i]; + return SYS_STATUS_SUCCESS; + } + } + + return SYS_STATUS_NOT_FOUND; +} + +static uint32_t open_path(const char* path, uint32_t* fileOut) { + uint32_t h = 0; + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(path), &h }; + uint32_t s = SysOpenFile(&req); + if (s == SYS_STATUS_SUCCESS) + *fileOut = h; + return s; +} + +static void fill_patterns(void) { + for (size_t i = 0; i < sizeof(gPatternA); ++i) { + gPatternA[i] = (uint8_t)(0x11u + (uint8_t)(i * 13u + (i >> 6))); + gPatternB[i] = (uint8_t)(0x80u + (uint8_t)(i * 31u + (i >> 4))); + } +} + +static void checked_write(uint32_t file, const void* buffer, size_t size, const char* what) { + size_t done = 0; + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(file, buffer, size, &done); + uint32_t s = SysWriteFile(&req); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); + if (done != size) fail("short write"); +} + +static void checked_read(uint32_t file, void* buffer, size_t size, size_t* gotOut, const char* what) { + size_t got = 0; + memset(buffer, 0xCC, size); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(file, buffer, size, &got); + uint32_t s = SysReadFile(&req); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); + if (gotOut) *gotOut = got; +} + +static void checked_seek(uint32_t file, uint64_t offset, const char* what) { + uint32_t s = SysSeekFile(file, offset); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); +} + +static void expect_writeback_progress(const SYS_FILE_CACHE_STATS* before, + const SYS_FILE_CACHE_STATS* after, + const char* what) { + if (after->FlushFileFailures != before->FlushFileFailures) + fail("flush failure counter increased"); + if (after->WritebackFailures != before->WritebackFailures) + fail("writeback failure counter increased"); + if (after->WritebackBytes <= before->WritebackBytes) { + print("dirty_writeback: "); + print(what); + print(" did not write back bytes\n"); + exit_now(1); + } +} + +int main(void) { + print("dirty_writeback: start\n"); + fill_patterns(); + + uint32_t file = 0; + const char* path = 0; + uint32_t s = create_temp(&file, &path); + if (s != SYS_STATUS_SUCCESS) fail_status("create temp", s); + + print("dirty_writeback: using "); + print(path ? path : ""); + print("\n"); + + SYS_FILE_CACHE_STATS before; + SYS_FILE_CACHE_STATS afterWrite; + SYS_FILE_CACHE_STATS afterFlush; + SYS_FILE_CACHE_STATS beforeClose; + SYS_FILE_CACHE_STATS afterClose; + + if (query_cache(&before) != SYS_STATUS_SUCCESS) fail("initial stats query failed"); + + checked_write(file, gPatternA, sizeof(gPatternA), "write pattern A"); + + if (query_cache(&afterWrite) != SYS_STATUS_SUCCESS) fail("post-write stats query failed"); + if (afterWrite.DirtyMarks <= before.DirtyMarks) fail("write did not mark dirty cache pages"); + if (afterWrite.DirtyPages <= before.DirtyPages) fail("write did not leave dirty pages resident"); + + checked_seek(file, 0, "seek readback before flush"); + size_t got = 0; + checked_read(file, gReadback, sizeof(gReadback), &got, "read before flush"); + if (got != sizeof(gPatternA)) fail("read before flush size mismatch"); + if (memcmp(gReadback, gPatternA, sizeof(gPatternA)) != 0) fail("cached read before flush mismatch"); + + s = SysFlushFile(file); + if (s != SYS_STATUS_SUCCESS) fail_status("explicit flush", s); + + if (query_cache(&afterFlush) != SYS_STATUS_SUCCESS) fail("post-flush stats query failed"); + if (afterFlush.FlushFileCalls <= afterWrite.FlushFileCalls) fail("flush call counter did not increase"); + expect_writeback_progress(&afterWrite, &afterFlush, "explicit flush"); + + checked_seek(file, 0, "seek rewrite"); + checked_write(file, gPatternB, REWRITE_SIZE, "rewrite pattern B"); + checked_seek(file, REWRITE_SIZE, "seek close-tail"); + checked_write(file, gPatternA + REWRITE_SIZE, CLOSE_SIZE - REWRITE_SIZE, "write close tail"); + + if (query_cache(&beforeClose) != SYS_STATUS_SUCCESS) fail("pre-close stats query failed"); + if (beforeClose.DirtyMarks <= afterFlush.DirtyMarks) fail("rewrite did not mark dirty pages"); + + s = SysCloseHandle(file); + file = 0; + if (s != SYS_STATUS_SUCCESS) fail_status("close", s); + + if (query_cache(&afterClose) != SYS_STATUS_SUCCESS) fail("post-close stats query failed"); + if (afterClose.FlushFileCalls <= beforeClose.FlushFileCalls) fail("close did not flush file"); + expect_writeback_progress(&beforeClose, &afterClose, "close flush"); + + s = open_path(path, &file); + if (s != SYS_STATUS_SUCCESS) fail_status("reopen", s); + + checked_read(file, gReadback, CLOSE_SIZE, &got, "read after reopen"); + if (got != CLOSE_SIZE) fail("read after reopen size mismatch"); + if (memcmp(gReadback, gPatternB, REWRITE_SIZE) != 0) fail("rewrite data not durable after close"); + if (memcmp(gReadback + REWRITE_SIZE, gPatternA + REWRITE_SIZE, + CLOSE_SIZE - REWRITE_SIZE) != 0) fail("tail data not durable after close"); + + s = SysCloseHandle(file); + file = 0; + if (s != SYS_STATUS_SUCCESS) fail_status("close reopened", s); + delete_path(path); + + print("dirty_writeback: dirty-marks+="); + print_u64(afterClose.DirtyMarks - before.DirtyMarks); + print(" writeback-bytes+="); + print_u64(afterClose.WritebackBytes - before.WritebackBytes); + print(" flushes+="); + print_u64(afterClose.FlushFileCalls - before.FlushFileCalls); + print("\n"); + + print("dirty_writeback: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/mmap_smoke/mmap_smoke.c b/code/user/tests/mmap_smoke/mmap_smoke.c new file mode 100644 index 0000000..62670bb --- /dev/null +++ b/code/user/tests/mmap_smoke/mmap_smoke.c @@ -0,0 +1,196 @@ +// User-mode regression test for mmap-style native file mappings. + +#include +#include +#include +#include + +#define TEST_MAP_SIZE 8192u + +static uint8_t gExpected[TEST_MAP_SIZE]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { + print("0"); + return; + } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c = buf[i]; + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromParts(&c, 1) }; + SysWriteConsole(&req); + } +} + +static void exit_now(int code) { + // SysExitProcess should not return, but keep tests from falling through to + // the synthetic entry frame if a broken kernel build returns anyway. + for (;;) + SysExitProcess(code); +} + +static void fail_status(const char* what, uint32_t status) { + print("mmap_smoke: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static void fail_msg(const char* msg) { + print("mmap_smoke: "); + print(msg); + print("\n"); + exit_now(1); +} + +static uint32_t open_first_existing(uint32_t* fileOut, const char** pathOut) { + static const char* candidates[] = { + "mmap_smoke.exe", + "/apps/mmap_smoke.exe", + "0:/apps/mmap_smoke.exe", + "1:/apps/mmap_smoke.exe", + + "/system/init.exe", + "0:/system/init.exe", + "1:/system/init.exe", + "system/init.exe", + }; + + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); ++i) { + uint32_t file = 0; + SYS_PATH_HANDLE_REQUEST openReq = { SysSvFromCstr(candidates[i]), &file }; + uint32_t s = SysOpenFile(&openReq); + if (s == SYS_STATUS_SUCCESS) { + *fileOut = file; + if (pathOut) + *pathOut = candidates[i]; + return SYS_STATUS_SUCCESS; + } + } + + return SYS_STATUS_NOT_FOUND; +} + +int main(void) { + print("mmap_smoke: start\n"); + + uint32_t file = 0; + const char* openedPath = 0; + uint32_t s = open_first_existing(&file, &openedPath); + if (s != SYS_STATUS_SUCCESS) + fail_status("open test file", s); + + print("mmap_smoke: using "); + print(openedPath ? openedPath : ""); + print("\n"); + + size_t got = 0; + memset(gExpected, 0xA5, sizeof(gExpected)); + + SYS_FILE_IO_REQUEST ioReq = SysMakeFileIoRequest(file, gExpected, sizeof(gExpected), &got); + s = SysReadFile(&ioReq); + if (s != SYS_STATUS_SUCCESS) + fail_status("read baseline", s); + if (got < 64) + fail_msg("baseline read was unexpectedly short"); + + print("mmap_smoke: baseline read OK\n"); + + uint32_t mapping = 0; + s = SysCreateFileMapping(file, + SYS_VM_READ, + SYS_MAP_FLAG_NONE, + sizeof(gExpected), + &mapping); + if (s != SYS_STATUS_SUCCESS) + fail_status("create file mapping", s); + + print("mmap_smoke: mapping created\n"); + + // The mapping object must hold its own file reference. Closing the source + // handle before SysMapFile catches lifetime bugs in the object layer. + SysCloseHandle(file); + file = 0; + + print("mmap_smoke: source handle closed\n"); + + void* base = 0; + s = SysMapFile(mapping, + 0, + sizeof(gExpected), + SYS_MAP_ACCESS_READ, + SYS_MAP_FLAG_NONE, + &base); + + SysCloseHandle(mapping); + mapping = 0; + + if (s != SYS_STATUS_SUCCESS) + fail_status("map file view", s); + if (!base) + fail_msg("map returned null base"); + + print("mmap_smoke: view mapped\n"); + + volatile uint8_t* view = (volatile uint8_t*)base; + + // Touch both pages before comparing. This ensures the test exercises + // demand-faulted file-backed VAD pages rather than only VAD creation. + (void)view[0]; + print("mmap_smoke: first page fault OK\n"); + + (void)view[4096]; + print("mmap_smoke: second page fault OK\n"); + + for (size_t i = 0; i < got; ++i) { + if (view[i] != gExpected[i]) { + print("mmap_smoke: byte mismatch at "); + print_u64(i); + print(" expected="); + print_u64(gExpected[i]); + print(" got="); + print_u64(view[i]); + print("\n"); + SysFreeVm(base); + exit_now(1); + } + } + + print("mmap_smoke: mapped bytes match\n"); + + // If the file is shorter than TEST_MAP_SIZE, the mapped tail must be + // zero-filled rather than exposing stale physical memory. + for (size_t i = got; i < sizeof(gExpected) && i < got + 64; ++i) { + if (view[i] != 0) { + print("mmap_smoke: nonzero EOF fill at "); + print_u64(i); + print(" got="); + print_u64(view[i]); + print("\n"); + SysFreeVm(base); + exit_now(1); + } + } + + print("mmap_smoke: EOF fill OK\n"); + + s = SysFreeVm(base); + if (s != SYS_STATUS_SUCCESS) + fail_status("unmap", s); + + print("mmap_smoke: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/pcache_invalidate/pcache_invalidate.c b/code/user/tests/pcache_invalidate/pcache_invalidate.c new file mode 100644 index 0000000..b5e1467 --- /dev/null +++ b/code/user/tests/pcache_invalidate/pcache_invalidate.c @@ -0,0 +1,223 @@ +// Page-cache write/truncate invalidation regression test. +// Verifies cached clean pages are not reused after file write or truncate. + +#include +#include +#include +#include + +#define TEST_SIZE 8192u +#define TRUNC_SIZE 1024u + +static uint8_t gOriginal[TEST_SIZE]; +static uint8_t gReplacement[4096]; +static uint8_t gReadback[TEST_SIZE]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c[2] = { buf[i], 0 }; + print(c); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("pcache_invalidate: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("pcache_invalidate: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static uint32_t query_cache(SYS_FILE_CACHE_STATS* out) { + memset(out, 0, sizeof(*out)); + return SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, out, sizeof(*out), 0); +} + +static void delete_path(const char* path) { + SYS_PATH_REQUEST req = { SysSvFromCstr(path) }; + (void)SysDeleteFile(&req); +} + +static uint32_t create_temp(uint32_t* fileOut, const char** pathOut) { + static const char* paths[] = { + "pcache_invalidate.tmp", + "/pcache_invalidate.tmp", + "1:/pcache_invalidate.tmp", + "0:/pcache_invalidate.tmp", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + delete_path(paths[i]); + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysCreateFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *fileOut = h; + if (pathOut) *pathOut = paths[i]; + return SYS_STATUS_SUCCESS; + } + } + + return SYS_STATUS_NOT_FOUND; +} + +static void fill_patterns(void) { + for (size_t i = 0; i < TEST_SIZE; ++i) + gOriginal[i] = (uint8_t)(0x30u + (uint8_t)(i * 17u + (i >> 7))); + for (size_t i = 0; i < sizeof(gReplacement); ++i) + gReplacement[i] = (uint8_t)(0xA0u + (uint8_t)(i * 29u + (i >> 5))); +} + +static void checked_write(uint32_t file, const void* buffer, size_t size, const char* what) { + size_t done = 0; + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(file, buffer, size, &done); + uint32_t s = SysWriteFile(&req); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); + if (done != size) fail("short write"); +} + +static void checked_read(uint32_t file, void* buffer, size_t size, size_t* gotOut, const char* what) { + size_t got = 0; + memset(buffer, 0xCC, size); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(file, buffer, size, &got); + uint32_t s = SysReadFile(&req); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); + if (gotOut) *gotOut = got; +} + +int main(void) { + print("pcache_invalidate: start\n"); + fill_patterns(); + + uint32_t file = 0; + const char* path = 0; + uint32_t s = create_temp(&file, &path); + if (s != SYS_STATUS_SUCCESS) fail_status("create temp", s); + + print("pcache_invalidate: using "); + print(path ? path : ""); + print("\n"); + + checked_write(file, gOriginal, sizeof(gOriginal), "write original"); + + s = SysSeekFile(file, 0); + if (s != SYS_STATUS_SUCCESS) fail_status("seek fill", s); + + SYS_FILE_CACHE_STATS before; + SYS_FILE_CACHE_STATS afterFill; + SYS_FILE_CACHE_STATS afterSecond; + SYS_FILE_CACHE_STATS afterWrite; + SYS_FILE_CACHE_STATS afterRewriteRead; + SYS_FILE_CACHE_STATS afterTruncate; + if (query_cache(&before) != SYS_STATUS_SUCCESS) fail("initial stats query failed"); + + size_t got = 0; + checked_read(file, gReadback, sizeof(gReadback), &got, "first read"); + if (got != sizeof(gOriginal)) fail("first read size mismatch"); + if (memcmp(gReadback, gOriginal, sizeof(gOriginal)) != 0) fail("first read data mismatch"); + + if (query_cache(&afterFill) != SYS_STATUS_SUCCESS) fail("fill stats query failed"); + if (afterFill.Loads <= before.Loads) fail("initial read did not populate cache"); + + s = SysSeekFile(file, 0); + if (s != SYS_STATUS_SUCCESS) fail_status("seek second", s); + checked_read(file, gReadback, sizeof(gReadback), &got, "second read"); + if (got != sizeof(gOriginal)) fail("second read size mismatch"); + if (memcmp(gReadback, gOriginal, sizeof(gOriginal)) != 0) fail("second read data mismatch"); + + if (query_cache(&afterSecond) != SYS_STATUS_SUCCESS) fail("second stats query failed"); + // Do not require this second read to hit: the cache is intentionally + // bounded and pcache_pressure.exe may have left it at the soft limit, so + // the newly-populated temp pages are eligible for immediate pressure + // eviction. This test is about write/truncate freshness; mmap/read reuse + // is covered by pcache_mmap_read_reuse.exe. + + s = SysSeekFile(file, 0); + if (s != SYS_STATUS_SUCCESS) fail_status("seek rewrite", s); + checked_write(file, gReplacement, sizeof(gReplacement), "rewrite first page"); + + if (query_cache(&afterWrite) != SYS_STATUS_SUCCESS) fail("post-write stats query failed"); + if (afterWrite.Invalidations <= afterSecond.Invalidations) fail("write did not invalidate cached page"); + + uint32_t mapping = 0; + s = SysCreateFileMapping(file, SYS_VM_READ, SYS_MAP_FLAG_NONE, sizeof(gReplacement), &mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("create mapping after rewrite", s); + + void* mappedBase = 0; + s = SysMapFile(mapping, 0, sizeof(gReplacement), SYS_MAP_ACCESS_READ, SYS_MAP_FLAG_NONE, &mappedBase); + SysCloseHandle(mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("map after rewrite", s); + if (!mappedBase) fail("map after rewrite returned null"); + + volatile uint8_t* mappedView = (volatile uint8_t*)mappedBase; + (void)mappedView[0]; + (void)mappedView[sizeof(gReplacement) - 1]; + if (memcmp((const void*)mappedBase, gReplacement, sizeof(gReplacement)) != 0) { + SysFreeVm(mappedBase); + fail("stale mmap data after write invalidation"); + } + s = SysFreeVm(mappedBase); + if (s != SYS_STATUS_SUCCESS) fail_status("unmap after rewrite", s); + + s = SysSeekFile(file, 0); + if (s != SYS_STATUS_SUCCESS) fail_status("seek verify rewrite", s); + checked_read(file, gReadback, sizeof(gReadback), &got, "read after rewrite"); + if (got != sizeof(gOriginal)) fail("rewrite read size mismatch"); + if (memcmp(gReadback, gReplacement, sizeof(gReplacement)) != 0) fail("stale data after write invalidation"); + if (memcmp(gReadback + sizeof(gReplacement), gOriginal + sizeof(gReplacement), + sizeof(gOriginal) - sizeof(gReplacement)) != 0) fail("unwritten page changed after rewrite"); + + if (query_cache(&afterRewriteRead) != SYS_STATUS_SUCCESS) fail("rewrite-read stats query failed"); + + s = SysTruncateFile(file, TRUNC_SIZE); + if (s != SYS_STATUS_SUCCESS) fail_status("truncate", s); + if (SysGetFileSize(file) != TRUNC_SIZE) fail("truncate size mismatch"); + + if (query_cache(&afterTruncate) != SYS_STATUS_SUCCESS) fail("truncate stats query failed"); + if (afterTruncate.Invalidations <= afterRewriteRead.Invalidations) fail("truncate did not invalidate cached tail"); + + s = SysSeekFile(file, 0); + if (s != SYS_STATUS_SUCCESS) fail_status("seek truncated", s); + checked_read(file, gReadback, sizeof(gReadback), &got, "read truncated"); + if (got != TRUNC_SIZE) fail("truncated read length mismatch"); + if (memcmp(gReadback, gReplacement, TRUNC_SIZE) != 0) fail("truncated data mismatch"); + + SysCloseHandle(file); + delete_path(path); + + print("pcache_invalidate: invalidations+="); + print_u64(afterTruncate.Invalidations - before.Invalidations); + print(" loads+="); + print_u64(afterTruncate.Loads - before.Loads); + print(" hits+="); + print_u64(afterTruncate.Hits - before.Hits); + print("\n"); + + print("pcache_invalidate: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/pcache_mmap_read_reuse/pcache_mmap_read_reuse.c b/code/user/tests/pcache_mmap_read_reuse/pcache_mmap_read_reuse.c new file mode 100644 index 0000000..872933e --- /dev/null +++ b/code/user/tests/pcache_mmap_read_reuse/pcache_mmap_read_reuse.c @@ -0,0 +1,176 @@ +// Page-cache mmap->read reuse regression test. +// Faults file pages through mmap first, then verifies SysReadFile reuses those pages. + +#include +#include +#include +#include + +#define TEST_SIZE 8192u + +static uint8_t gReadback[TEST_SIZE]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c[2] = { buf[i], 0 }; + print(c); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("pcache_mmap_read_reuse: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("pcache_mmap_read_reuse: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static uint32_t query_cache(SYS_FILE_CACHE_STATS* out) { + memset(out, 0, sizeof(*out)); + return SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, out, sizeof(*out), 0); +} + +static uint32_t open_test_file(uint32_t* out, const char** pathOut) { + static const char* paths[] = { + "pcache_mmap_read_reuse.exe", + "/apps/pcache_mmap_read_reuse.exe", + "1:/apps/pcache_mmap_read_reuse.exe", + "0:/apps/pcache_mmap_read_reuse.exe", + "pcache_smoke.exe", + "/apps/pcache_smoke.exe", + "1:/apps/pcache_smoke.exe", + "0:/apps/pcache_smoke.exe", + "/system/init.exe", + "0:/system/init.exe", + "1:/system/init.exe", + "system/init.exe", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysOpenFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *out = h; + if (pathOut) *pathOut = paths[i]; + return SYS_STATUS_SUCCESS; + } + } + return SYS_STATUS_NOT_FOUND; +} + +int main(void) { + print("pcache_mmap_read_reuse: start\n"); + + SYS_FILE_CACHE_STATS before; + SYS_FILE_CACHE_STATS afterFault; + SYS_FILE_CACHE_STATS afterRead; + uint32_t s = query_cache(&before); + if (s != SYS_STATUS_SUCCESS) fail_status("query initial stats", s); + + uint32_t file = 0; + const char* path = 0; + s = open_test_file(&file, &path); + if (s != SYS_STATUS_SUCCESS) fail_status("open test file", s); + + uint64_t fileSize = SysGetFileSize(file); + if (fileSize < 4096) fail("test file too small"); + size_t mapSize = (fileSize < TEST_SIZE) ? (size_t)fileSize : TEST_SIZE; + + print("pcache_mmap_read_reuse: using "); + print(path ? path : ""); + print(" size="); + print_u64(fileSize); + print("\n"); + + uint32_t mapping = 0; + s = SysCreateFileMapping(file, SYS_VM_READ, SYS_MAP_FLAG_NONE, mapSize, &mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("create mapping", s); + + void* base = 0; + s = SysMapFile(mapping, 0, mapSize, SYS_MAP_ACCESS_READ, SYS_MAP_FLAG_NONE, &base); + SysCloseHandle(mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("map file", s); + if (!base) fail("map returned null"); + + volatile uint8_t* view = (volatile uint8_t*)base; + for (size_t i = 0; i < mapSize; i += 4096) + (void)view[i]; + (void)view[mapSize - 1]; + + s = query_cache(&afterFault); + if (s != SYS_STATUS_SUCCESS) fail_status("query after mmap faults", s); + if (afterFault.Loads <= before.Loads) { + SysFreeVm(base); + fail("mmap faults did not populate file cache"); + } + + s = SysSeekFile(file, 0); + if (s != SYS_STATUS_SUCCESS) { + SysFreeVm(base); + fail_status("seek", s); + } + + size_t got = 0; + memset(gReadback, 0xCC, sizeof(gReadback)); + SYS_FILE_IO_REQUEST readReq = SysMakeFileIoRequest(file, gReadback, mapSize, &got); + s = SysReadFile(&readReq); + if (s != SYS_STATUS_SUCCESS) { + SysFreeVm(base); + fail_status("read", s); + } + if (got != mapSize) { + SysFreeVm(base); + fail("read length mismatch"); + } + if (memcmp(gReadback, (const void*)base, mapSize) != 0) { + SysFreeVm(base); + fail("read data differs from mapped data"); + } + + s = query_cache(&afterRead); + if (s != SYS_STATUS_SUCCESS) { + SysFreeVm(base); + fail_status("query after read", s); + } + if (afterRead.Hits <= afterFault.Hits) { + SysFreeVm(base); + fail("read did not reuse mmap-populated cache pages"); + } + + SysFreeVm(base); + SysCloseHandle(file); + + print("pcache_mmap_read_reuse: loads+="); + print_u64(afterRead.Loads - before.Loads); + print(" hits+="); + print_u64(afterRead.Hits - before.Hits); + print("\n"); + print("pcache_mmap_read_reuse: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/pcache_multithread/pcache_multithread.c b/code/user/tests/pcache_multithread/pcache_multithread.c new file mode 100644 index 0000000..53ba344 --- /dev/null +++ b/code/user/tests/pcache_multithread/pcache_multithread.c @@ -0,0 +1,227 @@ +// Multi-threaded file-cache/VAD fault regression test. +// Several threads map the same file mapping and fault/read pages concurrently. + +#include +#include +#include +#include + +#define THREAD_COUNT 6u +#define ITERATIONS 16u +#define TEST_SIZE 32768u + +static uint8_t gPattern[TEST_SIZE]; + +typedef struct THREAD_CTX { + uint32_t Index; + uint32_t Mapping; + volatile uint32_t Status; + volatile uint32_t IterationsDone; +} THREAD_CTX; + +static THREAD_CTX gThreads[THREAD_COUNT]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c[2] = { buf[i], 0 }; + print(c); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("pcache_multithread: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("pcache_multithread: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static uint32_t query_cache(SYS_FILE_CACHE_STATS* out) { + memset(out, 0, sizeof(*out)); + return SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, out, sizeof(*out), 0); +} + +static void delete_path(const char* path) { + SYS_PATH_REQUEST req = { SysSvFromCstr(path) }; + (void)SysDeleteFile(&req); +} + +static uint32_t create_temp(uint32_t* fileOut, const char** pathOut) { + static const char* paths[] = { + "pcache_multithread.tmp", + "/pcache_multithread.tmp", + "1:/pcache_multithread.tmp", + "0:/pcache_multithread.tmp", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + delete_path(paths[i]); + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysCreateFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *fileOut = h; + if (pathOut) *pathOut = paths[i]; + return SYS_STATUS_SUCCESS; + } + } + + return SYS_STATUS_NOT_FOUND; +} + +static void fill_pattern(void) { + for (size_t i = 0; i < TEST_SIZE; ++i) + gPattern[i] = (uint8_t)(0x5Au ^ (uint8_t)(i * 13u) ^ (uint8_t)(i >> 3)); +} + +static uint32_t validate_mapping(const uint8_t* base) { + for (size_t i = 0; i < TEST_SIZE; ++i) { + if (base[i] != gPattern[i]) + return 0x10000000u | (uint32_t)(i & 0x0FFFFFFFu); + } + return SYS_STATUS_SUCCESS; +} + +static void thread_main(uintptr_t arg) { + THREAD_CTX* ctx = (THREAD_CTX*)arg; + if (!ctx) SysExitThread(1); + + for (uint32_t iter = 0; iter < ITERATIONS; ++iter) { + void* base = 0; + uint32_t s = SysMapFile(ctx->Mapping, 0, TEST_SIZE, + SYS_MAP_ACCESS_READ, SYS_MAP_FLAG_NONE, &base); + if (s != SYS_STATUS_SUCCESS || !base) { + ctx->Status = s ? s : SYS_STATUS_INVALID_PARAMETER; + SysExitThread(1); + } + + volatile uint8_t* touch = (volatile uint8_t*)base; + for (size_t off = 0; off < TEST_SIZE; off += 4096) + (void)touch[off]; + (void)touch[TEST_SIZE - 1]; + + s = validate_mapping((const uint8_t*)base); + uint32_t fs = SysFreeVm(base); + if (s != SYS_STATUS_SUCCESS) { + ctx->Status = s; + SysExitThread(1); + } + if (fs != SYS_STATUS_SUCCESS) { + ctx->Status = fs; + SysExitThread(1); + } + ctx->IterationsDone++; + } + + ctx->Status = SYS_STATUS_SUCCESS; + SysExitThread(0); +} + +int main(void) { + print("pcache_multithread: start\n"); + fill_pattern(); + + uint32_t file = 0; + const char* path = 0; + uint32_t s = create_temp(&file, &path); + if (s != SYS_STATUS_SUCCESS) fail_status("create temp", s); + + size_t written = 0; + SYS_FILE_IO_REQUEST writeReq = SysMakeFileIoRequest(file, gPattern, sizeof(gPattern), &written); + s = SysWriteFile(&writeReq); + if (s != SYS_STATUS_SUCCESS) fail_status("write pattern", s); + if (written != sizeof(gPattern)) fail("short write"); + + uint32_t mapping = 0; + s = SysCreateFileMapping(file, SYS_VM_READ, SYS_MAP_FLAG_NONE, TEST_SIZE, &mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("create mapping", s); + + SYS_FILE_CACHE_STATS before; + SYS_FILE_CACHE_STATS after; + if (query_cache(&before) != SYS_STATUS_SUCCESS) fail("initial stats query failed"); + + uint32_t handles[THREAD_COUNT]; + uint32_t tids[THREAD_COUNT]; + memset(handles, 0, sizeof(handles)); + memset(tids, 0, sizeof(tids)); + memset(gThreads, 0, sizeof(gThreads)); + + for (uint32_t i = 0; i < THREAD_COUNT; ++i) { + gThreads[i].Index = i; + gThreads[i].Mapping = mapping; + gThreads[i].Status = SYS_STATUS_PENDING; + SYS_CREATE_THREAD_REQUEST req = { + (uintptr_t)thread_main, + (uintptr_t)&gThreads[i], + 1, + 0, + &handles[i], + &tids[i] + }; + s = SysCreateThread(&req); + if (s != SYS_STATUS_SUCCESS) fail_status("create thread", s); + } + + for (uint32_t i = 0; i < THREAD_COUNT; ++i) { + s = SysWait(handles[i], SYS_WAIT_FLAG_NONE); + if (s != SYS_STATUS_SUCCESS) fail_status("wait thread", s); + SysCloseHandle(handles[i]); + } + + for (uint32_t i = 0; i < THREAD_COUNT; ++i) { + if (gThreads[i].Status != SYS_STATUS_SUCCESS) { + print("pcache_multithread: worker "); + print_u64(i); + print(" status="); + print_u64(gThreads[i].Status); + print(" iterations="); + print_u64(gThreads[i].IterationsDone); + print("\n"); + exit_now(1); + } + if (gThreads[i].IterationsDone != ITERATIONS) fail("worker iteration count mismatch"); + } + + if (query_cache(&after) != SYS_STATUS_SUCCESS) fail("final stats query failed"); + if (after.Loads <= before.Loads) fail("concurrent mmap did not load cache pages"); + if (after.Hits <= before.Hits) fail("concurrent mmap did not reuse cache pages"); + + SysCloseHandle(mapping); + SysCloseHandle(file); + delete_path(path); + + print("pcache_multithread: loads+="); + print_u64(after.Loads - before.Loads); + print(" hits+="); + print_u64(after.Hits - before.Hits); + print(" races+="); + print_u64(after.RaceDiscards - before.RaceDiscards); + print("\n"); + print("pcache_multithread: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/pcache_pressure/pcache_pressure.c b/code/user/tests/pcache_pressure/pcache_pressure.c new file mode 100644 index 0000000..5f3f7ca --- /dev/null +++ b/code/user/tests/pcache_pressure/pcache_pressure.c @@ -0,0 +1,169 @@ +// Sequential file-cache pressure test. +// Reads a large read-only asset through SysReadFile and checks that the +// read-only page cache stays bounded by dropping clean pages. + +#include +#include +#include +#include + +#define READ_CHUNK 4096u +#define MIN_PRESSURE_BYTES (3u * 1024u * 1024u) +#define RESIDENT_LIMIT_PAGES 640u + +static uint8_t gBuf[READ_CHUNK]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c[2] = { buf[i], 0 }; + print(c); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("pcache_pressure: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("pcache_pressure: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static uint32_t query_cache(SYS_FILE_CACHE_STATS* out) { + return SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, out, sizeof(*out), 0); +} + +static uint32_t open_first_large(uint32_t* fileOut, const char** pathOut, uint64_t* sizeOut) { + static const char* candidates[] = { + "1:/apps/video.bk2", + "0:/apps/video.bk2", + "/apps/video.bk2", + "video.bk2", + + "1:/apps/doom1.wad", + "0:/apps/doom1.wad", + "/apps/doom1.wad", + "doom1.wad", + }; + + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); ++i) { + uint32_t h = 0; + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(candidates[i]), &h }; + uint32_t s = SysOpenFile(&req); + if (s != SYS_STATUS_SUCCESS) + continue; + + uint64_t size = SysGetFileSize(h); + if (size >= MIN_PRESSURE_BYTES) { + *fileOut = h; + *pathOut = candidates[i]; + *sizeOut = size; + return SYS_STATUS_SUCCESS; + } + + SysCloseHandle(h); + } + + return SYS_STATUS_NOT_FOUND; +} + +int main(void) { + print("pcache_pressure: start\n"); + + SYS_FILE_CACHE_STATS before; + SYS_FILE_CACHE_STATS after; + if (query_cache(&before) != SYS_STATUS_SUCCESS) + fail("initial cache stats query failed"); + + uint32_t file = 0; + const char* path = 0; + uint64_t size = 0; + uint32_t s = open_first_large(&file, &path, &size); + if (s != SYS_STATUS_SUCCESS) + fail_status("open large asset", s); + + print("pcache_pressure: using "); + print(path ? path : ""); + print(" size="); + print_u64(size); + print("\n"); + + uint64_t total = 0; + uint64_t target = size < (8u * 1024u * 1024u) ? size : (8u * 1024u * 1024u); + while (total < target) { + size_t got = 0; + SYS_FILE_IO_REQUEST readReq = SysMakeFileIoRequest(file, gBuf, sizeof(gBuf), &got); + s = SysReadFile(&readReq); + if (s != SYS_STATUS_SUCCESS) + fail_status("read", s); + if (got == 0) + break; + total += got; + } + + SysCloseHandle(file); + + if (total < MIN_PRESSURE_BYTES) + fail("asset read was too short to pressure the cache"); + + if (query_cache(&after) != SYS_STATUS_SUCCESS) + fail("final cache stats query failed"); + + uint64_t loads = after.Loads - before.Loads; + uint64_t hits = after.Hits - before.Hits; + uint64_t drops = after.Drops - before.Drops; + uint64_t raReq = after.ReadAheadRequests - before.ReadAheadRequests; + uint64_t raLoad = after.ReadAheadPagesLoaded - before.ReadAheadPagesLoaded; + uint64_t raThrottle = after.ReadAheadPagesThrottled - before.ReadAheadPagesThrottled; + uint64_t batchReads = after.BatchReads - before.BatchReads; + uint64_t batchPages = after.BatchPagesLoaded - before.BatchPagesLoaded; + + print("pcache_pressure: read bytes="); print_u64(total); + print(" loads+="); print_u64(loads); + print(" hits+="); print_u64(hits); + print(" drops+="); print_u64(drops); + print(" resident="); print_u64(after.ResidentPages); + print(" active="); print_u64(after.ActivePages); + print(" standby="); print_u64(after.StandbyPages); + print(" max="); print_u64(after.MaxResidentPages); + print(" readahead-req+="); print_u64(raReq); + print(" readahead-load+="); print_u64(raLoad); + print(" readahead-throttle+="); print_u64(raThrottle); + print(" batch-reads+="); print_u64(batchReads); + print(" batch-pages+="); print_u64(batchPages); + print("\n"); + + if (loads < 512) + fail("expected at least 512 cache page loads"); + if (after.ResidentPages > RESIDENT_LIMIT_PAGES) + fail("resident page cache exceeded pressure bound"); + if (loads > RESIDENT_LIMIT_PAGES && drops == 0) + fail("cache did not drop clean pages under pressure"); + + print("pcache_pressure: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/pcache_smoke/pcache_smoke.c b/code/user/tests/pcache_smoke/pcache_smoke.c new file mode 100644 index 0000000..53de5ba --- /dev/null +++ b/code/user/tests/pcache_smoke/pcache_smoke.c @@ -0,0 +1,156 @@ +// Read-only page-cache smoke test. +// Verifies that ReadFile and file-backed mappings share the same cached pages. + +#include +#include +#include +#include + +#define TEST_SIZE 8192u + +static uint8_t gA[TEST_SIZE]; +static uint8_t gB[TEST_SIZE]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c = buf[i]; + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromParts(&c, 1) }; + SysWriteConsole(&req); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("pcache_smoke: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t s) { + print("pcache_smoke: "); print(what); print(" failed status="); print_u64(s); print("\n"); + exit_now(1); +} + +static uint32_t query_cache(SYS_FILE_CACHE_STATS* out) { + memset(out, 0, sizeof(*out)); + return SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, out, sizeof(*out), 0); +} + +static uint32_t open_self(uint32_t* out) { + static const char* paths[] = { + "pcache_smoke.exe", + "/apps/pcache_smoke.exe", + "Y:/apps/pcache_smoke.exe", + "0:/apps/pcache_smoke.exe", + "1:/apps/pcache_smoke.exe", + "/apps/shell_native.exe", + "0:/apps/shell_native.exe", + "1:/apps/shell_native.exe", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysOpenFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *out = h; + print("pcache_smoke: using "); print(paths[i]); print("\n"); + return SYS_STATUS_SUCCESS; + } + } + return SYS_STATUS_NOT_FOUND; +} + +int main(void) { + print("pcache_smoke: start\n"); + + SYS_FILE_CACHE_STATS before, afterFirst, afterSecond, afterMap; + uint32_t s = query_cache(&before); + if (s != SYS_STATUS_SUCCESS) fail_status("query initial stats", s); + + uint32_t file = 0; + s = open_self(&file); + if (s != SYS_STATUS_SUCCESS) fail_status("open test file", s); + + size_t gotA = 0; + memset(gA, 0xA5, sizeof(gA)); + SYS_FILE_IO_REQUEST readReqA = SysMakeFileIoRequest(file, gA, sizeof(gA), &gotA); + s = SysReadFile(&readReqA); + if (s != SYS_STATUS_SUCCESS) fail_status("first read", s); + if (gotA < 4096) fail("first read too short"); + print("pcache_smoke: first read OK\n"); + + s = query_cache(&afterFirst); + if (s != SYS_STATUS_SUCCESS) fail_status("query after first read", s); + if (afterFirst.Loads <= before.Loads) + fail("first read did not load file-cache pages"); + + s = SysSeekFile(file, 0); + if (s != SYS_STATUS_SUCCESS) fail_status("seek", s); + + size_t gotB = 0; + memset(gB, 0x5A, sizeof(gB)); + SYS_FILE_IO_REQUEST readReqB = SysMakeFileIoRequest(file, gB, sizeof(gB), &gotB); + s = SysReadFile(&readReqB); + if (s != SYS_STATUS_SUCCESS) fail_status("second read", s); + if (gotB != gotA) fail("second read length mismatch"); + if (memcmp(gA, gB, gotA) != 0) fail("second read data mismatch"); + print("pcache_smoke: second read OK\n"); + + s = query_cache(&afterSecond); + if (s != SYS_STATUS_SUCCESS) fail_status("query after second read", s); + if (afterSecond.Hits <= afterFirst.Hits) + fail("second read did not hit file cache"); + + uint32_t mapping = 0; + s = SysCreateFileMapping(file, SYS_VM_READ, SYS_MAP_FLAG_NONE, sizeof(gA), &mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("create mapping", s); + SysCloseHandle(file); + file = 0; + + void* base = 0; + s = SysMapFile(mapping, 0, sizeof(gA), SYS_MAP_ACCESS_READ, SYS_MAP_FLAG_NONE, &base); + SysCloseHandle(mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("map file", s); + if (!base) fail("map returned null"); + + volatile uint8_t* view = (volatile uint8_t*)base; + (void)view[0]; + (void)view[4096]; + print("pcache_smoke: mmap faults OK\n"); + + if (memcmp((const void*)base, gA, gotA) != 0) { + SysFreeVm(base); + fail("mapped data mismatch"); + } + + s = query_cache(&afterMap); + if (s != SYS_STATUS_SUCCESS) fail_status("query after map", s); + if (afterMap.Hits <= afterSecond.Hits) + fail("mmap faults did not hit already cached pages"); + + SysFreeVm(base); + + print("pcache_smoke: stats loads="); print_u64(afterMap.Loads - before.Loads); + print(" hits="); print_u64(afterMap.Hits - before.Hits); + print(" resident="); print_u64(afterMap.ResidentPages); print("\n"); + print("pcache_smoke: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/shared_mmap/shared_mmap.c b/code/user/tests/shared_mmap/shared_mmap.c new file mode 100644 index 0000000..8ed5799 --- /dev/null +++ b/code/user/tests/shared_mmap/shared_mmap.c @@ -0,0 +1,216 @@ +// Shared writable mmap regression test. +// Verifies that writable mapped-file views dirty the coherent file cache and flush through the normal file path. + +#include +#include +#include +#include + +#define PAGE_SIZE_BYTES 4096u +#define TEST_SIZE (PAGE_SIZE_BYTES * 3u) + +static uint8_t gInitial[TEST_SIZE]; +static uint8_t gExpected[TEST_SIZE]; +static uint8_t gReadback[TEST_SIZE]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c[2] = { buf[i], 0 }; + print(c); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("shared_mmap: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("shared_mmap: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static uint32_t query_cache(SYS_FILE_CACHE_STATS* out) { + memset(out, 0, sizeof(*out)); + return SysQueryKernelStats(SYS_KERNEL_STATS_FILE_CACHE, out, sizeof(*out), 0); +} + +static void delete_path(const char* path) { + SYS_PATH_REQUEST req = { SysSvFromCstr(path) }; + (void)SysDeleteFile(&req); +} + +static uint32_t create_temp(uint32_t* fileOut, const char** pathOut) { + static const char* paths[] = { + "shared_mmap.tmp", + "/shared_mmap.tmp", + "1:/shared_mmap.tmp", + "0:/shared_mmap.tmp", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); ++i) { + uint32_t h = 0; + delete_path(paths[i]); + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(paths[i]), &h }; + uint32_t s = SysCreateFile(&req); + if (s == SYS_STATUS_SUCCESS) { + *fileOut = h; + if (pathOut) *pathOut = paths[i]; + return SYS_STATUS_SUCCESS; + } + } + return SYS_STATUS_NOT_FOUND; +} + +static uint32_t open_path(const char* path, uint32_t* fileOut) { + uint32_t h = 0; + SYS_PATH_HANDLE_REQUEST req = { SysSvFromCstr(path), &h }; + uint32_t s = SysOpenFile(&req); + if (s == SYS_STATUS_SUCCESS) + *fileOut = h; + return s; +} + +static void checked_seek(uint32_t file, uint64_t offset, const char* what) { + uint32_t s = SysSeekFile(file, offset); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); +} + +static void checked_write(uint32_t file, const void* buffer, size_t size, const char* what) { + size_t done = 0; + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(file, buffer, size, &done); + uint32_t s = SysWriteFile(&req); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); + if (done != size) fail("short write"); +} + +static void checked_read(uint32_t file, void* buffer, size_t size, size_t* gotOut, const char* what) { + size_t got = 0; + memset(buffer, 0xCC, size); + SYS_FILE_IO_REQUEST req = SysMakeFileIoRequest(file, buffer, size, &got); + uint32_t s = SysReadFile(&req); + if (s != SYS_STATUS_SUCCESS) fail_status(what, s); + if (gotOut) *gotOut = got; +} + +static void fill_initial(void) { + for (size_t i = 0; i < TEST_SIZE; ++i) { + gInitial[i] = (uint8_t)(0x20u + (uint8_t)(i * 7u + (i >> 5))); + gExpected[i] = gInitial[i]; + } +} + +int main(void) { + print("shared_mmap: start\n"); + fill_initial(); + + uint32_t file = 0; + const char* path = 0; + uint32_t s = create_temp(&file, &path); + if (s != SYS_STATUS_SUCCESS) fail_status("create temp", s); + + print("shared_mmap: creating temporary file "); + print(path ? path : ""); + print("\n"); + + checked_write(file, gInitial, TEST_SIZE, "initial write"); + s = SysFlushFile(file); + if (s != SYS_STATUS_SUCCESS) fail_status("initial flush", s); + + uint32_t mapping = 0; + s = SysCreateFileMapping(file, SYS_VM_READWRITE, SYS_MAP_FLAG_NONE, TEST_SIZE, &mapping); + if (s != SYS_STATUS_SUCCESS) fail_status("create mapping", s); + + uint8_t* view = 0; + s = SysMapFile(mapping, 0, TEST_SIZE, SYS_MAP_ACCESS_READ | SYS_MAP_ACCESS_WRITE, + SYS_MAP_FLAG_NONE, (void**)&view); + if (s != SYS_STATUS_SUCCESS) fail_status("map shared write", s); + + for (size_t i = 128; i < 128 + 3000; ++i) { + view[i] = (uint8_t)(0xA0u + (uint8_t)i); + gExpected[i] = view[i]; + } + for (size_t i = PAGE_SIZE_BYTES + 17; i < PAGE_SIZE_BYTES + 17 + 2048; ++i) { + view[i] = (uint8_t)(0x50u + (uint8_t)(i >> 1)); + gExpected[i] = view[i]; + } + + SYS_FILE_CACHE_STATS beforeFlush; + SYS_FILE_CACHE_STATS afterFlush; + if (query_cache(&beforeFlush) != SYS_STATUS_SUCCESS) fail("pre-flush stats failed"); + + s = SysFlushFile(file); + if (s != SYS_STATUS_SUCCESS) fail_status("flush mapped writes", s); + + if (query_cache(&afterFlush) != SYS_STATUS_SUCCESS) fail("post-flush stats failed"); + if (afterFlush.MappedDirtyPages <= beforeFlush.MappedDirtyPages) + fail("mapped dirty pages were not harvested"); + if (afterFlush.WritebackBytes <= beforeFlush.WritebackBytes) + fail("mapped writes did not write back"); + + checked_seek(file, 0, "seek after mapped flush"); + size_t got = 0; + checked_read(file, gReadback, TEST_SIZE, &got, "read after mapped flush"); + if (got != TEST_SIZE) fail("read after mapped flush size mismatch"); + if (memcmp(gReadback, gExpected, TEST_SIZE) != 0) fail("mapped flush data mismatch"); + + for (size_t i = PAGE_SIZE_BYTES * 2 + 3; i < PAGE_SIZE_BYTES * 2 + 900; ++i) { + view[i] = (uint8_t)(0xC0u ^ (uint8_t)i); + gExpected[i] = view[i]; + } + + s = SysFreeVm(view); + view = 0; + if (s != SYS_STATUS_SUCCESS) fail_status("unmap", s); + + s = SysCloseHandle(mapping); + mapping = 0; + if (s != SYS_STATUS_SUCCESS) fail_status("close mapping", s); + + s = SysCloseHandle(file); + file = 0; + if (s != SYS_STATUS_SUCCESS) fail_status("close file", s); + + s = open_path(path, &file); + if (s != SYS_STATUS_SUCCESS) fail_status("reopen", s); + + checked_read(file, gReadback, TEST_SIZE, &got, "read after close"); + if (got != TEST_SIZE) fail("read after close size mismatch"); + if (memcmp(gReadback, gExpected, TEST_SIZE) != 0) fail("unmap/close data mismatch"); + + s = SysCloseHandle(file); + file = 0; + if (s != SYS_STATUS_SUCCESS) fail_status("close reopened", s); + delete_path(path); + + print("shared_mmap: mapped-dirty-pages+="); + print_u64(afterFlush.MappedDirtyPages - beforeFlush.MappedDirtyPages); + print(" writeback-bytes+="); + print_u64(afterFlush.WritebackBytes - beforeFlush.WritebackBytes); + print("\n"); + print("shared_mmap: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/surface_invalid_rect/surface_invalid_rect.c b/code/user/tests/surface_invalid_rect/surface_invalid_rect.c new file mode 100644 index 0000000..5c64283 --- /dev/null +++ b/code/user/tests/surface_invalid_rect/surface_invalid_rect.c @@ -0,0 +1,105 @@ +// Native surface validation test. +// Exercises invalid parameters and clipped dirty rectangles without depending on windowing. + +#include +#include +#include +#include + +#define SURFACE_W 32u +#define SURFACE_H 24u + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c = buf[i]; + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromParts(&c, 1) }; + SysWriteConsole(&req); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("surface_invalid_rect: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("surface_invalid_rect: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static void expect_status(const char* what, uint32_t got, uint32_t expected) { + if (got == expected) return; + print("surface_invalid_rect: "); + print(what); + print(" returned "); + print_u64(got); + print(" expected "); + print_u64(expected); + print("\n"); + exit_now(1); +} + +int main(void) { + print("surface_invalid_rect: start\n"); + + uint32_t bad = 0; + uint32_t s = SysCreateSurface(0, SURFACE_H, SYS_SURFACE_FORMAT_XRGB8888, SYS_SURFACE_FLAG_NONE, &bad); + expect_status("zero width", s, SYS_STATUS_INVALID_PARAMETER); + + s = SysCreateSurface(SURFACE_W, SURFACE_H, 0x9999u, SYS_SURFACE_FLAG_NONE, &bad); + expect_status("bad format", s, SYS_STATUS_NOT_SUPPORTED); + + uint32_t surface = 0; + s = SysCreateSurface(SURFACE_W, SURFACE_H, SYS_SURFACE_FORMAT_XRGB8888, + SYS_SURFACE_FLAG_NONE, &surface); + if (s != SYS_STATUS_SUCCESS) fail_status("create surface", s); + + void* base = 0; + uint32_t pitch = 0; + s = SysMapSurface(surface, &base, &pitch); + if (s != SYS_STATUS_SUCCESS) fail_status("map surface", s); + if (!base || pitch < SURFACE_W * sizeof(uint32_t)) fail("invalid mapping"); + + s = SysPresentSurface(surface, 0, 1); + expect_status("null rect array with nonzero count", s, SYS_STATUS_INVALID_PARAMETER); + + DISPLAY_RECT rects[SYS_SURFACE_MAX_RECTS + 1]; + memset(rects, 0, sizeof(rects)); + s = SysPresentSurface(surface, rects, SYS_SURFACE_MAX_RECTS + 1); + expect_status("too many rects", s, SYS_STATUS_INVALID_PARAMETER); + + DISPLAY_RECT clipped = { -8, -8, 24, 24 }; + s = SysPresentSurface(surface, &clipped, 1); + if (s != SYS_STATUS_SUCCESS) fail_status("present clipped rect", s); + + DISPLAY_RECT empty = { 10000, 10000, 16, 16 }; + s = SysPresentSurface(surface, &empty, 1); + if (s != SYS_STATUS_SUCCESS) fail_status("present empty clipped rect", s); + + SysCloseHandle(surface); + print("surface_invalid_rect: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/surface_multithread_present/surface_multithread_present.c b/code/user/tests/surface_multithread_present/surface_multithread_present.c new file mode 100644 index 0000000..a382e64 --- /dev/null +++ b/code/user/tests/surface_multithread_present/surface_multithread_present.c @@ -0,0 +1,157 @@ +// Native surface multithreaded present test. +// Multiple user threads present one process-private surface to catch mapping and scratch-buffer races. + +#include +#include +#include +#include + +#define SURFACE_W 96u +#define SURFACE_H 64u +#define THREAD_COUNT 4u +#define ITERATIONS 32u + +typedef struct WORKER_STATE { + uint32_t Index; + uint32_t Surface; + uint8_t* Base; + uint32_t Pitch; + uint32_t Status; + uint32_t IterationsDone; +} WORKER_STATE; + +static WORKER_STATE gWorkers[THREAD_COUNT]; + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c = buf[i]; + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromParts(&c, 1) }; + SysWriteConsole(&req); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("surface_multithread_present: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("surface_multithread_present: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +static void worker_main(uintptr_t arg) { + WORKER_STATE* st = (WORKER_STATE*)arg; + if (!st) SysExitThread(1); + uint32_t bandHeight = SURFACE_H / THREAD_COUNT; + uint32_t y0 = st->Index * bandHeight; + uint32_t h = bandHeight; + if (st->Index == THREAD_COUNT - 1) + h = SURFACE_H - y0; + + for (uint32_t iter = 0; iter < ITERATIONS; ++iter) { + for (uint32_t y = 0; y < h; ++y) { + uint32_t* row = (uint32_t*)(st->Base + (size_t)(y0 + y) * st->Pitch); + for (uint32_t x = 0; x < SURFACE_W; ++x) + row[x] = 0x00010101u * ((st->Index + 1u) * 32u + (iter & 31u)); + } + + DISPLAY_RECT rect = { 0, (int32_t)y0, (int32_t)SURFACE_W, (int32_t)h }; + uint32_t s = SysPresentSurface(st->Surface, &rect, 1); + if (s != SYS_STATUS_SUCCESS) { + st->Status = s; + SysExitThread(1); + } + st->IterationsDone++; + SysYield(); + } + + st->Status = SYS_STATUS_SUCCESS; + SysExitThread(0); +} + +int main(void) { + print("surface_multithread_present: start\n"); + + uint32_t surface = 0; + uint32_t s = SysCreateSurface(SURFACE_W, SURFACE_H, SYS_SURFACE_FORMAT_XRGB8888, + SYS_SURFACE_FLAG_NONE, &surface); + if (s != SYS_STATUS_SUCCESS) fail_status("create surface", s); + + void* base = 0; + uint32_t pitch = 0; + s = SysMapSurface(surface, &base, &pitch); + if (s != SYS_STATUS_SUCCESS) fail_status("map surface", s); + if (!base || pitch < SURFACE_W * sizeof(uint32_t)) fail("invalid mapping"); + + uint32_t handles[THREAD_COUNT]; + uint32_t tids[THREAD_COUNT]; + memset(handles, 0, sizeof(handles)); + memset(tids, 0, sizeof(tids)); + memset(gWorkers, 0, sizeof(gWorkers)); + + for (uint32_t i = 0; i < THREAD_COUNT; ++i) { + gWorkers[i].Index = i; + gWorkers[i].Surface = surface; + gWorkers[i].Base = (uint8_t*)base; + gWorkers[i].Pitch = pitch; + gWorkers[i].Status = SYS_STATUS_PENDING; + SYS_CREATE_THREAD_REQUEST req = { + (uintptr_t)worker_main, + (uintptr_t)&gWorkers[i], + 1, + 0, + &handles[i], + &tids[i] + }; + s = SysCreateThread(&req); + if (s != SYS_STATUS_SUCCESS) fail_status("create thread", s); + } + + for (uint32_t i = 0; i < THREAD_COUNT; ++i) { + s = SysWait(handles[i], SYS_WAIT_FLAG_NONE); + if (s != SYS_STATUS_SUCCESS) fail_status("wait thread", s); + SysCloseHandle(handles[i]); + } + + for (uint32_t i = 0; i < THREAD_COUNT; ++i) { + if (gWorkers[i].Status != SYS_STATUS_SUCCESS) { + print("surface_multithread_present: worker "); + print_u64(i); + print(" status="); + print_u64(gWorkers[i].Status); + print(" iterations="); + print_u64(gWorkers[i].IterationsDone); + print("\n"); + exit_now(1); + } + if (gWorkers[i].IterationsDone != ITERATIONS) fail("worker iteration count mismatch"); + } + + SysCloseHandle(surface); + print("surface_multithread_present: PASS\n"); + exit_now(0); + return 0; +} diff --git a/code/user/tests/surface_smoke/surface_smoke.c b/code/user/tests/surface_smoke/surface_smoke.c new file mode 100644 index 0000000..c8304b0 --- /dev/null +++ b/code/user/tests/surface_smoke/surface_smoke.c @@ -0,0 +1,93 @@ +// Native surface smoke test. +// Verifies that process-private surface handles can be created, mapped, queried, presented, and closed. + +#include +#include +#include +#include + +#define SURFACE_W 64u +#define SURFACE_H 48u + +static void print(const char* s) { + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; + SysWriteConsole(&req); +} + +static void print_u64(uint64_t v) { + char buf[32]; + int i = 0; + if (v == 0) { print("0"); return; } + while (v && i < (int)sizeof(buf)) { + buf[i++] = (char)('0' + (v % 10)); + v /= 10; + } + while (i--) { + char c = buf[i]; + SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromParts(&c, 1) }; + SysWriteConsole(&req); + } +} + +static void exit_now(int code) { + for (;;) SysExitProcess(code); +} + +static void fail(const char* msg) { + print("surface_smoke: "); + print(msg); + print("\n"); + exit_now(1); +} + +static void fail_status(const char* what, uint32_t status) { + print("surface_smoke: "); + print(what); + print(" failed status="); + print_u64(status); + print("\n"); + exit_now(1); +} + +int main(void) { + print("surface_smoke: start\n"); + + uint32_t surface = 0; + uint32_t s = SysCreateSurface(SURFACE_W, SURFACE_H, SYS_SURFACE_FORMAT_XRGB8888, + SYS_SURFACE_FLAG_NONE, &surface); + if (s != SYS_STATUS_SUCCESS) fail_status("create surface", s); + if (!surface) fail("surface handle is zero"); + + void* base = 0; + uint32_t pitch = 0; + s = SysMapSurface(surface, &base, &pitch); + if (s != SYS_STATUS_SUCCESS) fail_status("map surface", s); + if (!base || pitch < SURFACE_W * sizeof(uint32_t)) fail("invalid mapping"); + + SYS_SURFACE_DESC desc; + memset(&desc, 0, sizeof(desc)); + s = SysQuerySurface(surface, &desc); + if (s != SYS_STATUS_SUCCESS) fail_status("query surface", s); + if (desc.Width != SURFACE_W || desc.Height != SURFACE_H) fail("query dimensions mismatch"); + if (desc.PitchBytes != pitch || desc.Format != SYS_SURFACE_FORMAT_XRGB8888) fail("query metadata mismatch"); + if (desc.Base != base) fail("query base mismatch"); + + uint8_t* rows = (uint8_t*)base; + for (uint32_t y = 0; y < SURFACE_H; ++y) { + uint32_t* row = (uint32_t*)(rows + (size_t)y * pitch); + for (uint32_t x = 0; x < SURFACE_W; ++x) + row[x] = 0x00202020u + ((x & 0xffu) << 16) + ((y & 0xffu) << 8); + } + + DISPLAY_RECT rect = { 0, 0, (int32_t)SURFACE_W, (int32_t)SURFACE_H }; + s = SysPresentSurface(surface, &rect, 1); + if (s != SYS_STATUS_SUCCESS) fail_status("present surface", s); + + s = SysPresentSurface(surface, 0, 0); + if (s != SYS_STATUS_SUCCESS) fail_status("present full surface", s); + + SysCloseHandle(surface); + print("surface_smoke: PASS\n"); + exit_now(0); + return 0; +} diff --git a/docs.md b/docs.md new file mode 100644 index 0000000..80605ce --- /dev/null +++ b/docs.md @@ -0,0 +1,93 @@ +# nt-lite development notes + +nt-lite is a clean-design hobby kernel. It is not a stable compatibility platform. Prefer the right design over preserving old APIs, shims, or compatibility aliases. + +## Principles + +- Break old code when the new model is better. +- Do not keep compatibility paths unless they have a clear purpose. +- Plan the model before writing the patch. +- Make one architectural move at a time. +- Delete stale paths once the real path works. +- Prefer compile failures over wrappers that hide stale users. +- Keep the tree internally consistent. +- Keep one canonical API for each operation. +- A small patch can still represent the final model. +- Do not land temporary architecture just to make a feature appear to work. +- Put policy in the layer that owns it. +- Shared state should have one clear owner. + +## Workflow + +- Keep patches reviewable. +- Boot-test meaningful changes. +- Add diagnostics before optimizing. +- Use logs, counters, and assertions instead of guessing. +- Remove stale comments and dead code as part of the same cleanup. +- Treat a fallback as either a supported mode or delete it before it becomes permanent. +- Ask design questions before changing API shape, object lifetime, ownership, cancellation, persistence, or compatibility behavior. +- Do not add a side path to avoid updating callers. +- When changing an interface, update every caller in the same patch. +- When adding a build artifact, update build, packaging, copy, and runtime discovery paths in the same patch. +- Do not count apply checks or syntax-only checks as a real build. +- Be precise about what was actually validated. +- If behavior can be tested cheaply, include a focused runtime test or a visible diagnostic. +- Prefer counters that prove both success and non-use. + +## Breaking changes + +Breaking changes are allowed. When making one: + +1. Define the new model. +2. Move every user to it. +3. Delete the old path. +4. Bump ABI or version constants when a contract changes. +5. Let stale users fail to compile. + +## Patch expectations for AI agents + +- Generate patches from the repository root. +- Paths must be rooted as `code/...`. +- `git apply ` should work from the repository root. +- Do not require `--directory=code`. +- Keep generated artifacts out of the source tree unless requested. +- When asked to provide the full tree, also provide the patch. +- The patch is for review; the full tree is for reproducing the exact baseline. +- Avoid optimistic claims. +- Say exactly whether a change was apply-checked, syntax-checked, built, booted, or runtime-tested. +- Do not introduce compatibility aliases unless they are intentionally supported API. + +## Comment style + +- Comments explain why, not what the next line already says. +- Use `//` comments only. +- Use sentence case. +- End every sentence with a period. +- Every owned source file should start with a short file comment. +- Every function should have a comment, even if it is only a one-line summary. +- Keep the tone practical and technical. +- Humor is allowed rarely, but never instead of the real explanation. +- Debugging history belongs in comments only when it explains a lasting constraint. +- Document workarounds by saying what looks strange, why it is necessary, and what breaks if it is simplified. +- Avoid trailing comments except for flags, bitfields, register tables, and packed layouts. +- Keep real limitations as normal prose. + +## Development lessons + +- Ask before changing the shape of an API or the ownership model behind it. +- The right model matters more than minimizing the diff. +- A patch should be small enough to review, but not so small that it creates a temporary model. +- If a compatibility path is not meant to stay, do not add it. +- If the old path is replaced, remove it. +- Ergonomics matter. A clean API should not make common calls annoying. +- Debug by evidence, not by the subsystem that changed most recently. +- Poison values, fault addresses, counters, and reproduction steps are more useful than guesses. +- Performance fixes need tail-latency checks, not only average throughput. +- Diagnostics must reach every active output path that developers rely on. +- Runtime tests should clean up after themselves and say clearly when they use temporary data. +- A passing test should leave the system in a sane state, not only print `PASS`. +- Build integration is part of the feature. +- Packaging mistakes are real bugs, not clerical issues. +- When a change crosses layers, verify every layer that knows about the new object, file, syscall, or artifact. +- Prefer finishing the feature the user needs over endlessly polishing the audit around it. +- Do not hide uncertainty. Say what was checked and what still needs a real build or boot test. diff --git a/image.png b/image.png index b6a6d87db2ab7f18f2c65e7781c4093c71f4acae..3a000b319c4a2ca3cb764f0918546dbc6bf89a1a 100644 GIT binary patch literal 387088 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 literal 337413 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

l_ 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�=(e?Ae&TZwuVfrm2$6?oo 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ȸRhQ5A^hBge7IMJZJHPbZur@YyH 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