Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions internal/tools/sandbox_denial.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,36 @@ func sandboxDenialKind(plan zeroSandbox.CommandPlan, exitCode int, sections ...s
if plan.TargetBackend == zeroSandbox.BackendLinuxBwrap && exitCode == 128+31 {
return SandboxDenialKindSandbox, "seccomp"
}
// Windows restricted-token failures are often SILENT: a spawn the token
// cannot satisfy (executable or DLL unreadable under the restricted-SID
// check, MSYS runtime init death, loader status 0xC0000135/0xC0000142)
// produces a nonzero exit with no output at all, so none of the keyword
// heuristics above can ever fire. Treat "wrapped Windows command failed
// without writing a single byte" as a likely sandbox denial so the caller
// offers the unsandboxed-retry prompt instead of surfacing a bare
// "command completed with no output". A command that legitimately fails
// silently (findstr with no match, for one) costs one extra approval
// prompt; the inverse misclassification silently strands the whole
// session, so the trade goes to prompting.
if windowsWrappedBackend(plan.TargetBackend) && sectionsEmpty(sections...) {
return SandboxDenialKindSandbox, "no output from sandboxed command"
}
return "", ""
}

func windowsWrappedBackend(backend zeroSandbox.BackendName) bool {
return backend == zeroSandbox.BackendWindowsRestrictedToken || backend == zeroSandbox.BackendWindowsElevated
}

func sectionsEmpty(sections ...string) bool {
for _, section := range sections {
if strings.TrimSpace(section) != "" {
return false
}
}
return true
}

func networkDeniedBySandbox(plan zeroSandbox.CommandPlan) bool {
return plan.PermissionProfile.Network.Mode == zeroSandbox.NetworkDeny || plan.Policy.Network == zeroSandbox.NetworkDeny
}
Expand Down
50 changes: 50 additions & 0 deletions internal/tools/sandbox_denial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,53 @@ func TestLikelySandboxDeniedIgnoresUnsandboxedFailure(t *testing.T) {
t.Fatal("unsandboxed command output must not be classified as a sandbox denial")
}
}

func TestLikelySandboxDeniedDetectsSilentWindowsWrappedFailure(t *testing.T) {
for _, backend := range []zeroSandbox.BackendName{
zeroSandbox.BackendWindowsRestrictedToken,
zeroSandbox.BackendWindowsElevated,
} {
plan := zeroSandbox.CommandPlan{
Wrapped: true,
TargetBackend: backend,
}
if !likelySandboxDenied(plan, 1, "", " \n") {
t.Fatalf("wrapped Windows command failing with empty output must be classified as sandbox denied (backend %s)", backend)
}
meta := map[string]string{}
markLikelySandboxDenial(meta, plan, 1, "")
if meta[SandboxLikelyDeniedMeta] != "true" || meta[SandboxDenialKindMeta] != SandboxDenialKindSandbox {
t.Fatalf("silent windows denial meta = %#v (backend %s)", meta, backend)
}
}
}

func TestLikelySandboxDeniedIgnoresSilentWindowsWrappedSuccess(t *testing.T) {
plan := zeroSandbox.CommandPlan{
Wrapped: true,
TargetBackend: zeroSandbox.BackendWindowsRestrictedToken,
}
if likelySandboxDenied(plan, 0, "") {
t.Fatal("wrapped Windows command exiting 0 with empty output must not be classified as sandbox denied")
}
}

func TestLikelySandboxDeniedIgnoresSilentFailureOnOtherBackends(t *testing.T) {
plan := zeroSandbox.CommandPlan{
Wrapped: true,
TargetBackend: zeroSandbox.BackendLinuxBwrap,
}
if likelySandboxDenied(plan, 1, "") {
t.Fatal("silent nonzero exit on non-Windows backends must not be classified as sandbox denied")
}
}

func TestLikelySandboxDeniedIgnoresSilentWindowsFailureWithOutput(t *testing.T) {
plan := zeroSandbox.CommandPlan{
Wrapped: true,
TargetBackend: zeroSandbox.BackendWindowsRestrictedToken,
}
if likelySandboxDenied(plan, 1, "fatal: not a git repository") {
t.Fatal("a wrapped Windows command that produced unrelated output must not be classified as sandbox denied")
}
}
Loading