Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

std::os::unix::process::CommandExt::exec should be unsafe #46775

Closed
Diggsey opened this issue Dec 16, 2017 · 14 comments · Fixed by #55939
Closed

std::os::unix::process::CommandExt::exec should be unsafe #46775

Diggsey opened this issue Dec 16, 2017 · 14 comments · Fixed by #55939
Labels
C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@Diggsey
Copy link
Contributor

Diggsey commented Dec 16, 2017

This method assigns to environ outside of any lock, meaning that it's automatically unsafe for multi-threaded programs.

For single-threaded programs, if the exec fails, environ will point to memory that has been freed. The documentation gives a false sense of security:

The process may be in a "broken state" if this function returns in error. For example the working directory, environment variables, signal handling settings, various user/group information, or aspects of stdio file descriptors may have changed.

@bstrie
Copy link
Contributor

bstrie commented Dec 18, 2017

Smells like #39575 , marking as unsound.

@bstrie bstrie added I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Dec 18, 2017
@XAMPPRocky XAMPPRocky added the C-bug Category: This is a bug. label Mar 26, 2018
@joshlf
Copy link
Contributor

joshlf commented Oct 24, 2018

@Diggsey If you could write some PoC code which demonstrates this, it'd be very helpful. This came up on chat today.

@Diggsey
Copy link
Contributor Author

Diggsey commented Oct 24, 2018

@joshlf not right now, but I can point you to the code:

*sys::os::environ() = envp.as_ptr();

If you look at the comments on that method, you can see that it's clearly been written under the assumption that it will be running in a child process (immediately after a fork). If this were the case, the code would be correct, because it would be guaranteed that there are no other threads running in the child, due to the nature of fork. However, this do_exec method is also directly called by exec and is therefore completely unsafe in a multi-threaded program.

@alex
Copy link
Member

alex commented Oct 24, 2018

I think there's an even more simple memory unsafety here, unless I'm missing something:

  • *sys::os::environ() = envp.as_ptr() escapes that ptr to global storage
  • However in exec(), we can see that envp is on the stack, so it'll be dropped if you return, which can only happen if exec fails.

In other words, any attempt to use the environment after a failed exec is memory unsafe, because the global environment is now pointing at some random heap memory?

Have I grievously missed something?

@alex
Copy link
Member

alex commented Oct 24, 2018

POC:

alexgaynor@penguin /tmp> cat t.rs 
use std::process::Command;
use std::os::unix::process::CommandExt;

fn main() {
    eprintln!("About to exec");
    Command::new("doesnt-exist").env("VARIABLE", "X").exec();
    eprintln!("Didn't exist!");
    eprintln!("VARIABLE: {:?}", std::env::var("VARAIABLE"));
}
alexgaynor@penguin /tmp> rustc +nightly -Z sanitizer=address t.rs 
alexgaynor@penguin /tmp> env ASAN_OPTIONS=detect_odr_violation=0 ./t 
About to exec
Didn't exist!
fish: “env ASAN_OPTIONS=detect_odr_vio…” terminated by signal SIGSEGV (Address boundary error)

@alex
Copy link
Member

alex commented Oct 24, 2018

It seems like a reasonable fix would be to use execvpe and let it be responsible for mutating the world.

@RalfJung
Copy link
Member

@alex This does not reproduce on playground, likely because asan doesn't run there.

Could you describe what is going on, for someone not knowing how env vars are implemented on Linux/Unix platforms?

@alex
Copy link
Member

alex commented Oct 25, 2018

On Linux, env vars are really just a pointer to an array of pointers to nul-terminated strings, with a static lifetime.

So in this case we set that pointer to a Vec's, which is on the stack, data buffer. And then that pointer lives beyond the lifetime of the value on the stack. Now all reads/writes to env vars are pointing to freed memory. A classic UAF.

FWIW a reproducer that will probably work regardless of memory allocator is to check if an env var exists before the exec call, and then try accessing it again afterwards.

You'll notice that I didn't get an ASAN report, that's because the standard lib code doing the read wasn't compiled with ASAN, but I do get the SEGV because it uses an allocator where as soon as some memory is freed it becomes unmapped.

@RalfJung
Copy link
Member

RalfJung commented Oct 25, 2018

Thanks!

