Skip to content

Commit e211489

Browse files
Auto merge of #142035 - ChrisDenton:parent-path, r=<try>
Add `Command::resolve_in_parent_path` This is part of #141976 (ACP: rust-lang/libs-team#591). libs-api could not come to a consensus on what the final API should look like so they've agreed to experiment with a few different approaches. This PR just implements the `resolve_in_parent_path` method which forces `Command` to ignore `PATH` set on the child for the purposes of resolving the executable. There was no consensus on what should happen if, for example, `chroot` is set (which can change the meaning of paths) so for now I've just done the easy thing. Figuring out the correct behaviour here will need to be done before this is considered for stabilisation but hopefully having it on nightly will nudge people in to giving their opinions. try-job: `x86_64-msvc-*` try-job: `dist-various-*`
2 parents ccf3198 + ee32374 commit e211489

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", "/a/b/c");
621+
cmd
622+
};
623+
624+
assert!(cmd.spawn().is_err());
625+
cmd.resolve_in_parent_path(true);
626+
cmd.spawn().unwrap();
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)