diff --git a/frontend/src-tauri/resources/windows/README.md b/frontend/src-tauri/resources/windows/README.md index d9b6bd73..8ffaa106 100644 --- a/frontend/src-tauri/resources/windows/README.md +++ b/frontend/src-tauri/resources/windows/README.md @@ -55,6 +55,10 @@ before the Tauri build. Those scripts use: - A SHA-verified WiX CLI NuGet package, used only to extract the VC++ redist bootstrapper payload reproducibly. +The VC++ redist contains ARM64EC payloads that report `AMD64` in the PE Machine +field. The staging script therefore rejects ARM64EC markers and only copies +native AMD64 runtime DLLs into the installer payload. + The PR build emits `target/reproducibility/desktop-pr-windows-*.sha256` proof manifests. The signed release build emits `target/reproducibility/desktop-release-windows-*.sha256` proof manifests after diff --git a/frontend/src-tauri/scripts/stage-windows-runtime-dlls.ps1 b/frontend/src-tauri/scripts/stage-windows-runtime-dlls.ps1 index f1e018eb..5606d9dd 100644 --- a/frontend/src-tauri/scripts/stage-windows-runtime-dlls.ps1 +++ b/frontend/src-tauri/scripts/stage-windows-runtime-dlls.ps1 @@ -169,6 +169,82 @@ function Test-PortableExecutableMachineAmd64 { } } +function Test-FileContainsAscii { + param( + [Parameter(Mandatory = $true)][string] $Path, + [Parameter(Mandatory = $true)][string] $Needle + ) + + $bytes = [IO.File]::ReadAllBytes($Path) + $needleBytes = [Text.Encoding]::ASCII.GetBytes($Needle) + + if (($needleBytes.Length -eq 0) -or ($bytes.Length -lt $needleBytes.Length)) { + return $false + } + + $lastStart = $bytes.Length - $needleBytes.Length + for ($i = 0; $i -le $lastStart; $i++) { + $matched = $true + for ($j = 0; $j -lt $needleBytes.Length; $j++) { + $actual = $bytes[$i + $j] + $expected = $needleBytes[$j] + + if (($actual -ge 0x41) -and ($actual -le 0x5A)) { + $actual += 0x20 + } + + if (($expected -ge 0x41) -and ($expected -le 0x5A)) { + $expected += 0x20 + } + + if ($actual -ne $expected) { + $matched = $false + break + } + } + + if ($matched) { + return $true + } + } + + return $false +} + +function Test-PortableExecutableArm64Ec { + param([Parameter(Mandatory = $true)][string] $Path) + + return ` + (Test-FileContainsAscii -Path $Path -Needle "arm64ec") -or ` + (Test-FileContainsAscii -Path $Path -Needle "arm64ret") -or ` + (Test-FileContainsAscii -Path $Path -Needle ".arm64.pdb") +} + +function Test-PortableExecutableNativeAmd64 { + param([Parameter(Mandatory = $true)][string] $Path) + + if (!(Test-PortableExecutableMachineAmd64 -Path $Path)) { + return $false + } + + return !(Test-PortableExecutableArm64Ec -Path $Path) +} + +function Assert-PortableExecutableNativeAmd64 { + param( + [Parameter(Mandatory = $true)][string] $Label, + [Parameter(Mandatory = $true)][string] $Path + ) + + if (!(Test-PortableExecutableMachineAmd64 -Path $Path)) { + throw "$Label is not an AMD64 portable executable: $Path" + } + + if (Test-PortableExecutableArm64Ec -Path $Path) { + throw "$Label is ARM64EC, not native AMD64: $Path" + } +} + function Expand-VcRedist { param( [Parameter(Mandatory = $true)][string] $ExePath, @@ -256,17 +332,24 @@ function Find-RequiredDll { } $x64Files = @($files | Where-Object { Test-PortableExecutableMachineAmd64 -Path $_.FullName }) + $arm64EcFiles = @($x64Files | Where-Object { Test-PortableExecutableArm64Ec -Path $_.FullName }) + $nativeAmd64Files = @($x64Files | Where-Object { !(Test-PortableExecutableArm64Ec -Path $_.FullName) }) + + foreach ($file in ($arm64EcFiles | Sort-Object FullName)) { + Write-Host ("skipped-windows-runtime-arm64ec {0} {1}" -f $Name, $file.FullName) + } - $match = $x64Files | + $match = $nativeAmd64Files | Where-Object { $_.Name -ieq $Name } | Sort-Object FullName | Select-Object -First 1 if ($null -ne $match) { + Write-Host ("selected-windows-runtime-dll {0} {1}" -f $Name, $match.FullName) return $match.FullName } - foreach ($file in ($x64Files | Sort-Object FullName)) { + foreach ($file in ($nativeAmd64Files | Sort-Object FullName)) { $versionInfo = $null try { $versionInfo = $file.VersionInfo @@ -295,12 +378,16 @@ function Find-RequiredDll { ForEach-Object { $originalFilename = "" $internalName = "" + $isAmd64 = $false + $isArm64Ec = $false try { $originalFilename = $_.VersionInfo.OriginalFilename $internalName = $_.VersionInfo.InternalName + $isAmd64 = Test-PortableExecutableMachineAmd64 -Path $_.FullName + $isArm64Ec = $isAmd64 -and (Test-PortableExecutableArm64Ec -Path $_.FullName) } catch { } - Write-Host (" {0} len={1} original={2} internal={3}" -f $_.FullName, $_.Length, $originalFilename, $internalName) + Write-Host (" {0} len={1} amd64={2} arm64ec={3} original={4} internal={5}" -f $_.FullName, $_.Length, $isAmd64, $isArm64Ec, $originalFilename, $internalName) } throw "Could not find $Name in extracted VC++ Redistributable payload roots: $($Roots -join '; ')" @@ -358,11 +445,16 @@ Assert-Sha256 -Label "VC++ Redistributable $vcRedistVersion" -Path $redistExe -E $wixExe = Get-WixExe -CacheRoot $cacheDir $payloadRoots = Expand-VcRedist -ExePath $redistExe -WixExe $wixExe -OutDir $redistDir -Copy-Item -LiteralPath $OrtDllPath -Destination (Join-Path $Destination "onnxruntime.dll") -Force +$stagedOrtDll = Join-Path $Destination "onnxruntime.dll" +Copy-Item -LiteralPath $OrtDllPath -Destination $stagedOrtDll -Force +Assert-PortableExecutableNativeAmd64 -Label "ONNX Runtime DLL" -Path $stagedOrtDll foreach ($dllName in $dllNames) { $source = Find-RequiredDll -Name $dllName -Roots $payloadRoots - Copy-Item -LiteralPath $source -Destination (Join-Path $Destination $dllName) -Force + Assert-PortableExecutableNativeAmd64 -Label $dllName -Path $source + $destinationDll = Join-Path $Destination $dllName + Copy-Item -LiteralPath $source -Destination $destinationDll -Force + Assert-PortableExecutableNativeAmd64 -Label $dllName -Path $destinationDll } Get-ChildItem -LiteralPath $Destination -Filter "*.dll" -File | diff --git a/scripts/ci/canonical-windows-nsis-payload-hash.py b/scripts/ci/canonical-windows-nsis-payload-hash.py index 74f1d8a6..6bc5db88 100644 --- a/scripts/ci/canonical-windows-nsis-payload-hash.py +++ b/scripts/ci/canonical-windows-nsis-payload-hash.py @@ -10,6 +10,15 @@ PE_CERTIFICATE_DIRECTORY_INDEX = 4 +PE_MACHINE_AMD64 = 0x8664 +WINDOWS_RUNTIME_PAYLOAD_FILES = ( + "maple.exe", + "onnxruntime.dll", + "MSVCP140.dll", + "MSVCP140_1.dll", + "VCRUNTIME140.dll", + "VCRUNTIME140_1.dll", +) def sha256_bytes(data): @@ -44,6 +53,23 @@ def is_pe(data): return False +def pe_machine(data, label): + if not is_pe(data): + raise ValueError(f"not a PE file: {label}") + + pe_offset = read_u32(data, 0x3C) + return read_u16(data, pe_offset + 4) + + +def has_arm64ec_markers(data): + lowered = data.lower() + return ( + b"arm64ec" in lowered + or b"arm64ret" in lowered + or b".arm64.pdb" in lowered + ) + + def canonical_pe_bytes(data, label): if not is_pe(data): return data @@ -129,6 +155,45 @@ def canonical_tree_entries(root): return entries +def payload_file(root, name): + matches = [] + for current_root, _, files in os.walk(root): + for filename in files: + if filename.lower() == name.lower(): + matches.append(os.path.join(current_root, filename)) + + if len(matches) != 1: + raise ValueError( + f"expected exactly one Windows payload file named {name}, found {len(matches)}" + ) + + return matches[0] + + +def verify_windows_runtime_payload(installer, label): + with tempfile.TemporaryDirectory() as tmp: + extract_installer(installer, tmp) + + for name in WINDOWS_RUNTIME_PAYLOAD_FILES: + path = payload_file(tmp, name) + with open(path, "rb") as f: + data = f.read() + + machine = pe_machine(data, name) + if machine != PE_MACHINE_AMD64: + raise ValueError( + f"Windows payload file is not native AMD64: {name} machine=0x{machine:04x}" + ) + + if has_arm64ec_markers(data): + raise ValueError( + f"Windows payload file is ARM64EC, not native AMD64: {name}" + ) + + digest = sha256_bytes(data) + print(f"verified-windows-runtime-native-amd64 {digest} {label}::{name}") + + def canonical_tree_hash(installer): with tempfile.TemporaryDirectory() as tmp: extract_installer(installer, tmp) @@ -182,11 +247,25 @@ def main(): parser = argparse.ArgumentParser( description="Compute or compare canonical NSIS payload hashes for Windows installers." ) - parser.add_argument("--compare", action="store_true", help="compare signed and unsigned installers") + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--compare", action="store_true", help="compare signed and unsigned installers") + mode.add_argument( + "--verify-runtime-dlls", + action="store_true", + help="verify installed Windows payload files are native AMD64, not ARM64EC", + ) parser.add_argument("paths", nargs="+") args = parser.parse_args() try: + if args.verify_runtime_dlls: + if len(args.paths) not in {1, 2}: + parser.error("--verify-runtime-dlls requires installer [label]") + installer = args.paths[0] + label = args.paths[1] if len(args.paths) == 2 else installer + verify_windows_runtime_payload(installer, label) + return 0 + if args.compare: if len(args.paths) not in {2, 4}: parser.error("--compare requires signed unsigned [signed-label unsigned-label]") diff --git a/scripts/ci/verify-release-artifacts.sh b/scripts/ci/verify-release-artifacts.sh index 09c038a1..42c2c693 100755 --- a/scripts/ci/verify-release-artifacts.sh +++ b/scripts/ci/verify-release-artifacts.sh @@ -184,6 +184,29 @@ verify_windows_runtime_manifest() { fi } +windows_setup_label_from_manifest() { + local manifest="$1" + + awk '$2 ~ /_x64-setup\.exe$/ { print $2; exit }' "${manifest}" +} + +verify_windows_installer_runtime_payload() { + local final_manifest="$1" + local label installer + + label="$(windows_setup_label_from_manifest "${final_manifest}")" + if [ -z "${label}" ]; then + echo "Could not find Windows setup executable in ${final_manifest}." >&2 + return 1 + fi + + installer="$(artifact_for_label "${label}")" + "$(python3_runner)" "${REPO_ROOT}/scripts/ci/canonical-windows-nsis-payload-hash.py" \ + --verify-runtime-dlls \ + "${installer}" \ + "${label}" +} + verify_windows_canonical_manifest() { local manifest="$1" local kind digest label artifact actual @@ -451,6 +474,7 @@ verify_windows() { fi verify_file_manifest "${final_manifest}" + verify_windows_installer_runtime_payload "${final_manifest}" if [ -n "${runtime_manifest}" ]; then verify_windows_runtime_manifest "${runtime_manifest}" fi