Skip to content

fix(install.ps1): reserve temp paths exclusively instead of adopting them - #2251

Merged
DeusData merged 1 commit into
mainfrom
distill/1245-installer-exclusive-temp
Oct 12, 2026
Merged

DeusData merged 1 commit into
mainfrom
distill/1245-installer-exclusive-temp

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Draft on purpose: this file has not been parsed by PowerShell yet. See the last section.

Distilled from #1245 by @ahundt — 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 carries an older TLS block that would re-break Windows 10 (#1856), so a wholesale take was never an option.

What was wrong

Two paths were guessable, and one 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; -Force on an existing directory simply 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.

$InstallerTmp = "$InstallerDest.new"

A fixed path in the install directory, reservable by anyone who could write there first.

The fix

  • Staging directory: GUID name, New-Item -ErrorAction Stop, 32 attempts. A collision is a retry, never an adoption.
  • Sibling temp: FileMode.CreateNew + FileShare.None on an unguessable name — an atomic reservation before anything is copied.

One deliberate difference from #1245: the sibling reservation runs inside the existing try, and cleanup checks a path was assigned. That block is documented as best-effort ("a failure here still leaves a working install"), so exhausting 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, so a stray non-ASCII byte is a silent breakage.
  • LF line endings, matching main.
  • Both old call sites gone from live code (the only remaining matches are in the explanatory comment).

Not verified — why this is a draft

No PowerShell has parsed this file. There is no pwsh on the macOS host, the Windows VM is down, and I checked CI: the Windows guards (tests/windows/test_windows_update_handoff.py) verify that update prints the install.ps1 command — they never execute it. So nothing in the pipeline would catch a syntax error here. It needs one [Parser]::ParseFile on a Windows machine before it leaves draft.

…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>
@DeusData
DeusData force-pushed the distill/1245-installer-exclusive-temp branch from 53bf96b to 7d86dfc Compare October 12, 2026 22:16
@DeusData

Copy link
Copy Markdown
Owner Author

The parse check this draft was waiting for is done, so the PR is leaving draft.

  • Rebased onto main bf625b0 as 7d86dfc: one commit, a clean rebase, and only install.ps1 changes.
  • Parsed on Windows (ARM64 VM). Windows PowerShell 5.1.26100 ran [System.Management.Automation.Language.Parser]::ParseFile on the rebased file: 2,336 tokens, 0 errors. The VM has no PowerShell 7.
  • Negative control: the same checker on a file with one missing } reports 1 error, so the check is not vacuous.
  • Re-checked: the file is still pure ASCII with LF line endings.

As before, no CI job executes install.ps1. The Windows guards only check that update prints the handoff command. So this parse is the verification of record.

Thanks again to @ahundt: the two exclusive-reservation helpers come from #1245.

@DeusData
DeusData marked this pull request as ready for review October 12, 2026 22:16
@DeusData
DeusData merged commit d76bf75 into main Oct 12, 2026
33 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant