Skip to main content

evalbox_sys/
landlock.rs

1//! Landlock LSM for unprivileged filesystem and network access control.
2//!
3//! Landlock is a Linux Security Module (LSM) that allows unprivileged processes
4//! to restrict their own access to the filesystem and network. Unlike traditional
5//! DAC/MAC, Landlock can be used without root privileges.
6//!
7//! ## ABI Versions
8//!
9//! | ABI | Kernel | Features |
10//! |-----|--------|----------|
11//! | 1 | 5.13 | Basic filesystem access |
12//! | 2 | 5.19 | `REFER` (cross-directory rename/link) |
13//! | 3 | 6.2 | `TRUNCATE` (file truncation) |
14//! | 4 | 6.7 | `IOCTL_DEV`, TCP network access |
15//! | 5 | 6.12 | `SCOPE_SIGNAL`, `SCOPE_ABSTRACT_UNIX_SOCKET` |
16//!
17//! ## Usage
18//!
19//! ```ignore
20//! let attr = LandlockRulesetAttr {
21//!     handled_access_fs: fs_access_for_abi(abi),
22//!     handled_access_net: net_access_for_abi(abi),
23//! };
24//! let ruleset_fd = landlock_create_ruleset(&attr)?;
25//!
26//! // Add rules for allowed paths
27//! let rule = LandlockPathBeneathAttr { allowed_access, parent_fd };
28//! landlock_add_rule_path(&ruleset_fd, &rule)?;
29//!
30//! // Restrict self - no going back after this!
31//! landlock_restrict_self(&ruleset_fd)?;
32//! ```
33//!
34//! ## Important
35//!
36//! - Once `landlock_restrict_self` is called, it cannot be undone
37//! - Access not explicitly allowed is denied
38//! - Network blocking requires ABI 4+ (kernel 6.7+)
39
40use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
41
42use rustix::io::Errno;
43
44use crate::last_errno;
45
46const SYS_LANDLOCK_CREATE_RULESET: i64 = 444;
47const SYS_LANDLOCK_ADD_RULE: i64 = 445;
48const SYS_LANDLOCK_RESTRICT_SELF: i64 = 446;
49
50const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1 << 0;
51const LANDLOCK_RULE_PATH_BENEATH: u32 = 1;
52
53// ABI v1
54pub const LANDLOCK_ACCESS_FS_EXECUTE: u64 = 1 << 0;
55pub const LANDLOCK_ACCESS_FS_WRITE_FILE: u64 = 1 << 1;
56pub const LANDLOCK_ACCESS_FS_READ_FILE: u64 = 1 << 2;
57pub const LANDLOCK_ACCESS_FS_READ_DIR: u64 = 1 << 3;
58pub const LANDLOCK_ACCESS_FS_REMOVE_DIR: u64 = 1 << 4;
59pub const LANDLOCK_ACCESS_FS_REMOVE_FILE: u64 = 1 << 5;
60pub const LANDLOCK_ACCESS_FS_MAKE_CHAR: u64 = 1 << 6;
61pub const LANDLOCK_ACCESS_FS_MAKE_DIR: u64 = 1 << 7;
62pub const LANDLOCK_ACCESS_FS_MAKE_REG: u64 = 1 << 8;
63pub const LANDLOCK_ACCESS_FS_MAKE_SOCK: u64 = 1 << 9;
64pub const LANDLOCK_ACCESS_FS_MAKE_FIFO: u64 = 1 << 10;
65pub const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u64 = 1 << 11;
66pub const LANDLOCK_ACCESS_FS_MAKE_SYM: u64 = 1 << 12;
67
68// ABI v2
69pub const LANDLOCK_ACCESS_FS_REFER: u64 = 1 << 13;
70
71// ABI v3
72pub const LANDLOCK_ACCESS_FS_TRUNCATE: u64 = 1 << 14;
73
74// ABI v4
75pub const LANDLOCK_ACCESS_FS_IOCTL_DEV: u64 = 1 << 15;
76pub const LANDLOCK_ACCESS_NET_BIND_TCP: u64 = 1 << 0;
77pub const LANDLOCK_ACCESS_NET_CONNECT_TCP: u64 = 1 << 1;
78
79// ABI v5 - Scoped restrictions
80/// Block abstract unix socket connections outside the sandbox.
81pub const LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: u64 = 1 << 0;
82/// Block signals to processes outside the sandbox.
83pub const LANDLOCK_SCOPE_SIGNAL: u64 = 1 << 1;
84
85#[repr(C)]
86#[derive(Debug, Default)]
87pub struct LandlockRulesetAttr {
88    pub handled_access_fs: u64,
89    pub handled_access_net: u64,
90    /// ABI 5+: Scoped restrictions (signal and abstract unix socket isolation).
91    pub scoped: u64,
92}
93
94#[repr(C)]
95#[derive(Debug)]
96pub struct LandlockPathBeneathAttr {
97    pub allowed_access: u64,
98    pub parent_fd: RawFd,
99}
100
101/// Returns the Landlock ABI version supported by the kernel.
102///
103/// # Errors
104///
105/// Returns `Errno` if the kernel doesn't support Landlock.
106pub fn landlock_abi_version() -> Result<u32, Errno> {
107    // SAFETY: Passing null with size 0 and VERSION flag queries the ABI version.
108    let ret = unsafe {
109        libc::syscall(
110            SYS_LANDLOCK_CREATE_RULESET,
111            std::ptr::null::<LandlockRulesetAttr>(),
112            0usize,
113            LANDLOCK_CREATE_RULESET_VERSION,
114        )
115    };
116    if ret < 0 {
117        Err(last_errno())
118    } else {
119        Ok(ret as u32)
120    }
121}
122
123/// Creates a new Landlock ruleset.
124///
125/// # Errors
126///
127/// Returns `Errno` if the ruleset creation fails.
128pub fn landlock_create_ruleset(attr: &LandlockRulesetAttr) -> Result<OwnedFd, Errno> {
129    // SAFETY: attr points to valid memory with correct size.
130    let ret = unsafe {
131        libc::syscall(
132            SYS_LANDLOCK_CREATE_RULESET,
133            attr as *const LandlockRulesetAttr,
134            size_of::<LandlockRulesetAttr>(),
135            0u32,
136        )
137    };
138    if ret < 0 {
139        Err(last_errno())
140    } else {
141        // SAFETY: On success, ret is a valid owned file descriptor.
142        Ok(unsafe { OwnedFd::from_raw_fd(ret as RawFd) })
143    }
144}
145
146/// Adds a path-based rule to a Landlock ruleset.
147///
148/// # Errors
149///
150/// Returns `Errno` if adding the rule fails.
151pub fn landlock_add_rule_path(
152    ruleset_fd: &OwnedFd,
153    attr: &LandlockPathBeneathAttr,
154) -> Result<(), Errno> {
155    // SAFETY: ruleset_fd is valid, attr points to valid memory.
156    let ret = unsafe {
157        libc::syscall(
158            SYS_LANDLOCK_ADD_RULE,
159            ruleset_fd.as_raw_fd(),
160            LANDLOCK_RULE_PATH_BENEATH,
161            attr as *const LandlockPathBeneathAttr,
162            0u32,
163        )
164    };
165    if ret < 0 { Err(last_errno()) } else { Ok(()) }
166}
167
168/// Restricts the calling thread to the given Landlock ruleset.
169///
170/// # Errors
171///
172/// Returns `Errno` if the restriction fails.
173pub fn landlock_restrict_self(ruleset_fd: &OwnedFd) -> Result<(), Errno> {
174    // SAFETY: ruleset_fd is a valid file descriptor.
175    let ret = unsafe { libc::syscall(SYS_LANDLOCK_RESTRICT_SELF, ruleset_fd.as_raw_fd(), 0u32) };
176    if ret < 0 { Err(last_errno()) } else { Ok(()) }
177}
178
179/// Returns the filesystem access flags for the given ABI version.
180pub fn fs_access_for_abi(abi: u32) -> u64 {
181    let mut access = LANDLOCK_ACCESS_FS_EXECUTE
182        | LANDLOCK_ACCESS_FS_WRITE_FILE
183        | LANDLOCK_ACCESS_FS_READ_FILE
184        | LANDLOCK_ACCESS_FS_READ_DIR
185        | LANDLOCK_ACCESS_FS_REMOVE_DIR
186        | LANDLOCK_ACCESS_FS_REMOVE_FILE
187        | LANDLOCK_ACCESS_FS_MAKE_CHAR
188        | LANDLOCK_ACCESS_FS_MAKE_DIR
189        | LANDLOCK_ACCESS_FS_MAKE_REG
190        | LANDLOCK_ACCESS_FS_MAKE_SOCK
191        | LANDLOCK_ACCESS_FS_MAKE_FIFO
192        | LANDLOCK_ACCESS_FS_MAKE_BLOCK
193        | LANDLOCK_ACCESS_FS_MAKE_SYM;
194
195    if abi >= 2 {
196        access |= LANDLOCK_ACCESS_FS_REFER;
197    }
198    if abi >= 3 {
199        access |= LANDLOCK_ACCESS_FS_TRUNCATE;
200    }
201    if abi >= 4 {
202        access |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
203    }
204
205    access
206}
207
208/// Returns the network access flags for the given ABI version.
209pub fn net_access_for_abi(abi: u32) -> u64 {
210    if abi >= 4 {
211        LANDLOCK_ACCESS_NET_BIND_TCP | LANDLOCK_ACCESS_NET_CONNECT_TCP
212    } else {
213        0
214    }
215}
216
217/// Returns the scoped restriction flags for the given ABI version.
218///
219/// ABI 5+ supports signal isolation and abstract unix socket isolation,
220/// replacing the need for PID and IPC namespaces.
221pub fn scope_for_abi(abi: u32) -> u64 {
222    if abi >= 5 {
223        LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL
224    } else {
225        0
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn abi_version() {
235        if let Ok(v) = landlock_abi_version() {
236            assert!(v >= 1);
237        }
238    }
239
240    #[test]
241    fn fs_access_increases_with_abi() {
242        assert!(fs_access_for_abi(2) > fs_access_for_abi(1));
243        assert!(fs_access_for_abi(3) > fs_access_for_abi(2));
244        assert!(fs_access_for_abi(4) > fs_access_for_abi(3));
245    }
246}