Skip to content

Commit 01c18e8

Browse files
committed
more strict bubblewrap sandbox, tweak specs
1 parent 25f1530 commit 01c18e8

File tree

3 files changed

+95
-17
lines changed

3 files changed

+95
-17
lines changed

runner/bubblewrap_linux.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"os/exec"
9+
"path/filepath"
910
"strings"
1011
)
1112

@@ -77,6 +78,7 @@ func (br *bubblewrapRunner) Run() error {
7778
if xdgRuntimeDir == "" {
7879
xdgRuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
7980
}
81+
createdSandboxDirs := make(map[string]struct{})
8082

8183
// X11
8284
if _, err := os.Stat("/tmp/.X11-unix"); err == nil {
@@ -92,24 +94,24 @@ func (br *bubblewrapRunner) Run() error {
9294
if waylandDisplay != "" {
9395
socketPath := xdgRuntimeDir + "/" + waylandDisplay
9496
if _, err := os.Stat(socketPath); err == nil {
97+
ensureSandboxParentDirs(&args, createdSandboxDirs, socketPath)
9598
args = append(args, "--ro-bind", socketPath, socketPath)
9699
}
97100
}
98101

99102
// PulseAudio
100103
pulsePath := xdgRuntimeDir + "/pulse"
101104
if _, err := os.Stat(pulsePath); err == nil {
105+
ensureSandboxParentDirs(&args, createdSandboxDirs, pulsePath)
102106
args = append(args, "--ro-bind", pulsePath, pulsePath)
103107
}
104108

105109
// PipeWire
106110
pipewirePath := xdgRuntimeDir + "/pipewire-0"
107111
if _, err := os.Stat(pipewirePath); err == nil {
112+
ensureSandboxParentDirs(&args, createdSandboxDirs, pipewirePath)
108113
args = append(args, "--ro-bind", pipewirePath, pipewirePath)
109114
}
110-
111-
// Bind XDG_RUNTIME_DIR itself as tmpfs so paths under it work
112-
args = append(args, "--bind", xdgRuntimeDir, xdgRuntimeDir)
113115
}
114116

115117
// Namespace isolation (keep network shared)
@@ -185,3 +187,28 @@ func envLookup(env []string, key string) string {
185187
}
186188
return ""
187189
}
190+
191+
func ensureSandboxParentDirs(args *[]string, seen map[string]struct{}, path string) {
192+
cleanPath := filepath.Clean(path)
193+
if cleanPath == "" || cleanPath == "." || !filepath.IsAbs(cleanPath) {
194+
return
195+
}
196+
197+
parent := filepath.Dir(cleanPath)
198+
if parent == "" || parent == "." || parent == "/" {
199+
return
200+
}
201+
202+
current := "/"
203+
for _, part := range strings.Split(strings.TrimPrefix(parent, "/"), "/") {
204+
if part == "" {
205+
continue
206+
}
207+
current = filepath.Join(current, part)
208+
if _, ok := seen[current]; ok {
209+
continue
210+
}
211+
*args = append(*args, "--dir", current)
212+
seen[current] = struct{}{}
213+
}
214+
}

runner/bubblewrap_linux_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//go:build linux
2+
3+
package runner
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestEnsureSandboxParentDirs(t *testing.T) {
12+
var args []string
13+
seen := make(map[string]struct{})
14+
15+
ensureSandboxParentDirs(&args, seen, "/run/user/1000/wayland-0")
16+
assert.Equal(t, []string{
17+
"--dir", "/run",
18+
"--dir", "/run/user",
19+
"--dir", "/run/user/1000",
20+
}, args)
21+
22+
ensureSandboxParentDirs(&args, seen, "/run/user/1000/pipewire-0")
23+
assert.Equal(t, []string{
24+
"--dir", "/run",
25+
"--dir", "/run/user",
26+
"--dir", "/run/user/1000",
27+
}, args)
28+
29+
ensureSandboxParentDirs(&args, seen, "relative/path.sock")
30+
assert.Equal(t, []string{
31+
"--dir", "/run",
32+
"--dir", "/run/user",
33+
"--dir", "/run/user/1000",
34+
}, args)
35+
}

runner/runner_test.go

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,34 @@ func TestContextCancellation(t *testing.T) {
209209
}
210210
}
211211

