1use serde::{Deserialize, Serialize};
26use tatara_lisp::DeriveTataraDomain;
27
28use crate::SpecError;
29
30#[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#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum SandboxPlatform {
52 Linux,
54 Darwin,
56 NoSandbox,
58}
59
60#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
62pub enum IsolationTier {
63 Strict,
66 Relaxed,
68 Permissive,
71 Off,
73}
74
75pub const CANONICAL_SANDBOX_LISP: &str = include_str!("../specs/sandbox.lisp");
78
79pub fn load_canonical() -> Result<Vec<SandboxSpec>, SpecError> {
85 crate::loader::load_all::<SandboxSpec>(CANONICAL_SANDBOX_LISP)
86}
87
88pub 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#[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
111pub 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 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 #[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}