Skip to content

fix(sandbox): use WRITE_RESTRICTED token when no DenyRead paths are configured#658

Merged
kevincodex1 merged 1 commit into
Gitlawb:mainfrom
euxaristia:fix/windows-write-restricted-token
Jul 13, 2026
Merged

fix(sandbox): use WRITE_RESTRICTED token when no DenyRead paths are configured#658
kevincodex1 merged 1 commit into
Gitlawb:mainfrom
euxaristia:fix/windows-write-restricted-token

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

Since #612 removed the WRITE_RESTRICTED flag, the Windows sandbox token applies the restricted-SID check to reads as well as writes. Default Windows DACLs grant BUILTIN\Users, not any SID in the token's restricted list (the random capability SIDs, the logon SID, and Everyone). The result is that a wrapped command cannot open any executable or non-KnownDlls DLL anywhere on the system:

  • The wrapped cmd.exe starts (its imports come from KnownDlls, and the unrestricted runner opens cmd.exe itself), but every child it spawns fails: gh, git, where.exe, even a nested cmd.exe, all exit 1 with zero bytes of output.
  • A directly wrapped where.exe dies with STATUS_DLL_NOT_FOUND (0xC0000135).
  • Because the failures are completely silent, the sandbox-denial keyword heuristics never fire, so no unsandboxed-retry prompt is offered and agent sessions strand on "Command completed with no output".

The env-gated smoke test TestWindowsRestrictedTokenNestedPipeCapture (spawns whoami.exe from inside the sandbox) reproduces this on current main.

Fix

Restore WRITE_RESTRICTED, but only when the permission profile has no DenyRead paths. The flag makes the kernel skip restricted-SID deny ACEs for reads, which is exactly the DenyRead bypass #612 fixed, so profiles that configure DenyRead keep the fully restricted token and trade spawn capability for read-deny enforcement. DenyRead is empty by default, so the common case gets a working sandbox back while the #612 guarantee holds for the profiles that rely on it.

Verification (Windows 11, real smoke tests)

With ZERO_SANDBOX_REAL_SMOKE=1 and the runner built from this branch:

  • TestWindowsRestrictedTokenNestedPipeCapture passes (fails against a runner built from main).
  • TestWindowsUnelevatedRealSandboxSmoke passes, including the DenyRead assertion added in fix(sandbox): make Windows token fully restricted to enforce DenyRead policies #612 (that profile configures DenyRead, so it exercises the fully restricted branch).
  • Manual check through the runner: cmd /d /c "git --version & gh --version" under an unelevated restricted-token profile prints both versions and exits 0; on main it exits 1 with no output.

go build ./... and go test ./internal/sandbox ./internal/tools ./internal/agent pass.

Relation to #640

#640 attacks the same regression by adding Users and Authenticated Users to the restricted-SID list. That restores executable reads, but it also widens the write jail to every location where those groups hold write access, and it still blocks reads of per-user files (profile directories grant the individual user, not the Users group), so gh can spawn but cannot read its own config. This PR keeps the write jail scoped to the capability SIDs and restores all reads the user can normally perform. Happy to close one in favor of the other.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Windows sandbox filesystem access enforcement based on configured read restrictions.
    • Read-denied paths now receive more consistent protection while preserving appropriate write restrictions.
    • Enhanced handling of restricted permissions for improved sandbox security.

…onfigured

Removing the WRITE_RESTRICTED flag in Gitlawb#612 made the restricted-SID check
apply to reads as well as writes. Default Windows DACLs grant
BUILTIN\Users rather than any SID in the token's restricted list
(random capability SIDs, logon SID, Everyone), so the sandboxed process
could no longer open any executable or non-KnownDlls DLL: every spawned
command failed with exit 1 and no output, including cmd builtins'
children (gh, git, where.exe). The existing
TestWindowsRestrictedTokenNestedPipeCapture smoke test reproduces the
regression.

Restore WRITE_RESTRICTED, but only when the permission profile has no
DenyRead paths. The flag makes the kernel skip restricted-SID deny ACEs
for reads, which is exactly the DenyRead bypass Gitlawb#612 fixed, so profiles
that configure DenyRead keep the fully restricted token and trade spawn
capability for read-deny enforcement. DenyRead is empty by default, so
the common case regains a working sandbox while Gitlawb#612's guarantee holds
for the profiles that rely on it.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19aa8fe6-5bd7-44da-a9df-c0b88280d115

📥 Commits

Reviewing files that changed from the base of the PR and between 5e1405d and 4e0bd22.

📒 Files selected for processing (2)
  • internal/sandbox/windows_command_runner_windows.go
  • internal/sandbox/windows_token_windows.go

Walkthrough

Changes

Windows token access control

Layer / File(s) Summary
Select restricted-token behavior
internal/sandbox/windows_command_runner_windows.go, internal/sandbox/windows_token_windows.go
runWindowsSandboxCommand derives writeRestricted from DenyRead and passes it through the restricted-token creation helpers.
Apply conditional token flag
internal/sandbox/windows_token_windows.go
createWindowsRestrictedTokenFromBase conditionally adds windowsWriteRestricted before calling CreateRestrictedToken.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SandboxCommandRunner
  participant RestrictedTokenBuilder
  participant WindowsCreateRestrictedToken
  SandboxCommandRunner->>RestrictedTokenBuilder: derive writeRestricted from DenyRead
  RestrictedTokenBuilder->>WindowsCreateRestrictedToken: pass computed token flags
  WindowsCreateRestrictedToken-->>SandboxCommandRunner: return restricted token
Loading

Possibly related PRs

Suggested reviewers: gnanam1990, kevincodex1, anandh8x, Vasanthdev2004

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: restoring WRITE_RESTRICTED tokens when DenyRead is not configured.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read through the token logic in internal/sandbox/windows_token_windows.go and the call site in windows_command_runner_windows.go. The gating is right: WRITE_RESTRICTED is only set when DenyRead is empty, so profiles that rely on read-deny keep the strict post-#612 token, and the flag never relaxes write checks — it only stops the restricted-SID check for reads, so the workspace write-jail stays intact. Build, vet, gofmt, and the sandbox package tests are clean on Windows and the branch merges cleanly. Approving.

One note: TestWindowsUnelevatedRealSandboxSmoke and TestWindowsRestrictedTokenNestedPipeCapture both stay skipped without ZERO_SANDBOX_REAL_SMOKE=1, so neither the spawn-capability fix nor the read-deny path is exercised in default CI — worth a manual smoke run before this lands.

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.

3 participants