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<()> {
321 let sig = Signal::try_from(signum)
322 .map_err(|e| AgentdError::ExecSession(format!("invalid signal {signum}: {e}")))?;
323 signal::kill(Pid::from_raw(self.pid), sig)?;
324 Ok(())
325 }
326
327 pub fn close_stdin(&mut self) {
332 self.stdin.take();
333 }
334}
335
336impl ExecSession {
337 fn spawn_pty(
339 id: u32,
340 req: &ExecRequest,
341 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
342 default_user: Option<&str>,
343 security_profile: SecurityProfile,
344 ) -> AgentdResult<Self> {
345 let pty = pty::openpty(None, None)?;
346 let err_pipe = new_exec_error_pipe()?;
347
348 let ws = libc::winsize {
350 ws_row: req.rows,
351 ws_col: req.cols,
352 ws_xpixel: 0,
353 ws_ypixel: 0,
354 };
355 let ret = unsafe { libc::ioctl(pty.master.as_raw_fd(), libc::TIOCSWINSZ, &ws) };
356 if ret < 0 {
357 return Err(std::io::Error::last_os_error().into());
358 }
359
360 let slave_fd = pty.slave.as_raw_fd();
361
362 let c_cmd = CString::new(req.cmd.as_str())
364 .map_err(|e| AgentdError::ExecSession(format!("invalid command: {e}")))?;
365 let mut c_args: Vec<CString> = vec![c_cmd.clone()];
366 for arg in &req.args {
367 c_args.push(
368 CString::new(arg.as_str())
369 .map_err(|e| AgentdError::ExecSession(format!("invalid arg: {e}")))?,
370 );
371 }
372
373 let argv_ptrs: Vec<*const libc::c_char> = c_args
375 .iter()
376 .map(|s| s.as_ptr())
377 .chain(iter::once(ptr::null()))
378 .collect();
379
380 let c_env: Vec<(CString, CString)> = req
382 .env
383 .iter()
384 .filter_map(|var| {
385 let (key, val) = var.split_once('=')?;
386 let k = CString::new(key).ok()?;
387 let v = CString::new(val).ok()?;
388 Some((k, v))
389 })
390 .collect();
391
392 let c_cwd = req
394 .cwd
395 .as_ref()
396 .map(|dir| CString::new(dir.as_str()))
397 .transpose()
398 .map_err(|e| AgentdError::ExecSession(format!("invalid cwd: {e}")))?;
399
400 let resolved_user = resolve_requested_user(req, default_user)?;
401 let default_home = default_home_dir(req, resolved_user.as_ref())?;
402 let home_key = default_home
403 .as_ref()
404 .map(|_| {
405 CString::new("HOME")
406 .map_err(|e| AgentdError::ExecSession(format!("invalid home env key: {e}")))
407 })
408 .transpose()?;
409
410 let parsed_rlimits = rlimit::to_libc(&req.rlimits);
412
413 let pid = unsafe { libc::fork() };
415 if pid < 0 {
416 let io_err = std::io::Error::last_os_error();
417 return Err(AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
418 &io_err, &req.cmd, "fork",
419 )));
420 }
421
422 #[allow(unreachable_code)]
423 if pid == 0 {
424 drop(pty.master);
426 drop(err_pipe.read_end);
427
428 if unsafe { libc::setsid() } < 0 {
430 unsafe { libc::_exit(1) };
431 }
432
433 if unsafe { libc::ioctl(slave_fd, libc::TIOCSCTTY, 0) } < 0 {
435 unsafe { libc::_exit(1) };
436 }
437
438 unsafe {
440 if libc::dup2(slave_fd, 0) < 0 {
441 libc::_exit(1);
442 }
443 if libc::dup2(slave_fd, 1) < 0 {
444 libc::_exit(1);
445 }
446 if libc::dup2(slave_fd, 2) < 0 {
447 libc::_exit(1);
448 }
449 if slave_fd > 2 {
450 libc::close(slave_fd);
451 }
452 }
453
454 for (key, val) in &c_env {
456 unsafe {
457 libc::setenv(key.as_ptr(), val.as_ptr(), 1);
458 }
459 }
460
461 if let Some(ref dir) = c_cwd {
463 unsafe {
464 libc::chdir(dir.as_ptr());
465 }
466 }
467
468 if apply_exec_security_profile(security_profile).is_err() {
469 unsafe { libc::_exit(1) };
470 }
471
472 if let Some(ref user) = resolved_user
473 && apply_resolved_user(user).is_err()
474 {
475 unsafe { libc::_exit(1) };
476 }
477
478 if let (Some(key), Some(home)) = (&home_key, &default_home) {
479 unsafe {
480 libc::setenv(key.as_ptr(), home.as_ptr(), 1);
481 }
482 }
483
484 for (resource, limit) in &parsed_rlimits {
486 if unsafe { libc::setrlimit(*resource as _, limit) } != 0 {
487 unsafe { libc::_exit(1) };
488 }
489 }
490
491 unsafe {
493 libc::execvp(argv_ptrs[0], argv_ptrs.as_ptr());
494 }
495
496 write_exec_error_and_exit(err_pipe.write_end.as_raw_fd());
498 }
499
500 drop(pty.slave);
502 drop(err_pipe.write_end);
503
504 if let Some(exec_errno) = read_exec_error(err_pipe.read_end.as_raw_fd())? {
505 let _ = wait_for_exec_failure_child(pid);
506 let io_err = std::io::Error::from_raw_os_error(exec_errno);
507 return Err(AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
508 &io_err, &req.cmd, "execvp",
509 )));
510 }
511
512 let reader_fd = unsafe { libc::dup(pty.master.as_raw_fd()) };
514 if reader_fd < 0 {
515 return Err(std::io::Error::last_os_error().into());
516 }
517 let reader_fd = unsafe { OwnedFd::from_raw_fd(reader_fd) };
518
519 tokio::spawn(pty_reader_task(id, pid, reader_fd, tx));
521
522 Ok(Self {
523 pid,
524 pty_master: Some(pty.master),
525 stdin: None,
526 })
527 }
528
529 fn spawn_pipe(
531 id: u32,
532 req: &ExecRequest,
533 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
534 default_user: Option<&str>,
535 security_profile: SecurityProfile,
536 ) -> AgentdResult<Self> {
537 let mut cmd = Command::new(&req.cmd);
538 cmd.args(&req.args)
539 .stdin(Stdio::piped())
540 .stdout(Stdio::piped())
541 .stderr(Stdio::piped());
542
543 for var in &req.env {
544 if let Some((key, val)) = var.split_once('=') {
545 cmd.env(key, val);
546 }
547 }
548
549 if let Some(ref dir) = req.cwd {
550 cmd.current_dir(dir);
551 }
552
553 let resolved_user = resolve_requested_user(req, default_user)?;
554 if let Some(home) = default_home_dir(req, resolved_user.as_ref())? {
555 cmd.env("HOME", home.to_string_lossy().into_owned());
556 }
557
558 let parsed_rlimits = rlimit::to_libc(&req.rlimits);
560 unsafe {
561 cmd.pre_exec(move || {
562 apply_exec_security_profile(security_profile).map_err(agentd_to_io_error)?;
563 if let Some(ref user) = resolved_user {
564 apply_resolved_user(user).map_err(agentd_to_io_error)?;
565 }
566 for (resource, limit) in &parsed_rlimits {
567 if libc::setrlimit(*resource as _, limit) != 0 {
568 return Err(std::io::Error::last_os_error());
569 }
570 }
571 Ok(())
572 });
573 }
574
575 let cmd_label = req.cmd.clone();
576 let mut child = cmd.spawn().map_err(|err| {
577 AgentdError::ExecSpawnFailed(exec_failed_from_io_error(
578 &err,
579 &cmd_label,
580 "Command::spawn",
581 ))
582 })?;
583 let pid = child.id().unwrap_or(0) as i32;
584 let stdin = child.stdin.take();
585 let stdout = child.stdout.take();
586 let stderr = child.stderr.take();
587
588 tokio::spawn(pipe_reader_task(id, child, stdout, stderr, tx));
590
591 Ok(Self {
592 pid,
593 pty_master: None,
594 stdin,
595 })
596 }
597}
598
599fn new_exec_error_pipe() -> AgentdResult<ExecErrorPipe> {
604 let mut fds = [0; 2];
605 let ret = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
606 if ret != 0 {
607 return Err(std::io::Error::last_os_error().into());
608 }
609
610 Ok(ExecErrorPipe {
611 read_end: unsafe { OwnedFd::from_raw_fd(fds[0]) },
612 write_end: unsafe { OwnedFd::from_raw_fd(fds[1]) },
613 })
614}
615
616fn write_exec_error_and_exit(err_fd: RawFd) -> ! {
617 let errno = unsafe { *libc::__errno_location() };
618 let bytes = errno.to_ne_bytes();
619 let _ = unsafe { libc::write(err_fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) };
620 unsafe { libc::_exit(127) }
621}
622
623fn read_exec_error(err_fd: RawFd) -> AgentdResult<Option<i32>> {
624 let mut buf = [0u8; mem::size_of::<i32>()];
625 let n = unsafe { libc::read(err_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
626 if n < 0 {
627 return Err(std::io::Error::last_os_error().into());
628 }
629 if n == 0 {
630 return Ok(None);
631 }
632 if n as usize != buf.len() {
633 return Err(AgentdError::ExecSession(format!(
634 "short exec error report: expected {} bytes, got {n}",
635 buf.len()
636 )));
637 }
638 Ok(Some(i32::from_ne_bytes(buf)))
639}
640
641fn wait_for_exec_failure_child(pid: i32) -> AgentdResult<()> {
642 let ret = unsafe { libc::waitpid(pid, ptr::null_mut(), 0) };
643 if ret < 0 {
644 return Err(std::io::Error::last_os_error().into());
645 }
646 Ok(())
647}
648
649fn apply_exec_security_profile(profile: SecurityProfile) -> AgentdResult<()> {
650 match profile {
651 SecurityProfile::Default => Ok(()),
652 SecurityProfile::Restricted => drop_mount_admin_privileges(),
653 }
654}
655
656fn drop_mount_admin_privileges() -> AgentdResult<()> {
657 if unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
658 return Err(std::io::Error::last_os_error().into());
659 }
660
661 let ret = unsafe { libc::prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) };
662 if ret != 0 {
663 let err = std::io::Error::last_os_error();
664 if err.raw_os_error() != Some(libc::EINVAL) {
665 return Err(err.into());
666 }
667 }
668
669 let mut header = CapUserHeader {
670 version: LINUX_CAPABILITY_VERSION_3,
671 pid: 0,
672 };
673 let mut data = [CapUserData {
674 effective: 0,
675 permitted: 0,
676 inheritable: 0,
677 }; 2];
678
679 if unsafe { libc::syscall(libc::SYS_capget, &mut header, data.as_mut_ptr()) } != 0 {
680 return Err(std::io::Error::last_os_error().into());
681 }
682
683 let index = (CAP_SYS_ADMIN / CAP_WORD_BITS) as usize;
684 let mask = 1u32 << (CAP_SYS_ADMIN % CAP_WORD_BITS);
685 let had_sys_admin = data[index].effective & mask != 0
686 || data[index].permitted & mask != 0
687 || data[index].inheritable & mask != 0;
688
689 if had_sys_admin {
690 data[index].effective &= !mask;
691 data[index].permitted &= !mask;
692 data[index].inheritable &= !mask;
693
694 if unsafe { libc::syscall(libc::SYS_capset, &mut header, data.as_ptr()) } != 0 {
695 return Err(std::io::Error::last_os_error().into());
696 }
697 }
698
699 let ret = unsafe { libc::prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN, 0, 0, 0) };
700 if ret != 0 {
701 let err = std::io::Error::last_os_error();
702 let errno = err.raw_os_error();
703 let already_unprivileged = !had_sys_admin && errno == Some(libc::EPERM);
705 if errno != Some(libc::EINVAL) && !already_unprivileged {
706 return Err(err.into());
707 }
708 }
709
710 Ok(())
711}
712
713pub(crate) fn resolve_default_user(default_user: Option<&str>) -> AgentdResult<(u32, u32)> {
714 let Some(spec) = default_user
715 .map(str::trim)
716 .filter(|value| !value.is_empty())
717 else {
718 return Ok((0, 0));
719 };
720
721 let resolved = resolve_user_spec(spec)?;
722 Ok((resolved.uid, resolved.gid))
723}
724
725fn resolve_requested_user(
726 req: &ExecRequest,
727 default_user: Option<&str>,
728) -> AgentdResult<Option<ResolvedUser>> {
729 let default_user = default_user
730 .map(str::trim)
731 .filter(|value| !value.is_empty());
732 let requested = req
733 .user
734 .as_deref()
735 .map(str::trim)
736 .filter(|value| !value.is_empty())
737 .or(default_user);
738
739 requested.map(resolve_user_spec).transpose()
740}
741
742fn resolve_user_spec(spec: &str) -> AgentdResult<ResolvedUser> {
743 let (user_part, group_part) = match spec.split_once(':') {
744 Some((user, group)) => (user.trim(), Some(group.trim())),
745 None => (spec.trim(), None),
746 };
747
748 if user_part.is_empty() {
749 return Err(AgentdError::ExecSession("user spec has empty user".into()));
750 }
751
752 let passwd = if let Ok(uid) = parse_id(user_part) {
753 lookup_passwd_by_uid(uid)?
754 } else {
755 lookup_passwd_by_name(user_part)?
756 .ok_or_else(|| AgentdError::ExecSession(format!("guest user not found: {user_part}")))?
757 .into()
758 };
759
760 let (uid, passwd_entry) = match passwd {
761 ResolvedUserLookup::Known(entry) => (entry.uid, Some(entry)),
762 ResolvedUserLookup::Numeric(uid) => (uid, None),
763 };
764
765 let gid = match group_part {
766 Some("") => {
767 return Err(AgentdError::ExecSession("user spec has empty group".into()));
768 }
769 Some(group) => resolve_group_spec(group)?,
770 None => passwd_entry
771 .as_ref()
772 .map(|entry| entry.gid)
773 .unwrap_or_else(|| unsafe { libc::getgid() }),
774 };
775
776 let initgroups_user = passwd_entry
777 .as_ref()
778 .map(|entry| CString::new(entry.name.as_str()))
779 .transpose()
780 .map_err(|e| AgentdError::ExecSession(format!("invalid guest user name: {e}")))?;
781
782 Ok(ResolvedUser {
783 uid,
784 gid,
785 initgroups_user,
786 home_dir: passwd_entry
787 .as_ref()
788 .and_then(|entry| entry.home_dir.as_deref())
789 .map(CString::new)
790 .transpose()
791 .map_err(|e| AgentdError::ExecSession(format!("invalid guest home directory: {e}")))?,
792 })
793}
794
795enum ResolvedUserLookup {
796 Known(PasswdEntry),
797 Numeric(libc::uid_t),
798}
799
800impl From<PasswdEntry> for ResolvedUserLookup {
801 fn from(value: PasswdEntry) -> Self {
802 Self::Known(value)
803 }
804}
805
806fn resolve_group_spec(spec: &str) -> AgentdResult<libc::gid_t> {
807 if let Ok(gid) = parse_id(spec) {
808 return Ok(gid);
809 }
810
811 lookup_group_by_name(spec)?
812 .map(|entry| entry.gid)
813 .ok_or_else(|| AgentdError::ExecSession(format!("guest group not found: {spec}")))
814}
815
816fn parse_id(value: &str) -> Result<u32, std::num::ParseIntError> {
817 value.parse::<u32>()
818}
819
820fn lookup_passwd_by_name(name: &str) -> AgentdResult<Option<PasswdEntry>> {
821 let name = CString::new(name)
822 .map_err(|e| AgentdError::ExecSession(format!("invalid guest user name: {e}")))?;
823 let mut pwd = MaybeUninit::<libc::passwd>::uninit();
824 let mut result = ptr::null_mut();
825 let mut buf = vec![0u8; lookup_buffer_len()];
826 let rc = unsafe {
827 libc::getpwnam_r(
828 name.as_ptr(),
829 pwd.as_mut_ptr(),
830 buf.as_mut_ptr().cast(),
831 buf.len(),
832 &mut result,
833 )
834 };
835 if rc != 0 {
836 return Err(AgentdError::ExecSession(format!(
837 "failed to resolve guest user {name:?}: {}",
838 std::io::Error::from_raw_os_error(rc)
839 )));
840 }
841 if result.is_null() {
842 return Ok(None);
843 }
844
845 let pwd = unsafe { pwd.assume_init() };
846 let name = unsafe { CStr::from_ptr(pwd.pw_name) }
847 .to_string_lossy()
848 .into_owned();
849 let home_dir = unsafe { CStr::from_ptr(pwd.pw_dir) }
850 .to_string_lossy()
851 .into_owned();
852 Ok(Some(PasswdEntry {
853 name,
854 uid: pwd.pw_uid,
855 gid: pwd.pw_gid,
856 home_dir: (!home_dir.is_empty()).then_some(home_dir),
857 }))
858}
859
860fn lookup_passwd_by_uid(uid: libc::uid_t) -> AgentdResult<ResolvedUserLookup> {
861 let mut pwd = MaybeUninit::<libc::passwd>::uninit();
862 let mut result = ptr::null_mut();
863 let mut buf = vec![0u8; lookup_buffer_len()];
864 let rc = unsafe {
865 libc::getpwuid_r(
866 uid,
867 pwd.as_mut_ptr(),
868 buf.as_mut_ptr().cast(),
869 buf.len(),
870 &mut result,
871 )
872 };
873 if rc != 0 {
874 return Err(AgentdError::ExecSession(format!(
875 "failed to resolve guest uid {uid}: {}",
876 std::io::Error::from_raw_os_error(rc)
877 )));
878 }
879 if result.is_null() {
880 return Ok(ResolvedUserLookup::Numeric(uid));
881 }
882
883 let pwd = unsafe { pwd.assume_init() };
884 let name = unsafe { CStr::from_ptr(pwd.pw_name) }
885 .to_string_lossy()
886 .into_owned();
887 let home_dir = unsafe { CStr::from_ptr(pwd.pw_dir) }
888 .to_string_lossy()
889 .into_owned();
890 Ok(ResolvedUserLookup::Known(PasswdEntry {
891 name,
892 uid: pwd.pw_uid,
893 gid: pwd.pw_gid,
894 home_dir: (!home_dir.is_empty()).then_some(home_dir),
895 }))
896}
897
898fn lookup_group_by_name(name: &str) -> AgentdResult<Option<GroupEntry>> {
899 let name = CString::new(name)
900 .map_err(|e| AgentdError::ExecSession(format!("invalid guest group name: {e}")))?;
901 let mut grp = MaybeUninit::<libc::group>::uninit();
902 let mut result = ptr::null_mut();
903 let mut buf = vec![0u8; lookup_buffer_len()];
904 let rc = unsafe {
905 libc::getgrnam_r(
906 name.as_ptr(),
907 grp.as_mut_ptr(),
908 buf.as_mut_ptr().cast(),
909 buf.len(),
910 &mut result,
911 )
912 };
913 if rc != 0 {
914 return Err(AgentdError::ExecSession(format!(
915 "failed to resolve guest group {name:?}: {}",
916 std::io::Error::from_raw_os_error(rc)
917 )));
918 }
919 if result.is_null() {
920 return Ok(None);
921 }
922
923 let grp = unsafe { grp.assume_init() };
924 Ok(Some(GroupEntry { gid: grp.gr_gid }))
925}
926
927fn lookup_buffer_len() -> usize {
928 let size = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
929 if size > 0 { size as usize } else { 16 * 1024 }
930}
931
932fn apply_resolved_user(user: &ResolvedUser) -> AgentdResult<()> {
933 if let Some(ref name) = user.initgroups_user {
934 if unsafe { libc::initgroups(name.as_ptr(), user.gid) } != 0 {
935 return Err(std::io::Error::last_os_error().into());
936 }
937 } else if unsafe { libc::setgroups(0, ptr::null()) } != 0 {
938 return Err(std::io::Error::last_os_error().into());
939 }
940
941 if unsafe { libc::setgid(user.gid) } != 0 {
942 return Err(std::io::Error::last_os_error().into());
943 }
944 if unsafe { libc::setuid(user.uid) } != 0 {
945 return Err(std::io::Error::last_os_error().into());
946 }
947
948 Ok(())
949}
950
951fn default_home_dir(
952 req: &ExecRequest,
953 user: Option<&ResolvedUser>,
954) -> AgentdResult<Option<CString>> {
955 if env_contains_key(&req.env, "HOME") {
956 return Ok(None);
957 }
958
959 if let Some(user) = user {
960 return Ok(user.home_dir.clone());
961 }
962
963 Ok(resolve_user_spec(DEFAULT_USER_SPEC)?.home_dir)
964}
965
966fn env_contains_key(env: &[String], key: &str) -> bool {
967 env.iter().any(|entry| {
968 entry
969 .split_once('=')
970 .map(|(entry_key, _)| entry_key == key)
971 .unwrap_or(false)
972 })
973}
974
975fn agentd_to_io_error(err: AgentdError) -> std::io::Error {
976 std::io::Error::other(err.to_string())
977}
978
979async fn blocking_write_fd(fd: RawFd, data: &[u8]) -> AgentdResult<()> {
981 let data = data.to_vec();
982 tokio::task::spawn_blocking(move || {
983 let mut written = 0;
984 while written < data.len() {
985 let ptr = unsafe { data.as_ptr().add(written) as *const libc::c_void };
986 let ret = unsafe { libc::write(fd, ptr, data.len() - written) };
987 if ret < 0 {
988 let err = std::io::Error::last_os_error();
989 let code = err.raw_os_error();
990 if code == Some(libc::EAGAIN) || code == Some(libc::EWOULDBLOCK) {
991 wait_fd_writable(fd)?;
992 continue;
993 }
994 if code == Some(libc::EINTR) {
995 continue;
996 }
997 return Err(AgentdError::Io(err));
998 }
999 if ret == 0 {
1000 wait_fd_writable(fd)?;
1001 continue;
1002 }
1003 written += ret as usize;
1004 }
1005 Ok(())
1006 })
1007 .await
1008 .map_err(|e| AgentdError::ExecSession(format!("stdin write join error: {e}")))?
1009}
1010
1011fn wait_fd_writable(fd: RawFd) -> AgentdResult<()> {
1012 let mut pollfd = libc::pollfd {
1013 fd,
1014 events: libc::POLLOUT,
1015 revents: 0,
1016 };
1017
1018 loop {
1019 let ret = unsafe { libc::poll(&mut pollfd, 1, -1) };
1020 if ret < 0 {
1021 let err = std::io::Error::last_os_error();
1022 if err.raw_os_error() == Some(libc::EINTR) {
1023 continue;
1024 }
1025 return Err(AgentdError::Io(err));
1026 }
1027 if ret == 0 {
1028 continue;
1029 }
1030 return Ok(());
1035 }
1036}
1037
1038async fn pty_reader_task(
1040 id: u32,
1041 pid: i32,
1042 master_fd: OwnedFd,
1043 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
1044) {
1045 let tx_output = tx.clone();
1046 let read_result = tokio::task::spawn_blocking(move || {
1047 let raw = master_fd.as_raw_fd();
1051 let flags = unsafe { libc::fcntl(raw, libc::F_GETFL) };
1052 if flags >= 0 {
1053 unsafe { libc::fcntl(raw, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
1054 }
1055
1056 loop {
1057 let mut buf = [0u8; 4096];
1058 let n = unsafe { libc::read(raw, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
1059
1060 if n > 0 {
1061 if tx_output
1062 .send((id, SessionOutput::Stdout(buf[..n as usize].to_vec())))
1063 .is_err()
1064 {
1065 break;
1066 }
1067 continue;
1068 }
1069
1070 if n == 0 {
1071 break;
1072 }
1073
1074 let err = std::io::Error::last_os_error();
1075 match err.raw_os_error() {
1076 Some(libc::EINTR) => continue,
1077 Some(libc::EIO) => break,
1078 _ => break,
1079 }
1080 }
1081 })
1082 .await;
1083
1084 let _ = read_result;
1085
1086 let code = wait_for_pid(pid).await;
1087 let _ = tx.send((id, SessionOutput::Exited(code)));
1088}
1089
1090async fn pipe_reader_task(
1092 id: u32,
1093 mut child: Child,
1094 stdout: Option<tokio::process::ChildStdout>,
1095 stderr: Option<tokio::process::ChildStderr>,
1096 tx: mpsc::UnboundedSender<(u32, SessionOutput)>,
1097) {
1098 let mut stdout = stdout;
1099 let mut stderr = stderr;
1100 let mut stdout_eof = stdout.is_none();
1101 let mut stderr_eof = stderr.is_none();
1102
1103 while !stdout_eof || !stderr_eof {
1104 let mut stdout_buf = [0u8; 4096];
1105 let mut stderr_buf = [0u8; 4096];
1106
1107 tokio::select! {
1108 result = async {
1109 match stdout.as_mut() {
1110 Some(out) => out.read(&mut stdout_buf).await,
1111 None => std::future::pending().await,
1112 }
1113 }, if !stdout_eof => {
1114 match result {
1115 Ok(0) | Err(_) => {
1116 stdout = None;
1117 stdout_eof = true;
1118 }
1119 Ok(n) => {
1120 let _ = tx.send((id, SessionOutput::Stdout(stdout_buf[..n].to_vec())));
1121 }
1122 }
1123 }
1124 result = async {
1125 match stderr.as_mut() {
1126 Some(err) => err.read(&mut stderr_buf).await,
1127 None => std::future::pending().await,
1128 }
1129 }, if !stderr_eof => {
1130 match result {
1131 Ok(0) | Err(_) => {
1132 stderr = None;
1133 stderr_eof = true;
1134 }
1135 Ok(n) => {
1136 let _ = tx.send((id, SessionOutput::Stderr(stderr_buf[..n].to_vec())));
1137 }
1138 }
1139 }
1140 }
1141 }
1142
1143 let code = match child.wait().await {
1145 Ok(status) => status.code().unwrap_or(-1),
1146 Err(_) => -1,
1147 };
1148
1149 let _ = tx.send((id, SessionOutput::Exited(code)));
1150}
1151
1152async fn wait_for_pid(pid: i32) -> i32 {
1154 tokio::task::spawn_blocking(move || {
1155 let mut status: i32 = 0;
1156 unsafe {
1157 libc::waitpid(pid, &mut status, 0);
1158 }
1159 if libc::WIFEXITED(status) {
1160 libc::WEXITSTATUS(status)
1161 } else {
1162 -1
1163 }
1164 })
1165 .await
1166 .unwrap_or(-1)
1167}
1168
1169#[cfg(test)]
1174mod tests {
1175 use std::time::Duration;
1176
1177 use tokio::time;
1178
1179 use microsandbox_protocol::exec::ExecRequest;
1180
1181 use super::*;
1182
1183 #[tokio::test]
1184 async fn test_pty_reader_drains_ready_fd() {
1185 let (tx, mut rx) = mpsc::unbounded_channel();
1186 let req = ExecRequest {
1187 cmd: "/bin/sh".to_string(),
1188 args: vec![
1189 "-c".to_string(),
1190 "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"
1191 .to_string(),
1192 ],
1193 env: vec!["PATH=/usr/local/bin:/usr/bin:/bin".to_string()],
1194 cwd: None,
1195 user: None,
1196 tty: true,
1197 rows: 24,
1198 cols: 80,
1199 rlimits: Vec::new(),
1200 };
1201
1202 let session = ExecSession::spawn(7, &req, tx, None, SecurityProfile::Default)
1203 .expect("spawn pty session");
1204 let mut stdout = Vec::new();
1205 let mut exit = None;
1206
1207 let recv_result = time::timeout(Duration::from_secs(15), async {
1208 while let Some((id, output)) = rx.recv().await {
1209 assert_eq!(id, 7);
1210 match output {
1211 SessionOutput::Stdout(data) => stdout.extend_from_slice(&data),
1212 SessionOutput::Exited(code) => {
1213 exit = Some(code);
1214 break;
1215 }
1216 SessionOutput::Stderr(_) | SessionOutput::Raw(_) => {}
1217 }
1218 }
1219 })
1220 .await;
1221
1222 if recv_result.is_err() {
1223 let _ = session.send_signal(libc::SIGKILL);
1224 panic!("timed out waiting for PTY output");
1225 }
1226
1227 assert_eq!(exit, Some(0));
1228
1229 let second = stdout
1230 .windows(b"SECOND".len())
1231 .position(|window| window == b"SECOND");
1232 let end = stdout
1233 .windows(b"<END>".len())
1234 .position(|window| window == b"<END>");
1235
1236 assert!(
1237 matches!((second, end), (Some(second), Some(end)) if second < end),
1238 "expected immediate PTY write to arrive before later output; got {:?}",
1239 String::from_utf8_lossy(&stdout),
1240 );
1241 }
1242
1243 #[test]
1244 fn test_resolve_user_spec_for_current_uid_gid() {
1245 let uid = unsafe { libc::getuid() };
1246 let gid = unsafe { libc::getgid() };
1247 let resolved = resolve_user_spec(&format!("{uid}:{gid}")).expect("resolve numeric user");
1248 assert_eq!(resolved.uid, uid);
1249 assert_eq!(resolved.gid, gid);
1250 }
1251
1252 #[test]
1253 fn test_request_user_overrides_config_default() {
1254 let req = ExecRequest {
1255 cmd: "/bin/true".to_string(),
1256 args: Vec::new(),
1257 env: Vec::new(),
1258 cwd: None,
1259 user: Some("1:1".to_string()),
1260 tty: false,
1261 rows: 24,
1262 cols: 80,
1263 rlimits: Vec::new(),
1264 };
1265
1266 let resolved = resolve_requested_user(&req, Some("0:0")).expect("resolve requested user");
1267 assert_eq!(resolved.unwrap().uid, 1);
1268 }
1269
1270 #[test]
1271 fn test_config_default_user_used_when_request_has_none() {
1272 let req = ExecRequest {
1273 cmd: "/bin/true".to_string(),
1274 args: Vec::new(),
1275 env: Vec::new(),
1276 cwd: None,
1277 user: None,
1278 tty: false,
1279 rows: 24,
1280 cols: 80,
1281 rlimits: Vec::new(),
1282 };
1283
1284 let uid = unsafe { libc::getuid() };
1285 let gid = unsafe { libc::getgid() };
1286 let resolved = resolve_requested_user(&req, Some(&format!("{uid}:{gid}")))
1287 .expect("resolve with config default");
1288 let resolved = resolved.expect("should resolve to a user");
1289 assert_eq!(resolved.uid, uid);
1290 assert_eq!(resolved.gid, gid);
1291 }
1292
1293 #[test]
1294 fn test_request_without_user_does_not_apply_user_switch() {
1295 let req = ExecRequest {
1296 cmd: "/bin/true".to_string(),
1297 args: Vec::new(),
1298 env: Vec::new(),
1299 cwd: None,
1300 user: None,
1301 tty: false,
1302 rows: 24,
1303 cols: 80,
1304 rlimits: Vec::new(),
1305 };
1306
1307 let resolved = resolve_requested_user(&req, None).expect("resolve absent user");
1308 assert!(resolved.is_none());
1309 }
1310
1311 #[test]
1312 fn test_default_user_absent_resolves_to_root() {
1313 let resolved = resolve_default_user(None).expect("resolve absent default user");
1314 assert_eq!(resolved, (0, 0));
1315 }
1316
1317 #[test]
1318 fn test_default_home_dir_uses_resolved_user_home() {
1319 let req = ExecRequest {
1320 cmd: "/bin/true".to_string(),
1321 args: Vec::new(),
1322 env: Vec::new(),
1323 cwd: None,
1324 user: None,
1325 tty: false,
1326 rows: 24,
1327 cols: 80,
1328 rlimits: Vec::new(),
1329 };
1330 let user = ResolvedUser {
1331 uid: 1000,
1332 gid: 1000,
1333 initgroups_user: None,
1334 home_dir: Some(CString::new("/home/tester").unwrap()),
1335 };
1336
1337 assert_eq!(
1338 default_home_dir(&req, Some(&user))
1339 .expect("resolve default home")
1340 .as_deref()
1341 .map(CStr::to_string_lossy),
1342 Some("/home/tester".into()),
1343 );
1344 }
1345
1346 #[test]
1347 fn test_default_home_dir_uses_root_when_user_absent() {
1348 let req = ExecRequest {
1349 cmd: "/bin/true".to_string(),
1350 args: Vec::new(),
1351 env: Vec::new(),
1352 cwd: None,
1353 user: None,
1354 tty: false,
1355 rows: 24,
1356 cols: 80,
1357 rlimits: Vec::new(),
1358 };
1359 let root = resolve_user_spec(DEFAULT_USER_SPEC).expect("resolve implicit root");
1360
1361 assert_eq!(
1362 default_home_dir(&req, None)
1363 .expect("resolve default home")
1364 .as_deref()
1365 .map(CStr::to_string_lossy),
1366 root.home_dir.as_deref().map(CStr::to_string_lossy),
1367 );
1368 }
1369
1370 #[test]
1371 fn test_default_home_dir_respects_explicit_home_env() {
1372 let req = ExecRequest {
1373 cmd: "/bin/true".to_string(),
1374 args: Vec::new(),
1375 env: vec!["HOME=/tmp/custom".to_string()],
1376 cwd: None,
1377 user: None,
1378 tty: false,
1379 rows: 24,
1380 cols: 80,
1381 rlimits: Vec::new(),
1382 };
1383 let user = ResolvedUser {
1384 uid: 1000,
1385 gid: 1000,
1386 initgroups_user: None,
1387 home_dir: Some(CString::new("/home/tester").unwrap()),
1388 };
1389
1390 assert!(
1391 default_home_dir(&req, Some(&user))
1392 .expect("resolve default home")
1393 .is_none()
1394 );
1395 }
1396
1397 #[tokio::test]
1398 async fn test_spawn_pipe_error_does_not_include_probe_details() {
1399 let (tx, _rx) = mpsc::unbounded_channel();
1400 let req = ExecRequest {
1401 cmd: "/definitely/not/a/real/binary".to_string(),
1402 args: Vec::new(),
1403 env: Vec::new(),
1404 cwd: None,
1405 user: None,
1406 tty: false,
1407 rows: 24,
1408 cols: 80,
1409 rlimits: Vec::new(),
1410 };
1411
1412 let err = ExecSession::spawn(9, &req, tx, None, SecurityProfile::Default)
1413 .expect_err("spawn should fail");
1414
1415 let payload = match &err {
1419 AgentdError::ExecSpawnFailed(p) => p,
1420 other => panic!("expected ExecSpawnFailed, got: {other:?}"),
1421 };
1422 assert_eq!(payload.kind, ExecFailureKind::NotFound);
1423 assert_eq!(payload.errno, Some(libc::ENOENT));
1424 assert_eq!(payload.errno_name.as_deref(), Some("ENOENT"));
1425
1426 let message = &payload.message;
1432 assert!(message.contains("spawn"));
1433 assert!(!message.contains("symlink_metadata="));
1434 assert!(!message.contains("metadata="));
1435 assert!(!message.contains("magic="));
1436 assert!(!message.contains("path_probe="));
1437 assert!(!message.contains("cwd_probe="));
1438 assert!(!message.contains("target_probe="));
1439 }
1440}