Skip to content

Commit 2b31ee9

Browse files
committed
feat: sandbox chromium using bubblewrap
1 parent a0f4442 commit 2b31ee9

File tree

4 files changed

+77
-94
lines changed

4 files changed

+77
-94
lines changed

Dockerfile

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,15 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
1111
--mount=type=cache,target=/root/go/pkg \
1212
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /usr/local/bin/crocochrome ./cmd
1313

14-
# For setting caps, use the same image than the final layer is using to avoid pulling two distinct ones.
15-
FROM ghcr.io/grafana/chromium-swiftshader-alpine:142.0.7444.59-r0-3.22.2@sha256:4bfff84902c23158c54dbcf94ec8267624ee30700e8c642cba9b7ebbdc756785 AS setcapper
16-
17-
RUN apk --no-cache add libcap
18-
19-
COPY --from=buildtools /usr/local/bin/crocochrome /usr/local/bin/crocochrome
20-
21-
# The following capabilities are used by sm-k6-runner to sandbox the k6 binary. More details about what each cap is used
22-
# for can be found in /sandbox/sandbox.go.
23-
# WARNING: The container MUST be also granted all of the following capabilities too, or the CRI will refuse to start it.
24-
RUN setcap cap_setuid,cap_setgid,cap_kill,cap_chown,cap_dac_override,cap_fowner+ep /usr/local/bin/crocochrome
25-
2614
FROM ghcr.io/grafana/chromium-swiftshader-alpine:142.0.7444.59-r0-3.22.2@sha256:4bfff84902c23158c54dbcf94ec8267624ee30700e8c642cba9b7ebbdc756785
2715

28-
RUN adduser --home / --uid 6666 --shell /bin/nologin --disabled-password k6
29-
30-
RUN apk --no-cache add --repository community tini
31-
32-
# As we rely on file capabilities, we cannot set `allowPrivilegeEscalation: false` in k8s. As a workaround, and to lower
33-
# potential attack surface, we get rid of any file that has the setuid bit set, such as
34-
# /usr/lib/chromium/chrome-sandbox.
35-
RUN find / -type f -perm -4000 -delete
16+
RUN <<EOF
17+
adduser --home / --uid 6666 --shell /bin/nologin --disabled-password k6
18+
apk --no-cache add --repository community tini bubblewrap
19+
EOF
3620

3721
# The crocochrome binary has extra capabilities, so we make sure only the k6 user (and not nobody) can run it.
38-
COPY --from=setcapper --chown=k6:k6 --chmod=0500 /usr/local/bin/crocochrome /usr/local/bin/crocochrome
22+
COPY --from=buildtools --chown=k6:k6 --chmod=0500 /usr/local/bin/crocochrome /usr/local/bin/crocochrome
3923

4024
USER k6
4125

crocochrome.go

