mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-28 18:19:28 +00:00
fix(update): detect the OS-native machine for the Windows exe integrity gate
The #71119 integrity gate rejected CORRECT ARM64 desktop rebuilds on Windows-on-ARM (#69179 follow-up): platform.machine() reports the PROCESS architecture, and the update chain runs an x64 hermes-setup.exe / x64 Python under ARM64 emulation, so the gate compared the ARM64 Hermes.exe against a phantom 'AMD64 host' and aborted the update. _windows_native_machine() now asks the OS via IsWow64Process2 (whose nativeMachine is truthful from emulated processes), falling back to PROCESSOR_ARCHITEW6432/PROCESSOR_ARCHITECTURE (pre-1511 Win10) and then platform.machine(). All three consumers — the integrity gate, the packaged-exe arch preference, and backup validation — go through it. Sabotage-verified: the three new regression tests fail against the old process-arch behavior and pass with the fix.
This commit is contained in:
parent
ce44c3413e
commit
d9c41b5178
2 changed files with 136 additions and 18 deletions
|
|
@ -5557,17 +5557,63 @@ _PE_MACHINE_NAMES = {
|
|||
}
|
||||
|
||||
|
||||
def _windows_native_machine() -> str:
|
||||
"""The Windows host OS's NATIVE machine architecture, normalized upper.
|
||||
|
||||
``platform.machine()`` reports the PROCESS architecture, which lies under
|
||||
emulation: the desktop update chain runs an x64 hermes-setup.exe (and thus
|
||||
x64 Python) on Windows-on-ARM devices, where ``platform.machine()``
|
||||
returns ``AMD64`` even though the OS is ARM64. The #71119 integrity gate
|
||||
then rejected the CORRECT ARM64 rebuild as an "architecture mismatch"
|
||||
(#69179 follow-up report). ``IsWow64Process2`` asks the OS for the true
|
||||
native machine and works from emulated processes; the classic
|
||||
``PROCESSOR_ARCHITEW6432`` fallback only covers WOW64 (32-bit processes)
|
||||
and is NOT set under x64-on-ARM64 emulation, so it cannot replace the API
|
||||
probe — it is kept only for pre-1511 Windows 10 hosts without the API.
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
process_machine = ctypes.c_ushort(0)
|
||||
native_machine = ctypes.c_ushort(0)
|
||||
if kernel32.IsWow64Process2(
|
||||
kernel32.GetCurrentProcess(),
|
||||
ctypes.byref(process_machine),
|
||||
ctypes.byref(native_machine),
|
||||
):
|
||||
name = {
|
||||
_PE_MACHINE_ARM64: "ARM64",
|
||||
_PE_MACHINE_AMD64: "AMD64",
|
||||
_PE_MACHINE_I386: "X86",
|
||||
}.get(native_machine.value)
|
||||
if name:
|
||||
return name
|
||||
except (OSError, AttributeError):
|
||||
# No IsWow64Process2 (pre-1511 Windows 10) — fall through to the
|
||||
# documented WOW64 env vars, then the process architecture.
|
||||
pass
|
||||
env_arch = os.environ.get("PROCESSOR_ARCHITEW6432") or os.environ.get(
|
||||
"PROCESSOR_ARCHITECTURE"
|
||||
)
|
||||
if env_arch:
|
||||
return env_arch.upper()
|
||||
import platform as _platform
|
||||
|
||||
return (_platform.machine() or "").upper()
|
||||
|
||||
|
||||
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.
|
||||
brick launch on exotic hosts. Host detection uses the OS-native machine
|
||||
(see ``_windows_native_machine``), not the process architecture.
|
||||
"""
|
||||
import platform as _platform
|
||||
|
||||
machine = (_platform.machine() or "").upper()
|
||||
machine = _windows_native_machine().upper()
|
||||
if machine in ("AMD64", "X86_64", "X64"):
|
||||
return {_PE_MACHINE_AMD64, _PE_MACHINE_I386}
|
||||
if machine in ("ARM64", "AARCH64"):
|
||||
|
|
@ -5645,12 +5691,10 @@ def _desktop_exe_integrity_error(path: Path) -> Optional[str]:
|
|||
return str(exc)
|
||||
expected = _expected_windows_pe_machines()
|
||||
if machine not in expected:
|
||||
import platform as _platform
|
||||
|
||||
got = _PE_MACHINE_NAMES.get(machine, f"unknown machine 0x{machine:04X}")
|
||||
return (
|
||||
f"architecture mismatch: built a {got} executable but this is a "
|
||||
f"{_platform.machine()} Windows host"
|
||||
f"{_windows_native_machine()} Windows host"
|
||||
)
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ def test_parse_pe_machine_rejects_bad_pe_signature(tmp_path):
|
|||
],
|
||||
)
|
||||
def test_expected_machines_per_host(host, loadable, not_loadable):
|
||||
with patch("platform.machine", return_value=host):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value=host):
|
||||
expected = cli_main._expected_windows_pe_machines()
|
||||
assert loadable <= expected
|
||||
assert not (not_loadable & expected)
|
||||
|
|
@ -115,17 +115,91 @@ def test_expected_machines_per_host(host, loadable, not_loadable):
|
|||
|
||||
def test_expected_machines_unknown_host_is_permissive():
|
||||
"""The gate must never brick launch on hosts we don't recognize."""
|
||||
with patch("platform.machine", return_value="RISCV64"):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="RISCV64"):
|
||||
expected = cli_main._expected_windows_pe_machines()
|
||||
assert {PE_AMD64, PE_ARM64, PE_I386} <= expected
|
||||
|
||||
|
||||
# ─── _windows_native_machine ────────────────────────────────────────────────
|
||||
|
||||
|
||||
class _FakeKernel32:
|
||||
"""Stands in for kernel32 so the IsWow64Process2 path runs on any CI OS."""
|
||||
|
||||
def __init__(self, native_machine: int):
|
||||
self._native = native_machine
|
||||
|
||||
def GetCurrentProcess(self):
|
||||
return -1
|
||||
|
||||
def IsWow64Process2(self, _handle, process_ref, native_ref):
|
||||
process_ref._obj.value = PE_AMD64 # emulated x64 process
|
||||
native_ref._obj.value = self._native
|
||||
return 1
|
||||
|
||||
|
||||
def _fake_windll(native_machine: int):
|
||||
class _WinDLL:
|
||||
kernel32 = _FakeKernel32(native_machine)
|
||||
|
||||
return _WinDLL()
|
||||
|
||||
|
||||
def test_native_machine_reports_os_arch_not_process_arch(monkeypatch):
|
||||
"""The #69179 WoA regression: x64 Python under ARM64 emulation must report
|
||||
ARM64 (the OS), not AMD64 (the process) — otherwise the integrity gate
|
||||
rejects the correct ARM64 rebuild."""
|
||||
import ctypes
|
||||
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
with patch.object(ctypes, "windll", _fake_windll(PE_ARM64), create=True), \
|
||||
patch("platform.machine", return_value="AMD64"):
|
||||
assert cli_main._windows_native_machine() == "ARM64"
|
||||
|
||||
|
||||
def test_native_machine_env_fallback_without_api(monkeypatch):
|
||||
"""Pre-1511 Windows 10: no IsWow64Process2 → the WOW64 env var wins."""
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
monkeypatch.setenv("PROCESSOR_ARCHITEW6432", "AMD64")
|
||||
# On non-Windows interpreters ctypes has no `windll`, which is exactly the
|
||||
# AttributeError shape a missing API produces.
|
||||
with patch("platform.machine", return_value="x86"):
|
||||
assert cli_main._windows_native_machine() == "AMD64"
|
||||
|
||||
|
||||
def test_native_machine_platform_fallback(monkeypatch):
|
||||
"""No API, no env vars → the historical platform.machine() answer."""
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
monkeypatch.delenv("PROCESSOR_ARCHITEW6432", raising=False)
|
||||
monkeypatch.delenv("PROCESSOR_ARCHITECTURE", raising=False)
|
||||
with patch("platform.machine", return_value="AMD64"):
|
||||
assert cli_main._windows_native_machine() == "AMD64"
|
||||
|
||||
|
||||
def test_native_machine_non_windows_uses_platform(monkeypatch):
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "linux")
|
||||
with patch("platform.machine", return_value="aarch64"):
|
||||
assert cli_main._windows_native_machine() == "AARCH64"
|
||||
|
||||
|
||||
def test_integrity_gate_accepts_arm64_exe_from_emulated_x64_process(monkeypatch, tmp_path):
|
||||
"""End-to-end shape of the reporter's failure: ARM64 host, x64 updater
|
||||
process, correctly-built ARM64 Hermes.exe. The gate must pass it."""
|
||||
import ctypes
|
||||
|
||||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
exe = make_pe(tmp_path / "Hermes.exe", PE_ARM64)
|
||||
with patch.object(ctypes, "windll", _fake_windll(PE_ARM64), create=True), \
|
||||
patch("platform.machine", return_value="AMD64"):
|
||||
assert cli_main._desktop_exe_integrity_error(exe) is None
|
||||
|
||||
|
||||
# ─── _desktop_exe_integrity_error ───────────────────────────────────────────
|
||||
|
||||
|
||||
def test_integrity_error_none_for_matching_arch(tmp_path):
|
||||
exe = make_pe(tmp_path / "Hermes.exe", PE_AMD64)
|
||||
with patch("platform.machine", return_value="AMD64"):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="AMD64"):
|
||||
assert cli_main._desktop_exe_integrity_error(exe) is None
|
||||
|
||||
|
||||
|
|
@ -133,7 +207,7 @@ def test_integrity_error_reports_arch_mismatch(tmp_path):
|
|||
"""ARM64 exe on the reporter's 'Windows 10 AMD64' host — the wrong-arch
|
||||
flavor of 'This app can't run on your computer'."""
|
||||
exe = make_pe(tmp_path / "Hermes.exe", PE_ARM64)
|
||||
with patch("platform.machine", return_value="AMD64"):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="AMD64"):
|
||||
error = cli_main._desktop_exe_integrity_error(exe)
|
||||
assert error is not None and "architecture mismatch" in error
|
||||
assert "ARM64" in error
|
||||
|
|
@ -159,7 +233,7 @@ def test_packaged_executable_prefers_host_arch_over_mtime(tmp_path, monkeypatch)
|
|||
|
||||
os.utime(bad, (bad.stat().st_atime + 1000, bad.stat().st_mtime + 1000))
|
||||
|
||||
with patch("platform.machine", return_value="AMD64"):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="AMD64"):
|
||||
assert cli_main._desktop_packaged_executable(desktop_dir) == good
|
||||
|
||||
|
||||
|
|
@ -175,7 +249,7 @@ def test_packaged_executable_falls_back_to_mtime_when_unparseable(tmp_path, monk
|
|||
import os
|
||||
|
||||
os.utime(b, (b.stat().st_atime + 1000, b.stat().st_mtime + 1000))
|
||||
with patch("platform.machine", return_value="AMD64"):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="AMD64"):
|
||||
assert cli_main._desktop_packaged_executable(desktop_dir) == b
|
||||
|
||||
|
||||
|
|
@ -194,7 +268,7 @@ def test_rollback_restores_backup_and_keeps_corrupt_copy(tmp_path):
|
|||
backup_exe = desktop_dir / "release" / "win-unpacked.bak" / "Hermes.exe"
|
||||
make_pe(backup_exe, PE_AMD64) # valid old build
|
||||
|
||||
with patch("platform.machine", return_value="AMD64"):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="AMD64"):
|
||||
restored = cli_main._rollback_desktop_from_backup(exe)
|
||||
|
||||
assert restored == exe
|
||||
|
|
@ -232,7 +306,7 @@ def test_gate_passes_valid_exe(tmp_path, monkeypatch):
|
|||
monkeypatch.setattr(cli_main.sys, "platform", "win32")
|
||||
desktop_dir, exe = _win_tree(tmp_path)
|
||||
make_pe(exe, PE_AMD64)
|
||||
with patch("platform.machine", return_value="AMD64"):
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="AMD64"):
|
||||
verified, rolled_back = cli_main._ensure_desktop_exe_launchable(desktop_dir, exe)
|
||||
assert verified == exe
|
||||
assert rolled_back is False
|
||||
|
|
@ -259,7 +333,7 @@ def test_gate_rolls_back_corrupt_exe_and_purges_cache(tmp_path, monkeypatch, cap
|
|||
stamp.parent.mkdir(parents=True, exist_ok=True)
|
||||
stamp.write_text("{}", encoding="utf-8")
|
||||
|
||||
with patch("platform.machine", return_value="AMD64"), \
|
||||
with patch("hermes_cli.main._windows_native_machine", return_value="AMD64"), \
|
||||
patch("hermes_cli.main._purge_electron_build_cache", return_value=[]) as mock_purge, \
|
||||
patch("hermes_cli.main._desktop_stamp_path", return_value=stamp):
|
||||
verified, rolled_back = cli_main._ensure_desktop_exe_launchable(desktop_dir, exe)
|
||||
|
|
@ -336,7 +410,7 @@ def test_build_only_fails_when_pack_produces_corrupt_exe(tmp_path, monkeypatch,
|
|||
patch("hermes_cli.main._purge_electron_build_cache", return_value=[]), \
|
||||
patch("hermes_cli.main._desktop_stamp_path", return_value=tmp_path / "stamp.json"), \
|
||||
patch("hermes_cli.main._write_desktop_build_stamp") as mock_stamp, \
|
||||
patch("platform.machine", return_value="AMD64"), \
|
||||
patch("hermes_cli.main._windows_native_machine", return_value="AMD64"), \
|
||||
patch("hermes_cli.main.subprocess.run", return_value=pack_ok), \
|
||||
pytest.raises(SystemExit) as exc:
|
||||
cli_main.cmd_gui(_ns())
|
||||
|
|
@ -368,7 +442,7 @@ def test_build_only_succeeds_with_valid_exe(tmp_path, monkeypatch, capsys):
|
|||
patch("hermes_cli.main._desktop_build_needed", return_value=True), \
|
||||
patch("hermes_cli.main._stop_desktop_processes_locking_build", return_value=[]), \
|
||||
patch("hermes_cli.main._write_desktop_build_stamp") as mock_stamp, \
|
||||
patch("platform.machine", return_value="AMD64"), \
|
||||
patch("hermes_cli.main._windows_native_machine", return_value="AMD64"), \
|
||||
patch("hermes_cli.main.subprocess.run", return_value=pack_ok):
|
||||
cli_main.cmd_gui(_ns())
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue