You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
WOW64 targets: 64-bit Python hands VirtualQueryEx a 28-byte MEMORY_BASIC_INFORMATION_32, so every query fails (ERROR_BAD_LENGTH) and region enumeration silently degenerates #86
On a 64-bit Python attached to a 32-bit (WOW64) target, GetMemoryRegions selects MEMORY_BASIC_INFORMATION_32 (28 bytes) and passes it to VirtualQueryEx. From a 64-bit caller, VirtualQueryEx requires the caller-native 48-byte MEMORY_BASIC_INFORMATION layout; the 28-byte struct fails with ERROR_BAD_LENGTH (24). The error-handling path treats 24 as a "transient" failure and steps one page forward, so the walk crawls across the whole address space yielding nothing — scans hang effectively forever and return empty results with only DEBUG-level logging.
# functions.pyreturn (
MEMORY_BASIC_INFORMATION_32ifis_wow64.valueelseMEMORY_BASIC_INFORMATION_64
)
...
region=mbi_class()
result=kernel32.VirtualQueryEx(
process_handle,
current_address,
ctypes.byref(region),
ctypes.sizeof(region), # 28 when the target is WOW64 and Python is x64
)
ifresult==0:
err=ctypes.get_last_error()
iferrin (0, 87):
return
...
current_address+=page_size# err=24 lands here: skip one page, continue
Problem
The docstring of mbi_class_for_handle reasons that "the Windows kernel still returns a 32-bit layout via VirtualQueryEx" for WOW64 targets. That is true only for a 32-bit caller. From a 64-bit process, VirtualQueryEx fills the caller-native MEMORY_BASIC_INFORMATION and rejects shorter buffers with ERROR_BAD_LENGTH (24). This is well-documented behavior of the API (the same constraint is why other tooling — e.g. System Informer and mozilla's masche library — enumerates WOW64 targets from x64 callers through the native structure; see also mozilla/masche issue #12 and Stack Overflow question 26767741 describing exactly this failure).
Consequently, on the exact configuration this code exists to support (x64 Python + 32-bit game/process):
Every single VirtualQueryEx call returns 0 with GetLastError() == 24.
24 is not in (0, 87), so the walker logs at DEBUG level and advances one page.
The loop runs to lpMaximumApplicationAddress, i.e. billions of doomed syscalls, while yielding zero regions.
All consumers of GetMemoryRegions are affected: get_memory_regions, value/pattern scans that iterate regions, and snapshots. The user sees an effectively hung scan that eventually returns nothing, without any exception or warning-level log.
Static analysis finding; not executed here. The struct size (28), the dispatch condition, and the skip-and-continue error path are all verbatim from source as quoted above.
Trigger / Reproduction
Run 64-bit Python on 64-bit Windows.
Attach to any 32-bit (WOW64) process.
Call anything that walks regions (e.g. search_by_value, or iterate get_memory_regions).
Expected Behavior
Region enumeration works from a 64-bit Python against WOW64 targets: queries succeed using the native layout (WOW64 addresses fit comfortably in it), producing the target's full region list.
Actual Behavior
Every query fails with error 24; enumeration degenerates into a page-by-page crawl that yields no regions and surfaces no error.
Impact
The flagship use case for this project — analyzing legacy 32-bit games/tools from modern 64-bit Python installs — is fully broken for all region-based features. The failure mode is silent (DEBUG-only logging plus a near-infinite walk), which makes diagnosis very hard for users.
Suggested Direction
From a 64-bit host, always pass the native MEMORY_BASIC_INFORMATION (48-byte) buffer regardless of target bitness — WOW64 target addresses are valid in it. If the 32-bit field widths matter downstream, convert after a successful native-layout query. Alternatively, treat ERROR_BAD_LENGTH as a hard configuration error rather than a transient skip, so at minimum users get a loud failure instead of a silent empty scan.
Summary
On a 64-bit Python attached to a 32-bit (WOW64) target,
GetMemoryRegionsselectsMEMORY_BASIC_INFORMATION_32(28 bytes) and passes it toVirtualQueryEx. From a 64-bit caller,VirtualQueryExrequires the caller-native 48-byteMEMORY_BASIC_INFORMATIONlayout; the 28-byte struct fails withERROR_BAD_LENGTH(24). The error-handling path treats 24 as a "transient" failure and steps one page forward, so the walk crawls across the whole address space yielding nothing — scans hang effectively forever and return empty results with only DEBUG-level logging.Location
PyMemoryEditor/win32/functions.pymbi_class_for_handle,GetMemoryRegions,MEMORY_BASIC_INFORMATION_32(PyMemoryEditor/win32/types.py)Problem
The docstring of
mbi_class_for_handlereasons that "the Windows kernel still returns a 32-bit layout via VirtualQueryEx" for WOW64 targets. That is true only for a 32-bit caller. From a 64-bit process,VirtualQueryExfills the caller-nativeMEMORY_BASIC_INFORMATIONand rejects shorter buffers withERROR_BAD_LENGTH(24). This is well-documented behavior of the API (the same constraint is why other tooling — e.g. System Informer and mozilla's masche library — enumerates WOW64 targets from x64 callers through the native structure; see also mozilla/masche issue #12 and Stack Overflow question 26767741 describing exactly this failure).Consequently, on the exact configuration this code exists to support (x64 Python + 32-bit game/process):
VirtualQueryExcall returns 0 withGetLastError() == 24.(0, 87), so the walker logs at DEBUG level and advances one page.lpMaximumApplicationAddress, i.e. billions of doomed syscalls, while yielding zero regions.All consumers of
GetMemoryRegionsare affected:get_memory_regions, value/pattern scans that iterate regions, and snapshots. The user sees an effectively hung scan that eventually returns nothing, without any exception or warning-level log.Static analysis finding; not executed here. The struct size (28), the dispatch condition, and the skip-and-continue error path are all verbatim from source as quoted above.
Trigger / Reproduction
search_by_value, or iterateget_memory_regions).Expected Behavior
Region enumeration works from a 64-bit Python against WOW64 targets: queries succeed using the native layout (WOW64 addresses fit comfortably in it), producing the target's full region list.
Actual Behavior
Every query fails with error 24; enumeration degenerates into a page-by-page crawl that yields no regions and surfaces no error.
Impact
The flagship use case for this project — analyzing legacy 32-bit games/tools from modern 64-bit Python installs — is fully broken for all region-based features. The failure mode is silent (DEBUG-only logging plus a near-infinite walk), which makes diagnosis very hard for users.
Suggested Direction
From a 64-bit host, always pass the native
MEMORY_BASIC_INFORMATION(48-byte) buffer regardless of target bitness — WOW64 target addresses are valid in it. If the 32-bit field widths matter downstream, convert after a successful native-layout query. Alternatively, treatERROR_BAD_LENGTHas a hard configuration error rather than a transient skip, so at minimum users get a loud failure instead of a silent empty scan.Evidence
win32/types.py:MEMORY_BASIC_INFORMATION_32._fields_= 7×DWORD⇒sizeof == 28.win32/functions.py::mbi_class_for_handle: returns the 32-bit layout precisely when a 64-bit host detects a WOW64 target.win32/functions.py::GetMemoryRegions: passesctypes.sizeof(region)and, forresult == 0witherr not in (0, 87), advances one page and continues.wow64,VirtualQueryEx,MEMORY_BASIC_INFORMATION,IsWow64Process,32-bit target) found no prior report; closed issue Skips some memory regions successfully read by Cheat Engine on Windows #9 concerns region filtering, unrelated.