sandbox_runtime/sandbox/linux/
filesystem.rs1use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6use crate::config::{FilesystemConfig, RipgrepConfig, DANGEROUS_DIRECTORIES, DANGEROUS_FILES};
7use crate::error::SandboxError;
8use crate::utils::{
9 contains_glob_chars, find_dangerous_files, is_symlink_outside_boundary,
10 normalize_path_for_sandbox, remove_trailing_glob_suffix,
11};
12
13#[derive(Debug, Clone)]
15pub struct BindMount {
16 pub source: PathBuf,
18 pub target: PathBuf,
20 pub readonly: bool,
22 pub dev_null: bool,
24}
25
26impl BindMount {
27 pub fn readonly(path: impl Into<PathBuf>) -> Self {
29 let path = path.into();
30 Self {
31 source: path.clone(),
32 target: path,
33 readonly: true,
34 dev_null: false,
35 }
36 }
37
38 pub fn writable(path: impl Into<PathBuf>) -> Self {
40 let path = path.into();
41 Self {
42 source: path.clone(),
43 target: path,
44 readonly: false,
45 dev_null: false,
46 }
47 }
48
49 pub fn block(path: impl Into<PathBuf>) -> Self {
51 let path = path.into();
52 Self {
53 source: PathBuf::from("/dev/null"),
54 target: path,
55 readonly: true,
56 dev_null: true,
57 }
58 }
59
60 pub fn to_bwrap_args(&self) -> Vec<String> {
62 if self.dev_null {
63 vec![
64 "--ro-bind".to_string(),
65 "/dev/null".to_string(),
66 self.target.display().to_string(),
67 ]
68 } else if self.readonly {
69 vec![
70 "--ro-bind".to_string(),
71 self.source.display().to_string(),
72 self.target.display().to_string(),
73 ]
74 } else {
75 vec![
76 "--bind".to_string(),
77 self.source.display().to_string(),
78 self.target.display().to_string(),
79 ]
80 }
81 }
82}
83
84pub fn generate_bind_mounts(
86 config: &FilesystemConfig,
87 cwd: &Path,
88 ripgrep_config: Option<&RipgrepConfig>,
89 max_depth: Option<u32>,
90) -> Result<(Vec<BindMount>, Vec<String>), SandboxError> {
91 let mut mounts = Vec::new();
92 let mut warnings = Vec::new();
93
94 let mut writable_paths: HashSet<PathBuf> = HashSet::new();
96 for path in &config.allow_write {
97 if contains_glob_chars(path) {
99 warnings.push(format!(
100 "Glob pattern '{}' is not supported on Linux; ignoring",
101 path
102 ));
103 continue;
104 }
105
106 let normalized = normalize_path_for_sandbox(path);
107 let path = PathBuf::from(&normalized);
108
109 if path.exists() {
110 writable_paths.insert(path);
111 } else {
112 warnings.push(format!("Write path '{}' does not exist", normalized));
113 }
114 }
115
116 let mut deny_paths: HashSet<PathBuf> = HashSet::new();
118 for path in &config.deny_write {
119 if contains_glob_chars(path) {
120 warnings.push(format!(
121 "Glob pattern '{}' is not supported on Linux; ignoring",
122 path
123 ));
124 continue;
125 }
126
127 let normalized = normalize_path_for_sandbox(path);
128 deny_paths.insert(PathBuf::from(&normalized));
129 }
130
131 let dangerous_files = find_dangerous_files(cwd, ripgrep_config, max_depth).unwrap_or_default();
133 for file in dangerous_files {
134 deny_paths.insert(PathBuf::from(file));
135 }
136
137 for dir in DANGEROUS_DIRECTORIES {
139 let path = cwd.join(dir);
141 if path.exists() {
142 deny_paths.insert(path);
143 }
144
145 if let Some(home) = dirs::home_dir() {
147 let path = home.join(dir);
148 if path.exists() {
149 deny_paths.insert(path);
150 }
151 }
152 }
153
154 for file in DANGEROUS_FILES {
155 if *file == ".gitconfig" && config.allow_git_config.unwrap_or(false) {
157 continue;
158 }
159
160 if let Some(home) = dirs::home_dir() {
161 let path = home.join(file);
162 if path.exists() {
163 deny_paths.insert(path);
164 }
165 }
166 }
167
168 for path in &writable_paths {
171 if let Ok(resolved) = std::fs::canonicalize(path) {
173 if is_symlink_outside_boundary(path, &resolved) {
174 mounts.push(BindMount::block(path.clone()));
175 continue;
176 }
177 }
178
179 mounts.push(BindMount::writable(path.clone()));
180 }
181
182 for path in &deny_paths {
184 if path.exists() {
185 mounts.push(BindMount::readonly(path.clone()));
186 } else {
187 mounts.push(BindMount::block(path.clone()));
189 }
190 }
191
192 Ok((mounts, warnings))
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_bind_mount_to_bwrap_args() {
201 let mount = BindMount::readonly("/path/to/file");
202 let args = mount.to_bwrap_args();
203 assert_eq!(args, vec!["--ro-bind", "/path/to/file", "/path/to/file"]);
204
205 let mount = BindMount::writable("/path/to/dir");
206 let args = mount.to_bwrap_args();
207 assert_eq!(args, vec!["--bind", "/path/to/dir", "/path/to/dir"]);
208
209 let mount = BindMount::block("/path/to/blocked");
210 let args = mount.to_bwrap_args();
211 assert_eq!(args, vec!["--ro-bind", "/dev/null", "/path/to/blocked"]);
212 }
213}