Skip to content

Commit b967d55

Browse files
alexcrichtonalex
andcommitted
std: Synchronize access to global env during exec
This commit, after reverting #55359, applies a different fix for #46775 while also fixing #55775. The basic idea was to go back to pre-#55359 libstd, and then fix #46775 in a way that doesn't expose #55775. The issue described in #46775 boils down to two problems: * First, the global environment is reset during `exec` but, but if the `exec` call fails then the global environment was a dangling pointer into free'd memory as the block of memory was deallocated when `Command` is dropped. This is fixed in this commit by installing a `Drop` stack object which ensures that the `environ` pointer is preserved on a failing `exec`. * Second, the global environment was accessed in an unsynchronized fashion during `exec`. This was fixed by ensuring that the Rust-specific environment lock is acquired for these system-level operations. Thanks to Alex Gaynor for pioneering the solution here! Closes #55775 Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
1 parent 5a6bfd5 commit b967d55

File tree

3 files changed

+72
-11
lines changed

3 files changed

+72
-11
lines changed

src/libstd/sys/unix/os.rs

+12-8
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,12 @@ use path::{self, PathBuf};
2727
use ptr;
2828
use slice;
2929
use str;
30-
use sys_common::mutex::Mutex;
30+
use sys_common::mutex::{Mutex, MutexGuard};
3131
use sys::cvt;
3232
use sys::fd;
3333
use vec;
3434

3535
const TMPBUF_SZ: usize = 128;
36-
// We never call `ENV_LOCK.init()`, so it is UB to attempt to
37-
// acquire this mutex reentrantly!
38-
static ENV_LOCK: Mutex = Mutex::new();
3936

4037

4138
extern {
@@ -408,11 +405,18 @@ pub unsafe fn environ() -> *mut *const *const c_char {
408405
&mut environ
409406
}
410407

408+
pub unsafe fn env_lock() -> MutexGuard<'static> {
409+
// We never call `ENV_LOCK.init()`, so it is UB to attempt to
410+
// acquire this mutex reentrantly!
411+
static ENV_LOCK: Mutex = Mutex::new();
412+
ENV_LOCK.lock()
413+
}
414+
411415
/// Returns a vector of (variable, value) byte-vector pairs for all the
412416
/// environment variables of the current process.
413417
pub fn env() -> Env {
414418
unsafe {
415-
let _guard = ENV_LOCK.lock();
419+
let _guard = env_lock();
416420
let mut environ = *environ();
417421
let mut result = Vec::new();
418422
while environ != ptr::null() && *environ != ptr::null() {
@@ -448,7 +452,7 @@ pub fn getenv(k: &OsStr) -> io::Result<Option<OsString>> {
448452
// always None as well
449453
let k = CString::new(k.as_bytes())?;
450454
unsafe {
451-
let _guard = ENV_LOCK.lock();
455+
let _guard = env_lock();
452456
let s = libc::getenv(k.as_ptr()) as *const libc::c_char;
453457
let ret = if s.is_null() {
454458
None
@@ -464,7 +468,7 @@ pub fn setenv(k: &OsStr, v: &OsStr) -> io::Result<()> {
464468
let v = CString::new(v.as_bytes())?;
465469

466470
unsafe {
467-
let _guard = ENV_LOCK.lock();
471+
let _guard = env_lock();
468472
cvt(libc::setenv(k.as_ptr(), v.as_ptr(), 1)).map(|_| ())
469473
}
470474
}
@@ -473,7 +477,7 @@ pub fn unsetenv(n: &OsStr) -> io::Result<()> {
473477
let nbuf = CString::new(n.as_bytes())?;
474478

475479
unsafe {
476-
let _guard = ENV_LOCK.lock();
480+
let _guard = env_lock();
477481
cvt(libc::unsetenv(nbuf.as_ptr())).map(|_| ())
478482
}
479483
}

src/libstd/sys/unix/process/process_unix.rs

+28-3
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,6 @@ impl Command {
193193
if let Some(ref cwd) = *self.get_cwd() {
194194
t!(cvt(libc::chdir(cwd.as_ptr())));
195195
}
196-
if let Some(envp) = maybe_envp {
197-
*sys::os::environ() = envp.as_ptr();
198-
}
199196

200197
// emscripten has no signal support.
201198
#[cfg(not(any(target_os = "emscripten")))]
@@ -231,6 +228,30 @@ impl Command {
231228
t!(callback());
232229
}
233230

231+
// Note that we're accessing process-global state, `environ`, which
232+
// means we need the rust-specific environment lock. Although we're
233+
// performing an exec here we may also return with an error from this
234+
// function (without actually exec'ing) in which case we want to be sure
235+
// to restore the global environment back to what it once was, ensuring
236+
// that our temporary override, when free'd, doesn't corrupt our
237+
// process's environment.
238+
let _lock = sys::os::env_lock();
239+
let mut _reset = None;
240+
if let Some(envp) = maybe_envp {
241+
struct Reset(*const *const libc::c_char);
242+
243+
impl Drop for Reset {
244+
fn drop(&mut self) {
245+
unsafe {
246+
*sys::os::environ() = self.0;
247+
}
248+
}
249+
}
250+
251+
_reset = Some(Reset(*sys::os::environ()));
252+
*sys::os::environ() = envp.as_ptr();
253+
}
254+
234255
libc::execvp(self.get_argv()[0], self.get_argv().as_ptr());
235256
io::Error::last_os_error()
236257
}
@@ -330,6 +351,10 @@ impl Command {
330351
libc::POSIX_SPAWN_SETSIGMASK;
331352
cvt(libc::posix_spawnattr_setflags(&mut attrs.0, flags as _))?;
332353

354+
// We'e reading `sys::os::environ` below so make sure that we do so
355+
// in a synchronized fashion via the rust-specific global
356+
// environment lock.
357+
let _lock = sys::os::env_lock();
333358
let envp = envp.map(|c| c.as_ptr())
334359
.unwrap_or_else(|| *sys::os::environ() as *const _);
335360
let ret = libc::posix_spawnp(

src/test/run-pass/command-exec.rs

+32
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ 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+
58+
"exec-test6" => {
59+
let err = Command::new("echo").arg("passed").env_clear().exec();
60+
panic!("failed to spawn: {}", err);
61+
}
62+
63+
"exec-test7" => {
64+
let err = Command::new("echo").arg("passed").env_remove("PATH").exec();
65+
panic!("failed to spawn: {}", err);
66+
}
67+
5168
_ => panic!("unknown argument: {}", arg),
5269
}
5370
return
@@ -72,4 +89,19 @@ fn main() {
7289
assert!(output.status.success());
7390
assert!(output.stderr.is_empty());
7491
assert_eq!(output.stdout, b"passed\n");
92+
93+
let output = Command::new(&me).arg("exec-test5").output().unwrap();
94+
assert!(output.status.success());
95+
assert!(output.stderr.is_empty());
96+
assert_eq!(output.stdout, b"passed\n");
97+
98+
let output = Command::new(&me).arg("exec-test6").output().unwrap();
99+
assert!(output.status.success());
100+
assert!(output.stderr.is_empty());
101+
assert_eq!(output.stdout, b"passed\n");
102+
103+
let output = Command::new(&me).arg("exec-test7").output().unwrap();
104+
assert!(output.status.success());
105+
assert!(output.stderr.is_empty());
106+
assert_eq!(output.stdout, b"passed\n");
75107
}

0 commit comments

Comments
 (0)