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, AccessNet, PathBeneath, PathFd, Ruleset, RulesetAttr,
8 RulesetCreatedAttr, RulesetStatus,
9};
10use std::io::{Error, Result};
11use std::path::Path;
12
13use crate::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 log::debug!(
38 "Landlock: skipping nonexistent path {}: {e}",
39 path.display()
40 );
41 Ok(ruleset)
42 }
43 }
44}
45
46/// Apply a Landlock sandbox to the current process.
47/// Must be called after fork(), before exec().
48pub fn apply_sandbox(config: &SandboxConfig) -> Result<()> {
49 let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
50 if ret != 0 {
51 return Err(Error::last_os_error());
52 }
53
54 let ruleset_base = Ruleset::default()
55 .handle_access(AccessFs::from_all(TARGET_ABI))
56 .map_err(|e| Error::other(format!("landlock ruleset create: {e}")))?;
57
58 let ruleset_with_net = if !config.allow_network {
59 ruleset_base
60 .handle_access(AccessNet::from_all(TARGET_ABI))
61 .map_err(|e| {
62 Error::other(format!(
63 "landlock network restriction not supported (requires kernel 6.4+): {e}"
64 ))
65 })?
66 } else {
67 ruleset_base
68 };
69
70 let mut ruleset = ruleset_with_net
71 .create()
72 .map_err(|e| Error::other(format!("landlock ruleset init: {e}")))?;
73
74 for path in &config.system_paths.executable {
75 ruleset = add_path_rule(ruleset, path, fs_read_exec())
76 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
77 }
78
79 for path in &config.system_paths.read_only {
80 ruleset = add_path_rule(ruleset, path, fs_read())
81 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
82 }
83
84 for path in &config.system_paths.read_write {
85 ruleset = add_path_rule(ruleset, path, fs_all())
86 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
87 }
88
89 ruleset = add_path_rule(ruleset, &config.project_dir, fs_all())
90 .map_err(|e| Error::other(format!("landlock project rule: {e}")))?;
91
92 for path in &config.additional_executable_paths {
93 ruleset = add_path_rule(ruleset, path, fs_read_exec())
94 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
95 }
96 for path in &config.additional_read_only_paths {
97 ruleset = add_path_rule(ruleset, path, fs_read())
98 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
99 }
100 for path in &config.additional_read_write_paths {
101 ruleset = add_path_rule(ruleset, path, fs_all())
102 .map_err(|e| Error::other(format!("landlock rule: {e}")))?;
103 }
104
105 if let Ok(home) = std::env::var("HOME") {
106 let home = Path::new(&home);
107 for dotfile in SandboxConfig::READ_ONLY_DOTFILES {
108 let path = home.join(dotfile);
109 if path.exists() {
110 ruleset = add_path_rule(ruleset, &path, fs_read())
111 .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
112 }
113 }
114 for dotfile in SandboxConfig::READ_WRITE_DOTFILES {
115 let path = home.join(dotfile);
116 if path.exists() {
117 ruleset = add_path_rule(ruleset, &path, fs_all())
118 .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
119 }
120 }
121 let config_dir = home.join(".config");
122 if config_dir.exists() {
123 ruleset = add_path_rule(ruleset, &config_dir, fs_read())
124 .map_err(|e| Error::other(format!("landlock .config rule: {e}")))?;
125 }
126 let proc_self = Path::new("/proc/self");
127 if proc_self.exists() {
128 ruleset = add_path_rule(ruleset, proc_self, fs_all())
129 .map_err(|e| Error::other(format!("landlock /proc/self rule: {e}")))?;
130 }
131 }
132
133 let status = ruleset
134 .restrict_self()
135 .map_err(|e| Error::other(format!("landlock restrict_self: {e}")))?;
136
137 match status.ruleset {
138 RulesetStatus::FullyEnforced => {
139 log::info!("Landlock sandbox fully enforced");
140 }
141 RulesetStatus::PartiallyEnforced => {
142 return Err(Error::other(
143 "Landlock sandbox only partially enforced on this kernel. \
144 The sandbox cannot guarantee the requested restrictions. \
145 Upgrade to kernel 6.4+ for full enforcement, or disable sandboxing.",
146 ));
147 }
148 RulesetStatus::NotEnforced => {
149 return Err(Error::other(
150 "Landlock is not supported on this kernel (requires 5.13+). \
151 The terminal cannot be sandboxed. \
152 Upgrade your kernel or disable sandboxing.",
153 ));
154 }
155 }
156
157 Ok(())
158}