Skip to content

Commit ad4c885

Browse files
committed
Auto merge of #55359 - alex:command-exec-uaf, r=alexcrichton
Fixes #46775 -- don't mutate the process's environment in Command::exec Instead, pass the environment to execvpe, so the kernel can apply it directly to the new process. This avoids a use-after-free in the case where exec'ing the new process fails for any reason, as well as a race condition if there are other threads alive during the exec. Fixes #46775
2 parents 5eda136 + 36fe3b6 commit ad4c885

File tree

3 files changed

+111
-8
lines changed

3 files changed

+111
-8
lines changed

Diff for: src/libstd/sys/unix/process/process_common.rs

+8
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ impl Command {
141141
pub fn get_argv(&self) -> &Vec<*const c_char> {
142142
&self.argv.0
143143
}
144+
#[cfg(not(target_os = "fuchsia"))]
145+
pub fn get_program(&self) -> &CString {
146+
return &self.program;
147+
}
144148

145149
#[allow(dead_code)]
146150
pub fn get_cwd(&self) -> &Option<CString> {
@@ -244,6 +248,10 @@ impl CStringArray {
244248
pub fn as_ptr(&self) -> *const *const c_char {
245249
self.ptrs.as_ptr()
246250
}
251+
#[cfg(not(target_os = "fuchsia"))]
252+
pub fn get_items(&self) -> &[CString] {
253+
return &self.items;
254+
}
247255
}
248256

249257
fn construct_envp(env: BTreeMap<DefaultEnvKey, OsString>, saw_nul: &mut bool) -> CStringArray {

Diff for: src/libstd/sys/unix/process/process_unix.rs

+91-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// option. This file may not be copied, modified, or distributed
99
// except according to those terms.
1010

11+
use env;
12+
use ffi::CString;
1113
use io::{self, Error, ErrorKind};
1214
use libc::{self, c_int, gid_t, pid_t, uid_t};
1315
use ptr;
@@ -39,13 +41,15 @@ impl Command {
3941
return Ok((ret, ours))
4042
}
4143

44+
let possible_paths = self.compute_possible_paths(envp.as_ref());
45+
4246
let (input, output) = sys::pipe::anon_pipe()?;
4347

4448
let pid = unsafe {
4549
match cvt(libc::fork())? {
4650
0 => {
4751
drop(input);
48-
let err = self.do_exec(theirs, envp.as_ref());
52+
let err = self.do_exec(theirs, envp.as_ref(), possible_paths);
4953
let errno = err.raw_os_error().unwrap_or(libc::EINVAL) as u32;
5054
let bytes = [
5155
(errno >> 24) as u8,
@@ -113,12 +117,48 @@ impl Command {
113117
"nul byte found in provided data")
114118
}
115119

120+
let possible_paths = self.compute_possible_paths(envp.as_ref());
116121
match self.setup_io(default, true) {
117-
Ok((_, theirs)) => unsafe { self.do_exec(theirs, envp.as_ref()) },
122+
Ok((_, theirs)) => unsafe { self.do_exec(theirs, envp.as_ref(), possible_paths) },
118123
Err(e) => e,
119124
}
120125
}
121126

127+
fn compute_possible_paths(&self, maybe_envp: Option<&CStringArray>) -> Option<Vec<CString>> {
128+
let program = self.get_program().as_bytes();
129+
if program.contains(&b'/') {
130+
return None;
131+
}
132+
// Outside the match so we can borrow it for the lifetime of the function.
133+
let parent_path = env::var("PATH").ok();
134+
let paths = match maybe_envp {
135+
Some(envp) => {
136+
match envp.get_items().iter().find(|var| var.as_bytes().starts_with(b"PATH=")) {
137+
Some(p) => &p.as_bytes()[5..],
138+
None => return None,
139+
}
140+
},
141+
// maybe_envp is None if the process isn't changing the parent's env at all.
142+
None => {
143+
match parent_path.as_ref() {
144+
Some(p) => p.as_bytes(),
145+
None => return None,
146+
}
147+
},
148+
};
149+
150+
let mut possible_paths = vec![];
151+
for path in paths.split(|p| *p == b':') {
152+
let mut binary_path = Vec::with_capacity(program.len() + path.len() + 1);
153+
binary_path.extend_from_slice(path);
154+
binary_path.push(b'/');
155+
binary_path.extend_from_slice(program);
156+
let c_binary_path = CString::new(binary_path).unwrap();
157+
possible_paths.push(c_binary_path);
158+
}
159+
return Some(possible_paths);
160+
}
161+
122162
// And at this point we've reached a special time in the life of the
123163
// child. The child must now be considered hamstrung and unable to
124164
// do anything other than syscalls really. Consider the following
@@ -152,7 +192,8 @@ impl Command {
152192
unsafe fn do_exec(
153193
&mut self,
154194
stdio: ChildPipes,
155-
maybe_envp: Option<&CStringArray>
195+
maybe_envp: Option<&CStringArray>,
196+
maybe_possible_paths: Option<Vec<CString>>,
156197
) -> io::Error {
157198
use sys::{self, cvt_r};
158199

@@ -193,9 +234,6 @@ impl Command {
193234
if let Some(ref cwd) = *self.get_cwd() {
194235
t!(cvt(libc::chdir(cwd.as_ptr())));
195236
}
196-
if let Some(envp) = maybe_envp {
197-
*sys::os::environ() = envp.as_ptr();
198-
}
199237

200238
// emscripten has no signal support.
201239
#[cfg(not(any(target_os = "emscripten")))]
@@ -231,8 +269,53 @@ impl Command {
231269
t!(callback());
232270
}
233271

234-
libc::execvp(self.get_argv()[0], self.get_argv().as_ptr());
235-
io::Error::last_os_error()
272+
// If the program isn't an absolute path, and our environment contains a PATH var, then we
273+
// implement the PATH traversal ourselves so that it honors the child's PATH instead of the
274+
// parent's. This mirrors the logic that exists in glibc's execvpe, except using the
275+
// child's env to fetch PATH.
276+
match maybe_possible_paths {
277+
Some(possible_paths) => {
278+
let mut pending_error = None;
279+
for path in possible_paths {
280+
libc::execve(
281+
path.as_ptr(),
282+
self.get_argv().as_ptr(),
283+
maybe_envp.map(|envp| envp.as_ptr()).unwrap_or_else(|| *sys::os::environ())
284+
);
285+
let err = io::Error::last_os_error();
286+
match err.kind() {
287+
io::ErrorKind::PermissionDenied => {
288+
// If we saw a PermissionDenied, and none of the other entries in
289+
// $PATH are successful, then we'll return the first EACCESS we see.
290+
if pending_error.is_none() {
291+
pending_error = Some(err);
292+
}
293+
},
294+
// Errors which indicate we failed to find a file are ignored and we try
295+
// the next entry in the path.
296+
io::ErrorKind::NotFound | io::ErrorKind::TimedOut => {
297+
continue
298+
},
299+
// Any other error means we found a file and couldn't execute it.
300+
_ => {
301+
return err;
302+
}
303+
}
304+
}
305+
if let Some(err) = pending_error {
306+
return err;
307+
}
308+
return io::Error::from_raw_os_error(libc::ENOENT);
309+
},
310+
_ => {
311+
libc::execve(
312+
self.get_argv()[0],
313+
self.get_argv().as_ptr(),
314+
maybe_envp.map(|envp| envp.as_ptr()).unwrap_or_else(|| *sys::os::environ())
315+
);
316+
return io::Error::last_os_error()
317+
}
318+
}
236319
}
237320

238321
#[cfg(not(any(target_os = "macos", target_os = "freebsd",

Diff for: src/test/run-pass/command-exec.rs

+12
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ fn main() {
4848
println!("passed");
4949
}
5050

51+
"exec-test5" => {
52+
env::set_var("VARIABLE", "ABC");
53+
Command::new("definitely-not-a-real-binary").env("VARIABLE", "XYZ").exec();
54+
assert_eq!(env::var("VARIABLE").unwrap(), "ABC");
55+
println!("passed");
56+
}
57+
5158
_ => panic!("unknown argument: {}", arg),
5259
}
5360
return
@@ -72,4 +79,9 @@ fn main() {
7279
assert!(output.status.success());
7380
assert!(output.stderr.is_empty());
7481
assert_eq!(output.stdout, b"passed\n");
82+
83+
let output = Command::new(&me).arg("exec-test5").output().unwrap();
84+
assert!(output.status.success());
85+
assert!(output.stderr.is_empty());
86+
assert_eq!(output.stdout, b"passed\n");
7587
}

0 commit comments

Comments
 (0)