Skip to main content

evalbox_sandbox/
executor.rs

1//! Sandbox executor for both blocking and concurrent execution.
2//!
3//! This module provides the unified API for sandbox execution:
4//!
5//! - `Executor::run()` - Blocking execution (single sandbox)
6//! - `Executor::spawn()` + `poll()` - Concurrent execution (multiple sandboxes)
7//!
8//! ## Blocking Example
9//!
10//! ```ignore
11//! use evalbox_sandbox::{Executor, Plan};
12//!
13//! let output = Executor::run(Plan::new(["echo", "hello"]))?;
14//! assert_eq!(output.stdout, b"hello\n");
15//! ```
16//!
17//! ## Concurrent Example
18//!
19//! ```ignore
20//! use evalbox_sandbox::{Executor, Plan, Event};
21//!
22//! let mut executor = Executor::new()?;
23//! let id = executor.spawn(Plan::new(["echo", "hello"]))?;
24//!
25//! let mut events = Vec::new();
26//! while executor.active_count() > 0 {
27//!     executor.poll(&mut events, None)?;
28//!     for event in events.drain(..) {
29//!         match event {
30//!             Event::Completed { id, output } => println!("Done: {:?}", output),
31//!             Event::Stdout { id, data } => print!("{}", String::from_utf8_lossy(&data)),
32//!             _ => {}
33//!         }
34//!     }
35//! }
36//! ```
37
38use std::collections::HashMap;
39use std::ffi::CString;
40use std::io::{self, Write as _};
41use std::os::fd::{AsRawFd, OwnedFd, RawFd};
42use std::path::{Path, PathBuf};
43use std::time::{Duration, Instant};
44
45use mio::unix::SourceFd;
46use mio::{Events as MioEvents, Interest, Poll, Token};
47use rustix::io::Errno;
48use rustix::process::{pidfd_open, pidfd_send_signal, Pid, PidfdFlags, Signal};
49use thiserror::Error;
50
51use evalbox_sys::{check, last_errno};
52
53use crate::isolation::{
54    bind_mount, lockdown, make_rprivate, mount_minimal_dev, mount_proc,
55    pivot_root_and_cleanup, set_hostname, setup_id_maps, LockdownError,
56};
57use crate::monitor::{monitor, set_nonblocking, wait_for_exit, write_stdin, Output, Status};
58use crate::plan::{Mount, Plan};
59use crate::resolve::{resolve_binary, ResolvedBinary};
60use crate::validate::validate_cmd;
61use crate::workspace::Workspace;
62
63/// Error during sandbox execution.
64#[derive(Debug, Error)]
65pub enum ExecutorError {
66    #[error("system check: {0}")]
67    SystemCheck(String),
68
69    #[error("validation: {0}")]
70    Validation(#[from] crate::validate::ValidationError),
71
72    #[error("workspace: {0}")]
73    Workspace(io::Error),
74
75    #[error("fork: {0}")]
76    Fork(Errno),
77
78    #[error("unshare: {0}")]
79    Unshare(Errno),
80
81    #[error("id map: {0}")]
82    IdMap(io::Error),
83
84    #[error("rootfs: {0}")]
85    Rootfs(Errno),
86
87    #[error("lockdown: {0}")]
88    Lockdown(#[from] LockdownError),
89
90    #[error("exec: {0}")]
91    Exec(Errno),
92
93    #[error("monitor: {0}")]
94    Monitor(io::Error),
95
96    #[error("child setup: {0}")]
97    ChildSetup(String),
98
99    #[error("pidfd: {0}")]
100    Pidfd(Errno),
101
102    #[error("command not found: {0}")]
103    CommandNotFound(String),
104
105    #[error("io: {0}")]
106    Io(#[from] io::Error),
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct SandboxId(pub usize);
111
112impl std::fmt::Display for SandboxId {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "Sandbox({})", self.0)
115    }
116}
117
118/// Events emitted by the Executor.
119#[derive(Debug)]
120pub enum Event {
121    /// Sandbox completed execution.
122    Completed { id: SandboxId, output: Output },
123    /// Sandbox timed out and was killed.
124    Timeout { id: SandboxId, output: Output },
125    /// Stdout data available (streaming mode).
126    Stdout { id: SandboxId, data: Vec<u8> },
127    /// Stderr data available (streaming mode).
128    Stderr { id: SandboxId, data: Vec<u8> },
129}
130
131struct ExecutionInfo {
132    binary_path: PathBuf,
133    extra_mounts: Vec<Mount>,
134}
135
136impl ExecutionInfo {
137    fn from_resolved(resolved: ResolvedBinary) -> Self {
138        let extra_mounts = resolved
139            .required_mounts
140            .into_iter()
141            .map(|m| Mount::bind(&m.source, &m.target))
142            .collect();
143        Self {
144            binary_path: resolved.path,
145            extra_mounts,
146        }
147    }
148
149    fn from_plan(plan: &Plan) -> Option<Self> {
150        plan.binary_path.as_ref().map(|path| Self {
151            binary_path: path.clone(),
152            extra_mounts: Vec::new(),
153        })
154    }
155}
156
157/// A spawned sandbox that hasn't been waited on yet.
158struct SpawnedSandbox {
159    pidfd: OwnedFd,
160    stdin_fd: RawFd,
161    stdout_fd: RawFd,
162    stderr_fd: RawFd,
163    #[allow(dead_code)]
164    workspace: std::mem::ManuallyDrop<Workspace>,
165}
166
167/// Internal state for a running sandbox.
168struct SandboxState {
169    spawned: SpawnedSandbox,
170    deadline: Instant,
171    start: Instant,
172    stdout: Vec<u8>,
173    stderr: Vec<u8>,
174    max_output: u64,
175    pidfd_ready: bool,
176    stdout_closed: bool,
177    stderr_closed: bool,
178}
179
180impl SandboxState {
181    fn is_done(&self) -> bool {
182        self.pidfd_ready && self.stdout_closed && self.stderr_closed
183    }
184}
185
186// Token encoding: [sandbox_id: 20 bits][type: 2 bits]
187const TOKEN_TYPE_BITS: usize = 2;
188const TOKEN_TYPE_MASK: usize = 0b11;
189const TOKEN_TYPE_PIDFD: usize = 0;
190const TOKEN_TYPE_STDOUT: usize = 1;
191const TOKEN_TYPE_STDERR: usize = 2;
192
193fn encode_token(sandbox_id: usize, token_type: usize) -> Token {
194    Token((sandbox_id << TOKEN_TYPE_BITS) | token_type)
195}
196
197fn decode_token(token: Token) -> (SandboxId, usize) {
198    let raw = token.0;
199    (SandboxId(raw >> TOKEN_TYPE_BITS), raw & TOKEN_TYPE_MASK)
200}
201
202pub struct Executor {
203    poll: Poll,
204    sandboxes: HashMap<SandboxId, SandboxState>,
205    next_id: usize,
206    mio_events: MioEvents,
207}
208
209impl Executor {
210    pub fn new() -> io::Result<Self> {
211        Ok(Self {
212            poll: Poll::new()?,
213            sandboxes: HashMap::new(),
214            next_id: 0,
215            mio_events: MioEvents::with_capacity(64),
216        })
217    }
218
219    /// Execute a sandbox and wait for completion (blocking).
220    pub fn run(plan: Plan) -> Result<Output, ExecutorError> {
221        let cmd_refs: Vec<&str> = plan.cmd.iter().map(|s| s.as_str()).collect();
222        validate_cmd(&cmd_refs).map_err(ExecutorError::Validation)?;
223
224        if let Err(e) = check::check() {
225            return Err(ExecutorError::SystemCheck(e.to_string()));
226        }
227
228        let exec_info = if let Some(info) = ExecutionInfo::from_plan(&plan) {
229            info
230        } else {
231            let resolved = resolve_binary(&plan.cmd[0])
232                .map_err(|e| ExecutorError::CommandNotFound(e.to_string()))?;
233            ExecutionInfo::from_resolved(resolved)
234        };
235
236        let workspace = Workspace::with_prefix("evalbox-").map_err(ExecutorError::Workspace)?;
237
238        for file in &plan.user_files {
239            workspace
240                .write_file(&file.path, &file.content, file.executable)
241                .map_err(ExecutorError::Workspace)?;
242        }
243        workspace.setup_sandbox_dirs().map_err(ExecutorError::Workspace)?;
244        create_mount_dirs(&workspace, &exec_info, &plan)?;
245
246        let child_pid = unsafe { libc::fork() };
247        if child_pid < 0 {
248            return Err(ExecutorError::Fork(last_errno()));
249        }
250
251        if child_pid == 0 {
252            match child_process(&workspace, &plan, &exec_info) {
253                Ok(()) => unsafe { libc::_exit(127) },
254                Err(e) => {
255                    writeln!(io::stderr(), "sandbox error: {e}").ok();
256                    unsafe { libc::_exit(126) }
257                }
258            }
259        }
260
261        let pid = unsafe { Pid::from_raw_unchecked(child_pid) };
262        let pidfd = pidfd_open(pid, PidfdFlags::empty()).map_err(ExecutorError::Pidfd)?;
263
264        blocking_parent(child_pid, pidfd, workspace, plan)
265    }
266
267    /// Spawn a new sandbox. Returns immediately with a [`SandboxId`].
268    pub fn spawn(&mut self, plan: Plan) -> Result<SandboxId, ExecutorError> {
269        let id = SandboxId(self.next_id);
270        self.next_id += 1;
271
272        let timeout = plan.timeout;
273        let max_output = plan.max_output;
274
275        let spawned = spawn_sandbox(plan)?;
276
277        // Register with mio
278        let pidfd_token = encode_token(id.0, TOKEN_TYPE_PIDFD);
279        let stdout_token = encode_token(id.0, TOKEN_TYPE_STDOUT);
280        let stderr_token = encode_token(id.0, TOKEN_TYPE_STDERR);
281
282        self.poll.registry().register(
283            &mut SourceFd(&spawned.pidfd.as_raw_fd()),
284            pidfd_token,
285            Interest::READABLE,
286        )?;
287        self.poll.registry().register(
288            &mut SourceFd(&spawned.stdout_fd),
289            stdout_token,
290            Interest::READABLE,
291        )?;
292        self.poll.registry().register(
293            &mut SourceFd(&spawned.stderr_fd),
294            stderr_token,
295            Interest::READABLE,
296        )?;
297
298        let state = SandboxState {
299            spawned,
300            deadline: Instant::now() + timeout,
301            start: Instant::now(),
302            stdout: Vec::new(),
303            stderr: Vec::new(),
304            max_output,
305            pidfd_ready: false,
306            stdout_closed: false,
307            stderr_closed: false,
308        };
309
310        self.sandboxes.insert(id, state);
311        Ok(id)
312    }
313
314    /// Poll for events. Blocks until events are available or timeout expires.
315    pub fn poll(&mut self, events: &mut Vec<Event>, timeout: Option<Duration>) -> io::Result<()> {
316        events.clear();
317
318        if self.sandboxes.is_empty() {
319            return Ok(());
320        }
321
322        let effective_timeout = self.calculate_timeout(timeout);
323        self.poll.poll(&mut self.mio_events, effective_timeout)?;
324
325        let mut pidfd_ready: Vec<SandboxId> = Vec::new();
326        let mut read_stdout: Vec<SandboxId> = Vec::new();
327        let mut read_stderr: Vec<SandboxId> = Vec::new();
328
329        for mio_event in &self.mio_events {
330            let (sandbox_id, token_type) = decode_token(mio_event.token());
331            if self.sandboxes.contains_key(&sandbox_id) {
332                match token_type {
333                    TOKEN_TYPE_PIDFD => pidfd_ready.push(sandbox_id),
334                    TOKEN_TYPE_STDOUT => read_stdout.push(sandbox_id),
335                    TOKEN_TYPE_STDERR => read_stderr.push(sandbox_id),
336                    _ => {}
337                }
338            }
339        }
340
341        for id in pidfd_ready {
342            if let Some(state) = self.sandboxes.get_mut(&id) {
343                state.pidfd_ready = true;
344            }
345        }
346
347        for id in read_stdout {
348            self.read_pipe(id, true, events);
349        }
350
351        for id in read_stderr {
352            self.read_pipe(id, false, events);
353        }
354
355        self.check_completions(events)?;
356        Ok(())
357    }
358
359    pub fn active_count(&self) -> usize {
360        self.sandboxes.len()
361    }
362
363    pub fn kill(&mut self, id: SandboxId) -> io::Result<()> {
364        if let Some(state) = self.sandboxes.get(&id) {
365            pidfd_send_signal(&state.spawned.pidfd, Signal::KILL)?;
366        }
367        Ok(())
368    }
369
370    /// Write data to a sandbox's stdin.
371    pub fn write_stdin(&mut self, id: SandboxId, data: &[u8]) -> io::Result<usize> {
372        if let Some(state) = self.sandboxes.get(&id) {
373            let fd = state.spawned.stdin_fd;
374            if fd < 0 {
375                return Err(io::Error::new(io::ErrorKind::BrokenPipe, "stdin closed"));
376            }
377            let ret = unsafe { libc::write(fd, data.as_ptr().cast(), data.len()) };
378            if ret < 0 {
379                Err(io::Error::last_os_error())
380            } else {
381                Ok(ret as usize)
382            }
383        } else {
384            Err(io::Error::new(io::ErrorKind::NotFound, "sandbox not found"))
385        }
386    }
387
388    /// Close a sandbox's stdin (signal EOF).
389    pub fn close_stdin(&mut self, id: SandboxId) -> io::Result<()> {
390        if let Some(state) = self.sandboxes.get_mut(&id) {
391            if state.spawned.stdin_fd >= 0 {
392                unsafe { libc::close(state.spawned.stdin_fd) };
393                state.spawned.stdin_fd = -1;
394            }
395        }
396        Ok(())
397    }
398
399    fn calculate_timeout(&self, user_timeout: Option<Duration>) -> Option<Duration> {
400        let now = Instant::now();
401        let nearest_deadline = self.sandboxes.values().map(|s| s.deadline).min();
402
403        match (user_timeout, nearest_deadline) {
404            (Some(user), Some(deadline)) => {
405                Some(user.min(deadline.saturating_duration_since(now)))
406            }
407            (Some(user), None) => Some(user),
408            (None, Some(deadline)) => Some(deadline.saturating_duration_since(now)),
409            (None, None) => None,
410        }
411    }
412
413    fn read_pipe(&mut self, sandbox_id: SandboxId, is_stdout: bool, events: &mut Vec<Event>) {
414        let Some(state) = self.sandboxes.get_mut(&sandbox_id) else {
415            return;
416        };
417
418        let fd = if is_stdout {
419            state.spawned.stdout_fd
420        } else {
421            state.spawned.stderr_fd
422        };
423
424        let mut buf = [0u8; 4096];
425        loop {
426            let ret = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) };
427
428            if ret < 0 {
429                let err = io::Error::last_os_error();
430                if err.kind() == io::ErrorKind::WouldBlock {
431                    break;
432                }
433                if is_stdout {
434                    state.stdout_closed = true;
435                } else {
436                    state.stderr_closed = true;
437                }
438                break;
439            } else if ret == 0 {
440                if is_stdout {
441                    state.stdout_closed = true;
442                } else {
443                    state.stderr_closed = true;
444                }
445                break;
446            } else {
447                let n = ret as usize;
448                let data = buf[..n].to_vec();
449
450                if is_stdout {
451                    state.stdout.extend_from_slice(&data);
452                    events.push(Event::Stdout { id: sandbox_id, data });
453                } else {
454                    state.stderr.extend_from_slice(&data);
455                    events.push(Event::Stderr { id: sandbox_id, data });
456                }
457
458                let total = state.stdout.len() + state.stderr.len();
459                if total > state.max_output as usize {
460                    pidfd_send_signal(&state.spawned.pidfd, Signal::KILL).ok();
461                    break;
462                }
463            }
464        }
465    }
466
467    fn check_completions(&mut self, events: &mut Vec<Event>) -> io::Result<()> {
468        let now = Instant::now();
469        let mut to_remove = Vec::new();
470
471        for (&id, state) in &mut self.sandboxes {
472            if now >= state.deadline && !state.pidfd_ready {
473                pidfd_send_signal(&state.spawned.pidfd, Signal::KILL).ok();
474                state.pidfd_ready = true;
475            }
476            if state.is_done() {
477                to_remove.push(id);
478            }
479        }
480
481        for id in to_remove {
482            if let Some(state) = self.sandboxes.remove(&id) {
483                self.poll
484                    .registry()
485                    .deregister(&mut SourceFd(&state.spawned.pidfd.as_raw_fd()))
486                    .ok();
487                self.poll
488                    .registry()
489                    .deregister(&mut SourceFd(&state.spawned.stdout_fd))
490                    .ok();
491                self.poll
492                    .registry()
493                    .deregister(&mut SourceFd(&state.spawned.stderr_fd))
494                    .ok();
495
496                let (exit_code, signal) = wait_for_exit(state.spawned.pidfd.as_raw_fd())?;
497                let duration = state.start.elapsed();
498                let timed_out = Instant::now() >= state.deadline;
499
500                let status = if timed_out {
501                    Status::Timeout
502                } else if signal.is_some() {
503                    Status::Signaled
504                } else if state.stdout.len() + state.stderr.len() > state.max_output as usize {
505                    Status::OutputLimitExceeded
506                } else {
507                    Status::Exited
508                };
509
510                let output = Output {
511                    stdout: state.stdout,
512                    stderr: state.stderr,
513                    status,
514                    duration,
515                    exit_code,
516                    signal,
517                };
518
519                if timed_out {
520                    events.push(Event::Timeout { id, output });
521                } else {
522                    events.push(Event::Completed { id, output });
523                }
524            }
525        }
526
527        Ok(())
528    }
529}
530
531
532fn spawn_sandbox(plan: Plan) -> Result<SpawnedSandbox, ExecutorError> {
533    let cmd_refs: Vec<&str> = plan.cmd.iter().map(|s| s.as_str()).collect();
534    validate_cmd(&cmd_refs).map_err(ExecutorError::Validation)?;
535
536    if let Err(e) = check::check() {
537        return Err(ExecutorError::SystemCheck(e.to_string()));
538    }
539
540    let exec_info = if let Some(info) = ExecutionInfo::from_plan(&plan) {
541        info
542    } else {
543        let resolved = resolve_binary(&plan.cmd[0])
544            .map_err(|e| ExecutorError::CommandNotFound(e.to_string()))?;
545        ExecutionInfo::from_resolved(resolved)
546    };
547
548    let workspace = Workspace::with_prefix("evalbox-").map_err(ExecutorError::Workspace)?;
549
550    for file in &plan.user_files {
551        workspace
552            .write_file(&file.path, &file.content, file.executable)
553            .map_err(ExecutorError::Workspace)?;
554    }
555    workspace.setup_sandbox_dirs().map_err(ExecutorError::Workspace)?;
556    create_mount_dirs(&workspace, &exec_info, &plan)?;
557
558    let child_pid = unsafe { libc::fork() };
559    if child_pid < 0 {
560        return Err(ExecutorError::Fork(last_errno()));
561    }
562
563    if child_pid == 0 {
564        match child_process(&workspace, &plan, &exec_info) {
565            Ok(()) => unsafe { libc::_exit(127) },
566            Err(e) => {
567                writeln!(io::stderr(), "sandbox error: {e}").ok();
568                unsafe { libc::_exit(126) }
569            }
570        }
571    }
572
573    let pid = unsafe { Pid::from_raw_unchecked(child_pid) };
574    let pidfd = pidfd_open(pid, PidfdFlags::empty()).map_err(ExecutorError::Pidfd)?;
575
576    // Parent: close unused pipe ends
577    let stdin_write_fd = workspace.pipes.stdin.write.as_raw_fd();
578    let stdout_read_fd = workspace.pipes.stdout.read.as_raw_fd();
579    let stderr_read_fd = workspace.pipes.stderr.read.as_raw_fd();
580
581    unsafe {
582        libc::close(workspace.pipes.stdin.read.as_raw_fd());
583        libc::close(workspace.pipes.stdout.write.as_raw_fd());
584        libc::close(workspace.pipes.stderr.write.as_raw_fd());
585    }
586
587    // Wait for child to signal readiness
588    let child_ready_fd = workspace.pipes.sync.child_ready_fd();
589    let mut pfd = libc::pollfd {
590        fd: child_ready_fd,
591        events: libc::POLLIN,
592        revents: 0,
593    };
594
595    if unsafe { libc::poll(&mut pfd, 1, 30000) } <= 0 {
596        unsafe { libc::kill(child_pid, libc::SIGKILL) };
597        return Err(ExecutorError::ChildSetup("timeout waiting for child".into()));
598    }
599
600    let mut value: u64 = 0;
601    if unsafe { libc::read(child_ready_fd, (&mut value as *mut u64).cast(), 8) } != 8 {
602        unsafe { libc::kill(child_pid, libc::SIGKILL) };
603        return Err(ExecutorError::ChildSetup("eventfd read failed".into()));
604    }
605
606    setup_id_maps(child_pid).map_err(ExecutorError::IdMap)?;
607
608    // Signal child to continue
609    let parent_done_fd = workspace.pipes.sync.parent_done_fd();
610    let signal_value: u64 = 1;
611    if unsafe { libc::write(parent_done_fd, (&signal_value as *const u64).cast(), 8) } != 8 {
612        unsafe { libc::kill(child_pid, libc::SIGKILL) };
613        return Err(ExecutorError::ChildSetup("eventfd write failed".into()));
614    }
615
616    // Write stdin if provided
617    if let Some(ref stdin_data) = plan.stdin {
618        write_stdin(&workspace, stdin_data).map_err(ExecutorError::Monitor)?;
619        unsafe { libc::close(stdin_write_fd) };
620    }
621
622    // Set non-blocking for async reading
623    set_nonblocking(stdout_read_fd).map_err(ExecutorError::Monitor)?;
624    set_nonblocking(stderr_read_fd).map_err(ExecutorError::Monitor)?;
625
626    // Close sync fds
627    unsafe {
628        libc::close(workspace.pipes.sync.child_ready_fd());
629        libc::close(workspace.pipes.sync.parent_done_fd());
630    }
631
632    Ok(SpawnedSandbox {
633        pidfd,
634        stdin_fd: if plan.stdin.is_some() { -1 } else { stdin_write_fd },
635        stdout_fd: stdout_read_fd,
636        stderr_fd: stderr_read_fd,
637        workspace: std::mem::ManuallyDrop::new(workspace),
638    })
639}
640
641fn blocking_parent(
642    child_pid: libc::pid_t,
643    pidfd: OwnedFd,
644    workspace: Workspace,
645    plan: Plan,
646) -> Result<Output, ExecutorError> {
647    let workspace = std::mem::ManuallyDrop::new(workspace);
648
649    unsafe {
650        libc::close(workspace.pipes.stdin.read.as_raw_fd());
651        libc::close(workspace.pipes.stdout.write.as_raw_fd());
652        libc::close(workspace.pipes.stderr.write.as_raw_fd());
653    }
654
655    let child_ready_fd = workspace.pipes.sync.child_ready_fd();
656    let mut pfd = libc::pollfd {
657        fd: child_ready_fd,
658        events: libc::POLLIN,
659        revents: 0,
660    };
661
662    if unsafe { libc::poll(&mut pfd, 1, 30000) } <= 0 {
663        unsafe { libc::kill(child_pid, libc::SIGKILL) };
664        return Err(ExecutorError::ChildSetup("timeout waiting for child".into()));
665    }
666
667    let mut value: u64 = 0;
668    if unsafe { libc::read(child_ready_fd, (&mut value as *mut u64).cast(), 8) } != 8 {
669        unsafe { libc::kill(child_pid, libc::SIGKILL) };
670        return Err(ExecutorError::ChildSetup("eventfd read failed".into()));
671    }
672
673    setup_id_maps(child_pid).map_err(ExecutorError::IdMap)?;
674
675    let parent_done_fd = workspace.pipes.sync.parent_done_fd();
676    let signal_value: u64 = 1;
677    if unsafe { libc::write(parent_done_fd, (&signal_value as *const u64).cast(), 8) } != 8 {
678        unsafe { libc::kill(child_pid, libc::SIGKILL) };
679        return Err(ExecutorError::ChildSetup("eventfd write failed".into()));
680    }
681
682    if let Some(ref stdin_data) = plan.stdin {
683        write_stdin(&workspace, stdin_data).map_err(ExecutorError::Monitor)?;
684    }
685    unsafe { libc::close(workspace.pipes.stdin.write.as_raw_fd()) };
686
687    let result = monitor(pidfd, &workspace, &plan).map_err(ExecutorError::Monitor);
688
689    unsafe {
690        libc::close(workspace.pipes.stdout.read.as_raw_fd());
691        libc::close(workspace.pipes.stderr.read.as_raw_fd());
692        libc::close(workspace.pipes.sync.child_ready_fd());
693        libc::close(workspace.pipes.sync.parent_done_fd());
694    }
695
696    result
697}
698
699
700fn child_process(
701    workspace: &Workspace,
702    plan: &Plan,
703    exec_info: &ExecutionInfo,
704) -> Result<(), ExecutorError> {
705    unsafe {
706        libc::close(workspace.pipes.stdin.write.as_raw_fd());
707        libc::close(workspace.pipes.stdout.read.as_raw_fd());
708        libc::close(workspace.pipes.stderr.read.as_raw_fd());
709    }
710
711    if unsafe { libc::unshare(libc::CLONE_NEWUSER) } != 0 {
712        return Err(ExecutorError::Unshare(last_errno()));
713    }
714
715    let child_ready_fd = workspace.pipes.sync.child_ready_fd();
716    let signal_value: u64 = 1;
717    if unsafe { libc::write(child_ready_fd, (&signal_value as *const u64).cast(), 8) } != 8 {
718        return Err(ExecutorError::ChildSetup("eventfd write failed".into()));
719    }
720
721    let parent_done_fd = workspace.pipes.sync.parent_done_fd();
722    let mut value: u64 = 0;
723    if unsafe { libc::read(parent_done_fd, (&mut value as *mut u64).cast(), 8) } != 8 {
724        return Err(ExecutorError::ChildSetup("eventfd read failed".into()));
725    }
726
727    if unsafe { libc::unshare(libc::CLONE_NEWNS | libc::CLONE_NEWUTS | libc::CLONE_NEWIPC) } != 0 {
728        return Err(ExecutorError::Unshare(last_errno()));
729    }
730
731    setup_rootfs(workspace, plan, exec_info)?;
732    setup_stdio(workspace)?;
733
734    let extra_paths: Vec<&str> = exec_info
735        .extra_mounts
736        .iter()
737        .filter_map(|m| m.target.to_str())
738        .collect();
739    lockdown(plan, None, &extra_paths).map_err(ExecutorError::Lockdown)?;
740
741    let cwd = CString::new(plan.cwd.as_bytes()).map_err(|_| ExecutorError::Exec(Errno::INVAL))?;
742    if unsafe { libc::chdir(cwd.as_ptr()) } != 0 {
743        return Err(ExecutorError::Exec(last_errno()));
744    }
745
746    exec_command(plan, exec_info)
747}
748
749fn setup_rootfs(
750    workspace: &Workspace,
751    plan: &Plan,
752    exec_info: &ExecutionInfo,
753) -> Result<(), ExecutorError> {
754    let sandbox_root = workspace.root();
755
756    make_rprivate().map_err(ExecutorError::Rootfs)?;
757
758    for mount in &exec_info.extra_mounts {
759        let target = sandbox_root.join(mount.target.strip_prefix("/").unwrap_or(&mount.target));
760        if mount.source.exists() {
761            bind_mount(&mount.source, &target, !mount.writable).map_err(ExecutorError::Rootfs)?;
762        }
763    }
764
765    for mount in &plan.mounts {
766        let target = sandbox_root.join(mount.target.strip_prefix("/").unwrap_or(&mount.target));
767        if let Some(parent) = target.parent() {
768            std::fs::create_dir_all(parent).map_err(ExecutorError::Workspace)?;
769        }
770        std::fs::create_dir_all(&target).map_err(ExecutorError::Workspace)?;
771        if mount.source.exists() {
772            bind_mount(&mount.source, &target, !mount.writable).map_err(ExecutorError::Rootfs)?;
773        }
774    }
775
776    mount_proc(&sandbox_root.join("proc")).map_err(ExecutorError::Rootfs)?;
777    mount_minimal_dev(&sandbox_root.join("dev")).map_err(ExecutorError::Rootfs)?;
778
779    for file in &plan.user_files {
780        let target_path = if file.path.starts_with('/') {
781            file.path[1..].to_string()
782        } else {
783            format!("work/{}", file.path)
784        };
785        workspace
786            .write_file(&target_path, &file.content, file.executable)
787            .map_err(ExecutorError::Workspace)?;
788    }
789
790    set_hostname("sandbox").map_err(ExecutorError::Rootfs)?;
791    pivot_root_and_cleanup(sandbox_root).map_err(ExecutorError::Rootfs)
792}
793
794fn setup_stdio(workspace: &Workspace) -> Result<(), ExecutorError> {
795    let stdin_fd = workspace.pipes.stdin.read.as_raw_fd();
796    let stdout_fd = workspace.pipes.stdout.write.as_raw_fd();
797    let stderr_fd = workspace.pipes.stderr.write.as_raw_fd();
798
799    unsafe {
800        libc::close(0);
801        libc::close(1);
802        libc::close(2);
803        if libc::dup2(stdin_fd, 0) < 0 {
804            return Err(ExecutorError::Exec(last_errno()));
805        }
806        if libc::dup2(stdout_fd, 1) < 0 {
807            return Err(ExecutorError::Exec(last_errno()));
808        }
809        if libc::dup2(stderr_fd, 2) < 0 {
810            return Err(ExecutorError::Exec(last_errno()));
811        }
812    }
813    Ok(())
814}
815
816fn exec_command(plan: &Plan, exec_info: &ExecutionInfo) -> Result<(), ExecutorError> {
817    let cmd_path = CString::new(exec_info.binary_path.to_string_lossy().as_bytes())
818        .map_err(|_| ExecutorError::Exec(Errno::INVAL))?;
819
820    let mut argv: Vec<CString> = Vec::with_capacity(plan.cmd.len());
821    argv.push(cmd_path.clone());
822    for arg in plan.cmd.iter().skip(1) {
823        argv.push(CString::new(arg.as_bytes()).map_err(|_| ExecutorError::Exec(Errno::INVAL))?);
824    }
825
826    let argv_ptrs: Vec<*const libc::c_char> = argv
827        .iter()
828        .map(|s| s.as_ptr())
829        .chain(std::iter::once(std::ptr::null()))
830        .collect();
831
832    let envp: Vec<CString> = plan
833        .env
834        .iter()
835        .map(|(k, v)| CString::new(format!("{k}={v}")))
836        .collect::<Result<Vec<_>, _>>()
837        .map_err(|_| ExecutorError::Exec(Errno::INVAL))?;
838
839    let envp_ptrs: Vec<*const libc::c_char> = envp
840        .iter()
841        .map(|s| s.as_ptr())
842        .chain(std::iter::once(std::ptr::null()))
843        .collect();
844
845    unsafe { libc::execve(cmd_path.as_ptr(), argv_ptrs.as_ptr(), envp_ptrs.as_ptr()) };
846
847    Err(ExecutorError::Exec(last_errno()))
848}
849
850
851fn create_mount_dirs(
852    workspace: &Workspace,
853    exec_info: &ExecutionInfo,
854    plan: &Plan,
855) -> Result<(), ExecutorError> {
856    for mount in &exec_info.extra_mounts {
857        create_mount_dir(workspace, &mount.target)?;
858    }
859    for mount in &plan.mounts {
860        create_mount_dir(workspace, &mount.target)?;
861    }
862    Ok(())
863}
864
865fn create_mount_dir(workspace: &Workspace, target: &Path) -> Result<(), ExecutorError> {
866    if let Some(parent) = target.parent() {
867        if parent != Path::new("/") {
868            let target_dir = workspace
869                .root()
870                .join(parent.strip_prefix("/").unwrap_or(parent));
871            std::fs::create_dir_all(&target_dir).map_err(ExecutorError::Workspace)?;
872        }
873    }
874    let mount_point = workspace
875        .root()
876        .join(target.strip_prefix("/").unwrap_or(target));
877    std::fs::create_dir_all(&mount_point).map_err(ExecutorError::Workspace)?;
878    Ok(())
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884
885    #[test]
886    fn token_encoding() {
887        let token = encode_token(42, TOKEN_TYPE_STDOUT);
888        let (id, ty) = decode_token(token);
889        assert_eq!(id.0, 42);
890        assert_eq!(ty, TOKEN_TYPE_STDOUT);
891    }
892
893    #[test]
894    fn sandbox_id_display() {
895        let id = SandboxId(123);
896        assert_eq!(format!("{id}"), "Sandbox(123)");
897    }
898}