Skip to content

fix(browse): grant Windows ACLs to token SID, not bare username#2170

Open
IntegriGit wants to merge 1 commit into
garrytan:mainfrom
IntegriGit:fix/windows-domain-sid-file-permissions
Open

fix(browse): grant Windows ACLs to token SID, not bare username#2170
IntegriGit wants to merge 1 commit into
garrytan:mainfrom
IntegriGit:fix/windows-domain-sid-file-permissions

Conversation

@IntegriGit

Copy link
Copy Markdown

Problem

browse/src/file-permissions.ts hardens sensitive gstack state (auth/canary tokens, chat history, agent queue, device salt, per-tab security decisions) by breaking ACL inheritance and granting a single-entry ACL to the current user via icacls. It resolved that user with os.userInfo().username — the bare login name.

On a domain-joined Windows host that also has a local account of the same login name, icacls /grant lmiller:... resolves the bare name to the local SAM account (HOST\lmiller), not the domain account the process actually runs as (DOMAIN\lmiller). Combined with /inheritance:r, the new ACL is granted to an account the process is not, locking it out of the .gstack state directory it just created. The browse daemon then can't create its lock/state file and fails to start with a misleading:

Another instance is starting… Timed out.

This reproduced reliably on a domain-joined Windows 11 box (INTEGRIBILT\lmiller process vs. same-named local OFC01\lmiller).

Fix

Add currentUserIcaclsPrincipal():

  • Resolves the current process token SID via System32\whoami.exe /user /fo csv /nh, called by absolute path so a shadowing Unix whoami earlier on PATH (e.g. Git-for-Windows) can't hijack it and force the weaker fallback.
  • Grants icacls ... *S-1-5-… (SID literal), which is immune to local-vs-domain name collisions.
  • Falls back to USERDOMAIN\username, then bare username, if the SID lookup is unavailable. Cached per process.

Both restrictFilePermissions and restrictDirectoryPermissions now use it instead of the bare os.userInfo().username. POSIX paths are unchanged.

Tests

bun test browse/test/file-permissions.test.ts15 pass / 0 fail.

Adds a regression test asserting the resolved principal is a SID literal (*S-1-5-…) on Windows, and a non-empty name-based fallback off-Windows. Verified on a domain-joined Windows 11 host: the emitted icacls grant is now *S-1-5-21-…:(F) rather than a bare name.

Co-Authored-By: Paperclip noreply@paperclip.ing

restrictFilePermissions / restrictDirectoryPermissions passed
os.userInfo().username straight to `icacls /grant`. On a domain-joined
Windows host that also has a *local* account of the same login name,
icacls resolves the bare name to the LOCAL account (e.g. HOST\user), not
the domain account the process runs as (e.g. DOMAIN\user). Combined with
`/inheritance:r`, the new single-entry ACL is granted to an account the
process is NOT, locking it out of the .gstack state dir it just created.
The browse daemon then can't create its lock/state file and startup fails
with a misleading "Another instance is starting... Timed out."

Fix: resolve the current process token SID via System32\whoami.exe
(absolute path, so a shadowing Unix `whoami` on PATH can't hijack it) and
grant `*<SID>`, which is immune to local-vs-domain name collisions. Fall
back to USERDOMAIN\username, then bare username, if the SID lookup is
unavailable. Cached per process.

Adds a regression test asserting the resolved principal is a SID literal
(*S-1-5-...) on Windows and a non-empty name-based fallback off-Windows.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@trunk-io

trunk-io Bot commented Jul 4, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

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