Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/windows/Bootstrap-Windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function Invoke-PrerequisiteEvaluation {

$gitBash = $prereqs.GitBash.Path
if (-not $prereqs.GitBash.Found) {
Add-Failure "Git Bash" "bash.exe was not found under Program Files\Git" "Git.Git"
Add-Failure "Git Bash" "Git for Windows bash.exe was not found in an installed or portable Git, or beside the git.exe on PATH (WSL, Microsoft Store, Cygwin, and MSYS2 bash.exe do not count)" "Git.Git"
} else {
Add-Pass "Git Bash" $gitBash
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/Doctor-Windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Check-Command "git" "Run: just bootstrap-windows install" $prereqs.Git | Out-Nul

$gitBash = $prereqs.GitBash.Path
if (-not $prereqs.GitBash.Found) {
Fail "Git Bash" "bash.exe not found. Install Git for Windows with: winget install --id Git.Git -e"
Fail "Git Bash" "Git for Windows bash.exe not found in an installed or portable Git, or beside the git.exe on PATH (WSL, Microsoft Store, Cygwin, and MSYS2 bash.exe do not count). Install Git for Windows with: winget install --id Git.Git -e"
} else {
Pass "Git Bash" $gitBash
}
Expand Down
43 changes: 43 additions & 0 deletions scripts/windows/Test-WindowsDev.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,49 @@ try {
Assert-Equal "process args: trailing backslash doubled inside quotes" (Join-WindowsProcessArguments -Arguments @("C:\Program Files\")) '"C:\Program Files\\"'
Assert-Equal "process args: embedded quote escaped" (Join-WindowsProcessArguments -Arguments @('say "hi"')) '"say \"hi\""'

# Git Bash discovery must never hand back the bash.exe stubs Windows puts
# ahead of Git on PATH (the WSL launcher, the Store app-execution alias).
Assert-Equal "git bash filter rejects the WSL launcher" (Test-GitBashCandidatePath "C:\Windows\System32\bash.exe") $false
Assert-Equal "git bash filter rejects the 32-bit WSL launcher" (Test-GitBashCandidatePath "C:\Windows\SysWOW64\bash.exe") $false
Assert-Equal "git bash filter rejects the Store app-execution alias" `
(Test-GitBashCandidatePath "C:\Users\dev\AppData\Local\Microsoft\WindowsApps\bash.exe") $false
Assert-Equal "git bash filter rejects Codex runtimes" (Test-GitBashCandidatePath "C:\Users\dev\.cache\codex-runtimes\git\bin\bash.exe") $false
Assert-Equal "git bash filter rejects blank paths" (Test-GitBashCandidatePath "") $false
Assert-Equal "git bash filter accepts the machine-wide Git install" (Test-GitBashCandidatePath "C:\Program Files\Git\bin\bash.exe") $true
Assert-Equal "git bash filter accepts a per-user Git install" `
(Test-GitBashCandidatePath "C:\Users\dev\AppData\Local\Programs\Git\usr\bin\bash.exe") $true
# Cygwin and MSYS2 ship bash.exe in the same bin\ and usr\bin\ shapes as
# Git for Windows, so the layout check must positively require the
# <root>\cmd\git.exe that only Git for Windows (installed or portable)
# provides. The file-exists probe is injected so no real installs are
# needed and the outcome is deterministic on every machine.
$gitForWindowsFixture = { param($p) $p -in @("D:\PortableGit\cmd\git.exe", "C:\Program Files\Git\cmd\git.exe") }
Assert-Equal "git bash layout rejects Cygwin bash" `
(Test-GitForWindowsLayout "C:\cygwin64\bin\bash.exe" -FileExists $gitForWindowsFixture) $false
Assert-Equal "git bash layout rejects MSYS2 bash" `
(Test-GitForWindowsLayout "C:\msys64\usr\bin\bash.exe" -FileExists $gitForWindowsFixture) $false
Assert-Equal "git bash layout accepts portable Git derived from git.exe" `
(Test-GitForWindowsLayout "D:\PortableGit\usr\bin\bash.exe" -FileExists $gitForWindowsFixture) $true
Assert-Equal "git bash layout accepts the machine-wide Git install" `
(Test-GitForWindowsLayout "C:\Program Files\Git\bin\bash.exe" -FileExists $gitForWindowsFixture) $true
Assert-Equal "git bash layout rejects a Git-shaped tree without cmd\git.exe" `
(Test-GitForWindowsLayout "C:\Program Files\Git\bin\bash.exe" -FileExists { param($p) $false }) $false
Assert-Equal "git bash layout rejects bash outside a bin directory" `
(Test-GitForWindowsLayout "C:\tools\bash.exe" -FileExists { param($p) $true }) $false
Assert-Equal "git bash layout rejects blank paths" (Test-GitForWindowsLayout "" -FileExists { param($p) $true }) $false
Assert-Equal "git bash lookup applies the filter to every candidate" `
((Get-Command Get-GitBashPath -CommandType Function).Definition -match 'Test-GitBashCandidatePath \$candidate') $true
Assert-Equal "git bash lookup verifies the Git for Windows layout of every candidate" `
((Get-Command Get-GitBashPath -CommandType Function).Definition -match 'Test-GitForWindowsLayout \$candidate') $true
Assert-Equal "git bash lookup derives candidates from git.exe" `
((Get-Command Get-GitBashPath -CommandType Function).Definition -match 'Get-CommandSource "git"') $true
$gitBashPath = Get-GitBashPath
if (-not [string]::IsNullOrWhiteSpace($gitBashPath)) {
Assert-Equal "git bash lookup result passes its own filter" (Test-GitBashCandidatePath $gitBashPath) $true
Assert-Equal "git bash lookup result is a Git for Windows layout" (Test-GitForWindowsLayout $gitBashPath) $true
Assert-Equal "git bash lookup result exists" (Test-Path -LiteralPath $gitBashPath -PathType Leaf) $true
}

Assert-Equal "public app feature defaults fail closed" (Get-BerdAppFeatures) "berdctl,app-test-driver"
$featureGateNames = @("VITE_AGENT_TOOLS", "VITE_AUTOMATIONS", "VITE_BUILDERBOT", "VITE_FEEDBACK", "VITE_MANAGED_CONNECTIONS", "VITE_SKILL_DISCOVERY", "VITE_TELEMETRY_ENFORCED", "VITE_VOICE_DICTATION")
$savedFeatureGates = @{}
Expand Down
95 changes: 86 additions & 9 deletions scripts/windows/WindowsDev.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -1589,21 +1589,98 @@ function Get-RustHostTriple {
return $null
}

function Test-GitBashCandidatePath {
# True when a bash.exe path can plausibly run repository scripts. Windows
# ships two bash.exe stubs that often precede Git for Windows on PATH and
# cannot: the WSL launcher in System32 and the Microsoft Store
# app-execution alias under WindowsApps. Pure string check so it is
# testable without those files present.
param([AllowNull()][string]$Path)
if ([string]::IsNullOrWhiteSpace($Path)) {
return $false
}
if ($Path -match '\\WindowsApps\\') {
return $false
}
if ($Path -match '\\(System32|SysWOW64)\\bash\.exe$') {
return $false
}
if (Test-CodexRuntimePath $Path) {
return $false
}
return $true
}

function Test-GitForWindowsLayout {
# True only when a bash.exe path sits inside a Git for Windows tree. Git
# for Windows (installed or portable) places bash.exe at <root>\bin\bash.exe
# and <root>\usr\bin\bash.exe, and always ships <root>\cmd\git.exe beside
# them. Cygwin (C:\cygwin64\bin\bash.exe) and MSYS2
# (C:\msys64\usr\bin\bash.exe) use the same bin\ and usr\bin\ shapes but
# have no cmd\git.exe, so requiring that file positively identifies Git for
# Windows instead of trusting whichever bash.exe happens to be on PATH.
# $FileExists is injectable so the check is testable without the files.
param(
[AllowNull()][string]$BashPath,
[scriptblock]$FileExists = { param($p) Test-Path -LiteralPath $p -PathType Leaf }
)
if ([string]::IsNullOrWhiteSpace($BashPath)) {
return $false
}
# Lazy root so `<root>\usr\bin\bash.exe` yields <root>, not <root>\usr.
if ($BashPath -notmatch '^(?<root>.+?)\\(usr\\)?bin\\bash\.exe$') {
return $false
}
# String concatenation instead of Join-Path: Windows PowerShell 5.1's
# Join-Path fails when the drive letter does not exist on this machine,
# which would break the pure check for injected fixtures.
$gitExe = $Matches['root'].TrimEnd('\') + '\cmd\git.exe'
return [bool](& $FileExists $gitExe)
}

function Get-GitBashPath {
$candidates = @()
if (-not [string]::IsNullOrWhiteSpace($env:ProgramFiles)) {
$candidates += (Join-Path $env:ProgramFiles "Git\bin\bash.exe")
# Prefer Git for Windows' own bash.exe. Machine-wide and per-user (winget
# user scope) install roots come first, then the root of whichever git.exe
# is on PATH (covers portable Git), then every bash.exe PATH resolves to.
# Every candidate must pass two checks before it is accepted: the string
# filter that drops the WSL/Store stubs and Codex runtimes, and a positive
# Git for Windows layout check (<root>\cmd\git.exe must exist beside it) so
# Cygwin, MSYS2, or a standalone bash.exe on PATH never passes as the Git
# Bash prerequisite. Returns $null when nothing usable exists so bootstrap
# and doctor report a real failure instead of an unrelated bash.
$candidates = New-Object System.Collections.Generic.List[string]
foreach ($root in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, (Join-Path (Get-LocalAppDataRoot) "Programs"))) {
if (-not [string]::IsNullOrWhiteSpace($root)) {
$candidates.Add((Join-Path $root "Git\bin\bash.exe"))
}
}
$programFilesX86 = ${env:ProgramFiles(x86)}
if (-not [string]::IsNullOrWhiteSpace($programFilesX86)) {
$candidates += (Join-Path $programFilesX86 "Git\bin\bash.exe")

$git = Get-CommandSource "git"
if (-not [string]::IsNullOrWhiteSpace($git) -and -not (Test-CodexRuntimePath $git)) {
# <root>\cmd\git.exe or <root>\bin\git.exe; bash lives under <root>\bin
# and <root>\usr\bin.
$gitRoot = Split-Path -Parent (Split-Path -Parent $git)
if (-not [string]::IsNullOrWhiteSpace($gitRoot)) {
$candidates.Add((Join-Path $gitRoot "bin\bash.exe"))
$candidates.Add((Join-Path $gitRoot "usr\bin\bash.exe"))
}
}
foreach ($candidate in $candidates) {
if (Test-Path $candidate -PathType Leaf) {

$whereBash = Invoke-CaptureCommand -FilePath "where.exe" -ArgumentList @("bash")
if ($whereBash.ExitCode -eq 0) {
foreach ($line in ($whereBash.Output -split "`r?`n")) {
if (-not [string]::IsNullOrWhiteSpace($line)) {
$candidates.Add($line.Trim())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 P1 · Unrelated Bash passes as Git Bash (blocking)

The lookup adds every path returned by where.exe bash, while the candidate filter rejects only WindowsApps, System32/SysWOW64 stubs, and Codex runtimes. A Cygwin, MSYS2, or standalone Bash path therefore passes as the Git-for-Windows prerequisite even when Git Bash is absent, preserving the original false-positive failure through another fallback.

User effect: Windows bootstrap and doctor can say Git Bash is ready when it is not, so people may continue into setup without the required tool and fail later without the expected installation guidance.

Recommended fix: Remove the unrestricted Bash-on-PATH fallback, or positively verify that every accepted candidate belongs to a Git for Windows installation associated with a trusted install root or the resolved git.exe layout.

Test: Add deterministic selection coverage where the only PATH result is Cygwin or MSYS2 Bash and assert that discovery returns null, plus coverage showing a portable Git installation derived from git.exe is accepted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 792becc. Get-GitBashPath now positively verifies every candidate (including the ones from where.exe bash and the one derived from git.exe) with a new pure Test-GitForWindowsLayout check: the path must be <root>\bin\bash.exe or <root>\usr\bin\bash.exe and <root>\cmd\git.exe must exist. That file is present in every Git for Windows install (installed and portable) and absent from Cygwin and MSYS2, so an unrelated Bash on PATH can no longer satisfy the prerequisite. The file-exists probe is injectable, and Test-WindowsDev.ps1 now asserts Cygwin (C:\cygwin64\bin\bash.exe) and MSYS2 (C:\msys64\usr\bin\bash.exe) are rejected, a portable Git derived from git.exe (D:\PortableGit\usr\bin\bash.exe) is accepted, a Git-shaped tree missing cmd\git.exe is rejected, and that the lookup applies the layout check to every candidate. Bootstrap/doctor failure text now names what counts.

}
}
}

foreach ($candidate in ($candidates | Select-Object -Unique)) {
if ((Test-GitBashCandidatePath $candidate) -and (Test-GitForWindowsLayout $candidate) -and (Test-Path -LiteralPath $candidate -PathType Leaf)) {
return $candidate
}
}
return (Get-CommandSource "bash")
return $null
}

function Get-VsWherePath {
Expand Down