Skip to main content

sandbox_landlock/
ruleset.rs

1//! Landlock filesystem access control
2//!
3//! Provides unprivileged filesystem sandboxing via the Landlock LSM (Linux 5.13+).
4//! This is the replacement for chroot which requires root.
5
6use sandbox_core::{Result, SandboxError};
7use std::path::PathBuf;
8
9// Landlock ABI constants
10const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1;
11
12// Access rights for files
13const LANDLOCK_ACCESS_FS_EXECUTE: u64 = 1;
14const LANDLOCK_ACCESS_FS_WRITE_FILE: u64 = 1 << 1;
15const LANDLOCK_ACCESS_FS_READ_FILE: u64 = 1 << 2;
16const LANDLOCK_ACCESS_FS_READ_DIR: u64 = 1 << 3;
17const LANDLOCK_ACCESS_FS_REMOVE_DIR: u64 = 1 << 4;
18const LANDLOCK_ACCESS_FS_REMOVE_FILE: u64 = 1 << 5;
19const LANDLOCK_ACCESS_FS_MAKE_CHAR: u64 = 1 << 6;
20const LANDLOCK_ACCESS_FS_MAKE_DIR: u64 = 1 << 7;
21const LANDLOCK_ACCESS_FS_MAKE_REG: u64 = 1 << 8;
22const LANDLOCK_ACCESS_FS_MAKE_SOCK: u64 = 1 << 9;
23const LANDLOCK_ACCESS_FS_MAKE_FIFO: u64 = 1 << 10;
24const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u64 = 1 << 11;
25const LANDLOCK_ACCESS_FS_MAKE_SYM: u64 = 1 << 12;
26
27const ALL_ACCESS_FS: u64 = LANDLOCK_ACCESS_FS_EXECUTE
28    | LANDLOCK_ACCESS_FS_WRITE_FILE
29    | LANDLOCK_ACCESS_FS_READ_FILE
30    | LANDLOCK_ACCESS_FS_READ_DIR
31    | LANDLOCK_ACCESS_FS_REMOVE_DIR
32    | LANDLOCK_ACCESS_FS_REMOVE_FILE
33    | LANDLOCK_ACCESS_FS_MAKE_CHAR
34    | LANDLOCK_ACCESS_FS_MAKE_DIR
35    | LANDLOCK_ACCESS_FS_MAKE_REG
36    | LANDLOCK_ACCESS_FS_MAKE_SOCK
37    | LANDLOCK_ACCESS_FS_MAKE_FIFO
38    | LANDLOCK_ACCESS_FS_MAKE_BLOCK
39    | LANDLOCK_ACCESS_FS_MAKE_SYM;
40
41const READ_ACCESS: u64 = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
42const WRITE_ACCESS: u64 = LANDLOCK_ACCESS_FS_WRITE_FILE
43    | LANDLOCK_ACCESS_FS_REMOVE_DIR
44    | LANDLOCK_ACCESS_FS_REMOVE_FILE
45    | LANDLOCK_ACCESS_FS_MAKE_CHAR
46    | LANDLOCK_ACCESS_FS_MAKE_DIR
47    | LANDLOCK_ACCESS_FS_MAKE_REG
48    | LANDLOCK_ACCESS_FS_MAKE_SOCK
49    | LANDLOCK_ACCESS_FS_MAKE_FIFO
50    | LANDLOCK_ACCESS_FS_MAKE_BLOCK
51    | LANDLOCK_ACCESS_FS_MAKE_SYM;
52const EXEC_ACCESS: u64 = LANDLOCK_ACCESS_FS_EXECUTE;
53
54/// Landlock filesystem access configuration
55#[derive(Debug, Clone, Default)]
56pub struct LandlockConfig {
57    /// Paths with read access
58    pub read_paths: Vec<PathBuf>,
59    /// Paths with write access
60    pub write_paths: Vec<PathBuf>,
61    /// Paths with execute access
62    pub exec_paths: Vec<PathBuf>,
63}
64
65// Kernel structures for landlock syscalls
66#[repr(C)]
67struct LandlockRulesetAttr {
68    handled_access_fs: u64,
69    handled_access_net: u64,
70}
71
72#[repr(C)]
73struct LandlockPathBeneathAttr {
74    allowed_access: u64,
75    parent_fd: i32,
76}
77
78const LANDLOCK_RULE_PATH_BENEATH: u32 = 1;
79
80impl LandlockConfig {
81    /// Check if landlock is available on this system
82    pub fn is_available() -> bool {
83        let ret = unsafe {
84            libc::syscall(
85                libc::SYS_landlock_create_ruleset,
86                std::ptr::null::<libc::c_void>(),
87                0usize,
88                LANDLOCK_CREATE_RULESET_VERSION,
89            )
90        };
91        if ret >= 0 {
92            unsafe {
93                libc::close(ret as i32);
94            }
95            return true;
96        }
97        let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
98        errno != libc::ENOSYS
99    }
100
101    /// Apply landlock restrictions to the current process.
102    /// WARNING: This is irreversible for the current process.
103    pub fn apply(&self) -> Result<()> {
104        if !Self::is_available() {
105            return Err(SandboxError::FeatureNotAvailable(
106                "Landlock is not available on this kernel (requires Linux 5.13+)".to_string(),
107            ));
108        }
109
110        // Create ruleset
111        let attr = LandlockRulesetAttr {
112            handled_access_fs: ALL_ACCESS_FS,
113            handled_access_net: 0,
114        };
115
116        let ruleset_fd = unsafe {
117            libc::syscall(
118                libc::SYS_landlock_create_ruleset,
119                &attr as *const LandlockRulesetAttr,
120                std::mem::size_of::<LandlockRulesetAttr>(),
121                0u32,
122            )
123        };
124
125        if ruleset_fd < 0 {
126            return Err(SandboxError::Landlock(format!(
127                "landlock_create_ruleset failed: {}",
128                std::io::Error::last_os_error()
129            )));
130        }
131
132        let ruleset_fd = ruleset_fd as i32;
133
134        // Add rules for each path
135        let result = self.add_all_rules(ruleset_fd);
136
137        if result.is_err() {
138            unsafe {
139                libc::close(ruleset_fd);
140            }
141            return result;
142        }
143
144        // Enforce ruleset
145        unsafe {
146            if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0 {
147                libc::close(ruleset_fd);
148                return Err(SandboxError::Landlock(format!(
149                    "PR_SET_NO_NEW_PRIVS failed: {}",
150                    std::io::Error::last_os_error()
151                )));
152            }
153
154            let ret = libc::syscall(libc::SYS_landlock_restrict_self, ruleset_fd, 0u32);
155
156            libc::close(ruleset_fd);
157
158            if ret < 0 {
159                return Err(SandboxError::Landlock(format!(
160                    "landlock_restrict_self failed: {}",
161                    std::io::Error::last_os_error()
162                )));
163            }
164        }
165
166        Ok(())
167    }
168
169    fn add_all_rules(&self, ruleset_fd: i32) -> Result<()> {
170        for path in &self.read_paths {
171            self.add_path_rule(ruleset_fd, path, READ_ACCESS)?;
172        }
173        for path in &self.write_paths {
174            self.add_path_rule(ruleset_fd, path, READ_ACCESS | WRITE_ACCESS)?;
175        }
176        for path in &self.exec_paths {
177            self.add_path_rule(ruleset_fd, path, READ_ACCESS | EXEC_ACCESS)?;
178        }
179        Ok(())
180    }
181
182    fn add_path_rule(&self, ruleset_fd: i32, path: &PathBuf, access: u64) -> Result<()> {
183        use std::os::unix::io::IntoRawFd;
184
185        let file = std::fs::File::open(path).map_err(|e| {
186            SandboxError::Landlock(format!(
187                "Failed to open path for landlock rule {}: {}",
188                path.display(),
189                e
190            ))
191        })?;
192
193        let fd = file.into_raw_fd();
194
195        let attr = LandlockPathBeneathAttr {
196            allowed_access: access,
197            parent_fd: fd,
198        };
199
200        let ret = unsafe {
201            libc::syscall(
202                libc::SYS_landlock_add_rule,
203                ruleset_fd,
204                LANDLOCK_RULE_PATH_BENEATH,
205                &attr as *const LandlockPathBeneathAttr,
206                0u32,
207            )
208        };
209
210        unsafe {
211            libc::close(fd);
212        }
213
214        if ret < 0 {
215            return Err(SandboxError::Landlock(format!(
216                "landlock_add_rule failed for {}: {}",
217                path.display(),
218                std::io::Error::last_os_error()
219            )));
220        }
221
222        Ok(())
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_landlock_config_default() {
232        let config = LandlockConfig::default();
233        assert!(config.read_paths.is_empty());
234        assert!(config.write_paths.is_empty());
235        assert!(config.exec_paths.is_empty());
236    }
237
238    #[test]
239    fn test_landlock_availability_check() {
240        // Just check it doesn't panic
241        let _ = LandlockConfig::is_available();
242    }
243}