Skip to content

Commit 5b34fd8

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: `aarch64-apple` try-job: `x86_64-msvc-*` try-job: `dist-various-*`
2 parents 425e142 + 57a631f commit 5b34fd8

File tree

6 files changed

+158
-12
lines changed

6 files changed

+158
-12
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/unix/common.rs

Lines changed: 8 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,9 @@ impl Command {
298303
pub fn get_chroot(&self) -> Option<&CStr> {
299304
self.chroot.as_deref()
300305
}
306+
pub fn get_resolve_in_parent_path(&self) -> bool {
307+
self.resolve_in_parent_path
308+
}
301309

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

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

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ impl Command {
8989
// The child calls `mem::forget` to leak the lock, which is crucial because
9090
// releasing a lock is not async-signal-safe.
9191
let env_lock = sys::env::env_read_lock();
92+
9293
let pid = unsafe { self.do_fork()? };
9394

9495
if pid == 0 {
@@ -276,7 +277,7 @@ impl Command {
276277
unsafe fn do_exec(
277278
&mut self,
278279
stdio: ChildPipes,
279-
maybe_envp: Option<&CStringArray>,
280+
mut maybe_envp: Option<&CStringArray>,
280281
) -> Result<!, io::Error> {
281282
use crate::sys::{self, cvt_r};
282283

@@ -378,13 +379,17 @@ impl Command {
378379
callback()?;
379380
}
380381

381-
// Although we're performing an exec here we may also return with an
382-
// error from this function (without actually exec'ing) in which case we
383-
// want to be sure to restore the global environment back to what it
384-
// once was, ensuring that our temporary override, when free'd, doesn't
385-
// corrupt our process's environment.
386382
let mut _reset = None;
387-
if let Some(envp) = maybe_envp {
383+
if let Some(envp) = maybe_envp
384+
&& (!self.get_resolve_in_parent_path()
385+
|| !self.env_saw_path()
386+
|| self.get_program_kind() != ProgramKind::PathLookup)
387+
{
388+
// Although we're performing an exec here we may also return with an
389+
// error from this function (without actually exec'ing) in which case we
390+
// want to be sure to restore the global environment back to what it
391+
// once was, ensuring that our temporary override, when free'd, doesn't
392+
// corrupt our process's environment.
388393
struct Reset(*const *const libc::c_char);
389394

390395
impl Drop for Reset {
@@ -397,9 +402,69 @@ impl Command {
397402

398403
_reset = Some(Reset(*sys::env::environ()));
399404
*sys::env::environ() = envp.as_ptr();
405+
// We've set the environment, no need to set it again.
406+
maybe_envp = None;
400407
}
401408

402-
libc::execvp(self.get_program_cstr().as_ptr(), self.get_argv().as_ptr());
409+
use crate::ffi::CStr;
410+
// execvpe is a gnu extension...
411+
#[cfg(all(target_os = "linux", target_env = "gnu"))]
412+
unsafe fn exec_with_env(
413+
program: &CStr,
414+
args: &CStringArray,
415+
envp: &CStringArray,
416+
) -> libc::c_int {
417+
unsafe { libc::execvpe(program.as_ptr(), args.as_ptr(), envp.as_ptr()) }
418+
}
419+
420+
// ...so if we're not gnu then use our own implementation.
421+
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
422+
unsafe fn exec_with_env(
423+
program: &CStr,
424+
args: &CStringArray,
425+
envp: &CStringArray,
426+
) -> libc::c_int {
427+
unsafe {
428+
let name = program.to_bytes();
429+
let mut buffer =
430+
[const { mem::MaybeUninit::<u8>::uninit() }; libc::PATH_MAX as usize];
431+
let mut environ = *sys::env::environ();
432+
// Search the environment for PATH and, if found,
433+
// search the paths for the executable by trying to execve each candidate.
434+
while !(*environ).is_null() {
435+
let kv = CStr::from_ptr(*environ);
436+
if let Some(value) = kv.to_bytes().strip_prefix(b"PATH=") {
437+
for path in value.split(|&b| b == b':') {
438+
if buffer.len() - 2 >= path.len().saturating_add(name.len()) {
439+
let buf_ptr = buffer.as_mut_ptr().cast::<u8>();
440+
let mut offset = 0;
441+
if !path.is_empty() {
442+
buf_ptr.copy_from(path.as_ptr(), path.len());
443+
offset += path.len();
444+
if path.last() != Some(&b'/') {
445+
*buf_ptr.add(path.len()) = b'/';
446+
offset += 1;
447+
}
448+
}
449+
buf_ptr.add(offset).copy_from(name.as_ptr(), name.len());
450+
offset += name.len();
451+
*buf_ptr.add(offset) = 0;
452+
libc::execve(buf_ptr.cast(), args.as_ptr(), envp.as_ptr());
453+
}
454+
}
455+
break;
456+
}
457+
environ = environ.add(1);
458+
}
459+
// If execve is successful then it'll never return, thus we'll never reach this point.
460+
-1
461+
}
462+
}
463+
if let Some(envp) = maybe_envp {
464+
exec_with_env(self.get_program_cstr(), self.get_argv(), envp);
465+
} else {
466+
libc::execvp(self.get_program_cstr().as_ptr(), self.get_argv().as_ptr());
467+
}
403468
Err(io::Error::last_os_error())
404469
}
405470

@@ -453,7 +518,9 @@ impl Command {
453518

454519
if self.get_gid().is_some()
455520
|| self.get_uid().is_some()
456-
|| (self.env_saw_path() && !self.program_is_path())
521+
|| (!self.get_resolve_in_parent_path()
522+
&& self.env_saw_path()
523+
&& !self.program_is_path())
457524
|| !self.get_closures().is_empty()
458525
|| self.get_groups().is_some()
459526
|| self.get_chroot().is_some()
@@ -793,6 +860,7 @@ impl Command {
793860
// Safety: -1 indicates we don't have a pidfd.
794861
let mut p = Process::new(0, -1);
795862

863+
// FIXME: if a list of paths to search is passed then retry for every path.
796864
let spawn_res = spawn_fn(
797865
&mut p.pid,
798866
self.get_program_cstr().as_ptr(),

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)