Skip to main content

sui_spec/
sandbox.rs

1//! Typed border for the build sandbox.
2//!
3//! cppnix's sandbox isolates each build from the host filesystem +
4//! network.  The shape varies by platform: Linux uses mount/user
5//! namespaces + seccomp; macOS uses `sandbox-exec` profiles;
6//! cppnix without sandbox support runs builds with only chroot.
7//!
8//! Today sui-build/src/sandbox.rs implements per-platform sandboxes;
9//! this module names the typed contract so policy + capabilities
10//! are explicit + spec-driven.
11//!
12//! ## Authoring surface
13//!
14//! ```lisp
15//! (defsandbox-spec
16//!   :name "cppnix-linux-strict"
17//!   :platform Linux
18//!   :isolation-tier Strict
19//!   :allowed-paths ("/nix/store" "/tmp/<build-id>" "/dev/null")
20//!   :network-allowed false
21//!   :seccomp-profile "deny-network-syscalls"
22//!   :user-namespacing true)
23//! ```
24
25use serde::{Deserialize, Serialize};
26use tatara_lisp::DeriveTataraDomain;
27
28use crate::SpecError;
29
30// ── Typed border ───────────────────────────────────────────────────
31
32#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
33#[tatara(keyword = "defsandbox-spec")]
34pub struct SandboxSpec {
35    pub name: String,
36    pub platform: SandboxPlatform,
37    #[serde(rename = "isolationTier")]
38    pub isolation_tier: IsolationTier,
39    #[serde(default, rename = "allowedPaths")]
40    pub allowed_paths: Vec<String>,
41    #[serde(default, rename = "networkAllowed")]
42    pub network_allowed: bool,
43    #[serde(default, rename = "seccompProfile")]
44    pub seccomp_profile: Option<String>,
45    #[serde(default, rename = "userNamespacing")]
46    pub user_namespacing: bool,
47}
48
49/// Target platform for the sandbox.
50#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum SandboxPlatform {
52    /// mount/user/pid namespaces + seccomp filter.
53    Linux,
54    /// `sandbox-exec` with a profile.
55    Darwin,
56    /// No sandbox; build runs with only chroot (legacy, untrusted).
57    NoSandbox,
58}
59
60/// How strictly the sandbox isolates the build.
61#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
62pub enum IsolationTier {
63    /// No network, no host paths, no devices beyond /dev/null.
64    /// Bit-for-bit reproducible builds.
65    Strict,
66    /// Network allowed (for FOD fetches).  Otherwise like Strict.
67    Relaxed,
68    /// Permissive — only sandboxed at the chroot level, no seccomp.
69    /// Used for builds that need host capabilities (Darwin .app).
70    Permissive,
71    /// No isolation at all.  Used for `:requires :no-sandbox` drvs.
72    Off,
73}
74
75// ── Canonical spec ─────────────────────────────────────────────────
76
77pub const CANONICAL_SANDBOX_LISP: &str = include_str!("../specs/sandbox.lisp");
78
79/// Compile every authored sandbox spec.
80///
81/// # Errors
82///
83/// Returns an error if the Lisp source fails to parse.
84pub fn load_canonical() -> Result<Vec<SandboxSpec>, SpecError> {
85    crate::loader::load_all::<SandboxSpec>(CANONICAL_SANDBOX_LISP)
86}
87
88/// Return the sandbox spec whose `name` matches.
89///
90/// # Errors
91///
92/// Returns an error if the spec fails to parse or `name` is missing.
93pub fn load_named(name: &str) -> Result<SandboxSpec, SpecError> {
94    load_canonical()?
95        .into_iter()
96        .find(|s| s.name == name)
97        .ok_or_else(|| SpecError::Load(format!("no (defsandbox-spec) with :name {name:?}")))
98}
99
100// ── M3.0 sandbox policy checker ────────────────────────────────────
101
102/// Check whether a path is allowed under a sandbox spec.  Used by
103/// the builder to validate any bind-mount or file-access request.
104#[must_use]
105pub fn path_allowed(spec: &SandboxSpec, path: &str) -> bool {
106    spec.allowed_paths.iter().any(|allowed| {
107        path == allowed || path.starts_with(&format!("{allowed}/"))
108    })
109}
110
111/// Check whether a build derivation's `__noSandbox` /
112/// `__sandboxAllowNetwork` setting is compatible with the
113/// declared sandbox spec.
114///
115/// # Errors
116///
117/// `sandbox-policy-violation` if the derivation requests
118/// capabilities the spec doesn't grant.
119pub fn check_drv_compat(
120    spec: &SandboxSpec,
121    requires_network: bool,
122    requires_no_sandbox: bool,
123) -> Result<(), SpecError> {
124    if requires_no_sandbox && spec.isolation_tier != IsolationTier::Off {
125        return Err(SpecError::Interp {
126            phase: "sandbox-policy-violation".into(),
127            message: format!(
128                "derivation requires no-sandbox but spec `{}` is tier {:?}",
129                spec.name, spec.isolation_tier,
130            ),
131        });
132    }
133    if requires_network && !spec.network_allowed {
134        return Err(SpecError::Interp {
135            phase: "sandbox-policy-violation".into(),
136            message: format!(
137                "derivation requires network but spec `{}` blocks it",
138                spec.name,
139            ),
140        });
141    }
142    Ok(())
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn canonical_sandboxes_parse() {
151        let specs = load_canonical().expect("canonical sandbox specs must compile");
152        assert!(!specs.is_empty());
153    }
154
155    #[test]
156    fn strict_linux_blocks_network() {
157        let s = load_named("cppnix-linux-strict").unwrap();
158        assert_eq!(s.platform, SandboxPlatform::Linux);
159        assert_eq!(s.isolation_tier, IsolationTier::Strict);
160        assert!(!s.network_allowed);
161        assert!(s.user_namespacing);
162    }
163
164    #[test]
165    fn fod_sandbox_allows_network() {
166        // Fixed-output derivations need network for fetchurl.
167        let s = load_named("cppnix-linux-fod").unwrap();
168        assert!(s.network_allowed);
169        assert_eq!(s.isolation_tier, IsolationTier::Relaxed);
170    }
171
172    #[test]
173    fn darwin_sandbox_targets_darwin() {
174        let s = load_named("cppnix-darwin-strict").unwrap();
175        assert_eq!(s.platform, SandboxPlatform::Darwin);
176    }
177
178    // ── M3.0 policy tests ──────────────────────────────────────
179
180    #[test]
181    fn path_allowed_matches_exact_and_descendants() {
182        let s = load_named("cppnix-linux-strict").unwrap();
183        assert!(path_allowed(&s, "/nix/store"));
184        assert!(path_allowed(&s, "/nix/store/abc-hello"));
185        assert!(!path_allowed(&s, "/etc/hosts"));
186        assert!(!path_allowed(&s, "/home/user"));
187    }
188
189    #[test]
190    fn strict_sandbox_blocks_network_drv() {
191        let s = load_named("cppnix-linux-strict").unwrap();
192        let err = check_drv_compat(&s, true, false).unwrap_err();
193        match err {
194            SpecError::Interp { phase, .. } => assert_eq!(phase, "sandbox-policy-violation"),
195            _ => panic!("expected policy-violation"),
196        }
197    }
198
199    #[test]
200    fn fod_sandbox_allows_network_drv() {
201        let s = load_named("cppnix-linux-fod").unwrap();
202        check_drv_compat(&s, true, false).unwrap();
203    }
204
205    #[test]
206    fn no_sandbox_drv_blocked_by_strict_spec() {
207        let s = load_named("cppnix-linux-strict").unwrap();
208        let err = check_drv_compat(&s, false, true).unwrap_err();
209        match err {
210            SpecError::Interp { phase, .. } => assert_eq!(phase, "sandbox-policy-violation"),
211            _ => panic!("expected policy-violation"),
212        }
213    }
214}