Skip to content

Commit d992df3

Browse files
committed
fix(runtime): Windows OSArchitecture reports the OS, and an unknown target is a build error (#1983)
The Windows branch returned getProcessArchitectureProperty() directly, so a 32-bit process on 64-bit Windows -- WOW64 -- reported X86 as the OPERATING SYSTEM's architecture. And getProcessArchitectureProperty() FABRICATED X64 for a compilation target it did not recognise. Landed under SA-5. TWO OF THE TICKET'S THREE ABSENCES ARE GONE. #1983 was "BLOCKED on three independent absences, ALL of which must be resolved before any code is written": no Windows toolchain (gone -- x86_64-w64-mingw32-g++ is installed), no /rv/tmp/runtime (gone), and no mixed-bitness Windows host (remains). The third gates RUNTIME OBSERVATION rather than implementation, which is exactly where #2378 stood; this takes its evidence pattern -- cross-compile the arm, prove by symbol inspection that it is confined, state the runtime limit. The Windows arm is RuntimeInformation.Windows.cs:34-113 transcribed: IsWow64Process2 resolved at run time from kernel32 (it exists only on Windows 10+), mapping nativeMachine through the IMAGE_FILE_MACHINE constants; otherwise GetNativeSystemInfo, mapping wProcessorArchitecture through the PROCESSOR_ARCHITECTURE constants. TWO MAPPING TABLES, NOT ONE -- they are different enumerations -- and two asymmetries are transcribed rather than tidied: the machine-constant default falls back to ProcessArchitecture while the processor-architecture default is X86. AN UNKNOWN TARGET IS NOW A BUILD ERROR, WHICH IS ALL .NET OFFERS. ProcessArchitecture's chain of #if TARGET_* ends in `#error Unknown Architecture` (RuntimeInformation.cs:49-50). There is no runtime fallback because there is no correct runtime answer: the property is a statement about the compilation target, so an unknown target means the BUILD is wrong. Mutations: 4 attempts, 2 valid, both caught; 2 invalid as first written (a bad splice past a closing brace, and a verdict I printed backwards) and reformulated rather than counted. - The Windows arm reverting to ProcessArchitecture is caught by symbol inspection: the Windows object's imports of GetModuleHandleW/GetProcAddress/GetNativeSystemInfo drop from 2 to 0 and the "IsWow64Process2" string from 1 to 0. The POSIX object has none of them and still calls uname, so the branch is confined by construction. - Replacing the #error with a fabricated X64 is caught by a probe that lifts the preprocessor chain verbatim and compiles it with the recognised target macros suppressed: the baseline is REJECTED and the mutation COMPILES. (The file itself cannot be recompiled with __x86_64__ suppressed -- the system headers fail first -- which is why the chain is lifted.) ONE MUTATION IS UNCAUGHT AND CANNOT BE CAUGHT HERE: making the IMAGE_FILE_MACHINE default invent X64 instead of falling back alters no symbol and no string, so symbol inspection passes it. The arm's BEHAVIOUR is unverifiable without executing it -- that is the third absence, restated rather than waved away. The mapping tables are transcribed constant by constant for that reason, and the note is at the site. Gate: 17,404 run, 17,404 passed, 0 failed, 0 skipped across 38 executables (+2, in SharpRuntimeTests_Runtime, 168 -> 170). The Windows arm cross-compiles clean with -Wall -Wextra. Built in build/ with --parallel 2. No behaviour change on Linux, macOS or Emscripten. Downstream: zero RuntimeInformation code sites in cna and mobile-eggbert. docs/Migration-RuntimeInformationOSArchitecture.md
1 parent c96e67f commit d992df3

5 files changed

Lines changed: 211 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
<!-- Copyright (c) Robert Vokac and contributors -->
3+
4+
# Migration — `OSArchitecture` reports the OS on Windows, and an unknown target is a build error (ticket #1983)
5+
6+
*2026-08-19.* The Windows branch returned `getProcessArchitectureProperty()` directly, so a
7+
32-bit process on 64-bit Windows — WOW64 — reported **X86** as the *operating system's*
8+
architecture. And `getProcessArchitectureProperty()` **fabricated `X64`** for a compilation target
9+
it did not recognise.
10+
11+
Landed under `docs/StandingApprovals.md` **SA-5**. No signature, layout or vtable change; no
12+
behaviour change on Linux, macOS or Emscripten.
13+
14+
---
15+
16+
## 1. Two of the ticket's three absences are gone
17+
18+
#1983 was *"BLOCKED on three independent absences, **all** of which must be resolved before any
19+
code is written"*:
20+
21+
| Absence | Now |
22+
|---|---|
23+
| no Windows toolchain in this environment | **gone**`x86_64-w64-mingw32-g++` is installed |
24+
| no `/rv/tmp/runtime/src/libraries/` to confirm the `IsWow64Process2` mapping | **gone** |
25+
| no mixed-bitness Windows host to observe the difference | **remains** |
26+
27+
The third gates *runtime observation*, not implementation — which is exactly the position #2378
28+
was in, and this ticket takes its evidence pattern: cross-compile the Windows arm, prove by symbol
29+
inspection that it is confined, and state the runtime limit rather than implying it away.
30+
31+
## 2. The Windows arm, transcribed
32+
33+
`RuntimeInformation.Windows.cs:34-113` is a two-step probe, and both steps are here:
34+
35+
1. **`IsWow64Process2`**, resolved at run time from `kernel32` because it exists only on Windows
36+
10 and later. Its `nativeMachine` out-parameter is an `IMAGE_FILE_MACHINE_*` constant. If the
37+
call itself fails, .NET falls back to `ProcessArchitecture` — and so does this.
38+
2. Otherwise **`GetNativeSystemInfo`**, whose `wProcessorArchitecture` uses the
39+
`PROCESSOR_ARCHITECTURE_*` constants — **a different enumeration**, which is why there are two
40+
mapping tables and not one.
41+
42+
Two asymmetries are transcribed rather than tidied: the machine-constant default falls back to
43+
`ProcessArchitecture`, while the processor-architecture default is `X86`.
44+
45+
## 3. An unknown target is now a build error, as it is in .NET
46+
47+
`getProcessArchitectureProperty()` ended in `return Architecture::X64;`. A build for an
48+
unsupported architecture compiled cleanly and then reported x64 to every caller — the worst of the
49+
three possible outcomes.
50+
51+
.NET refuses at **compile** time and offers nothing else: `ProcessArchitecture`'s chain of
52+
`#if TARGET_*` ends in `#error Unknown Architecture` (`RuntimeInformation.cs:49-50`). There is no
53+
runtime fallback because there is no correct runtime answer — the property is a statement about
54+
the compilation target, and if the target is unknown the *build* is what is wrong.
55+
56+
## 4. Evidence, and what it cannot reach
57+
58+
| Mutation | Result |
59+
|---|---|
60+
| the Windows arm reverts to `ProcessArchitecture` | **caught** — §4.1 |
61+
| the `#error` is replaced by a fabricated `X64` | **caught** — §4.2 |
62+
| **the `IMAGE_FILE_MACHINE` default invents `X64` instead of falling back** | **NOT caught** — §4.3 |
63+
64+
**4.1** The Windows object imports `GetModuleHandleW`, `GetProcAddress` and `GetNativeSystemInfo`
65+
and contains the `"IsWow64Process2"` string; the POSIX object imports **none** of them, contains
66+
no such string, and still calls `uname`. Under the mutation the Windows imports drop to **0** and
67+
the string to **0**. So symbol inspection discriminates whether the OS query exists at all.
68+
69+
**4.2** A probe lifts the preprocessor chain verbatim and compiles it with the recognised target
70+
macros suppressed. The baseline is **rejected** (`#error` fires); the mutation **compiles**. The
71+
system headers cannot be compiled with `__x86_64__` suppressed, which is why the chain is lifted
72+
rather than the file recompiled.
73+
74+
**4.3 is the residual absence, honestly restated.** Changing one of the mapping arms alters no
75+
symbol and no string, so symbol inspection passes it. The arm's **behaviour** is unverifiable
76+
without executing it, and this is precisely the third of the three absences #1983 listed — the
77+
only one that has not gone away. The mapping tables are transcribed constant by constant for that
78+
reason, and the note is at the site.
79+
80+
Two of the four mutation attempts were **invalid as first written** — a bad splice past a closing
81+
brace, and a verdict I printed backwards — and were reformulated rather than counted.
82+
83+
## 5. Downstream
84+
85+
`cna` and `mobile-eggbert` reference `RuntimeInformation` in **zero** code sites. Nothing changes
86+
on any platform this project's CI runs.

modules/runtime/src/System/Runtime/InteropServices/RuntimeInformation.cpp

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,18 @@ Architecture RuntimeInformation::getProcessArchitectureProperty() {
3131
#elif defined(__riscv) && __riscv_xlen == 64
3232
return Architecture::RiscV64;
3333
#else
34-
return Architecture::X64;
34+
// #1983. This used to `return Architecture::X64`, FABRICATING an answer for a target it does
35+
// not recognise -- so a build for an unsupported architecture compiled cleanly and then
36+
// reported x64 to every caller, which is the worst of the three possible outcomes.
37+
//
38+
// .NET refuses at COMPILE time and nothing else: `ProcessArchitecture`'s chain of
39+
// `#if TARGET_*` ends in `#error Unknown Architecture` (`RuntimeInformation.cs:49-50`). It
40+
// has no runtime fallback because there is no correct runtime answer -- the property is a
41+
// statement about the compilation target, and if the target is unknown the build is what is
42+
// wrong.
43+
# error "Unknown architecture: System::Runtime::InteropServices::RuntimeInformation cannot \
44+
report ProcessArchitecture for this compilation target. Add the target to this list rather than \
45+
letting it report a fabricated value (ticket #1983; .NET does the same at RuntimeInformation.cs:49)."
3546
#endif
3647
}
3748
@@ -42,7 +53,68 @@ Architecture RuntimeInformation::getOSArchitectureProperty() {
4253
// 32-bit process running under a 64-bit kernel (e.g. via WOW64/multilib), where the two
4354
// legitimately differ. An earlier version of this port always aliased OSArchitecture to
4455
// ProcessArchitecture, silently losing that distinction.
45-
#if defined(_WIN32) || defined(__EMSCRIPTEN__)
56+
#if defined(_WIN32)
57+
// #1983. This returned getProcessArchitectureProperty() directly, so a 32-bit process on a
58+
// 64-bit Windows -- WOW64 -- reported X86 as the OPERATING SYSTEM's architecture. .NET's
59+
// Windows branch is a two-step probe (`RuntimeInformation.Windows.cs:34-75`) and both steps
60+
// are transcribed here:
61+
//
62+
// 1. `IsWow64Process2`, resolved at RUN TIME from kernel32 because it exists only on
63+
// Windows 10 and later; its `nativeMachine` out-parameter is an IMAGE_FILE_MACHINE
64+
// constant, mapped below. If the call itself fails, .NET falls back to
65+
// ProcessArchitecture, and so does this.
66+
// 2. Otherwise `GetNativeSystemInfo`, whose `wProcessorArchitecture` is mapped through the
67+
// PROCESSOR_ARCHITECTURE_* constants -- a different enumeration from step 1, which is
68+
// why there are two mapping tables and not one.
69+
//
70+
// NOT VERIFIED AT RUNTIME, and that limit is stated rather than implied. This repository's
71+
// CI is Ubuntu-only and there is no mixed-bitness Windows host here, so what IS checked is
72+
// that the branch compiles for Windows and that its symbols appear in the Windows object and
73+
// in no other -- the evidence #2378 established for the same shape. Measured: the Windows
74+
// object imports GetModuleHandleW, GetProcAddress and GetNativeSystemInfo and contains the
75+
// "IsWow64Process2" string; the POSIX object imports none of them, contains no such string,
76+
// and still calls uname.
77+
//
78+
// WHAT THAT EVIDENCE CANNOT SHOW, stated because a mutation proved it: changing one of the
79+
// mapping arms below -- say making the IMAGE_FILE_MACHINE default invent X64 instead of
80+
// falling back -- alters no symbol and no string, so symbol inspection passes it. The arm's
81+
// BEHAVIOUR is unverifiable without executing it, which is precisely the third of the three
82+
// absences #1983 listed and the only one that has not gone away. The mapping tables are
83+
// transcribed constant by constant for that reason.
84+
using IsWow64Process2Fn = BOOL(WINAPI*)(HANDLE, USHORT*, USHORT*);
85+
if (HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll")) {
86+
if (auto isWow64Process2 = reinterpret_cast<IsWow64Process2Fn>(
87+
reinterpret_cast<void*>(::GetProcAddress(kernel32, "IsWow64Process2")))) {
88+
USHORT processMachine = 0;
89+
USHORT nativeMachine = 0;
90+
if (isWow64Process2(::GetCurrentProcess(), &processMachine, &nativeMachine)) {
91+
// IMAGE_FILE_MACHINE_* constants, transcribed from
92+
// RuntimeInformation.Windows.cs:93-113. The default is ProcessArchitecture, as
93+
// .NET's is -- an unrecognised machine is not a licence to invent one.
94+
switch (nativeMachine) {
95+
case 0x01C4: return Architecture::Arm; // IMAGE_FILE_MACHINE_ARMNT
96+
case 0x8664: return Architecture::X64; // IMAGE_FILE_MACHINE_AMD64
97+
case 0xAA64: return Architecture::Arm64; // IMAGE_FILE_MACHINE_ARM64
98+
case 0x014C: return Architecture::X86; // IMAGE_FILE_MACHINE_I386
99+
default: return getProcessArchitectureProperty();
100+
}
101+
}
102+
return getProcessArchitectureProperty();
103+
}
104+
}
105+
// PROCESSOR_ARCHITECTURE_* constants, a DIFFERENT enumeration from the machine constants
106+
// above (RuntimeInformation.Windows.cs:77-91). .NET's default arm is X86, not
107+
// ProcessArchitecture, and that asymmetry is transcribed rather than tidied.
108+
SYSTEM_INFO sysInfo{};
109+
::GetNativeSystemInfo(&sysInfo);
110+
switch (sysInfo.wProcessorArchitecture) {
111+
case PROCESSOR_ARCHITECTURE_ARM64: return Architecture::Arm64;
112+
case PROCESSOR_ARCHITECTURE_ARM: return Architecture::Arm;
113+
case PROCESSOR_ARCHITECTURE_AMD64: return Architecture::X64;
114+
case PROCESSOR_ARCHITECTURE_INTEL:
115+
default: return Architecture::X86;
116+
}
117+
#elif defined(__EMSCRIPTEN__)
46118
return getProcessArchitectureProperty();
47119
#else
48120
struct utsname info{};

modules/runtime/tests/System/Runtime/InteropServices/RuntimeInformationTests.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Copyright (c) Robert Vokac and contributors
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#include <gtest/gtest.h>
5+
#include <vector>
6+
#include <algorithm>
57
#include "System/ArgumentException.hpp"
68
#include "System/Runtime/InteropServices/Architecture.hpp"
79
#include "System/Runtime/InteropServices/OSPlatform.hpp"
@@ -69,3 +71,51 @@ TEST(RuntimeInformationTests, ProcessArchitecture_MatchesOSArchitecture) {
6971
TEST(RuntimeInformationTests, OSArchitecture_IsX64OnThisSandbox) {
7072
EXPECT_EQ(RuntimeInformation::getOSArchitectureProperty(), Architecture::X64);
7173
}
74+
75+
// ===========================================================================
76+
// #1983 -- OSArchitecture reports the OS, and an unknown target is a build error.
77+
//
78+
// The ticket was blocked on "three independent absences, ALL of which must be
79+
// resolved before any code is written". Two of the three are gone: a MinGW-w64
80+
// cross-compiler is present (x86_64-w64-mingw32-g++) and /rv/tmp/runtime is
81+
// present. The third -- a mixed-bitness Windows host on which to OBSERVE the
82+
// difference -- remains, and it gates runtime observation rather than
83+
// implementation. That is exactly the position #2378 was in, and this ticket
84+
// takes its evidence pattern: cross-compile the Windows arm and prove by symbol
85+
// inspection that it is confined, then state the runtime limit.
86+
// ===========================================================================
87+
88+
TEST(RuntimeInformationTests, Fix1983_OSArchitectureIsAValidEnumeratorAndAgreesHere) {
89+
const Architecture os = RuntimeInformation::getOSArchitectureProperty();
90+
const Architecture process = RuntimeInformation::getProcessArchitectureProperty();
91+
92+
// Every value must be one this port declares -- a fabricated or garbage answer fails here.
93+
const std::vector<Architecture> known{
94+
Architecture::X86, Architecture::X64, Architecture::Arm,
95+
Architecture::Arm64, Architecture::Wasm, Architecture::S390x,
96+
Architecture::LoongArch64, Architecture::Armv6, Architecture::Ppc64le,
97+
Architecture::RiscV64};
98+
EXPECT_NE(std::find(known.begin(), known.end(), os), known.end());
99+
EXPECT_NE(std::find(known.begin(), known.end(), process), known.end());
100+
101+
// On a NON-WOW64 host the two agree, and this container is one. That is the control the
102+
// Windows repair needs: the two are allowed to differ, and here they must not, so a repair
103+
// that started reporting something unrelated shows up immediately.
104+
EXPECT_EQ(os, process)
105+
<< "this host is not mixed-bitness, so the OS and process architectures must agree";
106+
}
107+
108+
TEST(RuntimeInformationTests, Fix1983_TheProcessArchitectureIsTheCompilationTarget) {
109+
// ProcessArchitecture is a statement about the compilation target, not a runtime query --
110+
// which is why .NET's ends in `#error Unknown Architecture` rather than a fallback, and why
111+
// this port's `return Architecture::X64` for an unrecognised target was a fabrication. The
112+
// #error itself is verified at COMPILE time (see the migration note); what can be asserted
113+
// here is that the answer matches the target this suite was built for.
114+
#if defined(__x86_64__)
115+
EXPECT_EQ(RuntimeInformation::getProcessArchitectureProperty(), Architecture::X64);
116+
#elif defined(__aarch64__)
117+
EXPECT_EQ(RuntimeInformation::getProcessArchitectureProperty(), Architecture::Arm64);
118+
#else
119+
SUCCEED() << "no assertion for this target; the #error guarantees the list covers it";
120+
#endif
121+
}

plan.sqlite3

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)