1use std::collections::HashMap;
2use std::os::fd::AsRawFd;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::SystemTime;
6
7use serde::{Deserialize, Serialize};
8use tokio::task::JoinHandle;
9
10use crate::context;
11use crate::error::SandboxError;
12pub use crate::http::{http_acl_check, normalize_path, prefix_or_exact_match, HttpRule};
13pub use crate::network::{IpCidr, NetAllow, NetDeny, NetRule, NetTarget, Protocol};
14use crate::protection::{Protection, ProtectionPolicy, ProtectionState, ProtectionStatus};
15
16mod builder;
17pub use builder::SandboxBuilder;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub struct ByteSize(pub u64);
22
23impl ByteSize {
24 pub fn bytes(n: u64) -> Self {
25 ByteSize(n)
26 }
27
28 pub fn kib(n: u64) -> Self {
29 ByteSize(n * 1024)
30 }
31
32 pub fn mib(n: u64) -> Self {
33 ByteSize(n * 1024 * 1024)
34 }
35
36 pub fn gib(n: u64) -> Self {
37 ByteSize(n * 1024 * 1024 * 1024)
38 }
39
40 pub fn parse(s: &str) -> Result<Self, SandboxError> {
41 let s = s.trim();
42 if s.is_empty() {
43 return Err(SandboxError::Invalid("empty byte size string".into()));
44 }
45
46 let last = s.chars().last().unwrap();
48 if last.is_ascii_alphabetic() {
49 let (num_str, suffix) = s.split_at(s.len() - 1);
50 let n: u64 = num_str
51 .trim()
52 .parse()
53 .map_err(|_| SandboxError::Invalid(format!("invalid byte size: {}", s)))?;
54 match suffix.to_ascii_uppercase().as_str() {
55 "K" => Ok(ByteSize::kib(n)),
56 "M" => Ok(ByteSize::mib(n)),
57 "G" => Ok(ByteSize::gib(n)),
58 other => Err(SandboxError::Invalid(format!("unknown byte size suffix: {}", other))),
59 }
60 } else {
61 let n: u64 = s
62 .parse()
63 .map_err(|_| SandboxError::Invalid(format!("invalid byte size: {}", s)))?;
64 Ok(ByteSize(n))
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub struct RunAs {
80 pub uid: u32,
81 pub gid: u32,
82}
83
84impl std::str::FromStr for RunAs {
85 type Err = String;
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 let (u, g) = s
88 .split_once(':')
89 .ok_or_else(|| format!("expected UID:GID, got {:?}", s))?;
90 let uid = u.trim().parse::<u32>().map_err(|_| format!("invalid uid {:?}", u))?;
91 let gid = g.trim().parse::<u32>().map_err(|_| format!("invalid gid {:?}", g))?;
92 Ok(RunAs { uid, gid })
93 }
94}
95
96#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
98pub struct Confinement {
99 pub fs_writable: Vec<PathBuf>,
100 pub fs_readable: Vec<PathBuf>,
101}
102
103impl Confinement {
104 pub fn builder() -> ConfinementBuilder {
105 ConfinementBuilder::default()
106 }
107}
108
109#[derive(Default)]
110pub struct ConfinementBuilder {
111 fs_writable: Vec<PathBuf>,
112 fs_readable: Vec<PathBuf>,
113}
114
115impl ConfinementBuilder {
116 pub fn fs_write(mut self, path: impl Into<PathBuf>) -> Self {
117 self.fs_writable.push(path.into());
118 self
119 }
120
121 pub fn fs_read(mut self, path: impl Into<PathBuf>) -> Self {
122 self.fs_readable.push(path.into());
123 self
124 }
125
126 pub fn build(self) -> Confinement {
127 Confinement {
128 fs_writable: self.fs_writable,
129 fs_readable: self.fs_readable,
130 }
131 }
132}
133
134impl TryFrom<&Sandbox> for Confinement {
135 type Error = SandboxError;
136
137 fn try_from(sandbox: &Sandbox) -> Result<Self, Self::Error> {
138 let mut unsupported = Vec::new();
139 if !sandbox.fs_denied.is_empty() { unsupported.push("fs_denied"); }
140 if !sandbox.extra_deny_syscalls.is_empty() { unsupported.push("extra_deny_syscalls"); }
141 if !sandbox.net_allow.is_empty() { unsupported.push("net_allow"); }
142 if !sandbox.net_deny.is_empty() { unsupported.push("net_deny"); }
143 if !sandbox.net_allow_bind.is_empty() { unsupported.push("net_allow_bind"); }
144 if !sandbox.net_deny_bind.is_empty() { unsupported.push("net_deny_bind"); }
145 if sandbox.allows_sysv_ipc() { unsupported.push("extra_allow_syscalls=[\"sysv_ipc\"]"); }
146 if !sandbox.http_allow.is_empty() { unsupported.push("http_allow"); }
147 if !sandbox.http_deny.is_empty() { unsupported.push("http_deny"); }
148 if !sandbox.http_ports.is_empty() { unsupported.push("http_ports"); }
149 if sandbox.http_ca.is_some() { unsupported.push("http_ca"); }
150 if sandbox.http_key.is_some() { unsupported.push("http_key"); }
151 if !sandbox.http_inject_ca.is_empty() { unsupported.push("http_inject_ca"); }
152 if sandbox.http_ca_out.is_some() { unsupported.push("http_ca_out"); }
153 if sandbox.max_memory.is_some() { unsupported.push("max_memory"); }
154 if sandbox.max_processes != 64 { unsupported.push("max_processes"); }
155 if sandbox.max_open_files.is_some() { unsupported.push("max_open_files"); }
156 if sandbox.max_cpu.is_some() { unsupported.push("max_cpu"); }
157 if sandbox.random_seed.is_some() { unsupported.push("random_seed"); }
158 if sandbox.time_start.is_some() { unsupported.push("time_start"); }
159 if sandbox.no_randomize_memory { unsupported.push("no_randomize_memory"); }
160 if sandbox.no_huge_pages { unsupported.push("no_huge_pages"); }
161 if sandbox.no_coredump { unsupported.push("no_coredump"); }
162 if sandbox.deterministic_dirs { unsupported.push("deterministic_dirs"); }
163 if sandbox.workdir.is_some() { unsupported.push("workdir"); }
164 if sandbox.cwd.is_some() { unsupported.push("cwd"); }
165 if sandbox.fs_storage.is_some() { unsupported.push("fs_storage"); }
166 if sandbox.max_disk.is_some() { unsupported.push("max_disk"); }
167 if sandbox.on_exit != BranchAction::Commit { unsupported.push("on_exit"); }
168 if sandbox.on_error != BranchAction::Abort { unsupported.push("on_error"); }
169 if !sandbox.fs_mount.is_empty() { unsupported.push("fs_mount"); }
170 if sandbox.chroot.is_some() { unsupported.push("chroot"); }
171 if sandbox.clean_env { unsupported.push("clean_env"); }
172 if !sandbox.env.is_empty() { unsupported.push("env"); }
173 if sandbox.gpu_devices.is_some() { unsupported.push("gpu_devices"); }
174 if sandbox.cpu_cores.is_some() { unsupported.push("cpu_cores"); }
175 if sandbox.num_cpus.is_some() { unsupported.push("num_cpus"); }
176 if sandbox.port_remap { unsupported.push("port_remap"); }
177 if sandbox.user.is_some() { unsupported.push("user"); }
178 if sandbox.policy_fn.is_some() { unsupported.push("policy_fn"); }
179
180 if !unsupported.is_empty() {
181 return Err(SandboxError::UnsupportedForConfine(unsupported.join(", ")));
182 }
183
184 Ok(Self {
185 fs_writable: sandbox.fs_writable.clone(),
186 fs_readable: sandbox.fs_readable.clone(),
187 })
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
193pub enum BranchAction {
194 #[default]
195 Commit,
196 Abort,
197 Keep,
198}
199
200struct Runtime {
207 name: String,
208 state: RuntimeState,
209 child_pid: Option<i32>,
210 pidfd: Option<std::os::fd::OwnedFd>,
211 notif_handle: Option<JoinHandle<()>>,
212 throttle_handle: Option<JoinHandle<()>>,
213 loadavg_handle: Option<JoinHandle<()>>,
214 _stdout_read: Option<std::os::fd::OwnedFd>,
215 _stderr_read: Option<std::os::fd::OwnedFd>,
216 seccomp_cow: Option<crate::cow::seccomp::SeccompCowBranch>,
217 supervisor_resource: Option<Arc<tokio::sync::Mutex<crate::seccomp::state::ResourceState>>>,
218 supervisor_cow: Option<Arc<tokio::sync::Mutex<crate::seccomp::state::CowState>>>,
219 supervisor_network: Option<Arc<tokio::sync::Mutex<crate::seccomp::state::NetworkState>>>,
220 ctrl_fd: Option<std::os::fd::OwnedFd>,
221 stdout_pipe: Option<std::os::fd::OwnedFd>,
222 io_overrides: Option<(Option<i32>, Option<i32>, Option<i32>)>,
223 extra_fds: Vec<(i32, i32)>,
224 http_acl_handle: Option<crate::transparent_proxy::HttpAclProxyHandle>,
225 #[allow(clippy::type_complexity)]
226 on_bind: Option<Box<dyn Fn(&HashMap<u16, u16>) + Send + Sync>>,
227 handlers: Vec<(i64, Arc<dyn crate::seccomp::dispatch::Handler>)>,
228 ready_w: Option<std::os::fd::OwnedFd>,
229}
230
231enum RuntimeState {
233 Created,
234 Running,
235 Paused,
236 Stopped(crate::result::ExitStatus),
237}
238
239#[derive(Serialize, Deserialize)]
241pub struct Sandbox {
242 pub fs_writable: Vec<PathBuf>,
244 pub fs_readable: Vec<PathBuf>,
245 pub fs_denied: Vec<PathBuf>,
246
247 pub extra_deny_syscalls: Vec<String>,
249 pub extra_allow_syscalls: Vec<String>,
250
251 pub protection_policy: ProtectionPolicy,
262
263 pub net_allow: Vec<NetAllow>,
287 pub net_deny: Vec<NetDeny>,
290 pub net_allow_bind: Vec<u16>,
293 pub net_deny_bind: Vec<u16>,
297 pub http_allow: Vec<HttpRule>,
299 pub http_deny: Vec<HttpRule>,
300 pub http_ports: Vec<u16>,
303 pub http_ca: Option<PathBuf>,
305 pub http_key: Option<PathBuf>,
307 pub http_inject_ca: Vec<PathBuf>,
309 pub http_ca_out: Option<PathBuf>,
312
313 pub max_memory: Option<ByteSize>,
315 pub max_processes: u32,
316 pub max_open_files: Option<u32>,
317 pub max_cpu: Option<u8>,
318
319 pub random_seed: Option<u64>,
321 pub time_start: Option<SystemTime>,
322 pub no_randomize_memory: bool,
323 pub no_huge_pages: bool,
324 pub no_coredump: bool,
325 pub deterministic_dirs: bool,
326
327 pub workdir: Option<PathBuf>,
329 pub cwd: Option<PathBuf>,
330 pub fs_storage: Option<PathBuf>,
331 pub max_disk: Option<ByteSize>,
332 pub on_exit: BranchAction,
333 pub on_error: BranchAction,
334
335 pub fs_mount: Vec<(PathBuf, PathBuf)>,
337 pub fs_mount_ro: Vec<PathBuf>,
340
341 pub chroot: Option<PathBuf>,
343
344 #[serde(skip)]
352 pub in_child_main: Option<fn()>,
353
354 pub clean_env: bool,
355 pub env: HashMap<String, String>,
356 pub gpu_devices: Option<Vec<u32>>,
358
359 pub cpu_cores: Option<Vec<u32>>,
361 pub num_cpus: Option<u32>,
362 pub port_remap: bool,
363
364 pub no_supervisor: bool,
371
372 pub user: Option<RunAs>,
374
375 #[serde(skip)]
377 pub policy_fn: Option<crate::policy_fn::PolicyCallback>,
378
379 #[serde(skip)]
382 pub name: Option<String>,
383
384 #[serde(skip)]
387 init_fn: Option<Box<dyn FnOnce() + Send + 'static>>,
388
389 #[serde(skip)]
392 work_fn: Option<Arc<dyn Fn(u32) + Send + Sync + 'static>>,
393
394 #[serde(skip)]
396 runtime: Option<Box<Runtime>>,
397}
398
399impl std::fmt::Debug for Sandbox {
400 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
401 f.debug_struct("Sandbox")
402 .field("fs_readable", &self.fs_readable)
403 .field("fs_writable", &self.fs_writable)
404 .field("max_memory", &self.max_memory)
405 .field("max_processes", &self.max_processes)
406 .field("policy_fn", &self.policy_fn.as_ref().map(|_| "<callback>"))
407 .field("name", &self.name)
408 .field("runtime", &self.runtime.as_ref().map(|_| "<runtime>"))
409 .finish_non_exhaustive()
410 }
411}
412
413impl Clone for Sandbox {
414 fn clone(&self) -> Self {
425 Self {
426 fs_writable: self.fs_writable.clone(),
427 fs_readable: self.fs_readable.clone(),
428 fs_denied: self.fs_denied.clone(),
429 extra_deny_syscalls: self.extra_deny_syscalls.clone(),
430 extra_allow_syscalls: self.extra_allow_syscalls.clone(),
431 protection_policy: self.protection_policy.clone(),
432 net_allow: self.net_allow.clone(),
433 net_deny: self.net_deny.clone(),
434 net_allow_bind: self.net_allow_bind.clone(),
435 net_deny_bind: self.net_deny_bind.clone(),
436 http_allow: self.http_allow.clone(),
437 http_deny: self.http_deny.clone(),
438 http_ports: self.http_ports.clone(),
439 http_ca: self.http_ca.clone(),
440 http_key: self.http_key.clone(),
441 http_inject_ca: self.http_inject_ca.clone(),
442 http_ca_out: self.http_ca_out.clone(),
443 max_memory: self.max_memory,
444 max_processes: self.max_processes,
445 max_open_files: self.max_open_files,
446 max_cpu: self.max_cpu,
447 random_seed: self.random_seed,
448 time_start: self.time_start,
449 no_randomize_memory: self.no_randomize_memory,
450 no_huge_pages: self.no_huge_pages,
451 no_coredump: self.no_coredump,
452 deterministic_dirs: self.deterministic_dirs,
453 workdir: self.workdir.clone(),
454 cwd: self.cwd.clone(),
455 fs_storage: self.fs_storage.clone(),
456 max_disk: self.max_disk,
457 on_exit: self.on_exit.clone(),
458 on_error: self.on_error.clone(),
459 fs_mount: self.fs_mount.clone(),
460 fs_mount_ro: self.fs_mount_ro.clone(),
461 chroot: self.chroot.clone(),
462 in_child_main: self.in_child_main,
463 clean_env: self.clean_env,
464 env: self.env.clone(),
465 gpu_devices: self.gpu_devices.clone(),
466 cpu_cores: self.cpu_cores.clone(),
467 num_cpus: self.num_cpus,
468 port_remap: self.port_remap,
469 no_supervisor: self.no_supervisor,
470 user: self.user,
471 policy_fn: self.policy_fn.clone(),
472 name: self.name.clone(),
473 init_fn: None,
476 work_fn: self.work_fn.clone(),
478 runtime: None,
480 }
481 }
482}
483
484impl Sandbox {
485 pub fn builder() -> SandboxBuilder {
486 SandboxBuilder::default()
487 }
488
489 pub fn allows_sysv_ipc(&self) -> bool {
491 self.extra_allow_syscalls.iter().any(|s| s == "sysv_ipc")
492 }
493
494 pub fn validate(&self) -> Result<(), SandboxError> {
499 Ok(())
500 }
501
502 pub fn active_protections(&self) -> Result<Vec<(Protection, ProtectionStatus)>, crate::error::SandlockError> {
506 let host_abi = crate::landlock::abi_version().map_err(|e| {
507 crate::error::SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(e))
508 })?;
509 Ok(Protection::all()
510 .map(|p| (p, ProtectionStatus::resolve(p, host_abi, &self.protection_policy)))
511 .collect())
512 }
513
514 fn rt(&self) -> &Runtime {
519 self.runtime.as_ref().expect("sandbox not started")
520 }
521
522 fn rt_mut(&mut self) -> &mut Runtime {
523 self.runtime.as_mut().expect("sandbox not started")
524 }
525
526 pub fn set_name(&mut self, name: impl Into<String>) {
533 self.name = Some(name.into());
534 }
535
536 pub fn with_name(mut self, name: impl Into<String>) -> Self {
546 self.name = Some(name.into());
547 self
548 }
549
550 pub fn with_init_fn(mut self, f: impl FnOnce() + Send + 'static) -> Self {
555 self.init_fn = Some(Box::new(f));
556 self
557 }
558
559 pub fn with_work_fn(mut self, f: impl Fn(u32) + Send + Sync + 'static) -> Self {
563 self.work_fn = Some(Arc::new(f));
564 self
565 }
566
567 pub fn instance_name(&self) -> Option<&str> {
569 self.runtime.as_ref().map(|r| r.name.as_str())
570 .or_else(|| self.name.as_deref())
571 }
572
573 pub fn pid(&self) -> Option<i32> {
575 self.runtime.as_ref().and_then(|r| r.child_pid)
576 }
577
578 pub fn is_running(&self) -> bool {
580 self.runtime.as_ref().map(|r| {
581 matches!(r.state, RuntimeState::Running | RuntimeState::Paused)
582 }).unwrap_or(false)
583 }
584
585 pub fn pause(&mut self) -> Result<(), crate::error::SandlockError> {
587 use crate::error::SandboxRuntimeError;
588 let pid = self.runtime.as_ref()
589 .and_then(|rt| rt.child_pid)
590 .ok_or(SandboxRuntimeError::NotRunning)?;
591 let ret = unsafe { libc::killpg(pid, libc::SIGSTOP) };
592 if ret < 0 {
593 return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
594 }
595 self.rt_mut().state = RuntimeState::Paused;
596 Ok(())
597 }
598
599 pub fn resume(&mut self) -> Result<(), crate::error::SandlockError> {
601 use crate::error::SandboxRuntimeError;
602 let pid = self.runtime.as_ref()
603 .and_then(|rt| rt.child_pid)
604 .ok_or(SandboxRuntimeError::NotRunning)?;
605 let ret = unsafe { libc::killpg(pid, libc::SIGCONT) };
606 if ret < 0 {
607 return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
608 }
609 self.rt_mut().state = RuntimeState::Running;
610 Ok(())
611 }
612
613 pub fn kill(&mut self) -> Result<(), crate::error::SandlockError> {
615 use crate::error::SandboxRuntimeError;
616 let pid = self.runtime.as_ref()
617 .and_then(|rt| rt.child_pid)
618 .ok_or(SandboxRuntimeError::NotRunning)?;
619 let ret = unsafe { libc::killpg(pid, libc::SIGKILL) };
620 if ret < 0 {
621 let err = std::io::Error::last_os_error();
622 if err.raw_os_error() != Some(libc::ESRCH) {
623 return Err(SandboxRuntimeError::Io(err).into());
624 }
625 }
626 Ok(())
627 }
628
629 pub fn set_on_bind(&mut self, cb: impl Fn(&HashMap<u16, u16>) + Send + Sync + 'static) {
631 let _ = self.ensure_runtime();
634 self.rt_mut().on_bind = Some(Box::new(cb));
635 }
636
637 pub async fn port_mappings(&self) -> HashMap<u16, u16> {
639 if let Some(ref rt) = self.runtime {
640 if let Some(ref net) = rt.supervisor_network {
641 let ns = net.lock().await;
642 return ns.port_map.virtual_to_real.clone();
643 }
644 }
645 HashMap::new()
646 }
647
648 pub async fn wait(&mut self) -> Result<crate::result::RunResult, crate::error::SandlockError> {
650 use crate::error::SandboxRuntimeError;
651 use crate::result::RunResult;
652
653 let pid = self.rt().child_pid.ok_or(SandboxRuntimeError::NotRunning)?;
654
655 if let RuntimeState::Stopped(ref es) = self.rt().state {
656 return Ok(RunResult {
657 exit_status: es.clone(),
658 stdout: None,
659 stderr: None,
660 });
661 }
662
663 let exit_status = match self.rt_mut().pidfd.take() {
672 Some(pidfd) => wait_child_exit_via_pidfd(pidfd, pid).await,
673 None => wait_child_exit_blocking(pid).await,
674 };
675
676 self.rt_mut().state = RuntimeState::Stopped(exit_status.clone());
677
678 let rt = self.rt_mut();
679 if let Some(h) = rt.notif_handle.take() { h.abort(); }
680 if let Some(h) = rt.throttle_handle.take() { h.abort(); }
681 if let Some(h) = rt.loadavg_handle.take() { h.abort(); }
682
683 if let Some(ref cow_state) = self.rt().supervisor_cow.clone() {
684 let mut cow = cow_state.lock().await;
685 self.rt_mut().seccomp_cow = cow.branch.take();
686 }
687
688 let stdout = self.rt_mut()._stdout_read.take().map(sandbox_read_fd_to_end);
689 let stderr = self.rt_mut()._stderr_read.take().map(sandbox_read_fd_to_end);
690
691 Ok(RunResult { exit_status, stdout, stderr })
692 }
693
694 pub async fn create(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
700 self.do_create(cmd, true).await
701 }
702
703 pub async fn create_interactive(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
705 self.do_create(cmd, false).await
706 }
707
708 pub fn start(&mut self) -> Result<(), crate::error::SandlockError> {
712 self.do_start()
713 }
714
715 pub async fn spawn(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
721 self.create(cmd).await?;
722 self.start()?;
723 self.wait_until_exec().await
724 }
725
726 pub async fn spawn_interactive(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
728 self.create_interactive(cmd).await?;
729 self.start()?;
730 self.wait_until_exec().await
731 }
732
733 pub async fn restore_interactive(
750 &mut self,
751 cp: &crate::checkpoint::Checkpoint,
752 ) -> Result<Vec<String>, crate::error::SandlockError> {
753 use crate::error::SandboxRuntimeError;
754
755 let exe = if cp.process_state.exe.is_empty() {
761 "/bin/true".to_string()
762 } else {
763 cp.process_state.exe.clone()
764 };
765 self.create_interactive(&[exe.as_str()]).await?;
766 let pid = self.pid().ok_or(SandboxRuntimeError::NotRunning)?;
767
768 let cp = cp.clone();
776 let skipped = tokio::task::spawn_blocking(
777 move || -> Result<Vec<String>, crate::error::SandlockError> {
778 crate::checkpoint::capture::ptrace_seize(pid).map_err(|e| {
780 SandboxRuntimeError::Child(format!("restore ptrace seize {pid}: {e}"))
781 })?;
782 let skipped = match crate::checkpoint::resume::restore_into(pid, &cp) {
787 Ok(s) => s,
788 Err(e) => {
789 let _ = crate::checkpoint::capture::ptrace_detach(pid);
790 return Err(e);
791 }
792 };
793 crate::checkpoint::capture::ptrace_detach(pid).map_err(|e| {
797 SandboxRuntimeError::Child(format!("restore ptrace detach {pid}: {e}"))
798 })?;
799 Ok(skipped)
800 },
801 )
802 .await
803 .map_err(|e| SandboxRuntimeError::Child(format!("restore join error: {e}")))??;
804
805 Ok(skipped)
806 }
807
808 async fn wait_until_exec(&self) -> Result<(), crate::error::SandlockError> {
813 use crate::error::SandboxRuntimeError;
814 let pid = self.pid().ok_or(SandboxRuntimeError::NotRunning)?;
815 let Some(our_exe) = std::fs::read_link("/proc/self/exe").ok() else {
816 return Ok(());
817 };
818 let child_link = format!("/proc/{}/exe", pid);
819 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
820 loop {
821 if let Ok(child_exe) = std::fs::read_link(&child_link) {
822 if child_exe != our_exe {
823 return Ok(());
824 }
825 }
826 if std::time::Instant::now() >= deadline {
827 return Err(SandboxRuntimeError::Child(
828 "child did not exec() within 5s".into(),
829 ).into());
830 }
831 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
832 }
833 }
834
835 #[doc(hidden)]
838 pub async fn create_with_io(
839 &mut self,
840 cmd: &[&str],
841 stdin_fd: Option<std::os::unix::io::RawFd>,
842 stdout_fd: Option<std::os::unix::io::RawFd>,
843 stderr_fd: Option<std::os::unix::io::RawFd>,
844 ) -> Result<(), crate::error::SandlockError> {
845 self.ensure_runtime()?;
846 self.rt_mut().io_overrides = Some((stdin_fd, stdout_fd, stderr_fd));
847 self.do_create(cmd, false).await
848 }
849
850 #[doc(hidden)]
852 pub async fn create_with_gather_io(
853 &mut self,
854 cmd: &[&str],
855 stdin_fd: Option<std::os::unix::io::RawFd>,
856 stdout_fd: Option<std::os::unix::io::RawFd>,
857 stderr_fd: Option<std::os::unix::io::RawFd>,
858 extra_fds: Vec<(i32, i32)>,
859 ) -> Result<(), crate::error::SandlockError> {
860 self.ensure_runtime()?;
861 self.rt_mut().io_overrides = Some((stdin_fd, stdout_fd, stderr_fd));
862 self.rt_mut().extra_fds = extra_fds;
863 self.do_create(cmd, false).await
864 }
865
866 pub async fn create_with_in_child_main(
877 &mut self,
878 name: &str,
879 extra_fds: Vec<(i32, i32)>,
880 entrypoint: fn(),
881 ) -> Result<(), crate::error::SandlockError> {
882 self.ensure_runtime()?;
883 self.in_child_main = Some(entrypoint);
884 self.rt_mut().extra_fds = extra_fds;
885 self.do_create(&[name], false).await
886 }
887
888 pub(crate) async fn freeze(&self) -> Result<(), crate::error::SandlockError> {
890 use crate::error::{SandboxRuntimeError, SandlockError};
891 let rt = self.runtime.as_ref().ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
892 let pid = rt.child_pid.ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
893 if let Some(ref resource) = rt.supervisor_resource {
894 let mut rs = resource.lock().await;
895 rs.hold_forks = true;
896 }
897 unsafe { libc::killpg(pid, libc::SIGSTOP); }
898 Ok(())
899 }
900
901 pub(crate) async fn thaw(&self) -> Result<(), crate::error::SandlockError> {
903 use crate::error::{SandboxRuntimeError, SandlockError};
904 let rt = self.runtime.as_ref().ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
905 let pid = rt.child_pid.ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
906 if let Some(ref resource) = rt.supervisor_resource {
907 let mut rs = resource.lock().await;
908 rs.hold_forks = false;
909 rs.held_notif_ids.clear();
910 }
911 unsafe { libc::killpg(pid, libc::SIGCONT); }
912 Ok(())
913 }
914
915 pub async fn checkpoint(&self) -> Result<crate::checkpoint::Checkpoint, crate::error::SandlockError> {
917 use crate::error::{SandboxRuntimeError, SandlockError};
918 let pid = self.runtime.as_ref()
919 .and_then(|rt| rt.child_pid)
920 .ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
921 self.checkpoint_pid(pid).await
922 }
923
924 pub async fn checkpoint_pid(&self, target_pid: i32) -> Result<crate::checkpoint::Checkpoint, crate::error::SandlockError> {
929 use crate::error::{SandboxRuntimeError, SandlockError};
930 if target_pid <= 0 {
931 return Err(SandlockError::Runtime(SandboxRuntimeError::NotRunning));
932 }
933 self.freeze().await?;
934 let cp = crate::checkpoint::capture(target_pid, self);
935 self.thaw().await?;
936 cp
937 }
938
939 pub async fn run(
954 &mut self,
955 cmd: &[&str],
956 ) -> Result<crate::result::RunResult, crate::error::SandlockError> {
957 self.do_create(cmd, true).await?;
958 self.do_start()?;
959 self.wait().await
960 }
961
962 pub async fn run_interactive(
964 &mut self,
965 cmd: &[&str],
966 ) -> Result<crate::result::RunResult, crate::error::SandlockError> {
967 self.do_create(cmd, false).await?;
968 self.do_start()?;
969 self.wait().await
970 }
971
972 pub async fn run_with_handlers<I, S, H>(
974 &mut self,
975 cmd: &[&str],
976 handlers: I,
977 ) -> Result<crate::result::RunResult, crate::error::SandlockError>
978 where
979 I: IntoIterator<Item = (S, H)>,
980 S: TryInto<crate::seccomp::syscall::Syscall, Error = crate::seccomp::syscall::SyscallError>,
981 H: crate::seccomp::dispatch::Handler,
982 {
983 let pending = sandbox_collect_handlers(handlers, self)?;
984 self.ensure_runtime()?;
985 self.rt_mut().handlers = pending;
986 self.do_create(cmd, true).await?;
987 self.do_start()?;
988 self.wait().await
989 }
990
991 pub async fn run_interactive_with_handlers<I, S, H>(
993 &mut self,
994 cmd: &[&str],
995 handlers: I,
996 ) -> Result<crate::result::RunResult, crate::error::SandlockError>
997 where
998 I: IntoIterator<Item = (S, H)>,
999 S: TryInto<crate::seccomp::syscall::Syscall, Error = crate::seccomp::syscall::SyscallError>,
1000 H: crate::seccomp::dispatch::Handler,
1001 {
1002 let pending = sandbox_collect_handlers(handlers, self)?;
1003 self.ensure_runtime()?;
1004 self.rt_mut().handlers = pending;
1005 self.do_create(cmd, false).await?;
1006 self.do_start()?;
1007 self.wait().await
1008 }
1009
1010 pub async fn dry_run(
1012 &mut self,
1013 cmd: &[&str],
1014 ) -> Result<crate::dry_run::DryRunResult, crate::error::SandlockError> {
1015 self.on_exit = BranchAction::Keep;
1016 self.on_error = BranchAction::Keep;
1017 self.do_create(cmd, true).await?;
1018 self.do_start()?;
1019 let run_result = self.wait().await?;
1020 let changes = self.collect_changes().await;
1021 self.do_abort().await;
1022 Ok(crate::dry_run::DryRunResult { run_result, changes })
1023 }
1024
1025 pub async fn dry_run_interactive(
1027 &mut self,
1028 cmd: &[&str],
1029 ) -> Result<crate::dry_run::DryRunResult, crate::error::SandlockError> {
1030 self.on_exit = BranchAction::Keep;
1031 self.on_error = BranchAction::Keep;
1032 self.do_create(cmd, false).await?;
1033 self.do_start()?;
1034 let run_result = self.wait().await?;
1035 let changes = self.collect_changes().await;
1036 self.do_abort().await;
1037 Ok(crate::dry_run::DryRunResult { run_result, changes })
1038 }
1039
1040 pub async fn fork(&mut self, n: u32) -> Result<Vec<Sandbox>, crate::error::SandlockError> {
1046 use crate::error::SandboxRuntimeError;
1047 use std::os::fd::{FromRawFd, OwnedFd};
1048
1049 let init_fn = self.init_fn.take()
1052 .ok_or_else(|| SandboxRuntimeError::Child("fork() requires init_fn and work_fn — use SandboxBuilder::init_fn() / work_fn() or Sandbox::with_init_fn() / with_work_fn()".into()))?;
1053 let work_fn = self.work_fn.take()
1054 .ok_or_else(|| SandboxRuntimeError::Child("fork() requires init_fn and work_fn — use SandboxBuilder::init_fn() / work_fn() or Sandbox::with_init_fn() / with_work_fn()".into()))?;
1055
1056 self.ensure_runtime()?;
1058
1059 let sandbox_cfg = self.clone(); let mut ctrl_fds = [0i32; 2];
1062 if unsafe { libc::pipe2(ctrl_fds.as_mut_ptr(), 0) } < 0 {
1063 return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1064 }
1065 let ctrl_parent = unsafe { OwnedFd::from_raw_fd(ctrl_fds[0]) };
1066 let ctrl_child_fd = ctrl_fds[1];
1067
1068 let mut pipe_read_ends: Vec<OwnedFd> = Vec::with_capacity(n as usize);
1069 let mut pipe_write_fds: Vec<i32> = Vec::with_capacity(n as usize);
1070 for _ in 0..n {
1071 let mut pfds = [0i32; 2];
1072 if unsafe { libc::pipe(pfds.as_mut_ptr()) } >= 0 {
1073 pipe_read_ends.push(unsafe { OwnedFd::from_raw_fd(pfds[0]) });
1074 pipe_write_fds.push(pfds[1]);
1075 } else {
1076 pipe_write_fds.push(-1);
1077 }
1078 }
1079
1080 let pid = unsafe { libc::fork() };
1081 if pid < 0 {
1082 unsafe { libc::close(ctrl_child_fd) };
1083 return Err(SandboxRuntimeError::Fork(std::io::Error::last_os_error()).into());
1084 }
1085
1086 if pid == 0 {
1087 drop(ctrl_parent);
1088 unsafe { libc::setpgid(0, 0) };
1089 unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) };
1090 unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
1091
1092 let _ = crate::landlock::confine(&sandbox_cfg);
1093
1094 let deny = crate::context::blocklist_syscall_numbers(&sandbox_cfg);
1095 let args = crate::context::arg_filters(&sandbox_cfg);
1096 let filter = match crate::seccomp::bpf::assemble_filter(&[], &deny, &args) {
1097 Ok(f) => f,
1098 Err(_) => unsafe { libc::_exit(1) },
1099 };
1100 let _ = crate::seccomp::bpf::install_deny_filter(&filter);
1101
1102 init_fn();
1103
1104 drop(pipe_read_ends);
1105 crate::fork::fork_ready_loop_fn(ctrl_child_fd, n, &*work_fn, &pipe_write_fds);
1106 unsafe { libc::_exit(0) };
1107 }
1108
1109 unsafe { libc::close(ctrl_child_fd) };
1110 for wfd in &pipe_write_fds {
1111 if *wfd >= 0 { unsafe { libc::close(*wfd) }; }
1112 }
1113 self.rt_mut().child_pid = Some(pid);
1114 self.rt_mut().state = RuntimeState::Running;
1115
1116 let ctrl_fd = ctrl_parent.as_raw_fd();
1117 let mut pid_buf = vec![0u8; n as usize * 4];
1118 sandbox_read_exact(ctrl_fd, &mut pid_buf);
1119
1120 let clone_pids: Vec<i32> = pid_buf.chunks(4)
1121 .map(|c| u32::from_be_bytes(c.try_into().unwrap_or([0; 4])) as i32)
1122 .collect();
1123 let live_count = clone_pids.iter().filter(|&&p| p > 0).count();
1124
1125 let mut code_buf = vec![0u8; live_count * 4];
1126 sandbox_read_exact(ctrl_fd, &mut code_buf);
1127 self.rt_mut().ctrl_fd = Some(ctrl_parent);
1128
1129 let mut status = 0i32;
1130 unsafe { libc::waitpid(pid, &mut status, 0) };
1131
1132 let mut code_idx = 0;
1133 let mut clones = Vec::with_capacity(live_count);
1134 let mut pipe_iter = pipe_read_ends.into_iter();
1135
1136 let rt_name = self.rt().name.clone();
1137 for &clone_pid in &clone_pids {
1138 let pipe = pipe_iter.next();
1139 if clone_pid <= 0 { continue; }
1140
1141 let code = i32::from_be_bytes(
1142 code_buf[code_idx * 4..(code_idx + 1) * 4].try_into().unwrap_or([0; 4])
1143 );
1144 code_idx += 1;
1145
1146 let mut clone_sb = sandbox_cfg.clone();
1147 let clone_name = format!("{}-fork-{}", rt_name, clone_pid);
1148 clone_sb.runtime = Some(Box::new(Runtime {
1149 name: clone_name,
1150 state: RuntimeState::Stopped(if code == 0 {
1151 crate::result::ExitStatus::Code(0)
1152 } else if code > 0 {
1153 crate::result::ExitStatus::Code(code)
1154 } else {
1155 crate::result::ExitStatus::Killed
1156 }),
1157 child_pid: Some(clone_pid),
1158 pidfd: None,
1159 notif_handle: None,
1160 throttle_handle: None,
1161 loadavg_handle: None,
1162 _stdout_read: None,
1163 _stderr_read: None,
1164 seccomp_cow: None,
1165 supervisor_resource: None,
1166 supervisor_cow: None,
1167 supervisor_network: None,
1168 ctrl_fd: None,
1169 stdout_pipe: pipe,
1170 io_overrides: None,
1171 extra_fds: Vec::new(),
1172 http_acl_handle: None,
1173 on_bind: None,
1174 handlers: Vec::new(),
1175 ready_w: None,
1176 }));
1177 clones.push(clone_sb);
1178 }
1179
1180 Ok(clones)
1181 }
1182
1183 pub async fn reduce(
1185 &self,
1186 cmd: &[&str],
1187 clones: &mut [Sandbox],
1188 ) -> Result<crate::result::RunResult, crate::error::SandlockError> {
1189 use crate::error::SandboxRuntimeError;
1190
1191 let mut combined = Vec::new();
1192 for clone in clones.iter_mut() {
1193 if let Some(ref mut rt) = clone.runtime {
1194 if let Some(pipe) = rt.stdout_pipe.take() {
1195 combined.extend_from_slice(&sandbox_read_fd_to_end(pipe));
1196 }
1197 }
1198 }
1199
1200 let mut stdin_fds = [0i32; 2];
1201 if unsafe { libc::pipe2(stdin_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
1202 return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1203 }
1204
1205 let write_fd = stdin_fds[1];
1206 let write_handle = tokio::task::spawn_blocking(move || {
1207 unsafe {
1208 libc::write(write_fd, combined.as_ptr() as *const _, combined.len());
1209 libc::close(write_fd);
1210 }
1211 });
1212
1213 let base_name = self.instance_name()
1214 .unwrap_or("sandbox")
1215 .to_owned();
1216 let reducer_name = base_name + "-reduce";
1217 let mut reducer = self.clone().with_name(reducer_name);
1218 reducer.ensure_runtime()?;
1219 reducer.rt_mut().io_overrides = Some((Some(stdin_fds[0]), None, None));
1220 reducer.do_create(cmd, true).await?;
1221 reducer.do_start()?;
1222 unsafe { libc::close(stdin_fds[0]) };
1223
1224 let _ = write_handle.await;
1225 reducer.wait().await
1226 }
1227
1228 pub(crate) fn has_unix_fs_gate(&self) -> bool {
1234 !self.fs_readable.is_empty() || !self.fs_writable.is_empty()
1235 }
1236
1237 fn ensure_runtime(&mut self) -> Result<(), crate::error::SandlockError> {
1243 if self.runtime.is_some() {
1244 return Ok(());
1245 }
1246 let name = sandbox_resolve_name(self.name.as_deref())?;
1247 self.runtime = Some(Box::new(Runtime {
1248 name,
1249 state: RuntimeState::Created,
1250 child_pid: None,
1251 pidfd: None,
1252 notif_handle: None,
1253 throttle_handle: None,
1254 loadavg_handle: None,
1255 _stdout_read: None,
1256 _stderr_read: None,
1257 seccomp_cow: None,
1258 supervisor_resource: None,
1259 supervisor_cow: None,
1260 supervisor_network: None,
1261 ctrl_fd: None,
1262 stdout_pipe: None,
1263 io_overrides: None,
1264 extra_fds: Vec::new(),
1265 http_acl_handle: None,
1266 on_bind: None,
1267 handlers: Vec::new(),
1268 ready_w: None,
1269 }));
1270 Ok(())
1271 }
1272
1273 async fn collect_changes(&self) -> Vec<crate::dry_run::Change> {
1278 if let Some(ref rt) = self.runtime {
1279 if let Some(ref cow) = rt.seccomp_cow {
1280 return cow.changes().unwrap_or_default();
1281 }
1282 }
1283 Vec::new()
1284 }
1285
1286 async fn do_abort(&mut self) {
1287 if let Some(ref mut rt) = self.runtime {
1288 if let Some(ref mut cow) = rt.seccomp_cow {
1289 let _ = cow.abort();
1290 }
1291 }
1292 }
1293
1294 async fn do_create(&mut self, cmd: &[&str], capture: bool) -> Result<(), crate::error::SandlockError> {
1300 use std::ffi::CString;
1301 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
1302 use crate::error::SandboxRuntimeError;
1303 use crate::context::{PipePair, read_u32_fd};
1304 use crate::network;
1305 use crate::seccomp::ctx::SupervisorCtx;
1306 use crate::seccomp::notif::{self, NotifPolicy};
1307 use crate::seccomp::state::{ChrootState, CowState, NetworkState, PolicyFnState, ProcfsState, ResourceState, TimeRandomState};
1308 use crate::sys::syscall;
1309 use std::time::Duration;
1310
1311 self.ensure_runtime()?;
1312
1313 if !matches!(self.rt().state, RuntimeState::Created) {
1314 return Err(SandboxRuntimeError::Child("sandbox already spawned".into()).into());
1315 }
1316
1317 if cmd.is_empty() {
1318 return Err(SandboxRuntimeError::Child("empty command".into()).into());
1319 }
1320
1321 let chroot_root = crate::chroot::resolve::resolve_chroot_root(self.chroot.as_deref())?;
1325
1326 if !self.http_inject_ca.is_empty() {
1331 let mounts = crate::chroot::resolve::resolve_chroot_mounts(&self.fs_mount);
1332 for p in &self.http_inject_ca {
1333 let host = resolve_sandbox_path_to_host(p, chroot_root.as_deref(), &mounts);
1334 if !host.exists() {
1335 return Err(SandboxRuntimeError::Child(format!(
1336 "--http-inject-ca {:?} not found in the sandbox view (resolved to {:?}); \
1337 the CA cannot be injected into it. Point it at the trust bundle the \
1338 workload actually reads (e.g. /etc/ssl/certs/ca-certificates.crt, or \
1339 certifi's cacert.pem).",
1340 p, host
1341 ))
1342 .into());
1343 }
1344 }
1345 }
1346
1347 let c_cmd: Vec<CString> = cmd
1348 .iter()
1349 .map(|s| CString::new(*s).map_err(|_| SandboxRuntimeError::Child("invalid command string".into())))
1350 .collect::<Result<Vec<_>, _>>()?;
1351
1352 let no_supervisor = self.no_supervisor;
1353
1354 let pipes = PipePair::new().map_err(SandboxRuntimeError::Io)?;
1355
1356 let resolved_net_allow = network::resolve_net_allow(&self.net_allow)
1357 .await
1358 .map_err(SandboxRuntimeError::Io)?;
1359 let virtual_etc_hosts = network::compose_virtual_etc_hosts(
1366 self.chroot.as_deref(),
1367 &resolved_net_allow.concrete_host_entries,
1368 );
1369
1370 let mut ca_inject_pem: Option<std::sync::Arc<Vec<u8>>> = None;
1371 if !self.http_allow.is_empty() || !self.http_deny.is_empty() {
1372 let generate = !self.http_inject_ca.is_empty();
1374 let ca_material = crate::transparent_proxy::resolve_ca(
1375 self.http_ca.as_deref(),
1376 self.http_key.as_deref(),
1377 generate,
1378 )
1379 .map_err(SandboxRuntimeError::Io)?;
1380
1381 if let (Some(out), Some(cm)) = (self.http_ca_out.as_deref(), ca_material.as_ref()) {
1383 std::fs::write(out, cm.cert_pem.as_bytes()).map_err(SandboxRuntimeError::Io)?;
1384 }
1385
1386 if !self.http_inject_ca.is_empty() {
1388 if let Some(cm) = ca_material.as_ref() {
1389 ca_inject_pem = Some(std::sync::Arc::new(cm.cert_pem.clone().into_bytes()));
1390 }
1391 }
1392
1393 let (cert_pem, key_pem) = match ca_material.as_ref() {
1394 Some(cm) => (Some(cm.cert_pem.as_str()), Some(cm.key_pem.as_str())),
1395 None => (None, None),
1396 };
1397
1398 let handle = crate::transparent_proxy::spawn_transparent_proxy(
1399 self.http_allow.clone(),
1400 self.http_deny.clone(),
1401 cert_pem,
1402 key_pem,
1403 )
1404 .await
1405 .map_err(SandboxRuntimeError::Io)?;
1406 self.rt_mut().http_acl_handle = Some(handle);
1407 }
1408
1409 let seccomp_cow_branch = if !no_supervisor && self.workdir.is_some() {
1415 let workdir = self.workdir.as_ref().unwrap().clone();
1416 let storage = self.fs_storage.clone();
1417 let max_disk = self.max_disk.map(|b| b.0).unwrap_or(0);
1418 match crate::cow::seccomp::SeccompCowBranch::create(&workdir, storage.as_deref(), max_disk) {
1419 Ok(branch) => {
1420 self.fs_readable.push(branch.upper_dir().to_path_buf());
1421 Some(branch)
1422 }
1423 Err(e) => {
1424 eprintln!("sandlock: seccomp COW branch creation failed: {}", e);
1425 None
1426 }
1427 }
1428 } else {
1429 None
1430 };
1431
1432 let handler_syscalls: Vec<i64> = self.rt().handlers.iter().map(|(nr, _)| *nr).collect();
1433 let resolved_sandbox_name = self.rt().name.clone();
1434 let resolved = crate::resolved::ResolvedSandbox::from_sandbox(
1435 self,
1436 Some(resolved_sandbox_name.as_str()),
1437 &handler_syscalls,
1438 );
1439
1440 let (stdout_r, stderr_r) = if capture {
1441 let mut stdout_fds = [0i32; 2];
1442 let mut stderr_fds = [0i32; 2];
1443 if unsafe { libc::pipe2(stdout_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
1444 return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1445 }
1446 if unsafe { libc::pipe2(stderr_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
1447 unsafe {
1448 libc::close(stdout_fds[0]);
1449 libc::close(stdout_fds[1]);
1450 }
1451 return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1452 }
1453 (
1454 Some((
1455 unsafe { OwnedFd::from_raw_fd(stdout_fds[0]) },
1456 unsafe { OwnedFd::from_raw_fd(stdout_fds[1]) },
1457 )),
1458 Some((
1459 unsafe { OwnedFd::from_raw_fd(stderr_fds[0]) },
1460 unsafe { OwnedFd::from_raw_fd(stderr_fds[1]) },
1461 )),
1462 )
1463 } else {
1464 (None, None)
1465 };
1466
1467 let parent_pid = unsafe { libc::getpid() };
1470
1471 let pid = unsafe { libc::fork() };
1472 if pid < 0 {
1473 return Err(SandboxRuntimeError::Fork(std::io::Error::last_os_error()).into());
1474 }
1475
1476 if pid == 0 {
1477 let io_overrides = self.rt().io_overrides;
1479 if let Some((stdin_fd, stdout_fd, stderr_fd)) = io_overrides {
1480 if let Some(fd) = stdin_fd { unsafe { libc::dup2(fd, 0) }; }
1481 if let Some(fd) = stdout_fd { unsafe { libc::dup2(fd, 1) }; }
1482 if let Some(fd) = stderr_fd { unsafe { libc::dup2(fd, 2) }; }
1483 }
1484
1485 let extra_fds_copy = self.rt().extra_fds.clone();
1486 for &(target_fd, source_fd) in &extra_fds_copy {
1487 unsafe { libc::dup2(source_fd, target_fd) };
1488 }
1489
1490 if let Some((_, ref stdout_w)) = stdout_r {
1491 unsafe { libc::dup2(stdout_w.as_raw_fd(), 1) };
1492 }
1493 if let Some((_, ref stderr_w)) = stderr_r {
1494 unsafe { libc::dup2(stderr_w.as_raw_fd(), 2) };
1495 }
1496 drop(stdout_r);
1497 drop(stderr_r);
1498
1499 let gather_keep_fds: Vec<i32> = extra_fds_copy.iter().map(|&(target, _)| target).collect();
1500
1501 let extra_syscalls: Vec<u32> = self.rt().handlers
1502 .iter()
1503 .map(|h| h.0 as u32)
1504 .collect();
1505
1506 let sandbox_name = self.rt().name.clone();
1507 let entry = match self.in_child_main {
1510 Some(run) => context::ChildEntry::InProcess { name: c_cmd[0].as_c_str(), run },
1511 None => context::ChildEntry::Exec(&c_cmd),
1512 };
1513 context::confine_child(context::ChildSpawnArgs {
1514 sandbox: self,
1515 entry,
1516 pipes: &pipes,
1517 no_supervisor,
1518 keep_fds: &gather_keep_fds,
1519 sandbox_name: Some(sandbox_name.as_str()),
1520 extra_syscalls: &extra_syscalls,
1521 parent_pid,
1522 });
1523 }
1524
1525 drop(pipes.notif_w);
1527 drop(pipes.ready_r);
1528
1529 self.rt_mut()._stdout_read = stdout_r.map(|(r, _w)| r);
1530 self.rt_mut()._stderr_read = stderr_r.map(|(r, _w)| r);
1531
1532 self.rt_mut().child_pid = Some(pid);
1533 let pidfd = match syscall::pidfd_open(pid as u32, 0) {
1537 Ok(fd) => Some(fd),
1538 Err(_) => None,
1539 };
1540
1541 let notif_fd_num = read_u32_fd(pipes.notif_r.as_raw_fd())
1542 .map_err(|e| SandboxRuntimeError::Child(format!("read notif fd from child: {}", e)))?;
1543
1544 let is_nested_mode = notif_fd_num == 0;
1545
1546 let notif_fd = if is_nested_mode {
1547 None
1548 } else if let Some(ref pfd) = pidfd {
1549 Some(syscall::pidfd_getfd(pfd, notif_fd_num as i32, 0)
1550 .map_err(|e| SandboxRuntimeError::Child(format!("pidfd_getfd: {}", e)))?)
1551 } else {
1552 let path = format!("/proc/{}/fd/{}", pid, notif_fd_num);
1553 let cpath = CString::new(path).unwrap();
1554 let raw = unsafe { libc::open(cpath.as_ptr(), libc::O_RDWR) };
1555 if raw < 0 {
1556 return Err(SandboxRuntimeError::Child("failed to open notif fd from /proc".into()).into());
1557 }
1558 Some(unsafe { OwnedFd::from_raw_fd(raw) })
1559 };
1560
1561 if let Some(notif_fd) = notif_fd {
1562 if self.time_start.is_some() || self.random_seed.is_some() {
1563 let time_offset = self.time_start.map(|t| crate::time::calculate_time_offset(t));
1564 if let Err(e) = crate::vdso::patch(pid, time_offset, self.random_seed.is_some()) {
1565 eprintln!("sandlock: pre-exec vDSO patching failed (will retry after exec): {}", e);
1566 }
1567 }
1568
1569 let time_offset_val = self.time_start
1570 .map(|t| crate::time::calculate_time_offset(t))
1571 .unwrap_or(0);
1572
1573 let rt_name = self.rt().name.clone();
1574 let notif_policy = NotifPolicy {
1575 max_memory_bytes: self.max_memory.map(|m| m.0).unwrap_or(0),
1576 max_processes: self.max_processes,
1577 has_memory_limit: resolved.features.memory_limit,
1578 has_net_destination_policy: resolved.features.network_destination_policy,
1579 has_bind_denylist: resolved.features.bind_denylist,
1580 has_unix_fs_gate: resolved.features.unix_fs_gate,
1581 has_random_seed: resolved.features.random_seed,
1582 has_time_start: resolved.features.time_start,
1583 argv_safety_required: resolved.features.argv_safety_required,
1584 time_offset: time_offset_val,
1585 num_cpus: self.num_cpus,
1586 port_remap: resolved.features.port_remap,
1587 cow_enabled: resolved.features.cow,
1588 chroot_root: chroot_root.clone(),
1589 chroot_readable: self.fs_readable.clone(),
1590 chroot_writable: self.fs_writable.clone(),
1591 chroot_denied: self.fs_denied.clone(),
1592 chroot_mounts: crate::chroot::resolve::resolve_chroot_mounts(&self.fs_mount),
1593 chroot_mount_ro: self.fs_mount_ro.clone(),
1594 deterministic_dirs: self.deterministic_dirs,
1595 virtual_hostname: Some(rt_name),
1596 has_http_acl: resolved.features.http_acl,
1597 virtual_etc_hosts,
1598 ca_inject_paths: self.http_inject_ca.clone(),
1599 ca_inject_pem: ca_inject_pem.clone(),
1600 };
1601
1602 use rand::SeedableRng;
1603 use rand_chacha::ChaCha8Rng;
1604
1605 let random_state = self.random_seed.map(|seed| ChaCha8Rng::seed_from_u64(seed));
1606 let time_offset = self.time_start.map(|t| crate::time::calculate_time_offset(t));
1607
1608 let time_random_state = TimeRandomState::new(time_offset, random_state);
1609
1610 let mut net_state = NetworkState::new();
1611 if !self.net_deny.is_empty() {
1612 let resolved_deny = network::resolve_net_deny(&self.net_deny);
1613 net_state.tcp_policy = resolved_deny.tcp;
1614 net_state.udp_policy = resolved_deny.udp;
1615 net_state.icmp_policy = resolved_deny.icmp;
1616 } else {
1617 let no_rules = self.net_allow.is_empty();
1618 let policy_from = |resolved: &network::ResolvedNetAllow| {
1619 if no_rules || resolved.any_ip_all_ports {
1620 crate::seccomp::notif::NetworkPolicy::Unrestricted
1621 } else {
1622 use crate::seccomp::notif::PortAllow;
1623 let per_ip = resolved
1624 .per_ip
1625 .iter()
1626 .map(|(ip, ports)| {
1627 let allow = if resolved.per_ip_all_ports.contains(ip) {
1628 PortAllow::Any
1629 } else {
1630 PortAllow::Specific(ports.clone())
1631 };
1632 (*ip, allow)
1633 })
1634 .collect();
1635 crate::seccomp::notif::NetworkPolicy::AllowList {
1636 per_ip,
1637 cidrs: resolved.cidrs.clone(),
1638 any_ip_ports: resolved.any_ip_ports.clone(),
1639 }
1640 }
1641 };
1642 net_state.tcp_policy = policy_from(&resolved_net_allow.tcp);
1643 net_state.udp_policy = policy_from(&resolved_net_allow.udp);
1644 net_state.icmp_policy = policy_from(&resolved_net_allow.icmp);
1645 }
1646 net_state.http_acl_addr = self.rt().http_acl_handle.as_ref().map(|h| h.addr);
1647 net_state.http_acl_ports = self.http_ports.iter().copied().collect();
1648 net_state.http_acl_orig_dest = self.rt().http_acl_handle.as_ref().map(|h| h.orig_dest.clone());
1649 net_state.bind_deny_ports = self.net_deny_bind.iter().copied().collect();
1650 if let Some(cb) = self.rt_mut().on_bind.take() {
1651 net_state.port_map.on_bind = Some(cb);
1652 }
1653
1654 let procfs_state = ProcfsState::new();
1655
1656 let mut res_state = ResourceState::new(
1657 notif_policy.max_memory_bytes,
1658 notif_policy.max_processes,
1659 );
1660 res_state.proc_count = 1;
1661
1662 let mut cow_state = CowState::new();
1663 cow_state.branch = seccomp_cow_branch;
1664
1665 let mut policy_fn_state = PolicyFnState::new();
1666
1667 for path in &self.fs_denied {
1668 policy_fn_state.denied.deny(&path.to_string_lossy());
1671 }
1672
1673 if let Some(ref callback) = self.policy_fn {
1674 let mut allowed_ips: std::collections::HashSet<std::net::IpAddr> =
1675 std::collections::HashSet::new();
1676 for p in [&net_state.tcp_policy, &net_state.udp_policy, &net_state.icmp_policy] {
1677 if let crate::seccomp::notif::NetworkPolicy::AllowList { per_ip, cidrs, .. } = p {
1678 allowed_ips.extend(per_ip.keys().copied());
1679 for (net, _) in cidrs {
1682 if net.is_single_host() {
1683 allowed_ips.insert(net.addr);
1684 }
1685 }
1686 }
1687 }
1688 let live = crate::policy_fn::LivePolicy {
1689 allowed_ips,
1690 max_memory_bytes: notif_policy.max_memory_bytes,
1691 max_processes: notif_policy.max_processes,
1692 };
1693 let ceiling = live.clone();
1694 let live = std::sync::Arc::new(std::sync::RwLock::new(live));
1695 let denied = policy_fn_state.denied.clone();
1696 let pid_overrides = net_state.pid_ip_overrides.clone();
1697 policy_fn_state.live_policy = Some(live.clone());
1698 let tx = crate::policy_fn::spawn_policy_fn(
1699 callback.clone(), live, ceiling, pid_overrides, denied,
1700 );
1701 policy_fn_state.event_tx = Some(tx);
1702 }
1703
1704 let chroot_state = ChrootState::new();
1705
1706 let notif_raw_fd = notif_fd.as_raw_fd();
1707 let child_pidfd_raw = pidfd.as_ref().map(|pfd| pfd.as_raw_fd());
1708
1709 let res_state = Arc::new(tokio::sync::Mutex::new(res_state));
1710 self.rt_mut().supervisor_resource = Some(Arc::clone(&res_state));
1711
1712 let cow_state = Arc::new(tokio::sync::Mutex::new(cow_state));
1713 self.rt_mut().supervisor_cow = Some(Arc::clone(&cow_state));
1714
1715 let net_state = Arc::new(tokio::sync::Mutex::new(net_state));
1716 self.rt_mut().supervisor_network = Some(Arc::clone(&net_state));
1717
1718 let procfs_state = Arc::new(tokio::sync::Mutex::new(procfs_state));
1719 let time_random_state = Arc::new(tokio::sync::Mutex::new(time_random_state));
1720 let policy_fn_state = Arc::new(tokio::sync::Mutex::new(policy_fn_state));
1721 let chroot_state = Arc::new(tokio::sync::Mutex::new(chroot_state));
1722 let processes = Arc::new(crate::seccomp::state::ProcessIndex::new());
1723
1724 let ctx = Arc::new(SupervisorCtx {
1725 resource: Arc::clone(&res_state),
1726 cow: Arc::clone(&cow_state),
1727 procfs: Arc::clone(&procfs_state),
1728 network: Arc::clone(&net_state),
1729 time_random: Arc::clone(&time_random_state),
1730 policy_fn: Arc::clone(&policy_fn_state),
1731 chroot: Arc::clone(&chroot_state),
1732 netlink: Arc::new(crate::netlink::NetlinkState::new()),
1733 processes: Arc::clone(&processes),
1734 policy: Arc::new(notif_policy),
1735 child_pidfd: child_pidfd_raw,
1736 notif_fd: notif_raw_fd,
1737 });
1738
1739 let handlers = std::mem::take(&mut self.rt_mut().handlers);
1740 let (startup_tx, startup_rx) = tokio::sync::oneshot::channel();
1741 self.rt_mut().notif_handle = Some(tokio::spawn(
1742 notif::supervisor(notif_fd, ctx, handlers, startup_tx),
1743 ));
1744 match startup_rx.await {
1751 Ok(Ok(())) => {}
1752 Ok(Err(e)) => return Err(SandboxRuntimeError::Io(e).into()),
1753 Err(_) => {
1754 return Err(SandboxRuntimeError::Child(
1755 "seccomp supervisor exited during startup".into(),
1756 ).into());
1757 }
1758 }
1759
1760 let la_resource = Arc::clone(&res_state);
1761 self.rt_mut().loadavg_handle = Some(tokio::spawn(async move {
1762 let mut interval = tokio::time::interval(Duration::from_secs(5));
1763 interval.tick().await;
1764 loop {
1765 interval.tick().await;
1766 let mut rs = la_resource.lock().await;
1767 let running = rs.proc_count;
1768 rs.load_avg.sample(running);
1769 }
1770 }));
1771 }
1772
1773 if let Some(cpu_pct) = self.max_cpu {
1774 if cpu_pct < 100 {
1775 let child_pid = pid;
1776 self.rt_mut().throttle_handle = Some(tokio::spawn(sandbox_throttle_cpu(child_pid, cpu_pct)));
1777 }
1778 }
1779
1780 self.rt_mut().pidfd = pidfd;
1781 self.rt_mut().ready_w = Some(pipes.ready_w);
1782
1783 Ok(())
1784 }
1785
1786 fn do_start(&mut self) -> Result<(), crate::error::SandlockError> {
1791 use std::os::fd::AsRawFd;
1792 use crate::context::write_u32_fd;
1793 use crate::error::SandboxRuntimeError;
1794
1795 if !matches!(self.rt().state, RuntimeState::Created) {
1796 return Err(SandboxRuntimeError::Child("start() requires a created sandbox".into()).into());
1797 }
1798 let ready_w = self.rt_mut().ready_w.take()
1799 .ok_or_else(|| SandboxRuntimeError::Child("start() called without a prior create()".into()))?;
1800 write_u32_fd(ready_w.as_raw_fd(), 1)
1801 .map_err(|e| SandboxRuntimeError::Child(format!("write ready signal: {}", e)))?;
1802 drop(ready_w);
1803 self.rt_mut().state = RuntimeState::Running;
1804 Ok(())
1805 }
1806}
1807
1808impl Drop for Sandbox {
1813 fn drop(&mut self) {
1814 if let Some(ref mut rt) = self.runtime {
1815 if let Some(pid) = rt.child_pid {
1816 if matches!(rt.state, RuntimeState::Created | RuntimeState::Running | RuntimeState::Paused) {
1817 unsafe { libc::killpg(pid, libc::SIGKILL) };
1818 let mut status: i32 = 0;
1819 unsafe { libc::waitpid(pid, &mut status, 0) };
1820 }
1821 }
1822
1823 if let Some(h) = rt.notif_handle.take() { h.abort(); }
1824 if let Some(h) = rt.throttle_handle.take() { h.abort(); }
1825 if let Some(h) = rt.loadavg_handle.take() { h.abort(); }
1826
1827 let is_error = matches!(
1828 rt.state,
1829 RuntimeState::Stopped(ref s) if !matches!(s, crate::result::ExitStatus::Code(0))
1830 );
1831 let action = if is_error { &self.on_error } else { &self.on_exit };
1832 let action = action.clone();
1833
1834 if let Some(ref mut cow) = rt.seccomp_cow {
1835 match action {
1836 BranchAction::Commit => { let _ = cow.commit(); }
1837 BranchAction::Abort => { let _ = cow.abort(); }
1838 BranchAction::Keep => {}
1839 }
1840 }
1841 }
1842 }
1843}
1844
1845async fn sandbox_throttle_cpu(pid: i32, cpu_pct: u8) {
1850 use std::time::Duration;
1851 let period = Duration::from_millis(100);
1852 let run_time = period * cpu_pct as u32 / 100;
1853 let stop_time = period - run_time;
1854 loop {
1855 tokio::time::sleep(run_time).await;
1856 if unsafe { libc::killpg(pid, libc::SIGSTOP) } < 0 { break; }
1857 tokio::time::sleep(stop_time).await;
1858 if unsafe { libc::killpg(pid, libc::SIGCONT) } < 0 { break; }
1859 }
1860}
1861
1862static NEXT_SANDBOX_NAME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1867
1868fn sandbox_resolve_name(name: Option<&str>) -> Result<String, crate::error::SandlockError> {
1869 match name {
1870 Some(n) => sandbox_validate_name(n.to_string()),
1871 None => Ok(format!(
1872 "sandbox-{}-{}",
1873 std::process::id(),
1874 NEXT_SANDBOX_NAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
1875 )),
1876 }
1877}
1878
1879fn sandbox_validate_name(name: String) -> Result<String, crate::error::SandlockError> {
1880 use crate::error::SandboxRuntimeError;
1881 if name.is_empty() {
1882 return Err(SandboxRuntimeError::Child("sandbox name must not be empty".into()).into());
1883 }
1884 if name.len() > 64 {
1885 return Err(SandboxRuntimeError::Child("sandbox name must be at most 64 bytes".into()).into());
1886 }
1887 if name.as_bytes().contains(&0) {
1888 return Err(SandboxRuntimeError::Child("sandbox name must not contain NUL bytes".into()).into());
1889 }
1890 Ok(name)
1891}
1892
1893fn sandbox_read_exact(fd: i32, buf: &mut [u8]) {
1898 let mut off = 0;
1899 while off < buf.len() {
1900 let r = unsafe { libc::read(fd, buf[off..].as_mut_ptr() as *mut _, buf.len() - off) };
1901 if r <= 0 { break; }
1902 off += r as usize;
1903 }
1904}
1905
1906fn sandbox_read_fd_to_end(fd: std::os::fd::OwnedFd) -> Vec<u8> {
1907 use std::io::Read;
1908 use std::os::fd::IntoRawFd;
1909 use std::os::unix::io::FromRawFd;
1910 let mut file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
1911 let mut buf = Vec::new();
1912 let _ = file.read_to_end(&mut buf);
1913 buf
1914}
1915
1916fn sandbox_wait_status_to_exit(status: i32) -> crate::result::ExitStatus {
1917 use crate::result::ExitStatus;
1918 if libc::WIFEXITED(status) {
1919 ExitStatus::Code(libc::WEXITSTATUS(status))
1920 } else if libc::WIFSIGNALED(status) {
1921 let sig = libc::WTERMSIG(status);
1922 if sig == libc::SIGKILL {
1923 ExitStatus::Killed
1924 } else {
1925 ExitStatus::Signal(sig)
1926 }
1927 } else {
1928 ExitStatus::Killed
1929 }
1930}
1931
1932async fn wait_child_exit_via_pidfd(
1938 pidfd: std::os::unix::io::OwnedFd,
1939 pid: libc::pid_t,
1940) -> crate::result::ExitStatus {
1941 use crate::result::ExitStatus;
1942
1943 let async_fd = match tokio::io::unix::AsyncFd::with_interest(
1944 pidfd,
1945 tokio::io::Interest::READABLE,
1946 ) {
1947 Ok(fd) => fd,
1948 Err(_) => return wait_child_exit_blocking(pid).await,
1949 };
1950
1951 loop {
1952 let mut guard = match async_fd.readable().await {
1954 Ok(g) => g,
1955 Err(_) => return ExitStatus::Killed,
1956 };
1957 let mut status: i32 = 0;
1958 let r = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
1960 if r > 0 {
1961 return sandbox_wait_status_to_exit(status);
1962 }
1963 if r == 0 {
1964 guard.clear_ready();
1966 continue;
1967 }
1968 return ExitStatus::Killed;
1970 }
1971}
1972
1973async fn wait_child_exit_blocking(pid: libc::pid_t) -> crate::result::ExitStatus {
1977 use crate::result::ExitStatus;
1978 tokio::task::spawn_blocking(move || -> ExitStatus {
1979 let mut status: i32 = 0;
1980 loop {
1981 let ret = unsafe { libc::waitpid(pid, &mut status, 0) };
1982 if ret < 0 {
1983 if std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
1984 continue;
1985 }
1986 return ExitStatus::Killed;
1987 }
1988 break;
1989 }
1990 sandbox_wait_status_to_exit(status)
1991 })
1992 .await
1993 .unwrap_or(ExitStatus::Killed)
1994}
1995
1996fn sandbox_collect_handlers<I, S, H>(
1997 handlers: I,
1998 sandbox: &Sandbox,
1999) -> Result<Vec<(i64, Arc<dyn crate::seccomp::dispatch::Handler>)>, crate::error::SandlockError>
2000where
2001 I: IntoIterator<Item = (S, H)>,
2002 S: TryInto<crate::seccomp::syscall::Syscall, Error = crate::seccomp::syscall::SyscallError>,
2003 H: crate::seccomp::dispatch::Handler,
2004{
2005 use crate::seccomp::dispatch::{Handler, HandlerError};
2006
2007 let pending: Vec<(i64, Arc<dyn Handler>)> = handlers
2008 .into_iter()
2009 .map(|(syscall, handler)| {
2010 let nr = syscall.try_into().map_err(HandlerError::from)?.raw();
2011 let h: Arc<dyn Handler> = Arc::new(handler);
2012 Ok::<_, HandlerError>((nr, h))
2013 })
2014 .collect::<Result<_, _>>()?;
2015
2016 let nrs: Vec<i64> = pending.iter().map(|(nr, _)| *nr).collect();
2017 crate::seccomp::dispatch::validate_handler_syscalls_against_policy(&nrs, sandbox)
2018 .map_err(|syscall_nr| HandlerError::OnDenySyscall { syscall_nr })?;
2019
2020 Ok(pending)
2021}
2022
2023fn validate_syscall_names(names: &[String]) -> Result<(), SandboxError> {
2024 let unknown: Vec<&str> = names
2025 .iter()
2026 .map(String::as_str)
2027 .filter(|name| crate::seccomp::syscall::syscall_name_to_nr(name).is_none())
2028 .collect();
2029 if unknown.is_empty() {
2030 Ok(())
2031 } else {
2032 Err(SandboxError::Invalid(format!(
2033 "unknown syscall name(s): {}",
2034 unknown.join(", ")
2035 )))
2036 }
2037}
2038
2039fn parse_bind_ports(specs: &[String], label: &str) -> Result<Vec<u16>, SandboxError> {
2043 let mut ports: std::collections::BTreeSet<u16> = std::collections::BTreeSet::new();
2044 for spec in specs {
2045 for part in spec.split(',') {
2046 let part = part.trim();
2047 if part.is_empty() {
2048 return Err(SandboxError::Invalid(format!(
2049 "{}: empty port in `{}`",
2050 label, spec
2051 )));
2052 }
2053 match part.split_once('-') {
2054 Some((lo, hi)) => {
2055 let lo: u16 = lo.trim().parse().map_err(|_| {
2056 SandboxError::Invalid(format!("{}: invalid port range `{}`", label, part))
2057 })?;
2058 let hi: u16 = hi.trim().parse().map_err(|_| {
2059 SandboxError::Invalid(format!("{}: invalid port range `{}`", label, part))
2060 })?;
2061 if lo > hi {
2062 return Err(SandboxError::Invalid(format!(
2063 "{}: reversed port range `{}` (lo > hi)",
2064 label, part
2065 )));
2066 }
2067 ports.extend(lo..=hi);
2068 }
2069 None => {
2070 let p: u16 = part.parse().map_err(|_| {
2071 SandboxError::Invalid(format!("{}: invalid port `{}`", label, part))
2072 })?;
2073 ports.insert(p);
2074 }
2075 }
2076 }
2077 }
2078 Ok(ports.into_iter().collect())
2079}
2080
2081fn resolve_sandbox_path_to_host(
2086 child_path: &std::path::Path,
2087 chroot_root: Option<&std::path::Path>,
2088 mounts: &[(std::path::PathBuf, std::path::PathBuf)],
2089) -> std::path::PathBuf {
2090 for (virt, host) in mounts {
2091 if let Ok(rest) = child_path.strip_prefix(virt) {
2092 return host.join(rest);
2093 }
2094 }
2095 if let Some(root) = chroot_root {
2096 if let Ok(rest) = child_path.strip_prefix("/") {
2097 return root.join(rest);
2098 }
2099 }
2100 child_path.to_path_buf()
2101}
2102
2103#[cfg(test)]
2104mod tests;