Skip to main content

evalbox_sandbox/
sysinfo.rs

1//! System information and path detection.
2//!
3//! Detects the system type (NixOS, traditional FHS, etc.) and provides
4//! appropriate paths for sandbox configuration.
5
6use std::path::{Path, PathBuf};
7use std::sync::LazyLock;
8
9pub static SYSTEM_PATHS: LazyLock<SystemPaths> = LazyLock::new(SystemPaths::detect);
10
11/// System type detection.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SystemType {
14    /// NixOS - binaries in /nix/store, no standard FHS paths
15    NixOS,
16    /// Guix - similar to NixOS, binaries in /gnu/store
17    Guix,
18    /// Standard FHS Linux (Debian, Ubuntu, Fedora, Arch, etc.)
19    Fhs,
20}
21
22impl SystemType {
23    /// Detect the current system type.
24    pub fn detect() -> Self {
25        // NixOS: Check for definitive markers
26        // - /etc/NIXOS is a NixOS-specific file
27        // - /nix/store exists and /bin/sh (if it exists) is a symlink to /nix/store
28        if Path::new("/etc/NIXOS").exists() {
29            return SystemType::NixOS;
30        }
31        if Path::new("/nix/store").exists() {
32            // Check if /bin/sh points to nix store (common on NixOS with compatibility)
33            if let Ok(target) = std::fs::read_link("/bin/sh") {
34                if target.to_string_lossy().contains("/nix/store") {
35                    return SystemType::NixOS;
36                }
37            }
38            // No /bin/sh or not a symlink = pure NixOS
39            if !Path::new("/bin/sh").exists() {
40                return SystemType::NixOS;
41            }
42        }
43
44        // Guix: Similar logic
45        if Path::new("/gnu/store").exists() {
46            if let Ok(target) = std::fs::read_link("/bin/sh") {
47                if target.to_string_lossy().contains("/gnu/store") {
48                    return SystemType::Guix;
49                }
50            }
51            if !Path::new("/bin/sh").exists() {
52                return SystemType::Guix;
53            }
54        }
55
56        SystemType::Fhs
57    }
58
59    pub fn is_nix_like(self) -> bool {
60        matches!(self, SystemType::NixOS | SystemType::Guix)
61    }
62}
63
64/// Paths that exist on the current system for mounting.
65#[derive(Debug, Clone)]
66pub struct SystemPaths {
67    /// System type
68    pub system_type: SystemType,
69    /// Paths to mount read-only (system directories)
70    pub readonly_mounts: Vec<PathBuf>,
71    /// Default PATH environment variable
72    pub default_path: String,
73}
74
75impl SystemPaths {
76    /// Detect system paths.
77    pub fn detect() -> Self {
78        let system_type = SystemType::detect();
79
80        match system_type {
81            SystemType::NixOS => Self::nixos_paths(),
82            SystemType::Guix => Self::guix_paths(),
83            SystemType::Fhs => Self::fhs_paths(),
84        }
85    }
86
87    fn nixos_paths() -> Self {
88        let mut readonly_mounts = Vec::new();
89
90        // On NixOS, we need /nix/store for all binaries and libraries
91        if Path::new("/nix/store").exists() {
92            readonly_mounts.push(PathBuf::from("/nix/store"));
93        }
94
95        // /run/current-system/sw contains symlinks to installed packages
96        if Path::new("/run/current-system/sw").exists() {
97            readonly_mounts.push(PathBuf::from("/run/current-system/sw"));
98        }
99
100        // NOTE: We do NOT mount /etc from host to prevent information leakage.
101        // Essential /etc files are created in workspace::setup_minimal_etc()
102
103        // Build PATH from NixOS locations
104        let path_dirs = [
105            "/run/current-system/sw/bin",
106            "/nix/var/nix/profiles/default/bin",
107        ];
108
109        let default_path = path_dirs
110            .iter()
111            .filter(|p| Path::new(p).exists())
112            .copied()
113            .collect::<Vec<_>>()
114            .join(":");
115
116        Self {
117            system_type: SystemType::NixOS,
118            readonly_mounts,
119            default_path: if default_path.is_empty() {
120                "/bin".to_string()
121            } else {
122                default_path
123            },
124        }
125    }
126
127    fn guix_paths() -> Self {
128        let mut readonly_mounts = Vec::new();
129
130        if Path::new("/gnu/store").exists() {
131            readonly_mounts.push(PathBuf::from("/gnu/store"));
132        }
133
134        // NOTE: We do NOT mount /etc from host to prevent information leakage.
135        // Essential /etc files are created in workspace::setup_minimal_etc()
136
137        Self {
138            system_type: SystemType::Guix,
139            readonly_mounts,
140            default_path: "/run/current-system/profile/bin".to_string(),
141        }
142    }
143
144    fn fhs_paths() -> Self {
145        // NOTE: /etc is NOT included to prevent information leakage.
146        // Essential /etc files are created in workspace::setup_minimal_etc()
147        const FHS_DIRS: &[&str] = &["/usr", "/bin", "/lib", "/lib64", "/sbin"];
148
149        let readonly_mounts = FHS_DIRS
150            .iter()
151            .map(Path::new)
152            .filter(|p| p.exists())
153            .map(Path::to_path_buf)
154            .collect();
155
156        Self {
157            system_type: SystemType::Fhs,
158            readonly_mounts,
159            default_path: "/usr/local/bin:/usr/bin:/bin".to_string(),
160        }
161    }
162
163    /// Get paths suitable for Landlock rules.
164    pub fn landlock_readonly_paths(&self) -> Vec<&Path> {
165        self.readonly_mounts.iter().map(|p| p.as_path()).collect()
166    }
167}
168
169pub fn is_nix_store_path(path: &Path) -> bool {
170    path.starts_with("/nix/store")
171}
172
173pub fn is_guix_store_path(path: &Path) -> bool {
174    path.starts_with("/gnu/store")
175}
176
177/// Get the store path for a binary (first component after /nix/store/ or /gnu/store/).
178pub fn get_store_path(path: &Path) -> Option<PathBuf> {
179    let path_str = path.to_string_lossy();
180
181    for store in ["/nix/store", "/gnu/store"] {
182        if path_str.starts_with(store) {
183            // Return the entire store since we can't easily know which packages are needed
184            return Some(PathBuf::from(store));
185        }
186    }
187
188    None
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_system_type_detect() {
197        let system_type = SystemType::detect();
198        // Should detect something
199        assert!(matches!(
200            system_type,
201            SystemType::NixOS | SystemType::Guix | SystemType::Fhs
202        ));
203    }
204
205    #[test]
206    fn test_system_paths_detect() {
207        let paths = SystemPaths::detect();
208        // Should have a non-empty PATH
209        assert!(!paths.default_path.is_empty());
210    }
211
212    #[test]
213    fn test_is_nix_store_path() {
214        assert!(is_nix_store_path(Path::new("/nix/store/abc123/bin/python")));
215        assert!(!is_nix_store_path(Path::new("/usr/bin/python")));
216    }
217
218    #[test]
219    fn test_get_store_path() {
220        let nix_path = Path::new("/nix/store/abc123/bin/python");
221        assert_eq!(get_store_path(nix_path), Some(PathBuf::from("/nix/store")));
222
223        let usr_path = Path::new("/usr/bin/python");
224        assert_eq!(get_store_path(usr_path), None);
225    }
226}