Skip to content

Commit 8d96d46

Browse files
authored
fix: preserve minimal /dev mounts in linux sandboxes (#83)
1 parent 89bbe92 commit 8d96d46

2 files changed

Lines changed: 116 additions & 16 deletions

File tree

internal/sandbox/linux.go

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ const (
5959
linuxBootstrapLogPath = linuxBootstrapDir + "/bootstrap.log"
6060
)
6161

62+
var linuxMinimalCoreDevicePaths = []string{
63+
"/dev/null",
64+
"/dev/zero",
65+
"/dev/full",
66+
"/dev/random",
67+
"/dev/urandom",
68+
"/dev/tty",
69+
"/dev/ptmx",
70+
}
71+
6272
// NewLinuxBridge creates Unix socket bridges to the proxy servers.
6373
// This allows sandboxed processes to communicate with the host's proxy (outbound).
6474
func NewLinuxBridge(httpProxyPort, socksProxyPort int, debug bool) (*LinuxBridge, error) {
@@ -222,6 +232,35 @@ func fileExists(path string) bool {
222232
return err == nil
223233
}
224234

235+
func appendLinuxDevicePassthrough(bwrapArgs []string, path string, bound map[string]bool, debug bool, reason string) []string {
236+
normalized := filepath.Clean(path)
237+
if bound[normalized] {
238+
return bwrapArgs
239+
}
240+
if fileExists(normalized) {
241+
bound[normalized] = true
242+
return append(bwrapArgs, "--dev-bind", normalized, normalized)
243+
}
244+
if debug {
245+
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping missing %s device passthrough: %s\n", reason, normalized)
246+
}
247+
return bwrapArgs
248+
}
249+
250+
func insertLinuxArgsBeforeSpecialMounts(args []string, insert []string) []string {
251+
for i := 0; i < len(args); i++ {
252+
if args[i] == "--dev" || args[i] == "--proc" ||
253+
(args[i] == "--dev-bind" && i+2 < len(args) && args[i+1] == "/dev" && args[i+2] == "/dev") {
254+
updated := make([]string, 0, len(args)+len(insert))
255+
updated = append(updated, args[:i]...)
256+
updated = append(updated, insert...)
257+
updated = append(updated, args[i:]...)
258+
return updated
259+
}
260+
}
261+
return append(args, insert...)
262+
}
263+
225264
// isDirectory returns true if the path exists and is a directory.
226265
func isDirectory(path string) bool {
227266
info, err := os.Stat(path)
@@ -758,20 +797,16 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
758797
bwrapArgs = append(bwrapArgs, "--dev-bind", "/dev", "/dev")
759798
default:
760799
// Prefer a fresh minimal /dev for predictable sandbox behavior.
800+
// Rebind core devices from the outer environment so they remain usable
801+
// even if the synthetic /dev tmpfs inherits restrictive mount flags.
761802
bwrapArgs = append(bwrapArgs, "--dev", "/dev")
803+
boundDevicePaths := make(map[string]bool, len(linuxMinimalCoreDevicePaths))
804+
for _, path := range linuxMinimalCoreDevicePaths {
805+
bwrapArgs = appendLinuxDevicePassthrough(bwrapArgs, path, boundDevicePaths, opts.Debug, "core")
806+
}
762807
if cfg != nil && len(cfg.Devices.Allow) > 0 {
763-
boundDevicePaths := make(map[string]bool, len(cfg.Devices.Allow))
764808
for _, path := range cfg.Devices.Allow {
765-
normalized := filepath.Clean(path)
766-
if boundDevicePaths[normalized] {
767-
continue
768-
}
769-
if fileExists(normalized) {
770-
bwrapArgs = append(bwrapArgs, "--dev-bind", normalized, normalized)
771-
boundDevicePaths[normalized] = true
772-
} else if opts.Debug {
773-
fmt.Fprintf(os.Stderr, "[fence:linux] Skipping missing device passthrough: %s\n", normalized)
774-
}
809+
bwrapArgs = appendLinuxDevicePassthrough(bwrapArgs, path, boundDevicePaths, opts.Debug, "custom")
775810
}
776811
}
777812
}
@@ -861,6 +896,11 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
861896
}
862897

863898
// Make writable paths actually writable (override read-only root)
899+
if writablePaths["/"] {
900+
delete(writablePaths, "/")
901+
bwrapArgs = insertLinuxArgsBeforeSpecialMounts(bwrapArgs, []string{"--bind", "/", "/"})
902+
}
903+
864904
for p := range writablePaths {
865905
if fileExists(p) {
866906
bwrapArgs = append(bwrapArgs, "--bind", p, p)
@@ -1124,16 +1164,17 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, bridge *Lin
11241164

11251165
// Build the final command
11261166
bwrapCmd := ShellQuote(bwrapArgs)
1167+
finalCmd := bwrapCmd
11271168

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

1136-
return bwrapCmd, nil
1177+
return finalCmd, nil
11371178
}
11381179

11391180
// StartLinuxMonitor starts violation monitoring for a Linux sandbox.

internal/sandbox/linux_test.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func TestWrapCommandLinuxWithOptions_UsesMinimalDevMode(t *testing.T) {
193193
cfg := &config.Config{
194194
Devices: config.DevicesConfig{
195195
Mode: config.DeviceModeMinimal,
196-
Allow: []string{"/dev/null"},
196+
Allow: []string{"/dev/null", "/dev/fd", "/dev/fd"},
197197
},
198198
}
199199
cmd, err := WrapCommandLinuxWithOptions(cfg, "echo ok", nil, nil, LinuxSandboxOptions{
@@ -212,8 +212,25 @@ func TestWrapCommandLinuxWithOptions_UsesMinimalDevMode(t *testing.T) {
212212
if strings.Contains(cmd, ShellQuote([]string{"--dev-bind", "/dev", "/dev"})) {
213213
t.Fatalf("did not expect host /dev bind in minimal mode: %s", cmd)
214214
}
215-
if !strings.Contains(cmd, ShellQuote([]string{"--dev-bind", "/dev/null", "/dev/null"})) {
216-
t.Fatalf("expected explicit device passthrough in minimal mode: %s", cmd)
215+
216+
for _, path := range linuxMinimalCoreDevicePaths {
217+
if !fileExists(path) {
218+
continue
219+
}
220+
fragment := ShellQuote([]string{"--dev-bind", path, path})
221+
if !strings.Contains(cmd, fragment) {
222+
t.Fatalf("expected core device passthrough for %s in minimal mode: %s", path, cmd)
223+
}
224+
}
225+
226+
nullFragment := ShellQuote([]string{"--dev-bind", "/dev/null", "/dev/null"})
227+
if count := strings.Count(cmd, nullFragment); count != 1 {
228+
t.Fatalf("expected /dev/null passthrough exactly once in minimal mode, got %d: %s", count, cmd)
229+
}
230+
231+
fdFragment := ShellQuote([]string{"--dev-bind", "/dev/fd", "/dev/fd"})
232+
if fileExists("/dev/fd") && strings.Count(cmd, fdFragment) != 1 {
233+
t.Fatalf("expected custom /dev/fd passthrough exactly once in minimal mode: %s", cmd)
217234
}
218235
}
219236

@@ -248,3 +265,45 @@ func TestWrapCommandLinuxWithOptions_UsesHostDevMode(t *testing.T) {
248265
t.Fatalf("did not expect per-device passthroughs in host mode: %s", cmd)
249266
}
250267
}
268+
269+
func TestWrapCommandLinuxWithOptions_RootBindPrecedesSpecialMounts(t *testing.T) {
270+
if _, err := exec.LookPath("bwrap"); err != nil {
271+
t.Skip("bwrap not available")
272+
}
273+
274+
cfg := &config.Config{
275+
Devices: config.DevicesConfig{
276+
Mode: config.DeviceModeMinimal,
277+
},
278+
Filesystem: config.FilesystemConfig{
279+
AllowWrite: []string{"/"},
280+
},
281+
}
282+
283+
cmd, err := WrapCommandLinuxWithOptions(cfg, "echo ok", nil, nil, LinuxSandboxOptions{
284+
UseLandlock: false,
285+
UseSeccomp: false,
286+
UseEBPF: false,
287+
ShellMode: ShellModeDefault,
288+
})
289+
if err != nil {
290+
t.Fatalf("WrapCommandLinuxWithOptions failed: %v", err)
291+
}
292+
293+
rootBind := ShellQuote([]string{"--bind", "/", "/"})
294+
devMount := ShellQuote([]string{"--dev", "/dev"})
295+
nullBind := ShellQuote([]string{"--dev-bind", "/dev/null", "/dev/null"})
296+
297+
rootIdx := strings.Index(cmd, rootBind)
298+
devIdx := strings.Index(cmd, devMount)
299+
nullIdx := strings.Index(cmd, nullBind)
300+
if rootIdx == -1 || devIdx == -1 || nullIdx == -1 {
301+
t.Fatalf("expected root bind, minimal /dev mount, and device passthroughs in command: %s", cmd)
302+
}
303+
if rootIdx > devIdx {
304+
t.Fatalf("expected root bind to appear before /dev mount: %s", cmd)
305+
}
306+
if rootIdx > nullIdx {
307+
t.Fatalf("expected root bind to appear before device passthroughs: %s", cmd)
308+
}
309+
}

0 commit comments

Comments
 (0)