1use std::ffi::{CStr, CString};
4use std::mem::MaybeUninit;
5use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
6use std::process::Stdio;
7use std::{iter, mem, ptr};
8
9use nix::pty;
10use nix::sys::signal::{self, Signal};
11use nix::unistd::Pid;
12use tokio::io::AsyncReadExt;
13use tokio::process::{Child, Command};
14use tokio::sync::mpsc;
15
16use microsandbox_protocol::exec::{ExecFailed, ExecFailureKind, ExecRequest};
17
18use crate::config::SecurityProfile;
19use crate::error::{AgentdError, AgentdResult};
20use crate::rlimit;
21
22const LINUX_CAPABILITY_VERSION_3: u32 = 0x20080522;
27const CAP_SYS_ADMIN: u32 = 21;
28const CAP_WORD_BITS: u32 = 32;
29const PR_CAPBSET_DROP: libc::c_int = 24;
30const PR_CAP_AMBIENT: libc::c_int = 47;
31const PR_CAP_AMBIENT_CLEAR_ALL: libc::c_int = 4;
32const DEFAULT_USER_SPEC: &str = "0:0";
33
34fn errno_name(e: i32) -> Option<&'static str> {
42 match e {
43 libc::E2BIG => Some("E2BIG"),
44 libc::EACCES => Some("EACCES"),
45 libc::EAGAIN => Some("EAGAIN"),
46 libc::EBUSY => Some("EBUSY"),
47 libc::EFAULT => Some("EFAULT"),
48 libc::EINVAL => Some("EINVAL"),
49 libc::EIO => Some("EIO"),
50 libc::EISDIR => Some("EISDIR"),
51 libc::ELOOP => Some("ELOOP"),
52 libc::EMFILE => Some("EMFILE"),
53 libc::ENAMETOOLONG => Some("ENAMETOOLONG"),
54 libc::ENFILE => Some("ENFILE"),
55 libc::ENOENT => Some("ENOENT"),
56 libc::ENOEXEC => Some("ENOEXEC"),
57 libc::ENOMEM => Some("ENOMEM"),
58 libc::ENOSYS => Some("ENOSYS"),
59 libc::ENOTDIR => Some("ENOTDIR"),
60 libc::ENXIO => Some("ENXIO"),
61 libc::EPERM => Some("EPERM"),
62 libc::ETXTBSY => Some("ETXTBSY"),
63 _ => None,
64 }
65}
66
67fn classify_spawn_errno(errno: i32) -> ExecFailureKind {
79 match errno {
80 libc::ENOENT => ExecFailureKind::NotFound,
81 libc::ENOTDIR => ExecFailureKind::BadCwd,
82 libc::EACCES | libc::EPERM => ExecFailureKind::PermissionDenied,
83 libc::ENOEXEC => ExecFailureKind::NotExecutable,
84 libc::EISDIR => ExecFailureKind::NotExecutable,
85 libc::ETXTBSY => ExecFailureKind::NotExecutable,
86 libc::E2BIG | libc::ELOOP | libc::ENAMETOOLONG | libc::EFAULT => ExecFailureKind::BadArgs,
87 libc::EMFILE | libc::ENFILE => ExecFailureKind::ResourceLimit,
88 libc::EAGAIN => ExecFailureKind::ResourceLimit,
89 libc::ENOMEM => ExecFailureKind::OutOfMemory,
90 libc::EINVAL => ExecFailureKind::Other,
91 _ => ExecFailureKind::Other,
92 }
93}
94
95fn exec_failed_from_io_error(err: &std::io::Error, cmd: &str, stage: &str) -> ExecFailed {
97 let errno = err.raw_os_error();
98 let kind = errno
99 .map(classify_spawn_errno)
100 .unwrap_or(ExecFailureKind::Other);
101 let errno_name = errno.and_then(errno_name).map(str::to_string);
102 let message = format!("spawn {cmd:?}: {err}");
103 ExecFailed {
104 kind,
105 errno,
106 errno_name,
107 message,
108 stage: Some(stage.to_string()),
109 }
110}
111
112#[derive(Debug)]
121pub struct ExecSession {
122 pid: i32,
124
125 pty_master: Option<OwnedFd>,
127
128 stdin: Option<tokio::process::ChildStdin>,
130}
131
132pub enum SessionOutput {
134 Stdout(Vec<u8>),
136
137 Stderr(Vec<u8>),
139
140 Exited(i32),
142
143 Raw(RawSessionOutput),
145}
146
147pub struct RawSessionOutput {
149 pub frame: Vec<u8>,
151
152 pub activity: RawActivity,
154
155 pub completion: Option<RawSessionCompletion>,
157}
158
159#[derive(Debug, Clone, Copy, Default)]
161pub struct RawActivity {
162 pub guest_message: bool,
164
165 pub fs_bytes: usize,
167
168 pub tcp_bytes: usize,
170}
171
172#[derive(Debug, Clone, Copy)]
174pub enum RawSessionCompletion {
175 FsRead,
177
178 Tcp,
180}
181
182struct ResolvedUser {
183 uid: libc::uid_t,
184 gid: libc::gid_t,
185 initgroups_user: Option<CString>,
186 home_dir: Option<CString>,
187}
188
189struct PasswdEntry {
190 name: String,
191 uid: libc::uid_t,
192 gid: libc::gid_t,
193 home_dir: Option<String>,
194}
195
196struct GroupEntry {
197 gid: libc::gid_t,
198}
199
200struct ExecErrorPipe {
201 read_end: OwnedFd,
202 write_end: OwnedFd,
203}
204
205#[repr(C)]
206#[derive(Clone, Copy)]
207struct CapUserHeader {
208 version: u32,
209 pid: libc::c_int,
210}
211
212#[repr(C)]
213#[derive(Clone, Copy)]
214struct CapUserData {
215 effective: u32,
216 permitted: u32,
217 inheritable: u32,
218}
219
220impl RawSessionOutput {
225 pub fn new(
227 frame: Vec<u8>,
228 activity: RawActivity,
229 completion: Option<RawSessionCompletion>,
230 ) -> Self {
231 Self {
232 frame,
233 activity,
234 completion,
235 }
236 }
237}
238
239impl RawActivity {
240 pub fn guest_message() -> Self {
242 Self {
243 guest_message: true,
244 ..Self::default()
245 }
246 }
247
248 pub fn fs_bytes(len: usize) -> Self {
250 Self {
251 guest_message: true,
252 fs_bytes: len,
253 tcp_bytes: 0,
254 }
255 }
256
257 pub fn tcp_bytes(len: usize) -> Self {
259 Self {
260 guest_message: true,
261 fs_bytes: 0,
262 tcp_bytes: len,
263 }
264 }
265}
266
267impl ExecSession {
268 pub fn spawn(
273 id: u32,
274 req: &ExecRequest,
275 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
276 default_user: Option<&str>,
277 security_profile: SecurityProfile,
278 ) -> AgentdResult<Self> {
279 if req.tty {
280 Self::spawn_pty(id, req, tx, default_user, security_profile)
281 } else {
282 Self::spawn_pipe(id, req, tx, default_user, security_profile)
283 }
284 }
285
286 pub fn pid(&self) -> u32 {
288 self.pid as u32
289 }
290
291 pub async fn write_stdin(&self, data: &[u8]) -> AgentdResult<()> {
293 if let Some(ref master) = self.pty_master {
294 blocking_write_fd(master.as_raw_fd(), data).await
295 } else if let Some(ref stdin) = self.stdin {
296 blocking_write_fd(stdin.as_raw_fd(), data).await
297 } else {
298 Ok(())
299 }
300 }
301
302 pub fn resize(&self, rows: u16, cols: u16) -> AgentdResult<()> {
304 if let Some(ref master) = self.pty_master {
305 let ws = libc::winsize {
306 ws_row: rows,
307 ws_col: cols,
308 ws_xpixel: 0,
309 ws_ypixel: 0,
310 };
311 let ret = unsafe { libc::ioctl(master.as_raw_fd(), libc::TIOCSWINSZ, &ws) };
312 if ret < 0 {
313 return Err(std::io::Error::last_os_error().into());
314 }
315 }
316 Ok(())
317 }
318
319 pub fn send_signal(&self, signum: i32) -> AgentdResult<()> {
327 let sig = Signal::try_from(signum)
328 .map_err(|e| AgentdError::ExecSession(format!("invalid signal {signum}: {e}")))?;
329 match signal::kill(Pid::from_raw(-self.pid), sig) {
330 Err(nix::errno::Errno::ESRCH) => Ok(()),
332 other => Ok(other?),
333 }
334 }
335
336 pub fn close_stdin(&mut self) {
341 self.stdin.take();
342 }
343}
344
345impl ExecSession {
346 fn spawn_pty(
348 id: u32,
349 req: &ExecRequest,
350 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
351 default_user: Option<&str>,
352 security_profile: SecurityProfile,
353 ) -> AgentdResult<Self> {
354 let pty = pty::openpty(None, None)?;
355 let err_pipe = new_exec_error_pipe()?;
356
357 let ws = libc::winsize {
359 ws_row: req.rows,
360 ws_col: req.cols,
361 ws_xpixel: 0,
362 ws_ypixel: 0,
363 };
364 let ret = unsafe { libc::ioctl(pty.master.as_raw_fd(), libc::TIOCSWINSZ, &ws) };
365 if ret < 0 {
366 return Err(std::io::Error::last_os_error().into());
367 }
368
369 let slave_fd = pty.slave.as_raw_fd();
370
371 let c_cmd = CString::new(req.cmd.as_str())
373 .map_err(|e| AgentdError::ExecSession(format!("invalid command: {e}")))?;
374 let mut c_args: Vec<CString> = vec![c_cmd.clone()];
375 for arg in &req.args {
376 c_args.push(
377 CString::new(arg.as_str())
378 .map_err(|e| AgentdError::ExecSession(format!("invalid arg: {e}")))?,
379 );
380 }
381
382 let argv_ptrs: Vec<*const libc::c_char> = c_args
384 .iter()
385 .map(|s| s.as_ptr())
386 .chain(iter::once(ptr::null()))
387 .collect();
388
389 let c_env: Vec<(CString, CString)> = req
391 .env
392 .iter()
393 .filter_map(|var| {
394 let (key, val) = var.split_once('=')?;
395 let k = CString::new(key).ok()?;
396 let v = CString::new(val).ok()?;
397 Some((k, v))
398 })
399 .collect();
400
401 let c_cwd = req
403 .cwd
404 .as_ref()
405 .map(|dir| CString::new(dir.as_str()))
406 .transpose()
407 .map_err(|e| AgentdError::ExecSession(format!("invalid cwd: {e}")))?;
408
409 let resolved_user = resolve_requested_user(req, default_user)?;
410 let default_home = default_home_dir(req, resolved_user.as_ref())?;
411 let home_key = default_home
412 .as_ref()
413 .map(|_| {
414 CString::new("HOME")
415 .map_err(|e| AgentdError::ExecSession(format!("invalid home env key: {e}")))
416 })
417 .transpose()?;
418
419 let parsed_rlimits = rlimit::to_libc(&req.rlimits);
421
422 let pid = unsafe { libc::fork() };
424 if pid < 0 {
425 let io_err = std::io::Error::last_os_error();
426 return Err(AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
427 &io_err, &req.cmd, "fork",
428 )));
429 }
430
431 #[allow(unreachable_code)]
432 if pid == 0 {
433 drop(pty.master);
435 drop(err_pipe.read_end);
436
437 if unsafe { libc::setsid() } < 0 {
439 unsafe { libc::_exit(1) };
440 }
441
442 if unsafe { libc::ioctl(slave_fd, libc::TIOCSCTTY, 0) } < 0 {
444 unsafe { libc::_exit(1) };
445 }
446
447 unsafe {
449 if libc::dup2(slave_fd, 0) < 0 {
450 libc::_exit(1);
451 }
452 if libc::dup2(slave_fd, 1) < 0 {
453 libc::_exit(1);
454 }
455 if libc::dup2(slave_fd, 2) < 0 {
456 libc::_exit(1);
457 }
458 if slave_fd > 2 {
459 libc::close(slave_fd);
460 }
461 }
462
463 for (key, val) in &c_env {
465 unsafe {
466 libc::setenv(key.as_ptr(), val.as_ptr(), 1);
467 }
468 }
469
470 if let Some(ref dir) = c_cwd {
472 unsafe {
473 libc::chdir(dir.as_ptr());
474 }
475 }
476
477 if apply_exec_security_profile(security_profile).is_err() {
478 unsafe { libc::_exit(1) };
479 }
480
481 if let Some(ref user) = resolved_user
482 && apply_resolved_user(user).is_err()
483 {
484 unsafe { libc::_exit(1) };
485 }
486
487 if let (Some(key), Some(home)) = (&home_key, &default_home) {
488 unsafe {
489 libc::setenv(key.as_ptr(), home.as_ptr(), 1);
490 }
491 }
492
493 for (resource, limit) in &parsed_rlimits {
495 if unsafe { libc::setrlimit(*resource as _, limit) } != 0 {
496 unsafe { libc::_exit(1) };
497 }
498 }
499
500 unsafe {
502 libc::execvp(argv_ptrs[0], argv_ptrs.as_ptr());
503 }
504
505 write_exec_error_and_exit(err_pipe.write_end.as_raw_fd());
507 }
508
509 drop(pty.slave);
511 drop(err_pipe.write_end);
512
513 if let Some(exec_errno) = read_exec_error(err_pipe.read_end.as_raw_fd())? {
514 let _ = wait_for_exec_failure_child(pid);
515 let io_err = std::io::Error::from_raw_os_error(exec_errno);
516 return Err(AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
517 &io_err, &req.cmd, "execvp",
518 )));
519 }
520
521 let reader_fd = unsafe { libc::dup(pty.master.as_raw_fd()) };
523 if reader_fd < 0 {
524 return Err(std::io::Error::last_os_error().into());
525 }
526 let reader_fd = unsafe { OwnedFd::from_raw_fd(reader_fd) };
527
528 tokio::spawn(pty_reader_task(id, pid, reader_fd, tx));
530
531 Ok(Self {
532 pid,
533 pty_master: Some(pty.master),
534 stdin: None,
535 })
536 }
537
538 fn spawn_pipe(
540 id: u32,
541 req: &ExecRequest,
542 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
543 default_user: Option<&str>,
544 security_profile: SecurityProfile,
545 ) -> AgentdResult<Self> {
546 let mut cmd = Command::new(&req.cmd);
547 cmd.args(&req.args)
548 .stdin(Stdio::piped())
549 .stdout(Stdio::piped())
550 .stderr(Stdio::piped());
551
552 for var in &req.env {
553 if let Some((key, val)) = var.split_once('=') {
554 cmd.env(key, val);
555 }
556 }
557
558 if let Some(ref dir) = req.cwd {
559 cmd.current_dir(dir);
560 }
561
562 let resolved_user = resolve_requested_user(req, default_user)?;
563 if let Some(home) = default_home_dir(req, resolved_user.as_ref())? {
564 cmd.env("HOME", home.to_string_lossy().into_owned());
565 }
566
567 let parsed_rlimits = rlimit::to_libc(&req.rlimits);
569 unsafe {
570 cmd.pre_exec(move || {
571 if libc::setsid() < 0 {
576 return Err(std::io::Error::last_os_error());
577 }
578 apply_exec_security_profile(security_profile).map_err(agentd_to_io_error)?;
579 if let Some(ref user) = resolved_user {
580 apply_resolved_user(user).map_err(agentd_to_io_error)?;
581 }
582 for (resource, limit) in &parsed_rlimits {
583 if libc::setrlimit(*resource as _, limit) != 0 {
584 return Err(std::io::Error::last_os_error());
585 }
586 }
587 Ok(())
588 });
589 }
590
591 let cmd_label = req.cmd.clone();
592 let mut child = cmd.spawn().map_err(|err| {
593 AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
594 &err,
595 &cmd_label,
596 "Command::spawn",
597 ))
598 })?;
599 let pid = child.id().unwrap_or(0) as i32;
600 let stdin = child.stdin.take();
601 let stdout = child.stdout.take();
602 let stderr = child.stderr.take();
603
604 tokio::spawn(pipe_reader_task(id, child, stdout, stderr, tx));
606
607 Ok(Self {
608 pid,
609 pty_master: None,
610 stdin,
611 })
612 }
613}
614
615fn new_exec_error_pipe() -> AgentdResult<ExecErrorPipe> {
620 let mut fds = [0; 2];
621 let ret = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
622 if ret != 0 {
623 return Err(std::io::Error::last_os_error().into());
624 }
625
626 Ok(ExecErrorPipe {
627 read_end: unsafe { OwnedFd::from_raw_fd(fds[0]) },
628 write_end: unsafe { OwnedFd::from_raw_fd(fds[1]) },
629 })
630}
631
632fn write_exec_error_and_exit(err_fd: RawFd) -> ! {
633 let errno = unsafe { *libc::__errno_location() };
634 let bytes = errno.to_ne_bytes();
635 let _ = unsafe { libc::write(err_fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) };
636 unsafe { libc::_exit(127) }
637}
638
639fn read_exec_error(err_fd: RawFd) -> AgentdResult<Option<i32>> {
640 let mut buf = [0u8; mem::size_of::<i32>()];
641 let n = unsafe { libc::read(err_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
642 if n < 0 {
643 return Err(std::io::Error::last_os_error().into());
644 }
645 if n == 0 {
646 return Ok(None);
647 }
648 if n as usize != buf.len() {
649 return Err(AgentdError::ExecSession(format!(
650 "short exec error report: expected {} bytes, got {n}",
651 buf.len()
652 )));
653 }
654 Ok(Some(i32::from_ne_bytes(buf)))
655}
656
657fn wait_for_exec_failure_child(pid: i32) -> AgentdResult<()> {
658 let ret = unsafe { libc::waitpid(pid, ptr::null_mut(), 0) };
659 if ret < 0 {
660 return Err(std::io::Error::last_os_error().into());
661 }
662 Ok(())
663}
664
665fn apply_exec_security_profile(profile: SecurityProfile) -> AgentdResult<()> {
666 match profile {
667 SecurityProfile::Default => Ok(()),
668 SecurityProfile::Restricted => drop_mount_admin_privileges(),
669 }
670}
671
672fn drop_mount_admin_privileges() -> AgentdResult<()> {
673 if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
674 return Err(std::io::Error::last_os_error().into());
675 }
676
677 let ret = unsafe { libc::prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) };
678 if ret != 0 {
679 let err = std::io::Error::last_os_error();
680 if err.raw_os_error() != Some(libc::EINVAL) {
681 return Err(err.into());
682 }
683 }
684
685 let mut header = CapUserHeader {
686 version: LINUX_CAPABILITY_VERSION_3,
687 pid: 0,
688 };
689 let mut data = [CapUserData {
690 effective: 0,
691 permitted: 0,
692 inheritable: 0,
693 }; 2];
694
695 if unsafe { libc::syscall(libc::SYS_capget, &mut header, data.as_mut_ptr()) } != 0 {
696 return Err(std::io::Error::last_os_error().into());
697 }
698
699 let index = (CAP_SYS_ADMIN / CAP_WORD_BITS) as usize;
700 let mask = 1u32 << (CAP_SYS_ADMIN % CAP_WORD_BITS);
701 let had_sys_admin = data[index].effective & mask != 0
702 || data[index].permitted & mask != 0
703 || data[index].inheritable & mask != 0;
704
705 if had_sys_admin {
706 data[index].effective &= !mask;
707 data[index].permitted &= !mask;
708 data[index].inheritable &= !mask;
709
710 if unsafe { libc::syscall(libc::SYS_capset, &mut header, data.as_ptr()) } != 0 {
711 return Err(std::io::Error::last_os_error().into());
712 }
713 }
714
715 let ret = unsafe { libc::prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN, 0, 0, 0) };
716 if ret != 0 {
717 let err = std::io::Error::last_os_error();
718 let errno = err.raw_os_error();
719 let already_unprivileged = !had_sys_admin && errno == Some(libc::EPERM);
721 if errno != Some(libc::EINVAL) && !already_unprivileged {
722 return Err(err.into());
723 }
724 }
725
726 Ok(())
727}
728
729pub(crate) fn resolve_default_user(default_user: Option<&str>) -> AgentdResult<(u32, u32)> {
730 let Some(spec) = default_user
731 .map(str::trim)
732 .filter(|value| !value.is_empty())
733 else {
734 return Ok((0, 0));
735 };
736
737 let resolved = resolve_user_spec(spec)?;
738 Ok((resolved.uid, resolved.gid))
739}
740
741fn resolve_requested_user(
742 req: &ExecRequest,
743 default_user: Option<&str>,
744) -> AgentdResult<Option<ResolvedUser>> {
745 let default_user = default_user
746 .map(str::trim)
747 .filter(|value| !value.is_empty());
748 let requested = req
749 .user
750 .as_deref()
751 .map(str::trim)
752 .filter(|value| !value.is_empty())
753 .or(default_user);
754
755 requested.map(resolve_user_spec).transpose()
756}
757
758fn resolve_user_spec(spec: &str) -> AgentdResult<ResolvedUser> {
759 let (user_part, group_part) = match spec.split_once(':') {
760 Some((user, group)) => (user.trim(), Some(group.trim())),
761 None => (spec.trim(), None),
762 };
763
764 if user_part.is_empty() {
765 return Err(AgentdError::ExecSession("user spec has empty user".into()));
766 }
767
768 let passwd = if let Ok(uid) = parse_id(user_part) {
769 lookup_passwd_by_uid(uid)?
770 } else {
771 lookup_passwd_by_name(user_part)?
772 .ok_or_else(|| AgentdError::ExecSession(format!("guest user not found: {user_part}")))?
773 .into()
774 };
775
776 let (uid, passwd_entry) = match passwd {
777 ResolvedUserLookup::Known(entry) => (entry.uid, Some(entry)),
778 ResolvedUserLookup::Numeric(uid) => (uid, None),
779 };
780
781 let gid = match group_part {
782 Some("") => {
783 return Err(AgentdError::ExecSession("user spec has empty group".into()));
784 }
785 Some(group) => resolve_group_spec(group)?,
786 None => passwd_entry
787 .as_ref()
788 .map(|entry| entry.gid)
789 .unwrap_or_else(|| unsafe { libc::getgid() }),
790 };
791
792 let initgroups_user = passwd_entry
793 .as_ref()
794 .map(|entry| CString::new(entry.name.as_str()))
795 .transpose()
796 .map_err(|e| AgentdError::ExecSession(format!("invalid guest user name: {e}")))?;
797
798 Ok(ResolvedUser {
799 uid,
800 gid,
801 initgroups_user,
802 home_dir: passwd_entry
803 .as_ref()
804 .and_then(|entry| entry.home_dir.as_deref())
805 .map(CString::new)
806 .transpose()
807 .map_err(|e| AgentdError::ExecSession(format!("invalid guest home directory: {e}")))?,
808 })
809}
810
811enum ResolvedUserLookup {
812 Known(PasswdEntry),
813 Numeric(libc::uid_t),
814}
815
816impl From<PasswdEntry> for ResolvedUserLookup {
817 fn from(value: PasswdEntry) -> Self {
818 Self::Known(value)
819 }
820}
821
822fn resolve_group_spec(spec: &str) -> AgentdResult<libc::gid_t> {
823 if let Ok(gid) = parse_id(spec) {
824 return Ok(gid);
825 }
826
827 lookup_group_by_name(spec)?
828 .map(|entry| entry.gid)
829 .ok_or_else(|| AgentdError::ExecSession(format!("guest group not found: {spec}")))
830}
831
832fn parse_id(value: &str) -> Result<u32, std::num::ParseIntError> {
833 value.parse::<u32>()
834}
835
836fn lookup_passwd_by_name(name: &str) -> AgentdResult<Option<PasswdEntry>> {
837 let name = CString::new(name)
838 .map_err(|e| AgentdError::ExecSession(format!("invalid guest user name: {e}")))?;
839 let mut pwd = MaybeUninit::<libc::passwd>::uninit();
840 let mut result = ptr::null_mut();
841 let mut buf = vec![0u8; lookup_buffer_len()];
842 let rc = unsafe {
843 libc::getpwnam_r(
844 name.as_ptr(),
845 pwd.as_mut_ptr(),
846 buf.as_mut_ptr().cast(),
847 buf.len(),
848 &mut result,
849 )
850 };
851 if rc != 0 {
852 return Err(AgentdError::ExecSession(format!(
853 "failed to resolve guest user {name:?}: {}",
854 std::io::Error::from_raw_os_error(rc)
855 )));
856 }
857 if result.is_null() {
858 return Ok(None);
859 }
860
861 let pwd = unsafe { pwd.assume_init() };
862 let name = unsafe { CStr::from_ptr(pwd.pw_name) }
863 .to_string_lossy()
864 .into_owned();
865 let home_dir = unsafe { CStr::from_ptr(pwd.pw_dir) }
866 .to_string_lossy()
867 .into_owned();
868 Ok(Some(PasswdEntry {
869 name,
870 uid: pwd.pw_uid,
871 gid: pwd.pw_gid,
872 home_dir: (!home_dir.is_empty()).then_some(home_dir),
873 }))
874}
875
876fn lookup_passwd_by_uid(uid: libc::uid_t) -> AgentdResult<ResolvedUserLookup> {
877 let mut pwd = MaybeUninit::<libc::passwd>::uninit();
878 let mut result = ptr::null_mut();
879 let mut buf = vec![0u8; lookup_buffer_len()];
880 let rc = unsafe {
881 libc::getpwuid_r(
882 uid,
883 pwd.as_mut_ptr(),
884 buf.as_mut_ptr().cast(),
885 buf.len(),
886 &mut result,
887 )
888 };
889 if rc != 0 {
890 return Err(AgentdError::ExecSession(format!(
891 "failed to resolve guest uid {uid}: {}",
892 std::io::Error::from_raw_os_error(rc)
893 )));
894 }
895 if result.is_null() {
896 return Ok(ResolvedUserLookup::Numeric(uid));
897 }
898
899 let pwd = unsafe { pwd.assume_init() };
900 let name = unsafe { CStr::from_ptr(pwd.pw_name) }
901 .to_string_lossy()
902 .into_owned();
903 let home_dir = unsafe { CStr::from_ptr(pwd.pw_dir) }
904 .to_string_lossy()
905 .into_owned();
906 Ok(ResolvedUserLookup::Known(PasswdEntry {
907 name,
908 uid: pwd.pw_uid,
909 gid: pwd.pw_gid,
910 home_dir: (!home_dir.is_empty()).then_some(home_dir),
911 }))
912}
913
914fn lookup_group_by_name(name: &str) -> AgentdResult<Option<GroupEntry>> {
915 let name = CString::new(name)
916 .map_err(|e| AgentdError::ExecSession(format!("invalid guest group name: {e}")))?;
917 let mut grp = MaybeUninit::<libc::group>::uninit();
918 let mut result = ptr::null_mut();
919 let mut buf = vec![0u8; lookup_buffer_len()];
920 let rc = unsafe {
921 libc::getgrnam_r(
922 name.as_ptr(),
923 grp.as_mut_ptr(),
924 buf.as_mut_ptr().cast(),
925 buf.len(),
926 &mut result,
927 )
928 };
929 if rc != 0 {
930 return Err(AgentdError::ExecSession(format!(
931 "failed to resolve guest group {name:?}: {}",
932 std::io::Error::from_raw_os_error(rc)
933 )));
934 }
935 if result.is_null() {
936 return Ok(None);
937 }
938
939 let grp = unsafe { grp.assume_init() };
940 Ok(Some(GroupEntry { gid: grp.gr_gid }))
941}
942
943fn lookup_buffer_len() -> usize {
944 let size = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
945 if size > 0 { size as usize } else { 16 * 1024 }
946}
947
948fn apply_resolved_user(user: &ResolvedUser) -> AgentdResult<()> {
949 if let Some(ref name) = user.initgroups_user {
950 if unsafe { libc::initgroups(name.as_ptr(), user.gid) } != 0 {
951 return Err(std::io::Error::last_os_error().into());
952 }
953 } else if unsafe { libc::setgroups(0, ptr::null()) } != 0 {
954 return Err(std::io::Error::last_os_error().into());
955 }
956
957 if unsafe { libc::setgid(user.gid) } != 0 {
958 return Err(std::io::Error::last_os_error().into());
959 }
960 if unsafe { libc::setuid(user.uid) } != 0 {
961 return Err(std::io::Error::last_os_error().into());
962 }
963
964 Ok(())
965}
966
967fn default_home_dir(
968 req: &ExecRequest,
969 user: Option<&ResolvedUser>,
970) -> AgentdResult<Option<CString>> {
971 if env_contains_key(&req.env, "HOME") {
972 return Ok(None);
973 }
974
975 if let Some(user) = user {
976 return Ok(user.home_dir.clone());
977 }
978
979 Ok(resolve_user_spec(DEFAULT_USER_SPEC)?.home_dir)
980}
981
982fn env_contains_key(env: &[String], key: &str) -> bool {
983 env.iter().any(|entry| {
984 entry
985 .split_once('=')
986 .map(|(entry_key, _)| entry_key == key)
987 .unwrap_or(false)
988 })
989}
990
991fn agentd_to_io_error(err: AgentdError) -> std::io::Error {
992 std::io::Error::other(err.to_string())
993}
994
995async fn blocking_write_fd(fd: RawFd, data: &[u8]) -> AgentdResult<()> {
997 let data = data.to_vec();
998 tokio::task::spawn_blocking(move || {
999 let mut written = 0;
1000 while written < data.len() {
1001 let ptr = unsafe { data.as_ptr().add(written) as *const libc::c_void };
1002 let ret = unsafe { libc::write(fd, ptr, data.len() - written) };
1003 if ret < 0 {
1004 let err = std::io::Error::last_os_error();
1005 let code = err.raw_os_error();
1006 if code == Some(libc::EAGAIN) || code == Some(libc::EWOULDBLOCK) {
1007 wait_fd_writable(fd)?;
1008 continue;
1009 }
1010 if code == Some(libc::EINTR) {
1011 continue;
1012 }
1013 return Err(AgentdError::Io(err));
1014 }
1015 if ret == 0 {
1016 wait_fd_writable(fd)?;
1017 continue;
1018 }
1019 written += ret as usize;
1020 }
1021 Ok(())
1022 })
1023 .await
1024 .map_err(|e| AgentdError::ExecSession(format!("stdin write join error: {e}")))?
1025}
1026
1027fn wait_fd_writable(fd: RawFd) -> AgentdResult<()> {
1028 let mut pollfd = libc::pollfd {
1029 fd,
1030 events: libc::POLLOUT,
1031 revents: 0,
1032 };
1033
1034 loop {
1035 let ret = unsafe { libc::poll(&mut pollfd, 1, -1) };
1036 if ret < 0 {
1037 let err = std::io::Error::last_os_error();
1038 if err.raw_os_error() == Some(libc::EINTR) {
1039 continue;
1040 }
1041 return Err(AgentdError::Io(err));
1042 }
1043 if ret == 0 {
1044 continue;
1045 }
1046 return Ok(());
1051 }
1052}
1053
1054async fn pty_reader_task(
1056 id: u32,
1057 pid: i32,
1058 master_fd: OwnedFd,
1059 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
1060) {
1061 let tx_output = tx.clone();
1062 let read_result = tokio::task::spawn_blocking(move || {
1063 let raw = master_fd.as_raw_fd();
1067 let flags = unsafe { libc::fcntl(raw, libc::F_GETFL) };
1068 if flags >= 0 {
1069 unsafe { libc::fcntl(raw, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
1070 }
1071
1072 loop {
1073 let mut buf = [0u8; 4096];
1074 let n = unsafe { libc::read(raw, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
1075
1076 if n > 0 {
1077 if tx_output
1078 .send((id, SessionOutput::Stdout(buf[..n as usize].to_vec())))
1079 .is_err()
1080 {
1081 break;
1082 }
1083 continue;
1084 }
1085
1086 if n == 0 {
1087 break;
1088 }
1089
1090 let err = std::io::Error::last_os_error();
1091 match err.raw_os_error() {
1092 Some(libc::EINTR) => continue,
1093 Some(libc::EIO) => break,
1094 _ => break,
1095 }
1096 }
1097 })
1098 .await;
1099
1100 let _ = read_result;
1101
1102 let code = wait_for_pid(pid).await;
1103 let _ = tx.send((id, SessionOutput::Exited(code)));
1104}
1105
1106async fn pipe_reader_task(
1108 id: u32,
1109 mut child: Child,
1110 stdout: Option<tokio::process::ChildStdout>,
1111 stderr: Option<tokio::process::ChildStderr>,
1112 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
1113) {
1114 let mut stdout = stdout;
1115 let mut stderr = stderr;
1116 let mut stdout_eof = stdout.is_none();
1117 let mut stderr_eof = stderr.is_none();
1118
1119 while !stdout_eof || !stderr_eof {
1120 let mut stdout_buf = [0u8; 4096];
1121 let mut stderr_buf = [0u8; 4096];
1122
1123 tokio::select! {
1124 result = async {
1125 match stdout.as_mut() {
1126 Some(out) => out.read(&mut stdout_buf).await,
1127 None => std::future::pending().await,
1128 }
1129 }, if !stdout_eof => {
1130 match result {
1131 Ok(0) | Err(_) => {
1132 stdout = None;
1133 stdout_eof = true;
1134 }
1135 Ok(n) => {
1136 let _ = tx.send((id, SessionOutput::Stdout(stdout_buf[..n].to_vec())));
1137 }
1138 }
1139 }
1140 result = async {
1141 match stderr.as_mut() {
1142 Some(err) => err.read(&mut stderr_buf).await,
1143 None => std::future::pending().await,
1144 }
1145 }, if !stderr_eof => {
1146 match result {
1147 Ok(0) | Err(_) => {
1148 stderr = None;
1149 stderr_eof = true;
1150 }
1151 Ok(n) => {
1152 let _ = tx.send((id, SessionOutput::Stderr(stderr_buf[..n].to_vec())));
1153 }
1154 }
1155 }
1156 }
1157 }
1158
1159 let code = match child.wait().await {
1161 Ok(status) => status.code().unwrap_or(-1),
1162 Err(_) => -1,
1163 };
1164
1165 let _ = tx.send((id, SessionOutput::Exited(code)));
1166}
1167
1168async fn wait_for_pid(pid: i32) -> i32 {
1170 tokio::task::spawn_blocking(move || {
1171 let mut status: i32 = 0;
1172 unsafe {
1173 libc::waitpid(pid, &mut status, 0);
1174 }
1175 if libc::WIFEXITED(status) {
1176 libc::WEXITSTATUS(status)
1177 } else {
1178 -1
1179 }
1180 })
1181 .await
1182 .unwrap_or(-1)
1183}
1184
1185#[cfg(test)]
1190mod tests {
1191 use std::time::Duration;
1192
1193 use tokio::time;
1194
1195 use microsandbox_protocol::exec::ExecRequest;
1196
1197 use super::*;
1198
1199 #[tokio::test]
1200 async fn test_pty_reader_drains_ready_fd() {
1201 let (tx, mut rx) = mpsc::unbounded_channel();
1202 let req = ExecRequest {
1203 cmd: "/bin/sh".to_string(),
1204 args: vec![
1205 "-c".to_string(),
1206 "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"
1207 .to_string(),
1208 ],
1209 env: vec!["PATH=/usr/local/bin:/usr/bin:/bin".to_string()],
1210 cwd: None,
1211 user: None,
1212 tty: true,
1213 rows: 24,
1214 cols: 80,
1215 rlimits: Vec::new(),
1216 };
1217
1218 let session = ExecSession::spawn(7, &req, tx, None, SecurityProfile::Default)
1219 .expect("spawn pty session");
1220 let mut stdout = Vec::new();
1221 let mut exit = None;
1222
1223 let recv_result = time::timeout(Duration::from_secs(15), async {
1224 while let Some((id, output)) = rx.recv().await {
1225 assert_eq!(id, 7);
1226 match output {
1227 SessionOutput::Stdout(data) => stdout.extend_from_slice(&data),
1228 SessionOutput::Exited(code) => {
1229 exit = Some(code);
1230 break;
1231 }
1232 SessionOutput::Stderr(_) | SessionOutput::Raw(_) => {}
1233 }
1234 }
1235 })
1236 .await;
1237
1238 if recv_result.is_err() {
1239 let _ = session.send_signal(libc::SIGKILL);
1240 panic!("timed out waiting for PTY output");
1241 }
1242
1243 assert_eq!(exit, Some(0));
1244
1245 let second = stdout
1246 .windows(b"SECOND".len())
1247 .position(|window| window == b"SECOND");
1248 let end = stdout
1249 .windows(b"<END>".len())
1250 .position(|window| window == b"<END>");
1251
1252 assert!(
1253 matches!((second, end), (Some(second), Some(end)) if second < end),
1254 "expected immediate PTY write to arrive before later output; got {:?}",
1255 String::from_utf8_lossy(&stdout),
1256 );
1257 }
1258
1259 #[test]
1260 fn test_resolve_user_spec_for_current_uid_gid() {
1261 let uid = unsafe { libc::getuid() };
1262 let gid = unsafe { libc::getgid() };
1263 let resolved = resolve_user_spec(&format!("{uid}:{gid}")).expect("resolve numeric user");
1264 assert_eq!(resolved.uid, uid);
1265 assert_eq!(resolved.gid, gid);
1266 }
1267
1268 #[test]
1269 fn test_request_user_overrides_config_default() {
1270 let req = ExecRequest {
1271 cmd: "/bin/true".to_string(),
1272 args: Vec::new(),
1273 env: Vec::new(),
1274 cwd: None,
1275 user: Some("1:1".to_string()),
1276 tty: false,
1277 rows: 24,
1278 cols: 80,
1279 rlimits: Vec::new(),
1280 };
1281
1282 let resolved = resolve_requested_user(&req, Some("0:0")).expect("resolve requested user");
1283 assert_eq!(resolved.unwrap().uid, 1);
1284 }
1285
1286 #[test]
1287 fn test_config_default_user_used_when_request_has_none() {
1288 let req = ExecRequest {
1289 cmd: "/bin/true".to_string(),
1290 args: Vec::new(),
1291 env: Vec::new(),
1292 cwd: None,
1293 user: None,
1294 tty: false,
1295 rows: 24,
1296 cols: 80,
1297 rlimits: Vec::new(),
1298 };
1299
1300 let uid = unsafe { libc::getuid() };
1301 let gid = unsafe { libc::getgid() };
1302 let resolved = resolve_requested_user(&req, Some(&format!("{uid}:{gid}")))
1303 .expect("resolve with config default");
1304 let resolved = resolved.expect("should resolve to a user");
1305 assert_eq!(resolved.uid, uid);
1306 assert_eq!(resolved.gid, gid);
1307 }
1308
1309 #[test]
1310 fn test_request_without_user_does_not_apply_user_switch() {
1311 let req = ExecRequest {
1312 cmd: "/bin/true".to_string(),
1313 args: Vec::new(),
1314 env: Vec::new(),
1315 cwd: None,
1316 user: None,
1317 tty: false,
1318 rows: 24,
1319 cols: 80,
1320 rlimits: Vec::new(),
1321 };
1322
1323 let resolved = resolve_requested_user(&req, None).expect("resolve absent user");
1324 assert!(resolved.is_none());
1325 }
1326
1327 #[test]
1328 fn test_default_user_absent_resolves_to_root() {
1329 let resolved = resolve_default_user(None).expect("resolve absent default user");
1330 assert_eq!(resolved, (0, 0));
1331 }
1332
1333 #[test]
1334 fn test_default_home_dir_uses_resolved_user_home() {
1335 let req = ExecRequest {
1336 cmd: "/bin/true".to_string(),
1337 args: Vec::new(),
1338 env: Vec::new(),
1339 cwd: None,
1340 user: None,
1341 tty: false,
1342 rows: 24,
1343 cols: 80,
1344 rlimits: Vec::new(),
1345 };
1346 let user = ResolvedUser {
1347 uid: 1000,
1348 gid: 1000,
1349 initgroups_user: None,
1350 home_dir: Some(CString::new("/home/tester").unwrap()),
1351 };
1352
1353 assert_eq!(
1354 default_home_dir(&req, Some(&user))
1355 .expect("resolve default home")
1356 .as_deref()
1357 .map(CStr::to_string_lossy),
1358 Some("/home/tester".into()),
1359 );
1360 }
1361
1362 #[test]
1363 fn test_default_home_dir_uses_root_when_user_absent() {
1364 let req = ExecRequest {
1365 cmd: "/bin/true".to_string(),
1366 args: Vec::new(),
1367 env: Vec::new(),
1368 cwd: None,
1369 user: None,
1370 tty: false,
1371 rows: 24,
1372 cols: 80,
1373 rlimits: Vec::new(),
1374 };
1375 let root = resolve_user_spec(DEFAULT_USER_SPEC).expect("resolve implicit root");
1376
1377 assert_eq!(
1378 default_home_dir(&req, None)
1379 .expect("resolve default home")
1380 .as_deref()
1381 .map(CStr::to_string_lossy),
1382 root.home_dir.as_deref().map(CStr::to_string_lossy),
1383 );
1384 }
1385
1386 #[test]
1387 fn test_default_home_dir_respects_explicit_home_env() {
1388 let req = ExecRequest {
1389 cmd: "/bin/true".to_string(),
1390 args: Vec::new(),
1391 env: vec!["HOME=/tmp/custom".to_string()],
1392 cwd: None,
1393 user: None,
1394 tty: false,
1395 rows: 24,
1396 cols: 80,
1397 rlimits: Vec::new(),
1398 };
1399 let user = ResolvedUser {
1400 uid: 1000,
1401 gid: 1000,
1402 initgroups_user: None,
1403 home_dir: Some(CString::new("/home/tester").unwrap()),
1404 };
1405
1406 assert!(
1407 default_home_dir(&req, Some(&user))
1408 .expect("resolve default home")
1409 .is_none()
1410 );
1411 }
1412
1413 #[tokio::test]
1414 async fn test_spawn_pipe_error_does_not_include_probe_details() {
1415 let (tx, _rx) = mpsc::unbounded_channel();
1416 let req = ExecRequest {
1417 cmd: "/definitely/not/a/real/binary".to_string(),
1418 args: Vec::new(),
1419 env: Vec::new(),
1420 cwd: None,
1421 user: None,
1422 tty: false,
1423 rows: 24,
1424 cols: 80,
1425 rlimits: Vec::new(),
1426 };
1427
1428 let err = ExecSession::spawn(9, &req, tx, None, SecurityProfile::Default)
1429 .expect_err("spawn should fail");
1430
1431 let payload = match &err {
1435 AgentdError::ExecSpawnFailed(p) => p,
1436 other => panic!("expected ExecSpawnFailed, got: {other:?}"),
1437 };
1438 assert_eq!(payload.kind, ExecFailureKind::NotFound);
1439 assert_eq!(payload.errno, Some(libc::ENOENT));
1440 assert_eq!(payload.errno_name.as_deref(), Some("ENOENT"));
1441
1442 let message = &payload.message;
1448 assert!(message.contains("spawn"));
1449 assert!(!message.contains("symlink_metadata="));
1450 assert!(!message.contains("metadata="));
1451 assert!(!message.contains("magic="));
1452 assert!(!message.contains("path_probe="));
1453 assert!(!message.contains("cwd_probe="));
1454 assert!(!message.contains("target_probe="));
1455 }
1456}