1use 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#[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#[derive(Debug)]
120pub enum Event {
121 Completed { id: SandboxId, output: Output },
123 Timeout { id: SandboxId, output: Output },
125 Stdout { id: SandboxId, data: Vec<u8> },
127 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
157struct 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
167struct 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
186const 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 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 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 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 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 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 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 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 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 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 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_nonblocking(stdout_read_fd).map_err(ExecutorError::Monitor)?;
624 set_nonblocking(stderr_read_fd).map_err(ExecutorError::Monitor)?;
625
626 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}