Skip to main content

zerobox_sandboxing/
manager.rs

1#[cfg(target_os = "linux")]
2use crate::bwrap::WSL1_BWRAP_WARNING;
3#[cfg(target_os = "linux")]
4use crate::bwrap::is_wsl1;
5use crate::landlock::ZEROBOX_LINUX_SANDBOX_ARG0;
6use crate::landlock::allow_network_for_proxy;
7use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
8use crate::policy_transforms::effective_permission_profile;
9use crate::policy_transforms::should_require_platform_sandbox;
10use std::collections::HashMap;
11use std::ffi::OsString;
12use std::path::Path;
13use zerobox_network_proxy::NetworkProxy;
14use zerobox_protocol::config_types::WindowsSandboxLevel;
15use zerobox_protocol::models::AdditionalPermissionProfile;
16use zerobox_protocol::models::PermissionProfile;
17use zerobox_protocol::permissions::FileSystemSandboxPolicy;
18use zerobox_protocol::permissions::NetworkSandboxPolicy;
19use zerobox_protocol::protocol::SandboxPolicy;
20use zerobox_utils_absolute_path::AbsolutePathBuf;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum SandboxType {
24    None,
25    MacosSeatbelt,
26    LinuxSeccomp,
27    WindowsRestrictedToken,
28}
29
30impl SandboxType {
31    pub fn as_metric_tag(self) -> &'static str {
32        match self {
33            SandboxType::None => "none",
34            SandboxType::MacosSeatbelt => "seatbelt",
35            SandboxType::LinuxSeccomp => "seccomp",
36            SandboxType::WindowsRestrictedToken => "windows_sandbox",
37        }
38    }
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum SandboxablePreference {
43    Auto,
44    Require,
45    Forbid,
46}
47
48pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
49    if cfg!(target_os = "macos") {
50        Some(SandboxType::MacosSeatbelt)
51    } else if cfg!(target_os = "linux") {
52        Some(SandboxType::LinuxSeccomp)
53    } else if cfg!(target_os = "windows") {
54        if windows_sandbox_enabled {
55            Some(SandboxType::WindowsRestrictedToken)
56        } else {
57            None
58        }
59    } else {
60        None
61    }
62}
63
64#[derive(Debug)]
65pub struct SandboxCommand {
66    pub program: OsString,
67    pub args: Vec<String>,
68    pub cwd: AbsolutePathBuf,
69    pub env: HashMap<String, String>,
70    pub additional_permissions: Option<AdditionalPermissionProfile>,
71}
72
73#[derive(Debug)]
74pub struct SandboxExecRequest {
75    pub command: Vec<String>,
76    pub cwd: AbsolutePathBuf,
77    pub env: HashMap<String, String>,
78    pub network: Option<NetworkProxy>,
79    pub sandbox: SandboxType,
80    pub windows_sandbox_level: WindowsSandboxLevel,
81    pub windows_sandbox_private_desktop: bool,
82    pub permission_profile: PermissionProfile,
83    pub file_system_sandbox_policy: FileSystemSandboxPolicy,
84    pub network_sandbox_policy: NetworkSandboxPolicy,
85    pub arg0: Option<String>,
86}
87
88/// Bundled arguments for sandbox transformation.
89///
90/// This keeps call sites self-documenting when several fields are optional.
91pub struct SandboxTransformRequest<'a> {
92    pub command: SandboxCommand,
93    pub permissions: &'a PermissionProfile,
94    pub sandbox: SandboxType,
95    pub enforce_managed_network: bool,
96    // TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>
97    // to make shared ownership explicit across runtime/sandbox plumbing.
98    pub network: Option<&'a NetworkProxy>,
99    pub sandbox_policy_cwd: &'a Path,
100    pub zerobox_linux_sandbox_exe: Option<&'a Path>,
101    pub use_legacy_landlock: bool,
102    pub windows_sandbox_level: WindowsSandboxLevel,
103    pub windows_sandbox_private_desktop: bool,
104}
105
106#[derive(Debug)]
107pub enum SandboxTransformError {
108    MissingLinuxSandboxExecutable,
109    #[cfg(target_os = "linux")]
110    Wsl1UnsupportedForBubblewrap,
111    #[cfg(not(target_os = "macos"))]
112    SeatbeltUnavailable,
113}
114
115impl std::fmt::Display for SandboxTransformError {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        match self {
118            Self::MissingLinuxSandboxExecutable => {
119                write!(f, "missing zerobox-linux-sandbox executable path")
120            }
121            #[cfg(target_os = "linux")]
122            Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
123            #[cfg(not(target_os = "macos"))]
124            Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"),
125        }
126    }
127}
128
129impl std::error::Error for SandboxTransformError {}
130
131#[derive(Default)]
132pub struct SandboxManager;
133
134impl SandboxManager {
135    pub fn new() -> Self {
136        Self
137    }
138
139    pub fn select_initial(
140        &self,
141        file_system_policy: &FileSystemSandboxPolicy,
142        network_policy: NetworkSandboxPolicy,
143        pref: SandboxablePreference,
144        windows_sandbox_level: WindowsSandboxLevel,
145        has_managed_network_requirements: bool,
146    ) -> SandboxType {
147        match pref {
148            SandboxablePreference::Forbid => SandboxType::None,
149            SandboxablePreference::Require => {
150                get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
151                    .unwrap_or(SandboxType::None)
152            }
153            SandboxablePreference::Auto => {
154                if should_require_platform_sandbox(
155                    file_system_policy,
156                    network_policy,
157                    has_managed_network_requirements,
158                ) {
159                    get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
160                        .unwrap_or(SandboxType::None)
161                } else {
162                    SandboxType::None
163                }
164            }
165        }
166    }
167
168    pub fn transform(
169        &self,
170        request: SandboxTransformRequest<'_>,
171    ) -> Result<SandboxExecRequest, SandboxTransformError> {
172        let SandboxTransformRequest {
173            mut command,
174            permissions,
175            sandbox,
176            enforce_managed_network,
177            network,
178            sandbox_policy_cwd,
179            zerobox_linux_sandbox_exe,
180            use_legacy_landlock,
181            windows_sandbox_level,
182            windows_sandbox_private_desktop,
183        } = request;
184        let additional_permissions = command.additional_permissions.take();
185        let effective_permission_profile =
186            effective_permission_profile(permissions, additional_permissions.as_ref());
187        let (effective_file_system_policy, effective_network_policy) =
188            effective_permission_profile.to_runtime_permissions();
189        let mut argv = Vec::with_capacity(1 + command.args.len());
190        argv.push(command.program);
191        argv.extend(command.args.into_iter().map(OsString::from));
192
193        let (argv, arg0_override) = match sandbox {
194            SandboxType::None => (os_argv_to_strings(argv), None),
195            #[cfg(target_os = "macos")]
196            SandboxType::MacosSeatbelt => {
197                use crate::seatbelt::CreateSeatbeltCommandArgsParams;
198                use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
199                use crate::seatbelt::create_seatbelt_command_args;
200
201                let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
202                    command: os_argv_to_strings(argv),
203                    file_system_sandbox_policy: &effective_file_system_policy,
204                    network_sandbox_policy: effective_network_policy,
205                    sandbox_policy_cwd,
206                    enforce_managed_network,
207                    network,
208                    extra_allow_unix_sockets: &[],
209                });
210                let mut full_command = Vec::with_capacity(1 + args.len());
211                full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
212                full_command.append(&mut args);
213                (full_command, None)
214            }
215            #[cfg(not(target_os = "macos"))]
216            SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
217            SandboxType::LinuxSeccomp => {
218                let exe = zerobox_linux_sandbox_exe
219                    .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
220                let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
221                #[cfg(target_os = "linux")]
222                ensure_linux_bubblewrap_is_supported(
223                    &effective_file_system_policy,
224                    use_legacy_landlock,
225                    allow_proxy_network,
226                    is_wsl1(),
227                )?;
228                let mut args = create_linux_sandbox_command_args_for_permission_profile(
229                    os_argv_to_strings(argv),
230                    command.cwd.as_path(),
231                    &effective_permission_profile,
232                    sandbox_policy_cwd,
233                    use_legacy_landlock,
234                    allow_proxy_network,
235                );
236                let mut full_command = Vec::with_capacity(1 + args.len());
237                full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
238                full_command.append(&mut args);
239                (full_command, Some(linux_sandbox_arg0_override(exe)))
240            }
241            #[cfg(target_os = "windows")]
242            SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
243            #[cfg(not(target_os = "windows"))]
244            SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
245        };
246
247        Ok(SandboxExecRequest {
248            command: argv,
249            cwd: command.cwd,
250            env: command.env,
251            network: network.cloned(),
252            sandbox,
253            windows_sandbox_level,
254            windows_sandbox_private_desktop,
255            permission_profile: effective_permission_profile,
256            file_system_sandbox_policy: effective_file_system_policy,
257            network_sandbox_policy: effective_network_policy,
258            arg0: arg0_override,
259        })
260    }
261}
262
263pub fn compatibility_sandbox_policy_for_permission_profile(
264    permissions: &PermissionProfile,
265    file_system_policy: &FileSystemSandboxPolicy,
266    network_policy: NetworkSandboxPolicy,
267    cwd: &Path,
268) -> SandboxPolicy {
269    permissions
270        .to_legacy_sandbox_policy(cwd)
271        .unwrap_or_else(|_| {
272            compatibility_workspace_write_policy(file_system_policy, network_policy, cwd)
273        })
274}
275
276fn compatibility_workspace_write_policy(
277    file_system_policy: &FileSystemSandboxPolicy,
278    network_policy: NetworkSandboxPolicy,
279    cwd: &Path,
280) -> SandboxPolicy {
281    let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok();
282    let writable_roots = file_system_policy
283        .get_writable_roots_with_cwd(cwd)
284        .into_iter()
285        .map(|root| root.root)
286        .filter(|root| cwd_abs.as_ref() != Some(root))
287        .collect();
288    let tmpdir_writable = std::env::var_os("TMPDIR")
289        .filter(|tmpdir| !tmpdir.is_empty())
290        .and_then(|tmpdir| {
291            AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()
292        })
293        .is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd));
294    let slash_tmp = Path::new("/tmp");
295    let slash_tmp_writable = slash_tmp.is_absolute()
296        && slash_tmp.is_dir()
297        && file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);
298
299    SandboxPolicy::WorkspaceWrite {
300        writable_roots,
301        network_access: network_policy.is_enabled(),
302        exclude_tmpdir_env_var: !tmpdir_writable,
303        exclude_slash_tmp: !slash_tmp_writable,
304    }
305}
306
307#[cfg(target_os = "linux")]
308fn ensure_linux_bubblewrap_is_supported(
309    file_system_sandbox_policy: &FileSystemSandboxPolicy,
310    use_legacy_landlock: bool,
311    allow_network_for_proxy: bool,
312    is_wsl1: bool,
313) -> Result<(), SandboxTransformError> {
314    let requires_bubblewrap = !use_legacy_landlock
315        && (!file_system_sandbox_policy.has_full_disk_write_access() || allow_network_for_proxy);
316    if is_wsl1 && requires_bubblewrap {
317        return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap);
318    }
319
320    Ok(())
321}
322
323fn os_argv_to_strings(argv: Vec<OsString>) -> Vec<String> {
324    argv.into_iter()
325        .map(os_string_to_command_component)
326        .collect()
327}
328
329fn os_string_to_command_component(value: OsString) -> String {
330    value
331        .into_string()
332        .unwrap_or_else(|value| value.to_string_lossy().into_owned())
333}
334
335fn linux_sandbox_arg0_override(exe: &Path) -> String {
336    if exe.file_name().and_then(|name| name.to_str()) == Some(ZEROBOX_LINUX_SANDBOX_ARG0) {
337        os_string_to_command_component(exe.as_os_str().to_owned())
338    } else {
339        ZEROBOX_LINUX_SANDBOX_ARG0.to_string()
340    }
341}
342
343#[cfg(test)]
344#[path = "manager_tests.rs"]
345mod tests;