From 47d2b53387a204a849a53b4a621efc92e1b45625 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 14 Aug 2026 14:34:26 +0200 Subject: [PATCH] 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 --- install.ps1 | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/install.ps1 b/install.ps1 index aaba0234..1433c926 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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"