1use std::collections::BTreeMap;
17use std::path::Path;
18use std::process::Command;
19
20use landlock::{
21 ABI, Access, AccessFs, CompatLevel, Compatible, PathBeneath, PathFd, Ruleset, RulesetAttr,
22 RulesetCreatedAttr, RulesetStatus,
23};
24use nix::libc;
25use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, SeccompRule, TargetArch};
26
27use super::jail::{JailBackend, RuntimeProfile, SpawnSpec};
28use super::{Runner, RunnerError, RunnerMessage};
29
30const LANDLOCK_ABI: ABI = ABI::V3;
32
33const RUNTIME_READ_PATHS: &[&str] = &[
43 "/lib",
44 "/lib64",
45 "/usr/lib",
46 "/usr/lib64",
47 "/etc/ld.so.cache",
48];
49
50pub struct NamespaceJail;
52
53impl JailBackend for NamespaceJail {
54 fn name(&self) -> &'static str {
55 "linux-namespace"
56 }
57
58 fn command(&self, spec: &SpawnSpec<'_>) -> Result<Command, RunnerError> {
59 if spec.runtime_profile == RuntimeProfile::Provider {
64 return Err(RunnerError::new(
65 "sandbox_unavailable",
66 RunnerMessage::ProviderPlatformUnsupported,
67 ));
68 }
69 let mut command = Command::new(spec.worker);
70 command
71 .arg("sandbox-helper")
72 .arg("--jail")
73 .arg(spec.jail)
74 .arg("--worker")
75 .arg(spec.worker)
76 .arg("--adapter")
77 .arg(spec.mode)
78 .arg("--rlimit-as")
79 .arg(match spec.limits.address_space_bytes {
80 Some(bytes) => bytes.to_string(),
81 None => "none".to_string(),
82 })
83 .arg("--rlimit-cpu")
84 .arg(spec.limits.cpu_seconds.to_string())
85 .arg("--rlimit-fsize")
86 .arg(spec.limits.file_size_bytes.to_string())
87 .arg("--rlimit-nproc")
88 .arg(spec.limits.max_processes.to_string())
89 .arg("--seccomp")
90 .arg(if spec.seccomp { "kill" } else { "off" });
91 for path in spec.exec_grants {
92 command.arg("--grant-exec").arg(path);
93 }
94 for path in spec.read_grants {
95 command.arg("--grant-read").arg(path);
96 }
97 Ok(command)
98 }
99
100 fn policy_material(&self) -> String {
101 format!(
102 "newuser+newnet landlock-abi-v3 runtime={} seccomp-allow={}",
103 RUNTIME_READ_PATHS.join(","),
104 allowed_syscalls()
105 .iter()
106 .map(|n| n.to_string())
107 .collect::<Vec<_>>()
108 .join(",")
109 )
110 }
111
112 fn probe(&self, runner: &Runner) -> Result<(), RunnerError> {
118 super::jail::probe_net(runner, false)?;
119 match runner.run_unprobed("probe-seccomp", serde_json::json!({}), &[], true) {
120 Err(e) if e.code == "adapter_crash" => Ok(()),
121 Ok(_) => Err(RunnerError::new(
122 "sandbox_probe_failed",
123 RunnerMessage::SyscallProbeAllowed,
124 )),
125 Err(_) => Err(RunnerError::new(
126 "sandbox_probe_failed",
127 RunnerMessage::SyscallProbeFailed,
128 )),
129 }
130 }
131}
132
133pub(super) fn unshare_namespaces() -> Result<(), String> {
136 use std::os::unix::fs::MetadataExt;
137 let this = std::fs::metadata("/proc/self")
138 .map_err(|e| format!("cannot read process identity: {:?}", e.kind()))?;
139 let (uid, gid) = (this.uid(), this.gid());
140 nix::sched::unshare(
141 nix::sched::CloneFlags::CLONE_NEWUSER | nix::sched::CloneFlags::CLONE_NEWNET,
142 )
143 .map_err(|e| format!("unshare(newuser|newnet) failed: {e:?}"))?;
144 std::fs::write("/proc/self/setgroups", "deny")
145 .map_err(|e| format!("cannot deny setgroups: {:?}", e.kind()))?;
146 std::fs::write("/proc/self/uid_map", format!("{uid} {uid} 1\n"))
147 .map_err(|e| format!("cannot write the uid map: {:?}", e.kind()))?;
148 std::fs::write("/proc/self/gid_map", format!("{gid} {gid} 1\n"))
149 .map_err(|e| format!("cannot write the gid map: {:?}", e.kind()))?;
150 Ok(())
151}
152
153pub(super) fn apply_landlock(
160 jail: &Path,
161 worker: &Path,
162 exec_grants: &[std::path::PathBuf],
163 read_grants: &[std::path::PathBuf],
164) -> Result<(), String> {
165 let ruleset_kind = |error: &landlock::RulesetError| match error {
166 landlock::RulesetError::HandleAccesses(_) => "HandleAccesses",
167 landlock::RulesetError::CreateRuleset(_) => "CreateRuleset",
168 landlock::RulesetError::AddRules(_) => "AddRules",
169 landlock::RulesetError::RestrictSelf(_) => "RestrictSelf",
170 landlock::RulesetError::Scope(_) => "Scope",
171 landlock::RulesetError::RestrictSelfFlags(_) => "RestrictSelfFlags",
172 _ => "Unknown",
173 };
174 let path_fd_kind = |error: landlock::PathFdError| match error {
175 landlock::PathFdError::OpenCall { source, .. } => source.kind(),
176 };
177 let fail = |stage: &str, kind: &str| format!("landlock {stage}: {kind}");
178 let mut ruleset = Ruleset::default()
179 .set_compatibility(CompatLevel::HardRequirement)
180 .handle_access(AccessFs::from_all(LANDLOCK_ABI))
181 .map_err(|e| fail("handle_access", ruleset_kind(&e)))?
182 .create()
183 .map_err(|e| fail("create", ruleset_kind(&e)))?;
184 ruleset = ruleset
185 .add_rule(PathBeneath::new(
186 PathFd::new(jail)
187 .map_err(|e| format!("landlock jail directory: {:?}", path_fd_kind(e)))?,
188 AccessFs::from_all(LANDLOCK_ABI),
189 ))
190 .map_err(|e| fail("jail directory rule", ruleset_kind(&e)))?;
191 ruleset = ruleset
192 .add_rule(PathBeneath::new(
193 PathFd::new(worker)
194 .map_err(|e| format!("landlock worker binary: {:?}", path_fd_kind(e)))?,
195 AccessFs::Execute | AccessFs::ReadFile,
196 ))
197 .map_err(|e| fail("worker binary rule", ruleset_kind(&e)))?;
198 for (index, path) in exec_grants.iter().enumerate() {
199 let label = format!("executable grant {} of {}", index + 1, exec_grants.len());
200 ruleset = ruleset
201 .add_rule(PathBeneath::new(
202 PathFd::new(path)
203 .map_err(|e| format!("landlock {label}: {:?}", path_fd_kind(e)))?,
204 AccessFs::Execute | AccessFs::ReadFile,
205 ))
206 .map_err(|e| fail(&format!("{label} rule"), ruleset_kind(&e)))?;
207 }
208 for (index, path) in read_grants.iter().enumerate() {
209 let label = format!("read grant {} of {}", index + 1, read_grants.len());
210 ruleset = ruleset
211 .add_rule(PathBeneath::new(
212 PathFd::new(path)
213 .map_err(|e| format!("landlock {label}: {:?}", path_fd_kind(e)))?,
214 AccessFs::ReadFile.into(),
215 ))
216 .map_err(|e| fail(&format!("{label} rule"), ruleset_kind(&e)))?;
217 }
218 let runtime_total = RUNTIME_READ_PATHS.len();
219 for (index, path) in RUNTIME_READ_PATHS.iter().enumerate() {
220 if !Path::new(path).exists() {
221 continue;
222 }
223 let access = if Path::new(path).is_dir() {
224 AccessFs::from_read(LANDLOCK_ABI)
225 } else {
226 AccessFs::ReadFile.into()
227 };
228 ruleset = ruleset
229 .add_rule(PathBeneath::new(
230 PathFd::new(path).map_err(|e| {
231 format!(
232 "landlock runtime grant {} of {runtime_total}: {:?}",
233 index + 1,
234 path_fd_kind(e)
235 )
236 })?,
237 access,
238 ))
239 .map_err(|e| {
240 fail(
241 &format!("runtime grant {} of {runtime_total} rule", index + 1),
242 ruleset_kind(&e),
243 )
244 })?;
245 }
246 let status = ruleset
247 .restrict_self()
248 .map_err(|e| fail("restrict_self", ruleset_kind(&e)))?;
249 if status.ruleset != RulesetStatus::FullyEnforced {
250 return Err(format!(
251 "landlock policy is {:?}, full enforcement is required, the kernel floor is 6.2",
252 status.ruleset
253 ));
254 }
255 Ok(())
256}
257
258pub(super) fn apply_seccomp() -> Result<(), String> {
261 let arch = TargetArch::try_from(std::env::consts::ARCH)
262 .map_err(|e| format!("seccomp does not support this architecture: {e:?}"))?;
263 let rules: BTreeMap<i64, Vec<SeccompRule>> = allowed_syscalls()
264 .iter()
265 .map(|syscall| (*syscall, Vec::new()))
266 .collect();
267 let filter = SeccompFilter::new(
268 rules,
269 SeccompAction::KillProcess,
270 SeccompAction::Allow,
271 arch,
272 )
273 .map_err(|e| format!("seccomp filter build failed: {e}"))?;
274 let program: BpfProgram = filter
275 .try_into()
276 .map_err(|e: seccompiler::BackendError| format!("seccomp compile failed: {e}"))?;
277 seccompiler::apply_filter(&program).map_err(|e| format!("seccomp install failed: {e}"))
278}
279
280fn allowed_syscalls() -> Vec<i64> {
287 let mut list = vec![
288 libc::SYS_read,
289 libc::SYS_write,
290 libc::SYS_readv,
291 libc::SYS_writev,
292 libc::SYS_pread64,
293 libc::SYS_pwrite64,
294 libc::SYS_close,
295 libc::SYS_fstat,
296 libc::SYS_newfstatat,
297 libc::SYS_statx,
298 libc::SYS_lseek,
299 libc::SYS_mmap,
300 libc::SYS_mprotect,
301 libc::SYS_munmap,
302 libc::SYS_mremap,
303 libc::SYS_madvise,
304 libc::SYS_brk,
305 libc::SYS_futex,
306 libc::SYS_sched_yield,
307 libc::SYS_sched_getaffinity,
308 libc::SYS_clock_gettime,
309 libc::SYS_clock_getres,
310 libc::SYS_clock_nanosleep,
311 libc::SYS_gettimeofday,
312 libc::SYS_getrandom,
313 libc::SYS_exit,
314 libc::SYS_exit_group,
315 libc::SYS_rt_sigaction,
316 libc::SYS_rt_sigprocmask,
317 libc::SYS_rt_sigreturn,
318 libc::SYS_sigaltstack,
319 libc::SYS_tgkill,
320 libc::SYS_tkill,
321 libc::SYS_getpid,
322 libc::SYS_gettid,
323 libc::SYS_getppid,
324 libc::SYS_getuid,
325 libc::SYS_geteuid,
326 libc::SYS_getgid,
327 libc::SYS_getegid,
328 libc::SYS_getrlimit,
329 libc::SYS_prlimit64,
330 libc::SYS_getrusage,
331 libc::SYS_sysinfo,
332 libc::SYS_times,
333 libc::SYS_uname,
334 libc::SYS_umask,
335 libc::SYS_openat,
336 libc::SYS_faccessat,
337 libc::SYS_faccessat2,
338 libc::SYS_getdents64,
339 libc::SYS_readlinkat,
340 libc::SYS_getcwd,
341 libc::SYS_chdir,
342 libc::SYS_fchdir,
343 libc::SYS_fcntl,
344 libc::SYS_dup,
345 libc::SYS_dup3,
346 libc::SYS_pipe2,
347 libc::SYS_ioctl,
348 libc::SYS_ppoll,
349 libc::SYS_pselect6,
350 libc::SYS_epoll_create1,
351 libc::SYS_epoll_ctl,
352 libc::SYS_epoll_pwait,
353 libc::SYS_eventfd2,
354 libc::SYS_mkdirat,
355 libc::SYS_unlinkat,
356 libc::SYS_renameat,
357 libc::SYS_ftruncate,
358 libc::SYS_fallocate,
359 libc::SYS_fsync,
360 libc::SYS_fdatasync,
361 libc::SYS_flock,
362 libc::SYS_utimensat,
363 libc::SYS_set_tid_address,
364 libc::SYS_set_robust_list,
365 libc::SYS_rseq,
366 libc::SYS_membarrier,
367 libc::SYS_prctl,
368 libc::SYS_clone,
369 libc::SYS_clone3,
370 libc::SYS_execve,
371 libc::SYS_wait4,
372 ];
373 #[cfg(target_arch = "x86_64")]
374 list.extend_from_slice(&[
375 libc::SYS_open,
376 libc::SYS_stat,
377 libc::SYS_lstat,
378 libc::SYS_access,
379 libc::SYS_readlink,
380 libc::SYS_poll,
381 libc::SYS_select,
382 libc::SYS_dup2,
383 libc::SYS_pipe,
384 libc::SYS_epoll_wait,
385 libc::SYS_mkdir,
386 libc::SYS_unlink,
387 libc::SYS_rename,
388 libc::SYS_arch_prctl,
389 libc::SYS_nanosleep,
390 libc::SYS_time,
391 libc::SYS_getdents,
392 ]);
393 list.sort_unstable();
394 list
395}