Skip to content

Commit f892c4c

Browse files
committed
Add Command::resolve_in_parent_path
1 parent df8102f commit f892c4c

File tree

7 files changed

+160
-13
lines changed

7 files changed

+160
-13
lines changed

library/std/src/process.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,22 @@ impl Command {
11951195
pub fn get_current_dir(&self) -> Option<&Path> {
11961196
self.inner.get_current_dir()
11971197
}
1198+
1199+
/// Resolve the program given in [`new`](Self::new) using the parent's PATH.
1200+
///
1201+
/// Usually when a `env` is used to set `PATH` on a child process,
1202+
/// that new `PATH` will be used to discover the process.
1203+
/// With `resolve_in_parent_path` to `true`, the parent's `PATH` will be used instead.
1204+
///
1205+
/// # Platform-specific behavior
1206+
///
1207+
/// On Unix the process is executed after fork and after setting up the rest of the new environment.
1208+
/// So if `chroot`, `uid`, `gid` or `groups` are used then the way `PATH` is interpreted may be changed.
1209+
#[unstable(feature = "command_resolve", issue = "none")]
1210+
pub fn resolve_in_parent_path(&mut self, use_parent: bool) -> &mut Self {
1211+
self.inner.resolve_in_parent_path(use_parent);
1212+
self
1213+
}
11981214
}
11991215

12001216
#[stable(feature = "rust1", since = "1.0.0")]

library/std/src/process/tests.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use super::{Command, Output, Stdio};
22
use crate::io::prelude::*;
33
use crate::io::{BorrowedBuf, ErrorKind};
44
use crate::mem::MaybeUninit;
5-
use crate::str;
5+
use crate::{fs, str};
66

77
fn known_command() -> Command {
8-
if cfg!(windows) { Command::new("help") } else { Command::new("echo") }
8+
if cfg!(windows) { Command::new("help.exe") } else { Command::new("echo") }
99
}
1010

1111
#[cfg(target_os = "android")]
@@ -603,3 +603,42 @@ fn terminate_exited_process() {
603603
assert!(p.kill().is_ok());
604604
assert!(p.kill().is_ok());
605605
}
606+
607+
#[test]
608+
fn resolve_in_parent_path() {
609+
let tempdir = crate::test_helpers::tmpdir();
610+
let mut cmd = if cfg!(windows) {
611+
// On Windows std prepends the system paths when searching for executables
612+
// but not if PATH is set on the command. Therefore adding a broken exe
613+
// using PATH will cause spawn to fail if it's invoked.
614+
let mut cmd = known_command();
615+
fs::write(tempdir.join(cmd.get_program().to_str().unwrap()), "").unwrap();
616+
cmd.env("PATH", tempdir.path());
617+
cmd
618+
} else {
619+
let mut cmd = known_command();
620+
cmd.env("PATH", "");
621+
cmd
622+
};
623+
624+
assert!(cmd.spawn().is_err());
625+
cmd.resolve_in_parent_path(true);
626+
assert!(cmd.status().unwrap().success());
627+
cmd.resolve_in_parent_path(false);
628+
assert!(cmd.spawn().is_err());
629+
630+
// If the platform is a unix and has posix_spawn then that will be used in the test above.
631+
// So we also test the fork/exec path by setting a pre_exec function.
632+
#[cfg(unix)]
633+
{
634+
use crate::os::unix::process::CommandExt;
635+
unsafe {
636+
cmd.pre_exec(|| Ok(()));
637+
}
638+
assert!(cmd.spawn().is_err());
639+
cmd.resolve_in_parent_path(true);
640+
assert!(cmd.status().unwrap().success());
641+
cmd.resolve_in_parent_path(false);
642+
assert!(cmd.spawn().is_err());
643+
}
644+
}

library/std/src/sys/process/uefi.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ impl Command {
7878
self.stderr = Some(stderr);
7979
}
8080

81+
pub fn resolve_in_parent_path(&mut self, _use_parent: bool) -> &mut Self {
82+
self
83+
}
84+
8185
pub fn get_program(&self) -> &OsStr {
8286
self.prog.as_ref()
8387
}

