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, SpawnSpec};
28use super::{Runner, RunnerError};
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 let mut command = Command::new(spec.worker);
60 command
61 .arg("sandbox-helper")
62 .arg("--jail")
63 .arg(spec.jail)
64 .arg("--worker")
65 .arg(spec.worker)
66 .arg("--adapter")
67 .arg(spec.mode)
68 .arg("--rlimit-as")
69 .arg(spec.limits.address_space_bytes.to_string())
70 .arg("--rlimit-cpu")
71 .arg(spec.limits.cpu_seconds.to_string())
72 .arg("--rlimit-fsize")
73 .arg(spec.limits.file_size_bytes.to_string())
74 .arg("--rlimit-nproc")
75 .arg(spec.limits.max_processes.to_string())
76 .arg("--seccomp")
77 .arg(if spec.seccomp { "kill" } else { "off" });
78 Ok(command)
79 }
80
81 fn policy_material(&self) -> String {
82 format!(
83 "newuser+newnet landlock-abi-v3 runtime={} seccomp-allow={}",
84 RUNTIME_READ_PATHS.join(","),
85 allowed_syscalls()
86 .iter()
87 .map(|n| n.to_string())
88 .collect::<Vec<_>>()
89 .join(",")
90 )
91 }
92
93 fn probe(&self, runner: &Runner) -> Result<(), RunnerError> {
99 super::jail::probe_net(runner, false)?;
100 match runner.run_unprobed("probe-seccomp", serde_json::json!({}), &[], true) {
101 Err(e) if e.code == "adapter_crash" => Ok(()),
102 Ok(_) => Err(RunnerError {
103 code: "sandbox_probe_failed",
104 message: "the syscall filter let a socket creation through".to_string(),
105 }),
106 Err(e) => Err(RunnerError {
107 code: "sandbox_probe_failed",
108 message: format!("the syscall filter probe failed to run: {e}"),
109 }),
110 }
111 }
112}
113
114pub(super) fn unshare_namespaces() -> Result<(), String> {
117 use std::os::unix::fs::MetadataExt;
118 let this = std::fs::metadata("/proc/self")
119 .map_err(|e| format!("cannot read process identity: {e}"))?;
120 let (uid, gid) = (this.uid(), this.gid());
121 nix::sched::unshare(
122 nix::sched::CloneFlags::CLONE_NEWUSER | nix::sched::CloneFlags::CLONE_NEWNET,
123 )
124 .map_err(|e| format!("unshare(newuser|newnet) failed: {e}"))?;
125 std::fs::write("/proc/self/setgroups", "deny")
126 .map_err(|e| format!("cannot deny setgroups: {e}"))?;
127 std::fs::write("/proc/self/uid_map", format!("{uid} {uid} 1\n"))
128 .map_err(|e| format!("cannot write the uid map: {e}"))?;
129 std::fs::write("/proc/self/gid_map", format!("{gid} {gid} 1\n"))
130 .map_err(|e| format!("cannot write the gid map: {e}"))?;
131 Ok(())
132}
133
134pub(super) fn apply_landlock(jail: &Path, worker: &Path) -> Result<(), String> {
138 let fail = |stage: &str, e: &dyn std::fmt::Display| format!("landlock {stage}: {e}");
139 let mut ruleset = Ruleset::default()
140 .set_compatibility(CompatLevel::HardRequirement)
141 .handle_access(AccessFs::from_all(LANDLOCK_ABI))
142 .map_err(|e| fail("handle_access", &e))?
143 .create()
144 .map_err(|e| fail("create", &e))?;
145 ruleset = ruleset
146 .add_rule(PathBeneath::new(
147 PathFd::new(jail).map_err(|e| fail("open jail", &e))?,
148 AccessFs::from_all(LANDLOCK_ABI),
149 ))
150 .map_err(|e| fail("jail rule", &e))?;
151 ruleset = ruleset
152 .add_rule(PathBeneath::new(
153 PathFd::new(worker).map_err(|e| fail("open worker", &e))?,
154 AccessFs::Execute | AccessFs::ReadFile,
155 ))
156 .map_err(|e| fail("worker rule", &e))?;
157 for path in RUNTIME_READ_PATHS {
158 if !Path::new(path).exists() {
159 continue;
160 }
161 let access = if Path::new(path).is_dir() {
162 AccessFs::from_read(LANDLOCK_ABI)
163 } else {
164 AccessFs::ReadFile.into()
165 };
166 ruleset = ruleset
167 .add_rule(PathBeneath::new(
168 PathFd::new(path).map_err(|e| fail("open runtime path", &e))?,
169 access,
170 ))
171 .map_err(|e| fail("runtime rule", &e))?;
172 }
173 let status = ruleset
174 .restrict_self()
175 .map_err(|e| fail("restrict_self", &e))?;
176 if status.ruleset != RulesetStatus::FullyEnforced {
177 return Err(format!(
178 "landlock policy is {:?}, full enforcement is required, the kernel floor is 6.2",
179 status.ruleset
180 ));
181 }
182 Ok(())
183}
184
185pub(super) fn apply_seccomp() -> Result<(), String> {
188 let arch = TargetArch::try_from(std::env::consts::ARCH)
189 .map_err(|e| format!("seccomp does not support this architecture: {e:?}"))?;
190 let rules: BTreeMap<i64, Vec<SeccompRule>> = allowed_syscalls()
191 .iter()
192 .map(|syscall| (*syscall, Vec::new()))
193 .collect();
194 let filter = SeccompFilter::new(
195 rules,
196 SeccompAction::KillProcess,
197 SeccompAction::Allow,
198 arch,
199 )
200 .map_err(|e| format!("seccomp filter build failed: {e}"))?;
201 let program: BpfProgram = filter
202 .try_into()
203 .map_err(|e: seccompiler::BackendError| format!("seccomp compile failed: {e}"))?;
204 seccompiler::apply_filter(&program).map_err(|e| format!("seccomp install failed: {e}"))
205}
206
207fn allowed_syscalls() -> Vec<i64> {
214 let mut list = vec![
215 libc::SYS_read,
216 libc::SYS_write,
217 libc::SYS_readv,
218 libc::SYS_writev,
219 libc::SYS_pread64,
220 libc::SYS_pwrite64,
221 libc::SYS_close,
222 libc::SYS_fstat,
223 libc::SYS_newfstatat,
224 libc::SYS_statx,
225 libc::SYS_lseek,
226 libc::SYS_mmap,
227 libc::SYS_mprotect,
228 libc::SYS_munmap,
229 libc::SYS_mremap,
230 libc::SYS_madvise,
231 libc::SYS_brk,
232 libc::SYS_futex,
233 libc::SYS_sched_yield,
234 libc::SYS_sched_getaffinity,
235 libc::SYS_clock_gettime,
236 libc::SYS_clock_getres,
237 libc::SYS_clock_nanosleep,
238 libc::SYS_gettimeofday,
239 libc::SYS_getrandom,
240 libc::SYS_exit,
241 libc::SYS_exit_group,
242 libc::SYS_rt_sigaction,
243 libc::SYS_rt_sigprocmask,
244 libc::SYS_rt_sigreturn,
245 libc::SYS_sigaltstack,
246 libc::SYS_tgkill,
247 libc::SYS_tkill,
248 libc::SYS_getpid,
249 libc::SYS_gettid,
250 libc::SYS_getppid,
251 libc::SYS_getuid,
252 libc::SYS_geteuid,
253 libc::SYS_getgid,
254 libc::SYS_getegid,
255 libc::SYS_getrlimit,
256 libc::SYS_prlimit64,
257 libc::SYS_getrusage,
258 libc::SYS_sysinfo,
259 libc::SYS_times,
260 libc::SYS_uname,
261 libc::SYS_umask,
262 libc::SYS_openat,
263 libc::SYS_faccessat,
264 libc::SYS_faccessat2,
265 libc::SYS_getdents64,
266 libc::SYS_readlinkat,
267 libc::SYS_getcwd,
268 libc::SYS_chdir,
269 libc::SYS_fchdir,
270 libc::SYS_fcntl,
271 libc::SYS_dup,
272 libc::SYS_dup3,
273 libc::SYS_pipe2,
274 libc::SYS_ioctl,
275 libc::SYS_ppoll,
276 libc::SYS_pselect6,
277 libc::SYS_epoll_create1,
278 libc::SYS_epoll_ctl,
279 libc::SYS_epoll_pwait,
280 libc::SYS_eventfd2,
281 libc::SYS_mkdirat,
282 libc::SYS_unlinkat,
283 libc::SYS_renameat,
284 libc::SYS_ftruncate,
285 libc::SYS_fallocate,
286 libc::SYS_fsync,
287 libc::SYS_fdatasync,
288 libc::SYS_flock,
289 libc::SYS_utimensat,
290 libc::SYS_set_tid_address,
291 libc::SYS_set_robust_list,
292 libc::SYS_rseq,
293 libc::SYS_membarrier,
294 libc::SYS_prctl,
295 libc::SYS_clone,
296 libc::SYS_clone3,
297 libc::SYS_execve,
298 libc::SYS_wait4,
299 ];
300 #[cfg(target_arch = "x86_64")]
301 list.extend_from_slice(&[
302 libc::SYS_open,
303 libc::SYS_stat,
304 libc::SYS_lstat,
305 libc::SYS_access,
306 libc::SYS_readlink,
307 libc::SYS_poll,
308 libc::SYS_select,
309 libc::SYS_dup2,
310 libc::SYS_pipe,
311 libc::SYS_epoll_wait,
312 libc::SYS_mkdir,
313 libc::SYS_unlink,
314 libc::SYS_rename,
315 libc::SYS_arch_prctl,
316 libc::SYS_nanosleep,
317 libc::SYS_time,
318 libc::SYS_getdents,
319 ]);
320 list.sort_unstable();
321 list
322}