evalbox_sandbox/isolation/
lockdown.rs1use std::ffi::CString;
20use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
21use std::os::unix::ffi::OsStrExt;
22use std::path::Path;
23
24use evalbox_sys::landlock::{
25 self, fs_access_for_abi, landlock_add_rule_path, landlock_create_ruleset,
26 landlock_restrict_self, net_access_for_abi, LandlockPathBeneathAttr, LandlockRulesetAttr,
27 LANDLOCK_ACCESS_FS_EXECUTE, LANDLOCK_ACCESS_FS_MAKE_DIR, LANDLOCK_ACCESS_FS_MAKE_REG,
28 LANDLOCK_ACCESS_FS_READ_DIR, LANDLOCK_ACCESS_FS_READ_FILE, LANDLOCK_ACCESS_FS_REMOVE_DIR,
29 LANDLOCK_ACCESS_FS_REMOVE_FILE, LANDLOCK_ACCESS_FS_TRUNCATE, LANDLOCK_ACCESS_FS_WRITE_FILE,
30};
31use evalbox_sys::last_errno;
32use evalbox_sys::seccomp::{build_whitelist_filter, seccomp_set_mode_filter, SockFprog, DEFAULT_WHITELIST};
33use rustix::io::Errno;
34use thiserror::Error;
35
36use super::rootfs::apply_rlimits;
37use crate::plan::Plan;
38
39#[derive(Debug, Error)]
41pub enum LockdownError {
42 #[error("landlock: {0}")]
43 Landlock(Errno),
44
45 #[error("seccomp: {0}")]
46 Seccomp(Errno),
47
48 #[error("rlimit: {0}")]
49 Rlimit(Errno),
50
51 #[error("capability: {0}")]
52 Capability(Errno),
53
54 #[error("close fds: {0}")]
55 CloseFds(Errno),
56}
57
58pub fn lockdown(plan: &Plan, workspace_path: Option<&Path>, extra_readonly_paths: &[&str]) -> Result<(), LockdownError> {
59 apply_landlock(plan, workspace_path, extra_readonly_paths)?;
60 apply_seccomp()?;
61 apply_rlimits(plan).map_err(LockdownError::Rlimit)?;
62 drop_all_caps()?;
63 close_extra_fds()?;
64 Ok(())
65}
66
67fn apply_landlock(plan: &Plan, workspace_path: Option<&Path>, extra_readonly_paths: &[&str]) -> Result<(), LockdownError> {
68 let abi = match landlock::landlock_abi_version() {
69 Ok(v) => v,
70 Err(_) => return Ok(()), };
72
73 let fs_access = fs_access_for_abi(abi);
74 let net_access = if plan.network_blocked && abi >= 4 { net_access_for_abi(abi) } else { 0 };
75
76 let attr = LandlockRulesetAttr { handled_access_fs: fs_access, handled_access_net: net_access };
77 let ruleset_fd = landlock_create_ruleset(&attr).map_err(LockdownError::Landlock)?;
78
79 let read_access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
80 let write_access = read_access
81 | LANDLOCK_ACCESS_FS_WRITE_FILE
82 | LANDLOCK_ACCESS_FS_MAKE_REG
83 | LANDLOCK_ACCESS_FS_MAKE_DIR
84 | LANDLOCK_ACCESS_FS_REMOVE_FILE
85 | LANDLOCK_ACCESS_FS_REMOVE_DIR
86 | LANDLOCK_ACCESS_FS_TRUNCATE;
87
88 for mount in &plan.mounts {
90 if !mount.writable {
91 let access = if mount.executable { read_access } else { read_access & !LANDLOCK_ACCESS_FS_EXECUTE };
92 add_path_rule(&ruleset_fd, &mount.target, access);
93 }
94 }
95
96 for path in extra_readonly_paths {
97 add_path_rule(&ruleset_fd, path, read_access);
98 }
99
100 if let Some(ws_path) = workspace_path {
102 add_path_rule(&ruleset_fd, ws_path, write_access);
103 }
104
105 for path in ["/work", "/tmp", "/home"] {
107 add_path_rule(&ruleset_fd, path, write_access);
108 }
109
110 add_path_rule(&ruleset_fd, "/proc", read_access);
112
113 add_path_rule(&ruleset_fd, "/dev", read_access | LANDLOCK_ACCESS_FS_WRITE_FILE);
115
116 landlock_restrict_self(&ruleset_fd).map_err(LockdownError::Landlock)
117}
118
119fn add_path_rule(ruleset_fd: &OwnedFd, path: impl AsRef<Path>, access: u64) {
125 let path = path.as_ref();
126 let fd = match open_path(path) {
127 Ok(fd) => fd,
128 Err(_) => return, };
130
131 let rule = LandlockPathBeneathAttr { allowed_access: access, parent_fd: fd.as_raw_fd() };
132 if let Err(e) = landlock_add_rule_path(ruleset_fd, &rule) {
133 eprintln!("warning: landlock rule for {path:?} failed: {e}");
135 }
136}
137
138#[inline]
139fn open_path(path: impl AsRef<Path>) -> Result<OwnedFd, Errno> {
140 let path_c = CString::new(path.as_ref().as_os_str().as_bytes()).map_err(|_| Errno::INVAL)?;
141 let fd = unsafe { libc::open(path_c.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
142 if fd < 0 { Err(last_errno()) } else { Ok(unsafe { OwnedFd::from_raw_fd(fd) }) }
143}
144
145fn apply_seccomp() -> Result<(), LockdownError> {
146 let filter = build_whitelist_filter(DEFAULT_WHITELIST);
147 let fprog = SockFprog { len: filter.len() as u16, filter: filter.as_ptr() };
148 unsafe { seccomp_set_mode_filter(&fprog) }.map_err(LockdownError::Seccomp)
149}
150
151fn drop_all_caps() -> Result<(), LockdownError> {
152 unsafe {
153 libc::prctl(libc::PR_CAP_AMBIENT, libc::PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
154 for cap in 0..64 {
155 libc::prctl(libc::PR_CAPBSET_DROP, cap, 0, 0, 0);
156 }
157 }
158
159 let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
160 if ret != 0 { Err(LockdownError::Capability(last_errno())) } else { Ok(()) }
161}
162
163fn close_extra_fds() -> Result<(), LockdownError> {
164 let mut fds_to_close = Vec::new();
165
166 if let Ok(entries) = std::fs::read_dir("/proc/self/fd") {
167 for entry in entries.flatten() {
168 if let Ok(fd) = entry.file_name().to_string_lossy().parse::<RawFd>() {
169 if fd > 2 {
170 fds_to_close.push(fd);
171 }
172 }
173 }
174 }
175
176 for fd in fds_to_close {
177 unsafe { libc::close(fd) };
178 }
179
180 Ok(())
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn open_path_valid() {
189 assert!(open_path("/tmp").is_ok());
190 }
191}