fix: Harden install scripts: detect libssl3 without ldconfig on PATH, survive noexec tmp dirs, and guard against partial curl|bash execution

This commit is contained in:
2026-08-28 23:46:14 -06:00
parent f3d44212d5
commit 9f7eeb7dc6
2 changed files with 323 additions and 279 deletions
+28 -4
View File
@@ -16,6 +16,8 @@ param(
[string]$BinDir = $env:BIN_DIR [string]$BinDir = $env:BIN_DIR
) )
if ($Version -and $Version -match '^[0-9]') { $Version = "v$Version" }
$Repo = 'Dark-Alex-17/coyote' $Repo = 'Dark-Alex-17/coyote'
function Write-Info($msg) { Write-Host "[coyote-install] $msg" } function Write-Info($msg) { Write-Host "[coyote-install] $msg" }
@@ -89,6 +91,7 @@ if ($os -eq 'windows') {
$tmp = New-Item -ItemType Directory -Force -Path ([IO.Path]::Combine([IO.Path]::GetTempPath(), "coyote-$(Get-Random)")) $tmp = New-Item -ItemType Directory -Force -Path ([IO.Path]::Combine([IO.Path]::GetTempPath(), "coyote-$(Get-Random)"))
try {
$exec = if ($isWin) { 'coyote.exe' } else { 'coyote' } $exec = if ($isWin) { 'coyote.exe' } else { 'coyote' }
$dest = Join-Path $BinDir $exec $dest = Join-Path $BinDir $exec
@@ -155,6 +158,19 @@ foreach ($c in $candidates) {
$works = $false $works = $false
try { & $bin --version *> $null; if ($LASTEXITCODE -eq 0) { $works = $true } } catch { } try { & $bin --version *> $null; if ($LASTEXITCODE -eq 0) { $works = $true } } catch { }
if (-not $works -and -not $isWin) {
# The temp dir may live on a noexec mount; retry from a probe file in
# the install directory before rejecting.
$probe = Join-Path $BinDir ".coyote-install-probe-$PID"
try {
Copy-Item -Force $bin $probe
& chmod +x -- $probe
& $probe --version *> $null
if ($LASTEXITCODE -eq 0) { $works = $true }
} catch { } finally {
Remove-Item -Force -ErrorAction SilentlyContinue $probe
}
}
if (-not $works) { if (-not $works) {
Write-Info "Downloaded $c but it failed to run on this system; trying next candidate" Write-Info "Downloaded $c but it failed to run on this system; trying next candidate"
$tried += "${c}: binary failed to run on this system" $tried += "${c}: binary failed to run on this system"
@@ -176,12 +192,18 @@ if (-not $installed) {
if ($isWin) { if ($isWin) {
$pathParts = ($env:Path -split ';') | Where-Object { $_ -ne '' } $pathParts = ($env:Path -split ';') | Where-Object { $_ -ne '' }
if ($pathParts -notcontains $BinDir) { if ($pathParts -notcontains $BinDir) {
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User'); if (-not $userPath) { $userPath = '' } # Read/write the User PATH via the registry directly: the [Environment]
if (-not ($userPath -split ';' | Where-Object { $_ -eq $BinDir })) { # round-trip expands %VAR% entries and bakes them in on write.
$regKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
if ($regKey) {
$userPath = [string]$regKey.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if (-not (($userPath -split ';') -contains $BinDir)) {
$newUserPath = if ($userPath.Trim().Length -gt 0) { "$userPath;$BinDir" } else { $BinDir } $newUserPath = if ($userPath.Trim().Length -gt 0) { "$userPath;$BinDir" } else { $BinDir }
[Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User') $regKey.SetValue('Path', $newUserPath, [Microsoft.Win32.RegistryValueKind]::ExpandString)
Write-Info "Added to User PATH: $BinDir (restart shell to take effect)" Write-Info "Added to User PATH: $BinDir (restart shell to take effect)"
} }
$regKey.Close()
}
} }
} else { } else {
if (-not ($env:PATH -split ':' | Where-Object { $_ -eq $BinDir })) { if (-not ($env:PATH -split ':' | Where-Object { $_ -eq $BinDir })) {
@@ -190,4 +212,6 @@ if ($isWin) {
} }
Write-Info "Done. Try: coyote --help" Write-Info "Done. Try: coyote --help"
} finally {
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $tmp
}
+53 -33
View File
@@ -13,8 +13,6 @@ set -euo pipefail
# --bin-dir <dir> Install directory (default: /usr/local/bin or ~/.local/bin). Or set BIN_DIR. # --bin-dir <dir> Install directory (default: /usr/local/bin or ~/.local/bin). Or set BIN_DIR.
REPO="Dark-Alex-17/coyote" REPO="Dark-Alex-17/coyote"
VERSION="${COYOTE_VERSION:-}"
BIN_DIR="${BIN_DIR:-}"
usage() { usage() {
echo "coyote installer (Linux/macOS)" echo "coyote installer (Linux/macOS)"
@@ -25,24 +23,6 @@ usage() {
echo " -h, --help Show help" echo " -h, --help Show help"
} }
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2;;
--bin-dir) BIN_DIR="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) echo "Unknown argument: $1" >&2; usage; exit 2;;
esac
done
if [[ -z "${BIN_DIR}" ]]; then
if [[ -w "/usr/local/bin" ]]; then
BIN_DIR="/usr/local/bin"
else
BIN_DIR="${HOME}/.local/bin"
fi
fi
mkdir -p "${BIN_DIR}"
log() { log() {
echo "[coyote-install] $*" echo "[coyote-install] $*"
} }
@@ -54,6 +34,52 @@ need_cmd() {
fi fi
} }
http_get() {
if [[ "$DL" == "curl" ]]; then
curl -fsSL -H 'User-Agent: coyote-installer' "$1"
else
wget -qO- --header='User-Agent: coyote-installer' "$1"
fi
}
smoke_test() {
# The scratch dir may live on a noexec mount; if running in place fails,
# retry from a probe file in the install directory before rejecting.
local bin="$1"
if "$bin" --version >/dev/null 2>&1; then return 0; fi
local probe="${BIN_DIR}/.coyote-install-probe.$$"
local ok=1
if cp "$bin" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then
if "$probe" --version >/dev/null 2>&1; then ok=0; fi
fi
rm -f "$probe"
return "$ok"
}
main() {
VERSION="${COYOTE_VERSION:-}"
BIN_DIR="${BIN_DIR:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2;;
--bin-dir) BIN_DIR="$2"; shift 2;;
-h|--help) usage; exit 0;;
*) echo "Unknown argument: $1" >&2; usage; exit 2;;
esac
done
if [[ -n "$VERSION" && "$VERSION" =~ ^[0-9] ]]; then VERSION="v${VERSION}"; fi
if [[ -z "${BIN_DIR}" ]]; then
if [[ -w "/usr/local/bin" ]]; then
BIN_DIR="/usr/local/bin"
else
BIN_DIR="${HOME}/.local/bin"
fi
fi
mkdir -p "${BIN_DIR}"
need_cmd uname need_cmd uname
need_cmd mktemp need_cmd mktemp
need_cmd tar need_cmd tar
@@ -90,19 +116,11 @@ else
RELEASE_URL="${API_BASE}/tags/${VERSION}" RELEASE_URL="${API_BASE}/tags/${VERSION}"
fi fi
http_get() { WORKDIR="$(mktemp -d)"
if [[ "$DL" == "curl" ]]; then trap 'rm -rf "$WORKDIR"; rm -f "${BIN_DIR}/.coyote-install-probe.$$"' EXIT
curl -fsSL -H 'User-Agent: coyote-installer' "$1"
else
wget -qO- --header='User-Agent: coyote-installer' "$1"
fi
}
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
log "Fetching release metadata from $RELEASE_URL" log "Fetching release metadata from $RELEASE_URL"
JSON="$TMPDIR/release.json" JSON="$WORKDIR/release.json"
if ! http_get "$RELEASE_URL" > "$JSON"; then if ! http_get "$RELEASE_URL" > "$JSON"; then
echo "Error: failed to fetch release metadata. Check version tag." >&2 echo "Error: failed to fetch release metadata. Check version tag." >&2
exit 1 exit 1
@@ -171,7 +189,7 @@ for candidate in "${ASSET_CANDIDATES[@]}"; do
fi fi
ATTEMPT=$((ATTEMPT + 1)) ATTEMPT=$((ATTEMPT + 1))
WORK="$TMPDIR/attempt-$ATTEMPT" WORK="$WORKDIR/attempt-$ATTEMPT"
mkdir -p "$WORK" mkdir -p "$WORK"
log "Selected asset: $candidate" log "Selected asset: $candidate"
@@ -230,7 +248,7 @@ for candidate in "${ASSET_CANDIDATES[@]}"; do
fi fi
chmod +x "$BIN_PATH" chmod +x "$BIN_PATH"
if ! "$BIN_PATH" --version >/dev/null 2>&1; then if ! smoke_test "$BIN_PATH"; then
log "Downloaded $candidate but it failed to run on this system; trying next candidate" log "Downloaded $candidate but it failed to run on this system; trying next candidate"
TRIED+=("$candidate: binary failed to run on this system") TRIED+=("$candidate: binary failed to run on this system")
continue continue
@@ -258,4 +276,6 @@ case ":$PATH:" in
esac esac
log "Done. Try: coyote --help" log "Done. Try: coyote --help"
}
main "$@"