lunatic_runtime/
config.rs

1use std::{
2    fmt::Debug,
3    path::{Component, Path, PathBuf},
4};
5
6use lunatic_process::config::ProcessConfig;
7use lunatic_process_api::ProcessConfigCtx;
8use lunatic_wasi_api::LunaticWasiConfigCtx;
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Serialize, Deserialize)]
12pub struct DefaultProcessConfig {
13    // Maximum amount of memory that can be used by processes in bytes
14    max_memory: usize,
15    // Maximum amount of compute expressed in units of 100k instructions.
16    max_fuel: Option<u64>,
17    // Can this process compile new WebAssembly modules
18    can_compile_modules: bool,
19    // Can this process create new configurations
20    can_create_configs: bool,
21    // Can this process spawn sub-processes
22    can_spawn_processes: bool,
23    // WASI configs
24    preopened_dirs: Vec<String>,
25    command_line_arguments: Vec<String>,
26    environment_variables: Vec<(String, String)>,
27}
28
29impl Debug for DefaultProcessConfig {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
31        f.debug_struct("EnvConfig")
32            .field("max_memory", &self.max_memory)
33            .field("max_fuel", &self.max_fuel)
34            .field("preopened_dirs", &self.preopened_dirs)
35            .field("args", &self.command_line_arguments)
36            .field("envs", &self.environment_variables)
37            .finish()
38    }
39}
40
41impl ProcessConfig for DefaultProcessConfig {
42    fn set_max_fuel(&mut self, max_fuel: Option<u64>) {
43        self.max_fuel = max_fuel;
44    }
45
46    fn get_max_fuel(&self) -> Option<u64> {
47        self.max_fuel
48    }
49
50    fn set_max_memory(&mut self, max_memory: usize) {
51        self.max_memory = max_memory
52    }
53
54    fn get_max_memory(&self) -> usize {
55        self.max_memory
56    }
57}
58
59impl LunaticWasiConfigCtx for DefaultProcessConfig {
60    fn add_environment_variable(&mut self, key: String, value: String) {
61        self.environment_variables.push((key, value));
62    }
63
64    fn add_command_line_argument(&mut self, argument: String) {
65        self.command_line_arguments.push(argument);
66    }
67
68    fn preopen_dir(&mut self, dir: String) {
69        self.preopened_dirs.push(dir);
70    }
71}
72
73impl DefaultProcessConfig {
74    pub fn preopened_dirs(&self) -> &[String] {
75        &self.preopened_dirs
76    }
77
78    /// Grant access to the given directory with this config.
79    pub fn preopen_dir<S: Into<String>>(&mut self, dir: S) {
80        self.preopened_dirs.push(dir.into())
81    }
82
83    pub fn set_command_line_arguments(&mut self, args: Vec<String>) {
84        self.command_line_arguments = args;
85    }
86
87    pub fn command_line_arguments(&self) -> &Vec<String> {
88        &self.command_line_arguments
89    }
90
91    pub fn set_environment_variables(&mut self, envs: Vec<(String, String)>) {
92        self.environment_variables = envs;
93    }
94
95    pub fn environment_variables(&self) -> &Vec<(String, String)> {
96        &self.environment_variables
97    }
98}
99
100impl ProcessConfigCtx for DefaultProcessConfig {
101    fn can_compile_modules(&self) -> bool {
102        self.can_compile_modules
103    }
104
105    fn set_can_compile_modules(&mut self, can: bool) {
106        self.can_compile_modules = can
107    }
108
109    fn can_create_configs(&self) -> bool {
110        self.can_create_configs
111    }
112
113    fn set_can_create_configs(&mut self, can: bool) {
114        self.can_create_configs = can
115    }
116
117    fn can_spawn_processes(&self) -> bool {
118        self.can_spawn_processes
119    }
120
121    fn set_can_spawn_processes(&mut self, can: bool) {
122        self.can_spawn_processes = can
123    }
124
125    fn can_access_fs_location(&self, path: &std::path::Path) -> Result<(), String> {
126        let (file_path, parent_dir) = match strip_file(path) {
127            Ok(p) => p,
128            Err(e) => {
129                return Err(e.to_string());
130            }
131        };
132        let has_access = self
133            .preopened_dirs()
134            .iter()
135            .filter_map(|dir| match get_absolute_path(Path::new(dir)) {
136                Ok(d) => Some(d),
137                _ => None,
138            })
139            .any(|dir| dir.exists() && path_is_ancestor(&dir, &parent_dir));
140
141        match has_access {
142            true => Ok(()),
143            false => Err(format!("Permission to '{file_path:?}' denied")),
144        }
145    }
146}
147
148fn path_is_ancestor(ancestor: &Path, descendant: &Path) -> bool {
149    let ancestor_path = Path::new(ancestor);
150    let descendant_path = Path::new(descendant);
151
152    if !ancestor_path.is_dir() {
153        return false;
154    }
155
156    // If the ancestor path is root, return true
157    if ancestor_path.as_os_str() == Path::new("/").as_os_str() {
158        return true;
159    }
160
161    let descendant_components = descendant_path.ancestors();
162
163    // Check if each component of the descendant path starts with the ancestor path
164    for component in descendant_components {
165        if component.as_os_str() == ancestor_path.as_os_str() {
166            return true;
167        }
168    }
169
170    false
171}
172
173// returns a tuple of paths, where the first is the full resolved canonicalized path
174// and the second one is stripped of the file extension, pointing to the parent directory
175// of the file that a program is trying to access
176fn strip_file(path: &Path) -> std::io::Result<(PathBuf, PathBuf)> {
177    let absolute_path = get_absolute_path(path)?;
178    if absolute_path.is_file() {
179        return Ok((absolute_path.clone(), absolute_path.join("..")));
180    }
181    Ok((absolute_path.clone(), absolute_path))
182}
183
184fn get_absolute_path(path: &std::path::Path) -> std::io::Result<PathBuf> {
185    let path = if path.is_relative() {
186        Path::join(std::env::current_dir().unwrap().as_path(), path)
187    } else {
188        path.to_path_buf()
189    };
190    Ok(normalize_path(&path))
191}
192
193fn normalize_path(path: &Path) -> PathBuf {
194    let mut components = path.components().peekable();
195    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
196        components.next();
197        PathBuf::from(c.as_os_str())
198    } else {
199        PathBuf::new()
200    };
201
202    for component in components {
203        match component {
204            Component::Prefix(..) => unreachable!(),
205            Component::RootDir => {
206                ret.push(component.as_os_str());
207            }
208            Component::CurDir => {}
209            Component::ParentDir => {
210                ret.pop();
211            }
212            Component::Normal(c) => {
213                ret.push(c);
214            }
215        }
216    }
217    ret
218}
219
220#[cfg(test)]
221mod tests {
222    use std::path::Path;
223
224    use crate::config::{get_absolute_path, path_is_ancestor};
225
226    use super::normalize_path;
227
228    #[test]
229    fn test_accessible_paths() {
230        let crates = get_absolute_path(Path::new("crates")).unwrap();
231        let sqlite = get_absolute_path(Path::new("crates/lunatic-sqlite-api")).unwrap();
232        let src = get_absolute_path(Path::new("crates/lunatic-sqlite-api/src")).unwrap();
233        let guest_api =
234            get_absolute_path(Path::new("crates/lunatic-sqlite-api/src/guest_api")).unwrap();
235        // checks
236        assert!(path_is_ancestor(&crates, &guest_api));
237        assert!(path_is_ancestor(&sqlite, &guest_api));
238        assert!(path_is_ancestor(&src, &guest_api));
239        assert!(path_is_ancestor(&guest_api, &guest_api));
240    }
241
242    #[test]
243    fn test_forbidden_paths() {
244        let crates = get_absolute_path(Path::new("crates")).unwrap();
245        let sqlite = get_absolute_path(Path::new("crates/lunatic-sqlite-api")).unwrap();
246        let src = get_absolute_path(Path::new("crates/lunatic-sqlite-api/src")).unwrap();
247        let guest_api =
248            get_absolute_path(Path::new("crates/lunatic-sqlite-api/src/guest_api")).unwrap();
249        // checks that there's no access to any ancestor paths
250        assert!(!path_is_ancestor(&guest_api, &crates));
251        assert!(!path_is_ancestor(&guest_api, &sqlite));
252        assert!(!path_is_ancestor(&guest_api, &src));
253    }
254
255    #[test]
256    fn test_forbidden_absolute_paths() {
257        let src = get_absolute_path(Path::new("crates/lunatic-sqlite-api/src")).unwrap();
258        // checks that there's no access to any ancestor paths
259        assert!(!path_is_ancestor(&src, Path::new("/")));
260        assert!(!path_is_ancestor(&src, Path::new("/etc/passwd")));
261    }
262
263    #[test]
264    fn normalized_paths() {
265        let crates = get_absolute_path(Path::new("crates")).unwrap();
266        let src = get_absolute_path(Path::new("crates/lunatic-sqlite-api/src")).unwrap();
267        let sneaky_src =
268            get_absolute_path(Path::new("crates/lunatic-sqlite-api/src/../src/.")).unwrap();
269        let sneaky_path =
270            get_absolute_path(Path::new("crates/lunatic-sqlite-api/src/../src/../../")).unwrap();
271        assert_eq!(src, normalize_path(&sneaky_src));
272        assert_eq!(crates, normalize_path(&sneaky_path));
273    }
274}
275
276impl Default for DefaultProcessConfig {
277    fn default() -> Self {
278        Self {
279            max_memory: u32::MAX as usize, // = 4 GB
280            max_fuel: None,
281            can_compile_modules: false,
282            can_create_configs: false,
283            can_spawn_processes: false,
284            preopened_dirs: vec![],
285            command_line_arguments: vec![],
286            environment_variables: vec![],
287        }
288    }
289}