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
67 changes: 54 additions & 13 deletions internal/sandbox/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ const (
linuxBootstrapLogPath = linuxBootstrapDir + "/bootstrap.log"
)

var linuxMinimalCoreDevicePaths = []string{
"/dev/null",
"/dev/zero",
"/dev/full",
"/dev/random",
"/dev/urandom",
"/dev/tty",
"/dev/ptmx",
}

// NewLinuxBridge creates Unix socket bridges to the proxy servers.
// This allows sandboxed processes to communicate with the host's proxy (outbound).
func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge, error) {
Expand Down Expand Up @@ -222,6 +232,35 @@ func fileExists(path string) bool {
return err == nil
}

func appendLinuxDevicePassthrough(bwrapArgs []string, path string, bound map[string]bool, debug bool, reason string) []string {
normalized := filepath.Clean(path)
if bound[normalized] {
return bwrapArgs
}
if fileExists(normalized) {
bound[normalized] = true
return append(bwrapArgs, "--dev-bind", normalized, normalized)
}
if debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping missing %s device passthrough: %s\n", reason, normalized)
}
return bwrapArgs
}

func insertLinuxArgsBeforeSpecialMounts(args []string, insert []string) []string {
for i := 0; i < len(args); i++ {
if args[i] == "--dev" || args[i] == "--proc" ||
(args[i] == "--dev-bind" && i+2 < len(args) && args[i+1] == "/dev" && args[i+2] == "/dev") {
updated := make([]string, 0, len(args)+len(insert))
updated = append(updated, args[:i]...)
updated = append(updated, insert...)
updated = append(updated, args[i:]...)
return updated
}
}
return append(args, insert...)
}

// isDirectory returns true if the path exists and is a directory.
func isDirectory(path string) bool {
info, err := os.Stat(path)
Expand Down Expand Up @@ -758,20 +797,16 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
bwrapArgs = append(bwrapArgs, "--dev-bind", "/dev", "/dev")
default:
// Prefer a fresh minimal /dev for predictable sandbox behavior.
// Rebind core devices from the outer environment so they remain usable
// even if the synthetic /dev tmpfs inherits restrictive mount flags.
bwrapArgs = append(bwrapArgs, "--dev", "/dev")
boundDevicePaths := make(map[string]bool, len(linuxMinimalCoreDevicePaths))
for _, path := range linuxMinimalCoreDevicePaths {
bwrapArgs = appendLinuxDevicePassthrough(bwrapArgs, path, boundDevicePaths, opts.Debug, "core")
}
if cfg != nil && len(cfg.Devices.Allow) > 0 {
boundDevicePaths := make(map[string]bool, len(cfg.Devices.Allow))
for _, path := range cfg.Devices.Allow {
normalized := filepath.Clean(path)
if boundDevicePaths[normalized] {
continue
}
if fileExists(normalized) {
bwrapArgs = append(bwrapArgs, "--dev-bind", normalized, normalized)
boundDevicePaths[normalized] = true
} else if opts.Debug {
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping missing device passthrough: %s\n", normalized)
}
bwrapArgs = appendLinuxDevicePassthrough(bwrapArgs, path, boundDevicePaths, opts.Debug, "custom")
}
}
}
Expand Down Expand Up @@ -861,6 +896,11 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
}

// Make writable paths actually writable (override read-only root)
if writablePaths["/"] {
delete(writablePaths, "/")
bwrapArgs = insertLinuxArgsBeforeSpecialMounts(bwrapArgs, []string{"--bind", "/", "/"})
}

