1use std::collections::{HashMap, HashSet};
39use std::ffi::CString;
40use std::io::{self, Write as _};
41use std::os::fd::{AsRawFd, OwnedFd, RawFd};
42use std::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::{Pid, PidfdFlags, Signal, pidfd_open, pidfd_send_signal};
49use thiserror::Error;
50
51use evalbox_sys::seccomp::{
52 SockFprog, build_notify_filter, build_whitelist_filter, default_whitelist, notify_fs_syscalls,
53};
54use evalbox_sys::seccomp_notify::seccomp_set_mode_filter_listener;
55use evalbox_sys::{check, last_errno, seccomp::seccomp_set_mode_filter};
56
57use crate::isolation::{LockdownError, close_extra_fds, lockdown};
58use crate::monitor::{Output, Status, monitor, set_nonblocking, wait_for_exit, write_stdin};
59use crate::notify::scm_rights;
60use crate::plan::{Mount, NotifyMode, Plan};
61use crate::resolve::{ResolvedBinary, resolve_binary};
62use crate::validate::validate_cmd;
63use crate::workspace::Workspace;
64
65#[derive(Debug, Error)]
67pub enum ExecutorError {
68 #[error("system check: {0}")]
69 SystemCheck(String),
70
71 #[error("validation: {0}")]
72 Validation(#[from] crate::validate::ValidationError),
73
74 #[error("workspace: {0}")]
75 Workspace(io::Error),
76
77 #[error("fork: {0}")]
78 Fork(Errno),
79
80 #[error("lockdown: {0}")]
81 Lockdown(#[from] LockdownError),
82
83 #[error("exec: {0}")]
84 Exec(Errno),
85
86 #[error("monitor: {0}")]
87 Monitor(io::Error),
88
89 #[error("child setup: {0}")]
90 ChildSetup(String),
91
92 #[error("pidfd: {0}")]
93 Pidfd(Errno),
94
95 #[error("command not found: {0}")]
96 CommandNotFound(String),
97
98 #[error("seccomp notify: {0}")]
99 SeccompNotify(String),
100
101 #[error("io: {0}")]
102 Io(#[from] io::Error),
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub struct SandboxId(pub usize);
107
108impl std::fmt::Display for SandboxId {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 write!(f, "Sandbox({})", self.0)
111 }
112}
113
114#[derive(Debug)]
116pub enum Event {
117 Completed { id: SandboxId, output: Output },
119 Timeout { id: SandboxId, output: Output },
121 Stdout { id: SandboxId, data: Vec<u8> },
123 Stderr { id: SandboxId, data: Vec<u8> },
125}
126
127struct ExecutionInfo {
128 binary_path: PathBuf,
129 extra_mounts: Vec<Mount>,
130}
131
132impl ExecutionInfo {
133 fn from_resolved(resolved: ResolvedBinary) -> Self {
134 let extra_mounts = resolved
135 .required_mounts
136 .into_iter()
137 .map(|m| Mount::bind(&m.source, &m.target))
138 .collect();
139 Self {
140 binary_path: resolved.path,
141 extra_mounts,
142 }
143 }
144
145 fn from_plan(plan: &Plan) -> Option<Self> {
146 plan.binary_path.as_ref().map(|path| Self {
147 binary_path: path.clone(),
148 extra_mounts: Vec::new(),
149 })
150 }
151}
152
153#[allow(dead_code)]
157struct SpawnedSandbox {
158 pidfd: OwnedFd,
159 stdin_fd: RawFd,
160 stdout_fd: RawFd,
161 stderr_fd: RawFd,
162 notify_fd: Option<OwnedFd>,
164 workspace: std::mem::ManuallyDrop<Workspace>,
166}
167
168impl Drop for SpawnedSandbox {
169 fn drop(&mut self) {
170 unsafe {
173 if self.stdin_fd >= 0 {
174 libc::close(self.stdin_fd);
175 }
176 if self.stdout_fd >= 0 {
177 libc::close(self.stdout_fd);
178 }
179 if self.stderr_fd >= 0 {
180 libc::close(self.stderr_fd);
181 }
182 }
183 let _ = std::fs::remove_dir_all(self.workspace.root());
186 }
187}
188
189struct SandboxState {
191 spawned: SpawnedSandbox,
192 deadline: Instant,
193 start: Instant,
194 stdout: Vec<u8>,
195 stderr: Vec<u8>,
196 max_output: u64,
197 pidfd_ready: bool,
198 stdout_closed: bool,
199 stderr_closed: bool,
200}
201
202impl SandboxState {
203 fn is_done(&self) -> bool {
204 self.pidfd_ready && self.stdout_closed && self.stderr_closed
205 }
206}
207
208const TOKEN_TYPE_BITS: usize = 2;
210const TOKEN_TYPE_MASK: usize = 0b11;
211const TOKEN_TYPE_PIDFD: usize = 0;
212const TOKEN_TYPE_STDOUT: usize = 1;
213const TOKEN_TYPE_STDERR: usize = 2;
214
215fn encode_token(sandbox_id: usize, token_type: usize) -> Token {
216 Token((sandbox_id << TOKEN_TYPE_BITS) | token_type)
217}
218
219fn decode_token(token: Token) -> (SandboxId, usize) {
220 let raw = token.0;
221 (SandboxId(raw >> TOKEN_TYPE_BITS), raw & TOKEN_TYPE_MASK)
222}
223
224pub struct Executor {
225 poll: Poll,
226 sandboxes: HashMap<SandboxId, SandboxState>,
227 next_id: usize,
228 mio_events: MioEvents,
229}
230
231impl Executor {
232 pub fn new() -> io::Result<Self> {
233 Ok(Self {
234 poll: Poll::new()?,
235 sandboxes: HashMap::new(),
236 next_id: 0,
237 mio_events: MioEvents::with_capacity(64),
238 })
239 }
240
241 pub fn run(plan: Plan) -> Result<Output, ExecutorError> {
243 let cmd_refs: Vec<&str> = plan.cmd.iter().map(|s| s.as_str()).collect();
244 validate_cmd(&cmd_refs).map_err(ExecutorError::Validation)?;
245
246 if let Err(e) = check::check() {
247 return Err(ExecutorError::SystemCheck(e.to_string()));
248 }
249
250 let exec_info = if let Some(info) = ExecutionInfo::from_plan(&plan) {
251 info
252 } else {
253 let resolved = resolve_binary(&plan.cmd[0])
254 .map_err(|e| ExecutorError::CommandNotFound(e.to_string()))?;
255 ExecutionInfo::from_resolved(resolved)
256 };
257
258 let workspace = Workspace::with_prefix("evalbox-").map_err(ExecutorError::Workspace)?;
259
260 workspace
261 .setup_sandbox_dirs()
262 .map_err(ExecutorError::Workspace)?;
263 for file in &plan.user_files {
264 let work_path = format!("work/{}", file.path);
265 workspace
266 .write_file(&work_path, &file.content, file.executable)
267 .map_err(ExecutorError::Workspace)?;
268 }
269
270 let notify_sockets = if plan.notify_mode != NotifyMode::Disabled {
272 Some(scm_rights::create_socketpair().map_err(ExecutorError::Workspace)?)
273 } else {
274 None
275 };
276
277 let child_pid = unsafe { libc::fork() };
279 if child_pid < 0 {
280 return Err(ExecutorError::Fork(last_errno()));
281 }
282
283 if child_pid == 0 {
284 let child_socket = notify_sockets.map(|(_, child)| child);
286 match child_process(&workspace, &plan, &exec_info, child_socket.as_ref()) {
287 Ok(()) => unsafe { libc::_exit(127) },
288 Err(e) => {
289 writeln!(io::stderr(), "sandbox error: {e}").ok();
290 unsafe { libc::_exit(126) }
291 }
292 }
293 }
294
295 let pid = unsafe { Pid::from_raw_unchecked(child_pid) };
301 let pidfd = pidfd_open(pid, PidfdFlags::empty()).map_err(ExecutorError::Pidfd)?;
302
303 let notify_fd = if let Some((parent_socket, _)) = notify_sockets {
305 poll_or_kill(
306 parent_socket.as_raw_fd(),
307 child_pid,
308 "timeout waiting for notify fd",
309 )?;
310 Some(
311 scm_rights::recv_fd(parent_socket.as_raw_fd())
312 .map_err(|e| ExecutorError::SeccompNotify(e.to_string()))?,
313 )
314 } else {
315 None
316 };
317
318 blocking_parent(child_pid, pidfd, notify_fd, workspace, plan)
319 }
320
321 pub fn spawn(&mut self, plan: Plan) -> Result<SandboxId, ExecutorError> {
323 let id = SandboxId(self.next_id);
324 self.next_id += 1;
325
326 let timeout = plan.timeout;
327 let max_output = plan.max_output;
328
329 let spawned = spawn_sandbox(plan)?;
330
331 let pidfd_token = encode_token(id.0, TOKEN_TYPE_PIDFD);
333 let stdout_token = encode_token(id.0, TOKEN_TYPE_STDOUT);
334 let stderr_token = encode_token(id.0, TOKEN_TYPE_STDERR);
335
336 self.poll.registry().register(
337 &mut SourceFd(&spawned.pidfd.as_raw_fd()),
338 pidfd_token,
339 Interest::READABLE,
340 )?;
341 self.poll.registry().register(
342 &mut SourceFd(&spawned.stdout_fd),
343 stdout_token,
344 Interest::READABLE,
345 )?;
346 self.poll.registry().register(
347 &mut SourceFd(&spawned.stderr_fd),
348 stderr_token,
349 Interest::READABLE,
350 )?;
351
352 let state = SandboxState {
353 spawned,
354 deadline: Instant::now() + timeout,
355 start: Instant::now(),
356 stdout: Vec::new(),
357 stderr: Vec::new(),
358 max_output,
359 pidfd_ready: false,
360 stdout_closed: false,
361 stderr_closed: false,
362 };
363
364 self.sandboxes.insert(id, state);
365 Ok(id)
366 }
367
368 pub fn poll(&mut self, events: &mut Vec<Event>, timeout: Option<Duration>) -> io::Result<()> {
370 events.clear();
371
372 if self.sandboxes.is_empty() {
373 return Ok(());
374 }
375
376 let effective_timeout = self.calculate_timeout(timeout);
377 self.poll.poll(&mut self.mio_events, effective_timeout)?;
378
379 let mut pidfd_ready: Vec<SandboxId> = Vec::new();
380 let mut read_stdout: Vec<SandboxId> = Vec::new();
381 let mut read_stderr: Vec<SandboxId> = Vec::new();
382
383 for mio_event in &self.mio_events {
384 let (sandbox_id, token_type) = decode_token(mio_event.token());
385 if self.sandboxes.contains_key(&sandbox_id) {
386 match token_type {
387 TOKEN_TYPE_PIDFD => pidfd_ready.push(sandbox_id),
388 TOKEN_TYPE_STDOUT => read_stdout.push(sandbox_id),
389 TOKEN_TYPE_STDERR => read_stderr.push(sandbox_id),
390 _ => {}
391 }
392 }
393 }
394
395 for id in pidfd_ready {
396 if let Some(state) = self.sandboxes.get_mut(&id) {
397 state.pidfd_ready = true;
398 }
399 }
400
401 for id in read_stdout {
402 self.read_pipe(id, true, events);
403 }
404
405 for id in read_stderr {
406 self.read_pipe(id, false, events);
407 }
408
409 self.check_completions(events)?;
410 Ok(())
411 }
412
413 pub fn active_count(&self) -> usize {
414 self.sandboxes.len()
415 }
416
417 pub fn kill(&mut self, id: SandboxId) -> io::Result<()> {
418 if let Some(state) = self.sandboxes.get(&id) {
419 pidfd_send_signal(&state.spawned.pidfd, Signal::KILL)?;
420 }
421 Ok(())
422 }
423
424 #[allow(clippy::cast_sign_loss)]
427 pub fn write_stdin(&mut self, id: SandboxId, data: &[u8]) -> io::Result<usize> {
428 if let Some(state) = self.sandboxes.get(&id) {
429 let fd = state.spawned.stdin_fd;
430 if fd < 0 {
431 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "stdin closed"));
432 }
433 let ret = unsafe { libc::write(fd, data.as_ptr().cast(), data.len()) };
434 if ret < 0 {
435 Err(io::Error::last_os_error())
436 } else {
437 Ok(ret as usize)
438 }
439 } else {
440 Err(io::Error::new(io::ErrorKind::NotFound, "sandbox not found"))
441 }
442 }
443
444 pub fn close_stdin(&mut self, id: SandboxId) -> io::Result<()> {
446 if let Some(state) = self.sandboxes.get_mut(&id) {
447 if state.spawned.stdin_fd >= 0 {
448 unsafe { libc::close(state.spawned.stdin_fd) };
449 state.spawned.stdin_fd = -1;
450 }
451 }
452 Ok(())
453 }
454
455 fn calculate_timeout(&self, user_timeout: Option<Duration>) -> Option<Duration> {
456 let now = Instant::now();
457 let nearest_deadline = self.sandboxes.values().map(|s| s.deadline).min();
458
459 match (user_timeout, nearest_deadline) {
460 (Some(user), Some(deadline)) => Some(user.min(deadline.saturating_duration_since(now))),
461 (Some(user), None) => Some(user),
462 (None, Some(deadline)) => Some(deadline.saturating_duration_since(now)),
463 (None, None) => None,
464 }
465 }
466
467 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
470 fn read_pipe(&mut self, sandbox_id: SandboxId, is_stdout: bool, events: &mut Vec<Event>) {
471 let Some(state) = self.sandboxes.get_mut(&sandbox_id) else {
472 return;
473 };
474
475 let fd = if is_stdout {
476 state.spawned.stdout_fd
477 } else {
478 state.spawned.stderr_fd
479 };
480
481 let mut buf = [0u8; 4096];
482 loop {
483 let ret = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) };
484
485 if ret < 0 {
486 let err = io::Error::last_os_error();
487 if err.kind() == io::ErrorKind::WouldBlock {
488 break;
489 }
490 if is_stdout {
491 state.stdout_closed = true;
492 } else {
493 state.stderr_closed = true;
494 }
495 break;
496 } else if ret == 0 {
497 if is_stdout {
498 state.stdout_closed = true;
499 } else {
500 state.stderr_closed = true;
501 }
502 break;
503 } else {
504 let n = ret as usize;
505 let data = buf[..n].to_vec();
506
507 if is_stdout {
508 state.stdout.extend_from_slice(&data);
509 events.push(Event::Stdout {
510 id: sandbox_id,
511 data,
512 });
513 } else {
514 state.stderr.extend_from_slice(&data);
515 events.push(Event::Stderr {
516 id: sandbox_id,
517 data,
518 });
519 }
520
521 let total = state.stdout.len() + state.stderr.len();
522 if total > state.max_output as usize {
523 pidfd_send_signal(&state.spawned.pidfd, Signal::KILL).ok();
524 break;
525 }
526 }
527 }
528 }
529
530 #[allow(clippy::cast_possible_truncation)]
532 fn check_completions(&mut self, events: &mut Vec<Event>) -> io::Result<()> {
533 let now = Instant::now();
534 let mut to_remove = Vec::new();
535
536 for (&id, state) in &mut self.sandboxes {
537 if now >= state.deadline && !state.pidfd_ready {
538 pidfd_send_signal(&state.spawned.pidfd, Signal::KILL).ok();
539 state.pidfd_ready = true;
540 }
541 if state.is_done() {
542 to_remove.push(id);
543 }
544 }
545
546 for id in to_remove {
547 if let Some(state) = self.sandboxes.remove(&id) {
548 self.poll
549 .registry()
550 .deregister(&mut SourceFd(&state.spawned.pidfd.as_raw_fd()))
551 .ok();
552 self.poll
553 .registry()
554 .deregister(&mut SourceFd(&state.spawned.stdout_fd))
555 .ok();
556 self.poll
557 .registry()
558 .deregister(&mut SourceFd(&state.spawned.stderr_fd))
559 .ok();
560
561 let (exit_code, signal) = wait_for_exit(state.spawned.pidfd.as_raw_fd())?;
562 let duration = state.start.elapsed();
563 let timed_out = Instant::now() >= state.deadline;
564
565 let status = if timed_out {
566 Status::Timeout
567 } else if signal.is_some() {
568 Status::Signaled
569 } else if state.stdout.len() + state.stderr.len() > state.max_output as usize {
570 Status::OutputLimitExceeded
571 } else {
572 Status::Exited
573 };
574
575 let output = Output {
576 stdout: state.stdout,
577 stderr: state.stderr,
578 status,
579 duration,
580 exit_code,
581 signal,
582 };
583
584 if timed_out {
585 events.push(Event::Timeout { id, output });
586 } else {
587 events.push(Event::Completed { id, output });
588 }
589 }
590 }
591
592 Ok(())
593 }
594}
595
596fn close_parent_pipe_ends(workspace: &Workspace) {
598 unsafe {
599 libc::close(workspace.pipes.stdin.read.as_raw_fd());
600 libc::close(workspace.pipes.stdout.write.as_raw_fd());
601 libc::close(workspace.pipes.stderr.write.as_raw_fd());
602 }
603}
604
605fn poll_or_kill(fd: RawFd, child_pid: libc::pid_t, msg: &str) -> Result<(), ExecutorError> {
607 let mut pfd = libc::pollfd {
608 fd,
609 events: libc::POLLIN,
610 revents: 0,
611 };
612 if unsafe { libc::poll(&mut pfd, 1, 30000) } <= 0 {
614 unsafe { libc::kill(child_pid, libc::SIGKILL) };
615 return Err(ExecutorError::ChildSetup(msg.into()));
616 }
617 Ok(())
618}
619
620fn sync_with_child(workspace: &Workspace, child_pid: libc::pid_t) -> Result<(), ExecutorError> {
622 let child_ready_fd = workspace.pipes.sync.child_ready_fd();
623 poll_or_kill(child_ready_fd, child_pid, "timeout waiting for child")?;
624
625 let mut value: u64 = 0;
626 if unsafe { libc::read(child_ready_fd, (&mut value as *mut u64).cast(), 8) } != 8 {
627 unsafe { libc::kill(child_pid, libc::SIGKILL) };
628 return Err(ExecutorError::ChildSetup("eventfd read failed".into()));
629 }
630
631 let parent_done_fd = workspace.pipes.sync.parent_done_fd();
632 let signal_value: u64 = 1;
633 if unsafe { libc::write(parent_done_fd, (&signal_value as *const u64).cast(), 8) } != 8 {
634 unsafe { libc::kill(child_pid, libc::SIGKILL) };
635 return Err(ExecutorError::ChildSetup("eventfd write failed".into()));
636 }
637
638 Ok(())
639}
640
641fn spawn_sandbox(plan: Plan) -> Result<SpawnedSandbox, ExecutorError> {
642 let cmd_refs: Vec<&str> = plan.cmd.iter().map(|s| s.as_str()).collect();
643 validate_cmd(&cmd_refs).map_err(ExecutorError::Validation)?;
644
645 if let Err(e) = check::check() {
646 return Err(ExecutorError::SystemCheck(e.to_string()));
647 }
648
649 let exec_info = if let Some(info) = ExecutionInfo::from_plan(&plan) {
650 info
651 } else {
652 let resolved = resolve_binary(&plan.cmd[0])
653 .map_err(|e| ExecutorError::CommandNotFound(e.to_string()))?;
654 ExecutionInfo::from_resolved(resolved)
655 };
656
657 let workspace = Workspace::with_prefix("evalbox-").map_err(ExecutorError::Workspace)?;
658
659 workspace
660 .setup_sandbox_dirs()
661 .map_err(ExecutorError::Workspace)?;
662 for file in &plan.user_files {
663 let work_path = format!("work/{}", file.path);
664 workspace
665 .write_file(&work_path, &file.content, file.executable)
666 .map_err(ExecutorError::Workspace)?;
667 }
668
669 let notify_sockets = if plan.notify_mode != NotifyMode::Disabled {
671 Some(scm_rights::create_socketpair().map_err(ExecutorError::Workspace)?)
672 } else {
673 None
674 };
675
676 let child_pid = unsafe { libc::fork() };
678 if child_pid < 0 {
679 return Err(ExecutorError::Fork(last_errno()));
680 }
681
682 if child_pid == 0 {
683 let child_socket = notify_sockets.map(|(_, child)| child);
684 match child_process(&workspace, &plan, &exec_info, child_socket.as_ref()) {
685 Ok(()) => unsafe { libc::_exit(127) },
686 Err(e) => {
687 writeln!(io::stderr(), "sandbox error: {e}").ok();
688 unsafe { libc::_exit(126) }
689 }
690 }
691 }
692
693 let pid = unsafe { Pid::from_raw_unchecked(child_pid) };
695 let pidfd = pidfd_open(pid, PidfdFlags::empty()).map_err(ExecutorError::Pidfd)?;
696
697 let stdin_write_fd = workspace.pipes.stdin.write.as_raw_fd();
698 let stdout_read_fd = workspace.pipes.stdout.read.as_raw_fd();
699 let stderr_read_fd = workspace.pipes.stderr.read.as_raw_fd();
700
701 close_parent_pipe_ends(&workspace);
702
703 let notify_fd = if let Some((parent_socket, _)) = notify_sockets {
705 poll_or_kill(
706 parent_socket.as_raw_fd(),
707 child_pid,
708 "timeout waiting for notify fd",
709 )?;
710 Some(
711 scm_rights::recv_fd(parent_socket.as_raw_fd())
712 .map_err(|e| ExecutorError::SeccompNotify(e.to_string()))?,
713 )
714 } else {
715 None
716 };
717
718 sync_with_child(&workspace, child_pid)?;
719
720 if let Some(ref stdin_data) = plan.stdin {
722 write_stdin(&workspace, stdin_data).map_err(ExecutorError::Monitor)?;
723 unsafe { libc::close(stdin_write_fd) };
724 }
725
726 set_nonblocking(stdout_read_fd).map_err(ExecutorError::Monitor)?;
728 set_nonblocking(stderr_read_fd).map_err(ExecutorError::Monitor)?;
729
730 unsafe {
732 libc::close(workspace.pipes.sync.child_ready_fd());
733 libc::close(workspace.pipes.sync.parent_done_fd());
734 }
735
736 Ok(SpawnedSandbox {
737 pidfd,
738 stdin_fd: if plan.stdin.is_some() {
739 -1
740 } else {
741 stdin_write_fd
742 },
743 stdout_fd: stdout_read_fd,
744 stderr_fd: stderr_read_fd,
745 notify_fd,
746 workspace: std::mem::ManuallyDrop::new(workspace),
747 })
748}
749
750fn blocking_parent(
751 child_pid: libc::pid_t,
752 pidfd: OwnedFd,
753 _notify_fd: Option<OwnedFd>,
754 workspace: Workspace,
755 plan: Plan,
756) -> Result<Output, ExecutorError> {
757 let workspace = std::mem::ManuallyDrop::new(workspace);
761
762 close_parent_pipe_ends(&workspace);
763
764 sync_with_child(&workspace, child_pid)?;
765
766 if let Some(ref stdin_data) = plan.stdin {
767 write_stdin(&workspace, stdin_data).map_err(ExecutorError::Monitor)?;
768 }
769 unsafe { libc::close(workspace.pipes.stdin.write.as_raw_fd()) };
771
772 let result = monitor(pidfd, &workspace, &plan).map_err(ExecutorError::Monitor);
773
774 unsafe {
775 libc::close(workspace.pipes.stdout.read.as_raw_fd());
776 libc::close(workspace.pipes.stderr.read.as_raw_fd());
777 libc::close(workspace.pipes.sync.child_ready_fd());
778 libc::close(workspace.pipes.sync.parent_done_fd());
779 }
780
781 let _ = std::fs::remove_dir_all(workspace.root());
784
785 result
786}
787
788#[allow(clippy::cast_possible_truncation)]
802fn child_process(
803 workspace: &Workspace,
804 plan: &Plan,
805 exec_info: &ExecutionInfo,
806 notify_socket: Option<&OwnedFd>,
807) -> Result<(), ExecutorError> {
808 unsafe {
810 libc::close(workspace.pipes.stdin.write.as_raw_fd());
811 libc::close(workspace.pipes.stdout.read.as_raw_fd());
812 libc::close(workspace.pipes.stderr.read.as_raw_fd());
813 }
814
815 setup_stdio(workspace)?;
817
818 let work_dir = workspace.root().join("work");
820 let work_cstr = CString::new(work_dir.to_string_lossy().as_bytes())
821 .map_err(|_| ExecutorError::Exec(Errno::INVAL))?;
822 if unsafe { libc::chdir(work_cstr.as_ptr()) } != 0 {
823 return Err(ExecutorError::Exec(last_errno()));
824 }
825
826 let extra_paths: Vec<&str> = exec_info
828 .extra_mounts
829 .iter()
830 .filter_map(|m| m.source.to_str())
831 .collect();
832 lockdown(plan, workspace.root(), &extra_paths).map_err(ExecutorError::Lockdown)?;
833
834 if plan.notify_mode != NotifyMode::Disabled {
836 let nfs = notify_fs_syscalls();
837 let notify_filter = build_notify_filter(&nfs);
838 let fprog = SockFprog {
839 len: notify_filter.len() as u16,
840 filter: notify_filter.as_ptr(),
841 };
842 let listener_fd = unsafe { seccomp_set_mode_filter_listener(&fprog) }.map_err(|e| {
843 ExecutorError::SeccompNotify(format!("failed to install notify filter: {e}"))
844 })?;
845
846 if let Some(sock) = notify_socket {
848 scm_rights::send_fd(sock.as_raw_fd(), listener_fd.as_raw_fd()).map_err(|e| {
849 ExecutorError::SeccompNotify(format!("failed to send listener fd: {e}"))
850 })?;
851 }
852 }
853
854 apply_seccomp(plan)?;
856
857 let child_ready_fd = workspace.pipes.sync.child_ready_fd();
859 let signal_value: u64 = 1;
860 if unsafe { libc::write(child_ready_fd, (&signal_value as *const u64).cast(), 8) } != 8 {
861 return Err(ExecutorError::ChildSetup("eventfd write failed".into()));
862 }
863
864 let parent_done_fd = workspace.pipes.sync.parent_done_fd();
866 let mut value: u64 = 0;
867 if unsafe { libc::read(parent_done_fd, (&mut value as *mut u64).cast(), 8) } != 8 {
868 return Err(ExecutorError::ChildSetup("eventfd read failed".into()));
869 }
870
871 close_extra_fds();
873
874 exec_command(plan, exec_info)
876}
877
878fn setup_stdio(workspace: &Workspace) -> Result<(), ExecutorError> {
879 let stdin_fd = workspace.pipes.stdin.read.as_raw_fd();
880 let stdout_fd = workspace.pipes.stdout.write.as_raw_fd();
881 let stderr_fd = workspace.pipes.stderr.write.as_raw_fd();
882
883 unsafe {
885 libc::close(0);
886 libc::close(1);
887 libc::close(2);
888 if libc::dup2(stdin_fd, 0) < 0 {
889 return Err(ExecutorError::Exec(last_errno()));
890 }
891 if libc::dup2(stdout_fd, 1) < 0 {
892 return Err(ExecutorError::Exec(last_errno()));
893 }
894 if libc::dup2(stderr_fd, 2) < 0 {
895 return Err(ExecutorError::Exec(last_errno()));
896 }
897 }
898 Ok(())
899}
900
901#[allow(clippy::cast_possible_truncation)]
903fn apply_seccomp(plan: &Plan) -> Result<(), ExecutorError> {
904 let base = default_whitelist();
905 let whitelist: Vec<i64> = if let Some(ref syscalls) = plan.syscalls {
906 let mut wl_set: HashSet<i64> = base.into_iter().collect();
907 for s in &syscalls.denied {
908 wl_set.remove(s);
909 }
910 for s in &syscalls.allowed {
911 wl_set.insert(*s);
912 }
913 wl_set.into_iter().collect()
914 } else {
915 base
916 };
917
918 let filter = build_whitelist_filter(&whitelist);
919 let fprog = SockFprog {
920 len: filter.len() as u16,
921 filter: filter.as_ptr(),
922 };
923 unsafe { seccomp_set_mode_filter(&fprog) }
924 .map_err(|e| ExecutorError::Lockdown(LockdownError::Seccomp(e)))?;
925 Ok(())
926}
927
928fn exec_command(plan: &Plan, exec_info: &ExecutionInfo) -> Result<(), ExecutorError> {
929 let cmd_path = CString::new(exec_info.binary_path.to_string_lossy().as_bytes())
930 .map_err(|_| ExecutorError::Exec(Errno::INVAL))?;
931
932 let mut argv: Vec<CString> = Vec::with_capacity(plan.cmd.len());
933 argv.push(cmd_path.clone());
934 for arg in plan.cmd.iter().skip(1) {
935 argv.push(CString::new(arg.as_bytes()).map_err(|_| ExecutorError::Exec(Errno::INVAL))?);
936 }
937
938 let argv_ptrs: Vec<*const libc::c_char> = argv
939 .iter()
940 .map(|s| s.as_ptr())
941 .chain(std::iter::once(std::ptr::null()))
942 .collect();
943
944 let envp: Vec<CString> = plan
945 .env
946 .iter()
947 .map(|(k, v)| CString::new(format!("{k}={v}")))
948 .collect::<Result<Vec<_>, _>>()
949 .map_err(|_| ExecutorError::Exec(Errno::INVAL))?;
950
951 let envp_ptrs: Vec<*const libc::c_char> = envp
952 .iter()
953 .map(|s| s.as_ptr())
954 .chain(std::iter::once(std::ptr::null()))
955 .collect();
956
957 unsafe { libc::execve(cmd_path.as_ptr(), argv_ptrs.as_ptr(), envp_ptrs.as_ptr()) };
958
959 Err(ExecutorError::Exec(last_errno()))
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965
966 #[test]
967 fn token_encoding() {
968 let token = encode_token(42, TOKEN_TYPE_STDOUT);
969 let (id, ty) = decode_token(token);
970 assert_eq!(id.0, 42);
971 assert_eq!(ty, TOKEN_TYPE_STDOUT);
972 }
973
974 #[test]
975 fn sandbox_id_display() {
976 let id = SandboxId(123);
977 assert_eq!(format!("{id}"), "Sandbox(123)");
978 }
979}