Leaking memory with VirtualAlloc
The Win32 function VirtualAlloc() can be used to allocate new blocks of committed memory. When this happens a new block that is large enough to satisfy the memory requirement is allocated. Careless use of VirtualAlloc() can lead to the waste of significant amounts of memory that you won’t be able to find or use. Effectively this is a VirtualAlloc() memory leak. And for added benefit, this is also a form of memory fragmentation.
But if you pair all your VirtualAlloc() calls with VirtualFree(), how can you be leaking memory?
Background
Before we get into the details of this particular memory problem, we need to cover some background information first.
VirtualAlloc() can be used to reserve memory and commit memory. VirtualAlloc sets memory protections in chunks that are a minimum of one page size, but allocates memory in chunks that are a minimum of the operating system allocation granularity. We refer to these as paragraphs. These sizes are typically 4KB and 64KB, respectively. That said, it is better to ask the operating system for these sizes because they are not fixed in stone and could change with each processor architecture.
SYSTEM_INFO sysInfo; GetSystemInfo(&sysInfo);
The SYSTEM_INFO data members dwPageSize and dwAllocationGranularity contain the two sizes we need.
For the rest of this article, we will assume these sizes are 4KB and 64KB.
Page protections are set in a minimum of one page size
Page protections are set for a memory region that is a minimum of one page size, but also large enough to satisfy the request.
Therefore if you call VirtualAlloc() asking for 1 byte or 2048 bytes, at least one page (4096 bytes) will be allocated and returned.
It’s possible that two pages could be returned. This happens if the region requested straddles a page boundary.
Memory allocation regions are a minimum of the operating system granularity
If VirtualAlloc() is committing or reserving new memory (rather than committing reserved memory), the smallest amount of memory that it can return is the allocation granularity (64KB).
Therefore if you call VirtualAlloc() asking for 1 byte or 2048 or 50,000 bytes, 64KB will be consumed.
VirtualAlloc memory leak
Having set out the basic rules above, how do they result in a memory leak?
For most calls to VirtualAlloc() that allocate new chunks of memory, the requested size will be less than the allocated size, resulting in the allocated block and a smaller block that comes after the allocated block. The smaller block’s creation is implicit. If the caller of VirtualAlloc() doesn’t know about this, they will be accidentally creating wasted regions in the memory space because although the memory is usable, there is no way to find the wasted regions address unless you calculate it at the time of the original block allocation. This is best explained with an example:
If you commit a 24KB block of memory with VirtualAlloc():
ptr = VirtualAlloc(NULL, 24 * 1024, MEM_COMMIT, PAGE_READWRITE);
two blocks result:
1 | 24KB | Committed | PAGE_READWRITE |
2 | 40KB | Free | PAGE_NO_ACCESS |
The first block is pointed to by the pointer returned from VirtualAlloc().
The second block isn’t pointed to by anything. You can calculate where it is if you know about the allocation and the size of the allocation.
But in most cases, for this type of allocation, the second block is there implicitly – the caller probably doesn’t realise they’ve used 64KB of memory to make that 24KB allocation, leaking 40KB of memory in the process.
You can see unused blocks of memory after most DLLs. This is a side effect of DLLs always loading on a 64KB boundary. But DLLs are very rarely an exact multiple of 64KB, which is why you get some wasted space after them. This isn’t really a problem with DLLs, as there are so few of them. But if you’re allocating memory directly from VirtualAlloc() rather than a dedicated heap (CRT, HeapAlloc) you can very quickly create some memory leaks without realising it.
But I’m calling VirtualFree()!
If you’re making VirtualAlloc() calls, you’re probably also cleaning up using VirtualFree() when you’re done with the memory. As such, it’s reasonable to think you aren’t leaking memory. In the start-the-application, do-the-work, cleanup order of things, you aren’t leaking memory. But during that period when you’re doing the work of the program, those VirtualAlloc() calls could be leaking memory if they exhibit the pattern above. That means you’re more likely to suffer memory fragmentation, and your application will use more memory to do its work than it needs to.
Fixing VirtualAlloc memory leaks
In theory, you could try using an alternate heap to allocate from, such as the win32 heap, HeapAlloc(). But if you’re using VirtualAlloc() it’s probably because you want to implement specific page protections on the memory being consumed. That may not be possible with a regular heap, as the memory may not be allocated in blocks on convenient page boundaries.
If that’s the case, you need to continue using VirtualAlloc() but change the way you allocate memory with it.
Keep track of the block following the allocation
You could add code to notice when there is an implicitly leaked block after the allocated block, and keep track of the implicitly leaked block for later use.
static const DWORD PAGE_SIZE = 1024 * 4; // Memory is protected in 4KB chunks static const DWORD VIRTUAL_ALLOC_SIZE = 1024 * 64; // VirtualAlloc() allocates in 64KB chunks static void *reservedPtr = NULL; static DWORD reservedSize = 0; void *allocateVM(DWORD size) // size must be less than VIRTUAL_ALLOC_SIZE for this code to work { void *ptr = NULL; ptr = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE); if (ptr != NULL) { // success, check if we have an implicitly leaked block DWORD vaSize; // size actually allocated by VirtualAlloc, not the size requested DWORD paraSize; vaSize = size % PAGE_SIZE; if (vaSize != 0) vaSize = PAGE_SIZE - vaSize; vaSize += size; paraSize = size % VIRTUAL_ALLOC_SIZE; if (paraSize != 0) paraSize = VIRTUAL_ALLOC_SIZE - paraSize; if (paraSize != 0) { DWORD_PTR addr; addr = (DWORD_PTR)ptr; addr += vaSize; void *ptrGiven; void *ptrNext = (void *)addr; ptrGiven = VirtualAlloc(ptrNext, paraSize, MEM_COMMIT, PAGE_READWRITE); if (ptrGiven != NULL) { // this block is the implicitly leaked block, but now committed and ready for use // save this block location for use next time some memory is needed // FIXME - the rest of this behaviour is left as an exercise for the reader } } } return ptr; }
The above shows the basic principle, but it’s clumsy. A better approach is to implement a simple heap manager.
A simple heap manager
You could reserve memory large enough for multiple memory allocations and then commit memory from the reserved area.
Here is some working code as an example. There is a lot to do to make this production-worthy code (error checking, tracking the allocations to allow for deallocations, etc), but this demonstrates the principle.
static const DWORD PAGE_SIZE = 1024 * 4; // Memory is protected in 4KB chunks static const DWORD VIRTUAL_ALLOC_SIZE = 1024 * 64; // VirtualAlloc() allocates in 64KB chunks static void *reservedPtr = NULL; static DWORD reservedSize = 0; void *allocateVM(DWORD size) // size must be less than VIRTUAL_ALLOC_SIZE for this code to work { void *ptr = NULL; // reserve the memory, if necessary if (reservedSize == 0) { reservedPtr = VirtualAlloc(NULL, VIRTUAL_ALLOC_SIZE, MEM_RESERVE, PAGE_NOACCESS); if (reservedPtr != NULL) reservedSize = VIRTUAL_ALLOC_SIZE; } // allocate from the reserved memory if (reservedSize != 0) { ptr = VirtualAlloc(reservedPtr, size, MEM_COMMIT, PAGE_READWRITE); if (ptr != NULL) { DWORD vaSize; // calculate the size of the memory committed // (if not a multiple of the page size, it will be rounded up to the next page size) vaSize = size % PAGE_SIZE; if (vaSize != 0) vaSize = PAGE_SIZE - vaSize; vaSize += size; // step along to the next reserved location reservedPtr = (void *)((DWORD_PTR)reservedPtr + vaSize); reservedSize -= vaSize; } } return ptr; }
This would be best implemented as a heap manager so that all the caller does is call allocateVM() and deallocateVM(). The heap manager manages the reserving, committing and freeing; the caller has no knowledge of what is happening behind the scenes. This also compartmentalizes the code, making the heap manager suitable for unit testing.
Identifying VirtualAlloc memory leaks
You can check for wasted memory and any possible memory leaks by looking for memory allocation patterns involving wasted memory.
We’re going to show you how to do this with two of our memory analysis tools.
We’ve created an example application testVirtualMemoryWasted.exe, which we’ve provided as source files, project files (Visual Studio), and built executables.
Using VM Validator to find VirtualAlloc() memory leaks
- Start your process with VM Validator. If your process is already running, attach to it (Launch menu, inject into process…).
- Use your application to do the activities that you suspect cause VirtualAlloc() memory leaks.
- Select the Pages tab.
- Change the Type filter to Private.
- The Working set filter should be set to All.
- Scroll through memory looking for Commited or Reserved memory (less than 64KB) followed by Free (Wasted) memory (less than 64KB).
You can also do this with the Paragraphs tab, but it is harder to do because the detail is at the 64KB level rather than individual pages (4KB).
Using Memory Validator to find VirtualAlloc() memory leaks
- Start your process with Memory Validator.
- Use your application to do the activities that you suspect cause VirtualAlloc() memory leaks.
- Select the Memory tab.
- Click Refresh.
- Look for callstack entries with a datatype of “Wasted”.
- When you find an entry with the type “Wasted”, you will find that the entry with a sequence id immediately prior also leaks virtual memory. These are both the same VirtualAlloc() call. The first one leaks memory in the same page as the allocation (the difference between the requested size and the number of pages to satisfy that request). The second one is the implicit memory leak in uncommitted memory.
The statistics tab will also show statistics that include “Wasted” and “void*” (the type most commonly associated with VirtualAlloc).
You can also view the virtual memory in the target application similarly to VM Validator.
- Select the Analysis tab.
- Select the Virtual sub-tab.
- Select the Pages sub-tab.
- Change the Type filter to All.
- Deselect Ignore Colours for Virtual Memory.
- Click Refresh.
- Scroll through memory looking for Committed or Reserved memory (less than 64KB) followed by Free (Wasted) memory (less than 64KB).
You can also do this with the Paragraphs tab, but it is harder to do because the detail is at the 64KB level rather than individual pages (4KB).
Conclusion
In this article, we’ve demonstrated how it is possible to leak memory when using VirtualAlloc().
We’ve explained the interaction between VirtualAlloc() request memory size, memory protection page size and operating system allocation granularity that leads to this type of memory leak.
We’ve demonstrated an approach that mitigates this memory leak.
We’ve used both VM Validator and Memory Validator to demonstrate how to identify these memory leaks in your software.