mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 18:00:54 -10:00
* 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
205 lines
7.9 KiB
Go
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)
|
|
}
|
|
}
|