So in this case we set that pointer to a Vec's, which is on the stack, data buffer. And then that pointer lives beyond the lifetime of the value on the stack. Now all reads/writes to env vars are pointing to freed memory. A classic UAF.

Changing the env var ptr is happening here, right?

if let Some(envp) = maybe_envp {
*sys::os::environ() = envp.as_ptr();
}

Also I was confused a bit here because I thought you said the data buffer is on the stack. But what you mean is that the Vec is on the stack, and usually the stack just disappears because of exec so its buffer in the heap gets leaked, but when exec fails we do pop and deallocate.

Seems like an "easy fix" would be to mem::forget the envp. That wouldn't change anything about the concurrency issue though that the issue was originally about.

@alex
Copy link
Member

alex commented Oct 25, 2018

Using execvpe seems much easier to me, that way we don't have to setup the environment ourselves.

@RalfJung
Copy link
Member

Reading the man page I agree, it seems designed for this situation (changing the executed program's environment).

Can we assume that it would also solve the data race pointed out at the beginning of this issue?

@alex
Copy link
Member

alex commented Oct 25, 2018 via email

@alex
Copy link
Member

alex commented Oct 25, 2018

I'll try to whip up a patch for this.

@RalfJung
Copy link
Member

Awesome :)

alex added a commit to alex/rust that referenced this issue Nov 1, 2018
…mmand::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.
bors added a commit that referenced this issue Nov 2, 2018
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
alexcrichton pushed a commit to alexcrichton/rust that referenced this issue Nov 6, 2018
…mmand::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.
bors added a commit that referenced this issue Nov 7, 2018
[beta] Fixes #46775 -- don't mutate the process's environment in Command::exec

This is a backport of the following PRs:

* #55359
* #55569
* #55304
bors added a commit that referenced this issue Nov 8, 2018
[beta] Fixes #46775 -- don't mutate the process's environment in Command::exec

This is a backport of the following PRs:

* #55359
* #55569
* #55304
* #55661
alexcrichton added a commit to alexcrichton/rust that referenced this issue Nov 13, 2018
alexcrichton added a commit to alexcrichton/rust that referenced this issue Nov 13, 2018
This commit, after reverting rust-lang#55359, applies a different fix for rust-lang#46775
while also fixing rust-lang#55775. The basic idea was to go back to pre-rust-lang#55359
libstd, and then fix rust-lang#46775 in a way that doesn't expose rust-lang#55775.

The issue described in rust-lang#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 rust-lang#55775

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
bors added a commit that referenced this issue Nov 14, 2018
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
alexcrichton added a commit to alexcrichton/rust that referenced this issue Nov 14, 2018
alexcrichton added a commit to alexcrichton/rust that referenced this issue Nov 14, 2018
This commit, after reverting rust-lang#55359, applies a different fix for rust-lang#46775
while also fixing rust-lang#55775. The basic idea was to go back to pre-rust-lang#55359
libstd, and then fix rust-lang#46775 in a way that doesn't expose rust-lang#55775.

The issue described in rust-lang#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 rust-lang#55775

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
bors added a commit that referenced this issue Nov 14, 2018
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
alexcrichton added a commit to alexcrichton/rust that referenced this issue Nov 15, 2018
alexcrichton added a commit to alexcrichton/rust that referenced this issue Nov 15, 2018
This commit, after reverting rust-lang#55359, applies a different fix for rust-lang#46775
while also fixing rust-lang#55775. The basic idea was to go back to pre-rust-lang#55359
libstd, and then fix rust-lang#46775 in a way that doesn't expose rust-lang#55775.

The issue described in rust-lang#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 rust-lang#55775

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
ischeinkman pushed a commit to ischeinkman/libnx-rs-std that referenced this issue Dec 20, 2018
…mmand::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.
ischeinkman pushed a commit to ischeinkman/libnx-rs-std that referenced this issue Dec 20, 2018
ischeinkman pushed a commit to ischeinkman/libnx-rs-std that referenced this issue Dec 20, 2018
This commit, after reverting rust-lang#55359, applies a different fix for rust-lang#46775
while also fixing rust-lang#55775. The basic idea was to go back to pre-rust-lang#55359
libstd, and then fix rust-lang#46775 in a way that doesn't expose rust-lang#55775.

The issue described in rust-lang#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 rust-lang#55775

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants