From a3cf6f640828647e34afe96a626b3b4f6bbb22b1 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Thu, 27 Feb 2025 22:00:15 +0000 Subject: [PATCH 1/4] Add `std::os::unix::process::CommandExt::chroot` to safely chroot a child process This adds a `chroot` method to the `CommandExt` extension trait for the `Command` builder, to set a directory to chroot into. This will chroot the child process into that directory right before calling chdir for the `Command`'s working directory. To avoid allowing a process to have a working directory outside of the chroot, if the `Command` does not yet have a working directory set, `chroot` will set its working directory to "/". --- library/std/src/os/unix/process.rs | 16 ++++++++++++++++ library/std/src/sys/process/unix/common.rs | 13 +++++++++++++ library/std/src/sys/process/unix/unix.rs | 10 ++++++++++ library/std/src/sys/process/unix/vxworks.rs | 6 ++++++ 4 files changed, 45 insertions(+) diff --git a/library/std/src/os/unix/process.rs b/library/std/src/os/unix/process.rs index 7c3fa7d6507e7..27866badfbe58 100644 --- a/library/std/src/os/unix/process.rs +++ b/library/std/src/os/unix/process.rs @@ -8,6 +8,7 @@ use cfg_if::cfg_if; use crate::ffi::OsStr; use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd}; +use crate::path::Path; use crate::sealed::Sealed; use crate::sys_common::{AsInner, AsInnerMut, FromInner, IntoInner}; use crate::{io, process, sys}; @@ -197,6 +198,16 @@ pub trait CommandExt: Sealed { /// ``` #[stable(feature = "process_set_process_group", since = "1.64.0")] fn process_group(&mut self, pgroup: i32) -> &mut process::Command; + + /// Set the root of the child process. This calls `chroot` in the child process before executing + /// the command. + /// + /// This happens before changing to the directory specified with `Command::current_dir`, and + /// that directory will be relative to the new root. If no directory has been specified with + /// `Command::current_dir`, this will set the directory to `/`, to avoid leaving the current + /// directory outside the chroot. + #[unstable(feature = "process_chroot", issue = "none")] + fn chroot>(&mut self, dir: P) -> &mut process::Command; } #[stable(feature = "rust1", since = "1.0.0")] @@ -242,6 +253,11 @@ impl CommandExt for process::Command { self.as_inner_mut().pgroup(pgroup); self } + + fn chroot>(&mut self, dir: P) -> &mut process::Command { + self.as_inner_mut().chroot(dir.as_ref()); + self + } } /// Unix-specific extensions to [`process::ExitStatus`] and diff --git a/library/std/src/sys/process/unix/common.rs b/library/std/src/sys/process/unix/common.rs index a9c2510e6d454..e205a8390052f 100644 --- a/library/std/src/sys/process/unix/common.rs +++ b/library/std/src/sys/process/unix/common.rs @@ -88,6 +88,7 @@ pub struct Command { program_kind: ProgramKind, cwd: Option, + chroot: Option, uid: Option, gid: Option, saw_nul: bool, @@ -182,6 +183,7 @@ impl Command { program_kind, env: Default::default(), cwd: None, + chroot: None, uid: None, gid: None, saw_nul, @@ -206,6 +208,7 @@ impl Command { program_kind, env: Default::default(), cwd: None, + chroot: None, uid: None, gid: None, saw_nul, @@ -254,6 +257,12 @@ impl Command { pub fn pgroup(&mut self, pgroup: pid_t) { self.pgroup = Some(pgroup); } + pub fn chroot(&mut self, dir: &Path) { + self.chroot = Some(os2c(dir.as_os_str(), &mut self.saw_nul)); + if self.cwd.is_none() { + self.cwd(&OsStr::new("/")); + } + } #[cfg(target_os = "linux")] pub fn create_pidfd(&mut self, val: bool) { @@ -326,6 +335,10 @@ impl Command { pub fn get_pgroup(&self) -> Option { self.pgroup } + #[allow(dead_code)] + pub fn get_chroot(&self) -> Option<&CStr> { + self.chroot.as_deref() + } pub fn get_closures(&mut self) -> &mut Vec io::Result<()> + Send + Sync>> { &mut self.closures diff --git a/library/std/src/sys/process/unix/unix.rs b/library/std/src/sys/process/unix/unix.rs index 1b3bd2de265da..4f595ac9a1c5f 100644 --- a/library/std/src/sys/process/unix/unix.rs +++ b/library/std/src/sys/process/unix/unix.rs @@ -323,6 +323,15 @@ impl Command { cvt(libc::setuid(u as uid_t))?; } } + if let Some(chroot) = self.get_chroot() { + #[cfg(not(target_os = "fuchsia"))] + cvt(libc::chroot(chroot.as_ptr()))?; + #[cfg(target_os = "fuchsia")] + return Err(io::const_error!( + io::ErrorKind::Unsupported, + "chroot not supported by fuchsia" + )); + } if let Some(cwd) = self.get_cwd() { cvt(libc::chdir(cwd.as_ptr()))?; } @@ -447,6 +456,7 @@ impl Command { || (self.env_saw_path() && !self.program_is_path()) || !self.get_closures().is_empty() || self.get_groups().is_some() + || self.get_chroot().is_some() { return Ok(None); } diff --git a/library/std/src/sys/process/unix/vxworks.rs b/library/std/src/sys/process/unix/vxworks.rs index fab3b36ebf3fa..f33b4a375da83 100644 --- a/library/std/src/sys/process/unix/vxworks.rs +++ b/library/std/src/sys/process/unix/vxworks.rs @@ -27,6 +27,12 @@ impl Command { "nul byte found in provided data", )); } + if self.get_chroot().is_some() { + return Err(io::const_error!( + ErrorKind::Unsupported, + "chroot not supported by vxworks", + )); + } let (ours, theirs) = self.setup_io(default, needs_stdin)?; let mut p = Process { pid: 0, status: None }; From c3b750ce0f6d1b40874f88b552d271abea3a3dde Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 20 May 2025 17:59:18 +0200 Subject: [PATCH 2/4] `CommandExt::chroot`: Document difference to underlying `chroot` --- library/std/src/os/unix/process.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/library/std/src/os/unix/process.rs b/library/std/src/os/unix/process.rs index 27866badfbe58..8d238aa583519 100644 --- a/library/std/src/os/unix/process.rs +++ b/library/std/src/os/unix/process.rs @@ -203,9 +203,11 @@ pub trait CommandExt: Sealed { /// the command. /// /// This happens before changing to the directory specified with `Command::current_dir`, and - /// that directory will be relative to the new root. If no directory has been specified with - /// `Command::current_dir`, this will set the directory to `/`, to avoid leaving the current - /// directory outside the chroot. + /// that directory will be relative to the new root. + /// + /// If no directory has been specified with `Command::current_dir`, this will set the directory + /// to `/`, to avoid leaving the current directory outside the chroot. (This is an intentional + /// difference from the underlying `chroot` system call.) #[unstable(feature = "process_chroot", issue = "none")] fn chroot>(&mut self, dir: P) -> &mut process::Command; } From 17fdf19c9cd566987c3f96f69ac60d4741e05b07 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 20 May 2025 18:04:53 +0200 Subject: [PATCH 3/4] `CommandExt::chroot`: Add tracking issue --- library/std/src/os/unix/process.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/std/src/os/unix/process.rs b/library/std/src/os/unix/process.rs index 8d238aa583519..659b354483458 100644 --- a/library/std/src/os/unix/process.rs +++ b/library/std/src/os/unix/process.rs @@ -208,7 +208,7 @@ pub trait CommandExt: Sealed { /// If no directory has been specified with `Command::current_dir`, this will set the directory /// to `/`, to avoid leaving the current directory outside the chroot. (This is an intentional /// difference from the underlying `chroot` system call.) - #[unstable(feature = "process_chroot", issue = "none")] + #[unstable(feature = "process_chroot", issue = "141298")] fn chroot>(&mut self, dir: P) -> &mut process::Command; } From 348c1b0d886960a57a866e537458dae6bf75ec23 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 20 May 2025 23:00:24 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Link `Command::current_dir`. Co-authored-by: Amanieu d'Antras --- library/std/src/os/unix/process.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/std/src/os/unix/process.rs b/library/std/src/os/unix/process.rs index 659b354483458..57ce3c5a4bf4a 100644 --- a/library/std/src/os/unix/process.rs +++ b/library/std/src/os/unix/process.rs @@ -202,12 +202,12 @@ pub trait CommandExt: Sealed { /// Set the root of the child process. This calls `chroot` in the child process before executing /// the command. /// - /// This happens before changing to the directory specified with `Command::current_dir`, and - /// that directory will be relative to the new root. + /// This happens before changing to the directory specified with + /// [`process::Command::current_dir`], and that directory will be relative to the new root. /// - /// If no directory has been specified with `Command::current_dir`, this will set the directory - /// to `/`, to avoid leaving the current directory outside the chroot. (This is an intentional - /// difference from the underlying `chroot` system call.) + /// If no directory has been specified with [`process::Command::current_dir`], this will set the + /// directory to `/`, to avoid leaving the current directory outside the chroot. (This is an + /// intentional difference from the underlying `chroot` system call.) #[unstable(feature = "process_chroot", issue = "141298")] fn chroot>(&mut self, dir: P) -> &mut process::Command; }