From 5a30951c4483281cbbf79b64a050cab417d55f4d Mon Sep 17 00:00:00 2001 From: JY Tan Date: Thu, 19 Mar 2026 16:34:31 -0700 Subject: [PATCH] Commit --- internal/sandbox/linux.go | 67 +++++++++++++++++++++++++++------- internal/sandbox/linux_test.go | 65 +++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 16 deletions(-) diff --git a/internal/sandbox/linux.go b/internal/sandbox/linux.go index 9146c55..4839c04 100644 --- a/internal/sandbox/linux.go +++ b/internal/sandbox/linux.go @@ -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) { @@ -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) @@ -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") } } } @@ -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) @@ -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. diff --git a/internal/sandbox/linux_test.go b/internal/sandbox/linux_test.go index 7f5089d..90011bf 100644 --- a/internal/sandbox/linux_test.go +++ b/internal/sandbox/linux_test.go @@ -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{ @@ -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) } } @@ -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) + } +}