library/std/src/sys/process/unix/common.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub struct Command {
9898
#[cfg(target_os = "linux")]
9999
create_pidfd: bool,
100100
pgroup: Option<pid_t>,
101+
resolve_in_parent_path: bool,
101102
}
102103

103104
// passed back to std::process with the pipes connected to the child, if any
@@ -185,6 +186,7 @@ impl Command {
185186
#[cfg(target_os = "linux")]
186187
create_pidfd: false,
187188
pgroup: None,
189+
resolve_in_parent_path: false,
188190
}
189191
}
190192

@@ -220,6 +222,9 @@ impl Command {
220222
self.cwd(&OsStr::new("/"));
221223
}
222224
}
225+
pub fn resolve_in_parent_path(&mut self, use_parent: bool) {
226+
self.resolve_in_parent_path = use_parent;
227+
}
223228

224229
#[cfg(target_os = "linux")]
225230
pub fn create_pidfd(&mut self, val: bool) {
@@ -298,6 +303,10 @@ impl Command {
298303
pub fn get_chroot(&self) -> Option<&CStr> {
299304
self.chroot.as_deref()
300305
}
306+
#[allow(dead_code)]
307+
pub fn get_resolve_in_parent_path(&self) -> bool {
308+
self.resolve_in_parent_path
309+
}
301310

302311
pub fn get_closures(&mut self) -> &mut Vec<Box<dyn FnMut() -> io::Result<()> + Send + Sync>> {
303312
&mut self.closures

library/std/src/sys/process/unix/unix.rs

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -378,27 +378,89 @@ impl Command {
378378
callback()?;
379379
}
380380

381+
let mut _reset = None;
381382
// Although we're performing an exec here we may also return with an
382383
// error from this function (without actually exec'ing) in which case we
383384
// want to be sure to restore the global environment back to what it
384385
// once was, ensuring that our temporary override, when free'd, doesn't
385386
// corrupt our process's environment.
386-
let mut _reset = None;
387-
if let Some(envp) = maybe_envp {
388-
struct Reset(*const *const libc::c_char);
387+
struct Reset(*const *const libc::c_char);
389388

390-
impl Drop for Reset {
391-
fn drop(&mut self) {
392-
unsafe {
393-
*sys::env::environ() = self.0;
394-
}
389+
impl Drop for Reset {
390+
fn drop(&mut self) {
391+
unsafe {
392+
*sys::env::environ() = self.0;
395393
}
396394
}
395+
}
397396

397+
if let Some(envp) = maybe_envp
398+
&& self.get_resolve_in_parent_path()
399+
&& self.env_saw_path()
400+
&& self.get_program_kind() == ProgramKind::PathLookup
401+
{
402+
use crate::ffi::CStr;
403+
// execvpe is a gnu extension...
404+
#[cfg(all(target_os = "linux", target_env = "gnu"))]
405+
unsafe fn exec_with_env(
406+
program: &CStr,
407+
args: &CStringArray,
408+
envp: &CStringArray,
409+
) -> io::Error {
410+
unsafe { libc::execvpe(program.as_ptr(), args.as_ptr(), envp.as_ptr()) };
411+
io::Error::last_os_error()
412+
}
413+
414+
// ...so if we're not gnu then use our own implementation.
415+
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
416+
unsafe fn exec_with_env(
417+
program: &CStr,
418+
args: &CStringArray,
419+
envp: &CStringArray,
420+
) -> io::Error {
421+
unsafe {
422+
let name = program.to_bytes();
423+
let mut buffer =
424+
[const { mem::MaybeUninit::<u8>::uninit() }; libc::PATH_MAX as usize];
425+
let mut environ = *sys::env::environ();
426+
// Search the environment for PATH and, if found,
427+
// search the paths for the executable by trying to execve each candidate.
428+
while !(*environ).is_null() {
429+
let kv = CStr::from_ptr(*environ);
430+
if let Some(value) = kv.to_bytes().strip_prefix(b"PATH=") {
431+
for path in value.split(|&b| b == b':') {
432+
if buffer.len() - 2 >= path.len().saturating_add(name.len()) {
433+
let buf_ptr = buffer.as_mut_ptr().cast::<u8>();
434+
let mut offset = 0;
435+
if !path.is_empty() {
436+
buf_ptr.copy_from(path.as_ptr(), path.len());
437+
offset += path.len();
438+
if path.last() != Some(&b'/') {
439+
*buf_ptr.add(path.len()) = b'/';
440+
offset += 1;
441+
}
442+
}
443+
buf_ptr.add(offset).copy_from(name.as_ptr(), name.len());
444+
offset += name.len();
445+
*buf_ptr.add(offset) = 0;
446+
libc::execve(buf_ptr.cast(), args.as_ptr(), envp.as_ptr());
447+
}
448+
}
449+
break;
450+
}
451+
environ = environ.add(1);
452+
}
453+
// If execve is successful then it'll never return,
454+
// thus we only reach this point on failure..
455+
io::Error::from_raw_os_error(libc::ENOENT)
456+
}
457+
}
458+
_reset = Some(Reset(*sys::env::environ()));
459+
return Err(exec_with_env(self.get_program_cstr(), self.get_argv(), envp));
460+
} else if let Some(envp) = maybe_envp {
398461
_reset = Some(Reset(*sys::env::environ()));
399462
*sys::env::environ() = envp.as_ptr();
400463
}
401-
402464
libc::execvp(self.get_program_cstr().as_ptr(), self.get_argv().as_ptr());
403465
Err(io::Error::last_os_error())
404466
}
@@ -453,7 +515,9 @@ impl Command {
453515

454516
if self.get_gid().is_some()
455517
|| self.get_uid().is_some()
456-
|| (self.env_saw_path() && !self.program_is_path())
518+
|| (!self.get_resolve_in_parent_path()
519+
&& self.env_saw_path()
520+
&& !self.program_is_path())
457521
|| !self.get_closures().is_empty()
458522
|| self.get_groups().is_some()
459523
|| self.get_chroot().is_some()

library/std/src/sys/process/unsupported.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub struct Command {
2121
stdin: Option<Stdio>,
2222
stdout: Option<Stdio>,
2323
stderr: Option<Stdio>,
24+
force_parent_path: bool,
2425
}
2526

2627
// passed back to std::process with the pipes connected to the child, if any
@@ -52,6 +53,7 @@ impl Command {
5253
stdin: None,
5354
stdout: None,
5455
stderr: None,
56+
force_parent_path: false,
5557
}
5658
}
5759

@@ -79,6 +81,10 @@ impl Command {
7981
self.stderr = Some(stderr);
8082
}
8183

84+
pub fn resolve_in_parent_path(&mut self, use_parent: bool) {
85+
self.force_parent_path = use_parent;
86+
}
87+
8288
pub fn get_program(&self) -> &OsStr {
8389
&self.program
8490
}

library/std/src/sys/process/windows.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ pub struct Command {
158158
startupinfo_fullscreen: bool,
159159
startupinfo_untrusted_source: bool,
160160
startupinfo_force_feedback: Option<bool>,
161+
force_parent_path: bool,
161162
}
162163

163164
pub enum Stdio {
@@ -192,6 +193,7 @@ impl Command {
192193
startupinfo_fullscreen: false,
193194
startupinfo_untrusted_source: false,
194195
startupinfo_force_feedback: None,
196+
force_parent_path: false,
195197
}
196198
}
197199

@@ -224,6 +226,10 @@ impl Command {
224226
self.force_quotes_enabled = enabled;
225227
}
226228

229+
pub fn resolve_in_parent_path(&mut self, use_parent: bool) {
230+
self.force_parent_path = use_parent;
231+
}
232+
227233
pub fn raw_arg(&mut self, command_str_to_append: &OsStr) {
228234
self.args.push(Arg::Raw(command_str_to_append.to_os_string()))
229235
}
@@ -274,7 +280,10 @@ impl Command {
274280
let env_saw_path = self.env.have_changed_path();
275281
let maybe_env = self.env.capture_if_changed();
276282

277-
let child_paths = if env_saw_path && let Some(env) = maybe_env.as_ref() {
283+
let child_paths = if env_saw_path
284+
&& !self.force_parent_path
285+
&& let Some(env) = maybe_env.as_ref()
286+
{
278287
env.get(&EnvKey::new("PATH")).map(|s| s.as_os_str())
279288
} else {
280289
None

0 commit comments

Comments
 (0)