Files
zero/internal/sandbox/windows_setup_test.go
Vasanth T 1b1abd5bf1 feat(sandbox): online/offline Windows network identity — run approved network commands sandboxed (#297)
* feat(sandbox): online/offline Windows network identity (run approved network commands)

Approved network commands bricked on Windows with "windows sandbox setup is out of
date: network policy changed": the setup marker was locked to network=deny, so any
command granted network (curl, git push, npm install) ran with network=allow and
failed marker validation.

Fix with a two-identity model that fits Zero's restricted-token architecture — no
extra logon users, no DPAPI, no admin at runtime: mint one synthetic
"offline-marker" SID per sandbox home and scope the persistent WFP block filter to
THAT SID only. Network is then gated purely by the restricted token's SID set:
- deny (offline): token = write-capability SIDs + offline-marker SID -> block filter
  matches -> no network; writes jailed by the capability SIDs.
- allow (online, approved): token = write-capability SIDs only -> filter never
  matches -> network reaches out; writes jailed identically.

The setup marker becomes mode-AGNOSTIC: it fingerprints the provisioned offline
infrastructure (NetworkInfraHash + OfflineFilterSID), not the per-command mode, so
one setup validly serves both an allow and a deny command. Schema bumped 3->4
(forces a clean re-setup; old markers scoped the filter to write SIDs).

- windows_runner.go: Offline SID in WindowsCapabilitySIDs (schema 2, back-compat
  upgrade) + WindowsOfflineMarkerSID + windowsRuntimeTokenSIDs.
- windows_network.go: BuildWindowsNetworkInfraPlan + WindowsNetworkInfraHash; drop
  the now-dead mode-coupled BuildWindowsNetworkPlan/*Hash helpers.
- windows_setup.go: marker schema 4; validation drops per-command-mode equality.
- windows_setup_windows.go: setup always installs the offline-scoped infra filter.
- windows_command_runner_windows.go: compose the token SID set per network mode.

Cross-platform core fully tested (mode selector, mode-independent infra+hash,
back-compat SID upgrade, one-marker-validates-both-modes regression, schema-3
rejected); sandbox/doctor suites + windows cross-compile green. The WFP
ALE_USER_ID/restricting-SID behavior needs the gated Windows integration smoke test.

* docs(sandbox): note the Schannel HTTPS limitation under the Windows restricted token

Document the known SEC_E_NO_CREDENTIALS limitation at the token-composition site:
an approved online command reaches the network, but HTTPS via Windows Schannel
fails inside the restricted token because Schannel can't acquire its per-user TLS
credential under a WRITE_RESTRICTED/LUA token. Fundamental restricted-token ↔
Schannel incompatibility, unsolved even in the reference sandboxes (codex#17459).
Workarounds: degraded mode or the in-process web_fetch tool. Comment-only.

* docs(sandbox): keep the Schannel-limitation note vendor-neutral
2026-06-21 22:49:54 +05:30

205 lines
7.9 KiB
Go

package sandbox
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestBuildAndParseWindowsSandboxSetupArgs(t *testing.T) {
home := t.TempDir()
args, err := BuildWindowsSandboxSetupArgs(WindowsSandboxSetupArgsOptions{
SandboxHome: home,
CommandCWD: `C:\workspace\src`,
WorkspaceRoots: []string{`C:\workspace`},
PermissionProfile: PermissionProfile{
FileSystem: FileSystemPolicy{
Kind: FileSystemRestricted,
WriteRoots: []WritableRoot{
{Root: `C:\workspace`, ProtectedMetadataNames: []string{".git"}},
},
DenyRead: []string{`C:\workspace\secret`},
},
Network: NetworkPolicy{Mode: NetworkDeny},
},
})
if err != nil {
t.Fatalf("BuildWindowsSandboxSetupArgs: %v", err)
}
config, err := ParseWindowsSandboxSetupArgs(args)
if err != nil {
t.Fatalf("ParseWindowsSandboxSetupArgs: %v", err)
}
if config.SandboxHome != home || config.CommandCWD != `C:\workspace\src` || len(config.WorkspaceRoots) != 1 || config.WorkspaceRoots[0] != `C:\workspace` {
t.Fatalf("setup config = %#v, want sandbox home, command cwd, and workspace root", config)
}
if config.PermissionProfile.FileSystem.Kind != FileSystemRestricted || len(config.PermissionProfile.FileSystem.DenyRead) != 1 {
t.Fatalf("permission profile = %#v, want restricted deny-read profile", config.PermissionProfile)
}
}
func TestWindowsSandboxSetupPathForRunner(t *testing.T) {
got := WindowsSandboxSetupPathForRunner(filepath.Join("C:", "zero", WindowsSandboxCommandRunnerName))
want := filepath.Join("C:", "zero", WindowsSandboxSetupName)
if got != want {
t.Fatalf("WindowsSandboxSetupPathForRunner = %q, want %q", got, want)
}
if got := WindowsSandboxSetupPathForRunner(""); got != "" {
t.Fatalf("empty runner setup path = %q, want empty", got)
}
}
func TestRunWindowsSandboxSetupRejectsInvalidArgs(t *testing.T) {
var stderr bytes.Buffer
code := RunWindowsSandboxSetup([]string{"--command-cwd", `C:\workspace`}, &stderr)
if code != 2 {
t.Fatalf("exit code = %d, want usage error", code)
}
if !strings.Contains(stderr.String(), WindowsSandboxSetupName) {
t.Fatalf("stderr = %q, want setup helper name", stderr.String())
}
}
func TestWindowsSandboxSetupMarkerRefreshesWhenProfileChanges(t *testing.T) {
config := WindowsSandboxSetupConfig{
SandboxHome: t.TempDir(),
CommandCWD: `C:\workspace`,
WorkspaceRoots: []string{`C:\workspace`},
PermissionProfile: PermissionProfile{
FileSystem: FileSystemPolicy{
Kind: FileSystemRestricted,
WriteRoots: []WritableRoot{{Root: `C:\workspace`}},
DenyRead: []string{`C:\workspace\secret-read`},
},
Network: NetworkPolicy{Mode: NetworkDeny},
},
}
if _, err := WriteWindowsSandboxSetupMarker(config); err != nil {
t.Fatalf("WriteWindowsSandboxSetupMarker: %v", err)
}
if err := ValidateWindowsSandboxSetupMarker(config); err != nil {
t.Fatalf("ValidateWindowsSandboxSetupMarker unchanged: %v", err)
}
changed := config
changed.PermissionProfile.FileSystem.DenyRead = []string{`C:\workspace\other-secret`}
err := ValidateWindowsSandboxSetupMarker(changed)
if err == nil || !strings.Contains(err.Error(), "out of date") {
t.Fatalf("ValidateWindowsSandboxSetupMarker changed error = %v, want out of date", err)
}
}
// One setup marker must validly serve BOTH network modes: an approved (allow)
// network command and an ordinary (deny) command both validate against the same
// setup. This is the fix for the "network policy changed" brick that hit every
// approved network command (curl, git push, …). The per-command mode is enforced
// at runtime by the token's SID set, not by which marker exists.
func TestWindowsSandboxSetupMarkerValidatesBothNetworkModes(t *testing.T) {
deny := WindowsSandboxSetupConfig{
SandboxHome: t.TempDir(),
CommandCWD: `C:\workspace`,
WorkspaceRoots: []string{`C:\workspace`},
PermissionProfile: PermissionProfile{
FileSystem: FileSystemPolicy{
Kind: FileSystemRestricted,
WriteRoots: []WritableRoot{{Root: `C:\workspace`}},
},
Network: NetworkPolicy{Mode: NetworkDeny},
},
}
// Set up once (as a deny command would).
if _, err := WriteWindowsSandboxSetupMarker(deny); err != nil {
t.Fatalf("WriteWindowsSandboxSetupMarker: %v", err)
}
if err := ValidateWindowsSandboxSetupMarker(deny); err != nil {
t.Fatalf("deny command must validate against its own setup: %v", err)
}
// An APPROVED (allow) command must ALSO validate against the SAME setup —
// previously this bricked with "network policy changed".
allow := deny
allow.PermissionProfile.Network = NetworkPolicy{Mode: NetworkAllow}
if err := ValidateWindowsSandboxSetupMarker(allow); err != nil {
t.Fatalf("approved (allow) command must validate against the deny setup, got: %v", err)
}
}
// A pre-v4 marker on disk must be rejected as out of date so the schema bump
// forces a clean re-setup (old markers scoped the filter to write SIDs).
func TestWindowsSandboxSetupMarkerRejectsOldSchema(t *testing.T) {
config := WindowsSandboxSetupConfig{
SandboxHome: t.TempDir(),
CommandCWD: `C:\workspace`,
WorkspaceRoots: []string{`C:\workspace`},
PermissionProfile: PermissionProfile{
FileSystem: FileSystemPolicy{Kind: FileSystemRestricted, WriteRoots: []WritableRoot{{Root: `C:\workspace`}}},
Network: NetworkPolicy{Mode: NetworkDeny},
},
}
marker, err := BuildWindowsSandboxSetupMarker(config)
if err != nil {
t.Fatalf("BuildWindowsSandboxSetupMarker: %v", err)
}
marker.SchemaVersion = 3
bytes, err := json.Marshal(marker)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if err := os.WriteFile(WindowsSandboxSetupMarkerPath(config.SandboxHome), bytes, 0o600); err != nil {
t.Fatalf("write marker: %v", err)
}
err = ValidateWindowsSandboxSetupMarker(config)
if err == nil || !strings.Contains(err.Error(), "out of date") {
t.Fatalf("schema-3 marker must be out of date, got: %v", err)
}
}
func TestWindowsSandboxSetupConfigFromCommandPreservesProfileInputs(t *testing.T) {
command := WindowsSandboxCommandConfig{
SandboxHome: t.TempDir(),
CommandCWD: `C:\workspace\src`,
WorkspaceRoots: []string{`C:\workspace`},
PermissionProfile: PermissionProfile{
FileSystem: FileSystemPolicy{
Kind: FileSystemRestricted,
WriteRoots: []WritableRoot{{Root: `C:\workspace`}},
DenyRead: []string{`C:\workspace\secret`},
},
Network: NetworkPolicy{Mode: NetworkDeny},
},
Env: map[string]string{"ZERO_SANDBOXED": "1"},
Command: []string{"cmd.exe", "/c", "dir"},
}
setup := WindowsSandboxSetupConfigFromCommand(command)
if setup.SandboxHome != command.SandboxHome || setup.CommandCWD != command.CommandCWD || len(setup.WorkspaceRoots) != 1 || setup.WorkspaceRoots[0] != `C:\workspace` {
t.Fatalf("setup config = %#v, want command roots", setup)
}
if setup.PermissionProfile.FileSystem.Kind != FileSystemRestricted || len(setup.PermissionProfile.FileSystem.DenyRead) != 1 {
t.Fatalf("setup profile = %#v, want command permission profile", setup.PermissionProfile)
}
}
func TestWindowsACLPlanHashIsStableAcrossEntryOrder(t *testing.T) {
left, err := WindowsACLPlanHash(WindowsACLPlan{Entries: []WindowsACLEntry{
{Action: WindowsACLDenyRead, Path: `C:\workspace\secret`, Capability: "S-1-5-21-3", Materialize: true},
{Action: WindowsACLAllowWrite, Path: `C:\workspace`, Capability: "S-1-5-21-1"},
{Action: WindowsACLDenyWrite, Path: `C:\workspace\.git`, Capability: "S-1-5-21-1"},
}})
if err != nil {
t.Fatalf("WindowsACLPlanHash left: %v", err)
}
right, err := WindowsACLPlanHash(WindowsACLPlan{Entries: []WindowsACLEntry{
{Action: WindowsACLAllowWrite, Path: `c:/workspace`, Capability: "s-1-5-21-1"},
{Action: WindowsACLDenyWrite, Path: `c:/workspace/.git`, Capability: "S-1-5-21-1"},
{Action: WindowsACLDenyRead, Path: `c:/workspace/secret`, Capability: "S-1-5-21-3", Materialize: true},
}})
if err != nil {
t.Fatalf("WindowsACLPlanHash right: %v", err)
}
if left != right {
t.Fatalf("ACL plan hashes differ: %q vs %q", left, right)
}
}