1use serde::{Deserialize, Serialize};
4
5use crate::config::{BoxConfig, ExecutionIsolation, TeeConfig};
6use crate::error::{BoxError, Result};
7use crate::network::NetworkMode;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum ExecutionBackend {
13 Krun,
15 Crun,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub enum IsolationClass {
23 HardwareVm,
25 SharedKernel,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct ResolvedExecutionPlan {
32 pub requested_isolation: ExecutionIsolation,
34 pub backend: ExecutionBackend,
36 pub isolation_class: IsolationClass,
38 pub required_controls: Vec<String>,
40}
41
42const SANDBOX_REQUIRED_CONTROLS: &[&str] = &[
43 "user-namespace",
44 "mount-namespace",
45 "pid-namespace",
46 "ipc-namespace",
47 "uts-namespace",
48 "network-namespace",
49 "seccomp",
50 "capability-bounding-set",
51 "no-new-privileges",
52 "cgroup-v2",
53];
54
55const SANDBOX_ALLOWED_ADDED_CAPABILITIES: &[&str] = &[
56 "AUDIT_WRITE",
57 "CHOWN",
58 "DAC_OVERRIDE",
59 "FOWNER",
60 "FSETID",
61 "KILL",
62 "MKNOD",
63 "NET_BIND_SERVICE",
64 "SETFCAP",
65 "SETGID",
66 "SETPCAP",
67 "SETUID",
68 "SYS_CHROOT",
69];
70
71pub fn resolve_execution(config: &BoxConfig) -> Result<ResolvedExecutionPlan> {
77 match config.isolation {
78 ExecutionIsolation::Microvm => Ok(ResolvedExecutionPlan {
79 requested_isolation: ExecutionIsolation::Microvm,
80 backend: ExecutionBackend::Krun,
81 isolation_class: IsolationClass::HardwareVm,
82 required_controls: Vec::new(),
83 }),
84 ExecutionIsolation::Sandbox => {
85 validate_sandbox_compatibility(config)?;
86 Ok(ResolvedExecutionPlan {
87 requested_isolation: ExecutionIsolation::Sandbox,
88 backend: ExecutionBackend::Crun,
89 isolation_class: IsolationClass::SharedKernel,
90 required_controls: SANDBOX_REQUIRED_CONTROLS
91 .iter()
92 .map(|control| (*control).to_string())
93 .collect(),
94 })
95 }
96 }
97}
98
99pub fn validate_sandbox_compatibility(config: &BoxConfig) -> Result<()> {
101 if !config.isolation.is_sandbox() {
102 return Ok(());
103 }
104
105 let mut unsupported = Vec::new();
106
107 if !matches!(config.tee, TeeConfig::None) {
108 unsupported.push("TEE and attestation");
109 }
110 if config.pool.enabled || config.pool.snapshot_fork {
111 unsupported.push("warm pools and snapshot-fork");
112 }
113 if config.deferred_main {
114 unsupported.push("deferred main execution");
115 }
116 if config.ksm {
117 unsupported.push("KSM");
118 }
119 if config.snapshot_mem_file.is_some()
120 || config.snapshot_sock.is_some()
121 || config.restore_from.is_some()
122 {
123 unsupported.push("VM snapshots and restore");
124 }
125 if config.privileged {
126 unsupported.push("privileged mode");
127 }
128 if config.sidecar.is_some() {
129 unsupported.push("vsock sidecars");
130 }
131 if !config.port_map.is_empty() {
132 unsupported.push("published ports");
133 }
134 if matches!(config.network, NetworkMode::Bridge { .. }) {
135 unsupported.push("named bridge networking");
136 }
137 if !config.sysctls.is_empty() {
138 unsupported.push("custom sysctls");
139 }
140 if config
141 .security_opt
142 .iter()
143 .any(|option| option.trim().eq_ignore_ascii_case("seccomp=unconfined"))
144 {
145 unsupported.push("unconfined seccomp");
146 }
147
148 let disallowed_capabilities: Vec<String> = config
149 .cap_add
150 .iter()
151 .map(|capability| normalize_capability(capability))
152 .filter(|capability| !SANDBOX_ALLOWED_ADDED_CAPABILITIES.contains(&capability.as_str()))
153 .collect();
154 if !disallowed_capabilities.is_empty() {
155 return Err(BoxError::ConfigError(format!(
156 "sandbox isolation rejects added capabilities outside its allowlist: {}",
157 disallowed_capabilities.join(", ")
158 )));
159 }
160
161 if unsupported.is_empty() {
162 Ok(())
163 } else {
164 Err(BoxError::ConfigError(format!(
165 "sandbox isolation does not support: {}",
166 unsupported.join(", ")
167 )))
168 }
169}
170
171fn normalize_capability(capability: &str) -> String {
172 let normalized = capability.trim().to_ascii_uppercase();
173 normalized
174 .strip_prefix("CAP_")
175 .unwrap_or(&normalized)
176 .to_string()
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::config::{PoolConfig, SidecarConfig};
183
184 fn sandbox_config() -> BoxConfig {
185 BoxConfig {
186 isolation: ExecutionIsolation::Sandbox,
187 ..Default::default()
188 }
189 }
190
191 #[test]
192 fn default_resolves_only_to_krun_hardware_vm() {
193 let plan = resolve_execution(&BoxConfig::default()).unwrap();
194 assert_eq!(plan.backend, ExecutionBackend::Krun);
195 assert_eq!(plan.isolation_class, IsolationClass::HardwareVm);
196 assert!(plan.required_controls.is_empty());
197 }
198
199 #[test]
200 fn sandbox_resolves_to_crun_shared_kernel_with_mandatory_controls() {
201 let plan = resolve_execution(&sandbox_config()).unwrap();
202 assert_eq!(plan.backend, ExecutionBackend::Crun);
203 assert_eq!(plan.isolation_class, IsolationClass::SharedKernel);
204 for required in SANDBOX_REQUIRED_CONTROLS {
205 assert!(plan.required_controls.iter().any(|value| value == required));
206 }
207 }
208
209 #[test]
210 fn sandbox_rejects_vm_only_features_together() {
211 let config = BoxConfig {
212 isolation: ExecutionIsolation::Sandbox,
213 tee: TeeConfig::Tdx {
214 workload_id: "test".to_string(),
215 simulate: true,
216 },
217 pool: PoolConfig {
218 enabled: true,
219 ..Default::default()
220 },
221 sidecar: Some(SidecarConfig::default()),
222 port_map: vec!["8080:80".to_string()],
223 privileged: true,
224 ..Default::default()
225 };
226
227 let error = resolve_execution(&config).unwrap_err().to_string();
228 assert!(error.contains("TEE and attestation"));
229 assert!(error.contains("warm pools"));
230 assert!(error.contains("vsock sidecars"));
231 assert!(error.contains("published ports"));
232 assert!(error.contains("privileged mode"));
233 }
234
235 #[test]
236 fn sandbox_rejects_unconfined_seccomp() {
237 let config = BoxConfig {
238 security_opt: vec!["seccomp=unconfined".to_string()],
239 ..sandbox_config()
240 };
241 assert!(resolve_execution(&config)
242 .unwrap_err()
243 .to_string()
244 .contains("unconfined seccomp"));
245 }
246
247 #[test]
248 fn sandbox_normalizes_and_allows_baseline_capabilities() {
249 let config = BoxConfig {
250 cap_add: vec!["cap_chown".to_string(), "NET_BIND_SERVICE".to_string()],
251 ..sandbox_config()
252 };
253 assert!(resolve_execution(&config).is_ok());
254 }
255
256 #[test]
257 fn sandbox_rejects_powerful_added_capability() {
258 let config = BoxConfig {
259 cap_add: vec!["CAP_SYS_ADMIN".to_string()],
260 ..sandbox_config()
261 };
262 let error = resolve_execution(&config).unwrap_err().to_string();
263 assert!(error.contains("SYS_ADMIN"));
264 }
265}