Skip to content

Commit 53bf96b

Browse files
DeusDataahundt
andcommitted
fix(install.ps1): reserve temp paths exclusively instead of adopting them
Distilled from #1245 by Andrew Hundt: the two exclusive-reservation helpers and their two call sites, hand-ported onto main's install.ps1. Nothing else from that file is taken -- the PR's copy also carries an older TLS block that would re-break Windows 10 (#1856), so a wholesale take was not an option. Two paths the installer used were guessable, and one of them adopted whatever it found: $TmpDir = Join-Path $env:TEMP "cbm-install-$(Get-Random)" New-Item -ItemType Directory -Path $TmpDir -Force Get-Random is seeded per process and its space is small, and -Force on an existing directory is a no-op that returns it. A directory planted at a guessed name became the staging area -- and the owner-only DACL the installer applies is set only afterwards, on a directory someone else created. The new helper names the directory with a GUID and creates it with -ErrorAction Stop, so a collision is a retry, never an adoption. $InstallerTmp = "$InstallerDest.new" was a fixed path in the install directory, reservable by anyone who could write there first. The sibling is now reserved with FileMode.CreateNew and FileShare.None on an unguessable name, which is atomic, before anything is copied into it. One deliberate difference from the PR: the sibling reservation runs INSIDE the existing try, and the cleanup checks that a path was assigned. That block is documented as best-effort ("a failure here still leaves a working install"), and a reservation that exhausts its 32 attempts should be one more best-effort miss, not a script abort under ErrorActionPreference=Stop. Verified: pure ASCII (PS 5.1 reads a BOM-less .ps1 as ANSI), LF endings matching main, both old call sites gone from live code. NOT verified: this file was not parsed by PowerShell. There is no pwsh on the macOS host, the Windows VM is down, and CI's Windows guards only check that `update` prints the install.ps1 command -- they never execute it. Opened as a draft for that reason; it needs one PowerShell parse before it merges. Co-authored-by: Andrew Hundt <ATHundt@gmail.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent fc70534 commit 53bf96b

1 file changed

Lines changed: 59 additions & 4 deletions

File tree

‎install.ps1‎

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,63 @@ Write-Host " arch: $Arch"
134134
Write-Host " target: $InstallDir\$BinName"
135135
Write-Host ""
136136

137+
# Reserve a temporary path that no other process can already own.
138+
#
139+
# The staging directory used to be "cbm-install-<Get-Random>" created with
140+
# -Force. Get-Random is seeded per process and its space is small, and -Force
141+
# ADOPTS a directory that already exists -- so a directory planted at a guessed
142+
# name became the installer's staging area, ahead of the owner-only DACL that
143+
# is applied below. GUID names plus -ErrorAction Stop mean a collision is a
144+
# retry, never an adoption. The same rule applies to the installer's own
145+
# sibling temp file: "<dest>.new" was a fixed path in the install directory,
146+
# reservable by anyone who could write there first. FileMode.CreateNew with
147+
# FileShare.None reserves an unguessable sibling atomically instead.
148+
function New-CbmExclusiveSiblingTemp {
149+
param([Parameter(Mandatory=$true)][string]$Destination)
150+
151+
$directory = [System.IO.Path]::GetDirectoryName($Destination)
152+
$leaf = [System.IO.Path]::GetFileName($Destination)
153+
for ($attempt = 0; $attempt -lt 32; $attempt++) {
154+
$random = [System.IO.Path]::GetRandomFileName()
155+
$candidate = Join-Path $directory ".$leaf.tmp-$random"
156+
try {
157+
$reservation = [System.IO.File]::Open(
158+
$candidate,
159+
[System.IO.FileMode]::CreateNew,
160+
[System.IO.FileAccess]::Write,
161+
[System.IO.FileShare]::None)
162+
$reservation.Dispose()
163+
return $candidate
164+
} catch [System.IO.IOException] {
165+
# A collision belongs to another process; reserve a fresh sibling.
166+
}
167+
}
168+
throw "could not reserve an exclusive temporary sibling for $Destination"
169+
}
170+
171+
function New-CbmExclusiveTempDirectory {
172+
param([Parameter(Mandatory=$true)][string]$ParentDirectory)
173+
174+
for ($attempt = 0; $attempt -lt 32; $attempt++) {
175+
$candidate = Join-Path $ParentDirectory (
176+
"cbm-install-" + [guid]::NewGuid().ToString("N")
177+
)
178+
try {
179+
New-Item -ItemType Directory -Path $candidate -ErrorAction Stop | Out-Null
180+
return $candidate
181+
} catch [System.IO.IOException] {
182+
# Never adopt or remove a colliding path owned by another process.
183+
}
184+
}
185+
throw "could not reserve an exclusive installer temporary directory"
186+
}
187+
137188
# Build download URL
138189
$Archive = "codebase-memory-mcp-windows-$Arch.zip"
139190
$Url = "$BaseUrl/$Archive"
140191

141192
# Download
142-
$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "cbm-install-$(Get-Random)"
143-
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
193+
$TmpDir = New-CbmExclusiveTempDirectory -ParentDirectory ([System.IO.Path]::GetTempPath())
144194

145195
# Give the staging directory a protected owner-only DACL.
146196
#
@@ -364,13 +414,18 @@ if ($LASTEXITCODE -ne 0) {
364414
$DownloadedInstaller = Join-Path $TmpDir "install.ps1"
365415
if (Test-Path -LiteralPath $DownloadedInstaller -PathType Leaf) {
366416
$InstallerDest = Join-Path $InstallDir "install.ps1"
367-
$InstallerTmp = "$InstallerDest.new"
417+
# Reserved inside the try: failing to reserve a sibling is a best-effort
418+
# miss like any other step here, not a reason to abort a finished install.
419+
$InstallerTmp = $null
368420
try {
421+
$InstallerTmp = New-CbmExclusiveSiblingTemp -Destination $InstallerDest
369422
Copy-Item -LiteralPath $DownloadedInstaller -Destination $InstallerTmp -Force -ErrorAction Stop
370423
Move-Item -LiteralPath $InstallerTmp -Destination $InstallerDest -Force -ErrorAction Stop
371424
Write-Host "Installed updater -> $InstallerDest"
372425
} catch {
373-
Remove-Item -LiteralPath $InstallerTmp -Force -ErrorAction SilentlyContinue
426+
if ($InstallerTmp) {
427+
Remove-Item -LiteralPath $InstallerTmp -Force -ErrorAction SilentlyContinue
428+
}
374429
Write-Host "note: could not place install.ps1 in $InstallDir (update will explain where to find it)"
375430
}
376431
}

0 commit comments

Comments
 (0)