for p := range writablePaths {
if fileExists(p) {
bwrapArgs = append(bwrapArgs, "--bind", p, p)
Expand Down Expand Up @@ -1124,16 +1164,17 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin

// Build the final command
bwrapCmd := ShellQuote(bwrapArgs)
finalCmd := bwrapCmd

// If seccomp filter is enabled, wrap with fd redirection
// bwrap --seccomp expects the filter on the specified fd
if seccompFilterPath != "" {
// Open filter file on fd 3, then run bwrap
// The filter file will be cleaned up after the sandbox exits
return fmt.Sprintf("exec 3<%s; %s", ShellQuoteSingle(seccompFilterPath), bwrapCmd), nil
finalCmd = fmt.Sprintf("exec 3<%s; %s", ShellQuoteSingle(seccompFilterPath), bwrapCmd)
}

return bwrapCmd, nil
return finalCmd, nil
}

// StartLinuxMonitor starts violation monitoring for a Linux sandbox.
Expand Down
65 changes: 62 additions & 3 deletions internal/sandbox/linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func TestWrapCommandLinuxWithOptions_UsesMinimalDevMode(t *testing.T) {
cfg := &config.Config{
Devices: config.DevicesConfig{
Mode: config.DeviceModeMinimal,
Allow: []string{"/dev/null"},
Allow: []string{"/dev/null", "/dev/fd", "/dev/fd"},
},
}
cmd, err := WrapCommandLinuxWithOptions(cfg, "echo ok", nil, nil, LinuxSandboxOptions{
Expand All @@ -212,8 +212,25 @@ func TestWrapCommandLinuxWithOptions_UsesMinimalDevMode(t *testing.T) {
if strings.Contains(cmd, ShellQuote([]string{"--dev-bind", "/dev", "/dev"})) {
t.Fatalf("did not expect host /dev bind in minimal mode: %s", cmd)
}
if !strings.Contains(cmd, ShellQuote([]string{"--dev-bind", "/dev/null", "/dev/null"})) {
t.Fatalf("expected explicit device passthrough in minimal mode: %s", cmd)

for _, path := range linuxMinimalCoreDevicePaths {
if !fileExists(path) {
continue
}
fragment := ShellQuote([]string{"--dev-bind", path, path})
if !strings.Contains(cmd, fragment) {
t.Fatalf("expected core device passthrough for %s in minimal mode: %s", path, cmd)
}
}

nullFragment := ShellQuote([]string{"--dev-bind", "/dev/null", "/dev/null"})
if count := strings.Count(cmd, nullFragment); count != 1 {
t.Fatalf("expected /dev/null passthrough exactly once in minimal mode, got %d: %s", count, cmd)
}

fdFragment := ShellQuote([]string{"--dev-bind", "/dev/fd", "/dev/fd"})
if fileExists("/dev/fd") && strings.Count(cmd, fdFragment) != 1 {
t.Fatalf("expected custom /dev/fd passthrough exactly once in minimal mode: %s", cmd)
}
}

Expand Down Expand Up @@ -248,3 +265,45 @@ func TestWrapCommandLinuxWithOptions_UsesHostDevMode(t *testing.T) {
t.Fatalf("did not expect per-device passthroughs in host mode: %s", cmd)
}
}

func TestWrapCommandLinuxWithOptions_RootBindPrecedesSpecialMounts(t *testing.T) {
if _, err := exec.LookPath("bwrap"); err != nil {
t.Skip("bwrap not available")
}

cfg := &config.Config{
Devices: config.DevicesConfig{
Mode: config.DeviceModeMinimal,
},
Filesystem: config.FilesystemConfig{
AllowWrite: []string{"/"},
},
}

cmd, err := WrapCommandLinuxWithOptions(cfg, "echo ok", nil, nil, LinuxSandboxOptions{
UseLandlock: false,
UseSeccomp: false,
UseEBPF: false,
ShellMode: ShellModeDefault,
})
if err != nil {
t.Fatalf("WrapCommandLinuxWithOptions failed: %v", err)
}

rootBind := ShellQuote([]string{"--bind", "/", "/"})
devMount := ShellQuote([]string{"--dev", "/dev"})
nullBind := ShellQuote([]string{"--dev-bind", "/dev/null", "/dev/null"})

rootIdx := strings.Index(cmd, rootBind)
devIdx := strings.Index(cmd, devMount)
nullIdx := strings.Index(cmd, nullBind)
if rootIdx == -1 || devIdx == -1 || nullIdx == -1 {
t.Fatalf("expected root bind, minimal /dev mount, and device passthroughs in command: %s", cmd)
}
if rootIdx > devIdx {
t.Fatalf("expected root bind to appear before /dev mount: %s", cmd)
}
if rootIdx > nullIdx {
t.Fatalf("expected root bind to appear before device passthroughs: %s", cmd)
}
}
Loading