Files
pmg/sandbox/platform/bubblewrap_linux_test.go
5131c3f641 feat(sandbox): ExecutionContext plumbing and fail-closed lockdown contract (#371)
* feat(sandbox): ExecutionContext plumbing and fail-closed lockdown contract

Network lockdown needs the PMG proxy's address, which is only known at
spawn time. Thread an ExecutionContext from the proxy flow through the
runner and executor into every sandbox driver, and enforce the
network_via_proxy_only fail-closed contract: lockdown without a running
loopback proxy, or on a driver that cannot enforce it, is a hard error —
never a silent fallback to unrestricted network.

- sandbox.ExecutionContext{ProxyAddr} + 4-arg Sandbox.Execute
- sandbox.ValidateLockdown validates the proxy address (loopback only)
  with usefulerror code SandboxRequiresProxy
- Seatbelt validates lockdown before translation (translation itself
  lands next); bubblewrap and landlock reject lockdown as unsupported
  until Linux enforcement is implemented
- executor.WithExecutionContext, runner.ExecuteOptions.SandboxProxyAddr,
  proxy flow passes the live proxy address
- ApplySandbox also validates centrally before invoking the driver

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

* fix(sandbox): require numeric in-range proxy port in ValidateLockdown

The validated port string is embedded into generated sandbox profiles,
so service names, zero, and out-of-range ports are refused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

* fix(sandbox): fail closed on Seatbelt lockdown until translation lands

A lockdown policy that passed proxy validation would silently receive
the pre-lockdown network rules from the translator. Reject it until the
lockdown profile translation is implemented, keeping the window between
plumbing and enforcement fail-closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

* chore: review feedback — drop redundant comment, simplify stub help text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 20:12:33 +05:30

321 lines
7.7 KiB
Go

//go:build linux
// +build linux
package platform
import (
"context"
"os/exec"
"testing"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/sandbox"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBubblewrapSandboxCreation(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
assert.NotNil(t, sb)
assert.Equal(t, sandbox.DriverBubblewrap, sb.Name())
}
func TestBubblewrapSandboxIsAvailable(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
// This test will pass if bwrap is installed, skip if not
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
assert.True(t, sb.IsAvailable())
}
func TestBubblewrapSandboxExecute(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test policy",
PackageManagers: []string{"test"},
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{"/usr", "/lib", "/bin"},
AllowWrite: []string{"/tmp"},
},
Network: sandbox.NetworkPolicy{
AllowOutbound: []string{"*:*"},
},
}
// Create a simple command to wrap
cmd := exec.Command("/bin/echo", "hello")
ctx := context.Background()
result, err := sb.Execute(ctx, cmd, policy, nil)
require.NoError(t, err)
require.NotNil(t, result)
// Result should indicate caller must run the command
assert.True(t, result.ShouldRun(), "Bubblewrap should return executed=false")
// Command should be modified to use bwrap
assert.Equal(t, "bwrap", cmd.Args[0])
assert.Contains(t, cmd.Args, "/bin/echo")
assert.Contains(t, cmd.Args, "hello")
// Cleanup
err = result.Close()
assert.NoError(t, err)
}
func TestBubblewrapSandboxExecuteCommandWrapping(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test policy",
PackageManagers: []string{"npm"},
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{"/usr"},
},
}
// Create a command with multiple arguments
originalCmd := "/usr/bin/node"
originalArgs := []string{"/usr/bin/node", "--version"}
cmd := exec.Command(originalCmd, originalArgs[1:]...)
ctx := context.Background()
result, err := sb.Execute(ctx, cmd, policy, nil)
require.NoError(t, err)
// Verify command structure
// bwrap [bwrap-args] -- /usr/bin/node --version
assert.Contains(t, cmd.Args, "bwrap")
assert.Contains(t, cmd.Args, "--") // Separator
assert.Contains(t, cmd.Args, originalCmd)
assert.Contains(t, cmd.Args, "--version")
// Find the separator and verify structure
separatorIdx := -1
for i, arg := range cmd.Args {
if arg == "--" {
separatorIdx = i
break
}
}
assert.NotEqual(t, -1, separatorIdx, "Should have -- separator")
// After separator should be the original command and args
afterSeparator := cmd.Args[separatorIdx+1:]
assert.Equal(t, originalCmd, afterSeparator[0])
assert.Equal(t, "--version", afterSeparator[1])
// Cleanup
err = result.Close()
assert.NoError(t, err)
}
func TestBubblewrapSandboxExecuteWithPTY(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test with PTY",
PackageManagers: []string{"npm"},
AllowPTY: utils.PtrTo(true),
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{"/usr"},
},
}
cmd := exec.Command("/bin/echo", "test")
ctx := context.Background()
result, err := sb.Execute(ctx, cmd, policy, nil)
require.NoError(t, err)
// Should have PTY-related arguments
argsStr := ""
for _, arg := range cmd.Args {
argsStr += arg + " "
}
assert.Contains(t, argsStr, "/dev/pts")
assert.Contains(t, argsStr, "/dev/ptmx")
// Cleanup
err = result.Close()
assert.NoError(t, err)
}
func TestBubblewrapSandboxExecuteWithNetworkIsolation(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test with network isolation",
PackageManagers: []string{"npm"},
Network: sandbox.NetworkPolicy{
DenyOutbound: []string{"*:*"},
},
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{"/usr"},
},
}
cmd := exec.Command("/bin/echo", "test")
ctx := context.Background()
result, err := sb.Execute(ctx, cmd, policy, nil)
require.NoError(t, err)
// Should have network isolation
argsStr := ""
for _, arg := range cmd.Args {
argsStr += arg + " "
}
assert.Contains(t, argsStr, "--unshare-net")
// Cleanup
err = result.Close()
assert.NoError(t, err)
}
func TestBubblewrapSandboxClose(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
// Close should be idempotent
err = sb.Close()
assert.NoError(t, err)
err = sb.Close()
assert.NoError(t, err)
}
func TestBubblewrapSandboxExecutionResult(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
policy := &sandbox.SandboxPolicy{
Name: "test",
PackageManagers: []string{"test"},
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{"/usr"},
},
}
cmd := exec.Command("/bin/echo", "test")
ctx := context.Background()
result, err := sb.Execute(ctx, cmd, policy, nil)
require.NoError(t, err)
// Verify ExecutionResult properties
assert.True(t, result.ShouldRun(), "Bubblewrap uses CLI wrapper, should return executed=false")
// Close should succeed
err = result.Close()
assert.NoError(t, err)
// Multiple closes should be safe
err = result.Close()
assert.NoError(t, err)
}
func TestBubblewrapSandboxTranslationError(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
// Create a policy with invalid patterns (shouldn't cause translation error)
policy := &sandbox.SandboxPolicy{
Name: "test",
PackageManagers: []string{"test"},
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{"/usr"},
},
}
cmd := exec.Command("/bin/echo", "test")
ctx := context.Background()
// Should succeed even with complex patterns
result, err := sb.Execute(ctx, cmd, policy, nil)
assert.NoError(t, err)
assert.NotNil(t, result)
// Cleanup
if result != nil {
_ = result.Close()
}
}
func TestBubblewrapSandboxEssentialBindMounts(t *testing.T) {
sb, err := newBubblewrapSandbox()
require.NoError(t, err)
if !sb.IsAvailable() {
t.Skip("bubblewrap (bwrap) is not installed on this system")
}
policy := &sandbox.SandboxPolicy{
Name: "test",
PackageManagers: []string{"test"},
// Minimal policy - should still get essential mounts
Filesystem: sandbox.FilesystemPolicy{},
}
cmd := exec.Command("/bin/echo", "test")
ctx := context.Background()
result, err := sb.Execute(ctx, cmd, policy, nil)
require.NoError(t, err)
argsStr := ""
for _, arg := range cmd.Args {
argsStr += arg + " "
}
// Should have essential system paths
assert.Contains(t, argsStr, "/usr")
// Should have essential devices
assert.Contains(t, argsStr, "/dev/null")
// Should have proc filesystem
assert.Contains(t, argsStr, "--proc")
// Should have tmpdir
assert.Contains(t, argsStr, "--bind")
// Cleanup
err = result.Close()
assert.NoError(t, err)
}