1use std::ffi::{CStr, CString};
4use std::mem::MaybeUninit;
5use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
6use std::os::unix::process::CommandExt;
7use std::process::{Command, Stdio};
8use std::sync::Arc;
9use std::{iter, mem, ptr};
10
11use nix::pty;
12use nix::sys::signal::Signal;
13use tokio::io::AsyncReadExt;
14use tokio::sync::mpsc;
15
16use microsandbox_protocol::exec::{ExecFailed, ExecFailureKind, ExecRequest};
17
18use crate::config::SecurityProfile;
19use crate::error::{AgentdError, AgentdResult};
20use crate::process::{ProcessExitWatcher, ProcessIdentity, ProcessManager};
21use crate::rlimit;
22
23const LINUX_CAPABILITY_VERSION_3: u32 = 0x20080522;
28const CAP_SYS_ADMIN: u32 = 21;
29const CAP_WORD_BITS: u32 = 32;
30const PR_CAPBSET_DROP: libc::c_int = 24;
31const PR_CAP_AMBIENT: libc::c_int = 47;
32const PR_CAP_AMBIENT_CLEAR_ALL: libc::c_int = 4;
33const DEFAULT_USER_SPEC: &str = "0:0";
34
35fn errno_name(e: i32) -> Option<&'static str> {
43 match e {
44 libc::E2BIG => Some("E2BIG"),
45 libc::EACCES => Some("EACCES"),
46 libc::EAGAIN => Some("EAGAIN"),
47 libc::EBUSY => Some("EBUSY"),
48 libc::EFAULT => Some("EFAULT"),
49 libc::EINVAL => Some("EINVAL"),
50 libc::EIO => Some("EIO"),
51 libc::EISDIR => Some("EISDIR"),
52 libc::ELOOP => Some("ELOOP"),
53 libc::EMFILE => Some("EMFILE"),
54 libc::ENAMETOOLONG => Some("ENAMETOOLONG"),
55 libc::ENFILE => Some("ENFILE"),
56 libc::ENOENT => Some("ENOENT"),
57 libc::ENOEXEC => Some("ENOEXEC"),
58 libc::ENOMEM => Some("ENOMEM"),
59 libc::ENOSYS => Some("ENOSYS"),
60 libc::ENOTDIR => Some("ENOTDIR"),
61 libc::ENXIO => Some("ENXIO"),
62 libc::EPERM => Some("EPERM"),
63 libc::ETXTBSY => Some("ETXTBSY"),
64 _ => None,
65 }
66}
67
68fn classify_spawn_errno(errno: i32) -> ExecFailureKind {
80 match errno {
81 libc::ENOENT => ExecFailureKind::NotFound,
82 libc::ENOTDIR => ExecFailureKind::BadCwd,
83 libc::EACCES | libc::EPERM => ExecFailureKind::PermissionDenied,
84 libc::ENOEXEC => ExecFailureKind::NotExecutable,
85 libc::EISDIR => ExecFailureKind::NotExecutable,
86 libc::ETXTBSY => ExecFailureKind::NotExecutable,
87 libc::E2BIG | libc::ELOOP | libc::ENAMETOOLONG | libc::EFAULT => ExecFailureKind::BadArgs,
88 libc::EMFILE | libc::ENFILE => ExecFailureKind::ResourceLimit,
89 libc::EAGAIN => ExecFailureKind::ResourceLimit,
90 libc::ENOMEM => ExecFailureKind::OutOfMemory,
91 libc::EINVAL => ExecFailureKind::Other,
92 _ => ExecFailureKind::Other,
93 }
94}
95
96fn exec_failed_from_io_error(err: &std::io::Error, cmd: &str, stage: &str) -> ExecFailed {
98 let errno = err.raw_os_error();
99 let kind = errno
100 .map(classify_spawn_errno)
101 .unwrap_or(ExecFailureKind::Other);
102 let errno_name = errno.and_then(errno_name).map(str::to_string);
103 let message = format!("spawn {cmd:?}: {err}");
104 ExecFailed {
105 kind,
106 errno,
107 errno_name,
108 message,
109 stage: Some(stage.to_string()),
110 }
111}
112
113#[derive(Debug)]
122pub struct ExecSession {
123 process_identity: ProcessIdentity,
125
126 process_manager: Arc<ProcessManager>,
128
129 pty_master: Option<OwnedFd>,
131
132 stdin: Option<tokio::process::ChildStdin>,
134}
135
136pub enum SessionOutput {
138 Stdout(Vec<u8>),
140
141 Stderr(Vec<u8>),
143
144 Exited(i32),
146
147 Raw(RawSessionOutput),
149}
150
151pub struct RawSessionOutput {
153 pub frame: Vec<u8>,
155
156 pub activity: RawActivity,
158
159 pub completion: Option<RawSessionCompletion>,
161}
162
163#[derive(Debug, Clone, Copy, Default)]
165pub struct RawActivity {
166 pub guest_message: bool,
168
169 pub fs_bytes: usize,
171
172 pub tcp_bytes: usize,
174}
175
176#[derive(Debug, Clone, Copy)]
178pub enum RawSessionCompletion {
179 FsRead,
181
182 Tcp,
184}
185
186struct ResolvedUser {
187 uid: libc::uid_t,
188 gid: libc::gid_t,
189 initgroups_user: Option<CString>,
190 home_dir: Option<CString>,
191}
192
193struct PasswdEntry {
194 name: String,
195 uid: libc::uid_t,
196 gid: libc::gid_t,
197 home_dir: Option<String>,
198}
199
200struct GroupEntry {
201 gid: libc::gid_t,
202}
203
204struct ExecErrorPipe {
205 read_end: OwnedFd,
206 write_end: OwnedFd,
207}
208
209struct PipedProcess {
211 stdin: Option<tokio::process::ChildStdin>,
212 stdout: Option<tokio::process::ChildStdout>,
213 stderr: Option<tokio::process::ChildStderr>,
214 exit_watcher: ProcessExitWatcher,
215}
216
217#[repr(C)]
218#[derive(Clone, Copy)]
219struct CapUserHeader {
220 version: u32,
221 pid: libc::c_int,
222}
223
224#[repr(C)]
225#[derive(Clone, Copy)]
226struct CapUserData {
227 effective: u32,
228 permitted: u32,
229 inheritable: u32,
230}
231
232impl RawSessionOutput {
237 pub fn new(
239 frame: Vec<u8>,
240 activity: RawActivity,
241 completion: Option<RawSessionCompletion>,
242 ) -> Self {
243 Self {
244 frame,
245 activity,
246 completion,
247 }
248 }
249}
250
251impl RawActivity {
252 pub fn guest_message() -> Self {
254 Self {
255 guest_message: true,
256 ..Self::default()
257 }
258 }
259
260 pub fn fs_bytes(len: usize) -> Self {
262 Self {
263 guest_message: true,
264 fs_bytes: len,
265 tcp_bytes: 0,
266 }
267 }
268
269 pub fn tcp_bytes(len: usize) -> Self {
271 Self {
272 guest_message: true,
273 fs_bytes: 0,
274 tcp_bytes: len,
275 }
276 }
277}
278
279impl ExecSession {
280 pub fn spawn(
285 id: u32,
286 req: &ExecRequest,
287 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
288 default_user: Option<&str>,
289 security_profile: SecurityProfile,
290 ) -> AgentdResult<Self> {
291 let process_manager = ProcessManager::get()?;
292 if req.tty {
293 Self::spawn_pty(
294 id,
295 req,
296 tx,
297 default_user,
298 security_profile,
299 &process_manager,
300 )
301 } else {
302 Self::spawn_pipe(
303 id,
304 req,
305 tx,
306 default_user,
307 security_profile,
308 &process_manager,
309 )
310 }
311 }
312
313 pub fn pid(&self) -> u32 {
315 self.process_identity.pid() as u32
316 }
317
318 pub async fn write_stdin(&self, data: &[u8]) -> AgentdResult<()> {
320 if let Some(ref master) = self.pty_master {
321 blocking_write_fd(master.as_raw_fd(), data).await
322 } else if let Some(ref stdin) = self.stdin {
323 blocking_write_fd(stdin.as_raw_fd(), data).await
324 } else {
325 Ok(())
326 }
327 }
328
329 pub fn resize(&self, rows: u16, cols: u16) -> AgentdResult<()> {
331 if let Some(ref master) = self.pty_master {
332 let ws = libc::winsize {
333 ws_row: rows,
334 ws_col: cols,
335 ws_xpixel: 0,
336 ws_ypixel: 0,
337 };
338 let ret = unsafe { libc::ioctl(master.as_raw_fd(), libc::TIOCSWINSZ, &ws) };
339 if ret < 0 {
340 return Err(std::io::Error::last_os_error().into());
341 }
342 }
343 Ok(())
344 }
345
346 pub fn send_signal(&self, signum: i32) -> AgentdResult<()> {
354 let sig = Signal::try_from(signum)
355 .map_err(|e| AgentdError::ExecSession(format!("invalid signal {signum}: {e}")))?;
356 self.process_manager
357 .signal_process_group(self.process_identity, sig as i32)
358 }
359
360 pub fn close_stdin(&mut self) {
365 self.stdin.take();
366 }
367}
368
369impl ExecSession {
370 fn spawn_pty(
372 id: u32,
373 req: &ExecRequest,
374 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
375 default_user: Option<&str>,
376 security_profile: SecurityProfile,
377 process_manager: &Arc<ProcessManager>,
378 ) -> AgentdResult<Self> {
379 let pty = pty::openpty(None, None)?;
380 let err_pipe = new_exec_error_pipe()?;
381
382 let ws = libc::winsize {
384 ws_row: req.rows,
385 ws_col: req.cols,
386 ws_xpixel: 0,
387 ws_ypixel: 0,
388 };
389 let ret = unsafe { libc::ioctl(pty.master.as_raw_fd(), libc::TIOCSWINSZ, &ws) };
390 if ret < 0 {
391 return Err(std::io::Error::last_os_error().into());
392 }
393
394 let slave_fd = pty.slave.as_raw_fd();
395
396 let c_cmd = CString::new(req.cmd.as_str())
398 .map_err(|e| AgentdError::ExecSession(format!("invalid command: {e}")))?;
399 let mut c_args: Vec<CString> = vec![c_cmd.clone()];
400 for arg in &req.args {
401 c_args.push(
402 CString::new(arg.as_str())
403 .map_err(|e| AgentdError::ExecSession(format!("invalid arg: {e}")))?,
404 );
405 }
406
407 let argv_ptrs: Vec<*const libc::c_char> = c_args
409 .iter()
410 .map(|s| s.as_ptr())
411 .chain(iter::once(ptr::null()))
412 .collect();
413
414 let c_env: Vec<(CString, CString)> = req
416 .env
417 .iter()
418 .filter_map(|var| {
419 let (key, val) = var.split_once('=')?;
420 let k = CString::new(key).ok()?;
421 let v = CString::new(val).ok()?;
422 Some((k, v))
423 })
424 .collect();
425
426 let c_cwd = req
428 .cwd
429 .as_ref()
430 .map(|dir| CString::new(dir.as_str()))
431 .transpose()
432 .map_err(|e| AgentdError::ExecSession(format!("invalid cwd: {e}")))?;
433
434 let resolved_user = resolve_requested_user(req, default_user)?;
435 let default_home = default_home_dir(req, resolved_user.as_ref())?;
436 let home_key = default_home
437 .as_ref()
438 .map(|_| {
439 CString::new("HOME")
440 .map_err(|e| AgentdError::ExecSession(format!("invalid home env key: {e}")))
441 })
442 .transpose()?;
443
444 let parsed_rlimits = rlimit::to_libc(&req.rlimits);
446
447 let spawn_guard = process_manager.spawn_guard()?;
450
451 let pid = unsafe { libc::fork() };
453 if pid < 0 {
454 let io_err = std::io::Error::last_os_error();
455 return Err(AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
456 &io_err, &req.cmd, "fork",
457 )));
458 }
459
460 #[allow(unreachable_code)]
461 if pid == 0 {
462 drop(pty.master);
464 drop(err_pipe.read_end);
465
466 if unsafe { libc::setsid() } < 0 {
468 unsafe { libc::_exit(1) };
469 }
470
471 if unsafe { libc::ioctl(slave_fd, libc::TIOCSCTTY, 0) } < 0 {
473 unsafe { libc::_exit(1) };
474 }
475
476 unsafe {
478 if libc::dup2(slave_fd, 0) < 0 {
479 libc::_exit(1);
480 }
481 if libc::dup2(slave_fd, 1) < 0 {
482 libc::_exit(1);
483 }
484 if libc::dup2(slave_fd, 2) < 0 {
485 libc::_exit(1);
486 }
487 if slave_fd > 2 {
488 libc::close(slave_fd);
489 }
490 }
491
492 for (key, val) in &c_env {
494 unsafe {
495 libc::setenv(key.as_ptr(), val.as_ptr(), 1);
496 }
497 }
498
499 if let Some(ref dir) = c_cwd {
501 unsafe {
502 libc::chdir(dir.as_ptr());
503 }
504 }
505
506 if apply_exec_security_profile(security_profile).is_err() {
507 unsafe { libc::_exit(1) };
508 }
509
510 if let Some(ref user) = resolved_user
511 && apply_resolved_user(user).is_err()
512 {
513 unsafe { libc::_exit(1) };
514 }
515
516 if let (Some(key), Some(home)) = (&home_key, &default_home) {
517 unsafe {
518 libc::setenv(key.as_ptr(), home.as_ptr(), 1);
519 }
520 }
521
522 for (resource, limit) in &parsed_rlimits {
524 if unsafe { libc::setrlimit(*resource as _, limit) } != 0 {
525 unsafe { libc::_exit(1) };
526 }
527 }
528
529 unsafe {
531 libc::execvp(argv_ptrs[0], argv_ptrs.as_ptr());
532 }
533
534 write_exec_error_and_exit(err_pipe.write_end.as_raw_fd());
536 }
537
538 drop(pty.slave);
540 drop(err_pipe.write_end);
541 let exit_watcher = spawn_guard.track(pid)?;
542 let process_identity = exit_watcher.identity();
543
544 match read_exec_error(err_pipe.read_end.as_raw_fd()) {
545 Ok(Some(exec_errno)) => {
546 drop(exit_watcher);
547 process_manager.release(process_identity);
548 let io_err = std::io::Error::from_raw_os_error(exec_errno);
549 return Err(AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
550 &io_err, &req.cmd, "execvp",
551 )));
552 }
553 Ok(None) => {}
554 Err(error) => {
555 let _ =
556 process_manager.signal_process_group(process_identity, Signal::SIGKILL as i32);
557 process_manager.release(process_identity);
558 return Err(error);
559 }
560 }
561
562 let reader_fd = unsafe { libc::dup(pty.master.as_raw_fd()) };
564 if reader_fd < 0 {
565 let _ = process_manager.signal_process_group(process_identity, Signal::SIGKILL as i32);
566 process_manager.release(process_identity);
567 return Err(std::io::Error::last_os_error().into());
568 }
569 let reader_fd = unsafe { OwnedFd::from_raw_fd(reader_fd) };
570
571 tokio::spawn(pty_reader_task(id, reader_fd, exit_watcher, tx));
573
574 Ok(Self {
575 process_identity,
576 process_manager: Arc::clone(process_manager),
577 pty_master: Some(pty.master),
578 stdin: None,
579 })
580 }
581
582 fn spawn_pipe(
584 id: u32,
585 req: &ExecRequest,
586 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
587 default_user: Option<&str>,
588 security_profile: SecurityProfile,
589 process_manager: &Arc<ProcessManager>,
590 ) -> AgentdResult<Self> {
591 let mut cmd = Command::new(&req.cmd);
592 cmd.args(&req.args)
593 .stdin(Stdio::piped())
594 .stdout(Stdio::piped())
595 .stderr(Stdio::piped());
596
597 for var in &req.env {
598 if let Some((key, val)) = var.split_once('=') {
599 cmd.env(key, val);
600 }
601 }
602
603 if let Some(ref dir) = req.cwd {
604 cmd.current_dir(dir);
605 }
606
607 let resolved_user = resolve_requested_user(req, default_user)?;
608 if let Some(home) = default_home_dir(req, resolved_user.as_ref())? {
609 cmd.env("HOME", home.to_string_lossy().into_owned());
610 }
611
612 let parsed_rlimits = rlimit::to_libc(&req.rlimits);
614 unsafe {
615 cmd.pre_exec(move || {
616 if libc::setsid() < 0 {
621 return Err(std::io::Error::last_os_error());
622 }
623 apply_exec_security_profile(security_profile).map_err(agentd_to_io_error)?;
624 if let Some(ref user) = resolved_user {
625 apply_resolved_user(user).map_err(agentd_to_io_error)?;
626 }
627 for (resource, limit) in &parsed_rlimits {
628 if libc::setrlimit(*resource as _, limit) != 0 {
629 return Err(std::io::Error::last_os_error());
630 }
631 }
632 Ok(())
633 });
634 }
635
636 let PipedProcess {
637 stdin,
638 stdout,
639 stderr,
640 exit_watcher,
641 } = spawn_piped_process(cmd, process_manager)?;
642 let process_identity = exit_watcher.identity();
643
644 tokio::spawn(pipe_reader_task(id, stdout, stderr, exit_watcher, tx));
646
647 Ok(Self {
648 process_identity,
649 process_manager: Arc::clone(process_manager),
650 pty_master: None,
651 stdin,
652 })
653 }
654}
655
656impl Drop for ExecSession {
661 fn drop(&mut self) {
662 self.process_manager.release(self.process_identity);
665 }
666}
667
668fn spawn_piped_process(
673 mut command: Command,
674 process_manager: &ProcessManager,
675) -> AgentdResult<PipedProcess> {
676 let cmd_label = command.get_program().to_string_lossy().into_owned();
677
678 let spawn_guard = process_manager.spawn_guard()?;
681 let mut child = command.spawn().map_err(|error| {
682 AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
683 &error,
684 &cmd_label,
685 "Command::spawn",
686 ))
687 })?;
688 let pid = child.id() as i32;
689 let exit_watcher = spawn_guard.track(pid)?;
690 let process_identity = exit_watcher.identity();
691
692 let stdio = (|| {
693 let stdin = child
694 .stdin
695 .take()
696 .map(tokio::process::ChildStdin::from_std)
697 .transpose()?;
698 let stdout = child
699 .stdout
700 .take()
701 .map(tokio::process::ChildStdout::from_std)
702 .transpose()?;
703 let stderr = child
704 .stderr
705 .take()
706 .map(tokio::process::ChildStderr::from_std)
707 .transpose()?;
708 Ok::<_, std::io::Error>((stdin, stdout, stderr))
709 })();
710 let (stdin, stdout, stderr) = stdio.map_err(|error| {
711 let _ = process_manager.signal_process_group(process_identity, Signal::SIGKILL as i32);
715 process_manager.release(process_identity);
716 AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
717 &error,
718 &cmd_label,
719 "Command::spawn",
720 ))
721 })?;
722
723 drop(child);
727
728 Ok(PipedProcess {
729 stdin,
730 stdout,
731 stderr,
732 exit_watcher,
733 })
734}
735
736fn new_exec_error_pipe() -> AgentdResult<ExecErrorPipe> {
737 let mut fds = [0; 2];
738 let ret = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
739 if ret != 0 {
740 return Err(std::io::Error::last_os_error().into());
741 }
742
743 Ok(ExecErrorPipe {
744 read_end: unsafe { OwnedFd::from_raw_fd(fds[0]) },
745 write_end: unsafe { OwnedFd::from_raw_fd(fds[1]) },
746 })
747}
748
749fn write_exec_error_and_exit(err_fd: RawFd) -> ! {
750 let errno = unsafe { *libc::__errno_location() };
751 let bytes = errno.to_ne_bytes();
752 let _ = unsafe { libc::write(err_fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) };
753 unsafe { libc::_exit(127) }
754}
755
756fn read_exec_error(err_fd: RawFd) -> AgentdResult<Option<i32>> {
757 let mut buf = [0u8; mem::size_of::<i32>()];
758 let n = unsafe { libc::read(err_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
759 if n < 0 {
760 return Err(std::io::Error::last_os_error().into());
761 }
762 if n == 0 {
763 return Ok(None);
764 }
765 if n as usize != buf.len() {
766 return Err(AgentdError::ExecSession(format!(
767 "short exec error report: expected {} bytes, got {n}",
768 buf.len()
769 )));
770 }
771 Ok(Some(i32::from_ne_bytes(buf)))
772}
773
774fn apply_exec_security_profile(profile: SecurityProfile) -> AgentdResult<()> {
775 match profile {
776 SecurityProfile::Default => Ok(()),
777 SecurityProfile::Restricted => drop_mount_admin_privileges(),
778 }
779}
780
781fn drop_mount_admin_privileges() -> AgentdResult<()> {
782 if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
783 return Err(std::io::Error::last_os_error().into());
784 }
785
786 let ret = unsafe { libc::prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) };
787 if ret != 0 {
788 let err = std::io::Error::last_os_error();
789 if err.raw_os_error() != Some(libc::EINVAL) {
790 return Err(err.into());
791 }
792 }
793
794 let mut header = CapUserHeader {
795 version: LINUX_CAPABILITY_VERSION_3,
796 pid: 0,
797 };
798 let mut data = [CapUserData {
799 effective: 0,
800 permitted: 0,
801 inheritable: 0,
802 }; 2];
803
804 if unsafe { libc::syscall(libc::SYS_capget, &mut header, data.as_mut_ptr()) } != 0 {
805 return Err(std::io::Error::last_os_error().into());
806 }
807
808 let index = (CAP_SYS_ADMIN / CAP_WORD_BITS) as usize;
809 let mask = 1u32 << (CAP_SYS_ADMIN % CAP_WORD_BITS);
810 let had_sys_admin = data[index].effective & mask != 0
811 || data[index].permitted & mask != 0
812 || data[index].inheritable & mask != 0;
813
814 if had_sys_admin {
815 data[index].effective &= !mask;
816 data[index].permitted &= !mask;
817 data[index].inheritable &= !mask;
818
819 if unsafe { libc::syscall(libc::SYS_capset, &mut header, data.as_ptr()) } != 0 {
820 return Err(std::io::Error::last_os_error().into());
821 }
822 }
823
824 let ret = unsafe { libc::prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN, 0, 0, 0) };
825 if ret != 0 {
826 let err = std::io::Error::last_os_error();
827 let errno = err.raw_os_error();
828 let already_unprivileged = !had_sys_admin && errno == Some(libc::EPERM);
830 if errno != Some(libc::EINVAL) && !already_unprivileged {
831 return Err(err.into());
832 }
833 }
834
835 Ok(())
836}
837
838pub(crate) fn resolve_default_user(default_user: Option<&str>) -> AgentdResult<(u32, u32)> {
839 let Some(spec) = default_user
840 .map(str::trim)
841 .filter(|value| !value.is_empty())
842 else {
843 return Ok((0, 0));
844 };
845
846 let resolved = resolve_user_spec(spec)?;
847 Ok((resolved.uid, resolved.gid))
848}
849
850fn resolve_requested_user(
851 req: &ExecRequest,
852 default_user: Option<&str>,
853) -> AgentdResult<Option<ResolvedUser>> {
854 let default_user = default_user
855 .map(str::trim)
856 .filter(|value| !value.is_empty());
857 let requested = req
858 .user
859 .as_deref()
860 .map(str::trim)
861 .filter(|value| !value.is_empty())
862 .or(default_user);
863
864 requested.map(resolve_user_spec).transpose()
865}
866
867fn resolve_user_spec(spec: &str) -> AgentdResult<ResolvedUser> {
868 let (user_part, group_part) = match spec.split_once(':') {
869 Some((user, group)) => (user.trim(), Some(group.trim())),
870 None => (spec.trim(), None),
871 };
872
873 if user_part.is_empty() {
874 return Err(AgentdError::ExecSession("user spec has empty user".into()));
875 }
876
877 let passwd = if let Ok(uid) = parse_id(user_part) {
878 lookup_passwd_by_uid(uid)?
879 } else {
880 lookup_passwd_by_name(user_part)?
881 .ok_or_else(|| AgentdError::ExecSession(format!("guest user not found: {user_part}")))?
882 .into()
883 };
884
885 let (uid, passwd_entry) = match passwd {
886 ResolvedUserLookup::Known(entry) => (entry.uid, Some(entry)),
887 ResolvedUserLookup::Numeric(uid) => (uid, None),
888 };
889
890 let gid = match group_part {
891 Some("") => {
892 return Err(AgentdError::ExecSession("user spec has empty group".into()));
893 }
894 Some(group) => resolve_group_spec(group)?,
895 None => passwd_entry
896 .as_ref()
897 .map(|entry| entry.gid)
898 .unwrap_or_else(|| unsafe { libc::getgid() }),
899 };
900
901 let initgroups_user = passwd_entry
902 .as_ref()
903 .map(|entry| CString::new(entry.name.as_str()))
904 .transpose()
905 .map_err(|e| AgentdError::ExecSession(format!("invalid guest user name: {e}")))?;
906
907 Ok(ResolvedUser {
908 uid,
909 gid,
910 initgroups_user,
911 home_dir: passwd_entry
912 .as_ref()
913 .and_then(|entry| entry.home_dir.as_deref())
914 .map(CString::new)
915 .transpose()
916 .map_err(|e| AgentdError::ExecSession(format!("invalid guest home directory: {e}")))?,
917 })
918}
919
920enum ResolvedUserLookup {
921 Known(PasswdEntry),
922 Numeric(libc::uid_t),
923}
924
925impl From<PasswdEntry> for ResolvedUserLookup {
926 fn from(value: PasswdEntry) -> Self {
927 Self::Known(value)
928 }
929}
930
931fn resolve_group_spec(spec: &str) -> AgentdResult<libc::gid_t> {
932 if let Ok(gid) = parse_id(spec) {
933 return Ok(gid);
934 }
935
936 lookup_group_by_name(spec)?
937 .map(|entry| entry.gid)
938 .ok_or_else(|| AgentdError::ExecSession(format!("guest group not found: {spec}")))
939}
940
941fn parse_id(value: &str) -> Result<u32, std::num::ParseIntError> {
942 value.parse::<u32>()
943}
944
945fn lookup_passwd_by_name(name: &str) -> AgentdResult<Option<PasswdEntry>> {
946 let name = CString::new(name)
947 .map_err(|e| AgentdError::ExecSession(format!("invalid guest user name: {e}")))?;
948 let mut pwd = MaybeUninit::<libc::passwd>::uninit();
949 let mut result = ptr::null_mut();
950 let mut buf = vec![0u8; lookup_buffer_len()];
951 let rc = unsafe {
952 libc::getpwnam_r(
953 name.as_ptr(),
954 pwd.as_mut_ptr(),
955 buf.as_mut_ptr().cast(),
956 buf.len(),
957 &mut result,
958 )
959 };
960 if rc != 0 {
961 return Err(AgentdError::ExecSession(format!(
962 "failed to resolve guest user {name:?}: {}",
963 std::io::Error::from_raw_os_error(rc)
964 )));
965 }
966 if result.is_null() {
967 return Ok(None);
968 }
969
970 let pwd = unsafe { pwd.assume_init() };
971 let name = unsafe { CStr::from_ptr(pwd.pw_name) }
972 .to_string_lossy()
973 .into_owned();
974 let home_dir = unsafe { CStr::from_ptr(pwd.pw_dir) }
975 .to_string_lossy()
976 .into_owned();
977 Ok(Some(PasswdEntry {
978 name,
979 uid: pwd.pw_uid,
980 gid: pwd.pw_gid,
981 home_dir: (!home_dir.is_empty()).then_some(home_dir),
982 }))
983}
984
985fn lookup_passwd_by_uid(uid: libc::uid_t) -> AgentdResult<ResolvedUserLookup> {
986 let mut pwd = MaybeUninit::<libc::passwd>::uninit();
987 let mut result = ptr::null_mut();
988 let mut buf = vec![0u8; lookup_buffer_len()];
989 let rc = unsafe {
990 libc::getpwuid_r(
991 uid,
992 pwd.as_mut_ptr(),
993 buf.as_mut_ptr().cast(),
994 buf.len(),
995 &mut result,
996 )
997 };
998 if rc != 0 {
999 return Err(AgentdError::ExecSession(format!(
1000 "failed to resolve guest uid {uid}: {}",
1001 std::io::Error::from_raw_os_error(rc)
1002 )));
1003 }
1004 if result.is_null() {
1005 return Ok(ResolvedUserLookup::Numeric(uid));
1006 }
1007
1008 let pwd = unsafe { pwd.assume_init() };
1009 let name = unsafe { CStr::from_ptr(pwd.pw_name) }
1010 .to_string_lossy()
1011 .into_owned();
1012 let home_dir = unsafe { CStr::from_ptr(pwd.pw_dir) }
1013 .to_string_lossy()
1014 .into_owned();
1015 Ok(ResolvedUserLookup::Known(PasswdEntry {
1016 name,
1017 uid: pwd.pw_uid,
1018 gid: pwd.pw_gid,
1019 home_dir: (!home_dir.is_empty()).then_some(home_dir),
1020 }))
1021}
1022
1023fn lookup_group_by_name(name: &str) -> AgentdResult<Option<GroupEntry>> {
1024 let name = CString::new(name)
1025 .map_err(|e| AgentdError::ExecSession(format!("invalid guest group name: {e}")))?;
1026 let mut grp = MaybeUninit::<libc::group>::uninit();
1027 let mut result = ptr::null_mut();
1028 let mut buf = vec![0u8; lookup_buffer_len()];
1029 let rc = unsafe {
1030 libc::getgrnam_r(
1031 name.as_ptr(),
1032 grp.as_mut_ptr(),
1033 buf.as_mut_ptr().cast(),
1034 buf.len(),
1035 &mut result,
1036 )
1037 };
1038 if rc != 0 {
1039 return Err(AgentdError::ExecSession(format!(
1040 "failed to resolve guest group {name:?}: {}",
1041 std::io::Error::from_raw_os_error(rc)
1042 )));
1043 }
1044 if result.is_null() {
1045 return Ok(None);
1046 }
1047
1048 let grp = unsafe { grp.assume_init() };
1049 Ok(Some(GroupEntry { gid: grp.gr_gid }))
1050}
1051
1052fn lookup_buffer_len() -> usize {
1053 let size = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
1054 if size > 0 { size as usize } else { 16 * 1024 }
1055}
1056
1057fn apply_resolved_user(user: &ResolvedUser) -> AgentdResult<()> {
1058 if let Some(ref name) = user.initgroups_user {
1059 if unsafe { libc::initgroups(name.as_ptr(), user.gid) } != 0 {
1060 return Err(std::io::Error::last_os_error().into());
1061 }
1062 } else if unsafe { libc::setgroups(0, ptr::null()) } != 0 {
1063 return Err(std::io::Error::last_os_error().into());
1064 }
1065
1066 if unsafe { libc::setgid(user.gid) } != 0 {
1067 return Err(std::io::Error::last_os_error().into());
1068 }
1069 if unsafe { libc::setuid(user.uid) } != 0 {
1070 return Err(std::io::Error::last_os_error().into());
1071 }
1072
1073 Ok(())
1074}
1075
1076fn default_home_dir(
1077 req: &ExecRequest,
1078 user: Option<&ResolvedUser>,
1079) -> AgentdResult<Option<CString>> {
1080 if env_contains_key(&req.env, "HOME") {
1081 return Ok(None);
1082 }
1083
1084 if let Some(user) = user {
1085 return Ok(user.home_dir.clone());
1086 }
1087
1088 Ok(resolve_user_spec(DEFAULT_USER_SPEC)?.home_dir)
1089}
1090
1091fn env_contains_key(env: &[String], key: &str) -> bool {
1092 env.iter().any(|entry| {
1093 entry
1094 .split_once('=')
1095 .map(|(entry_key, _)| entry_key == key)
1096 .unwrap_or(false)
1097 })
1098}
1099
1100fn agentd_to_io_error(err: AgentdError) -> std::io::Error {
1101 std::io::Error::other(err.to_string())
1102}
1103
1104async fn blocking_write_fd(fd: RawFd, data: &[u8]) -> AgentdResult<()> {
1106 let data = data.to_vec();
1107 tokio::task::spawn_blocking(move || {
1108 let mut written = 0;
1109 while written < data.len() {
1110 let ptr = unsafe { data.as_ptr().add(written) as *const libc::c_void };
1111 let ret = unsafe { libc::write(fd, ptr, data.len() - written) };
1112 if ret < 0 {
1113 let err = std::io::Error::last_os_error();
1114 let code = err.raw_os_error();
1115 if code == Some(libc::EAGAIN) || code == Some(libc::EWOULDBLOCK) {
1116 wait_fd_writable(fd)?;
1117 continue;
1118 }
1119 if code == Some(libc::EINTR) {
1120 continue;
1121 }
1122 return Err(AgentdError::Io(err));
1123 }
1124 if ret == 0 {
1125 wait_fd_writable(fd)?;
1126 continue;
1127 }
1128 written += ret as usize;
1129 }
1130 Ok(())
1131 })
1132 .await
1133 .map_err(|e| AgentdError::ExecSession(format!("stdin write join error: {e}")))?
1134}
1135
1136fn wait_fd_writable(fd: RawFd) -> AgentdResult<()> {
1137 let mut pollfd = libc::pollfd {
1138 fd,
1139 events: libc::POLLOUT,
1140 revents: 0,
1141 };
1142
1143 loop {
1144 let ret = unsafe { libc::poll(&mut pollfd, 1, -1) };
1145 if ret < 0 {
1146 let err = std::io::Error::last_os_error();
1147 if err.raw_os_error() == Some(libc::EINTR) {
1148 continue;
1149 }
1150 return Err(AgentdError::Io(err));
1151 }
1152 if ret == 0 {
1153 continue;
1154 }
1155 return Ok(());
1160 }
1161}
1162
1163async fn pty_reader_task(
1165 id: u32,
1166 master_fd: OwnedFd,
1167 exit_watcher: ProcessExitWatcher,
1168 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
1169) {
1170 let tx_output = tx.clone();
1171 let read_result = tokio::task::spawn_blocking(move || {
1172 let raw = master_fd.as_raw_fd();
1176 let flags = unsafe { libc::fcntl(raw, libc::F_GETFL) };
1177 if flags >= 0 {
1178 unsafe { libc::fcntl(raw, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
1179 }
1180
1181 loop {
1182 let mut buf = [0u8; 4096];
1183 let n = unsafe { libc::read(raw, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
1184
1185 if n > 0 {
1186 if tx_output
1187 .send((id, SessionOutput::Stdout(buf[..n as usize].to_vec())))
1188 .is_err()
1189 {
1190 break;
1191 }
1192 continue;
1193 }
1194
1195 if n == 0 {
1196 break;
1197 }
1198
1199 let err = std::io::Error::last_os_error();
1200 match err.raw_os_error() {
1201 Some(libc::EINTR) => continue,
1202 Some(libc::EIO) => break,
1203 _ => break,
1204 }
1205 }
1206 })
1207 .await;
1208
1209 let _ = read_result;
1210
1211 let code = exit_watcher.await;
1212 let _ = tx.send((id, SessionOutput::Exited(code)));
1213}
1214
1215async fn pipe_reader_task(
1217 id: u32,
1218 stdout: Option<tokio::process::ChildStdout>,
1219 stderr: Option<tokio::process::ChildStderr>,
1220 exit_watcher: ProcessExitWatcher,
1221 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
1222) {
1223 let mut stdout = stdout;
1224 let mut stderr = stderr;
1225 let mut stdout_eof = stdout.is_none();
1226 let mut stderr_eof = stderr.is_none();
1227
1228 while !stdout_eof || !stderr_eof {
1229 let mut stdout_buf = [0u8; 4096];
1230 let mut stderr_buf = [0u8; 4096];
1231
1232 tokio::select! {
1233 result = async {
1234 match stdout.as_mut() {
1235 Some(out) => out.read(&mut stdout_buf).await,
1236 None => std::future::pending().await,
1237 }
1238 }, if !stdout_eof => {
1239 match result {
1240 Ok(0) | Err(_) => {
1241 stdout = None;
1242 stdout_eof = true;
1243 }
1244 Ok(n) => {
1245 let _ = tx.send((id, SessionOutput::Stdout(stdout_buf[..n].to_vec())));
1246 }
1247 }
1248 }
1249 result = async {
1250 match stderr.as_mut() {
1251 Some(err) => err.read(&mut stderr_buf).await,
1252 None => std::future::pending().await,
1253 }
1254 }, if !stderr_eof => {
1255 match result {
1256 Ok(0) | Err(_) => {
1257 stderr = None;
1258 stderr_eof = true;
1259 }
1260 Ok(n) => {
1261 let _ = tx.send((id, SessionOutput::Stderr(stderr_buf[..n].to_vec())));
1262 }
1263 }
1264 }
1265 }
1266 }
1267
1268 let code = exit_watcher.await;
1269
1270 let _ = tx.send((id, SessionOutput::Exited(code)));
1271}
1272
1273#[cfg(test)]
1278mod tests {
1279 use std::collections::HashMap;
1280 use std::io::Read;
1281 use std::process::{Command as StdCommand, Stdio as StdStdio};
1282 use std::time::Duration;
1283
1284 use tokio::time;
1285
1286 use microsandbox_protocol::exec::ExecRequest;
1287
1288 use super::*;
1289
1290 const REAP_HELPER_ENV: &str = "MSB_AGENTD_SESSION_REAP_HELPER";
1291 const REAP_HELPER_SENTINEL: &str = "session-reap-helper-passed";
1292 const REAP_TEST_NAME: &str = "session::tests::test_spawn_reaps_adopted_descendant";
1293 const CONCURRENT_HELPER_ENV: &str = "MSB_AGENTD_CONCURRENT_SPAWN_HELPER";
1294 const CONCURRENT_HELPER_SENTINEL: &str = "concurrent-spawn-helper-passed";
1295 const CONCURRENT_TEST_NAME: &str = "session::tests::test_concurrent_spawn_exit_codes";
1296 const RUNTIME_HELPER_ENV: &str = "MSB_AGENTD_RUNTIME_REPLACEMENT_HELPER";
1297 const RUNTIME_HELPER_SENTINEL: &str = "runtime-replacement-helper-passed";
1298 const RUNTIME_TEST_NAME: &str = "session::tests::test_spawn_survives_runtime_replacement";
1299 const PIPE_OWNER_HELPER_ENV: &str = "MSB_AGENTD_PIPE_OWNER_HELPER";
1300 const PIPE_OWNER_HELPER_SENTINEL: &str = "pipe-owner-helper-passed";
1301 const PIPE_OWNER_TEST_NAME: &str =
1302 "session::tests::test_piped_process_exit_outlives_spawning_runtime";
1303
1304 #[test]
1305 fn test_spawn_reaps_adopted_descendant() {
1306 if std::env::var_os(REAP_HELPER_ENV).is_some() {
1307 let runtime = tokio::runtime::Builder::new_current_thread()
1308 .enable_all()
1309 .build()
1310 .expect("session reap test runtime");
1311 runtime.block_on(run_adopted_descendant_scenario());
1312 println!("{REAP_HELPER_SENTINEL}");
1313 return;
1314 }
1315
1316 let mut helper = StdCommand::new(std::env::current_exe().expect("current test binary"))
1317 .args(["--exact", REAP_TEST_NAME, "--nocapture"])
1318 .env(REAP_HELPER_ENV, "1")
1319 .stdout(StdStdio::piped())
1320 .spawn()
1321 .expect("spawn isolated session reap test");
1322 let mut output = String::new();
1323 helper
1324 .stdout
1325 .take()
1326 .expect("helper stdout")
1327 .read_to_string(&mut output)
1328 .expect("read helper stdout");
1329
1330 match helper.wait() {
1331 Ok(status) => assert!(status.success(), "helper failed: {status}\n{output}"),
1332 Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {}
1333 Err(error) => panic!("wait for helper: {error}"),
1334 }
1335 assert!(
1336 output.contains(REAP_HELPER_SENTINEL),
1337 "helper did not complete the session reap scenario:\n{output}"
1338 );
1339 }
1340
1341 async fn run_adopted_descendant_scenario() {
1342 let ret = unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1) };
1343 assert_eq!(
1344 ret,
1345 0,
1346 "set child subreaper: {}",
1347 std::io::Error::last_os_error()
1348 );
1349
1350 let (tx, mut rx) = mpsc::unbounded_channel();
1351 let req = ExecRequest {
1352 cmd: "/bin/sh".to_string(),
1353 args: vec!["-c".to_string(), "sleep 30 & echo $!".to_string()],
1354 env: Vec::new(),
1355 cwd: None,
1356 user: None,
1357 tty: false,
1358 rows: 24,
1359 cols: 80,
1360 rlimits: Vec::new(),
1361 };
1362
1363 let session = ExecSession::spawn(17, &req, tx, None, SecurityProfile::Default)
1364 .expect("spawn background descendant session");
1365 let leader_pid = session.pid() as i32;
1366 let mut stdout = Vec::new();
1367 time::timeout(Duration::from_secs(10), async {
1368 while !stdout.contains(&b'\n') {
1369 let (id, output) = rx.recv().await.expect("session output");
1370 assert_eq!(id, 17);
1371 match output {
1372 SessionOutput::Stdout(data) => stdout.extend_from_slice(&data),
1373 SessionOutput::Exited(code) => panic!("session exited early with {code}"),
1374 SessionOutput::Stderr(_) | SessionOutput::Raw(_) => {}
1375 }
1376 }
1377 })
1378 .await
1379 .expect("wait for background descendant session");
1380
1381 let descendant_pid: i32 = String::from_utf8(stdout)
1382 .expect("descendant PID is UTF-8")
1383 .trim()
1384 .parse()
1385 .expect("parse descendant PID");
1386 let expected_parent = std::process::id().to_string();
1387 let status_path = format!("/proc/{descendant_pid}/status");
1388 time::timeout(Duration::from_secs(5), async {
1389 loop {
1390 if let Ok(status) = std::fs::read_to_string(&status_path)
1391 && status
1392 .lines()
1393 .find_map(|line| line.strip_prefix("PPid:"))
1394 .is_some_and(|ppid| ppid.trim() == expected_parent)
1395 {
1396 break;
1397 }
1398 time::sleep(Duration::from_millis(10)).await;
1399 }
1400 })
1401 .await
1402 .expect("descendant should be adopted by the helper subreaper");
1403
1404 let leader_path = format!("/proc/{leader_pid}");
1405 time::timeout(Duration::from_secs(5), async {
1406 while std::path::Path::new(&leader_path).exists() {
1407 time::sleep(Duration::from_millis(10)).await;
1408 }
1409 })
1410 .await
1411 .expect("direct child should be reaped before signalling its descendants");
1412
1413 session
1414 .send_signal(libc::SIGTERM)
1415 .expect("signal descendants through completed process registration");
1416 let exit = time::timeout(Duration::from_secs(5), async {
1417 loop {
1418 let (id, output) = rx.recv().await.expect("session output after signal");
1419 assert_eq!(id, 17);
1420 if let SessionOutput::Exited(code) = output {
1421 break code;
1422 }
1423 }
1424 })
1425 .await
1426 .expect("session should finish after its descendant is signalled");
1427 assert_eq!(exit, 0);
1428
1429 let proc_path = format!("/proc/{descendant_pid}");
1430 time::timeout(Duration::from_secs(5), async {
1431 while std::path::Path::new(&proc_path).exists() {
1432 time::sleep(Duration::from_millis(10)).await;
1433 }
1434 })
1435 .await
1436 .expect("descendant should be reaped");
1437
1438 let ret = unsafe { libc::waitpid(descendant_pid, ptr::null_mut(), libc::WNOHANG) };
1439 assert_eq!(ret, -1, "descendant {descendant_pid} was not reaped");
1440 assert_eq!(
1441 std::io::Error::last_os_error().raw_os_error(),
1442 Some(libc::ECHILD)
1443 );
1444 }
1445
1446 #[test]
1447 fn test_concurrent_spawn_exit_codes() {
1448 if std::env::var_os(CONCURRENT_HELPER_ENV).is_some() {
1449 let runtime = tokio::runtime::Builder::new_current_thread()
1450 .enable_all()
1451 .build()
1452 .expect("concurrent spawn test runtime");
1453 runtime.block_on(run_concurrent_spawn_scenario());
1454 println!("{CONCURRENT_HELPER_SENTINEL}");
1455 return;
1456 }
1457
1458 let mut helper = StdCommand::new(std::env::current_exe().expect("current test binary"))
1459 .args(["--exact", CONCURRENT_TEST_NAME, "--nocapture"])
1460 .env(CONCURRENT_HELPER_ENV, "1")
1461 .stdout(StdStdio::piped())
1462 .spawn()
1463 .expect("spawn isolated concurrent session test");
1464 let mut output = String::new();
1465 helper
1466 .stdout
1467 .take()
1468 .expect("helper stdout")
1469 .read_to_string(&mut output)
1470 .expect("read helper stdout");
1471
1472 match helper.wait() {
1473 Ok(status) => assert!(status.success(), "helper failed: {status}\n{output}"),
1474 Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {}
1475 Err(error) => panic!("wait for helper: {error}"),
1476 }
1477 assert!(
1478 output.contains(CONCURRENT_HELPER_SENTINEL),
1479 "helper did not complete the concurrent spawn scenario:\n{output}"
1480 );
1481 }
1482
1483 async fn run_concurrent_spawn_scenario() {
1484 const PROCESS_COUNT: u32 = 12;
1485
1486 let runtime_handle = tokio::runtime::Handle::current();
1487 let (tx, mut rx) = mpsc::unbounded_channel();
1488 let mut spawn_threads = Vec::new();
1489 for offset in 0..PROCESS_COUNT {
1490 let handle = runtime_handle.clone();
1491 let tx = tx.clone();
1492 spawn_threads.push(std::thread::spawn(move || {
1493 let _runtime = handle.enter();
1494 let code = 20 + offset as i32;
1495 let req = ExecRequest {
1496 cmd: "/bin/sh".to_string(),
1497 args: vec!["-c".to_string(), format!("exit {code}")],
1498 env: Vec::new(),
1499 cwd: None,
1500 user: None,
1501 tty: offset % 2 == 1,
1502 rows: 24,
1503 cols: 80,
1504 rlimits: Vec::new(),
1505 };
1506 ExecSession::spawn(100 + offset, &req, tx, None, SecurityProfile::Default)
1507 }));
1508 }
1509 drop(tx);
1510
1511 let mut sessions = Vec::new();
1512 for thread in spawn_threads {
1513 sessions.push(
1514 thread
1515 .join()
1516 .expect("concurrent spawn thread")
1517 .expect("concurrent process spawn"),
1518 );
1519 }
1520
1521 let mut exits = HashMap::new();
1522 time::timeout(Duration::from_secs(15), async {
1523 while exits.len() < PROCESS_COUNT as usize {
1524 let (id, output) = rx.recv().await.expect("session output");
1525 if let SessionOutput::Exited(code) = output {
1526 exits.insert(id, code);
1527 }
1528 }
1529 })
1530 .await
1531 .expect("wait for concurrent exits");
1532
1533 for offset in 0..PROCESS_COUNT {
1534 assert_eq!(exits.get(&(100 + offset)), Some(&(20 + offset as i32)));
1535 }
1536 drop(sessions);
1537 }
1538
1539 #[test]
1540 fn test_spawn_survives_runtime_replacement() {
1541 if std::env::var_os(RUNTIME_HELPER_ENV).is_some() {
1542 run_runtime_replacement_scenario();
1543 println!("{RUNTIME_HELPER_SENTINEL}");
1544 return;
1545 }
1546
1547 let mut helper = StdCommand::new(std::env::current_exe().expect("current test binary"))
1548 .args(["--exact", RUNTIME_TEST_NAME, "--nocapture"])
1549 .env(RUNTIME_HELPER_ENV, "1")
1550 .stdout(StdStdio::piped())
1551 .spawn()
1552 .expect("spawn isolated runtime replacement test");
1553 let mut output = String::new();
1554 helper
1555 .stdout
1556 .take()
1557 .expect("helper stdout")
1558 .read_to_string(&mut output)
1559 .expect("read helper stdout");
1560
1561 match helper.wait() {
1562 Ok(status) => assert!(status.success(), "helper failed: {status}\n{output}"),
1563 Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {}
1564 Err(error) => panic!("wait for helper: {error}"),
1565 }
1566 assert!(
1567 output.contains(RUNTIME_HELPER_SENTINEL),
1568 "helper did not complete the runtime replacement scenario:\n{output}"
1569 );
1570 }
1571
1572 fn run_runtime_replacement_scenario() {
1573 for (id, code) in [(201, 51), (202, 52)] {
1574 let runtime = tokio::runtime::Builder::new_current_thread()
1575 .enable_all()
1576 .build()
1577 .expect("replacement test runtime");
1578 runtime.block_on(run_single_pipe_spawn(id, code));
1579 }
1580 }
1581
1582 async fn run_single_pipe_spawn(id: u32, code: i32) {
1583 let (tx, mut rx) = mpsc::unbounded_channel();
1584 let req = ExecRequest {
1585 cmd: "/bin/sh".to_string(),
1586 args: vec!["-c".to_string(), format!("exit {code}")],
1587 env: Vec::new(),
1588 cwd: None,
1589 user: None,
1590 tty: false,
1591 rows: 24,
1592 cols: 80,
1593 rlimits: Vec::new(),
1594 };
1595 let _session = ExecSession::spawn(id, &req, tx, None, SecurityProfile::Default)
1596 .expect("spawn session on replacement runtime");
1597
1598 let actual = time::timeout(Duration::from_secs(5), async {
1599 loop {
1600 let (actual_id, output) = rx.recv().await.expect("session output");
1601 assert_eq!(actual_id, id);
1602 if let SessionOutput::Exited(actual) = output {
1603 break actual;
1604 }
1605 }
1606 })
1607 .await
1608 .expect("wait for exit on replacement runtime");
1609 assert_eq!(actual, code);
1610 }
1611
1612 #[test]
1613 fn test_piped_process_exit_outlives_spawning_runtime() {
1614 if std::env::var_os(PIPE_OWNER_HELPER_ENV).is_some() {
1615 run_piped_process_exit_scenario();
1616 println!("{PIPE_OWNER_HELPER_SENTINEL}");
1617 return;
1618 }
1619
1620 let mut helper = StdCommand::new(std::env::current_exe().expect("current test binary"))
1621 .args(["--exact", PIPE_OWNER_TEST_NAME, "--nocapture"])
1622 .env(PIPE_OWNER_HELPER_ENV, "1")
1623 .stdout(StdStdio::piped())
1624 .spawn()
1625 .expect("spawn isolated pipe owner test");
1626 let mut output = String::new();
1627 helper
1628 .stdout
1629 .take()
1630 .expect("helper stdout")
1631 .read_to_string(&mut output)
1632 .expect("read helper stdout");
1633
1634 match helper.wait() {
1635 Ok(status) => assert!(status.success(), "helper failed: {status}\n{output}"),
1636 Err(error) if error.raw_os_error() == Some(libc::ECHILD) => {}
1637 Err(error) => panic!("wait for helper: {error}"),
1638 }
1639 assert!(
1640 output.contains(PIPE_OWNER_HELPER_SENTINEL),
1641 "helper did not complete the pipe owner scenario:\n{output}"
1642 );
1643 }
1644
1645 fn run_piped_process_exit_scenario() {
1646 let process_manager = ProcessManager::get().expect("get process manager");
1647 let spawning_runtime = tokio::runtime::Builder::new_current_thread()
1648 .enable_all()
1649 .build()
1650 .expect("spawning runtime");
1651 let exit_watcher = {
1652 let _runtime_guard = spawning_runtime.enter();
1653 let mut command = Command::new("/bin/sh");
1654 command
1655 .args(["-c", "exit 63"])
1656 .stdin(Stdio::piped())
1657 .stdout(Stdio::piped())
1658 .stderr(Stdio::piped());
1659 let process =
1660 spawn_piped_process(command, &process_manager).expect("spawn piped process");
1661 let PipedProcess { exit_watcher, .. } = process;
1662 exit_watcher
1663 };
1664 drop(spawning_runtime);
1665
1666 let waiting_runtime = tokio::runtime::Builder::new_current_thread()
1667 .enable_all()
1668 .build()
1669 .expect("waiting runtime");
1670 let code = waiting_runtime.block_on(async {
1671 time::timeout(Duration::from_secs(5), exit_watcher)
1672 .await
1673 .expect("wait for piped process exit")
1674 });
1675 assert_eq!(code, 63);
1676 }
1677
1678 #[tokio::test]
1679 async fn test_pty_reader_drains_ready_fd() {
1680 let (tx, mut rx) = mpsc::unbounded_channel();
1681 let req = ExecRequest {
1682 cmd: "/bin/sh".to_string(),
1683 args: vec![
1684 "-c".to_string(),
1685 "i=0; while [ $i -lt 256 ]; do printf AAAA; i=$((i+1)); done; printf SECOND; sleep 0.1; printf '<END>\\n'; sleep 0.1; exit 0"
1686 .to_string(),
1687 ],
1688 env: vec!["PATH=/usr/local/bin:/usr/bin:/bin".to_string()],
1689 cwd: None,
1690 user: None,
1691 tty: true,
1692 rows: 24,
1693 cols: 80,
1694 rlimits: Vec::new(),
1695 };
1696
1697 let session = ExecSession::spawn(7, &req, tx, None, SecurityProfile::Default)
1698 .expect("spawn pty session");
1699 let mut stdout = Vec::new();
1700 let mut exit = None;
1701
1702 let recv_result = time::timeout(Duration::from_secs(15), async {
1703 while let Some((id, output)) = rx.recv().await {
1704 assert_eq!(id, 7);
1705 match output {
1706 SessionOutput::Stdout(data) => stdout.extend_from_slice(&data),
1707 SessionOutput::Exited(code) => {
1708 exit = Some(code);
1709 break;
1710 }
1711 SessionOutput::Stderr(_) | SessionOutput::Raw(_) => {}
1712 }
1713 }
1714 })
1715 .await;
1716
1717 if recv_result.is_err() {
1718 let _ = session.send_signal(libc::SIGKILL);
1719 panic!("timed out waiting for PTY output");
1720 }
1721
1722 assert_eq!(exit, Some(0));
1723
1724 let second = stdout
1725 .windows(b"SECOND".len())
1726 .position(|window| window == b"SECOND");
1727 let end = stdout
1728 .windows(b"<END>".len())
1729 .position(|window| window == b"<END>");
1730
1731 assert!(
1732 matches!((second, end), (Some(second), Some(end)) if second < end),
1733 "expected immediate PTY write to arrive before later output; got {:?}",
1734 String::from_utf8_lossy(&stdout),
1735 );
1736 }
1737
1738 #[test]
1739 fn test_resolve_user_spec_for_current_uid_gid() {
1740 let uid = unsafe { libc::getuid() };
1741 let gid = unsafe { libc::getgid() };
1742 let resolved = resolve_user_spec(&format!("{uid}:{gid}")).expect("resolve numeric user");
1743 assert_eq!(resolved.uid, uid);
1744 assert_eq!(resolved.gid, gid);
1745 }
1746
1747 #[test]
1748 fn test_request_user_overrides_config_default() {
1749 let req = ExecRequest {
1750 cmd: "/bin/true".to_string(),
1751 args: Vec::new(),
1752 env: Vec::new(),
1753 cwd: None,
1754 user: Some("1:1".to_string()),
1755 tty: false,
1756 rows: 24,
1757 cols: 80,
1758 rlimits: Vec::new(),
1759 };
1760
1761 let resolved = resolve_requested_user(&req, Some("0:0")).expect("resolve requested user");
1762 assert_eq!(resolved.unwrap().uid, 1);
1763 }
1764
1765 #[test]
1766 fn test_config_default_user_used_when_request_has_none() {
1767 let req = ExecRequest {
1768 cmd: "/bin/true".to_string(),
1769 args: Vec::new(),
1770 env: Vec::new(),
1771 cwd: None,
1772 user: None,
1773 tty: false,
1774 rows: 24,
1775 cols: 80,
1776 rlimits: Vec::new(),
1777 };
1778
1779 let uid = unsafe { libc::getuid() };
1780 let gid = unsafe { libc::getgid() };
1781 let resolved = resolve_requested_user(&req, Some(&format!("{uid}:{gid}")))
1782 .expect("resolve with config default");
1783 let resolved = resolved.expect("should resolve to a user");
1784 assert_eq!(resolved.uid, uid);
1785 assert_eq!(resolved.gid, gid);
1786 }
1787
1788 #[test]
1789 fn test_request_without_user_does_not_apply_user_switch() {
1790 let req = ExecRequest {
1791 cmd: "/bin/true".to_string(),
1792 args: Vec::new(),
1793 env: Vec::new(),
1794 cwd: None,
1795 user: None,
1796 tty: false,
1797 rows: 24,
1798 cols: 80,
1799 rlimits: Vec::new(),
1800 };
1801
1802 let resolved = resolve_requested_user(&req, None).expect("resolve absent user");
1803 assert!(resolved.is_none());
1804 }
1805
1806 #[test]
1807 fn test_default_user_absent_resolves_to_root() {
1808 let resolved = resolve_default_user(None).expect("resolve absent default user");
1809 assert_eq!(resolved, (0, 0));
1810 }
1811
1812 #[test]
1813 fn test_default_home_dir_uses_resolved_user_home() {
1814 let req = ExecRequest {
1815 cmd: "/bin/true".to_string(),
1816 args: Vec::new(),
1817 env: Vec::new(),
1818 cwd: None,
1819 user: None,
1820 tty: false,
1821 rows: 24,
1822 cols: 80,
1823 rlimits: Vec::new(),
1824 };
1825 let user = ResolvedUser {
1826 uid: 1000,
1827 gid: 1000,
1828 initgroups_user: None,
1829 home_dir: Some(CString::new("/home/tester").unwrap()),
1830 };
1831
1832 assert_eq!(
1833 default_home_dir(&req, Some(&user))
1834 .expect("resolve default home")
1835 .as_deref()
1836 .map(CStr::to_string_lossy),
1837 Some("/home/tester".into()),
1838 );
1839 }
1840
1841 #[test]
1842 fn test_default_home_dir_uses_root_when_user_absent() {
1843 let req = ExecRequest {
1844 cmd: "/bin/true".to_string(),
1845 args: Vec::new(),
1846 env: Vec::new(),
1847 cwd: None,
1848 user: None,
1849 tty: false,
1850 rows: 24,
1851 cols: 80,
1852 rlimits: Vec::new(),
1853 };
1854 let root = resolve_user_spec(DEFAULT_USER_SPEC).expect("resolve implicit root");
1855
1856 assert_eq!(
1857 default_home_dir(&req, None)
1858 .expect("resolve default home")
1859 .as_deref()
1860 .map(CStr::to_string_lossy),
1861 root.home_dir.as_deref().map(CStr::to_string_lossy),
1862 );
1863 }
1864
1865 #[test]
1866 fn test_default_home_dir_respects_explicit_home_env() {
1867 let req = ExecRequest {
1868 cmd: "/bin/true".to_string(),
1869 args: Vec::new(),
1870 env: vec!["HOME=/tmp/custom".to_string()],
1871 cwd: None,
1872 user: None,
1873 tty: false,
1874 rows: 24,
1875 cols: 80,
1876 rlimits: Vec::new(),
1877 };
1878 let user = ResolvedUser {
1879 uid: 1000,
1880 gid: 1000,
1881 initgroups_user: None,
1882 home_dir: Some(CString::new("/home/tester").unwrap()),
1883 };
1884
1885 assert!(
1886 default_home_dir(&req, Some(&user))
1887 .expect("resolve default home")
1888 .is_none()
1889 );
1890 }
1891
1892 #[tokio::test]
1893 async fn test_spawn_pipe_error_does_not_include_probe_details() {
1894 let (tx, _rx) = mpsc::unbounded_channel();
1895 let req = ExecRequest {
1896 cmd: "/definitely/not/a/real/binary".to_string(),
1897 args: Vec::new(),
1898 env: Vec::new(),
1899 cwd: None,
1900 user: None,
1901 tty: false,
1902 rows: 24,
1903 cols: 80,
1904 rlimits: Vec::new(),
1905 };
1906
1907 let process_manager = ProcessManager::get().expect("get process manager");
1911 let err = ExecSession::spawn_pipe(
1912 9,
1913 &req,
1914 tx,
1915 None,
1916 SecurityProfile::Default,
1917 &process_manager,
1918 )
1919 .expect_err("spawn should fail");
1920
1921 let payload = match &err {
1925 AgentdError::ExecSpawnFailed(p) => p,
1926 other => panic!("expected ExecSpawnFailed, got: {other:?}"),
1927 };
1928 assert_eq!(payload.kind, ExecFailureKind::NotFound);
1929 assert_eq!(payload.errno, Some(libc::ENOENT));
1930 assert_eq!(payload.errno_name.as_deref(), Some("ENOENT"));
1931
1932 let message = &payload.message;
1938 assert!(message.contains("spawn"));
1939 assert!(!message.contains("symlink_metadata="));
1940 assert!(!message.contains("metadata="));
1941 assert!(!message.contains("magic="));
1942 assert!(!message.contains("path_probe="));
1943 assert!(!message.contains("cwd_probe="));
1944 assert!(!message.contains("target_probe="));
1945 }
1946}