mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 08:41:32 -10:00
Removing the WRITE_RESTRICTED flag in #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 #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 #612's guarantee holds for the profiles that rely on it.
120 lines
5.6 KiB
Go
120 lines
5.6 KiB
Go
//go:build windows
|
|
|
|
package sandbox
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
func runWindowsSandboxCommand(config WindowsSandboxCommandConfig, stderr io.Writer) int {
|
|
switch config.SandboxLevel {
|
|
case WindowsSandboxLevelRestrictedToken:
|
|
if err := ValidateWindowsSandboxSetupMarker(WindowsSandboxSetupConfigFromCommand(config)); err != nil {
|
|
fmt.Fprintln(stderr, WindowsSandboxCommandRunnerName+": "+err.Error())
|
|
return 1
|
|
}
|
|
case WindowsSandboxLevelUnelevated:
|
|
if err := ensureWindowsUnelevatedSetup(config); err != nil {
|
|
fmt.Fprintln(stderr, WindowsSandboxCommandRunnerName+": "+err.Error())
|
|
return 1
|
|
}
|
|
default:
|
|
fmt.Fprintf(stderr, "%s: unsupported Windows sandbox level %q\n", WindowsSandboxCommandRunnerName, config.SandboxLevel)
|
|
return 1
|
|
}
|
|
if err := ValidateWindowsNetworkPolicy(config.PermissionProfile.Network); err != nil {
|
|
fmt.Fprintln(stderr, WindowsSandboxCommandRunnerName+": "+err.Error())
|
|
return 1
|
|
}
|
|
capabilitySIDs, err := WindowsCapabilitySIDsForConfig(config)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, WindowsSandboxCommandRunnerName+": "+err.Error())
|
|
return 1
|
|
}
|
|
offlineSID, err := WindowsOfflineMarkerSID(config.SandboxHome)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, WindowsSandboxCommandRunnerName+": "+err.Error())
|
|
return 1
|
|
}
|
|
// Compose the restricting-SID set: both modes keep the write-capability SIDs
|
|
// (workspace write-jail); deny additionally carries the offline-marker SID
|
|
// that the persistent WFP block filter matches — so a deny command has no
|
|
// network while an approved allow command reaches it, both write-jailed.
|
|
//
|
|
// KNOWN LIMITATION: an approved online command reaches the network, but HTTPS
|
|
// via Windows Schannel (e.g. a Schannel-backed curl.exe) fails inside this
|
|
// restricted token with SEC_E_NO_CREDENTIALS — Schannel can't acquire its
|
|
// per-user TLS credential under a WRITE_RESTRICTED/LUA token. This is a
|
|
// fundamental restricted-token vs Schannel incompatibility (the standard
|
|
// mitigation is to run TLS in a broker process, not the sandboxed one) and
|
|
// has no clean in-token fix. Workarounds: the degraded path (no restricted
|
|
// token) or the in-process web_fetch tool.
|
|
//
|
|
// KNOWN LIMITATION: MSYS2/Cygwin binaries (Git for Windows bash.exe,
|
|
// sh.exe, and the usr\bin coreutils) cannot initialize under this token at
|
|
// all, whether invoked directly or spawned internally by an otherwise
|
|
// native command (git hooks, git/gh credential helpers). The MSYS runtime
|
|
// secures its signal pipe and shared-memory sections with explicit DACLs
|
|
// granting only the user, Administrators, and SYSTEM (msys2-runtime
|
|
// sigproc.cc sigproc_init -> sec_user_nih -> __sec_user), and a
|
|
// WRITE_RESTRICTED write check must ALSO match one of the token's
|
|
// restricted SIDs (logon SID, Everyone, capability SIDs). None of the
|
|
// granted SIDs can be added to the restricted list without collapsing the
|
|
// write jail (each has write access nearly everywhere), so MSYS startup
|
|
// dies with "couldn't create signal pipe" or "CreateFileMapping <SID>.1",
|
|
// Win32 error 5, and exit status 0xC0000142. The System32 WSL bash
|
|
// launcher fails equivalently (the restricted token cannot connect to the
|
|
// WSL service: Bash/Service/CreateInstance/E_ACCESSDENIED). Like Schannel,
|
|
// this has no in-token fix; preflight blocking and output hints live in
|
|
// internal/tools/shell_runtime.go.
|
|
tokenSIDs := windowsRuntimeTokenSIDs(capabilitySIDs, offlineSID, config.PermissionProfile.Network.Mode)
|
|
// A WRITE_RESTRICTED token keeps reads unrestricted so sandboxed commands
|
|
// can actually launch executables; it is only unsafe when DenyRead paths
|
|
// are configured, because the kernel skips restricted-SID deny ACEs for
|
|
// reads under that flag (#612). Profiles with DenyRead keep the fully
|
|
// restricted token, trading spawn capability for read-deny enforcement.
|
|
writeRestricted := len(config.PermissionProfile.FileSystem.DenyRead) == 0
|
|
token, err := createWindowsRestrictedTokenForCapabilitySIDs(tokenSIDs, writeRestricted)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, WindowsSandboxCommandRunnerName+": "+err.Error())
|
|
return 1
|
|
}
|
|
defer token.Close()
|
|
exitCode, err := runWindowsCommandAsUser(token, config)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, WindowsSandboxCommandRunnerName+": "+err.Error())
|
|
return 1
|
|
}
|
|
return exitCode
|
|
}
|
|
|
|
// ensureWindowsUnelevatedSetup applies the workspace ACL plan from the current
|
|
// (non-elevated) process so the write-restricted token has somewhere its
|
|
// capability SIDs are granted. DACL edits on user-owned workspace and temp
|
|
// roots need no Administrator rights; the WFP network filters DO, so this tier
|
|
// provisions no network enforcement — the offline-marker SID composed into the
|
|
// token stays inert until an elevated `zero sandbox setup` installs the block
|
|
// filters. Applied plans are recorded by hash so repeat commands skip the
|
|
// re-apply; like the elevated setup, grants are left in place (the rollback is
|
|
// deliberately discarded) because they only name synthetic capability SIDs
|
|
// that no other token carries.
|
|
func ensureWindowsUnelevatedSetup(config WindowsSandboxCommandConfig) error {
|
|
applied, plan, err := buildWindowsUnelevatedAppliedPlan(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
marker, err := loadWindowsUnelevatedSetupMarker(config.SandboxHome)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if marker.contains(applied) {
|
|
return nil
|
|
}
|
|
if _, err := applyWindowsACLPlan(plan); err != nil {
|
|
return fmt.Errorf("apply unelevated workspace ACLs: %w — the workspace may be on a filesystem the current user does not own; "+
|
|
"run `zero sandbox setup` from an elevated (Administrator) terminal, or re-run with `--sandbox forbid` to skip OS sandboxing", err)
|
|
}
|
|
return recordWindowsUnelevatedAppliedPlan(config.SandboxHome, applied)
|
|
}
|