From 16c29463f986c293cbd02378ca4fef82a127613c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 7 Nov 2025 22:55:03 +0000 Subject: [PATCH] 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. --- internal/api/router.go | 19 +++++----- scripts/install-host-agent.ps1 | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/internal/api/router.go b/internal/api/router.go index a6ac780..cd4eceb 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -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 == "" { diff --git a/scripts/install-host-agent.ps1 b/scripts/install-host-agent.ps1 index 634e99c..949b364 100644 --- a/scripts/install-host-agent.ps1 +++ b/scripts/install-host-agent.ps1 @@ -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`"")