-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix zend_alloc aligned allocation on Windows #9721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
c0580d7
to
d6fc341
Compare
Please target the "master" branch. |
d6fc341
to
d16b939
Compare
d16b939
to
f708b7c
Compare
f708b7c
to
d1dde41
Compare
So if VirtualAlloc fails on win32 when realigning the pointer, was your plan to allocate a larger block of size EDIT: I assume it's doing that but still failing - I assume the call to zend_mm_mmap_fixed is entirely within the region that was just unmapped ptr = zend_mm_mmap(size + alignment - REAL_PAGE_SIZE);
#ifdef _WIN32
offset = ZEND_MM_ALIGNED_OFFSET(ptr, alignment);
zend_mm_munmap(ptr, size + alignment - REAL_PAGE_SIZE); #define ZEND_MM_CHUNK_SIZE ((size_t) (2 * 1024 * 1024)) /* 2 MB */
This is a bug fix for a random crash due to getting unlucky allocating a memory page that affects 32-bit windows, so my preference would be targeting 8.0 myself. Maybe
|
https://stackoverflow.com/a/14816824 Are we sure that the page size is actually 4KB at runtime in appveyor? Maybe VirtualAlloc is somehow using a large page size for VirtualAlloc, and as a result it's rounding up to the next 8KB, and as a result it's more likely to request mmap_fixed after the end of a region https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsysteminfo can be called to check the page size at runtime
|
Right. If there is a real bug, it should be fixed in PHP-8.0. But a sloppy error reporting doesn't look like a real bug to me. |
More brainstorming: https://github.com/microsoft/mimalloc/blob/master/src/os.c#L411-L422 https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc2 - it seems like VirtualAlloc2 supports huge pages as well as passing the requested alignment to the VirtualAlloc2 call itself, but only works in windows 10+ (that seems too new, but it's useful to know about) Zend/zend.c already defines a helper to get the page size - |
I did some more debugging on x86 Win build:
syntax is:
PHP zend_alloc is extending the memory by passing the next address and tries to allocate it. This have at least two problems: 1. invalid address can be passed if the previous virtual address was on the very end of the valid spacelike passing value greater than 0x7FFFFFFF in x86 Win build 2. if allocation fails, php currently tries to free the memory (ptr passed to the alloc func), this is wrong, as passed pointer might be not previously allocated (as shown in the log, in the code, we must pass an extra arg to
|
From the trace you posted, it looks like the free then alloc is
So the surprise seems to be that calling VirtualFree then immediately calling VirtualAlloc on a region entirely within the memory freed by VirtualFree doesn't always work. Brainstorming:
https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces - each process's virtual memory space is distinct, so it probably isn't the action of other processes Calling VirtualFree with null seems like it would have no user impact, but could be fixed regardless It'd be useful to have to get the actual error code from VirtualAlloc instead of the 0x000001e7 from VirtualFree for |
My best idea for a workaround is to retry the entire request for aligned memory (alloc with extra + free + alloc aligned at pointer) on win32 if it fails, a finite number of times (in case there's actually no memory or another error)
E.g. 10 times. If this bug (race condition?) is only encountered 1 in 10000 times independently (not actually independent), then 1 in 1000010 chance of it happening would be a better user experience If that fails, then return a fatal error like it did before for being unable to allocate memory For Windows 10, https://github.com/microsoft/mimalloc/blob/master/src/os.c# has an example of calling GetProcAddress to look up the function pointer of VirtualAlloc2, which lets you pass in the alignment hint to the allocator
|
it is a "my custom debug log" from zend_alloc, so there are/were some php ops between
so this is thankfully not needed
comes from linux has same problem, free may be called on data php did not allocate I am understanding this problem, so I will soon, then we can discuss if there is anything non-bug related to improve
memory addresses are randomized even within a single process, and some addresses are reserved for dll, exe itself, ... there are definitely some addresses taken |
Thank you! (Although I don't think that 32bit Windows builds are important; does anybody still use them?)
That looks fishy. Why would we want to allocate zero bytes in the first place? And I wouldn't be surprised that nothing is allocated in this case, so trying to free should fail. |
I do not use 32 bit builds ATM neither, but I belive 32 bit builds are important to discover issues like these, which are present also on 64 bit builds but are much harder to debug because they are much more rare.
see "syntax is", the middle value is result of VirtualAlloc, all ok, we only need to handle it :) |
what looks a little fishy to me is |
Yes, that's deliberate - a block of memory of size 2MB + 2MB - 4kb (size + alignment - REAL_PAGE_SIZE is guaranteed to have one region that's of size exactly 2MB if the allocator aligns to the 4kb page size
|
d1dde41
to
35a9eeb
Compare
If it's possible to fix a problem with 1 screen diff. It doesn't make a lot of sense to do the same with 10 times bigger patch. (introducing functions used once, reordering them and so on). The less - the better, especial if we target this into an old stable version and don't like to introduce new problems. |
@dstogov for Windows the allocation logic is changed and improved, for example we do not try to allocate the size first in hope it will be aligned, because in almost all cases it is not aligned, we also do not need the problematic code with race condition for x64 (for x86 it is there too utilize the virtual address space more dense) the static methods for Windows are better then using so the proposed changes should be about minimal |
@mvorisek in case your patch would clean, I would approve it on last week. Today I spent an hour trying to read it, understand all the changes and don't miss anything. I failed. I understand, it's clear for you, because you already worked on it for a while, but for others it would be easier to rewrite this (without noise).
I may still misunderstand some details of your patch |
56f70f5
to
a64231a
Compare
I did the refactoring as the new fixed code can be then reviewed much more easier. The 1st commit can be reviewed by moving the methods locally in text editor and then comparing the result. There are very few changes actually.
There are static methods:
It is handled the same, only on x86 we try allocation subject to race condition first. If it fails, we continue with unified x64/x86 allocation.
It is needed, as the commited memory start can be different vs. the reserved memory start. To release all/reserved memory, we need to query Please review the commits separately, the 1st one is a preparation for the 2nd one, 1st one can be reviewed easily, 2nd one should be obvious quickly. |
Zend/zend_alloc.c
Outdated
MEMORY_BASIC_INFORMATION mbi; | ||
if (VirtualQuery(addr, &mbi, sizeof(mbi)) == 0) { | ||
#if ZEND_MM_ERROR | ||
stderr_last_error("VirtualQuery() failed"); | ||
#endif | ||
} | ||
addr = mbi.AllocationBase; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that this VirtualQuery() is need to handle unaligned reserved blocks, but it's better to reserve aligned block in first place. I don't know where you find this trick. Is it recommended by MSDN?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I came to the solution after huge research, all Windows memory allocators are wrappers around VirtualAlloc/VirtualFree/VirtualQuery API
5a99eee
to
09fd584
Compare
4031b1c
to
93b71b8
Compare
I have reworked this PR to be a minimalistic bugfix and a fallback solution only when #9650 is hit. When #9650 is hit, the allocated memory might be up to the alignment size larger, but such problem is hit in about 0.01% alloc calls on x86 Windows only, so it should be fine. For easier review, please follow the individual commits, if possible, please keep the commits unsquashed when merging. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The patch might be smaller, but now it's readable and looks right.
(I didn't test, just looked over the code).
Zend/zend_alloc.c
Outdated
offset = ZEND_MM_ALIGNED_OFFSET(ptr, alignment); | ||
if (offset != 0) { | ||
zend_mm_munmap(ptr, size); | ||
return NULL; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this check is necessary, because you already request the fixed aligned segment. Anyway, this is not a big problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed - good catch, this was there originally, but it will never be executed, also there is ZEND_ASSERT(ptr == addr)
newly in zend_mm_mmap_fixed
Zend/zend_alloc.c
Outdated
{ | ||
#ifdef _WIN32 | ||
MEMORY_BASIC_INFORMATION mbi; | ||
if (VirtualQuery(addr, &mbi, sizeof(mbi)) == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may make sense to call VirtualFree() first and than fall-back to VirtualQuery()+VirtualFree() like mi_os_mem_free().
See https://github.com/microsoft/mimalloc/blob/v2.0.6/src/os.c#L359..L372
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am aware they do it this way, but I query VirtualQuery
always to prevent the modification of global error code (err
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now the allocation of "unaligned" block occurs as a fall-back for case when aligned block can't be allocated. I suspect we will have more aligned blocks then unaligned and therefore single VirualFree() for the most often case might be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cmb69 could you please take a final look and merge this
3c090bd
to
817fd19
Compare
Thank you all! |
originally, on Windows, the memory was aligned by an assumption the original memory is available for immediately following allocation
even if the aligned allocation was inside the original address space, it seems this assumption is not always correct and the aligned allocation might fail, especially in 32-builds where is address space is much smaller
fixes #9650 and https://bugs.php.net/bug.php?id=77194
also no need to dump
errno
on linux twice, it is dumped already when reporting lower level alloc failures (mmap, nmmap, munmap)