Lines changed: 40 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import (
77
"encoding/hex"
88
"errors"
99
"fmt"
10-
"io/fs"
1110
"log/slog"
1211
"net"
1312
"os"
1413
"os/exec"
14+
"strconv"
1515
"strings"
1616
"sync"
1717
"syscall"
@@ -51,6 +51,8 @@ type Options struct {
5151
// ChromiumPort is the port where chromium will be instructed to listen.
5252
// Defaults to 5222.
5353
ChromiumPort string
54+
// DisableBubblewrap runs chromium directly, instead of using the bubblewrap sandbox (`bwrap`).
55+
DisableBubblewrap bool
5456
// Maximum time a browser is allowed to be running, after which it will be killed unconditionally.
5557
// Defaults to 5m.
5658
SessionTimeout time.Duration
@@ -254,20 +256,29 @@ func (s *Supervisor) launch(ctx context.Context, sessionID string) error {
254256
stdout := &bytes.Buffer{}
255257
stderr := &bytes.Buffer{}
256258

257-
tmpDir, err := s.mkdirTemp()
258-
if err != nil {
259-
return fmt.Errorf("creating temporary directory: %w", err)
259+
bwrapArgs := []string{
260+
"--die-with-parent", // Ensures child process (COMMAND) dies when bwrap's parent dies.
261+
"--unshare-all",
262+
"--share-net",
263+
"--proc", "/proc",
264+
"--dev", "/dev",
265+
"--ro-bind", "/etc", "/etc",
266+
"--ro-bind", "/usr", "/usr",
267+
"--ro-bind", "/lib", "/lib", // TODO: Remove after Alpine's /usr merge.
268+
"--ro-bind", "/bin", "/bin", // TODO: Remove after Alpine's /usr merge.
269+
"--dir", "/tmp",
270+
"--clearenv",
271+
"--setenv", "TMPDIR", "/tmp",
260272
}
261273

262-
defer func() {
263-
// Clean up files after chromium exits.
264-
err := os.RemoveAll(tmpDir)
265-
if err != nil {
266-
panic(fmt.Errorf("deleting tmpdir, bug or sandbox compromised: %w", err))
267-
}
268-
}()
274+
if s.opts.UserGroup != 0 {
275+
bwrapArgs = append(bwrapArgs,
276+
"--uid", strconv.Itoa(s.opts.UserGroup),
277+
"--gid", strconv.Itoa(s.opts.UserGroup),
278+
)
279+
}
269280

270-
args := []string{
281+
chromiumArgs := []string{
271282
// The following flags have been tested to be required:
272283
"--headless",
273284
"--remote-debugging-address=0.0.0.0",
@@ -291,42 +302,38 @@ func (s *Supervisor) launch(ctx context.Context, sessionID string) error {
291302
}
292303

293304
if s.userAgent != "" {
294-
args = append(
295-
args,
305+
chromiumArgs = append(
306+
chromiumArgs,
296307
"--user-agent="+s.userAgent,
297308
)
298309
}
299310

300-
cmd := exec.CommandContext(ctx,
301-
s.opts.ChromiumPath,
302-
args...,
303-
)
304-
cmd.Env = []string{
305-
// Chromium uses this env var to figure where the temporary directory is. We want that to be the directory
306-
// we created for this session, because /tmp is read-only in production.
307-
// https://github.com/chromium/chromium/blob/7c4f56ca9dba3a884212ef3a71c8db5d3633f0a6/base/files/file_util_posix.cc#L764
308-
"TMPDIR=" + tmpDir,
311+
var cmd *exec.Cmd
312+
if s.opts.DisableBubblewrap {
313+
logger.Warn("bubblewrap is disabled, chromium will run with highly elevated permissions")
314+
315+
cmd = exec.CommandContext(ctx,
316+
s.opts.ChromiumPath,
317+
chromiumArgs...,
318+
)
319+
} else {
320+
cmd = exec.CommandContext(ctx,
321+
"bwrap",
322+
append(append(bwrapArgs, s.opts.ChromiumPath), chromiumArgs...)...,
323+
)
309324
}
325+
310326
cmd.Stdout = stdout
311327
cmd.Stderr = stderr
312-
if s.opts.UserGroup != 0 {
313-
cmd.SysProcAttr = &syscall.SysProcAttr{
314-
Credential: &syscall.Credential{
315-
Uid: uint32(s.opts.UserGroup),
316-
Gid: uint32(s.opts.UserGroup),
317-
},
318-
}
319-
}
320328

321329
created := time.Now()
322330
defer func() {
323331
s.metrics.SessionDuration.Observe(time.Since(created).Seconds())
324332
}()
325333

326-
err = cmd.Run()
327-
328334
attrs := make([]slog.Attr, 0, 9)
329335

336+
err := cmd.Run()
330337
if err != nil {
331338
attrs = append(attrs, slog.Attr{Key: "err", Value: slog.AnyValue(err)})
332339
}
@@ -382,37 +389,6 @@ func (s *Supervisor) launch(ctx context.Context, sessionID string) error {
382389
return nil
383390
}
384391

385-
func (s *Supervisor) mkdirTemp() (string, error) {
386-
_, err := os.Stat(s.opts.TempDir)
387-
if errors.Is(err, fs.ErrNotExist) {
388-
s.logger.Warn(
389-
"Specified TempDir does not exist, is it mounted? Falling back to creating it.",
390-
"TempDir", s.opts.TempDir,
391-
)
392-
err = os.MkdirAll(s.opts.TempDir, 0o755) // 700 would not allow other users to descend into subdirectories.
393-
if err != nil {
394-
return "", fmt.Errorf("tmpdir does not exist and couldn't be created: %w", err)
395-
}
396-
}
397-
398-
tmpDir, err := os.MkdirTemp(s.opts.TempDir, "")
399-
if err != nil {
400-
return "", err
401-
}
402-
403-
if s.opts.UserGroup == 0 {
404-
// No chowning necessary.
405-
return tmpDir, nil
406-
}
407-
408-
err = os.Chown(tmpDir, s.opts.UserGroup, s.opts.UserGroup)
409-
if err != nil {
410-
return "", fmt.Errorf("chowning temporary dir: %w", err)
411-
}
412-
413-
return tmpDir, nil
414-
}
415-
416392
// ComputeUserAgent runs chromium once, retrieves its default user agent, and stores a patched version so it can be used
417393
// in all subsequent calls.
418394
func (s *Supervisor) ComputeUserAgent(ctx context.Context) error {

crocochrome_test.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ func TestCrocochrome(t *testing.T) {
2222

2323
hb := testutil.NewHeartbeat(t)
2424
port := testutil.HTTPInfo(t, testutil.ChromiumVersionHandler)
25-
cc := crocochrome.New(logger, crocochrome.Options{ChromiumPath: hb.Path, ChromiumPort: port})
25+
cc := crocochrome.New(logger, crocochrome.Options{
26+
ChromiumPath: hb.Path,
27+
ChromiumPort: port,
28+
DisableBubblewrap: true,
29+
})
2630

2731
session, err := cc.Create()
2832
if err != nil {
@@ -62,7 +66,11 @@ func TestCrocochrome(t *testing.T) {
6266

6367
hb := testutil.NewHeartbeat(t)
6468
port := testutil.HTTPInfo(t, testutil.InternalServerErrorHandler)
65-
cc := crocochrome.New(logger, crocochrome.Options{ChromiumPath: hb.Path, ChromiumPort: port})
69+
cc := crocochrome.New(logger, crocochrome.Options{
70+
ChromiumPath: hb.Path,
71+
ChromiumPort: port,
72+
DisableBubblewrap: true,
73+
})
6674

6775
_, err := cc.Create()
6876
if err == nil {
@@ -76,7 +84,11 @@ func TestCrocochrome(t *testing.T) {
7684
t.Parallel()
7785

7886
hb := testutil.NewHeartbeat(t)
79-
cc := crocochrome.New(logger, crocochrome.Options{ChromiumPath: hb.Path, ChromiumPort: "0"})
87+
cc := crocochrome.New(logger, crocochrome.Options{
88+
ChromiumPath: hb.Path,
89+
ChromiumPort: "0",
90+
DisableBubblewrap: true,
91+
})
8092

8193
_, err := cc.Create()
8294
if err == nil {
@@ -92,7 +104,11 @@ func TestCrocochrome(t *testing.T) {
92104

93105
hb := testutil.NewHeartbeat(t)
94106
port := testutil.HTTPInfo(t, testutil.ChromiumVersionHandler)
95-
cc := crocochrome.New(logger, crocochrome.Options{ChromiumPath: hb.Path, ChromiumPort: port})
107+
cc := crocochrome.New(logger, crocochrome.Options{
108+
ChromiumPath: hb.Path,
109+
ChromiumPort: port,
110+
DisableBubblewrap: true,
111+
})
96112

97113
sess, err := cc.Create()
98114
if err != nil {
@@ -115,7 +131,11 @@ func TestCrocochrome(t *testing.T) {
115131

116132
hb := testutil.NewHeartbeat(t)
117133
port := testutil.HTTPInfo(t, testutil.ChromiumVersionHandler)
118-
cc := crocochrome.New(logger, crocochrome.Options{ChromiumPath: hb.Path, ChromiumPort: port})
134+
cc := crocochrome.New(logger, crocochrome.Options{
135+
ChromiumPath: hb.Path,
136+
ChromiumPort: port,
137+
DisableBubblewrap: true,
138+
})
119139

120140
sess1, err := cc.Create()
121141
if err != nil {
@@ -143,7 +163,12 @@ func TestCrocochrome(t *testing.T) {
143163

144164
hb := testutil.NewHeartbeat(t)
145165
port := testutil.HTTPInfo(t, testutil.ChromiumVersionHandler)
146-
cc := crocochrome.New(logger, crocochrome.Options{ChromiumPath: hb.Path, ChromiumPort: port, SessionTimeout: 3 * time.Second})
166+
cc := crocochrome.New(logger, crocochrome.Options{
167+
ChromiumPath: hb.Path,
168+
ChromiumPort: port,
169+
DisableBubblewrap: true,
170+
SessionTimeout: 3 * time.Second,
171+
})
147172

148173
_, err := cc.Create()
149174
if err != nil {

integration/integration_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ func TestIntegration(t *testing.T) {
5454
ExposedPorts: []string{"8080/tcp"},
5555
WaitingFor: wait.ForExposedPort(),
5656
Networks: []string{network.Name},
57-
// Since https://github.com/grafana/crocochrome/pull/12, crocochrome requires /chromium-tmp to exist
58-
// and be writable.
59-
Mounts: testcontainers.Mounts(testcontainers.VolumeMount("chromium-tmp", "/chromium-tmp")),
57+
Privileged: true, // Required for bubblewrap to work.
6058
},
6159
})
6260
testcontainers.CleanupContainer(t, cc)

0 commit comments

Comments
 (0)