diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5933ca2a..b8f1fe6587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - **core:** Fix substitute handling of substring keys ([#6561](https://github.com/ScoopInstaller/Scoop/issues/6561)) - **core:** Check `$deprecated_dir` exists before accessing it ([#6574](https://github.com/ScoopInstaller/Scoop/issues/6574)) - **checkver:** Remove redundant always-true condition in GitHub checkver logic ([#6571](https://github.com/ScoopInstaller/Scoop/issues/6571)) +- **shim:** Fix WoW64 file system redirection for x86 shim executables on x64 OS by rewriting System32/SysWOW64 paths in `.shim` config ([#6619](https://github.com/ScoopInstaller/Scoop/issues/6619)) ### Code Refactoring diff --git a/lib/core.ps1 b/lib/core.ps1 index 11daac73f5..b8915e6914 100644 --- a/lib/core.ps1 +++ b/lib/core.ps1 @@ -21,6 +21,26 @@ function Get-PESubsystem($filePath) { } } +function Get-PEMachine($filePath) { + try { + $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $binaryReader = [System.IO.BinaryReader]::new($fileStream) + + $fileStream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) | Out-Null + $peOffset = $binaryReader.ReadInt32() + + # Machine field is at PE signature (4 bytes) + offset 0 of COFF header + $fileStream.Seek($peOffset + 4, [System.IO.SeekOrigin]::Begin) | Out-Null + + return $binaryReader.ReadUInt16() + } catch { + return 0 + } finally { + if ($null -ne $binaryReader) { $binaryReader.Close() } + if ($null -ne $fileStream) { $fileStream.Close() } + } +} + function Set-PESubsystem($filePath, $targetSubsystem) { try { $fileStream = [System.IO.FileStream]::new($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite) @@ -867,7 +887,12 @@ function Get-ShimTarget($ShimPath) { if (!$shimTarget) { $shimTarget = ((Select-String -Path $ShimPath -Pattern '[''"]([^@&]*?)[''"]' -AllMatches).Matches.Groups | Select-Object -Last 1).Value } - $shimTarget | Convert-Path -ErrorAction SilentlyContinue + $shimTargetConverted = $shimTarget | Convert-Path -ErrorAction SilentlyContinue + if (!$shimTargetConverted -and $shimTarget.Contains('\Sysnative\')) { + $shimTarget.Replace('\Sysnative\', '\System32\') | Convert-Path -ErrorAction SilentlyContinue + } else { + return $shimTargetConverted + } } } @@ -908,7 +933,27 @@ function shim($path, $global, $name, $arg) { # for programs with no awareness of any shell warn_on_overwrite "$shim.shim" $path Copy-Item (get_shim_path) "$shim.exe" -Force - Write-Output "path = `"$resolved_path`"" | Out-UTF8File "$shim.shim" + + $rewrote_path = $resolved_path + + # If the shim exe is x86 on a x64 OS, rewrite paths so the shim resolves correctly: + # System32 -> Sysnative (x64 resolve x64 program in System32, and it should be Sysnative in x86 program) + # SysWOW64 -> System32 (x64 resolve x86 program in SysWOW64, and it should be System32 in x86 program) + $shim_machine = Get-PEMachine "$shim.exe" + # 0x014c is IMAGE_FILE_MACHINE_I386 + # https://learn.microsoft.com/en-us/windows/win32/sysinfo/image-file-machine-constants + if ($shim_machine -eq 0x014c -and [System.Environment]::Is64BitOperatingSystem) { + $sysdir = [System.IO.Path]::Combine($env:SystemRoot, 'System32') + $sysnative = [System.IO.Path]::Combine($env:SystemRoot, 'Sysnative') + $syswow = [System.IO.Path]::Combine($env:SystemRoot, 'SysWOW64') + if ($rewrote_path -like "$sysdir\*") { + $rewrote_path = $rewrote_path -replace [regex]::Escape($sysdir), $sysnative + } elseif ($rewrote_path -like "$syswow\*") { + $rewrote_path = $rewrote_path -replace [regex]::Escape($syswow), $sysdir + } + } + + Write-Output "path = `"$rewrote_path`"" | Out-UTF8File "$shim.shim" if ($arg) { Write-Output "args = $arg" | Out-UTF8File "$shim.shim" -Append } diff --git a/test/Scoop-Core.Tests.ps1 b/test/Scoop-Core.Tests.ps1 index a857ec47e4..a973221418 100644 --- a/test/Scoop-Core.Tests.ps1 +++ b/test/Scoop-Core.Tests.ps1 @@ -357,6 +357,71 @@ Describe 'app' -Tag 'Scoop' { } } +Describe 'Get-PEMachine' -Tag 'Scoop', 'Windows' { + It 'returns machine type for a valid PE file' { + $shim_path = get_shim_path + if ($shim_path -and (Test-Path $shim_path)) { + $machine = Get-PEMachine $shim_path + # Should be a known machine type (I386 (x86): 0x014c, amd64 (x64): 0x8664, arm64: 0xAA64) + # https://learn.microsoft.com/en-us/windows/win32/sysinfo/image-file-machine-constants + $machine | Should -BeIn @(0x014c, 0x8664, 0xAA64) + } else { + Set-ItResult -Skipped -Because 'shim exe not found' + } + } + + It 'returns 0 for a non-existent file' { + Get-PEMachine 'C:\nonexistent\fake.exe' | Should -Be 0 + } + + It 'returns 0 for a non-PE file' { + $working_dir = setup_working 'shim' + Get-PEMachine "$working_dir\shim-test.ps1" | Should -Be 0 + } +} + +Describe 'WoW64 path rewriting in shim' -Tag 'Scoop', 'Windows' { + It 'rewrites System32 to Sysnative for x86 shim on x64 OS' { + $sysdir = [System.IO.Path]::Combine($env:SystemRoot, 'System32') + $sysnative = [System.IO.Path]::Combine($env:SystemRoot, 'Sysnative') + $testPath = "$sysdir\notepad.exe" + + if ([System.Environment]::Is64BitOperatingSystem) { + $result = $testPath -replace [regex]::Escape($sysdir), $sysnative + $result | Should -Be "$sysnative\notepad.exe" + } else { + Set-ItResult -Skipped -Because 'not a x64 OS' + } + } + + It 'rewrites SysWOW64 to System32 for x86 shim on x64 OS' { + $sysdir = [System.IO.Path]::Combine($env:SystemRoot, 'System32') + $syswow = [System.IO.Path]::Combine($env:SystemRoot, 'SysWOW64') + $testPath = "$syswow\notepad.exe" + + if ([System.Environment]::Is64BitOperatingSystem) { + $result = $testPath -replace [regex]::Escape($syswow), $sysdir + $result | Should -Be "$sysdir\notepad.exe" + } else { + Set-ItResult -Skipped -Because 'not a x64 OS' + } + } + + It 'does not rewrite paths outside System32 and SysWOW64' { + $sysdir = [System.IO.Path]::Combine($env:SystemRoot, 'System32') + $syswow = [System.IO.Path]::Combine($env:SystemRoot, 'SysWOW64') + $testPath = 'C:\Program Files\test\app.exe' + + $result = $testPath + if ($result -like "$sysdir\*") { + $result = $result -replace [regex]::Escape($sysdir), 'Sysnative' + } elseif ($result -like "$syswow\*") { + $result = $result -replace [regex]::Escape($syswow), $sysdir + } + $result | Should -Be $testPath + } +} + Describe 'Format Architecture String' -Tag 'Scoop' { It 'should keep correct architectures' { Format-ArchitectureString '32bit' | Should -Be '32bit'