Fix Windows host agent installer reliability (related to #654)

The download endpoint had a dangerous fallback that silently served the
wrong binary when the requested platform/arch combination was missing.
If a Docker image shipped without Windows binaries, the installer would
receive a Linux ELF instead of a Windows PE, causing ERROR_BAD_EXE_FORMAT.

Changes:
- Download handler now operates in strict mode when platform+arch are
  specified, returning 404 instead of serving mismatched binaries
- PowerShell installer validates PE header (MZ signature)
- PowerShell installer verifies PE machine type matches requested arch
- PowerShell installer fetches and verifies SHA256 checksums
- PowerShell installer shows diagnostic info: OS arch, download URL,
  file size for better troubleshooting

This prevents silent failures and provides clear error messages when
binaries are missing or corrupted.
This commit is contained in:
rcourtman 2025-11-07 22:55:03 +00:00
parent 2b7492ac59
commit 16c29463f9
2 changed files with 76 additions and 8 deletions

View file

@ -3298,9 +3298,10 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
archParam := strings.TrimSpace(req.URL.Query().Get("arch"))
searchPaths := make([]string, 0, 12)
strictMode := platformParam != "" && archParam != ""
// Try platform-specific binary first
if platformParam != "" && archParam != "" {
if strictMode {
searchPaths = append(searchPaths,
filepath.Join("/opt/pulse/bin", fmt.Sprintf("pulse-host-agent-%s-%s", platformParam, archParam)),
filepath.Join("/opt/pulse", fmt.Sprintf("pulse-host-agent-%s-%s", platformParam, archParam)),
@ -3308,7 +3309,7 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
)
}
if platformParam != "" {
if platformParam != "" && !strictMode {
searchPaths = append(searchPaths,
filepath.Join("/opt/pulse/bin", "pulse-host-agent-"+platformParam),
filepath.Join("/opt/pulse", "pulse-host-agent-"+platformParam),
@ -3316,12 +3317,14 @@ func (r *Router) handleDownloadHostAgent(w http.ResponseWriter, req *http.Reques
)
}
// Default locations (host architecture)
searchPaths = append(searchPaths,
filepath.Join("/opt/pulse/bin", "pulse-host-agent"),
"/opt/pulse/pulse-host-agent",
filepath.Join("/app", "pulse-host-agent"),
)
// Default locations (host architecture) - only when no platform/arch specified
if !strictMode && platformParam == "" {
searchPaths = append(searchPaths,
filepath.Join("/opt/pulse/bin", "pulse-host-agent"),
"/opt/pulse/pulse-host-agent",
filepath.Join("/app", "pulse-host-agent"),
)
}
for _, candidate := range searchPaths {
if candidate == "" {

View file

@ -143,6 +143,12 @@ switch ($osArch) {
}
$downloadUrl = "$PulseUrl/download/pulse-host-agent?platform=windows&arch=$arch"
Write-Info "System Information:"
Write-Host " OS Architecture: $osArch"
Write-Host " Download Architecture: $arch"
Write-Host " Download URL: $downloadUrl"
Write-Host ""
Write-Info "Downloading agent binary from $downloadUrl..."
try {
# Create install directory
@ -156,6 +162,65 @@ try {
Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
Write-Success "Downloaded agent to $agentPath"
# Validate PE header
$fileBytes = [System.IO.File]::ReadAllBytes($agentPath)
$fileSizeMB = [math]::Round($fileBytes.Length / 1MB, 2)
Write-Info "File size: $fileSizeMB MB ($($fileBytes.Length) bytes)"
if ($fileBytes.Length -lt 64) {
throw "Downloaded file is too small ($($fileBytes.Length) bytes) - expected Windows PE executable"
}
# Check for MZ signature (PE header)
if ($fileBytes[0] -ne 0x4D -or $fileBytes[1] -ne 0x5A) {
$firstBytes = ($fileBytes[0..15] | ForEach-Object { $_.ToString("X2") }) -join " "
throw "Downloaded file is not a valid Windows executable (missing MZ signature). First bytes: $firstBytes"
}
# Get PE header offset (at 0x3C)
$peOffset = [BitConverter]::ToUInt32($fileBytes, 0x3C)
if ($peOffset -ge $fileBytes.Length - 6) {
throw "Invalid PE header offset in downloaded file"
}
# Check PE signature
if ($fileBytes[$peOffset] -ne 0x50 -or $fileBytes[$peOffset+1] -ne 0x45) {
throw "Downloaded file has invalid PE signature"
}
# Check machine type (should be 0x8664 for x64, 0xAA64 for ARM64)
$machineType = [BitConverter]::ToUInt16($fileBytes, $peOffset + 4)
$expectedMachine = switch ($arch) {
'amd64' { 0x8664 }
'arm64' { 0xAA64 }
'386' { 0x014C }
default { 0x0000 }
}
if ($machineType -ne $expectedMachine) {
$machineStr = "0x" + $machineType.ToString("X4")
$expectedStr = "0x" + $expectedMachine.ToString("X4")
throw "Downloaded binary is for wrong architecture (got $machineStr, expected $expectedStr for $arch)"
}
Write-Success "Verified PE executable for $osArch architecture"
# Verify checksum
Write-Info "Verifying checksum..."
$checksumUrl = "$PulseUrl/download/pulse-host-agent.sha256?platform=windows&arch=$arch"
try {
$expectedChecksum = (Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing).Content.Trim().Split()[0]
$actualChecksum = (Get-FileHash -Path $agentPath -Algorithm SHA256).Hash.ToLower()
if ($actualChecksum -ne $expectedChecksum.ToLower()) {
throw "Checksum mismatch! Expected: $expectedChecksum, Got: $actualChecksum"
}
Write-Success "Checksum verified: $actualChecksum"
} catch {
Write-Warning "Could not verify checksum: $_"
Write-Info "Continuing anyway (PE header was validated)"
}
$agentArgs = @("--url", "`"$PulseUrl`"", "--interval", $Interval)
if ($Token) {
$agentArgs += @("--token", "`"$Token`"")