返回 DeepSeek-TUI-2026
landlock.rs
根目录 / crates / tui / src / sandbox / landlock.rs
1 //! Linux Landlock sandbox implementation.
2 //!
3 //! Landlock is a security mechanism introduced in Linux kernel 5.13 that allows
4 //! processes to restrict their own access rights. Unlike Seatbelt on macOS which
5 //! uses an external sandbox-exec wrapper, Landlock applies restrictions directly
6 //! to the current process.
7 //!
8 //! # Requirements
9 //!
10 //! - Linux kernel 5.13 or later with Landlock enabled
11 //! - The kernel must be compiled with `CONFIG_SECURITY_LANDLOCK=y`
12 //!
13 //! # How it works
14 //!
15 //! 1. Create a landlock ruleset with desired restrictions
16 //! 2. Add rules to allow specific file paths
17 //! 3. Restrict the process using the ruleset
18 //!
19 //! Note: Once restricted, the process cannot gain more privileges.
20
21 use super::{CommandSpec, SandboxPolicy};
22 use std::ffi::CString;
23 use std::path::Path;
24
25 /// Check if Landlock is available on this system.
26 pub fn is_available() -> bool {
27 // Check if the landlock syscall is available
28 #[cfg(target_os = "linux")]
29 {
30 // Try to create a minimal ruleset to test availability
31 // Landlock ABI version check
32 // Safety: syscall uses a null ruleset pointer for ABI probing and does not dereference it.
33 unsafe {
34 let result = libc::syscall(
35 libc::SYS_landlock_create_ruleset,
36 std::ptr::null::<libc::c_void>(),
37 0usize,
38 LANDLOCK_CREATE_RULESET_VERSION,
39 );
40 result >= 0
41 }
42 }
43
44 #[cfg(not(target_os = "linux"))]
45 {
46 false
47 }
48 }
49
50 /// Get the Landlock ABI version supported by the kernel.
51 #[cfg(target_os = "linux")]
52 pub fn get_abi_version() -> Option<i32> {
53 // Safety: syscall uses a null ruleset pointer for ABI probing and does not dereference it.
54 unsafe {
55 let result = libc::syscall(
56 libc::SYS_landlock_create_ruleset,
57 std::ptr::null::<libc::c_void>(),
58 0usize,
59 LANDLOCK_CREATE_RULESET_VERSION,
60 );
61 if result >= 0 {
62 i32::try_from(result).ok()
63 } else {
64 None
65 }
66 }
67 }
68
69 // Landlock syscall constants (not yet in libc crate)
70 #[cfg(target_os = "linux")]
71 const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1 << 0;
72
73 #[cfg(target_os = "linux")]
74 const LANDLOCK_ACCESS_FS_EXECUTE: u64 = 1 << 0;
75 #[cfg(target_os = "linux")]
76 const LANDLOCK_ACCESS_FS_WRITE_FILE: u64 = 1 << 1;
77 #[cfg(target_os = "linux")]
78 const LANDLOCK_ACCESS_FS_READ_FILE: u64 = 1 << 2;
79 #[cfg(target_os = "linux")]
80 const LANDLOCK_ACCESS_FS_READ_DIR: u64 = 1 << 3;
81 #[cfg(target_os = "linux")]
82 const LANDLOCK_ACCESS_FS_REMOVE_DIR: u64 = 1 << 4;
83 #[cfg(target_os = "linux")]
84 const LANDLOCK_ACCESS_FS_REMOVE_FILE: u64 = 1 << 5;
85 #[cfg(target_os = "linux")]
86 const LANDLOCK_ACCESS_FS_MAKE_CHAR: u64 = 1 << 6;
87 #[cfg(target_os = "linux")]
88 const LANDLOCK_ACCESS_FS_MAKE_DIR: u64 = 1 << 7;
89 #[cfg(target_os = "linux")]
90 const LANDLOCK_ACCESS_FS_MAKE_REG: u64 = 1 << 8;
91 #[cfg(target_os = "linux")]
92 const LANDLOCK_ACCESS_FS_MAKE_SOCK: u64 = 1 << 9;
93 #[cfg(target_os = "linux")]
94 const LANDLOCK_ACCESS_FS_MAKE_FIFO: u64 = 1 << 10;
95 #[cfg(target_os = "linux")]
96 const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u64 = 1 << 11;
97 #[cfg(target_os = "linux")]
98 const LANDLOCK_ACCESS_FS_MAKE_SYM: u64 = 1 << 12;
99 #[cfg(target_os = "linux")]
100 const LANDLOCK_ACCESS_FS_REFER: u64 = 1 << 13;
101 #[cfg(target_os = "linux")]
102 const LANDLOCK_ACCESS_FS_TRUNCATE: u64 = 1 << 14;
103
104 // Combinations
105 #[cfg(target_os = "linux")]
106 const LANDLOCK_ACCESS_FS_READ: u64 = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
107
108 #[cfg(target_os = "linux")]
109 const LANDLOCK_ACCESS_FS_WRITE: u64 = LANDLOCK_ACCESS_FS_WRITE_FILE
110 | LANDLOCK_ACCESS_FS_REMOVE_DIR
111 | LANDLOCK_ACCESS_FS_REMOVE_FILE
112 | LANDLOCK_ACCESS_FS_MAKE_DIR
113 | LANDLOCK_ACCESS_FS_MAKE_REG
114 | LANDLOCK_ACCESS_FS_MAKE_SYM
115 | LANDLOCK_ACCESS_FS_TRUNCATE;
116
117 /// Landlock ruleset attribute structure
118 #[cfg(target_os = "linux")]
119 #[repr(C)]
120 struct LandlockRulesetAttr {
121 handled_access_fs: u64,
122 }
123
124 /// Landlock path beneath attribute structure
125 #[cfg(target_os = "linux")]
126 #[repr(C)]
127 struct LandlockPathBeneathAttr {
128 allowed_access: u64,
129 parent_fd: i32,
130 }
131
132 /// Rule type constants
133 #[cfg(target_os = "linux")]
134 const LANDLOCK_RULE_PATH_BENEATH: u32 = 1;
135
136 /// A configured Landlock sandbox
137 #[cfg(target_os = "linux")]
138 pub struct LandlockSandbox {
139 ruleset_fd: i32,
140 policy: SandboxPolicy,
141 }
142
143 #[cfg(target_os = "linux")]
144 impl LandlockSandbox {
145 /// Create a new Landlock sandbox from policy
146 pub fn from_policy(policy: &SandboxPolicy) -> std::io::Result<Self> {
147 // Determine what filesystem access to handle (restrict)
148 let handled_access =
149 LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_WRITE;
150
151 let attr = LandlockRulesetAttr {
152 handled_access_fs: handled_access,
153 };
154
155 // Create the ruleset
156 // Safety: `attr` is a valid pointer for the syscall duration and size is correct.
157 let ruleset_fd = unsafe {
158 libc::syscall(
159 libc::SYS_landlock_create_ruleset,
160 &raw const attr,
161 std::mem::size_of::<LandlockRulesetAttr>(),
162 0u32,
163 )
164 };
165
166 if ruleset_fd < 0 {
167 return Err(std::io::Error::last_os_error());
168 }
169
170 let ruleset_fd = i32::try_from(ruleset_fd).map_err(|_| {
171 std::io::Error::other("Failed to create Landlock ruleset: file descriptor out of range")
172 })?;
173
174 Ok(Self {
175 ruleset_fd,
176 policy: policy.clone(),
177 })
178 }
179
180 /// Add a read-only rule for a path
181 pub fn allow_read(&self, path: &Path) -> std::io::Result<()> {
182 self.add_rule(path, LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_EXECUTE)
183 }
184
185 /// Add a read-write rule for a path
186 pub fn allow_write(&self, path: &Path) -> std::io::Result<()> {
187 self.add_rule(
188 path,
189 LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_WRITE | LANDLOCK_ACCESS_FS_EXECUTE,
190 )
191 }
192
193 /// Add a path rule to the ruleset
194 fn add_rule(&self, path: &Path, access: u64) -> std::io::Result<()> {
195 let path_cstr = CString::new(path.to_string_lossy().as_bytes())
196 .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid path"))?;
197
198 // Open the path to get a file descriptor
199 // Safety: `path_cstr` is NUL-terminated and lives for the duration of the call.
200 let fd = unsafe { libc::open(path_cstr.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
201
202 if fd < 0 {
203 // Path doesn't exist, skip this rule
204 return Ok(());
205 }
206
207 let attr = LandlockPathBeneathAttr {
208 allowed_access: access,
209 parent_fd: fd,
210 };
211
212 // Safety: `attr` is a valid pointer for the syscall duration.
213 let result = unsafe {
214 libc::syscall(
215 libc::SYS_landlock_add_rule,
216 self.ruleset_fd,
217 LANDLOCK_RULE_PATH_BENEATH,
218 &raw const attr,
219 0u32,
220 )
221 };
222
223 // Safety: `fd` is a valid file descriptor from libc::open.
224 unsafe {
225 libc::close(fd);
226 }
227
228 if result < 0 {
229 return Err(std::io::Error::last_os_error());
230 }
231
232 Ok(())
233 }
234
235 /// Apply the sandbox to the current process
236 ///
237 /// WARNING: This is irreversible for the current process!
238 pub fn apply(&self) -> std::io::Result<()> {
239 // First, drop privileges using prctl
240 // Safety: prctl call uses constant arguments and does not access memory.
241 let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
242 if result < 0 {
243 return Err(std::io::Error::last_os_error());
244 }
245
246 // Now restrict the process
247 // Safety: syscall uses a valid ruleset fd and no pointer arguments.
248 let result =
249 unsafe { libc::syscall(libc::SYS_landlock_restrict_self, self.ruleset_fd, 0u32) };
250
251 if result < 0 {
252 return Err(std::io::Error::last_os_error());
253 }
254
255 Ok(())
256 }
257 }
258
259 #[cfg(target_os = "linux")]
260 impl Drop for LandlockSandbox {
261 fn drop(&mut self) {
262 // Safety: `ruleset_fd` is a valid descriptor created by landlock.
263 unsafe {
264 libc::close(self.ruleset_fd);
265 }
266 }
267 }
268
269 /// Create a helper script that sets up Landlock before running the command.
270 ///
271 /// Since Landlock restricts the current process, we need a helper that:
272 /// 1. Sets up the Landlock ruleset
273 /// 2. Applies the restrictions
274 /// 3. Execs the target command
275 ///
276 /// This returns the command to run with the helper.
277 #[cfg(target_os = "linux")]
278 pub fn create_landlock_wrapper(
279 spec: &CommandSpec,
280 _writable_paths: &[std::path::PathBuf],
281 _readable_paths: &[std::path::PathBuf],
282 ) -> Vec<String> {
283 // For simplicity, we'll use a shell wrapper that applies Landlock via a helper binary
284 // In production, this would be a compiled binary that's part of the CLI
285
286 // For now, just return the original command without sandboxing
287 // A full implementation would include a compiled landlock-helper binary
288 let mut cmd = vec![spec.program.clone()];
289 cmd.extend(spec.args.clone());
290 cmd
291 }
292
293 /// Detect if a failure was caused by Landlock denial
294 #[cfg(target_os = "linux")]
295 pub fn detect_denial(exit_code: i32, stderr: &str) -> bool {
296 if exit_code == 0 {
297 return false;
298 }
299
300 // Landlock denials typically result in EACCES or EPERM
301 stderr.contains("Permission denied")
302 || stderr.contains("Operation not permitted")
303 || stderr.contains("EACCES")
304 || stderr.contains("EPERM")
305 }
306
307 // Stub implementations for non-Linux platforms
308 #[cfg(not(target_os = "linux"))]
309 pub fn get_abi_version() -> Option<i32> {
310 None
311 }
312
313 #[cfg(not(target_os = "linux"))]
314 pub fn detect_denial(_exit_code: i32, _stderr: &str) -> bool {
315 false
316 }
317
318 #[cfg(test)]
319 mod tests {
320 use super::*;
321
322 #[test]
323 fn test_is_available() {
324 // This test will pass regardless of platform
325 let _ = is_available();
326 }
327
328 #[test]
329 #[cfg(target_os = "linux")]
330 fn test_get_abi_version() {
331 // May or may not be available depending on kernel
332 let _ = get_abi_version();
333 }
334
335 #[test]
336 fn test_detect_denial() {
337 #[cfg(target_os = "linux")]
338 {
339 assert!(detect_denial(1, "Permission denied"));
340 assert!(detect_denial(1, "Operation not permitted"));
341 assert!(!detect_denial(0, "Success"));
342 }
343 }
344 }
345
345 lines RUST