[PATCH v3 0/2] MR11600: ntdll: Handle the MEM_LARGE_PAGES and MEM_PHYSICAL allocation flags.
The flag combination shows up in Forza Motorsport, which allocates a 14 MB staging pool with MEM_COMMIT|MEM_RESERVE|MEM_PHYSICAL|MEM_LARGE_PAGES and does not check the result, so under Wine it runs on a NULL pointer. Behaviour measured on real Windows with the Test Bot (job 163938, `ntdll_test.exe virtual`, 64 bit, SeLockMemoryPrivilege not held on any VM): | type | Win7 | Win8.1 | 10-1507 | 10-22H2 | Win11 | Wine before | |---|---|---|---|---|---|---| | `0x3000` COMMIT\|RESERVE | 0 | 0 | 0 | 0 | 0 | 0 | | `0x20003000` +LARGE | c0000061 | c0000061 | c0000061 | c0000061 | c0000061 | c000000d | | `0x402000` RESERVE\|PHYSICAL, RW | 0 | 0 | 0 | 0 | 0 | c000000d | | `0x402000` RESERVE\|PHYSICAL, EXEC | - | c00000f4 | c00000f4 | c0000045 | c0000045 | c000000d | | `0x403000` COMMIT\|RESERVE\|PHYSICAL | - | c00000f3 | c00000f3 | c000000d | c000000d | c000000d | | `0x20403000` PHYSICAL\|LARGE | c00000f3 | c00000f3 | c00000f3 | 0 | 0 | c000000d | | `0x80003000` +4MB_PAGES | - | c00000f3 | c00000f3 | c000000d | c000000d | c000000d | Two things the patch deliberately does not do: * It does not implement the large page size alignment rule. An unaligned request now returns STATUS_PRIVILEGE_NOT_HELD where 10-22H2 and 11 return STATUS_INVALID_PARAMETER, because <= 10-1507 check the privilege first and the later versions check the alignment first. The test traces that case but does not assert it. * AWE itself stays unimplemented. The reservation succeeds and AllocateUserPhysicalPages() still fails with its existing FIXME, instead of the reservation failing with a status that says the arguments were wrong. Verified in three states: the tests alone on Wine report the five assertions as todo with no failures, the same binary passes on the seven Windows VMs above, and tests plus fix report no todo and no failures. -- v3: ntdll: Handle the MEM_LARGE_PAGES and MEM_PHYSICAL allocation flags. ntdll/tests: Test MEM_LARGE_PAGES and MEM_PHYSICAL allocation flags. https://gitlab.winehq.org/wine/wine/-/merge_requests/11600
From: Allan Vester <vesterallan246@gmail.com> NtAllocateVirtualMemory() and NtAllocateVirtualMemoryEx() validate their type argument against a mask containing neither bit, so an allocation carrying either one is refused with STATUS_INVALID_PARAMETER before the size, the protection or the caller's privileges are looked at. --- dlls/ntdll/tests/virtual.c | 165 +++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/dlls/ntdll/tests/virtual.c b/dlls/ntdll/tests/virtual.c index 5d34639cf7c..fb1214aebd9 100644 --- a/dlls/ntdll/tests/virtual.c +++ b/dlls/ntdll/tests/virtual.c @@ -1011,6 +1011,170 @@ static void test_NtAllocateVirtualMemoryEx_address_requirements(void) ok(status == STATUS_INVALID_PARAMETER, "Unexpected status %08lx.\n", status); } +static BOOL enable_privilege( const char *name ) +{ + TOKEN_PRIVILEGES privs; + HANDLE token; + BOOL ret; + + if (!OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token )) return FALSE; + + privs.PrivilegeCount = 1; + privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + if (!(ret = LookupPrivilegeValueA( NULL, name, &privs.Privileges[0].Luid ))) + { + CloseHandle( token ); + return FALSE; + } + SetLastError( 0xdeadbeef ); + ret = AdjustTokenPrivileges( token, FALSE, &privs, sizeof(privs), NULL, NULL ) && + GetLastError() == ERROR_SUCCESS; + CloseHandle( token ); + return ret; +} + +static NTSTATUS alloc_and_free( SIZE_T size, ULONG type, ULONG protect, BOOL use_ex ) +{ + NTSTATUS status, ret; + void *addr = NULL; + + if (use_ex) + status = pNtAllocateVirtualMemoryEx( NtCurrentProcess(), &addr, &size, type, protect, NULL, 0 ); + else + status = NtAllocateVirtualMemory( NtCurrentProcess(), &addr, 0, &size, type, protect ); + + if (!status) + { + size = 0; + ret = NtFreeVirtualMemory( NtCurrentProcess(), &addr, &size, MEM_RELEASE ); + ok( !ret, "NtFreeVirtualMemory returned %08lx\n", ret ); + } + return status; +} + +/* MEM_LARGE_PAGES and MEM_PHYSICAL are absent from the type masks that + * NtAllocateVirtualMemory[Ex] validate against, so Wine refuses any allocation + * carrying either bit with STATUS_INVALID_PARAMETER. Windows tells the cases + * apart: an AWE reservation succeeds, a large page request without + * SeLockMemoryPrivilege fails on the privilege, and the two combined succeed. + */ +static void test_large_pages(void) +{ + static const char *const api_name[2] = { "NtAllocateVirtualMemory ", "NtAllocateVirtualMemoryEx" }; + SIZE_T (WINAPI *pGetLargePageMinimum)(void); + SIZE_T large_min, large_size; + NTSTATUS status, status_ex; + BOOL have_privilege; + unsigned int i, j; + + pGetLargePageMinimum = (void *)GetProcAddress( GetModuleHandleA("kernel32.dll"), "GetLargePageMinimum" ); + if (!pGetLargePageMinimum) + { + win_skip( "GetLargePageMinimum is not available\n" ); + return; + } + large_min = pGetLargePageMinimum(); + have_privilege = enable_privilege( "SeLockMemoryPrivilege" ); + + /* 0xe00000 is the size an affected application asks for; it is already a + * multiple of the 2MB minimum, but do not assume that minimum. */ + large_size = large_min ? (0xe00000 + large_min - 1) & ~(large_min - 1) : 0xe00000; + + trace( "large page minimum %#Ix, allocation size %#Ix, SeLockMemoryPrivilege %s\n", + large_min, large_size, have_privilege ? "enabled" : "not held" ); + + { + const struct + { + const char *name; + SIZE_T size; + ULONG type; + ULONG protect; + } + tests[] = + { + { "COMMIT|RESERVE (control)", large_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE }, + { "COMMIT|RESERVE|LARGE", large_size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE }, + { "RESERVE|LARGE", large_size, MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE }, + { "COMMIT|LARGE", large_size, MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE }, + { "COMMIT|RESERVE|LARGE, unaligned", page_size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE }, + { "COMMIT|RESERVE|LARGE, exec prot", large_size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_EXECUTE_READWRITE }, + { "RESERVE|PHYSICAL (AWE)", large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE }, + { "RESERVE|PHYSICAL, exec prot", large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_EXECUTE_READWRITE }, + { "COMMIT|RESERVE|PHYSICAL", large_size, MEM_COMMIT | MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE }, + /* 0x20403000, the combination an affected application passes */ + { "COMMIT|RESERVE|PHYSICAL|LARGE", large_size, MEM_COMMIT | MEM_RESERVE | MEM_PHYSICAL | MEM_LARGE_PAGES, PAGE_READWRITE }, + { "COMMIT|RESERVE|4MB_PAGES", large_size, MEM_COMMIT | MEM_RESERVE | MEM_4MB_PAGES, PAGE_READWRITE }, + }; + + for (i = 0; i < ARRAY_SIZE(tests); i++) + { + NTSTATUS results[2]; + + winetest_push_context( "%s", tests[i].name ); + for (j = 0; j < 2; j++) + { + if (j && !pNtAllocateVirtualMemoryEx) + { + results[j] = results[0]; + continue; + } + results[j] = alloc_and_free( tests[i].size, tests[i].type, tests[i].protect, j ); + trace( "%s size %#Ix type %#lx prot %#lx -> %08lx\n", api_name[j], + tests[i].size, tests[i].type, tests[i].protect, results[j] ); + } + ok( results[0] == results[1], "Nt %08lx and NtEx %08lx disagree\n", results[0], results[1] ); + winetest_pop_context(); + } + } + + /* An ordinary allocation of the same size must work, so nothing below can be + * blamed on the size or on memory pressure. */ + status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE, FALSE ); + ok( !status, "MEM_COMMIT|MEM_RESERVE returned %08lx\n", status ); + + /* Reserving an AWE region needs no privilege: MEM_RESERVE on its own, and + * PAGE_READWRITE on its own. */ + status = alloc_and_free( large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE, FALSE ); + todo_wine ok( !status, "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status ); + if (pNtAllocateVirtualMemoryEx) + { + status_ex = alloc_and_free( large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE, TRUE ); + todo_wine ok( !status_ex, "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status_ex ); + } + + status = alloc_and_free( large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_EXECUTE_READWRITE, FALSE ); + todo_wine ok( status == STATUS_INVALID_PAGE_PROTECTION || + broken( status == STATUS_INVALID_PARAMETER_6 ) /* <= win10v1507 */, + "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status ); + + status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE, FALSE ); + ok( status == STATUS_INVALID_PARAMETER || + broken( status == STATUS_INVALID_PARAMETER_5 ) /* <= win10v1507 */, + "MEM_COMMIT|MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status ); + + /* Combined with MEM_LARGE_PAGES the commit is accepted, and still without + * needing a privilege. */ + status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_PHYSICAL | MEM_LARGE_PAGES, + PAGE_READWRITE, FALSE ); + todo_wine ok( !status || broken( status == STATUS_INVALID_PARAMETER_5 ) /* <= win10v1507 */, + "MEM_COMMIT|MEM_RESERVE|MEM_PHYSICAL|MEM_LARGE_PAGES returned %08lx\n", status ); + + /* Large pages on their own need SeLockMemoryPrivilege. Without it the + * failure is about the privilege, not about the arguments. */ + if (!have_privilege) + { + status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE, FALSE ); + todo_wine ok( status == STATUS_PRIVILEGE_NOT_HELD, + "MEM_COMMIT|MEM_RESERVE|MEM_LARGE_PAGES returned %08lx\n", status ); + } + + status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_4MB_PAGES, PAGE_READWRITE, FALSE ); + ok( status == STATUS_INVALID_PARAMETER || + broken( status == STATUS_INVALID_PARAMETER_5 ) /* <= win10v1507 */, + "MEM_COMMIT|MEM_RESERVE|MEM_4MB_PAGES returned %08lx\n", status ); +} + struct test_stack_size_thread_args { DWORD expect_committed; @@ -3602,6 +3766,7 @@ START_TEST(virtual) test_NtAllocateVirtualMemory(); test_NtAllocateVirtualMemoryEx(); test_NtAllocateVirtualMemoryEx_address_requirements(); + test_large_pages(); test_NtFreeVirtualMemory(); test_NtProtectVirtualMemory(); test_RtlCreateUserStack(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11600
From: Allan Vester <vesterallan246@gmail.com> Both bits were missing from the type masks, so any allocation carrying one was refused with STATUS_INVALID_PARAMETER. Windows reserves address space for MEM_PHYSICAL, fails a large page request that does not hold SeLockMemoryPrivilege on the privilege, and accepts the two combined. Neither large pages nor AWE are implemented, so the flags are validated and then dropped. --- dlls/ntdll/tests/virtual.c | 18 +++++++++--------- dlls/ntdll/unix/virtual.c | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/dlls/ntdll/tests/virtual.c b/dlls/ntdll/tests/virtual.c index fb1214aebd9..30a387a6c20 100644 --- a/dlls/ntdll/tests/virtual.c +++ b/dlls/ntdll/tests/virtual.c @@ -1136,17 +1136,17 @@ static void test_large_pages(void) /* Reserving an AWE region needs no privilege: MEM_RESERVE on its own, and * PAGE_READWRITE on its own. */ status = alloc_and_free( large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE, FALSE ); - todo_wine ok( !status, "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status ); + ok( !status, "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status ); if (pNtAllocateVirtualMemoryEx) { status_ex = alloc_and_free( large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE, TRUE ); - todo_wine ok( !status_ex, "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status_ex ); + ok( !status_ex, "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status_ex ); } status = alloc_and_free( large_size, MEM_RESERVE | MEM_PHYSICAL, PAGE_EXECUTE_READWRITE, FALSE ); - todo_wine ok( status == STATUS_INVALID_PAGE_PROTECTION || - broken( status == STATUS_INVALID_PARAMETER_6 ) /* <= win10v1507 */, - "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status ); + ok( status == STATUS_INVALID_PAGE_PROTECTION || + broken( status == STATUS_INVALID_PARAMETER_6 ) /* <= win10v1507 */, + "MEM_RESERVE|MEM_PHYSICAL returned %08lx\n", status ); status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_PHYSICAL, PAGE_READWRITE, FALSE ); ok( status == STATUS_INVALID_PARAMETER || @@ -1157,16 +1157,16 @@ static void test_large_pages(void) * needing a privilege. */ status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_PHYSICAL | MEM_LARGE_PAGES, PAGE_READWRITE, FALSE ); - todo_wine ok( !status || broken( status == STATUS_INVALID_PARAMETER_5 ) /* <= win10v1507 */, - "MEM_COMMIT|MEM_RESERVE|MEM_PHYSICAL|MEM_LARGE_PAGES returned %08lx\n", status ); + ok( !status || broken( status == STATUS_INVALID_PARAMETER_5 ) /* <= win10v1507 */, + "MEM_COMMIT|MEM_RESERVE|MEM_PHYSICAL|MEM_LARGE_PAGES returned %08lx\n", status ); /* Large pages on their own need SeLockMemoryPrivilege. Without it the * failure is about the privilege, not about the arguments. */ if (!have_privilege) { status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE, FALSE ); - todo_wine ok( status == STATUS_PRIVILEGE_NOT_HELD, - "MEM_COMMIT|MEM_RESERVE|MEM_LARGE_PAGES returned %08lx\n", status ); + ok( status == STATUS_PRIVILEGE_NOT_HELD, + "MEM_COMMIT|MEM_RESERVE|MEM_LARGE_PAGES returned %08lx\n", status ); } status = alloc_and_free( large_size, MEM_COMMIT | MEM_RESERVE | MEM_4MB_PAGES, PAGE_READWRITE, FALSE ); diff --git a/dlls/ntdll/unix/virtual.c b/dlls/ntdll/unix/virtual.c index db820a89525..7aba549d081 100644 --- a/dlls/ntdll/unix/virtual.c +++ b/dlls/ntdll/unix/virtual.c @@ -5239,6 +5239,29 @@ static NTSTATUS allocate_virtual_memory( void **ret, SIZE_T *size_ptr, ULONG typ } +/*********************************************************************** + * check_large_page_type + * + * Validate MEM_LARGE_PAGES and MEM_PHYSICAL and remove them from the type. + * Neither large pages nor AWE are implemented, but an AWE reservation is + * ordinary address space, and MEM_LARGE_PAGES is only a page size request. + */ +static NTSTATUS check_large_page_type( ULONG *type, ULONG protect ) +{ + if (*type & MEM_PHYSICAL) + { + /* the physical pages themselves are not implemented, see AllocateUserPhysicalPages() */ + if (!(*type & MEM_LARGE_PAGES) && (*type & MEM_COMMIT)) return STATUS_INVALID_PARAMETER; + if (protect != PAGE_READWRITE) return STATUS_INVALID_PAGE_PROTECTION; + } + /* large pages require SeLockMemoryPrivilege, which we never grant */ + else if (*type & MEM_LARGE_PAGES) return STATUS_PRIVILEGE_NOT_HELD; + + *type &= ~(MEM_PHYSICAL | MEM_LARGE_PAGES); + return STATUS_SUCCESS; +} + + /*********************************************************************** * NtAllocateVirtualMemory (NTDLL.@) * ZwAllocateVirtualMemory (NTDLL.@) @@ -5246,7 +5269,9 @@ static NTSTATUS allocate_virtual_memory( void **ret, SIZE_T *size_ptr, ULONG typ NTSTATUS WINAPI NtAllocateVirtualMemory( HANDLE process, PVOID *ret, ULONG_PTR zero_bits, SIZE_T *size_ptr, ULONG type, ULONG protect ) { - static const ULONG type_mask = MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN | MEM_WRITE_WATCH | MEM_RESET; + static const ULONG type_mask = MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN | MEM_WRITE_WATCH + | MEM_RESET | MEM_PHYSICAL | MEM_LARGE_PAGES; + unsigned int status; ULONG_PTR limit; TRACE("%p %p %08lx %x %08x\n", process, *ret, *size_ptr, type, protect ); @@ -5258,12 +5283,12 @@ NTSTATUS WINAPI NtAllocateVirtualMemory( HANDLE process, PVOID *ret, ULONG_PTR z if (!is_old_wow64() && zero_bits >= 32) return STATUS_INVALID_PARAMETER_3; #endif if (type & ~type_mask) return STATUS_INVALID_PARAMETER; + if ((status = check_large_page_type( &type, protect ))) return status; if (process != NtCurrentProcess()) { union apc_call call; union apc_result result; - unsigned int status; memset( &call, 0, sizeof(call) ); @@ -5386,7 +5411,8 @@ NTSTATUS WINAPI NtAllocateVirtualMemoryEx( HANDLE process, PVOID *ret, SIZE_T *s ULONG count ) { static const ULONG type_mask = MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN | MEM_WRITE_WATCH - | MEM_RESET | MEM_RESERVE_PLACEHOLDER | MEM_REPLACE_PLACEHOLDER; + | MEM_RESET | MEM_RESERVE_PLACEHOLDER | MEM_REPLACE_PLACEHOLDER + | MEM_PHYSICAL | MEM_LARGE_PAGES; ULONG_PTR limit_low = 0; ULONG_PTR limit_high = 0; ULONG_PTR align = 0; @@ -5402,6 +5428,7 @@ NTSTATUS WINAPI NtAllocateVirtualMemoryEx( HANDLE process, PVOID *ret, SIZE_T *s if (status) return status; if (type & ~type_mask) return STATUS_INVALID_PARAMETER; + if ((status = check_large_page_type( &type, protect ))) return status; if (*ret && (align || limit_low || limit_high)) return STATUS_INVALID_PARAMETER; if (!*size_ptr) return STATUS_INVALID_PARAMETER; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11600
Alexandre Julliard (@julliard) commented about dlls/ntdll/tests/virtual.c:
+ + if (!status) + { + size = 0; + ret = NtFreeVirtualMemory( NtCurrentProcess(), &addr, &size, MEM_RELEASE ); + ok( !ret, "NtFreeVirtualMemory returned %08lx\n", ret ); + } + return status; +} + +/* MEM_LARGE_PAGES and MEM_PHYSICAL are absent from the type masks that + * NtAllocateVirtualMemory[Ex] validate against, so Wine refuses any allocation + * carrying either bit with STATUS_INVALID_PARAMETER. Windows tells the cases + * apart: an AWE reservation succeeds, a large page request without + * SeLockMemoryPrivilege fails on the privilege, and the two combined succeed. + */ It's not useful to describe Wine's implementation here, particularly since that's what you are trying to fix. Was this LLM-generated by any chance?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11600#note_149153
On Tue Aug 18 10:54:38 2026 +0000, Alexandre Julliard wrote:
It's not useful to describe Wine's implementation here, particularly since that's what you are trying to fix. Was this LLM-generated by any chance? Yes. I did use Claude to write the test and the fix. I hadn't checked the guidelines on LLM-use before submitting, which was my mistake.
The finding itself isn't LLM-generated however. The Windows behaviour comes from a TestBot job I submitted (163938), across Win7 through Win11 21H2. I hit this flag combination because Forza Motorsport allocates a staging pool with `MEM_COMMIT|MEM_RESERVE|MEM_PHYSICAL|MEM_LARGE_PAGES` and doesn't check the result, so no Microsoft source, headers or disassembly were involved. If the fix is still useful, I can go ahead and remove the comments. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11600#note_149272
On Wed Aug 19 16:01:53 2026 +0000, Allan Vester wrote:
Yes. I did use Claude to write the test and the fix. I hadn't checked the guidelines on LLM-use before submitting, which was my mistake. The finding itself isn't LLM-generated however. The Windows behaviour comes from a TestBot job I submitted (163938), across Win7 through Win11 21H2. I hit this flag combination because Forza Motorsport allocates a staging pool with `MEM_COMMIT|MEM_RESERVE|MEM_PHYSICAL|MEM_LARGE_PAGES` and doesn't check the result, so no Microsoft source, headers or disassembly were involved. If the fix is still useful, I can go ahead and remove the comments. The implementation of the fix isn't so great either, I've created !11728 as an alternative. Does it work for you?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11600#note_149526
participants (3)
-
Alexandre Julliard (@julliard) -
Allan Vester -
Allan Vester (@AllanVester)