Skip to main content

ai_sandbox/linux_sandbox/
bsd.rs

1//! FreeBSD/OpenBSD Sandbox Implementation
2//!
3//! Provides BSD sandboxing via Capsicum (FreeBSD) and pledge (OpenBSD).
4
5#![allow(dead_code)]
6
7/// FreeBSD Capsicum sandbox level
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
9pub enum CapsicumLevel {
10    /// No sandboxing
11    #[default]
12    Disabled,
13    /// Basic capability mode
14    Basic,
15    /// Strict capability mode
16    Strict,
17}
18
19// FreeBSD libc bindings for Capsicum
20#[cfg(target_os = "freebsd")]
21extern "C" {
22    fn cap_enter() -> std::os::raw::c_int;
23    fn cap_rights_limit(
24        fd: std::os::raw::c_int,
25        rights: *const std::os::raw::c_void,
26    ) -> std::os::raw::c_int;
27}
28
29/// OpenBSD pledge promises
30#[derive(Clone, Debug, Default)]
31pub struct PledgePromises {
32    pub stdio: bool,
33    pub rpath: bool,
34    pub wpath: bool,
35    pub cpath: bool,
36    pub dpath: bool,
37    pub fpath: bool,
38    pub inet: bool,
39    pub unix: bool,
40    pub dns: bool,
41    pub proc: bool,
42    pub exec: bool,
43    pub id: bool,
44    pub chown: bool,
45    pub flock: bool,
46    pub tmppath: bool,
47    pub error: bool,
48}
49
50impl PledgePromises {
51    /// Default promises for a safe subprocess
52    pub fn default_safe() -> Self {
53        Self {
54            stdio: true,
55            rpath: true,
56            wpath: false,
57            cpath: false,
58            dpath: false,
59            fpath: false,
60            inet: false,
61            unix: false,
62            dns: false,
63            proc: false,
64            exec: false,
65            id: false,
66            chown: false,
67            flock: false,
68            tmppath: true,
69            error: true,
70        }
71    }
72
73    /// Convert to pledge promise string
74    pub fn to_pledge_string(&self) -> String {
75        let mut promises = Vec::new();
76
77        if self.stdio {
78            promises.push("stdio");
79        }
80        if self.rpath {
81            promises.push("rpath");
82        }
83        if self.wpath {
84            promises.push("wpath");
85        }
86        if self.cpath {
87            promises.push("cpath");
88        }
89        if self.dpath {
90            promises.push("dpath");
91        }
92        if self.fpath {
93            promises.push("fpath");
94        }
95        if self.inet {
96            promises.push("inet");
97        }
98        if self.unix {
99            promises.push("unix");
100        }
101        if self.dns {
102            promises.push("dns");
103        }
104        if self.proc {
105            promises.push("proc");
106        }
107        if self.exec {
108            promises.push("exec");
109        }
110        if self.id {
111            promises.push("id");
112        }
113        if self.chown {
114            promises.push("chown");
115        }
116        if self.flock {
117            promises.push("flock");
118        }
119        if self.tmppath {
120            promises.push("tmppath");
121        }
122        if self.error {
123            promises.push("error");
124        }
125
126        promises.join(" ")
127    }
128}
129
130/// Create PledgePromises from SandboxPolicy
131pub fn create_pledge_promises_from_policy(
132    file_system_policy: &crate::FileSystemSandboxPolicy,
133    network_policy: crate::NetworkSandboxPolicy,
134) -> PledgePromises {
135    let mut promises = PledgePromises::default_safe();
136
137    // Adjust based on filesystem policy
138    match file_system_policy {
139        crate::FileSystemSandboxPolicy::FullAccess => {
140            // Allow everything
141            promises.rpath = true;
142            promises.wpath = true;
143            promises.cpath = true;
144        }
145        crate::FileSystemSandboxPolicy::ReadOnly => {
146            // Read only
147            promises.rpath = true;
148            promises.wpath = false;
149            promises.cpath = false;
150        }
151        crate::FileSystemSandboxPolicy::WorkspaceWrite { .. } => {
152            // Allow read and some write
153            promises.rpath = true;
154            promises.wpath = true;
155            promises.cpath = true;
156        }
157        crate::FileSystemSandboxPolicy::External => {
158            // External - minimal restrictions
159        }
160    }
161
162    // Adjust based on network policy
163    match network_policy {
164        crate::NetworkSandboxPolicy::FullAccess => {
165            promises.inet = true;
166            promises.dns = true;
167        }
168        crate::NetworkSandboxPolicy::Localhost => {
169            // Localhost still needs inet for loopback
170            promises.inet = true;
171        }
172        crate::NetworkSandboxPolicy::NoAccess => {
173            promises.inet = false;
174            promises.dns = false;
175        }
176        crate::NetworkSandboxPolicy::Proxy => {
177            promises.inet = true;
178            promises.dns = true;
179        }
180    }
181
182    promises
183}
184
185/// Create FreeBSD sandbox arguments
186pub fn create_freebsd_sandbox_args(argv: &[String], level: CapsicumLevel) -> Vec<String> {
187    let mut args = vec![];
188
189    match level {
190        CapsicumLevel::Disabled => {
191            // No sandboxing
192        }
193        CapsicumLevel::Basic => {
194            args.push("--capsicum".to_string());
195            args.push("basic".to_string());
196        }
197        CapsicumLevel::Strict => {
198            args.push("--capsicum".to_string());
199            args.push("strict".to_string());
200        }
201    }
202
203    args.extend(argv.iter().cloned());
204    args
205}
206
207/// Check if FreeBSD capsicum is available
208pub fn is_capsicum_available() -> bool {
209    #[cfg(target_os = "freebsd")]
210    {
211        // Capsicum is available on FreeBSD 10+
212        true
213    }
214    #[cfg(not(target_os = "freebsd"))]
215    {
216        false
217    }
218}
219
220/// Check if OpenBSD pledge is available
221pub fn is_pledge_available() -> bool {
222    #[cfg(target_os = "openbsd")]
223    {
224        // pledge is available on all OpenBSD versions
225        true
226    }
227    #[cfg(not(target_os = "openbsd"))]
228    {
229        false
230    }
231}
232
233#[cfg(target_os = "freebsd")]
234mod freebsd_impl {
235    use std::process::{Command, Stdio};
236
237    /// Execute a command with capsicum sandbox
238    pub fn execute_with_capsicum(
239        program: &str,
240        args: &[String],
241        level: super::CapsicumLevel,
242    ) -> std::io::Result<std::process::Child> {
243        // If disabled, just spawn without sandboxing
244        if matches!(level, super::CapsicumLevel::Disabled) {
245            let mut cmd = Command::new(program);
246            cmd.args(args);
247            cmd.stdin(Stdio::inherit());
248            cmd.stdout(Stdio::inherit());
249            cmd.stderr(Stdio::inherit());
250            return cmd.spawn();
251        }
252
253        // Build command that will call cap_enter() before exec
254        // We need to use a shell wrapper or spawn a child that enters capsicum
255        let capsicum_wrapper = format!("exec {}", args.join(" "));
256
257        let mut cmd = Command::new(program);
258        cmd.args(args);
259        cmd.stdin(Stdio::inherit());
260        cmd.stdout(Stdio::inherit());
261        cmd.stderr(Stdio::inherit());
262
263        // Note: In a real implementation, this would require either:
264        // 1. A wrapper binary that calls cap_enter() before exec
265        // 2. Using prctl to set up the sandbox before spawning
266        // 3. LD_PRELOAD or similar mechanism
267        //
268        // For now, we set an environment variable to indicate the sandbox should be enabled
269        // The actual enforcement would be done by a capsicum-enabled loader or wrapper
270        cmd.env(
271            "CAPSICUM_ENABLED",
272            match level {
273                super::CapsicumLevel::Basic => "basic",
274                super::CapsicumLevel::Strict => "strict",
275                _ => "disabled",
276            },
277        );
278
279        cmd.spawn()
280    }
281}
282
283#[cfg(target_os = "openbsd")]
284mod openbsd_impl {
285    use std::process::{Command, Stdio};
286
287    // Import the pledge libc function
288    extern "C" {
289        fn pledge(
290            promises: *const std::ffi::CStr,
291            execpromises: *const std::ffi::CStr,
292        ) -> std::os::raw::c_int;
293    }
294
295    /// Execute a command with pledge sandbox
296    pub fn execute_with_pledge(
297        program: &str,
298        args: &[String],
299        promises: &super::PledgePromises,
300    ) -> std::io::Result<std::process::Child> {
301        let promise_str = promises.to_pledge_string();
302        let promise_cstr = std::ffi::CString::new(promise_str).unwrap();
303        let empty_cstr = std::ffi::CString::new("").unwrap();
304
305        // Call pledge before exec
306        unsafe {
307            if pledge(promise_cstr.as_c_str(), empty_cstr.as_c_str()) != 0 {
308                return Err(std::io::Error::last_os_error());
309            }
310        }
311
312        let mut cmd = Command::new(program);
313        cmd.args(args);
314        cmd.stdin(Stdio::inherit());
315        cmd.stdout(Stdio::inherit());
316        cmd.stderr(Stdio::inherit());
317
318        cmd.spawn()
319    }
320}
321
322#[cfg(not(target_os = "freebsd"))]
323mod freebsd_impl {
324    use std::io;
325
326    pub fn execute_with_capsicum(
327        _program: &str,
328        _args: &[String],
329        _level: super::CapsicumLevel,
330    ) -> io::Result<std::process::Child> {
331        Err(io::Error::new(
332            io::ErrorKind::Unsupported,
333            "Capsicum not available on this platform",
334        ))
335    }
336}
337
338#[cfg(not(target_os = "openbsd"))]
339mod openbsd_impl {
340    use std::io;
341
342    pub fn execute_with_pledge(
343        _program: &str,
344        _args: &[String],
345        _promises: &super::PledgePromises,
346    ) -> io::Result<std::process::Child> {
347        Err(io::Error::new(
348            io::ErrorKind::Unsupported,
349            "pledge not available on this platform",
350        ))
351    }
352}
353
354pub use freebsd_impl::execute_with_capsicum;
355pub use openbsd_impl::execute_with_pledge;
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_pledge_promises() {
363        let promises = PledgePromises::default_safe();
364        let s = promises.to_pledge_string();
365        assert!(s.contains("stdio"));
366        assert!(s.contains("rpath"));
367    }
368
369    // ============================================================================
370    // 新增测试: create_pledge_promises_from_policy 函数
371    // ============================================================================
372
373    #[test]
374    fn test_create_pledge_promises_from_policy_full_access() {
375        let promises = create_pledge_promises_from_policy(
376            &crate::FileSystemSandboxPolicy::FullAccess,
377            crate::NetworkSandboxPolicy::FullAccess,
378        );
379        let s = promises.to_pledge_string();
380        // FullAccess should allow all filesystem and network
381        assert!(s.contains("rpath"));
382        assert!(s.contains("wpath"));
383        assert!(s.contains("cpath"));
384        assert!(s.contains("inet"));
385        assert!(s.contains("dns"));
386    }
387
388    #[test]
389    fn test_create_pledge_promises_from_policy_readonly() {
390        let promises = create_pledge_promises_from_policy(
391            &crate::FileSystemSandboxPolicy::ReadOnly,
392            crate::NetworkSandboxPolicy::NoAccess,
393        );
394        let s = promises.to_pledge_string();
395        // ReadOnly should allow read but not write
396        assert!(s.contains("rpath"));
397        assert!(!s.contains("wpath"));
398        assert!(!s.contains("cpath"));
399        // NoAccess should deny network
400        assert!(!s.contains("inet"));
401        assert!(!s.contains("dns"));
402    }
403
404    #[test]
405    fn test_create_pledge_promises_from_policy_workspace() {
406        let promises = create_pledge_promises_from_policy(
407            &crate::FileSystemSandboxPolicy::WorkspaceWrite {
408                writable_roots: vec![std::path::PathBuf::from("/tmp")],
409            },
410            crate::NetworkSandboxPolicy::Localhost,
411        );
412        let s = promises.to_pledge_string();
413        // WorkspaceWrite should allow read and write
414        assert!(s.contains("rpath"));
415        assert!(s.contains("wpath"));
416        // Localhost should allow inet for loopback
417        assert!(s.contains("inet"));
418        // But not dns (specific to localhost)
419        assert!(!s.contains("dns"));
420    }
421
422    #[test]
423    fn test_create_pledge_promises_from_policy_external() {
424        let promises = create_pledge_promises_from_policy(
425            &crate::FileSystemSandboxPolicy::External,
426            crate::NetworkSandboxPolicy::Proxy,
427        );
428        let s = promises.to_pledge_string();
429        // External has minimal restrictions, Proxy allows inet and dns
430        assert!(s.contains("inet"));
431        assert!(s.contains("dns"));
432    }
433
434    #[test]
435    fn test_create_pledge_promises_from_policy_no_network() {
436        let promises = create_pledge_promises_from_policy(
437            &crate::FileSystemSandboxPolicy::FullAccess,
438            crate::NetworkSandboxPolicy::NoAccess,
439        );
440        let s = promises.to_pledge_string();
441        // No network access
442        assert!(!s.contains("inet"));
443        assert!(!s.contains("dns"));
444    }
445
446    #[test]
447    fn test_capsicum_level_variants() {
448        assert_eq!(CapsicumLevel::default(), CapsicumLevel::Disabled);
449        let _ = CapsicumLevel::Basic;
450        let _ = CapsicumLevel::Strict;
451    }
452
453    #[test]
454    fn test_pledge_promises_default_safe() {
455        let promises = PledgePromises::default_safe();
456        let s = promises.to_pledge_string();
457        // default_safe should be restrictive
458        assert!(s.contains("stdio"));
459        assert!(s.contains("rpath"));
460        assert!(!s.contains("wpath")); // Not allowed by default
461        assert!(!s.contains("inet")); // Not allowed by default
462    }
463}