3350 lines
120 KiB
C
3350 lines
120 KiB
C
// 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 allocator starts at POOL_BASE and commits pages on demand.
|
|
|
|
// User mappings:
|
|
// VADs separate reservation from commitment. Committed pages become resident
|
|
// when a user fault or kernel prefault path materializes them.
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
#include <intrin.h>
|
|
#include <kernel/mm_virt.h>
|
|
#include <kernel/mm_pte.h>
|
|
#include <kernel/mm_pfn.h>
|
|
#include <kernel/mm_swap.h>
|
|
#include <kernel/io_file.h>
|
|
#include <kernel/mm_file_cache.h>
|
|
#include <kernel/ldr_section.h>
|
|
#include <kernel/ob_object.h>
|
|
#include <kernel/ke_process.h>
|
|
#include <kernel/ke_debug.h>
|
|
#include <kernel/ke_spinlock.h>
|
|
#include <kernel/ke_irql.h>
|
|
#include <kernel/ke_sched.h>
|
|
|
|
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;
|
|
if (addr < USER_BASE) return 0;
|
|
if (addr >= USER_LIMIT) return 0;
|
|
if (size > (size_t)(USER_LIMIT - addr)) return 0;
|
|
return 1;
|
|
}
|
|
|
|
// 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;
|
|
|
|
if (size == 0)
|
|
return STATUS_SUCCESS;
|
|
|
|
PROCESS* process = KeGetCurrentProcess();
|
|
if (!process)
|
|
return STATUS_ACCESS_DENIED;
|
|
|
|
return KiVadEnsureResidentRange(process, addr, size, requiredFlags);
|
|
}
|
|
|
|
// Probes user Read.
|
|
__declspec(dllexport) KSTATUS MmProbeUserRead(const void* ptr, size_t size) {
|
|
return KiProbeUserRange((uintptr_t)ptr, size, PAGE_PRESENT | PAGE_USER);
|
|
}
|
|
|
|
// Probes user Write.
|
|
__declspec(dllexport) KSTATUS MmProbeUserWrite(void* ptr, size_t size) {
|
|
return KiProbeUserRange((uintptr_t)ptr, size, PAGE_PRESENT | PAGE_USER | PAGE_WRITE);
|
|
}
|
|
|
|
// Copies from a user buffer through page-sized, pinned segments.
|
|
__declspec(dllexport) KSTATUS CopyFromUser(void* kernelDst, const void* userSrc, size_t size) {
|
|
return KiCopyFromUserPaged(kernelDst, userSrc, size);
|
|
}
|
|
|
|
// Copies to a user buffer through page-sized, pinned segments.
|
|
__declspec(dllexport) KSTATUS CopyToUser(void* userDst, const void* kernelSrc, size_t size) {
|
|
return KiCopyToUserPaged(userDst, kernelSrc, size);
|
|
}
|
|
|
|
// Boundary-tag allocator (used for both pool and user heap)
|
|
// Each allocation has a BLOCK_HDR immediately before the user data, and a
|
|
// BLOCK_FTR immediately after. Both carry the block size and a 'free' flag.
|
|
// This allows O(1) coalescing of adjacent free blocks.
|
|
|
|
#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; // 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 {
|
|
size_t Size;
|
|
uint32_t Free;
|
|
uint32_t Magic;
|
|
} BLOCK_FTR;
|
|
|
|
#define HDR_SIZE (sizeof(BLOCK_HDR))
|
|
#define FTR_SIZE (sizeof(BLOCK_FTR))
|
|
#define OVERHEAD (HDR_SIZE + FTR_SIZE)
|
|
#define MIN_BLOCK (OVERHEAD + sizeof(void*))
|
|
|
|
// Implements the hdr To Ftr operation.
|
|
static inline BLOCK_FTR* KiHdrToFtr(BLOCK_HDR* h) {
|
|
return (BLOCK_FTR *)((char *)h + HDR_SIZE + h->Size);
|
|
}
|
|
// Implements the ftr To Hdr operation.
|
|
static inline BLOCK_HDR* KiFtrToHdr(BLOCK_FTR* f) {
|
|
return (BLOCK_HDR *)((char *)f - HDR_SIZE - f->Size);
|
|
}
|
|
// Implements the next Block operation.
|
|
static inline BLOCK_HDR* KiNextBlock(BLOCK_HDR* h) {
|
|
return (BLOCK_HDR *)((char *)h + HDR_SIZE + h->Size + FTR_SIZE);
|
|
}
|
|
// Implements the prev Ftr operation.
|
|
static inline BLOCK_FTR* KiPrevFtr(BLOCK_HDR* h) {
|
|
return (BLOCK_FTR *)((char *)h - FTR_SIZE);
|
|
}
|
|
|
|
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;
|
|
int DecommitTailOnFree;
|
|
|
|
uint64_t NextSequence;
|
|
BLOCK_HDR* Quarantine[POOL_QUARANTINE_SLOTS];
|
|
size_t QuarantineCursor;
|
|
size_t QuarantineCount;
|
|
};
|
|
|
|
static inline uint32_t KiPoolDebugFlags(void) {
|
|
return KeGetDebugFlags() & (KE_DEBUG_POOL_CANARY |
|
|
KE_DEBUG_POOL_POISON |
|
|
KE_DEBUG_POOL_QUARANTINE |
|
|
KE_DEBUG_POOL_CALLERS);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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) : "<null>",
|
|
(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);
|
|
}
|
|
}
|
|
|
|
// Extend heap by at least 'need' bytes (rounded up to page boundary).
|
|
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;
|
|
|
|
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);
|
|
}
|
|
|
|
// 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 *)oldBrk;
|
|
blk->Magic = BLOCK_MAGIC;
|
|
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 = BLOCK_STATE_FREE;
|
|
KiHdrToFtr(blk)->Size = blk->Size;
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Unlink a free block from the free list.
|
|
static void KiFreeListRemove(HEAP* h, BLOCK_HDR* blk) {
|
|
if (blk->PrevFree) blk->PrevFree->NextFree = blk->NextFree;
|
|
else h->FreeList = blk->NextFree;
|
|
if (blk->NextFree) blk->NextFree->PrevFree = blk->PrevFree;
|
|
blk->PrevFree = blk->NextFree = (BLOCK_HDR *)0;
|
|
}
|
|
|
|
// Implements the heap Try Decommit Tail operation.
|
|
static void KiHeapTryDecommitTail(HEAP* h, BLOCK_HDR* blk) {
|
|
if (!h || !blk || blk->Free == BLOCK_STATE_USED) return;
|
|
|
|
// 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 = 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* 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);
|
|
|
|
// 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);
|
|
return (void *)0;
|
|
}
|
|
// Re-search; KiHeapGrow inserted at the front.
|
|
best = h->FreeList;
|
|
while (best && best->Size < size) best = best->NextFree;
|
|
if (!best) {
|
|
KeReleaseSpinlock(&h->Lock, flags);
|
|
return (void *)0;
|
|
}
|
|
KiPoolValidateFooterLocked(h, best, "grown free block corrupt");
|
|
}
|
|
|
|
KiFreeListRemove(h, best);
|
|
|
|
// Split the block if the remainder is big enough.
|
|
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 = 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 = BLOCK_STATE_FREE;
|
|
KiHdrToFtr(rem)->Size = rem->Size;
|
|
|
|
best->Size = size;
|
|
}
|
|
|
|
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(KiBlockUserBase(best), 0, requestedSize);
|
|
if (best->Size > requestedSize)
|
|
memset(KiBlockUserBase(best) + requestedSize, 0, best->Size - requestedSize);
|
|
KiPoolWriteTailCanary(best);
|
|
|
|
KeReleaseSpinlock(&h->Lock, flags);
|
|
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 KiHeapFreeEx(HEAP* h, void* ptr, uintptr_t caller) {
|
|
if (!ptr) return;
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
KeReleaseSpinlock(&h->Lock, flags);
|
|
}
|
|
|
|
static void KiHeapFree(HEAP* h, void* ptr) {
|
|
KiHeapFreeEx(h, ptr, (uintptr_t)_ReturnAddress());
|
|
}
|
|
|
|
// Implements the heap Realloc operation.
|
|
static void* KiHeapReallocEx(HEAP* h, void* ptr, size_t newSize, uintptr_t caller) {
|
|
// If ptr is NULL, realloc acts like malloc.
|
|
if (!ptr) {
|
|
return KiHeapAllocEx(h, newSize, POOL_TAG_DEFAULT, caller);
|
|
}
|
|
|
|
// If size is 0, realloc acts like free.
|
|
if (newSize == 0) {
|
|
KiHeapFreeEx(h, ptr, caller);
|
|
return (void*)0;
|
|
}
|
|
|
|
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);
|
|
|
|
BLOCK_HDR* blk = (BLOCK_HDR *)(u - HDR_SIZE);
|
|
|
|
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");
|
|
|
|
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);
|
|
|
|
void* newPtr = KiHeapAllocEx(h, newSize, POOL_TAG_DEFAULT, caller);
|
|
if (!newPtr)
|
|
return (void*)0;
|
|
memcpy(newPtr, ptr, oldCopy);
|
|
KiHeapFreeEx(h, ptr, caller);
|
|
return newPtr;
|
|
}
|
|
|
|
static void* KiHeapRealloc(HEAP* h, void* ptr, size_t newSize) {
|
|
return KiHeapReallocEx(h, ptr, newSize, (uintptr_t)_ReturnAddress());
|
|
}
|
|
|
|
// Kernel pool.
|
|
static HEAP gPoolHeap;
|
|
|
|
// 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;
|
|
|
|
uintptr_t ReservationBase;
|
|
size_t ReservationSize;
|
|
uintptr_t Base;
|
|
size_t Size;
|
|
size_t PayloadSize;
|
|
size_t PageCount;
|
|
size_t CommittedPages;
|
|
|
|
uint64_t PageFlags;
|
|
MM_VAD_TYPE Type;
|
|
uint32_t Flags;
|
|
uint32_t LockCount;
|
|
MM_VAD_PAGE* Pages;
|
|
|
|
FILE_OBJECT* FileObject;
|
|
uint64_t FileOffset;
|
|
size_t FileLength;
|
|
uint32_t FileMappingFlags;
|
|
|
|
IMAGE_SECTION_OBJECT* ImageSection;
|
|
} MM_VAD;
|
|
|
|
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) {
|
|
MmPteInitialize(bootPml4Phys);
|
|
|
|
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));
|
|
|
|
if (poolSize) {
|
|
poolSize = (poolSize + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
|
|
KiHeapGrow(&gPoolHeap, poolSize);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// 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());
|
|
}
|
|
|
|
// 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());
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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 (!gVadListReady) return STATUS_FAILURE;
|
|
|
|
uintptr_t candidate = KiAlignUpPtr(MM_VAD_ARENA_BASE, alignment);
|
|
if (!candidate) return STATUS_INVALID_PARAMETER;
|
|
|
|
for (;;) {
|
|
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 = 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;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!collided) {
|
|
*baseOut = candidate;
|
|
return STATUS_SUCCESS;
|
|
}
|
|
candidate = next;
|
|
}
|
|
}
|
|
|
|
// 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 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->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 (MM_VAD*)0;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
|
|
size_t payloadSize = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
|
|
if (payloadSize < size) return STATUS_NO_MEMORY;
|
|
|
|
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;
|
|
|
|
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 reservationBase = 0;
|
|
uintptr_t globalFlags = KeAcquireSpinlock(&gVadLock);
|
|
KSTATUS s = KiFindVadRange(reservationSize, alignment, &reservationBase);
|
|
if (KSUCCESS(s)) {
|
|
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(&gVadLock, globalFlags);
|
|
|
|
if (!KSUCCESS(s)) {
|
|
MmFreePool(pages);
|
|
MmFreePool(vad);
|
|
return s;
|
|
}
|
|
|
|
uintptr_t procFlags = KeAcquireSpinlock(&process->VadLock);
|
|
ListInsertTail(&process->VadList, &vad->ProcessLink);
|
|
KeReleaseSpinlock(&process->VadLock, procFlags);
|
|
|
|
*outBase = (void*)vad->Base;
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
// 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;
|
|
|
|
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) {
|
|
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 : vad->PageFlags;
|
|
if (flags) flags |= PAGE_PRESENT | PAGE_USER;
|
|
|
|
size_t first = KiVadPageIndex(vad, alignedStart);
|
|
size_t count = KiVadRangePageCount(alignedStart, alignedEnd);
|
|
if (first > vad->PageCount || count > vad->PageCount - first)
|
|
return STATUS_ACCESS_DENIED;
|
|
|
|
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;
|
|
|
|
s = MmPfnRetain(frames[i]);
|
|
if (!KSUCCESS(s))
|
|
break;
|
|
|
|
s = KiVadMapPageTracked(dst + i * PAGE_SIZE, page->Frame, KiVadHardwarePageFlags(page), MmPteMapFailIfPresent);
|
|
if (!KSUCCESS(s)) {
|
|
(void)MmPfnRelease(frames[i]);
|
|
break;
|
|
}
|
|
|
|
gVadStats.CowPagesShared++;
|
|
}
|
|
|
|
if (!KSUCCESS(s)) {
|
|
(void)MmVadDestroy(targetProcess, targetBase);
|
|
return s;
|
|
}
|
|
|
|
gVadStats.CowViewsCreated++;
|
|
*outBase = targetBase;
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
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;
|
|
|
|
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 || !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;
|
|
|
|
uint64_t finalFlags = pageFlags ? (pageFlags | PAGE_PRESENT | PAGE_USER) : 0;
|
|
size_t first = KiVadPageIndex(vad, alignedStart);
|
|
size_t count = KiVadRangePageCount(alignedStart, alignedEnd);
|
|
|
|
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;
|
|
|
|
if (!finalFlags) {
|
|
if (page->Resident && page->PageFlags)
|
|
(void)KiVadUnmapPageTracked(va, page->Frame);
|
|
page->PageFlags = 0;
|
|
continue;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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->VadLock);
|
|
MM_VAD* vad = KiFindVadByBaseLocked(process, base);
|
|
if (!vad) {
|
|
KeReleaseSpinlock(&process->VadLock, procFlags);
|
|
return STATUS_NOT_FOUND;
|
|
}
|
|
if (vad->LockCount) {
|
|
KeReleaseSpinlock(&process->VadLock, procFlags);
|
|
return STATUS_ACCESS_DENIED;
|
|
}
|
|
ListRemove(&vad->ProcessLink);
|
|
KeReleaseSpinlock(&process->VadLock, procFlags);
|
|
|
|
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);
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
// 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->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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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->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->VadLock, procFlags);
|
|
return STATUS_NOT_FOUND;
|
|
}
|
|
|
|
// 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->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->VadLock, flags);
|
|
}
|
|
|
|
if (vadCountOut) *vadCountOut = vads;
|
|
if (reservedBytesOut) *reservedBytesOut = reserved;
|
|
if (committedBytesOut) *committedBytesOut = committed;
|
|
}
|
|
|
|
// 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->VadLock);
|
|
MM_VAD* vad = KiFindVadByBaseLocked(process, base);
|
|
size_t size = vad ? vad->Size : 0;
|
|
KeReleaseSpinlock(&process->VadLock, procFlags);
|
|
return size;
|
|
}
|
|
|
|
// 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(&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(&gVadLock, flags);
|
|
return 0;
|
|
}
|
|
|
|
// Implements the VAD Type Name operation.
|
|
static const char* KiVadTypeName(MM_VAD_TYPE type) {
|
|
switch (type) {
|
|
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 VADs operation.
|
|
void MmVadDebugDumpProcess(PROCESS* process) {
|
|
if (!process) {
|
|
DbgPrint("MM: VADs: <no process>\n");
|
|
return;
|
|
}
|
|
|
|
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->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,
|
|
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->CommittedPages * PAGE_SIZE),
|
|
(unsigned long long)r->PayloadSize,
|
|
r->Flags,
|
|
(unsigned long long)r->PageFlags);
|
|
count++;
|
|
}
|
|
KeReleaseSpinlock(&process->VadLock, flags);
|
|
if (!count) DbgPrint(" <none>\n");
|
|
}
|
|
|
|
// Implements the debug Describe Pool Address operation.
|
|
static void KiDebugDescribePoolAddress(uintptr_t addr) {
|
|
DbgPrint("MM: kernel pool start=%016llx brk=%016llx limit=%016llx usage=%llu KiB\n",
|
|
(unsigned long long)gPoolHeap.Start,
|
|
(unsigned long long)gPoolHeap.Brk,
|
|
(unsigned long long)gPoolHeap.Limit,
|
|
(unsigned long long)((gPoolHeap.Brk - gPoolHeap.Start) / 1024));
|
|
|
|
if (addr < gPoolHeap.Start || addr >= gPoolHeap.Limit) return;
|
|
if (addr >= gPoolHeap.Brk) {
|
|
DbgPrint("MM: address %016llx is inside pool VA range but above committed brk by +0x%llx\n",
|
|
(unsigned long long)addr,
|
|
(unsigned long long)(addr - gPoolHeap.Brk));
|
|
return;
|
|
}
|
|
|
|
uintptr_t flags = KeAcquireSpinlock(&gPoolHeap.Lock);
|
|
for (BLOCK_HDR* cur = (BLOCK_HDR*)gPoolHeap.Start;
|
|
(uintptr_t)cur < gPoolHeap.Brk;
|
|
cur = KiNextBlock(cur)) {
|
|
if (cur->Magic != BLOCK_MAGIC) {
|
|
DbgPrint("MM: pool walk corrupt at hdr=%p magic=%08x\n", cur, cur->Magic);
|
|
break;
|
|
}
|
|
uintptr_t hdr = (uintptr_t)cur;
|
|
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 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,
|
|
(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) {
|
|
DbgPrint("MM: pool walk stopped: non-advancing block hdr=%p size=%llx\n", cur, (unsigned long long)cur->Size);
|
|
break;
|
|
}
|
|
}
|
|
KeReleaseSpinlock(&gPoolHeap.Lock, flags);
|
|
}
|
|
|
|
// Implements the debug Describe Address operation.
|
|
void MmDebugDescribeAddress(uintptr_t addr) {
|
|
PHYSICAL_ADDRESS phys = 0;
|
|
uint64_t flags = 0;
|
|
KSTATUS qs = MmPteQueryPageFor(MmPteQueryDebug, addr, &phys, &flags);
|
|
DbgPrint("MM: describe va=%016llx query=%d phys=%016llx flags=%016llx\n",
|
|
(unsigned long long)addr,
|
|
qs,
|
|
(unsigned long long)phys,
|
|
(unsigned long long)flags);
|
|
|
|
if (addr >= POOL_BASE && addr < POOL_LIMIT) {
|
|
KiDebugDescribePoolAddress(addr);
|
|
return;
|
|
}
|
|
|
|
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) {
|
|
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 : "?",
|
|
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(&gVadLock, gl);
|
|
return;
|
|
}
|
|
}
|
|
KeReleaseSpinlock(&gVadLock, gl);
|
|
DbgPrint("MM: address is in user arena but not reserved by any MM_VAD\n");
|
|
}
|
|
}
|
|
|
|
// Creates user Memory For Process.
|
|
__declspec(dllexport) void* MmAllocateUserMemoryForProcess(PROCESS* process, size_t size) {
|
|
void* base = (void*)0;
|
|
if (!process || !size) return (void*)0;
|
|
KSTATUS s = MmVadCreateCommitted(process, size, PAGE_SIZE,
|
|
PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX,
|
|
MM_VAD_ZERO,
|
|
MmVadHeap,
|
|
&base);
|
|
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)MmVadDestroy(process, ptr);
|
|
}
|
|
|
|
// Implements the reallocate User Memory For Process operation.
|
|
__declspec(dllexport) void* MmReallocateUserMemoryForProcess(PROCESS* process, void* ptr, size_t newSize) {
|
|
if (!process) return (void*)0;
|
|
if (!ptr) return MmAllocateUserMemoryForProcess(process, newSize);
|
|
if (!newSize) {
|
|
MmFreeUserMemoryForProcess(process, ptr);
|
|
return (void*)0;
|
|
}
|
|
|
|
size_t oldSize = MmVadGetSize(process, ptr);
|
|
if (!oldSize) return (void*)0;
|
|
if (newSize <= oldSize) return ptr;
|
|
|
|
void* newPtr = MmAllocateUserMemoryForProcess(process, newSize);
|
|
if (!newPtr) return (void*)0;
|
|
memcpy(newPtr, ptr, oldSize);
|
|
MmFreeUserMemoryForProcess(process, ptr);
|
|
return newPtr;
|
|
}
|
|
|
|
// Creates user Memory.
|
|
__declspec(dllexport) void* MmAllocateUserMemory(size_t size) {
|
|
PROCESS* process = KeGetCurrentProcess();
|
|
return MmAllocateUserMemoryForProcess(process, size);
|
|
}
|
|
|
|
// Releases user Memory.
|
|
__declspec(dllexport) void MmFreeUserMemory(void* ptr) {
|
|
PROCESS* process = KeGetCurrentProcess();
|
|
MmFreeUserMemoryForProcess(process, ptr);
|
|
}
|
|
|
|
// Implements the reallocate User Memory operation.
|
|
__declspec(dllexport) void* MmReallocateUserMemory(void* ptr, size_t newSize) {
|
|
PROCESS* process = KeGetCurrentProcess();
|
|
return MmReallocateUserMemoryForProcess(process, ptr, newSize);
|
|
}
|
|
|
|
// Creates user Image For Process.
|
|
__declspec(dllexport) void* MmAllocateUserImageForProcess(PROCESS* process, size_t size) {
|
|
void* base = (void*)0;
|
|
if (!process || !size) return (void*)0;
|
|
KSTATUS s = MmVadCreateCommitted(process, size, PAGE_SIZE,
|
|
PAGE_PRESENT | PAGE_WRITE | PAGE_USER | PAGE_NX,
|
|
MM_VAD_ZERO | MM_VAD_GUARD_LOW | MM_VAD_GUARD_HIGH,
|
|
MmVadImage,
|
|
&base);
|
|
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)MmVadDestroy(process, ptr);
|
|
}
|
|
|
|
// Creates user Image.
|
|
__declspec(dllexport) void* MmAllocateUserImage(size_t size) {
|
|
PROCESS* process = KeGetCurrentProcess();
|
|
return MmAllocateUserImageForProcess(process, size);
|
|
}
|
|
|
|
// Releases user Image.
|
|
__declspec(dllexport) void MmFreeUserImage(void* ptr, size_t size) {
|
|
PROCESS* process = KeGetCurrentProcess();
|
|
MmFreeUserImageForProcess(process, ptr, size);
|
|
}
|
|
|
|
// Returns kernel Pool Usage.
|
|
size_t MmGetKernelPoolUsage(void) {
|
|
// Returns bytes committed to the kernel pool.
|
|
return (size_t)(gPoolHeap.Brk - gPoolHeap.Start);
|
|
}
|
|
|
|
// Returns user Heap Usage.
|
|
size_t MmGetUserHeapUsage(void) {
|
|
size_t total = 0;
|
|
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(&gVadLock, flags);
|
|
return total;
|
|
}
|