Introduction
Recently, I encountered a rather odd memory problem on the Windows Server virtual machine I was using to develop ExDAV, my Exchange-to-CalDAV synchronization tool. Proxmox assigned 8 GB of RAM to the VM, but Windows suddenly reported exactly 2 GB after a reboot.
At first, this looked like a Proxmox, Q35, SeaBIOS or memory ballooning problem. The evidence eventually pointed towards a truncated Windows Software Protection policy which no longer contained the memory entitlement for the x64 kernel.
This article will show you how to distinguish this specific problem from the usual VM memory issues, rebuild the affected Windows licensing store and verify that the full memory allocation is available again.
Please don’t use the repair commands based on the 2 GB symptom alone. This procedure was validated only on x64 Windows Server 2016 build 14393 with all of the following observations:
- Proxmox supplies more RAM than Windows exposes, while another operating system can use the full allocation.
- Windows exposes only the RAM below the VM’s PCI/MMIO aperture.
ProductPolicyis abnormally small and lacksKernel-WindowsMaxMemAllowedx64.- The Windows event logs support a Software Protection token or policy failure.
If these points don’t match your machine, you are dealing with another memory problem.
Step 1: Confirm that Proxmox passes the expected memory to QEMU
Before changing anything within Windows, make sure that Proxmox actually starts QEMU with the expected amount of memory. On your Proxmox host, inspect the VM configuration first:
qm config 100Replace 100 with the ID of your affected virtual machine. The relevant part should look similar to this:
balloon: 0
machine: q35
memory: 8192In my case, ballooning was disabled and the VM had a fixed allocation of 8 GB. You can also inspect the full QEMU command generated by Proxmox:
qm showcmd 100 --prettyLook for the -m argument within the output:
-m 8192This confirms that the configured memory reaches QEMU, but it does not yet prove that the guest can use its full physical address map. For that, I booted Ubuntu within the same VM configuration. Ubuntu immediately detected and used all 8 GB.
Windows Setup had also recorded all 8 GB when the server was originally installed and a healthy Windows Server VM worked on the same Proxmox host. Together, these observations ruled out the hypervisor, virtual chipset and physical host memory quite convincingly.
Step 2: Rule out the usual Windows memory limits
Next, verify that Windows itself is not configured with a normal boot-time memory restriction. Open an elevated Command Prompt within the affected VM and inspect the current BCD entry:
bcdedit /enum {current}Make sure that the output contains none of the following options:
maxmem
truncatememory
removememoryProceed to inspect the bad-memory list as well:
bcdedit /enum {badmemory}There should be no badmemorylist excluding the missing address range. Also verify the installed Windows edition and architecture:
Get-CimInstance Win32_OperatingSystem |
Select-Object Caption, Version, BuildNumber, OSArchitecture, TotalVisibleMemorySizeThe affected system was running 64-bit Windows Server 2016 Standard, version 10.0.14393. If your build is different, stop here: This procedure has not been validated on another Windows release.
According to Microsoft’s memory limit documentation, both the Standard and Datacenter editions support up to 24 TB of physical memory. An 8 GB allocation is therefore nowhere near the edition limit.
The VM also exposed a 64-bit CPU with PAE, long mode and sufficient physical-address bits. Its kernel, HAL and loader passed signature and System File Checker verification. None of the regular limits explained the exact 2 GB result.
Step 3: Understand the suspicious 2 GB value
The exact amount was an important clue here. On this particular Q35 VM, the 8 GiB physical memory map was divided like this:
0x00000000-0x7FFFFFFF 2 GiB RAM below 4 GiB
0x80000000-0xFFFFFFFF PCI and MMIO address space
0x100000000-0x27FFFFFFF 6 GiB RAM remapped above 4 GiBWindows accepted the first 2 GiB below the PCI/MMIO aperture, but it rejected the entire 6 GiB range above 4 GiB. So the number was not an arbitrary Windows limit: It matched the low-memory region of this VM exactly.
Please keep in mind that this address layout is specific to the affected VM configuration. Q35 does not necessarily place exactly 2 GiB below 4 GiB in every possible setup.
This exact boundary shifted the investigation away from memory detection: Windows exposed the low region while all RAM remapped above 4 GiB was missing. Rebuilding the licensing store later restored the absent policy values and made this high-memory region available again.
Step 4: Inspect the Windows ProductPolicy
Windows keeps its generated kernel licensing and feature policy within the following registry value:
HKLM\SYSTEM\CurrentControlSet\Control\ProductOptions\ProductPolicyThis is a binary policy generated from the Software Protection licensing store. You can inspect its size and search for the relevant policy names without modifying it. Open an elevated 64-bit PowerShell session and run:
$path = 'HKLM:\SYSTEM\CurrentControlSet\Control\ProductOptions'
$policy = [byte[]](Get-ItemProperty -LiteralPath $path -Name ProductPolicy).ProductPolicy
$policyText = [Text.Encoding]::Unicode.GetString($policy)
[pscustomobject]@{
ProductPolicyBytes = $policy.Length
HasX64MemoryPolicy = $policyText.Contains('Kernel-WindowsMaxMemAllowedx64')
HasProcessorPolicy = $policyText.Contains('Kernel-RegisteredProcessors')
} | Format-ListOn the affected server, the result looked like this:
ProductPolicyBytes : 2248
HasX64MemoryPolicy : False
HasProcessorPolicy : FalseThe healthy comparison server, however, had a 46,156-byte ProductPolicy containing both values:
ProductPolicyBytes : 46156
HasX64MemoryPolicy : True
HasProcessorPolicy : TrueKernel-WindowsMaxMemAllowedx64 on the healthy server described a maximum of 25,165,824 MiB, which corresponds to the normal 24 TiB allowance of Windows Server 2016 Standard. On the affected server, querying that policy returned STATUS_OBJECT_NAME_NOT_FOUND instead.
This did not mean that Windows had simply become unlicensed. The Evaluation license was still recognized, but the licensing store had generated an incomplete kernel policy.
The byte size can differ between Windows installations, so don’t treat exactly 46,156 bytes as a universal requirement. A tiny policy without Kernel-WindowsMaxMemAllowedx64 is the relevant combination here. If your policy has a normal size and the x64 memory value is present, stop at this point and investigate another cause.
Also do not copy ProductPolicy from a healthy machine or edit the binary registry value manually. It is generated from the machine’s own licensing store, which is what we are going to rebuild later.
Step 5: Check for Software Protection failures during memory exhaustion
The policy explained why Windows rejected the high-memory range, but there was still the question of how it became incomplete.
In my case, the server had been under prolonged commit-memory exhaustion caused by Exchange and its supporting processes. Windows recorded Resource Exhaustion Event 2004 repeatedly, often every five minutes.
You can inspect the affected period in an elevated PowerShell session. Adjust both timestamps for your incident:
$startTime = Get-Date '2026-08-09 08:45:00'
$endTime = Get-Date '2026-08-09 10:00:00'
Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-Resource-Exhaustion-Detector'
Id = 2004
StartTime = $startTime
EndTime = $endTime
} |
Select-Object TimeCreated, ProviderName, Id, Message |
Format-ListFrequent consumers on this machine included Microsoft.Exchange.Store.Worker.exe, scanningprocess.exe, noderunner.exe, several w3wp.exe processes, Microsoft Defender and the usual Exchange health services.
Next, search the Application log for the matching Software Protection failures:
Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = 'Microsoft-Windows-Security-SPP'
StartTime = $startTime
EndTime = $endTime
} |
Where-Object {
$_.Message -match '0xD0000017|0x8007000E|BUILD_MASTER_RULES_FROM_FRAGMENTS|Kernel policy cache update failed'
} |
Select-Object TimeCreated, Id, Message |
Format-ListThe Software Protection errors should occur within the same period as the resource-exhaustion events. A similar error from an unrelated date is not sufficient evidence for this repair.
At 09:03:58, wlms.exe, the Windows License Monitoring Service, started the Software Protection service for its regular Evaluation license check. Software Protection then failed while rebuilding its master policy:
Stage: BUILD_MASTER_RULES_FROM_FRAGMENTS
Kernel policy cache update failed: 0xD00000170xD0000017 represents STATUS_NO_MEMORY. A later attempt reported the following error as well:
0x8007000EThis is E_OUTOFMEMORY. The existing cache.dat was last written during this exhausted period and was substantially smaller than its counterpart on the healthy comparison VM.
The timestamps produce a rather convincing sequence: Exchange exhausted commit memory, Software Protection failed while rebuilding its master policy and the resulting ProductPolicy lacked its normal kernel values. The exact internal write sequence cannot be proven after replacing the corrupted cache, however. This is a high-confidence reconstruction of this incident rather than a generally proven Windows behavior.
The problem also remained hidden until the next reboot. The server kept using all 8 GB after the failed rebuild, while the incomplete policy was already present on disk and within the registry. Only the following boot exposed the 2 GB restriction, which strongly suggests that the running kernel retained the policy loaded during its previous boot.
Step 6: Prepare the server for the repair
We are going to rebuild the Software Protection token store. Resolve the immediate commit-memory pressure first, since repeating the policy rebuild under the same out-of-memory condition rather defeats the purpose.
Open Task Manager and check the first value shown under Memory > Committed. Aim for several gigabytes between the current commit and its limit. On this server, I treated 1,536 MiB as a local stop threshold rather than a recommended target.
This value is not a Microsoft requirement and no fixed amount can guarantee a successful rebuild. Stop memory-heavy applications or enlarge the page file before proceeding.
Now record the installed edition, activation channel, partial product key and license status:
cscript.exe %windir%\System32\slmgr.vbs /dlvMake sure that you have a valid activation path before modifying the store. For a retail or MAK installation, this means having the legitimate product key available. For KMS, install the edition-specific KMS client key and make sure that the KMS host is reachable.
Also create an Exchange-aware backup where applicable. Afterwards, shut the VM down cleanly and take a Proxmox snapshot while it is powered off. A VM snapshot is useful for this repair, but it is not a replacement for an application-aware Exchange backup.
Start Windows normally again, stop the memory-heavy services once more and recheck your commit headroom. Run the repair from an elevated Command Prompt, not from Safe Mode.
The original investigation temporarily involved Safe Mode, but that introduced additional service handling which is neither necessary nor desirable for the actual repair. Microsoft’s regular procedure is intended to run with the Software Protection service available normally.
Step 7: Rebuild the Software Protection token store
Microsoft provides an official procedure for rebuilding the tokens.dat file. The following commands preserve the old token store under a unique timestamped name instead of deleting it. Check the result of every command and stop if one of them fails.
First, stop the Software Protection service:
net stop sppsvcProceed to the Windows Server token-store directory:
cd /d %windir%\System32\spp\store\2.0Choose one unique backup name and record it before continuing. I used the following timestamped name during this repair:
ren tokens.dat tokens.memory-policy-backup-20260812-120000.datAdjust the timestamp for your repair, but use that exact same filename if you need to restore the token store later.
Start the Software Protection service again and reinstall the built-in licensing files:
net start sppsvc
cscript.exe %windir%\System32\slmgr.vbs /rilcThe /rilc command may print a long list of reinstalled license files. This is expected. Verify that a new, nonempty tokens.dat has been created afterwards:
dir %windir%\System32\spp\store\2.0\tokens.dat
dir %windir%\System32\spp\store\2.0\cache\cache.datThe cache may be updated at this point or during the first normal boot. The new tokens.dat, however, must be present before you continue.
Now restore the correct activation state. On my Evaluation installation, the existing license could be reacquired with:
cscript.exe %windir%\System32\slmgr.vbs /atoFor a regular Standard or Datacenter installation, Microsoft instructs you to reinstall the legitimate product key after rebuilding tokens.dat. Install the correct key for your edition and activation channel, then activate it:
cscript.exe %windir%\System32\slmgr.vbs /ipk XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
cscript.exe %windir%\System32\slmgr.vbs /atoUse one of these activation routes, not both. Do not use /upk for this procedure.
Before rebooting, inspect the licensing state again:
cscript.exe %windir%\System32\slmgr.vbs /dlvThe dialog must report License Status: Licensed. If token generation or activation fails, don’t continue with the restarts. The complete recovery path is to restore the powered-off VM snapshot.
If snapshot restoration is unavailable, you can attempt to stop sppsvc, rename the failed tokens.dat, restore tokens.memory-policy-backup-20260812-120000.dat as tokens.dat and start the service again. This is only a best-effort token-file recovery: It does not necessarily reverse changes to cache.dat, installed licensing files, the product key or activation. I have not validated this failure path on an affected clone.
Keep the original token backup and the Proxmox snapshot until you have completed Step 9 and confirmed that licensing remains stable.
Step 8: Reboot Windows twice
Microsoft explicitly requires two restarts after rebuilding tokens.dat. This turned out to be especially relevant for the memory-policy failure.
After the first normal reboot, Windows still reported only 2 GB in my case. This initially looked like a failed repair, but Software Protection then processed the rebuilt token store and published the full 46,156-byte ProductPolicy during that boot.
The most likely explanation is that the running kernel had already initialized with the previous memory policy and could not add the rejected physical range afterwards. The second reboot then allowed the newly generated policy to take effect during startup.
So restart Windows normally once, allow all services to settle and then restart it a second time:
shutdown /r /t 0Run the same command again after Windows has completed its first normal boot. Don’t switch into Safe Mode between these restarts.
The observed behavior matches the policy architecture and Microsoft’s two-restart requirement. The detailed division of work between both boots is based on the policy change and memory mapping observed during this incident, rather than a public Microsoft description of the internal boot sequence.
Step 9: Verify the repaired policy and memory
After the second reboot, run the ProductPolicy check from Step 4 again. The result should now show a substantially larger policy with both kernel values present:
ProductPolicyBytes : 46156
HasX64MemoryPolicy : True
HasProcessorPolicy : TrueAgain, the exact byte count may differ on your installation. The presence of both policy values matters more than the exact byte count.
Now check the physical memory visible to Windows:
systeminfo | findstr /C:"Total Physical Memory"The repaired server returned:
Total Physical Memory: 8,191 MBThe small difference from 8,192 MB is normal reserved memory. Windows’ physical resource map now contained approximately 2 GiB below 4 GiB and the full high-memory interval from 0x100000000 up to, but not including, 0x280000000.
Finally, verify the licensing state:
cscript.exe %windir%\System32\slmgr.vbs /dlvThe server returned to the Licensed state with approximately 178 days left in its Evaluation period. No new Software Protection policy errors appeared after the successful second boot.
Step 10: Prevent the problem from returning
The policy was repaired at this point, but the underlying capacity problem remained. With all 8 GB available, the Exchange server was using approximately:
Committed memory: 14.7 GB
Commit limit: 17.4 GB
Physical memory: 8.0 GB
Page file: 9.2 GBThat leaves rather little room for another memory spike. Eight GB is extremely constrained for this particular Exchange installation, so I increased the VM memory instead of relying on the repaired policy alone. At least 12 GB and preferably 16 GB or more, is a much more sensible allocation here.
Keep the page file system-managed with sufficient free disk space. Monitor Event 2004 alongside Security-SPP Events 1032, 8228 and 8229. Repeated 0x8007000E or 0xD0000017 errors deserve immediate attention, especially before licensing work, Exchange maintenance or cumulative updates.
Finished
There you have it. Proxmox, Q35, SeaBIOS and ballooning were not the reason the VM was missing 6 GB. The evidence indicates that a Software Protection rebuild failed during commit exhaustion, leaving the generated kernel policy incomplete before the next reboot.
Rebuilding tokens.dat, restoring the license and rebooting twice restored both Kernel-WindowsMaxMemAllowedx64 and all 8 GB of RAM. Checking the policy first also prevents this from becoming a dangerous generic fix for every Windows VM which happens to report too little RAM.