1//! Linux Landlock sandbox implementation.
2//!
3//! Uses the Landlock LSM to restrict filesystem access for the current process.
4//! Must be called after fork(), before exec().
5
6use landlock::{
7 ABI, Access, AccessFs, PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr,
8 RulesetStatus,
9};
10use std::io::{Error, Result};
11use std::path::Path;
12
13use crate::terminal_settings::SandboxConfig;
14
15const TARGET_ABI: ABI = ABI::V5;
16
17fn fs_read() -> AccessFs {
18 AccessFs::ReadFile | AccessFs::ReadDir
19}
20
21fn fs_read_exec() -> AccessFs {
22 fs_read() | AccessFs::Execute
23}
24
25fn fs_all() -> AccessFs {
26 AccessFs::from_all(TARGET_ABI)
27}
28
29fn add_path_rule(
30 ruleset: landlock::RulesetCreated,
31 path: &Path,
32 access: AccessFs,
33) -> std::result::Result<landlock::RulesetCreated, landlock::RulesetError> {
34 match PathFd::new(path) {
35 Ok(fd) => ruleset.add_rule(PathBeneath::new(fd, access)),
36 Err(e) => {
37 // Path doesn't exist — skip it (e.g., /opt/homebrew on non-Homebrew systems)
38 log::debug!(
39 "Landlock: skipping nonexistent path {}: {e}",
40 path.display()
41 );
42 Ok(ruleset)
43 }
44 }
45}
46
47/// Apply a Landlock sandbox to the current process.
48/// Must be called after fork(), before exec().
49pub fn apply_sandbox(config: &SandboxConfig) -> Result<()> {
50 // PR_SET_NO_NEW_PRIVS is required before landlock_restrict_self.
51 // It prevents the process from gaining privileges via setuid binaries.
52 let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
53 if ret != 0 {
54 return Err(Error::last_os_error());
55 }
56
57 let mut ruleset = Ruleset::default()
58 .handle_access(AccessFs::from_all(TARGET_ABI))
59 .map_err(|e| Error::other(format!("landlock ruleset create: {e}")))?
60 .create()
61 .map_err(|e| Error::other(format!("landlock ruleset init: {e}")))?;
62
63 // System executable paths (read + execute)
64 for path in &config.system_paths.executable {
65 ruleset = add_path_rule(ruleset, path, fs_read_exec())
66 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
67 }
68
69 // System read-only paths
70 for path in &config.system_paths.read_only {
71 ruleset = add_path_rule(ruleset, path, fs_read())
72 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
73 }
74
75 // System read+write paths
76 for path in &config.system_paths.read_write {
77 ruleset = add_path_rule(ruleset, path, fs_all())
78 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
79 }
80
81 // Project directory: full access
82 ruleset = add_path_rule(ruleset, &config.project_dir, fs_all())
83 .map_err(|e| Error::other(format!("landlock project rule: {e}")))?;
84
85 // User-configured paths
86 for path in &config.additional_executable_paths {
87 ruleset = add_path_rule(ruleset, path, fs_read_exec())
88 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
89 }
90 for path in &config.additional_read_only_paths {
91 ruleset = add_path_rule(ruleset, path, fs_read())
92 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
93 }
94 for path in &config.additional_read_write_paths {
95 ruleset = add_path_rule(ruleset, path, fs_all())
96 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
97 }
98
99 // Shell config dotfiles: read-only
100 if let Ok(home) = std::env::var("HOME") {
101 let home = Path::new(&home);
102 for dotfile in &[
103 ".bashrc",
104 ".bash_profile",
105 ".bash_login",
106 ".profile",
107 ".zshrc",
108 ".zshenv",
109 ".zprofile",
110 ".zlogin",
111 ".zlogout",
112 ".inputrc",
113 ".terminfo",
114 ".gitconfig",
115 ] {
116 let path = home.join(dotfile);
117 if path.exists() {
118 ruleset = add_path_rule(ruleset, &path, fs_read())
119 .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
120 }
121 }
122 let config_dir = home.join(".config");
123 if config_dir.exists() {
124 ruleset = add_path_rule(ruleset, &config_dir, fs_read())
125 .map_err(|e| Error::other(format!("landlock .config rule: {e}")))?;
126 }
127 // /proc/self for bash process substitution
128 let proc_self = Path::new("/proc/self");
129 if proc_self.exists() {
130 ruleset = add_path_rule(ruleset, proc_self, fs_read())
131 .map_err(|e| Error::other(format!("landlock /proc/self rule: {e}")))?;
132 }
133 }
134
135 let status = ruleset
136 .restrict_self()
137 .map_err(|e| Error::other(format!("landlock restrict_self: {e}")))?;
138
139 match status.ruleset {
140 RulesetStatus::FullyEnforced => {
141 log::info!("Landlock sandbox fully enforced");
142 }
143 RulesetStatus::PartiallyEnforced => {
144 log::warn!("Landlock sandbox partially enforced (older kernel ABI)");
145 }
146 RulesetStatus::NotEnforced => {
147 log::warn!("Landlock not supported on this kernel; running unsandboxed");
148 }
149 }
150
151 Ok(())
152}