Skip to content

Commit ea4626a

Browse files
koki-developclaude
andcommitted
fix: add nosuid/nodev to /code mount and block Landlock syscalls
Move /code bind mount from CLI -B flag to nsjail protobuf config using prefix_src_env so nosuid/nodev flags can be set (N-2). Block landlock_create_ruleset, landlock_add_rule, and landlock_restrict_self syscalls in seccomp policy to reduce kernel attack surface (S-2). Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a10a600 commit ea4626a

6 files changed

Lines changed: 108 additions & 5 deletions

File tree

e2e/tests/security/bind_mount_flags.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,24 @@ tests:
7979
exit_code: 0
8080
status: "OK"
8181
signal: null
82+
83+
- name: "/code bind mount has nosuid and nodev flags"
84+
requests:
85+
- input:
86+
runtime: bash
87+
files:
88+
- name: main.sh
89+
type: plain
90+
content: |
91+
grep ' /code ' /proc/mounts | tr ' ' '\n' | sed -n '4p' | tr ',' '\n' | grep -E '^(nosuid|nodev)$' | sort
92+
output:
93+
status: 200
94+
body:
95+
compile: null
96+
run:
97+
stdout: "nodev\nnosuid\n"
98+
stderr: ""
99+
output: "nodev\nnosuid\n"
100+
exit_code: 0
101+
status: "OK"
102+
signal: null

e2e/tests/security/seccomp_misc.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,66 @@ tests:
8181
exit_code: 159
8282
status: "SIGNAL"
8383
signal: "SIGSYS"
84+
85+
- name: "landlock_create_ruleset is blocked with SIGSYS"
86+
requests:
87+
- input:
88+
runtime: ruby
89+
files:
90+
- name: main.rb
91+
type: plain
92+
content: |
93+
syscall(444, 0, 0, 0)
94+
output:
95+
status: 200
96+
body:
97+
compile: null
98+
run:
99+
stdout: ""
100+
stderr: ""
101+
output: ""
102+
exit_code: 159
103+
status: "SIGNAL"
104+
signal: "SIGSYS"
105+
106+
- name: "landlock_add_rule is blocked with SIGSYS"
107+
requests:
108+
- input:
109+
runtime: ruby
110+
files:
111+
- name: main.rb
112+
type: plain
113+
content: |
114+
syscall(445, 0, 0, 0)
115+
output:
116+
status: 200
117+
body:
118+
compile: null
119+
run:
120+
stdout: ""
121+
stderr: ""
122+
output: ""
123+
exit_code: 159
124+
status: "SIGNAL"
125+
signal: "SIGSYS"
126+
127+
- name: "landlock_restrict_self is blocked with SIGSYS"
128+
requests:
129+
- input:
130+
runtime: ruby
131+
files:
132+
- name: main.rb
133+
type: plain
134+
content: |
135+
syscall(446, 0, 0, 0)
136+
output:
137+
status: 200
138+
body:
139+
compile: null
140+
run:
141+
stdout: ""
142+
stderr: ""
143+
output: ""
144+
exit_code: 159
145+
status: "SIGNAL"
146+
signal: "SIGSYS"

internal/sandbox/configs/nsjail.cfg

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Static nsjail configuration for the code execution sandbox.
2-
# All settings here are identical across every invocation. Per-invocation
3-
# settings (runtime bind mounts, user code directory, resource limits,
4-
# timeout, environment variables, command) are passed as CLI flags.
2+
# The user code directory source path varies per request via the
3+
# NSJAIL_WORKING_DIR environment variable. Other per-invocation settings
4+
# (runtime bind mounts, resource limits, timeout, environment variables,
5+
# command) are passed as CLI flags.
56

67
# Run the sandboxed process exactly once, then exit.
78
mode: ONCE
@@ -181,6 +182,18 @@ mount {
181182
is_symlink: true
182183
}
183184

185+
# User code directory (read-write, per-invocation). The source path is set
186+
# dynamically via the NSJAIL_WORKING_DIR environment variable, which the Go
187+
# process sets to the per-request temporary directory before launching nsjail.
188+
mount {
189+
prefix_src_env: "NSJAIL_WORKING_DIR"
190+
dst: "/code"
191+
is_bind: true
192+
rw: true
193+
nosuid: true
194+
nodev: true
195+
}
196+
184197
# -- UID/GID mapping --
185198
# Map host UID 0 (root inside the Docker container) to nobody (65534) inside
186199
# the sandbox. outside_id is omitted, defaulting to "" which resolves to the

internal/sandbox/configs/seccomp.kafel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ POLICY blocked {
195195
// always wins), but block as defense-in-depth.
196196
seccomp,
197197

198+
// -- Landlock LSM (Linux 5.13+) --
199+
// Non-privileged access control. Sandboxed code has no legitimate use
200+
// for Landlock; block to reduce kernel attack surface.
201+
landlock_create_ruleset,
202+
landlock_add_rule,
203+
landlock_restrict_self,
204+
198205
// -- I/O privilege (x86_64 only) --
199206
// Direct I/O port access bypasses all OS-level protection.
200207
ioperm ON x86_64,

internal/sandbox/execution.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ func (e *execution) buildArgs() []string {
5656
}
5757

5858
args = append(args,
59-
"-B", e.tmpDir+":/code", // user code directory (read-write, per-invocation)
6059
// Per-invocation resource limits: vary by runtime and execution step.
6160
"--rlimit_as", e.limits.Rlimits.AS,
6261
"--rlimit_fsize", e.limits.Rlimits.Fsize,
@@ -142,6 +141,7 @@ func (e *execution) closeReadEnds() {
142141

143142
func (e *execution) start(ctx context.Context, args []string) (*exec.Cmd, error) {
144143
cmd := exec.CommandContext(ctx, nsjailPath, args...)
144+
cmd.Env = append(os.Environ(), "NSJAIL_WORKING_DIR="+e.tmpDir)
145145

146146
cmd.Stdout = e.stdoutW
147147
cmd.Stderr = e.stderrW

internal/sandbox/sandbox_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ func TestExecution_buildArgs(t *testing.T) {
133133
want := []string{
134134
"-C", "/etc/nsjail/nsjail.cfg",
135135
"-R", "/mise/installs/node/24:/mise/installs/node/24",
136-
"-B", "/tmp/sandbox-code:/code",
137136
"--rlimit_as", "4096",
138137
"--rlimit_fsize", "64",
139138
"--rlimit_nofile", "64",

0 commit comments

Comments
 (0)