212-
func skipIfNoBubblewrap(t *testing.T) {
212+
func skipIfNoBubblewrap(t *testing.T) string {
213213
t.Helper()
214214
if runtime.GOOS != "linux" {
215215
t.Skip("bubblewrap tests only run on Linux")
216216
}
217-
if _, err := exec.LookPath("bwrap"); err != nil {
217+
bwrapPath, err := exec.LookPath("bwrap")
218+
if err != nil {
218219
t.Skip("bwrap not found in PATH")
219220
}
221+
222+
// Bubblewrap can be present but unusable in restricted environments.
223+
probeCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
224+
defer cancel()
225+
probeCmd := exec.CommandContext(probeCtx, bwrapPath, "--unshare-user", "--ro-bind", "/", "/", "--", "true")
226+
out, err := probeCmd.CombinedOutput()
227+
if err != nil {
228+
msg := strings.TrimSpace(string(out))
229+
if msg == "" {
230+
msg = err.Error()
231+
}
232+
t.Skipf("bwrap found but unusable in this environment: %s", msg)
233+
}
234+
235+
return bwrapPath
220236
}
221237

222-
func newBubblewrapParams(t *testing.T, args ...string) runner.RunnerParams {
238+
func newBubblewrapParams(t *testing.T, bwrapPath string, args ...string) runner.RunnerParams {
223239
t.Helper()
224-
bwrapPath, _ := exec.LookPath("bwrap")
225240
params := newTestParams(t, args...)
226241
params.Sandbox = true
227242
params.BubblewrapParams = runner.BubblewrapParams{
@@ -231,10 +246,10 @@ func newBubblewrapParams(t *testing.T, args ...string) runner.RunnerParams {
231246
}
232247

233248
func TestBubblewrapBasicExecution(t *testing.T) {
234-
skipIfNoBubblewrap(t)
249+
bwrapPath := skipIfNoBubblewrap(t)
235250

236251
var stdout bytes.Buffer
237-
params := newBubblewrapParams(t, "echo", "hello")
252+
params := newBubblewrapParams(t, bwrapPath, "echo", "hello")
238253
params.Stdout = &stdout
239254

240255
r, err := runner.GetRunner(params)
@@ -246,10 +261,10 @@ func TestBubblewrapBasicExecution(t *testing.T) {
246261
}
247262

248263
func TestBubblewrapArgumentPassing(t *testing.T) {
249-
skipIfNoBubblewrap(t)
264+
bwrapPath := skipIfNoBubblewrap(t)
250265

251266
var stdout bytes.Buffer
252-
params := newBubblewrapParams(t, "echo", "hello world", "foo\tbar", "baz\"qux")
267+
params := newBubblewrapParams(t, bwrapPath, "echo", "hello world", "foo\tbar", "baz\"qux")
253268
params.Stdout = &stdout
254269

255270
r, err := runner.GetRunner(params)
@@ -265,10 +280,10 @@ func TestBubblewrapArgumentPassing(t *testing.T) {
265280
}
266281

267282
func TestBubblewrapStdoutStderr(t *testing.T) {
268-
skipIfNoBubblewrap(t)
283+
bwrapPath := skipIfNoBubblewrap(t)
269284

270285
var stdout, stderr bytes.Buffer
271-
params := newBubblewrapParams(t, "output", "stdout", "out-msg", "stderr", "err-msg")
286+
params := newBubblewrapParams(t, bwrapPath, "output", "stdout", "out-msg", "stderr", "err-msg")
272287
params.Stdout = &stdout
273288
params.Stderr = &stderr
274289

@@ -282,12 +297,12 @@ func TestBubblewrapStdoutStderr(t *testing.T) {
282297
}
283298

284299
func TestBubblewrapContextCancellation(t *testing.T) {
285-
skipIfNoBubblewrap(t)
300+
bwrapPath := skipIfNoBubblewrap(t)
286301

287302
ctx, cancel := context.WithCancel(context.Background())
288303
defer cancel()
289304

290-
params := newBubblewrapParams(t, "sleep", "30000")
305+
params := newBubblewrapParams(t, bwrapPath, "sleep", "30000")
291306
params.Ctx = ctx
292307

293308
r, err := runner.GetRunner(params)
@@ -303,8 +318,9 @@ func TestBubblewrapContextCancellation(t *testing.T) {
303318
cancel()
304319

305320
select {
306-
case <-done:
307-
// Run returned promptly after cancellation
321+
case err := <-done:
322+
// Run returned promptly after cancellation and did not fail before cancellation.
323+
require.NoError(t, err)
308324
case <-time.After(5 * time.Second):
309325
t.Fatal("Run() did not return within 5 seconds after context cancellation")
310326
}

0 commit comments

Comments
 (0)