mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(update): drop the GetNativeSystemInfo probe that lies under WoA emulation
The residual Windows-on-ARM integrity-gate fix added GetNativeSystemInfo as a second "OS-native" probe behind IsWow64Process2. Microsoft documents the opposite behavior: "the API GetNativeSystemInfo also returns emulated processor details when run from an app under emulation." On the exact hosts the probe was added to rescue -- an x64 hermes-setup.exe emulated on an ARM64 Surface -- it therefore reports AMD64, making it a duplicate of the PROCESSOR_ARCHITECTURE rung already below it rather than a fallback for it. Its test only passed because the kernel32 fake was hand-fed ARM64, asserting behavior real Windows does not exhibit. Replace it with GetMachineTypeAttributes, which answers the question the gate actually asks -- "can this host load a PE of machine X?" -- instead of inferring it from an architecture name. It is also the only documented API that reports AMD64-on-ARM64 emulation support. The gate prefers it and falls back to the existing name-based mapping on pre-Windows-11 hosts. The load-bearing half of the previous fix (typing GetCurrentProcess as HANDLE so IsWow64Process2 stops failing ERROR_INVALID_HANDLE) is unchanged. Co-authored-by: xxxigm <xxxigm@users.noreply.github.com> Co-authored-by: Teknium <teknium1@users.noreply.github.com>
This commit is contained in:
parent
a606d24cf2
commit
c03a977a96
2 changed files with 106 additions and 87 deletions
|
|
@ -5556,26 +5556,15 @@ _PE_MACHINE_NAMES = {
|
|||
_PE_MACHINE_ARM64: "ARM64",
|
||||
}
|
||||
|
||||
# SYSTEM_INFO.wProcessorArchitecture values (winnt.h). Used by
|
||||
# GetNativeSystemInfo as a second OS-native probe when IsWow64Process2 is
|
||||
# unavailable or fails under a mistyped ctypes HANDLE.
|
||||
_PROCESSOR_ARCHITECTURE_INTEL = 0
|
||||
_PROCESSOR_ARCHITECTURE_ARM = 5
|
||||
_PROCESSOR_ARCHITECTURE_AMD64 = 9
|
||||
_PROCESSOR_ARCHITECTURE_ARM64 = 12
|
||||
|
||||
_PE_MACHINE_TO_NAME = {
|
||||
_PE_MACHINE_ARM64: "ARM64",
|
||||
_PE_MACHINE_AMD64: "AMD64",
|
||||
_PE_MACHINE_I386: "X86",
|
||||
}
|
||||
|
||||
_SYSTEM_INFO_ARCH_TO_NAME = {
|
||||
_PROCESSOR_ARCHITECTURE_ARM64: "ARM64",
|
||||
_PROCESSOR_ARCHITECTURE_AMD64: "AMD64",
|
||||
_PROCESSOR_ARCHITECTURE_INTEL: "X86",
|
||||
_PROCESSOR_ARCHITECTURE_ARM: "ARM",
|
||||
}
|
||||
# MACHINE_ATTRIBUTES bits (processthreadsapi.h). UserEnabled means the host
|
||||
# can run user-mode code of that machine type — natively or under emulation.
|
||||
_MACHINE_ATTRIBUTE_USER_ENABLED = 0x00000001
|
||||
|
||||
|
||||
def _windows_native_machine_from_iswow64() -> Optional[str]:
|
||||
|
|
@ -5615,39 +5604,37 @@ def _windows_native_machine_from_iswow64() -> Optional[str]:
|
|||
return _PE_MACHINE_TO_NAME.get(native_machine.value)
|
||||
|
||||
|
||||
def _windows_native_machine_from_native_system_info() -> Optional[str]:
|
||||
"""GetNativeSystemInfo — truthful under x64-on-ARM64 when IsWow64 fails.
|
||||
def _windows_user_runnable_pe_machines() -> Optional[set]:
|
||||
"""PE machines this host can run in user mode, via GetMachineTypeAttributes.
|
||||
|
||||
Unlike ``PROCESSOR_ARCHITECTURE`` (process/emulation view) this reports
|
||||
the OS-native architecture, so it covers the residual #71218 failure mode
|
||||
where ``IsWow64Process2`` returns FALSE and the env-var fallback would
|
||||
otherwise lie as AMD64.
|
||||
This asks the question the integrity gate actually cares about — "can this
|
||||
Windows host load a PE of machine X?" — instead of inferring it from a
|
||||
host-architecture name. It is also the only documented API that reports
|
||||
AMD64-on-ARM64 emulation support; ``IsWow64GuestMachineSupported`` only
|
||||
answers for 32-bit guests.
|
||||
|
||||
Returns None when the API is unavailable (pre-Windows-11 build 22000) or
|
||||
reports nothing runnable, so callers fall back to name-based detection.
|
||||
"""
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class _SYSTEM_INFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wProcessorArchitecture", wintypes.WORD),
|
||||
("wReserved", wintypes.WORD),
|
||||
("dwPageSize", wintypes.DWORD),
|
||||
("lpMinimumApplicationAddress", ctypes.c_void_p),
|
||||
("lpMaximumApplicationAddress", ctypes.c_void_p),
|
||||
("dwActiveProcessorMask", ctypes.c_size_t),
|
||||
("dwNumberOfProcessors", wintypes.DWORD),
|
||||
("dwProcessorType", wintypes.DWORD),
|
||||
("dwAllocationGranularity", wintypes.DWORD),
|
||||
("wProcessorLevel", wintypes.WORD),
|
||||
("wProcessorRevision", wintypes.WORD),
|
||||
]
|
||||
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
kernel32.GetNativeSystemInfo.argtypes = [ctypes.POINTER(_SYSTEM_INFO)]
|
||||
kernel32.GetNativeSystemInfo.restype = None
|
||||
kernel32.GetMachineTypeAttributes.argtypes = [
|
||||
wintypes.USHORT,
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
]
|
||||
kernel32.GetMachineTypeAttributes.restype = ctypes.c_long
|
||||
|
||||
info = _SYSTEM_INFO()
|
||||
kernel32.GetNativeSystemInfo(ctypes.byref(info))
|
||||
return _SYSTEM_INFO_ARCH_TO_NAME.get(info.wProcessorArchitecture)
|
||||
runnable = set()
|
||||
for machine in (_PE_MACHINE_ARM64, _PE_MACHINE_AMD64, _PE_MACHINE_I386):
|
||||
attributes = ctypes.c_int(0)
|
||||
# HRESULT: zero is success, any nonzero value is a failure.
|
||||
if kernel32.GetMachineTypeAttributes(machine, ctypes.byref(attributes)):
|
||||
continue
|
||||
if attributes.value & _MACHINE_ATTRIBUTE_USER_ENABLED:
|
||||
runnable.add(machine)
|
||||
return runnable or None
|
||||
|
||||
|
||||
def _windows_native_machine() -> str:
|
||||
|
|
@ -5661,26 +5648,26 @@ def _windows_native_machine() -> str:
|
|||
(#69179 follow-up report). Probe order:
|
||||
|
||||
1. ``IsWow64Process2`` with a correctly-typed current-process HANDLE
|
||||
(#71218 + HANDLE-truncation fix).
|
||||
2. ``GetNativeSystemInfo`` — still OS-native when (1) fails under
|
||||
x64-on-ARM64 emulation (env vars alone cannot cover that case).
|
||||
3. ``PROCESSOR_ARCHITEW6432`` / ``PROCESSOR_ARCHITECTURE`` — WOW64
|
||||
(32-bit) hosts and pre-1511 Windows 10 without the newer APIs.
|
||||
4. ``platform.machine()``.
|
||||
(#71218 + HANDLE-truncation fix). This is the only API that tells the
|
||||
truth from an x64 process emulated on ARM64.
|
||||
2. ``PROCESSOR_ARCHITEW6432`` / ``PROCESSOR_ARCHITECTURE`` — WOW64
|
||||
(32-bit) hosts and pre-1511 Windows 10 without the newer API.
|
||||
3. ``platform.machine()``.
|
||||
|
||||
Note ``GetNativeSystemInfo`` is deliberately NOT used: Microsoft documents
|
||||
that it "also returns emulated processor details when run from an app
|
||||
under emulation", so on the very WoA hosts this function exists to serve
|
||||
it reports AMD64 — no better than the env-var rung below it.
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
for probe in (
|
||||
_windows_native_machine_from_iswow64,
|
||||
_windows_native_machine_from_native_system_info,
|
||||
):
|
||||
try:
|
||||
name = probe()
|
||||
except (OSError, AttributeError, TypeError, ValueError):
|
||||
# API missing (pre-1511), DLL load failure in tests, or a
|
||||
# mistyped ctypes binding — try the next probe.
|
||||
name = None
|
||||
if name:
|
||||
return name
|
||||
try:
|
||||
name = _windows_native_machine_from_iswow64()
|
||||
except (OSError, AttributeError, TypeError, ValueError):
|
||||
# API missing (pre-1511), DLL load failure in tests, or a
|
||||
# mistyped ctypes binding — fall through to the env vars.
|
||||
name = None
|
||||
if name:
|
||||
return name
|
||||
env_arch = os.environ.get("PROCESSOR_ARCHITEW6432") or os.environ.get(
|
||||
"PROCESSOR_ARCHITECTURE"
|
||||
)
|
||||
|
|
@ -5694,12 +5681,23 @@ def _windows_native_machine() -> str:
|
|||
def _expected_windows_pe_machines() -> set:
|
||||
"""PE machine values the current Windows host can natively load.
|
||||
|
||||
AMD64 hosts run x64 and (via WOW64) x86. ARM64 hosts run ARM64 and
|
||||
(Windows 11 emulation) x64. 32-bit x86 hosts run only x86. Unknown
|
||||
machines return the permissive full set so the integrity gate can never
|
||||
brick launch on exotic hosts. Host detection uses the OS-native machine
|
||||
(see ``_windows_native_machine``), not the process architecture.
|
||||
Preferred source is ``GetMachineTypeAttributes``, which answers this
|
||||
question directly (including AMD64-on-ARM64 emulation) instead of
|
||||
inferring it from an architecture name.
|
||||
|
||||
Fallback is name-based: AMD64 hosts run x64 and (via WOW64) x86. ARM64
|
||||
hosts run ARM64 and (Windows 11 emulation) x64. 32-bit x86 hosts run only
|
||||
x86. Unknown machines return the permissive full set so the integrity gate
|
||||
can never brick launch on exotic hosts. Host detection uses the OS-native
|
||||
machine (see ``_windows_native_machine``), not the process architecture.
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
runnable = _windows_user_runnable_pe_machines()
|
||||
except (OSError, AttributeError, TypeError, ValueError):
|
||||
runnable = None
|
||||
if runnable:
|
||||
return runnable
|
||||
machine = _windows_native_machine().upper()
|
||||
if machine in ("AMD64", "X86_64", "X64"):
|
||||
return {_PE_MACHINE_AMD64, _PE_MACHINE_I386}
|
||||
|
|
|
|||
|
|
@ -122,9 +122,8 @@ def test_expected_machines_unknown_host_is_permissive():
|
|||
|
||||
# ─── _windows_native_machine ────────────────────────────────────────────────
|
||||
|
||||
# winnt.h PROCESSOR_ARCHITECTURE_* — mirrors hermes_cli.main constants.
|
||||
_SYSINFO_AMD64 = 9
|
||||
_SYSINFO_ARM64 = 12
|
||||
# MACHINE_ATTRIBUTES.UserEnabled — mirrors hermes_cli.main.
|
||||
_USER_ENABLED = 0x00000001
|
||||
|
||||
|
||||
class _SettableFunc:
|
||||
|
|
@ -147,14 +146,16 @@ class _FakeKernel32:
|
|||
native_pe_machine: int,
|
||||
*,
|
||||
wow64_ok: bool = True,
|
||||
system_info_arch: int | None = None,
|
||||
user_runnable: set | None = None,
|
||||
):
|
||||
self._native_pe = native_pe_machine
|
||||
self._wow64_ok = wow64_ok
|
||||
self._system_info_arch = system_info_arch
|
||||
self._user_runnable = user_runnable
|
||||
self.GetCurrentProcess = _SettableFunc(lambda: -1)
|
||||
self.IsWow64Process2 = _SettableFunc(self._is_wow64_process2)
|
||||
self.GetNativeSystemInfo = _SettableFunc(self._get_native_system_info)
|
||||
self.GetMachineTypeAttributes = _SettableFunc(
|
||||
self._get_machine_type_attributes
|
||||
)
|
||||
|
||||
def _is_wow64_process2(self, _handle, process_ref, native_ref):
|
||||
if not self._wow64_ok:
|
||||
|
|
@ -163,22 +164,26 @@ class _FakeKernel32:
|
|||
native_ref._obj.value = self._native_pe
|
||||
return 1
|
||||
|
||||
def _get_native_system_info(self, info_ref):
|
||||
if self._system_info_arch is None:
|
||||
raise AttributeError("GetNativeSystemInfo unavailable in this fake")
|
||||
info_ref._obj.wProcessorArchitecture = self._system_info_arch
|
||||
def _get_machine_type_attributes(self, machine, attributes_ref):
|
||||
if self._user_runnable is None:
|
||||
raise AttributeError("GetMachineTypeAttributes unavailable in this fake")
|
||||
if machine not in self._user_runnable:
|
||||
attributes_ref._obj.value = 0
|
||||
return 0
|
||||
attributes_ref._obj.value = _USER_ENABLED
|
||||
return 0
|
||||
|
||||
|
||||
def _fake_windll(
|
||||
native_pe_machine: int,
|
||||
*,
|
||||
wow64_ok: bool = True,
|
||||
system_info_arch: int | None = None,
|
||||
user_runnable: set | None = None,
|
||||
):
|
||||
kernel32 = _FakeKernel32(
|
||||
native_pe_machine,
|
||||
wow64_ok=wow64_ok,
|
||||
system_info_arch=system_info_arch,
|
||||
user_runnable=user_runnable,
|
||||
)
|
||||
|
||||
def _windll(name, *args, **kwargs):
|
||||
|
|
@ -218,11 +223,10 @@ def test_native_machine_binds_current_process_handle_restype(monkeypatch):
|
|||
assert kernel32.IsWow64Process2.argtypes is not None
|
||||
|
||||
|
||||
def test_native_machine_system_info_fallback_when_iswow64_fails(monkeypatch):
|
||||
"""Residual #71218 failure: IsWow64Process2 returns FALSE (e.g. invalid
|
||||
handle) while PROCESSOR_ARCHITECTURE still lies as AMD64. GetNativeSystemInfo
|
||||
must still report the real ARM64 host so the integrity gate accepts the
|
||||
correctly-built ARM64 Hermes.exe."""
|
||||
def test_expected_machines_prefers_user_runnable_api_over_arch_name(monkeypatch):
|
||||
"""GetMachineTypeAttributes answers "can this host load PE machine X?"
|
||||
directly, so a WoA host that reports AMD64 everywhere else still accepts an
|
||||
ARM64 exe."""
|
||||
import ctypes
|
||||
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
|
|
@ -231,14 +235,28 @@ def test_native_machine_system_info_fallback_when_iswow64_fails(monkeypatch):
|
|||
with patch.object(
|
||||
ctypes,
|
||||
"WinDLL",
|
||||
_fake_windll(PE_ARM64, wow64_ok=False, system_info_arch=_SYSINFO_ARM64),
|
||||
_fake_windll(
|
||||
PE_ARM64, wow64_ok=False, user_runnable={PE_ARM64, PE_AMD64}
|
||||
),
|
||||
create=True,
|
||||
), patch("platform.machine", return_value="AMD64"):
|
||||
assert cli_main._windows_native_machine() == "ARM64"
|
||||
assert cli_main._expected_windows_pe_machines() == {PE_ARM64, PE_AMD64}
|
||||
|
||||
|
||||
def test_expected_machines_falls_back_when_attributes_api_missing(monkeypatch):
|
||||
"""Pre-Windows-11 hosts have no GetMachineTypeAttributes — the name-based
|
||||
mapping must still drive the gate."""
|
||||
import ctypes
|
||||
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
with patch.object(
|
||||
ctypes, "WinDLL", _fake_windll(PE_ARM64, user_runnable=None), create=True
|
||||
), patch("platform.machine", return_value="AMD64"):
|
||||
assert cli_main._expected_windows_pe_machines() == {PE_ARM64, PE_AMD64}
|
||||
|
||||
|
||||
def test_native_machine_env_fallback_without_api(monkeypatch):
|
||||
"""Pre-1511 Windows 10: no IsWow64Process2 / GetNativeSystemInfo → env."""
|
||||
"""Pre-1511 Windows 10: no IsWow64Process2 → env."""
|
||||
import ctypes
|
||||
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
|
|
@ -286,11 +304,12 @@ def test_integrity_gate_accepts_arm64_exe_from_emulated_x64_process(monkeypatch,
|
|||
assert cli_main._desktop_exe_integrity_error(exe) is None
|
||||
|
||||
|
||||
def test_integrity_gate_accepts_arm64_when_iswow64_fails_but_system_info_ok(
|
||||
def test_integrity_gate_accepts_arm64_when_iswow64_fails_but_attributes_ok(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
"""End-to-end residual WoA shape: IsWow64Process2 fails, env lies as AMD64,
|
||||
GetNativeSystemInfo reports ARM64, ARM64 Hermes.exe must pass the gate."""
|
||||
"""End-to-end residual WoA shape: IsWow64Process2 fails and env lies as
|
||||
AMD64, but GetMachineTypeAttributes reports ARM64 as user-runnable, so the
|
||||
ARM64 Hermes.exe must pass the gate."""
|
||||
import ctypes
|
||||
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
|
|
@ -300,7 +319,9 @@ def test_integrity_gate_accepts_arm64_when_iswow64_fails_but_system_info_ok(
|
|||
with patch.object(
|
||||
ctypes,
|
||||
"WinDLL",
|
||||
_fake_windll(PE_ARM64, wow64_ok=False, system_info_arch=_SYSINFO_ARM64),
|
||||
_fake_windll(
|
||||
PE_ARM64, wow64_ok=False, user_runnable={PE_ARM64, PE_AMD64}
|
||||
),
|
||||
create=True,
|
||||
), patch("platform.machine", return_value="AMD64"):
|
||||
assert cli_main._desktop_exe_integrity_error(exe) is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue