mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
fix(install): detect Git Bash Mandatory ASLR failures (#64651)
This commit is contained in:
parent
c856f36459
commit
1f7d2be22f
4 changed files with 419 additions and 7 deletions
|
|
@ -705,6 +705,100 @@ function Test-Python {
|
|||
return $false
|
||||
}
|
||||
|
||||
$script:GitInstallFailureReason = $null
|
||||
$script:GitBashPath = $null
|
||||
$script:GitBashProbeOutput = $null
|
||||
|
||||
function Test-GitBashCompatibility {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Verify that Git Bash can launch external MSYS programs, not just evaluate
|
||||
shell builtins. Mandatory ASLR can allow bash.exe itself to start while
|
||||
every child linked to msys-2.0.dll fails during fork/spawn.
|
||||
#>
|
||||
param([Parameter(Mandatory = $true)][string]$BashPath)
|
||||
|
||||
$script:GitBashProbeOutput = $null
|
||||
if (-not (Test-Path -LiteralPath $BashPath)) {
|
||||
$script:GitBashProbeOutput = "bash.exe was not found at $BashPath"
|
||||
return $false
|
||||
}
|
||||
|
||||
$process = New-Object System.Diagnostics.Process
|
||||
try {
|
||||
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$startInfo.FileName = $BashPath
|
||||
$startInfo.Arguments = '--noprofile --norc -c "/usr/bin/true; /usr/bin/cat --version >/dev/null"'
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.CreateNoWindow = $true
|
||||
$startInfo.RedirectStandardOutput = $true
|
||||
$startInfo.RedirectStandardError = $true
|
||||
$process.StartInfo = $startInfo
|
||||
|
||||
if (-not $process.Start()) {
|
||||
$script:GitBashProbeOutput = "bash.exe did not start"
|
||||
return $false
|
||||
}
|
||||
if (-not $process.WaitForExit(15000)) {
|
||||
try { $process.Kill() } catch { }
|
||||
$script:GitBashProbeOutput = "Git Bash compatibility probe timed out"
|
||||
return $false
|
||||
}
|
||||
|
||||
$stdout = $process.StandardOutput.ReadToEnd()
|
||||
$stderr = $process.StandardError.ReadToEnd()
|
||||
$script:GitBashProbeOutput = ("$stdout`n$stderr").Trim()
|
||||
return ($process.ExitCode -eq 0)
|
||||
} catch {
|
||||
$script:GitBashProbeOutput = $_.Exception.Message
|
||||
return $false
|
||||
} finally {
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Test-MandatoryAslrEnabled {
|
||||
<# Return true only when Windows reports system-wide ForceRelocateImages=ON. #>
|
||||
try {
|
||||
$cmd = Get-Command Get-ProcessMitigation -ErrorAction SilentlyContinue
|
||||
if (-not $cmd) { return $false }
|
||||
$mitigations = & $cmd -System
|
||||
$value = $mitigations.Aslr.ForceRelocateImages
|
||||
return ($null -ne $value -and $value.ToString().ToUpperInvariant() -eq "ON")
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-GitRootFromBashPath {
|
||||
param([Parameter(Mandatory = $true)][string]$BashPath)
|
||||
|
||||
$binDir = Split-Path -Path $BashPath -Parent
|
||||
if ((Split-Path -Path $binDir -Leaf) -ine "bin") {
|
||||
return (Split-Path -Path $binDir -Parent)
|
||||
}
|
||||
|
||||
$parent = Split-Path -Path $binDir -Parent
|
||||
if ((Split-Path -Path $parent -Leaf) -ieq "usr") {
|
||||
return (Split-Path -Path $parent -Parent)
|
||||
}
|
||||
return $parent
|
||||
}
|
||||
|
||||
function New-GitBashAslrFailureReason {
|
||||
param([Parameter(Mandatory = $true)][string]$BashPath)
|
||||
|
||||
$gitRoot = Get-GitRootFromBashPath -BashPath $BashPath
|
||||
$escapedRoot = $gitRoot -replace "'", "''"
|
||||
return @(
|
||||
"Git Bash at $BashPath cannot launch required MSYS child processes because Windows Mandatory ASLR (ForceRelocateImages) is enabled system-wide. Reinstalling Git will not change this policy."
|
||||
"Open PowerShell as Administrator and run:"
|
||||
"`$gitRoot = '$escapedRoot'"
|
||||
'Get-Item "$gitRoot\bin\bash.exe", "$gitRoot\usr\bin\*.exe" -ErrorAction SilentlyContinue | ForEach-Object { Set-ProcessMitigation -Name $_.FullName -Disable ForceRelocateImages }'
|
||||
"Then rerun Hermes setup. If the override is blocked or later re-applied, ask your Windows administrator to allow this per-program exception."
|
||||
) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
function Install-Git {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
|
|
@ -737,13 +831,31 @@ function Install-Git {
|
|||
``HERMES_GIT_BASH_PATH`` (User scope) so Hermes can find it in a fresh
|
||||
shell without a second PATH refresh.
|
||||
#>
|
||||
$script:GitInstallFailureReason = $null
|
||||
Write-Info "Checking Git..."
|
||||
|
||||
if (Get-Command git -ErrorAction SilentlyContinue) {
|
||||
$version = git --version
|
||||
Write-Success "Git found ($version)"
|
||||
Set-GitBashEnvVar
|
||||
return $true
|
||||
if ($script:GitBashPath -and (Test-GitBashCompatibility -BashPath $script:GitBashPath)) {
|
||||
Write-Success "Git Bash can launch MSYS programs"
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($script:GitBashPath -and (Test-MandatoryAslrEnabled)) {
|
||||
$script:GitInstallFailureReason = New-GitBashAslrFailureReason -BashPath $script:GitBashPath
|
||||
Write-Err $script:GitInstallFailureReason
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($script:GitBashPath) {
|
||||
$probeDetail = if ($script:GitBashProbeOutput) { ": $script:GitBashProbeOutput" } else { "" }
|
||||
Write-Warn "System Git Bash could not launch required MSYS programs$probeDetail"
|
||||
} else {
|
||||
Write-Warn "Git is on PATH, but its Git Bash installation could not be located."
|
||||
}
|
||||
Write-Info "Trying a Hermes-managed PortableGit install instead..."
|
||||
}
|
||||
|
||||
# Download PortableGit into $HermesHome\git. Always works as long as
|
||||
|
|
@ -854,8 +966,25 @@ function Install-Git {
|
|||
$version = & $gitExe --version
|
||||
Write-Success "Git $version installed to $gitDir (portable, user-scoped)"
|
||||
Set-GitBashEnvVar
|
||||
if (-not $script:GitBashPath) {
|
||||
throw "PortableGit extraction did not produce a usable bash.exe"
|
||||
}
|
||||
if (-not (Test-GitBashCompatibility -BashPath $script:GitBashPath)) {
|
||||
if (Test-MandatoryAslrEnabled) {
|
||||
$script:GitInstallFailureReason = New-GitBashAslrFailureReason -BashPath $script:GitBashPath
|
||||
} else {
|
||||
$probeDetail = if ($script:GitBashProbeOutput) { " Probe output: $script:GitBashProbeOutput" } else { "" }
|
||||
$script:GitInstallFailureReason = "Git Bash at $script:GitBashPath exists but cannot launch required MSYS programs.$probeDetail"
|
||||
}
|
||||
throw $script:GitInstallFailureReason
|
||||
}
|
||||
Write-Success "Git Bash can launch MSYS programs"
|
||||
return $true
|
||||
} catch {
|
||||
if ($script:GitInstallFailureReason) {
|
||||
Write-Err $script:GitInstallFailureReason
|
||||
return $false
|
||||
}
|
||||
Write-Err "Could not install portable Git: $_"
|
||||
Write-Info ""
|
||||
Write-Info "Fallback: install Git manually from https://git-scm.com/download/win"
|
||||
|
|
@ -872,6 +1001,7 @@ function Set-GitBashEnvVar {
|
|||
``HERMES_GIT_BASH_PATH`` (User env scope) so Hermes can find it even before
|
||||
PATH propagation completes in a newly-spawned shell.
|
||||
#>
|
||||
$script:GitBashPath = $null
|
||||
$candidates = @()
|
||||
|
||||
# Our own portable Git install is ALWAYS checked first, so a broken
|
||||
|
|
@ -908,6 +1038,7 @@ function Set-GitBashEnvVar {
|
|||
if ($candidate -and (Test-Path $candidate)) {
|
||||
[Environment]::SetEnvironmentVariable("HERMES_GIT_BASH_PATH", $candidate, "User")
|
||||
$env:HERMES_GIT_BASH_PATH = $candidate
|
||||
$script:GitBashPath = $candidate
|
||||
Write-Info "Set HERMES_GIT_BASH_PATH=$candidate"
|
||||
return
|
||||
}
|
||||
|
|
@ -3293,7 +3424,12 @@ $InstallStages += @(
|
|||
# process), and throws cleanly if uv truly isn't installed yet.
|
||||
function Stage-Uv { if (-not (Install-Uv)) { throw "uv installation failed" } }
|
||||
function Stage-Python { Resolve-UvCmd; if (-not (Test-Python)) { throw "Python $PythonVersion not available" } }
|
||||
function Stage-Git { if (-not (Install-Git)) { throw "Git not available and auto-install failed -- install from https://git-scm.com/download/win then re-run" } }
|
||||
function Stage-Git {
|
||||
if (-not (Install-Git)) {
|
||||
if ($script:GitInstallFailureReason) { throw $script:GitInstallFailureReason }
|
||||
throw "Git not available and auto-install failed -- install from https://git-scm.com/download/win then re-run"
|
||||
}
|
||||
}
|
||||
# Node is optional (browser tools degrade gracefully without it). Surface
|
||||
# failure to the JSON contract as skipped=true / reason rather than ok=true,
|
||||
# so a GUI driver consuming the manifest can distinguish "node ready" from
|
||||
|
|
|
|||
120
scripts/tests/test-install-ps1-gitbash-compatibility.ps1
Normal file
120
scripts/tests/test-install-ps1-gitbash-compatibility.ps1
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Unit tests for install.ps1's Git Bash compatibility and Mandatory-ASLR
|
||||
# guidance helpers. The installer itself is never executed: functions are
|
||||
# extracted through the PowerShell AST to avoid downloads, PATH changes, or
|
||||
# user-environment writes.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path))
|
||||
$installScript = Join-Path $repoRoot "scripts\install.ps1"
|
||||
|
||||
$failures = 0
|
||||
function Assert-Equal {
|
||||
param($Expected, $Actual, [string]$Label)
|
||||
if ($Expected -ne $Actual) {
|
||||
Write-Host "FAIL: $Label" -ForegroundColor Red
|
||||
Write-Host " expected: $Expected"
|
||||
Write-Host " actual: $Actual"
|
||||
$script:failures++
|
||||
} else {
|
||||
Write-Host "OK: $Label" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
function Assert-True {
|
||||
param($Condition, [string]$Label)
|
||||
if (-not $Condition) {
|
||||
Write-Host "FAIL: $Label" -ForegroundColor Red
|
||||
$script:failures++
|
||||
} else {
|
||||
Write-Host "OK: $Label" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
$installScript, [ref]$tokens, [ref]$parseErrors
|
||||
)
|
||||
if ($parseErrors.Count -gt 0) {
|
||||
throw "install.ps1 has parse errors: $($parseErrors -join '; ')"
|
||||
}
|
||||
|
||||
foreach ($name in @(
|
||||
"Test-GitBashCompatibility",
|
||||
"Test-MandatoryAslrEnabled",
|
||||
"Get-GitRootFromBashPath",
|
||||
"New-GitBashAslrFailureReason",
|
||||
"Stage-Git"
|
||||
)) {
|
||||
$fnAst = $ast.FindAll(
|
||||
{
|
||||
param($node)
|
||||
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
|
||||
$node.Name -eq $name
|
||||
}, $true
|
||||
) | Select-Object -First 1
|
||||
if (-not $fnAst) { throw "$name not found in install.ps1" }
|
||||
. ([scriptblock]::Create($fnAst.Extent.Text))
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "-- Git root resolution --"
|
||||
Assert-Equal "C:\Program Files\Git" `
|
||||
(Get-GitRootFromBashPath "C:\Program Files\Git\bin\bash.exe") `
|
||||
"PortableGit/full Git bin layout"
|
||||
Assert-Equal "C:\Program Files\Git" `
|
||||
(Get-GitRootFromBashPath "C:\Program Files\Git\usr\bin\bash.exe") `
|
||||
"usr/bin layout"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "-- Mandatory ASLR detection --"
|
||||
function Get-ProcessMitigation {
|
||||
param([switch]$System)
|
||||
[pscustomobject]@{ Aslr = [pscustomobject]@{ ForceRelocateImages = "ON" } }
|
||||
}
|
||||
Assert-Equal $true (Test-MandatoryAslrEnabled) "ForceRelocateImages ON is detected"
|
||||
function Get-ProcessMitigation {
|
||||
param([switch]$System)
|
||||
[pscustomobject]@{ Aslr = [pscustomobject]@{ ForceRelocateImages = "NOTSET" } }
|
||||
}
|
||||
Assert-Equal $false (Test-MandatoryAslrEnabled) "ForceRelocateImages NOTSET is not diagnosed"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "-- Actionable remediation --"
|
||||
$reason = New-GitBashAslrFailureReason "C:\Program Files\Git\bin\bash.exe"
|
||||
Assert-True ($reason -match "Mandatory ASLR") "reason identifies Mandatory ASLR"
|
||||
Assert-True ($reason -match "Reinstalling Git will not change") "reason rejects ineffective reinstall"
|
||||
Assert-True ($reason -match [regex]::Escape("C:\Program Files\Git")) "reason uses selected Git root"
|
||||
Assert-True ($reason -match "Set-ProcessMitigation") "reason includes targeted mitigation command"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "-- External-program probe --"
|
||||
$gitCommand = Get-Command git -ErrorAction SilentlyContinue
|
||||
$gitBash = $null
|
||||
if ($gitCommand -and $gitCommand.Source) {
|
||||
$gitRoot = Split-Path (Split-Path $gitCommand.Source -Parent) -Parent
|
||||
foreach ($candidate in @("$gitRoot\bin\bash.exe", "$gitRoot\usr\bin\bash.exe")) {
|
||||
if (Test-Path -LiteralPath $candidate) { $gitBash = $candidate; break }
|
||||
}
|
||||
}
|
||||
if ($gitBash) {
|
||||
Assert-Equal $true (Test-GitBashCompatibility $gitBash) `
|
||||
"installed Git Bash launches external MSYS programs"
|
||||
} else {
|
||||
Write-Host "SKIP: no Git Bash found next to git.exe" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "-- Stage failure propagation --"
|
||||
function Install-Git { return $false }
|
||||
$script:GitInstallFailureReason = "specific Git Bash failure"
|
||||
$stageError = $null
|
||||
try { Stage-Git } catch { $stageError = $_.Exception.Message }
|
||||
Assert-Equal "specific Git Bash failure" $stageError "Git stage preserves actionable reason"
|
||||
|
||||
Write-Host ""
|
||||
if ($failures -gt 0) {
|
||||
Write-Host "FAILED: $failures assertion(s) failed" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "All Git Bash compatibility tests passed." -ForegroundColor Green
|
||||
exit 0
|
||||
|
|
@ -143,6 +143,62 @@ class TestFindBashSkipsBrokenCustomPath:
|
|||
assert _find_bash() == str(portable)
|
||||
|
||||
|
||||
class TestGitBashExternalProgramProbe:
|
||||
"""The Windows health check must exercise MSYS child-process creation."""
|
||||
|
||||
def test_probe_runs_external_msys_programs(self, monkeypatch):
|
||||
import tools.environments.local as local_mod
|
||||
|
||||
local_mod._bash_starts_cache.clear()
|
||||
local_mod._bash_probe_details_cache.clear()
|
||||
calls = []
|
||||
|
||||
def fake_run(argv, **kwargs):
|
||||
calls.append((argv, kwargs))
|
||||
return subprocess.CompletedProcess(argv, 0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setattr(local_mod.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
|
||||
|
||||
assert local_mod._bash_starts(r"C:\Git\bin\bash.exe") is True
|
||||
assert calls[0][0][-1] == "/usr/bin/true; /usr/bin/cat --version >/dev/null"
|
||||
|
||||
def test_aslr_failure_surfaces_targeted_windows_command(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
import tools.environments.local as local_mod
|
||||
|
||||
local_mod._bash_starts_cache.clear()
|
||||
local_mod._bash_probe_details_cache.clear()
|
||||
portable = tmp_path / "hermes" / "git" / "bin" / "bash.exe"
|
||||
portable.parent.mkdir(parents=True)
|
||||
portable.write_text("", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(local_mod, "_IS_WINDOWS", True)
|
||||
monkeypatch.setenv("HERMES_GIT_BASH_PATH", "")
|
||||
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path))
|
||||
monkeypatch.setenv("ProgramFiles", str(tmp_path / "empty-program-files"))
|
||||
monkeypatch.delenv("ProgramFiles(x86)", raising=False)
|
||||
monkeypatch.setattr(local_mod.shutil, "which", lambda _name: None)
|
||||
monkeypatch.setattr(local_mod, "_mandatory_aslr_enabled", lambda: True)
|
||||
|
||||
def failed_probe(path: str) -> bool:
|
||||
local_mod._bash_probe_details_cache[path] = (
|
||||
"dofork: child -1 - forked process died unexpectedly"
|
||||
)
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(local_mod, "_bash_starts", failed_probe)
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
local_mod._find_bash()
|
||||
message = str(exc_info.value)
|
||||
assert "Mandatory ASLR" in message
|
||||
assert "Reinstalling Git will not change" in message
|
||||
assert "Set-ProcessMitigation" in message
|
||||
assert str(tmp_path / "hermes" / "git") in message
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.isfile("/bin/bash") or sys.platform != "darwin",
|
||||
reason="reproduces the macOS system-bash-3.2 login-shell swallow",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Local execution environment — spawn-per-call with session snapshot."""
|
||||
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
|
@ -653,8 +654,19 @@ def _find_bash() -> str:
|
|||
return candidate
|
||||
|
||||
if candidates:
|
||||
# Last resort: return the first path even if the probe failed, so the
|
||||
# caller still sees the real bash error instead of "not found".
|
||||
probe_details = "\n".join(
|
||||
detail
|
||||
for candidate in candidates
|
||||
if (detail := _bash_probe_details_cache.get(candidate))
|
||||
)
|
||||
if _mandatory_aslr_enabled() is True or _looks_like_msys_spawn_failure(
|
||||
probe_details
|
||||
):
|
||||
raise RuntimeError(_git_bash_aslr_help(candidates[0], probe_details))
|
||||
|
||||
# Last resort for failures unrelated to the known MSYS/ASLR class:
|
||||
# return the first path so the caller still sees the real bash error
|
||||
# instead of the less useful "not found" message.
|
||||
return candidates[0]
|
||||
|
||||
raise RuntimeError(
|
||||
|
|
@ -665,14 +677,100 @@ def _find_bash() -> str:
|
|||
|
||||
|
||||
_bash_starts_cache: dict[str, bool] = {}
|
||||
_bash_probe_details_cache: dict[str, str] = {}
|
||||
_mandatory_aslr_enabled_cache: "bool | None" = None
|
||||
|
||||
_BASH_EXTERNAL_PROGRAM_PROBE = "/usr/bin/true; /usr/bin/cat --version >/dev/null"
|
||||
|
||||
|
||||
def _looks_like_msys_spawn_failure(details: str) -> bool:
|
||||
"""Match Git-for-Windows child-launch failures associated with ASLR."""
|
||||
lowered = details.lower()
|
||||
return any(
|
||||
marker in lowered
|
||||
for marker in (
|
||||
"dofork:",
|
||||
"child_copy:",
|
||||
"0xc0000142",
|
||||
"0xc0000005",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _mandatory_aslr_enabled() -> "bool | None":
|
||||
"""Return Windows' system-wide ForceRelocateImages state when available."""
|
||||
global _mandatory_aslr_enabled_cache
|
||||
if _mandatory_aslr_enabled_cache is not None:
|
||||
return _mandatory_aslr_enabled_cache
|
||||
|
||||
try:
|
||||
powershell = shutil.which("powershell.exe") or "powershell.exe"
|
||||
result = subprocess.run(
|
||||
[
|
||||
powershell,
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"(Get-ProcessMitigation -System).Aslr.ForceRelocateImages.ToString()",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
value = (result.stdout or "").strip().upper()
|
||||
if value == "ON":
|
||||
_mandatory_aslr_enabled_cache = True
|
||||
return True
|
||||
if value in {"OFF", "NOTSET"}:
|
||||
_mandatory_aslr_enabled_cache = False
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.debug("Could not query Windows Mandatory ASLR state: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _git_root_from_bash(bash: str) -> str:
|
||||
"""Resolve Git's root from either <root>/bin or <root>/usr/bin bash."""
|
||||
bin_dir = ntpath.dirname(ntpath.normpath(bash))
|
||||
if ntpath.basename(bin_dir).lower() != "bin":
|
||||
return ntpath.dirname(bin_dir)
|
||||
parent = ntpath.dirname(bin_dir)
|
||||
if ntpath.basename(parent).lower() == "usr":
|
||||
return ntpath.dirname(parent)
|
||||
return parent
|
||||
|
||||
|
||||
def _git_bash_aslr_help(bash: str, details: str = "") -> str:
|
||||
"""Build the targeted per-program Mandatory-ASLR remediation."""
|
||||
git_root = _git_root_from_bash(bash)
|
||||
escaped_root = git_root.replace("'", "''")
|
||||
detail_line = f"\nGit Bash probe output: {details[:500]}" if details else ""
|
||||
return (
|
||||
f"Git Bash at {bash} cannot launch required MSYS child processes while "
|
||||
"Windows Mandatory ASLR (ForceRelocateImages) is enabled, or its output "
|
||||
f"matches that Git-for-Windows failure class.{detail_line}\n"
|
||||
"Reinstalling Git will not change the Windows mitigation policy. Open "
|
||||
"PowerShell as Administrator and run:\n"
|
||||
f"$gitRoot = '{escaped_root}'\n"
|
||||
'Get-Item "$gitRoot\\bin\\bash.exe", "$gitRoot\\usr\\bin\\*.exe" '
|
||||
"-ErrorAction SilentlyContinue | ForEach-Object { "
|
||||
"Set-ProcessMitigation -Name $_.FullName -Disable ForceRelocateImages }\n"
|
||||
"Then restart Hermes. If the override is blocked or later re-applied, "
|
||||
"ask your Windows administrator to allow this per-program exception."
|
||||
)
|
||||
|
||||
|
||||
def _bash_starts(bash: str) -> bool:
|
||||
"""True if *bash* can run a trivial non-login command.
|
||||
"""True if *bash* can launch external MSYS programs.
|
||||
|
||||
Uses ``--noprofile --norc`` so a broken login post-install
|
||||
(``Directory \\drivers\\etc``) does not falsely condemn an otherwise
|
||||
usable bash. Cached per path for the process lifetime.
|
||||
usable bash. The external ``true`` and ``cat`` calls are intentional:
|
||||
a builtin-only ``exit 0`` probe misses Git-for-Windows fork/spawn failures
|
||||
under system-wide Mandatory ASLR. Cached per path for the process lifetime.
|
||||
"""
|
||||
cached = _bash_starts_cache.get(bash)
|
||||
if cached is not None:
|
||||
|
|
@ -680,7 +778,7 @@ def _bash_starts(bash: str) -> bool:
|
|||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[bash, "--noprofile", "--norc", "-c", "exit 0"],
|
||||
[bash, "--noprofile", "--norc", "-c", _BASH_EXTERNAL_PROGRAM_PROBE],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
|
|
@ -689,8 +787,10 @@ def _bash_starts(bash: str) -> bool:
|
|||
ok = result.returncode == 0
|
||||
if not ok:
|
||||
combined = f"{result.stdout or ''}{result.stderr or ''}"
|
||||
_bash_probe_details_cache[bash] = combined.strip()[:2000]
|
||||
logger.debug("bash probe failed for %s: %s", bash, combined.strip()[:200])
|
||||
except Exception as exc:
|
||||
_bash_probe_details_cache[bash] = str(exc)[:2000]
|
||||
logger.debug("bash probe error for %s: %s", bash, exc)
|
||||
ok = False
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue