Skip to content

Commit 55f6408

Browse files
committed
updating firejail execution to have parity with bubblewrap
1 parent 8bf797c commit 55f6408

6 files changed

Lines changed: 203 additions & 11 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Three sandbox backends are supported. `GetRunner()` selects one automatically wh
3232

3333
2. **Bubblewrap** — chosen when `BubblewrapParams.BinaryPath` is set (and not inside a Flatpak). Uses [bubblewrap](https://github.com/containers/bubblewrap) to create a lightweight user-namespace sandbox. Mounts system directories read-only, bind-mounts the game's install folder read-write, and forwards display/audio sockets (X11, Wayland, PulseAudio, PipeWire). Namespace isolation covers user, PID, and UTS; IPC stays shared for X11 MIT-SHM compatibility. Network access is shared by default, with optional isolation via `BubblewrapParams.NoNetwork`.
3434

35-
3. **Firejail** — the fallback when neither of the above apply. Uses [firejail](https://firejail.wordpress.com/) with a generated profile at `{InstallFolder}/.itch/isolate-app.profile` that blacklists sensitive directories and whitelists the game's install folder and temp directory. Per-game local overrides can be placed in `/etc/firejail/` (e.g. `itch_game_{name}.local`), and a global override file `itch_games_globals.local` is also included if present.
35+
3. **Firejail** — the fallback when neither of the above apply. Uses [firejail](https://firejail.wordpress.com/) with a generated profile at `{InstallFolder}/.itch/isolate-app.profile` that blacklists sensitive directories and whitelists the game's install folder and temp directory. Environment forwarding follows the same allowlist baseline as bubblewrap (including itch launch vars and temp vars), and network access can be disabled with `FirejailParams.NoNetwork`. Per-game local overrides can be placed in `/etc/firejail/` (e.g. `itch_game_{name}.local`), and a global override file `itch_games_globals.local` is also included if present.
3636

3737
Selection priority: **Flatpak-spawn > Bubblewrap > Firejail**.
3838

runner/bubblewrap_linux.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,7 @@ func (br *bubblewrapRunner) Run() error {
194194
args = append(args, "--clearenv")
195195

196196
// Environment passthrough
197-
envVarsToForward := []string{
198-
"DISPLAY", "XAUTHORITY", "WAYLAND_DISPLAY", "XDG_RUNTIME_DIR",
199-
"PULSE_SERVER", "DBUS_SESSION_BUS_ADDRESS",
200-
"HOME", "USER", "LANG", "PATH",
201-
"TMP", "TEMP", "TMPDIR",
202-
}
203-
envVarsToForward = append(envVarsToForward, ItchioLaunchEnvVars...)
204-
for _, key := range envVarsToForward {
197+
for _, key := range SandboxEnvAllowlist() {
205198
if val := envLookup(params.Env, key); val != "" {
206199
args = append(args, "--setenv", key, val)
207200
} else if val := os.Getenv(key); val != "" {

runner/env_allowlist_linux.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//go:build linux
2+
3+
package runner
4+
5+
// BaseSandboxEnvVars are platform/session vars required by sandboxed games.
6+
var BaseSandboxEnvVars = []string{
7+
"DISPLAY",
8+
"XAUTHORITY",
9+
"WAYLAND_DISPLAY",
10+
"XDG_RUNTIME_DIR",
11+
"PULSE_SERVER",
12+
"DBUS_SESSION_BUS_ADDRESS",
13+
"HOME",
14+
"USER",
15+
"LANG",
16+
"PATH",
17+
"TMP",
18+
"TEMP",
19+
"TMPDIR",
20+
}
21+
22+
func SandboxEnvAllowlist() []string {
23+
allowlist := make([]string, 0, len(BaseSandboxEnvVars)+len(ItchioLaunchEnvVars))
24+
allowlist = append(allowlist, BaseSandboxEnvVars...)
25+
allowlist = append(allowlist, ItchioLaunchEnvVars...)
26+
return allowlist
27+
}
28+
29+
func collectAllowedEnv(paramsEnv []string, hostEnv []string) []string {
30+
allowlist := SandboxEnvAllowlist()
31+
out := make([]string, 0, len(allowlist))
32+
for _, key := range allowlist {
33+
if val := envLookup(paramsEnv, key); val != "" {
34+
out = append(out, key+"="+val)
35+
continue
36+
}
37+
if val := envLookup(hostEnv, key); val != "" {
38+
out = append(out, key+"="+val)
39+
}
40+
}
41+
return out
42+
}

runner/firejail_linux.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type firejailRunner struct {
1717
}
1818

1919
var _ Runner = (*firejailRunner)(nil)
20+
var firejailCommand = exec.Command
2021

2122
func newFirejailRunner(params RunnerParams) (Runner, error) {
2223
if params.FirejailParams.BinaryPath == "" {
@@ -67,13 +68,16 @@ func (fr *firejailRunner) Run() error {
6768

6869
var args []string
6970
args = append(args, fmt.Sprintf("--profile=%s", sandboxProfilePath))
71+
if params.FirejailParams.NoNetwork {
72+
args = append(args, "--net=none")
73+
}
7074
args = append(args, "--")
7175
args = append(args, params.FullTargetPath)
7276
args = append(args, params.Args...)
7377

74-
cmd := exec.Command(firejailPath, args...)
78+
cmd := firejailCommand(firejailPath, args...)
7579
cmd.Dir = params.Dir
76-
cmd.Env = params.Env
80+
cmd.Env = collectAllowedEnv(params.Env, os.Environ())
7781
cmd.Stdout = params.Stdout
7882
cmd.Stderr = params.Stderr
7983

runner/firejail_linux_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//go:build linux
2+
3+
package runner
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"os/exec"
9+
"strings"
10+
"testing"
11+
12+
"github.com/itchio/headway/state"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func newFirejailTestConsumer(t *testing.T) *state.Consumer {
18+
t.Helper()
19+
return &state.Consumer{
20+
OnMessage: func(lvl string, msg string) {
21+
t.Logf("[%s] %s", lvl, msg)
22+
},
23+
}
24+
}
25+
26+
func newFirejailTestRunner(t *testing.T, noNetwork bool) *firejailRunner {
27+
t.Helper()
28+
return &firejailRunner{
29+
params: RunnerParams{
30+
Consumer: newFirejailTestConsumer(t),
31+
Ctx: context.Background(),
32+
Name: "test-game",
33+
FullTargetPath: "/bin/true",
34+
InstallFolder: t.TempDir(),
35+
TempDir: t.TempDir(),
36+
FirejailParams: FirejailParams{
37+
BinaryPath: "/fake/firejail",
38+
NoNetwork: noNetwork,
39+
},
40+
},
41+
}
42+
}
43+
44+
func parseEnvironmentOutput(output string) map[string]string {
45+
out := make(map[string]string)
46+
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
47+
if line == "" {
48+
continue
49+
}
50+
key, value, ok := strings.Cut(line, "=")
51+
if !ok {
52+
continue
53+
}
54+
out[key] = value
55+
}
56+
return out
57+
}
58+
59+
func TestFirejailNoNetworkAddsNetNone(t *testing.T) {
60+
origCommand := firejailCommand
61+
t.Cleanup(func() {
62+
firejailCommand = origCommand
63+
})
64+
65+
var gotName string
66+
var gotArgs []string
67+
firejailCommand = func(name string, args ...string) *exec.Cmd {
68+
gotName = name
69+
gotArgs = append([]string{}, args...)
70+
return exec.Command("sh", "-c", "true")
71+
}
72+
73+
fr := newFirejailTestRunner(t, true)
74+
require.NoError(t, fr.Run())
75+
assert.Equal(t, "/fake/firejail", gotName)
76+
assert.Contains(t, gotArgs, "--net=none")
77+
}
78+
79+
func TestFirejailNoNetworkDisabledOmitsNetNone(t *testing.T) {
80+
origCommand := firejailCommand
81+
t.Cleanup(func() {
82+
firejailCommand = origCommand
83+
})
84+
85+
var gotArgs []string
86+
firejailCommand = func(name string, args ...string) *exec.Cmd {
87+
gotArgs = append([]string{}, args...)
88+
return exec.Command("sh", "-c", "true")
89+
}
90+
91+
fr := newFirejailTestRunner(t, false)
92+
require.NoError(t, fr.Run())
93+
assert.NotContains(t, gotArgs, "--net=none")
94+
}
95+
96+
func TestFirejailUsesAllowlistedEnvironment(t *testing.T) {
97+
origCommand := firejailCommand
98+
t.Cleanup(func() {
99+
firejailCommand = origCommand
100+
})
101+
102+
firejailCommand = func(name string, args ...string) *exec.Cmd {
103+
return exec.Command("sh", "-c", "env")
104+
}
105+
106+
var stdout bytes.Buffer
107+
fr := newFirejailTestRunner(t, false)
108+
fr.params.Stdout = &stdout
109+
fr.params.Env = []string{
110+
"OPENAI_API_KEY=super-secret",
111+
"USER=sandbox-user",
112+
"ITCHIO_SANDBOX=1",
113+
"TMP=/game/.itch/temp",
114+
}
115+
116+
require.NoError(t, fr.Run())
117+
118+
gotEnv := parseEnvironmentOutput(stdout.String())
119+
assert.Equal(t, "sandbox-user", gotEnv["USER"])
120+
assert.Equal(t, "1", gotEnv["ITCHIO_SANDBOX"])
121+
assert.Equal(t, "/game/.itch/temp", gotEnv["TMP"])
122+
_, hasSecret := gotEnv["OPENAI_API_KEY"]
123+
assert.False(t, hasSecret, "unlisted variables must not be forwarded")
124+
}
125+
126+
func TestFirejailEnvironmentPrefersParamsAndFallsBackToHost(t *testing.T) {
127+
origCommand := firejailCommand
128+
t.Cleanup(func() {
129+
firejailCommand = origCommand
130+
})
131+
132+
firejailCommand = func(name string, args ...string) *exec.Cmd {
133+
return exec.Command("sh", "-c", "env")
134+
}
135+
136+
t.Setenv("USER", "host-user")
137+
t.Setenv("LANG", "host-lang")
138+
139+
var stdout bytes.Buffer
140+
fr := newFirejailTestRunner(t, false)
141+
fr.params.Stdout = &stdout
142+
fr.params.Env = []string{
143+
"USER=params-user",
144+
}
145+
146+
require.NoError(t, fr.Run())
147+
148+
gotEnv := parseEnvironmentOutput(stdout.String())
149+
assert.Equal(t, "params-user", gotEnv["USER"])
150+
assert.Equal(t, "host-lang", gotEnv["LANG"])
151+
}

runner/runner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type RunnerParams struct {
4242

4343
type FirejailParams struct {
4444
BinaryPath string
45+
// NoNetwork disables network access in the sandbox (--net=none flag)
46+
NoNetwork bool
4547
}
4648

4749
type BubblewrapParams struct {

0 commit comments

Comments
 (0)