fix(installer): give the Windows staging directory an owner-only DACL

install.ps1 created its staging directory with New-Item, which inherits whatever
%TEMP% carries. It then ran the downloaded binary from that directory, and the
binary validates its own directory and refuses inherited cross-account mutation
grants. So the installer handed our own validator a directory the installer had
made wrong:

  error: failed to stage install candidate: activation transaction I/O failed:
  acl-grants-cross-account-mutation to S-1-5-21-...-1003

Five reporters, three different offending identities - a CodexSandboxUsers local
group, a synthesized SID recorded in .codex/cap_sid, and orphaned SIDs left by
uninstalled software - all naming an ACE the installer inherited rather than
anything cbm wrote. Still live on v0.10.4 (#1614). Closes #1529, #1614, #1571.

cbm's own C staging already creates its directory with a protected owner-only
DACL (win_mkdtemp_private_create). install.ps1 was the single path that skipped
it, which is exactly why redirecting TMP/TEMP to a fresh directory worked around
the failure - that workaround is now unnecessary.

Applied after creation rather than atomically on purpose: the CreateDirectory
overload taking a DirectorySecurity exists on Windows PowerShell 5.1 but not on
PowerShell 7, and Set-Acl works on both. The directory name is unpredictable and
nothing is written into it before the download, so the window is not usefully
attackable.

Best-effort by design: a filesystem that cannot carry a DACL must not fail the
install, and if the hardening does not take, the binary's own validation still
refuses - the honest outcome rather than a silent downgrade.

Pure ASCII, as PowerShell 5.1 decodes a BOM-less .ps1 as ANSI.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-08-14 14:34:26 +02:00
parent c0bd4bbf8d
commit 47d2b53387
+35
View File
@@ -128,6 +128,41 @@ $Url = "$BaseUrl/$Archive"
$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "cbm-install-$(Get-Random)"
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
# Give the staging directory a protected owner-only DACL.
#
# Without this it inherits whatever %TEMP% carries, and the binary we are about
# to run from here validates its own directory and refuses inherited
# cross-account mutation grants. That is not a hypothetical: sandboxed clients
# leave ACEs on %TEMP% (a CodexSandboxUsers group, AppContainer SIDs, and
# orphaned SIDs from uninstalled software have all been reported), and installs
# failed with
# activation transaction I/O failed: acl-grants-cross-account-mutation to S-1-5-21-...
# naming an ACE the installer itself inherited. See issues 1529, 1614 and 1571.
#
# cbm's own C staging already creates its directory this way; install.ps1 was
# the one path that did not, which is why redirecting TMP/TEMP worked around it.
#
# Applied after creation rather than atomically on purpose: the overload that
# takes a DirectorySecurity exists on Windows PowerShell 5.1 but not on
# PowerShell 7, and Set-Acl works on both. The directory name is unpredictable
# and nothing is written into it until the download below, so the window is not
# usefully attackable.
#
# Best-effort: a filesystem that cannot carry a DACL must not fail the install.
# If this does not take, the binary's own validation still refuses to proceed,
# which is the honest outcome rather than a silent downgrade.
try {
$stagingAcl = New-Object System.Security.AccessControl.DirectorySecurity
$stagingAcl.SetAccessRuleProtection($true, $false)
$stagingOwner = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User
$stagingAcl.SetOwner($stagingOwner)
$stagingAcl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
$stagingOwner, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
Set-Acl -Path $TmpDir -AclObject $stagingAcl -ErrorAction Stop
} catch {
Write-Host "note: could not harden the staging directory ACL: $($_.Exception.Message)"
}
Write-Host "Downloading $Archive..."
try {
Invoke-CbmDownload -Url $Url -OutFile "$TmpDir\$Archive"